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]
2479fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2480 init_test(cx, |_| {});
2481
2482 let editor = cx.add_window(|window, cx| {
2483 let buffer = MultiBuffer::build_simple("one two three four", cx);
2484 build_editor(buffer, window, cx)
2485 });
2486
2487 _ = editor.update(cx, |editor, window, cx| {
2488 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2489 s.select_display_ranges([
2490 // an empty selection - the preceding word fragment is deleted
2491 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2492 // characters selected - they are deleted
2493 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 12),
2494 ])
2495 });
2496 editor.delete_to_previous_word_start(
2497 &DeleteToPreviousWordStart {
2498 ignore_newlines: false,
2499 },
2500 window,
2501 cx,
2502 );
2503 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e two te four");
2504 });
2505
2506 _ = editor.update(cx, |editor, window, cx| {
2507 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2508 s.select_display_ranges([
2509 // an empty selection - the following word fragment is deleted
2510 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
2511 // characters selected - they are deleted
2512 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 10),
2513 ])
2514 });
2515 editor.delete_to_next_word_end(
2516 &DeleteToNextWordEnd {
2517 ignore_newlines: false,
2518 },
2519 window,
2520 cx,
2521 );
2522 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e t te our");
2523 });
2524}
2525
2526#[gpui::test]
2527fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2528 init_test(cx, |_| {});
2529
2530 let editor = cx.add_window(|window, cx| {
2531 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2532 build_editor(buffer, window, cx)
2533 });
2534 let del_to_prev_word_start = DeleteToPreviousWordStart {
2535 ignore_newlines: false,
2536 };
2537 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2538 ignore_newlines: true,
2539 };
2540
2541 _ = editor.update(cx, |editor, window, cx| {
2542 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2543 s.select_display_ranges([
2544 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2545 ])
2546 });
2547 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2548 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
2549 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2550 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
2551 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2552 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
2553 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2554 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
2555 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2556 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
2557 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2558 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2559 });
2560}
2561
2562#[gpui::test]
2563fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
2564 init_test(cx, |_| {});
2565
2566 let editor = cx.add_window(|window, cx| {
2567 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
2568 build_editor(buffer, window, cx)
2569 });
2570 let del_to_next_word_end = DeleteToNextWordEnd {
2571 ignore_newlines: false,
2572 };
2573 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
2574 ignore_newlines: true,
2575 };
2576
2577 _ = editor.update(cx, |editor, window, cx| {
2578 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2579 s.select_display_ranges([
2580 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2581 ])
2582 });
2583 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2584 assert_eq!(
2585 editor.buffer.read(cx).read(cx).text(),
2586 "one\n two\nthree\n four"
2587 );
2588 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2589 assert_eq!(
2590 editor.buffer.read(cx).read(cx).text(),
2591 "\n two\nthree\n four"
2592 );
2593 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2594 assert_eq!(
2595 editor.buffer.read(cx).read(cx).text(),
2596 "two\nthree\n four"
2597 );
2598 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2599 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
2600 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2601 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
2602 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2603 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2604 });
2605}
2606
2607#[gpui::test]
2608fn test_newline(cx: &mut TestAppContext) {
2609 init_test(cx, |_| {});
2610
2611 let editor = cx.add_window(|window, cx| {
2612 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
2613 build_editor(buffer, window, cx)
2614 });
2615
2616 _ = editor.update(cx, |editor, window, cx| {
2617 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2618 s.select_display_ranges([
2619 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2620 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2621 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
2622 ])
2623 });
2624
2625 editor.newline(&Newline, window, cx);
2626 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
2627 });
2628}
2629
2630#[gpui::test]
2631fn test_newline_with_old_selections(cx: &mut TestAppContext) {
2632 init_test(cx, |_| {});
2633
2634 let editor = cx.add_window(|window, cx| {
2635 let buffer = MultiBuffer::build_simple(
2636 "
2637 a
2638 b(
2639 X
2640 )
2641 c(
2642 X
2643 )
2644 "
2645 .unindent()
2646 .as_str(),
2647 cx,
2648 );
2649 let mut editor = build_editor(buffer, window, cx);
2650 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2651 s.select_ranges([
2652 Point::new(2, 4)..Point::new(2, 5),
2653 Point::new(5, 4)..Point::new(5, 5),
2654 ])
2655 });
2656 editor
2657 });
2658
2659 _ = editor.update(cx, |editor, window, cx| {
2660 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2661 editor.buffer.update(cx, |buffer, cx| {
2662 buffer.edit(
2663 [
2664 (Point::new(1, 2)..Point::new(3, 0), ""),
2665 (Point::new(4, 2)..Point::new(6, 0), ""),
2666 ],
2667 None,
2668 cx,
2669 );
2670 assert_eq!(
2671 buffer.read(cx).text(),
2672 "
2673 a
2674 b()
2675 c()
2676 "
2677 .unindent()
2678 );
2679 });
2680 assert_eq!(
2681 editor.selections.ranges(cx),
2682 &[
2683 Point::new(1, 2)..Point::new(1, 2),
2684 Point::new(2, 2)..Point::new(2, 2),
2685 ],
2686 );
2687
2688 editor.newline(&Newline, window, cx);
2689 assert_eq!(
2690 editor.text(cx),
2691 "
2692 a
2693 b(
2694 )
2695 c(
2696 )
2697 "
2698 .unindent()
2699 );
2700
2701 // The selections are moved after the inserted newlines
2702 assert_eq!(
2703 editor.selections.ranges(cx),
2704 &[
2705 Point::new(2, 0)..Point::new(2, 0),
2706 Point::new(4, 0)..Point::new(4, 0),
2707 ],
2708 );
2709 });
2710}
2711
2712#[gpui::test]
2713async fn test_newline_above(cx: &mut TestAppContext) {
2714 init_test(cx, |settings| {
2715 settings.defaults.tab_size = NonZeroU32::new(4)
2716 });
2717
2718 let language = Arc::new(
2719 Language::new(
2720 LanguageConfig::default(),
2721 Some(tree_sitter_rust::LANGUAGE.into()),
2722 )
2723 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2724 .unwrap(),
2725 );
2726
2727 let mut cx = EditorTestContext::new(cx).await;
2728 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2729 cx.set_state(indoc! {"
2730 const a: ˇA = (
2731 (ˇ
2732 «const_functionˇ»(ˇ),
2733 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2734 )ˇ
2735 ˇ);ˇ
2736 "});
2737
2738 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
2739 cx.assert_editor_state(indoc! {"
2740 ˇ
2741 const a: A = (
2742 ˇ
2743 (
2744 ˇ
2745 ˇ
2746 const_function(),
2747 ˇ
2748 ˇ
2749 ˇ
2750 ˇ
2751 something_else,
2752 ˇ
2753 )
2754 ˇ
2755 ˇ
2756 );
2757 "});
2758}
2759
2760#[gpui::test]
2761async fn test_newline_below(cx: &mut TestAppContext) {
2762 init_test(cx, |settings| {
2763 settings.defaults.tab_size = NonZeroU32::new(4)
2764 });
2765
2766 let language = Arc::new(
2767 Language::new(
2768 LanguageConfig::default(),
2769 Some(tree_sitter_rust::LANGUAGE.into()),
2770 )
2771 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2772 .unwrap(),
2773 );
2774
2775 let mut cx = EditorTestContext::new(cx).await;
2776 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2777 cx.set_state(indoc! {"
2778 const a: ˇA = (
2779 (ˇ
2780 «const_functionˇ»(ˇ),
2781 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2782 )ˇ
2783 ˇ);ˇ
2784 "});
2785
2786 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
2787 cx.assert_editor_state(indoc! {"
2788 const a: A = (
2789 ˇ
2790 (
2791 ˇ
2792 const_function(),
2793 ˇ
2794 ˇ
2795 something_else,
2796 ˇ
2797 ˇ
2798 ˇ
2799 ˇ
2800 )
2801 ˇ
2802 );
2803 ˇ
2804 ˇ
2805 "});
2806}
2807
2808#[gpui::test]
2809async fn test_newline_comments(cx: &mut TestAppContext) {
2810 init_test(cx, |settings| {
2811 settings.defaults.tab_size = NonZeroU32::new(4)
2812 });
2813
2814 let language = Arc::new(Language::new(
2815 LanguageConfig {
2816 line_comments: vec!["// ".into()],
2817 ..LanguageConfig::default()
2818 },
2819 None,
2820 ));
2821 {
2822 let mut cx = EditorTestContext::new(cx).await;
2823 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2824 cx.set_state(indoc! {"
2825 // Fooˇ
2826 "});
2827
2828 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2829 cx.assert_editor_state(indoc! {"
2830 // Foo
2831 // ˇ
2832 "});
2833 // Ensure that we add comment prefix when existing line contains space
2834 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2835 cx.assert_editor_state(
2836 indoc! {"
2837 // Foo
2838 //s
2839 // ˇ
2840 "}
2841 .replace("s", " ") // s is used as space placeholder to prevent format on save
2842 .as_str(),
2843 );
2844 // Ensure that we add comment prefix when existing line does not contain space
2845 cx.set_state(indoc! {"
2846 // Foo
2847 //ˇ
2848 "});
2849 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2850 cx.assert_editor_state(indoc! {"
2851 // Foo
2852 //
2853 // ˇ
2854 "});
2855 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
2856 cx.set_state(indoc! {"
2857 ˇ// Foo
2858 "});
2859 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2860 cx.assert_editor_state(indoc! {"
2861
2862 ˇ// Foo
2863 "});
2864 }
2865 // Ensure that comment continuations can be disabled.
2866 update_test_language_settings(cx, |settings| {
2867 settings.defaults.extend_comment_on_newline = Some(false);
2868 });
2869 let mut cx = EditorTestContext::new(cx).await;
2870 cx.set_state(indoc! {"
2871 // Fooˇ
2872 "});
2873 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2874 cx.assert_editor_state(indoc! {"
2875 // Foo
2876 ˇ
2877 "});
2878}
2879
2880#[gpui::test]
2881async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
2882 init_test(cx, |settings| {
2883 settings.defaults.tab_size = NonZeroU32::new(4)
2884 });
2885
2886 let language = Arc::new(Language::new(
2887 LanguageConfig {
2888 line_comments: vec!["// ".into(), "/// ".into()],
2889 ..LanguageConfig::default()
2890 },
2891 None,
2892 ));
2893 {
2894 let mut cx = EditorTestContext::new(cx).await;
2895 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2896 cx.set_state(indoc! {"
2897 //ˇ
2898 "});
2899 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2900 cx.assert_editor_state(indoc! {"
2901 //
2902 // ˇ
2903 "});
2904
2905 cx.set_state(indoc! {"
2906 ///ˇ
2907 "});
2908 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2909 cx.assert_editor_state(indoc! {"
2910 ///
2911 /// ˇ
2912 "});
2913 }
2914}
2915
2916#[gpui::test]
2917async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
2918 init_test(cx, |settings| {
2919 settings.defaults.tab_size = NonZeroU32::new(4)
2920 });
2921
2922 let language = Arc::new(
2923 Language::new(
2924 LanguageConfig {
2925 documentation_comment: Some(language::BlockCommentConfig {
2926 start: "/**".into(),
2927 end: "*/".into(),
2928 prefix: "* ".into(),
2929 tab_size: 1,
2930 }),
2931
2932 ..LanguageConfig::default()
2933 },
2934 Some(tree_sitter_rust::LANGUAGE.into()),
2935 )
2936 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
2937 .unwrap(),
2938 );
2939
2940 {
2941 let mut cx = EditorTestContext::new(cx).await;
2942 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2943 cx.set_state(indoc! {"
2944 /**ˇ
2945 "});
2946
2947 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2948 cx.assert_editor_state(indoc! {"
2949 /**
2950 * ˇ
2951 "});
2952 // Ensure that if cursor is before the comment start,
2953 // we do not actually insert a comment prefix.
2954 cx.set_state(indoc! {"
2955 ˇ/**
2956 "});
2957 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2958 cx.assert_editor_state(indoc! {"
2959
2960 ˇ/**
2961 "});
2962 // Ensure that if cursor is between it doesn't add comment prefix.
2963 cx.set_state(indoc! {"
2964 /*ˇ*
2965 "});
2966 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2967 cx.assert_editor_state(indoc! {"
2968 /*
2969 ˇ*
2970 "});
2971 // Ensure that if suffix exists on same line after cursor it adds new line.
2972 cx.set_state(indoc! {"
2973 /**ˇ*/
2974 "});
2975 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2976 cx.assert_editor_state(indoc! {"
2977 /**
2978 * ˇ
2979 */
2980 "});
2981 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2982 cx.set_state(indoc! {"
2983 /**ˇ */
2984 "});
2985 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2986 cx.assert_editor_state(indoc! {"
2987 /**
2988 * ˇ
2989 */
2990 "});
2991 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2992 cx.set_state(indoc! {"
2993 /** ˇ*/
2994 "});
2995 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2996 cx.assert_editor_state(
2997 indoc! {"
2998 /**s
2999 * ˇ
3000 */
3001 "}
3002 .replace("s", " ") // s is used as space placeholder to prevent format on save
3003 .as_str(),
3004 );
3005 // Ensure that delimiter space is preserved when newline on already
3006 // spaced delimiter.
3007 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3008 cx.assert_editor_state(
3009 indoc! {"
3010 /**s
3011 *s
3012 * ˇ
3013 */
3014 "}
3015 .replace("s", " ") // s is used as space placeholder to prevent format on save
3016 .as_str(),
3017 );
3018 // Ensure that delimiter space is preserved when space is not
3019 // on existing delimiter.
3020 cx.set_state(indoc! {"
3021 /**
3022 *ˇ
3023 */
3024 "});
3025 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3026 cx.assert_editor_state(indoc! {"
3027 /**
3028 *
3029 * ˇ
3030 */
3031 "});
3032 // Ensure that if suffix exists on same line after cursor it
3033 // doesn't add extra new line if prefix is not on same line.
3034 cx.set_state(indoc! {"
3035 /**
3036 ˇ*/
3037 "});
3038 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3039 cx.assert_editor_state(indoc! {"
3040 /**
3041
3042 ˇ*/
3043 "});
3044 // Ensure that it detects suffix after existing prefix.
3045 cx.set_state(indoc! {"
3046 /**ˇ/
3047 "});
3048 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3049 cx.assert_editor_state(indoc! {"
3050 /**
3051 ˇ/
3052 "});
3053 // Ensure that if suffix exists on same line before
3054 // cursor it does not add comment prefix.
3055 cx.set_state(indoc! {"
3056 /** */ˇ
3057 "});
3058 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3059 cx.assert_editor_state(indoc! {"
3060 /** */
3061 ˇ
3062 "});
3063 // Ensure that if suffix exists on same line before
3064 // cursor it does not add comment prefix.
3065 cx.set_state(indoc! {"
3066 /**
3067 *
3068 */ˇ
3069 "});
3070 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3071 cx.assert_editor_state(indoc! {"
3072 /**
3073 *
3074 */
3075 ˇ
3076 "});
3077
3078 // Ensure that inline comment followed by code
3079 // doesn't add comment prefix on newline
3080 cx.set_state(indoc! {"
3081 /** */ textˇ
3082 "});
3083 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3084 cx.assert_editor_state(indoc! {"
3085 /** */ text
3086 ˇ
3087 "});
3088
3089 // Ensure that text after comment end tag
3090 // doesn't add comment prefix on newline
3091 cx.set_state(indoc! {"
3092 /**
3093 *
3094 */ˇtext
3095 "});
3096 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3097 cx.assert_editor_state(indoc! {"
3098 /**
3099 *
3100 */
3101 ˇtext
3102 "});
3103
3104 // Ensure if not comment block it doesn't
3105 // add comment prefix on newline
3106 cx.set_state(indoc! {"
3107 * textˇ
3108 "});
3109 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3110 cx.assert_editor_state(indoc! {"
3111 * text
3112 ˇ
3113 "});
3114 }
3115 // Ensure that comment continuations can be disabled.
3116 update_test_language_settings(cx, |settings| {
3117 settings.defaults.extend_comment_on_newline = Some(false);
3118 });
3119 let mut cx = EditorTestContext::new(cx).await;
3120 cx.set_state(indoc! {"
3121 /**ˇ
3122 "});
3123 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3124 cx.assert_editor_state(indoc! {"
3125 /**
3126 ˇ
3127 "});
3128}
3129
3130#[gpui::test]
3131async fn test_newline_comments_with_block_comment(cx: &mut TestAppContext) {
3132 init_test(cx, |settings| {
3133 settings.defaults.tab_size = NonZeroU32::new(4)
3134 });
3135
3136 let lua_language = Arc::new(Language::new(
3137 LanguageConfig {
3138 line_comments: vec!["--".into()],
3139 block_comment: Some(language::BlockCommentConfig {
3140 start: "--[[".into(),
3141 prefix: "".into(),
3142 end: "]]".into(),
3143 tab_size: 0,
3144 }),
3145 ..LanguageConfig::default()
3146 },
3147 None,
3148 ));
3149
3150 let mut cx = EditorTestContext::new(cx).await;
3151 cx.update_buffer(|buffer, cx| buffer.set_language(Some(lua_language), cx));
3152
3153 // Line with line comment should extend
3154 cx.set_state(indoc! {"
3155 --ˇ
3156 "});
3157 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3158 cx.assert_editor_state(indoc! {"
3159 --
3160 --ˇ
3161 "});
3162
3163 // Line with block comment that matches line comment should not extend
3164 cx.set_state(indoc! {"
3165 --[[ˇ
3166 "});
3167 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3168 cx.assert_editor_state(indoc! {"
3169 --[[
3170 ˇ
3171 "});
3172}
3173
3174#[gpui::test]
3175fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3176 init_test(cx, |_| {});
3177
3178 let editor = cx.add_window(|window, cx| {
3179 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3180 let mut editor = build_editor(buffer, window, cx);
3181 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3182 s.select_ranges([3..4, 11..12, 19..20])
3183 });
3184 editor
3185 });
3186
3187 _ = editor.update(cx, |editor, window, cx| {
3188 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3189 editor.buffer.update(cx, |buffer, cx| {
3190 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
3191 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3192 });
3193 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
3194
3195 editor.insert("Z", window, cx);
3196 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3197
3198 // The selections are moved after the inserted characters
3199 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
3200 });
3201}
3202
3203#[gpui::test]
3204async fn test_tab(cx: &mut TestAppContext) {
3205 init_test(cx, |settings| {
3206 settings.defaults.tab_size = NonZeroU32::new(3)
3207 });
3208
3209 let mut cx = EditorTestContext::new(cx).await;
3210 cx.set_state(indoc! {"
3211 ˇabˇc
3212 ˇ🏀ˇ🏀ˇefg
3213 dˇ
3214 "});
3215 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3216 cx.assert_editor_state(indoc! {"
3217 ˇab ˇc
3218 ˇ🏀 ˇ🏀 ˇefg
3219 d ˇ
3220 "});
3221
3222 cx.set_state(indoc! {"
3223 a
3224 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3225 "});
3226 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3227 cx.assert_editor_state(indoc! {"
3228 a
3229 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3230 "});
3231}
3232
3233#[gpui::test]
3234async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3235 init_test(cx, |_| {});
3236
3237 let mut cx = EditorTestContext::new(cx).await;
3238 let language = Arc::new(
3239 Language::new(
3240 LanguageConfig::default(),
3241 Some(tree_sitter_rust::LANGUAGE.into()),
3242 )
3243 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3244 .unwrap(),
3245 );
3246 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3247
3248 // test when all cursors are not at suggested indent
3249 // then simply move to their suggested indent location
3250 cx.set_state(indoc! {"
3251 const a: B = (
3252 c(
3253 ˇ
3254 ˇ )
3255 );
3256 "});
3257 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3258 cx.assert_editor_state(indoc! {"
3259 const a: B = (
3260 c(
3261 ˇ
3262 ˇ)
3263 );
3264 "});
3265
3266 // test cursor already at suggested indent not moving when
3267 // other cursors are yet to reach their suggested indents
3268 cx.set_state(indoc! {"
3269 ˇ
3270 const a: B = (
3271 c(
3272 d(
3273 ˇ
3274 )
3275 ˇ
3276 ˇ )
3277 );
3278 "});
3279 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3280 cx.assert_editor_state(indoc! {"
3281 ˇ
3282 const a: B = (
3283 c(
3284 d(
3285 ˇ
3286 )
3287 ˇ
3288 ˇ)
3289 );
3290 "});
3291 // test when all cursors are at suggested indent then tab is inserted
3292 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3293 cx.assert_editor_state(indoc! {"
3294 ˇ
3295 const a: B = (
3296 c(
3297 d(
3298 ˇ
3299 )
3300 ˇ
3301 ˇ)
3302 );
3303 "});
3304
3305 // test when current indent is less than suggested indent,
3306 // we adjust line to match suggested indent and move cursor to it
3307 //
3308 // when no other cursor is at word boundary, all of them should move
3309 cx.set_state(indoc! {"
3310 const a: B = (
3311 c(
3312 d(
3313 ˇ
3314 ˇ )
3315 ˇ )
3316 );
3317 "});
3318 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3319 cx.assert_editor_state(indoc! {"
3320 const a: B = (
3321 c(
3322 d(
3323 ˇ
3324 ˇ)
3325 ˇ)
3326 );
3327 "});
3328
3329 // test when current indent is less than suggested indent,
3330 // we adjust line to match suggested indent and move cursor to it
3331 //
3332 // when some other cursor is at word boundary, it should not move
3333 cx.set_state(indoc! {"
3334 const a: B = (
3335 c(
3336 d(
3337 ˇ
3338 ˇ )
3339 ˇ)
3340 );
3341 "});
3342 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3343 cx.assert_editor_state(indoc! {"
3344 const a: B = (
3345 c(
3346 d(
3347 ˇ
3348 ˇ)
3349 ˇ)
3350 );
3351 "});
3352
3353 // test when current indent is more than suggested indent,
3354 // we just move cursor to current indent instead of suggested indent
3355 //
3356 // when no other cursor is at word boundary, all of them should move
3357 cx.set_state(indoc! {"
3358 const a: B = (
3359 c(
3360 d(
3361 ˇ
3362 ˇ )
3363 ˇ )
3364 );
3365 "});
3366 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3367 cx.assert_editor_state(indoc! {"
3368 const a: B = (
3369 c(
3370 d(
3371 ˇ
3372 ˇ)
3373 ˇ)
3374 );
3375 "});
3376 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3377 cx.assert_editor_state(indoc! {"
3378 const a: B = (
3379 c(
3380 d(
3381 ˇ
3382 ˇ)
3383 ˇ)
3384 );
3385 "});
3386
3387 // test when current indent is more than suggested indent,
3388 // we just move cursor to current indent instead of suggested indent
3389 //
3390 // when some other cursor is at word boundary, it doesn't move
3391 cx.set_state(indoc! {"
3392 const a: B = (
3393 c(
3394 d(
3395 ˇ
3396 ˇ )
3397 ˇ)
3398 );
3399 "});
3400 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3401 cx.assert_editor_state(indoc! {"
3402 const a: B = (
3403 c(
3404 d(
3405 ˇ
3406 ˇ)
3407 ˇ)
3408 );
3409 "});
3410
3411 // handle auto-indent when there are multiple cursors on the same line
3412 cx.set_state(indoc! {"
3413 const a: B = (
3414 c(
3415 ˇ ˇ
3416 ˇ )
3417 );
3418 "});
3419 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3420 cx.assert_editor_state(indoc! {"
3421 const a: B = (
3422 c(
3423 ˇ
3424 ˇ)
3425 );
3426 "});
3427}
3428
3429#[gpui::test]
3430async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3431 init_test(cx, |settings| {
3432 settings.defaults.tab_size = NonZeroU32::new(3)
3433 });
3434
3435 let mut cx = EditorTestContext::new(cx).await;
3436 cx.set_state(indoc! {"
3437 ˇ
3438 \t ˇ
3439 \t ˇ
3440 \t ˇ
3441 \t \t\t \t \t\t \t\t \t \t ˇ
3442 "});
3443
3444 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3445 cx.assert_editor_state(indoc! {"
3446 ˇ
3447 \t ˇ
3448 \t ˇ
3449 \t ˇ
3450 \t \t\t \t \t\t \t\t \t \t ˇ
3451 "});
3452}
3453
3454#[gpui::test]
3455async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3456 init_test(cx, |settings| {
3457 settings.defaults.tab_size = NonZeroU32::new(4)
3458 });
3459
3460 let language = Arc::new(
3461 Language::new(
3462 LanguageConfig::default(),
3463 Some(tree_sitter_rust::LANGUAGE.into()),
3464 )
3465 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3466 .unwrap(),
3467 );
3468
3469 let mut cx = EditorTestContext::new(cx).await;
3470 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3471 cx.set_state(indoc! {"
3472 fn a() {
3473 if b {
3474 \t ˇc
3475 }
3476 }
3477 "});
3478
3479 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3480 cx.assert_editor_state(indoc! {"
3481 fn a() {
3482 if b {
3483 ˇc
3484 }
3485 }
3486 "});
3487}
3488
3489#[gpui::test]
3490async fn test_indent_outdent(cx: &mut TestAppContext) {
3491 init_test(cx, |settings| {
3492 settings.defaults.tab_size = NonZeroU32::new(4);
3493 });
3494
3495 let mut cx = EditorTestContext::new(cx).await;
3496
3497 cx.set_state(indoc! {"
3498 «oneˇ» «twoˇ»
3499 three
3500 four
3501 "});
3502 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3503 cx.assert_editor_state(indoc! {"
3504 «oneˇ» «twoˇ»
3505 three
3506 four
3507 "});
3508
3509 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3510 cx.assert_editor_state(indoc! {"
3511 «oneˇ» «twoˇ»
3512 three
3513 four
3514 "});
3515
3516 // select across line ending
3517 cx.set_state(indoc! {"
3518 one two
3519 t«hree
3520 ˇ» four
3521 "});
3522 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3523 cx.assert_editor_state(indoc! {"
3524 one two
3525 t«hree
3526 ˇ» four
3527 "});
3528
3529 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3530 cx.assert_editor_state(indoc! {"
3531 one two
3532 t«hree
3533 ˇ» four
3534 "});
3535
3536 // Ensure that indenting/outdenting works when the cursor is at column 0.
3537 cx.set_state(indoc! {"
3538 one two
3539 ˇthree
3540 four
3541 "});
3542 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3543 cx.assert_editor_state(indoc! {"
3544 one two
3545 ˇthree
3546 four
3547 "});
3548
3549 cx.set_state(indoc! {"
3550 one two
3551 ˇ three
3552 four
3553 "});
3554 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3555 cx.assert_editor_state(indoc! {"
3556 one two
3557 ˇthree
3558 four
3559 "});
3560}
3561
3562#[gpui::test]
3563async fn test_indent_yaml_comments_with_multiple_cursors(cx: &mut TestAppContext) {
3564 // This is a regression test for issue #33761
3565 init_test(cx, |_| {});
3566
3567 let mut cx = EditorTestContext::new(cx).await;
3568 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3569 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3570
3571 cx.set_state(
3572 r#"ˇ# ingress:
3573ˇ# api:
3574ˇ# enabled: false
3575ˇ# pathType: Prefix
3576ˇ# console:
3577ˇ# enabled: false
3578ˇ# pathType: Prefix
3579"#,
3580 );
3581
3582 // Press tab to indent all lines
3583 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3584
3585 cx.assert_editor_state(
3586 r#" ˇ# ingress:
3587 ˇ# api:
3588 ˇ# enabled: false
3589 ˇ# pathType: Prefix
3590 ˇ# console:
3591 ˇ# enabled: false
3592 ˇ# pathType: Prefix
3593"#,
3594 );
3595}
3596
3597#[gpui::test]
3598async fn test_indent_yaml_non_comments_with_multiple_cursors(cx: &mut TestAppContext) {
3599 // This is a test to make sure our fix for issue #33761 didn't break anything
3600 init_test(cx, |_| {});
3601
3602 let mut cx = EditorTestContext::new(cx).await;
3603 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3604 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3605
3606 cx.set_state(
3607 r#"ˇingress:
3608ˇ api:
3609ˇ enabled: false
3610ˇ pathType: Prefix
3611"#,
3612 );
3613
3614 // Press tab to indent all lines
3615 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3616
3617 cx.assert_editor_state(
3618 r#"ˇingress:
3619 ˇapi:
3620 ˇenabled: false
3621 ˇpathType: Prefix
3622"#,
3623 );
3624}
3625
3626#[gpui::test]
3627async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
3628 init_test(cx, |settings| {
3629 settings.defaults.hard_tabs = Some(true);
3630 });
3631
3632 let mut cx = EditorTestContext::new(cx).await;
3633
3634 // select two ranges on one line
3635 cx.set_state(indoc! {"
3636 «oneˇ» «twoˇ»
3637 three
3638 four
3639 "});
3640 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3641 cx.assert_editor_state(indoc! {"
3642 \t«oneˇ» «twoˇ»
3643 three
3644 four
3645 "});
3646 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3647 cx.assert_editor_state(indoc! {"
3648 \t\t«oneˇ» «twoˇ»
3649 three
3650 four
3651 "});
3652 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3653 cx.assert_editor_state(indoc! {"
3654 \t«oneˇ» «twoˇ»
3655 three
3656 four
3657 "});
3658 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3659 cx.assert_editor_state(indoc! {"
3660 «oneˇ» «twoˇ»
3661 three
3662 four
3663 "});
3664
3665 // select across a line ending
3666 cx.set_state(indoc! {"
3667 one two
3668 t«hree
3669 ˇ»four
3670 "});
3671 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3672 cx.assert_editor_state(indoc! {"
3673 one two
3674 \tt«hree
3675 ˇ»four
3676 "});
3677 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3678 cx.assert_editor_state(indoc! {"
3679 one two
3680 \t\tt«hree
3681 ˇ»four
3682 "});
3683 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3684 cx.assert_editor_state(indoc! {"
3685 one two
3686 \tt«hree
3687 ˇ»four
3688 "});
3689 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3690 cx.assert_editor_state(indoc! {"
3691 one two
3692 t«hree
3693 ˇ»four
3694 "});
3695
3696 // Ensure that indenting/outdenting works when the cursor is at column 0.
3697 cx.set_state(indoc! {"
3698 one two
3699 ˇthree
3700 four
3701 "});
3702 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3703 cx.assert_editor_state(indoc! {"
3704 one two
3705 ˇthree
3706 four
3707 "});
3708 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3709 cx.assert_editor_state(indoc! {"
3710 one two
3711 \tˇthree
3712 four
3713 "});
3714 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3715 cx.assert_editor_state(indoc! {"
3716 one two
3717 ˇthree
3718 four
3719 "});
3720}
3721
3722#[gpui::test]
3723fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
3724 init_test(cx, |settings| {
3725 settings.languages.0.extend([
3726 (
3727 "TOML".into(),
3728 LanguageSettingsContent {
3729 tab_size: NonZeroU32::new(2),
3730 ..Default::default()
3731 },
3732 ),
3733 (
3734 "Rust".into(),
3735 LanguageSettingsContent {
3736 tab_size: NonZeroU32::new(4),
3737 ..Default::default()
3738 },
3739 ),
3740 ]);
3741 });
3742
3743 let toml_language = Arc::new(Language::new(
3744 LanguageConfig {
3745 name: "TOML".into(),
3746 ..Default::default()
3747 },
3748 None,
3749 ));
3750 let rust_language = Arc::new(Language::new(
3751 LanguageConfig {
3752 name: "Rust".into(),
3753 ..Default::default()
3754 },
3755 None,
3756 ));
3757
3758 let toml_buffer =
3759 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
3760 let rust_buffer =
3761 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
3762 let multibuffer = cx.new(|cx| {
3763 let mut multibuffer = MultiBuffer::new(ReadWrite);
3764 multibuffer.push_excerpts(
3765 toml_buffer.clone(),
3766 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
3767 cx,
3768 );
3769 multibuffer.push_excerpts(
3770 rust_buffer.clone(),
3771 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
3772 cx,
3773 );
3774 multibuffer
3775 });
3776
3777 cx.add_window(|window, cx| {
3778 let mut editor = build_editor(multibuffer, window, cx);
3779
3780 assert_eq!(
3781 editor.text(cx),
3782 indoc! {"
3783 a = 1
3784 b = 2
3785
3786 const c: usize = 3;
3787 "}
3788 );
3789
3790 select_ranges(
3791 &mut editor,
3792 indoc! {"
3793 «aˇ» = 1
3794 b = 2
3795
3796 «const c:ˇ» usize = 3;
3797 "},
3798 window,
3799 cx,
3800 );
3801
3802 editor.tab(&Tab, window, cx);
3803 assert_text_with_selections(
3804 &mut editor,
3805 indoc! {"
3806 «aˇ» = 1
3807 b = 2
3808
3809 «const c:ˇ» usize = 3;
3810 "},
3811 cx,
3812 );
3813 editor.backtab(&Backtab, window, cx);
3814 assert_text_with_selections(
3815 &mut editor,
3816 indoc! {"
3817 «aˇ» = 1
3818 b = 2
3819
3820 «const c:ˇ» usize = 3;
3821 "},
3822 cx,
3823 );
3824
3825 editor
3826 });
3827}
3828
3829#[gpui::test]
3830async fn test_backspace(cx: &mut TestAppContext) {
3831 init_test(cx, |_| {});
3832
3833 let mut cx = EditorTestContext::new(cx).await;
3834
3835 // Basic backspace
3836 cx.set_state(indoc! {"
3837 onˇe two three
3838 fou«rˇ» five six
3839 seven «ˇeight nine
3840 »ten
3841 "});
3842 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3843 cx.assert_editor_state(indoc! {"
3844 oˇe two three
3845 fouˇ five six
3846 seven ˇten
3847 "});
3848
3849 // Test backspace inside and around indents
3850 cx.set_state(indoc! {"
3851 zero
3852 ˇone
3853 ˇtwo
3854 ˇ ˇ ˇ three
3855 ˇ ˇ four
3856 "});
3857 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3858 cx.assert_editor_state(indoc! {"
3859 zero
3860 ˇone
3861 ˇtwo
3862 ˇ threeˇ four
3863 "});
3864}
3865
3866#[gpui::test]
3867async fn test_delete(cx: &mut TestAppContext) {
3868 init_test(cx, |_| {});
3869
3870 let mut cx = EditorTestContext::new(cx).await;
3871 cx.set_state(indoc! {"
3872 onˇe two three
3873 fou«rˇ» five six
3874 seven «ˇeight nine
3875 »ten
3876 "});
3877 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
3878 cx.assert_editor_state(indoc! {"
3879 onˇ two three
3880 fouˇ five six
3881 seven ˇten
3882 "});
3883}
3884
3885#[gpui::test]
3886fn test_delete_line(cx: &mut TestAppContext) {
3887 init_test(cx, |_| {});
3888
3889 let editor = cx.add_window(|window, cx| {
3890 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3891 build_editor(buffer, window, cx)
3892 });
3893 _ = editor.update(cx, |editor, window, cx| {
3894 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3895 s.select_display_ranges([
3896 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
3897 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3898 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3899 ])
3900 });
3901 editor.delete_line(&DeleteLine, window, cx);
3902 assert_eq!(editor.display_text(cx), "ghi");
3903 assert_eq!(
3904 editor.selections.display_ranges(cx),
3905 vec![
3906 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
3907 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
3908 ]
3909 );
3910 });
3911
3912 let editor = cx.add_window(|window, cx| {
3913 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3914 build_editor(buffer, window, cx)
3915 });
3916 _ = editor.update(cx, |editor, window, cx| {
3917 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3918 s.select_display_ranges([
3919 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
3920 ])
3921 });
3922 editor.delete_line(&DeleteLine, window, cx);
3923 assert_eq!(editor.display_text(cx), "ghi\n");
3924 assert_eq!(
3925 editor.selections.display_ranges(cx),
3926 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
3927 );
3928 });
3929}
3930
3931#[gpui::test]
3932fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
3933 init_test(cx, |_| {});
3934
3935 cx.add_window(|window, cx| {
3936 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3937 let mut editor = build_editor(buffer.clone(), window, cx);
3938 let buffer = buffer.read(cx).as_singleton().unwrap();
3939
3940 assert_eq!(
3941 editor.selections.ranges::<Point>(cx),
3942 &[Point::new(0, 0)..Point::new(0, 0)]
3943 );
3944
3945 // When on single line, replace newline at end by space
3946 editor.join_lines(&JoinLines, window, cx);
3947 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3948 assert_eq!(
3949 editor.selections.ranges::<Point>(cx),
3950 &[Point::new(0, 3)..Point::new(0, 3)]
3951 );
3952
3953 // When multiple lines are selected, remove newlines that are spanned by the selection
3954 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3955 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
3956 });
3957 editor.join_lines(&JoinLines, window, cx);
3958 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
3959 assert_eq!(
3960 editor.selections.ranges::<Point>(cx),
3961 &[Point::new(0, 11)..Point::new(0, 11)]
3962 );
3963
3964 // Undo should be transactional
3965 editor.undo(&Undo, window, cx);
3966 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3967 assert_eq!(
3968 editor.selections.ranges::<Point>(cx),
3969 &[Point::new(0, 5)..Point::new(2, 2)]
3970 );
3971
3972 // When joining an empty line don't insert a space
3973 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3974 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
3975 });
3976 editor.join_lines(&JoinLines, window, cx);
3977 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
3978 assert_eq!(
3979 editor.selections.ranges::<Point>(cx),
3980 [Point::new(2, 3)..Point::new(2, 3)]
3981 );
3982
3983 // We can remove trailing newlines
3984 editor.join_lines(&JoinLines, window, cx);
3985 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3986 assert_eq!(
3987 editor.selections.ranges::<Point>(cx),
3988 [Point::new(2, 3)..Point::new(2, 3)]
3989 );
3990
3991 // We don't blow up on the last line
3992 editor.join_lines(&JoinLines, window, cx);
3993 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3994 assert_eq!(
3995 editor.selections.ranges::<Point>(cx),
3996 [Point::new(2, 3)..Point::new(2, 3)]
3997 );
3998
3999 // reset to test indentation
4000 editor.buffer.update(cx, |buffer, cx| {
4001 buffer.edit(
4002 [
4003 (Point::new(1, 0)..Point::new(1, 2), " "),
4004 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
4005 ],
4006 None,
4007 cx,
4008 )
4009 });
4010
4011 // We remove any leading spaces
4012 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
4013 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4014 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
4015 });
4016 editor.join_lines(&JoinLines, window, cx);
4017 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
4018
4019 // We don't insert a space for a line containing only spaces
4020 editor.join_lines(&JoinLines, window, cx);
4021 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
4022
4023 // We ignore any leading tabs
4024 editor.join_lines(&JoinLines, window, cx);
4025 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
4026
4027 editor
4028 });
4029}
4030
4031#[gpui::test]
4032fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
4033 init_test(cx, |_| {});
4034
4035 cx.add_window(|window, cx| {
4036 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4037 let mut editor = build_editor(buffer.clone(), window, cx);
4038 let buffer = buffer.read(cx).as_singleton().unwrap();
4039
4040 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4041 s.select_ranges([
4042 Point::new(0, 2)..Point::new(1, 1),
4043 Point::new(1, 2)..Point::new(1, 2),
4044 Point::new(3, 1)..Point::new(3, 2),
4045 ])
4046 });
4047
4048 editor.join_lines(&JoinLines, window, cx);
4049 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
4050
4051 assert_eq!(
4052 editor.selections.ranges::<Point>(cx),
4053 [
4054 Point::new(0, 7)..Point::new(0, 7),
4055 Point::new(1, 3)..Point::new(1, 3)
4056 ]
4057 );
4058 editor
4059 });
4060}
4061
4062#[gpui::test]
4063async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
4064 init_test(cx, |_| {});
4065
4066 let mut cx = EditorTestContext::new(cx).await;
4067
4068 let diff_base = r#"
4069 Line 0
4070 Line 1
4071 Line 2
4072 Line 3
4073 "#
4074 .unindent();
4075
4076 cx.set_state(
4077 &r#"
4078 ˇLine 0
4079 Line 1
4080 Line 2
4081 Line 3
4082 "#
4083 .unindent(),
4084 );
4085
4086 cx.set_head_text(&diff_base);
4087 executor.run_until_parked();
4088
4089 // Join lines
4090 cx.update_editor(|editor, window, cx| {
4091 editor.join_lines(&JoinLines, window, cx);
4092 });
4093 executor.run_until_parked();
4094
4095 cx.assert_editor_state(
4096 &r#"
4097 Line 0ˇ Line 1
4098 Line 2
4099 Line 3
4100 "#
4101 .unindent(),
4102 );
4103 // Join again
4104 cx.update_editor(|editor, window, cx| {
4105 editor.join_lines(&JoinLines, window, cx);
4106 });
4107 executor.run_until_parked();
4108
4109 cx.assert_editor_state(
4110 &r#"
4111 Line 0 Line 1ˇ Line 2
4112 Line 3
4113 "#
4114 .unindent(),
4115 );
4116}
4117
4118#[gpui::test]
4119async fn test_custom_newlines_cause_no_false_positive_diffs(
4120 executor: BackgroundExecutor,
4121 cx: &mut TestAppContext,
4122) {
4123 init_test(cx, |_| {});
4124 let mut cx = EditorTestContext::new(cx).await;
4125 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
4126 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
4127 executor.run_until_parked();
4128
4129 cx.update_editor(|editor, window, cx| {
4130 let snapshot = editor.snapshot(window, cx);
4131 assert_eq!(
4132 snapshot
4133 .buffer_snapshot
4134 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
4135 .collect::<Vec<_>>(),
4136 Vec::new(),
4137 "Should not have any diffs for files with custom newlines"
4138 );
4139 });
4140}
4141
4142#[gpui::test]
4143async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppContext) {
4144 init_test(cx, |_| {});
4145
4146 let mut cx = EditorTestContext::new(cx).await;
4147
4148 // Test sort_lines_case_insensitive()
4149 cx.set_state(indoc! {"
4150 «z
4151 y
4152 x
4153 Z
4154 Y
4155 Xˇ»
4156 "});
4157 cx.update_editor(|e, window, cx| {
4158 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
4159 });
4160 cx.assert_editor_state(indoc! {"
4161 «x
4162 X
4163 y
4164 Y
4165 z
4166 Zˇ»
4167 "});
4168
4169 // Test sort_lines_by_length()
4170 //
4171 // Demonstrates:
4172 // - ∞ is 3 bytes UTF-8, but sorted by its char count (1)
4173 // - sort is stable
4174 cx.set_state(indoc! {"
4175 «123
4176 æ
4177 12
4178 ∞
4179 1
4180 æˇ»
4181 "});
4182 cx.update_editor(|e, window, cx| e.sort_lines_by_length(&SortLinesByLength, window, cx));
4183 cx.assert_editor_state(indoc! {"
4184 «æ
4185 ∞
4186 1
4187 æ
4188 12
4189 123ˇ»
4190 "});
4191
4192 // Test reverse_lines()
4193 cx.set_state(indoc! {"
4194 «5
4195 4
4196 3
4197 2
4198 1ˇ»
4199 "});
4200 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4201 cx.assert_editor_state(indoc! {"
4202 «1
4203 2
4204 3
4205 4
4206 5ˇ»
4207 "});
4208
4209 // Skip testing shuffle_line()
4210
4211 // From here on out, test more complex cases of manipulate_immutable_lines() with a single driver method: sort_lines_case_sensitive()
4212 // Since all methods calling manipulate_immutable_lines() are doing the exact same general thing (reordering lines)
4213
4214 // Don't manipulate when cursor is on single line, but expand the selection
4215 cx.set_state(indoc! {"
4216 ddˇdd
4217 ccc
4218 bb
4219 a
4220 "});
4221 cx.update_editor(|e, window, cx| {
4222 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4223 });
4224 cx.assert_editor_state(indoc! {"
4225 «ddddˇ»
4226 ccc
4227 bb
4228 a
4229 "});
4230
4231 // Basic manipulate case
4232 // Start selection moves to column 0
4233 // End of selection shrinks to fit shorter line
4234 cx.set_state(indoc! {"
4235 dd«d
4236 ccc
4237 bb
4238 aaaaaˇ»
4239 "});
4240 cx.update_editor(|e, window, cx| {
4241 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4242 });
4243 cx.assert_editor_state(indoc! {"
4244 «aaaaa
4245 bb
4246 ccc
4247 dddˇ»
4248 "});
4249
4250 // Manipulate case with newlines
4251 cx.set_state(indoc! {"
4252 dd«d
4253 ccc
4254
4255 bb
4256 aaaaa
4257
4258 ˇ»
4259 "});
4260 cx.update_editor(|e, window, cx| {
4261 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4262 });
4263 cx.assert_editor_state(indoc! {"
4264 «
4265
4266 aaaaa
4267 bb
4268 ccc
4269 dddˇ»
4270
4271 "});
4272
4273 // Adding new line
4274 cx.set_state(indoc! {"
4275 aa«a
4276 bbˇ»b
4277 "});
4278 cx.update_editor(|e, window, cx| {
4279 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added_line"))
4280 });
4281 cx.assert_editor_state(indoc! {"
4282 «aaa
4283 bbb
4284 added_lineˇ»
4285 "});
4286
4287 // Removing line
4288 cx.set_state(indoc! {"
4289 aa«a
4290 bbbˇ»
4291 "});
4292 cx.update_editor(|e, window, cx| {
4293 e.manipulate_immutable_lines(window, cx, |lines| {
4294 lines.pop();
4295 })
4296 });
4297 cx.assert_editor_state(indoc! {"
4298 «aaaˇ»
4299 "});
4300
4301 // Removing all lines
4302 cx.set_state(indoc! {"
4303 aa«a
4304 bbbˇ»
4305 "});
4306 cx.update_editor(|e, window, cx| {
4307 e.manipulate_immutable_lines(window, cx, |lines| {
4308 lines.drain(..);
4309 })
4310 });
4311 cx.assert_editor_state(indoc! {"
4312 ˇ
4313 "});
4314}
4315
4316#[gpui::test]
4317async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4318 init_test(cx, |_| {});
4319
4320 let mut cx = EditorTestContext::new(cx).await;
4321
4322 // Consider continuous selection as single selection
4323 cx.set_state(indoc! {"
4324 Aaa«aa
4325 cˇ»c«c
4326 bb
4327 aaaˇ»aa
4328 "});
4329 cx.update_editor(|e, window, cx| {
4330 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4331 });
4332 cx.assert_editor_state(indoc! {"
4333 «Aaaaa
4334 ccc
4335 bb
4336 aaaaaˇ»
4337 "});
4338
4339 cx.set_state(indoc! {"
4340 Aaa«aa
4341 cˇ»c«c
4342 bb
4343 aaaˇ»aa
4344 "});
4345 cx.update_editor(|e, window, cx| {
4346 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4347 });
4348 cx.assert_editor_state(indoc! {"
4349 «Aaaaa
4350 ccc
4351 bbˇ»
4352 "});
4353
4354 // Consider non continuous selection as distinct dedup operations
4355 cx.set_state(indoc! {"
4356 «aaaaa
4357 bb
4358 aaaaa
4359 aaaaaˇ»
4360
4361 aaa«aaˇ»
4362 "});
4363 cx.update_editor(|e, window, cx| {
4364 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4365 });
4366 cx.assert_editor_state(indoc! {"
4367 «aaaaa
4368 bbˇ»
4369
4370 «aaaaaˇ»
4371 "});
4372}
4373
4374#[gpui::test]
4375async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4376 init_test(cx, |_| {});
4377
4378 let mut cx = EditorTestContext::new(cx).await;
4379
4380 cx.set_state(indoc! {"
4381 «Aaa
4382 aAa
4383 Aaaˇ»
4384 "});
4385 cx.update_editor(|e, window, cx| {
4386 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4387 });
4388 cx.assert_editor_state(indoc! {"
4389 «Aaa
4390 aAaˇ»
4391 "});
4392
4393 cx.set_state(indoc! {"
4394 «Aaa
4395 aAa
4396 aaAˇ»
4397 "});
4398 cx.update_editor(|e, window, cx| {
4399 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4400 });
4401 cx.assert_editor_state(indoc! {"
4402 «Aaaˇ»
4403 "});
4404}
4405
4406#[gpui::test]
4407async fn test_wrap_in_tag_single_selection(cx: &mut TestAppContext) {
4408 init_test(cx, |_| {});
4409
4410 let mut cx = EditorTestContext::new(cx).await;
4411
4412 let js_language = Arc::new(Language::new(
4413 LanguageConfig {
4414 name: "JavaScript".into(),
4415 wrap_characters: Some(language::WrapCharactersConfig {
4416 start_prefix: "<".into(),
4417 start_suffix: ">".into(),
4418 end_prefix: "</".into(),
4419 end_suffix: ">".into(),
4420 }),
4421 ..LanguageConfig::default()
4422 },
4423 None,
4424 ));
4425
4426 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
4427
4428 cx.set_state(indoc! {"
4429 «testˇ»
4430 "});
4431 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4432 cx.assert_editor_state(indoc! {"
4433 <«ˇ»>test</«ˇ»>
4434 "});
4435
4436 cx.set_state(indoc! {"
4437 «test
4438 testˇ»
4439 "});
4440 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4441 cx.assert_editor_state(indoc! {"
4442 <«ˇ»>test
4443 test</«ˇ»>
4444 "});
4445
4446 cx.set_state(indoc! {"
4447 teˇst
4448 "});
4449 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4450 cx.assert_editor_state(indoc! {"
4451 te<«ˇ»></«ˇ»>st
4452 "});
4453}
4454
4455#[gpui::test]
4456async fn test_wrap_in_tag_multi_selection(cx: &mut TestAppContext) {
4457 init_test(cx, |_| {});
4458
4459 let mut cx = EditorTestContext::new(cx).await;
4460
4461 let js_language = Arc::new(Language::new(
4462 LanguageConfig {
4463 name: "JavaScript".into(),
4464 wrap_characters: Some(language::WrapCharactersConfig {
4465 start_prefix: "<".into(),
4466 start_suffix: ">".into(),
4467 end_prefix: "</".into(),
4468 end_suffix: ">".into(),
4469 }),
4470 ..LanguageConfig::default()
4471 },
4472 None,
4473 ));
4474
4475 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
4476
4477 cx.set_state(indoc! {"
4478 «testˇ»
4479 «testˇ» «testˇ»
4480 «testˇ»
4481 "});
4482 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4483 cx.assert_editor_state(indoc! {"
4484 <«ˇ»>test</«ˇ»>
4485 <«ˇ»>test</«ˇ»> <«ˇ»>test</«ˇ»>
4486 <«ˇ»>test</«ˇ»>
4487 "});
4488
4489 cx.set_state(indoc! {"
4490 «test
4491 testˇ»
4492 «test
4493 testˇ»
4494 "});
4495 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4496 cx.assert_editor_state(indoc! {"
4497 <«ˇ»>test
4498 test</«ˇ»>
4499 <«ˇ»>test
4500 test</«ˇ»>
4501 "});
4502}
4503
4504#[gpui::test]
4505async fn test_wrap_in_tag_does_nothing_in_unsupported_languages(cx: &mut TestAppContext) {
4506 init_test(cx, |_| {});
4507
4508 let mut cx = EditorTestContext::new(cx).await;
4509
4510 let plaintext_language = Arc::new(Language::new(
4511 LanguageConfig {
4512 name: "Plain Text".into(),
4513 ..LanguageConfig::default()
4514 },
4515 None,
4516 ));
4517
4518 cx.update_buffer(|buffer, cx| buffer.set_language(Some(plaintext_language), cx));
4519
4520 cx.set_state(indoc! {"
4521 «testˇ»
4522 "});
4523 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4524 cx.assert_editor_state(indoc! {"
4525 «testˇ»
4526 "});
4527}
4528
4529#[gpui::test]
4530async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
4531 init_test(cx, |_| {});
4532
4533 let mut cx = EditorTestContext::new(cx).await;
4534
4535 // Manipulate with multiple selections on a single line
4536 cx.set_state(indoc! {"
4537 dd«dd
4538 cˇ»c«c
4539 bb
4540 aaaˇ»aa
4541 "});
4542 cx.update_editor(|e, window, cx| {
4543 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4544 });
4545 cx.assert_editor_state(indoc! {"
4546 «aaaaa
4547 bb
4548 ccc
4549 ddddˇ»
4550 "});
4551
4552 // Manipulate with multiple disjoin selections
4553 cx.set_state(indoc! {"
4554 5«
4555 4
4556 3
4557 2
4558 1ˇ»
4559
4560 dd«dd
4561 ccc
4562 bb
4563 aaaˇ»aa
4564 "});
4565 cx.update_editor(|e, window, cx| {
4566 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4567 });
4568 cx.assert_editor_state(indoc! {"
4569 «1
4570 2
4571 3
4572 4
4573 5ˇ»
4574
4575 «aaaaa
4576 bb
4577 ccc
4578 ddddˇ»
4579 "});
4580
4581 // Adding lines on each selection
4582 cx.set_state(indoc! {"
4583 2«
4584 1ˇ»
4585
4586 bb«bb
4587 aaaˇ»aa
4588 "});
4589 cx.update_editor(|e, window, cx| {
4590 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added line"))
4591 });
4592 cx.assert_editor_state(indoc! {"
4593 «2
4594 1
4595 added lineˇ»
4596
4597 «bbbb
4598 aaaaa
4599 added lineˇ»
4600 "});
4601
4602 // Removing lines on each selection
4603 cx.set_state(indoc! {"
4604 2«
4605 1ˇ»
4606
4607 bb«bb
4608 aaaˇ»aa
4609 "});
4610 cx.update_editor(|e, window, cx| {
4611 e.manipulate_immutable_lines(window, cx, |lines| {
4612 lines.pop();
4613 })
4614 });
4615 cx.assert_editor_state(indoc! {"
4616 «2ˇ»
4617
4618 «bbbbˇ»
4619 "});
4620}
4621
4622#[gpui::test]
4623async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) {
4624 init_test(cx, |settings| {
4625 settings.defaults.tab_size = NonZeroU32::new(3)
4626 });
4627
4628 let mut cx = EditorTestContext::new(cx).await;
4629
4630 // MULTI SELECTION
4631 // Ln.1 "«" tests empty lines
4632 // Ln.9 tests just leading whitespace
4633 cx.set_state(indoc! {"
4634 «
4635 abc // No indentationˇ»
4636 «\tabc // 1 tabˇ»
4637 \t\tabc « ˇ» // 2 tabs
4638 \t ab«c // Tab followed by space
4639 \tabc // Space followed by tab (3 spaces should be the result)
4640 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4641 abˇ»ˇc ˇ ˇ // Already space indented«
4642 \t
4643 \tabc\tdef // Only the leading tab is manipulatedˇ»
4644 "});
4645 cx.update_editor(|e, window, cx| {
4646 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4647 });
4648 cx.assert_editor_state(
4649 indoc! {"
4650 «
4651 abc // No indentation
4652 abc // 1 tab
4653 abc // 2 tabs
4654 abc // Tab followed by space
4655 abc // Space followed by tab (3 spaces should be the result)
4656 abc // Mixed indentation (tab conversion depends on the column)
4657 abc // Already space indented
4658 ·
4659 abc\tdef // Only the leading tab is manipulatedˇ»
4660 "}
4661 .replace("·", "")
4662 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4663 );
4664
4665 // Test on just a few lines, the others should remain unchanged
4666 // Only lines (3, 5, 10, 11) should change
4667 cx.set_state(
4668 indoc! {"
4669 ·
4670 abc // No indentation
4671 \tabcˇ // 1 tab
4672 \t\tabc // 2 tabs
4673 \t abcˇ // Tab followed by space
4674 \tabc // Space followed by tab (3 spaces should be the result)
4675 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4676 abc // Already space indented
4677 «\t
4678 \tabc\tdef // Only the leading tab is manipulatedˇ»
4679 "}
4680 .replace("·", "")
4681 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4682 );
4683 cx.update_editor(|e, window, cx| {
4684 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4685 });
4686 cx.assert_editor_state(
4687 indoc! {"
4688 ·
4689 abc // No indentation
4690 « abc // 1 tabˇ»
4691 \t\tabc // 2 tabs
4692 « abc // Tab followed by spaceˇ»
4693 \tabc // Space followed by tab (3 spaces should be the result)
4694 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4695 abc // Already space indented
4696 « ·
4697 abc\tdef // Only the leading tab is manipulatedˇ»
4698 "}
4699 .replace("·", "")
4700 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4701 );
4702
4703 // SINGLE SELECTION
4704 // Ln.1 "«" tests empty lines
4705 // Ln.9 tests just leading whitespace
4706 cx.set_state(indoc! {"
4707 «
4708 abc // No indentation
4709 \tabc // 1 tab
4710 \t\tabc // 2 tabs
4711 \t abc // Tab followed by space
4712 \tabc // Space followed by tab (3 spaces should be the result)
4713 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4714 abc // Already space indented
4715 \t
4716 \tabc\tdef // Only the leading tab is manipulatedˇ»
4717 "});
4718 cx.update_editor(|e, window, cx| {
4719 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4720 });
4721 cx.assert_editor_state(
4722 indoc! {"
4723 «
4724 abc // No indentation
4725 abc // 1 tab
4726 abc // 2 tabs
4727 abc // Tab followed by space
4728 abc // Space followed by tab (3 spaces should be the result)
4729 abc // Mixed indentation (tab conversion depends on the column)
4730 abc // Already space indented
4731 ·
4732 abc\tdef // Only the leading tab is manipulatedˇ»
4733 "}
4734 .replace("·", "")
4735 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4736 );
4737}
4738
4739#[gpui::test]
4740async fn test_convert_indentation_to_tabs(cx: &mut TestAppContext) {
4741 init_test(cx, |settings| {
4742 settings.defaults.tab_size = NonZeroU32::new(3)
4743 });
4744
4745 let mut cx = EditorTestContext::new(cx).await;
4746
4747 // MULTI SELECTION
4748 // Ln.1 "«" tests empty lines
4749 // Ln.11 tests just leading whitespace
4750 cx.set_state(indoc! {"
4751 «
4752 abˇ»ˇc // No indentation
4753 abc ˇ ˇ // 1 space (< 3 so dont convert)
4754 abc « // 2 spaces (< 3 so dont convert)
4755 abc // 3 spaces (convert)
4756 abc ˇ» // 5 spaces (1 tab + 2 spaces)
4757 «\tˇ»\t«\tˇ»abc // Already tab indented
4758 «\t abc // Tab followed by space
4759 \tabc // Space followed by tab (should be consumed due to tab)
4760 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4761 \tˇ» «\t
4762 abcˇ» \t ˇˇˇ // Only the leading spaces should be converted
4763 "});
4764 cx.update_editor(|e, window, cx| {
4765 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
4766 });
4767 cx.assert_editor_state(indoc! {"
4768 «
4769 abc // No indentation
4770 abc // 1 space (< 3 so dont convert)
4771 abc // 2 spaces (< 3 so dont convert)
4772 \tabc // 3 spaces (convert)
4773 \t abc // 5 spaces (1 tab + 2 spaces)
4774 \t\t\tabc // Already tab indented
4775 \t abc // Tab followed by space
4776 \tabc // Space followed by tab (should be consumed due to tab)
4777 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4778 \t\t\t
4779 \tabc \t // Only the leading spaces should be convertedˇ»
4780 "});
4781
4782 // Test on just a few lines, the other should remain unchanged
4783 // Only lines (4, 8, 11, 12) should change
4784 cx.set_state(
4785 indoc! {"
4786 ·
4787 abc // No indentation
4788 abc // 1 space (< 3 so dont convert)
4789 abc // 2 spaces (< 3 so dont convert)
4790 « abc // 3 spaces (convert)ˇ»
4791 abc // 5 spaces (1 tab + 2 spaces)
4792 \t\t\tabc // Already tab indented
4793 \t abc // Tab followed by space
4794 \tabc ˇ // Space followed by tab (should be consumed due to tab)
4795 \t\t \tabc // Mixed indentation
4796 \t \t \t \tabc // Mixed indentation
4797 \t \tˇ
4798 « abc \t // Only the leading spaces should be convertedˇ»
4799 "}
4800 .replace("·", "")
4801 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4802 );
4803 cx.update_editor(|e, window, cx| {
4804 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
4805 });
4806 cx.assert_editor_state(
4807 indoc! {"
4808 ·
4809 abc // No indentation
4810 abc // 1 space (< 3 so dont convert)
4811 abc // 2 spaces (< 3 so dont convert)
4812 «\tabc // 3 spaces (convert)ˇ»
4813 abc // 5 spaces (1 tab + 2 spaces)
4814 \t\t\tabc // Already tab indented
4815 \t abc // Tab followed by space
4816 «\tabc // Space followed by tab (should be consumed due to tab)ˇ»
4817 \t\t \tabc // Mixed indentation
4818 \t \t \t \tabc // Mixed indentation
4819 «\t\t\t
4820 \tabc \t // Only the leading spaces should be convertedˇ»
4821 "}
4822 .replace("·", "")
4823 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4824 );
4825
4826 // SINGLE SELECTION
4827 // Ln.1 "«" tests empty lines
4828 // Ln.11 tests just leading whitespace
4829 cx.set_state(indoc! {"
4830 «
4831 abc // No indentation
4832 abc // 1 space (< 3 so dont convert)
4833 abc // 2 spaces (< 3 so dont convert)
4834 abc // 3 spaces (convert)
4835 abc // 5 spaces (1 tab + 2 spaces)
4836 \t\t\tabc // Already tab indented
4837 \t abc // Tab followed by space
4838 \tabc // Space followed by tab (should be consumed due to tab)
4839 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4840 \t \t
4841 abc \t // Only the leading spaces should be convertedˇ»
4842 "});
4843 cx.update_editor(|e, window, cx| {
4844 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
4845 });
4846 cx.assert_editor_state(indoc! {"
4847 «
4848 abc // No indentation
4849 abc // 1 space (< 3 so dont convert)
4850 abc // 2 spaces (< 3 so dont convert)
4851 \tabc // 3 spaces (convert)
4852 \t abc // 5 spaces (1 tab + 2 spaces)
4853 \t\t\tabc // Already tab indented
4854 \t abc // Tab followed by space
4855 \tabc // Space followed by tab (should be consumed due to tab)
4856 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4857 \t\t\t
4858 \tabc \t // Only the leading spaces should be convertedˇ»
4859 "});
4860}
4861
4862#[gpui::test]
4863async fn test_toggle_case(cx: &mut TestAppContext) {
4864 init_test(cx, |_| {});
4865
4866 let mut cx = EditorTestContext::new(cx).await;
4867
4868 // If all lower case -> upper case
4869 cx.set_state(indoc! {"
4870 «hello worldˇ»
4871 "});
4872 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4873 cx.assert_editor_state(indoc! {"
4874 «HELLO WORLDˇ»
4875 "});
4876
4877 // If all upper case -> lower case
4878 cx.set_state(indoc! {"
4879 «HELLO WORLDˇ»
4880 "});
4881 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4882 cx.assert_editor_state(indoc! {"
4883 «hello worldˇ»
4884 "});
4885
4886 // If any upper case characters are identified -> lower case
4887 // This matches JetBrains IDEs
4888 cx.set_state(indoc! {"
4889 «hEllo worldˇ»
4890 "});
4891 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4892 cx.assert_editor_state(indoc! {"
4893 «hello worldˇ»
4894 "});
4895}
4896
4897#[gpui::test]
4898async fn test_convert_to_sentence_case(cx: &mut TestAppContext) {
4899 init_test(cx, |_| {});
4900
4901 let mut cx = EditorTestContext::new(cx).await;
4902
4903 cx.set_state(indoc! {"
4904 «implement-windows-supportˇ»
4905 "});
4906 cx.update_editor(|e, window, cx| {
4907 e.convert_to_sentence_case(&ConvertToSentenceCase, window, cx)
4908 });
4909 cx.assert_editor_state(indoc! {"
4910 «Implement windows supportˇ»
4911 "});
4912}
4913
4914#[gpui::test]
4915async fn test_manipulate_text(cx: &mut TestAppContext) {
4916 init_test(cx, |_| {});
4917
4918 let mut cx = EditorTestContext::new(cx).await;
4919
4920 // Test convert_to_upper_case()
4921 cx.set_state(indoc! {"
4922 «hello worldˇ»
4923 "});
4924 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4925 cx.assert_editor_state(indoc! {"
4926 «HELLO WORLDˇ»
4927 "});
4928
4929 // Test convert_to_lower_case()
4930 cx.set_state(indoc! {"
4931 «HELLO WORLDˇ»
4932 "});
4933 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
4934 cx.assert_editor_state(indoc! {"
4935 «hello worldˇ»
4936 "});
4937
4938 // Test multiple line, single selection case
4939 cx.set_state(indoc! {"
4940 «The quick brown
4941 fox jumps over
4942 the lazy dogˇ»
4943 "});
4944 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
4945 cx.assert_editor_state(indoc! {"
4946 «The Quick Brown
4947 Fox Jumps Over
4948 The Lazy Dogˇ»
4949 "});
4950
4951 // Test multiple line, single selection case
4952 cx.set_state(indoc! {"
4953 «The quick brown
4954 fox jumps over
4955 the lazy dogˇ»
4956 "});
4957 cx.update_editor(|e, window, cx| {
4958 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
4959 });
4960 cx.assert_editor_state(indoc! {"
4961 «TheQuickBrown
4962 FoxJumpsOver
4963 TheLazyDogˇ»
4964 "});
4965
4966 // From here on out, test more complex cases of manipulate_text()
4967
4968 // Test no selection case - should affect words cursors are in
4969 // Cursor at beginning, middle, and end of word
4970 cx.set_state(indoc! {"
4971 ˇhello big beauˇtiful worldˇ
4972 "});
4973 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4974 cx.assert_editor_state(indoc! {"
4975 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
4976 "});
4977
4978 // Test multiple selections on a single line and across multiple lines
4979 cx.set_state(indoc! {"
4980 «Theˇ» quick «brown
4981 foxˇ» jumps «overˇ»
4982 the «lazyˇ» dog
4983 "});
4984 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4985 cx.assert_editor_state(indoc! {"
4986 «THEˇ» quick «BROWN
4987 FOXˇ» jumps «OVERˇ»
4988 the «LAZYˇ» dog
4989 "});
4990
4991 // Test case where text length grows
4992 cx.set_state(indoc! {"
4993 «tschüߡ»
4994 "});
4995 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4996 cx.assert_editor_state(indoc! {"
4997 «TSCHÜSSˇ»
4998 "});
4999
5000 // Test to make sure we don't crash when text shrinks
5001 cx.set_state(indoc! {"
5002 aaa_bbbˇ
5003 "});
5004 cx.update_editor(|e, window, cx| {
5005 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5006 });
5007 cx.assert_editor_state(indoc! {"
5008 «aaaBbbˇ»
5009 "});
5010
5011 // Test to make sure we all aware of the fact that each word can grow and shrink
5012 // Final selections should be aware of this fact
5013 cx.set_state(indoc! {"
5014 aaa_bˇbb bbˇb_ccc ˇccc_ddd
5015 "});
5016 cx.update_editor(|e, window, cx| {
5017 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5018 });
5019 cx.assert_editor_state(indoc! {"
5020 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
5021 "});
5022
5023 cx.set_state(indoc! {"
5024 «hElLo, WoRld!ˇ»
5025 "});
5026 cx.update_editor(|e, window, cx| {
5027 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
5028 });
5029 cx.assert_editor_state(indoc! {"
5030 «HeLlO, wOrLD!ˇ»
5031 "});
5032}
5033
5034#[gpui::test]
5035fn test_duplicate_line(cx: &mut TestAppContext) {
5036 init_test(cx, |_| {});
5037
5038 let editor = cx.add_window(|window, cx| {
5039 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5040 build_editor(buffer, window, cx)
5041 });
5042 _ = editor.update(cx, |editor, window, cx| {
5043 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5044 s.select_display_ranges([
5045 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5046 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5047 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5048 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5049 ])
5050 });
5051 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5052 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5053 assert_eq!(
5054 editor.selections.display_ranges(cx),
5055 vec![
5056 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
5057 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
5058 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5059 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
5060 ]
5061 );
5062 });
5063
5064 let editor = cx.add_window(|window, cx| {
5065 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5066 build_editor(buffer, window, cx)
5067 });
5068 _ = editor.update(cx, |editor, window, cx| {
5069 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5070 s.select_display_ranges([
5071 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5072 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5073 ])
5074 });
5075 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5076 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5077 assert_eq!(
5078 editor.selections.display_ranges(cx),
5079 vec![
5080 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
5081 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
5082 ]
5083 );
5084 });
5085
5086 // With `move_upwards` the selections stay in place, except for
5087 // the lines inserted above them
5088 let editor = cx.add_window(|window, cx| {
5089 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5090 build_editor(buffer, window, cx)
5091 });
5092 _ = editor.update(cx, |editor, window, cx| {
5093 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5094 s.select_display_ranges([
5095 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5096 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5097 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5098 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5099 ])
5100 });
5101 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5102 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5103 assert_eq!(
5104 editor.selections.display_ranges(cx),
5105 vec![
5106 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5107 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5108 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
5109 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
5110 ]
5111 );
5112 });
5113
5114 let editor = cx.add_window(|window, cx| {
5115 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5116 build_editor(buffer, window, cx)
5117 });
5118 _ = editor.update(cx, |editor, window, cx| {
5119 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5120 s.select_display_ranges([
5121 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5122 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5123 ])
5124 });
5125 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5126 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5127 assert_eq!(
5128 editor.selections.display_ranges(cx),
5129 vec![
5130 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5131 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5132 ]
5133 );
5134 });
5135
5136 let editor = cx.add_window(|window, cx| {
5137 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5138 build_editor(buffer, window, cx)
5139 });
5140 _ = editor.update(cx, |editor, window, cx| {
5141 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5142 s.select_display_ranges([
5143 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5144 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5145 ])
5146 });
5147 editor.duplicate_selection(&DuplicateSelection, window, cx);
5148 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
5149 assert_eq!(
5150 editor.selections.display_ranges(cx),
5151 vec![
5152 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5153 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
5154 ]
5155 );
5156 });
5157}
5158
5159#[gpui::test]
5160fn test_move_line_up_down(cx: &mut TestAppContext) {
5161 init_test(cx, |_| {});
5162
5163 let editor = cx.add_window(|window, cx| {
5164 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5165 build_editor(buffer, window, cx)
5166 });
5167 _ = editor.update(cx, |editor, window, cx| {
5168 editor.fold_creases(
5169 vec![
5170 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5171 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5172 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5173 ],
5174 true,
5175 window,
5176 cx,
5177 );
5178 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5179 s.select_display_ranges([
5180 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5181 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5182 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5183 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
5184 ])
5185 });
5186 assert_eq!(
5187 editor.display_text(cx),
5188 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
5189 );
5190
5191 editor.move_line_up(&MoveLineUp, window, cx);
5192 assert_eq!(
5193 editor.display_text(cx),
5194 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
5195 );
5196 assert_eq!(
5197 editor.selections.display_ranges(cx),
5198 vec![
5199 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5200 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5201 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5202 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5203 ]
5204 );
5205 });
5206
5207 _ = editor.update(cx, |editor, window, cx| {
5208 editor.move_line_down(&MoveLineDown, window, cx);
5209 assert_eq!(
5210 editor.display_text(cx),
5211 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
5212 );
5213 assert_eq!(
5214 editor.selections.display_ranges(cx),
5215 vec![
5216 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5217 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5218 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5219 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5220 ]
5221 );
5222 });
5223
5224 _ = editor.update(cx, |editor, window, cx| {
5225 editor.move_line_down(&MoveLineDown, window, cx);
5226 assert_eq!(
5227 editor.display_text(cx),
5228 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
5229 );
5230 assert_eq!(
5231 editor.selections.display_ranges(cx),
5232 vec![
5233 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5234 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5235 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5236 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5237 ]
5238 );
5239 });
5240
5241 _ = editor.update(cx, |editor, window, cx| {
5242 editor.move_line_up(&MoveLineUp, window, cx);
5243 assert_eq!(
5244 editor.display_text(cx),
5245 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
5246 );
5247 assert_eq!(
5248 editor.selections.display_ranges(cx),
5249 vec![
5250 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5251 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5252 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5253 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5254 ]
5255 );
5256 });
5257}
5258
5259#[gpui::test]
5260fn test_move_line_up_selection_at_end_of_fold(cx: &mut TestAppContext) {
5261 init_test(cx, |_| {});
5262 let editor = cx.add_window(|window, cx| {
5263 let buffer = MultiBuffer::build_simple("\n\n\n\n\n\naaaa\nbbbb\ncccc", cx);
5264 build_editor(buffer, window, cx)
5265 });
5266 _ = editor.update(cx, |editor, window, cx| {
5267 editor.fold_creases(
5268 vec![Crease::simple(
5269 Point::new(6, 4)..Point::new(7, 4),
5270 FoldPlaceholder::test(),
5271 )],
5272 true,
5273 window,
5274 cx,
5275 );
5276 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5277 s.select_ranges([Point::new(7, 4)..Point::new(7, 4)])
5278 });
5279 assert_eq!(editor.display_text(cx), "\n\n\n\n\n\naaaa⋯\ncccc");
5280 editor.move_line_up(&MoveLineUp, window, cx);
5281 let buffer_text = editor.buffer.read(cx).snapshot(cx).text();
5282 assert_eq!(buffer_text, "\n\n\n\n\naaaa\nbbbb\n\ncccc");
5283 });
5284}
5285
5286#[gpui::test]
5287fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
5288 init_test(cx, |_| {});
5289
5290 let editor = cx.add_window(|window, cx| {
5291 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5292 build_editor(buffer, window, cx)
5293 });
5294 _ = editor.update(cx, |editor, window, cx| {
5295 let snapshot = editor.buffer.read(cx).snapshot(cx);
5296 editor.insert_blocks(
5297 [BlockProperties {
5298 style: BlockStyle::Fixed,
5299 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
5300 height: Some(1),
5301 render: Arc::new(|_| div().into_any()),
5302 priority: 0,
5303 }],
5304 Some(Autoscroll::fit()),
5305 cx,
5306 );
5307 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5308 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
5309 });
5310 editor.move_line_down(&MoveLineDown, window, cx);
5311 });
5312}
5313
5314#[gpui::test]
5315async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
5316 init_test(cx, |_| {});
5317
5318 let mut cx = EditorTestContext::new(cx).await;
5319 cx.set_state(
5320 &"
5321 ˇzero
5322 one
5323 two
5324 three
5325 four
5326 five
5327 "
5328 .unindent(),
5329 );
5330
5331 // Create a four-line block that replaces three lines of text.
5332 cx.update_editor(|editor, window, cx| {
5333 let snapshot = editor.snapshot(window, cx);
5334 let snapshot = &snapshot.buffer_snapshot;
5335 let placement = BlockPlacement::Replace(
5336 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
5337 );
5338 editor.insert_blocks(
5339 [BlockProperties {
5340 placement,
5341 height: Some(4),
5342 style: BlockStyle::Sticky,
5343 render: Arc::new(|_| gpui::div().into_any_element()),
5344 priority: 0,
5345 }],
5346 None,
5347 cx,
5348 );
5349 });
5350
5351 // Move down so that the cursor touches the block.
5352 cx.update_editor(|editor, window, cx| {
5353 editor.move_down(&Default::default(), window, cx);
5354 });
5355 cx.assert_editor_state(
5356 &"
5357 zero
5358 «one
5359 two
5360 threeˇ»
5361 four
5362 five
5363 "
5364 .unindent(),
5365 );
5366
5367 // Move down past the block.
5368 cx.update_editor(|editor, window, cx| {
5369 editor.move_down(&Default::default(), window, cx);
5370 });
5371 cx.assert_editor_state(
5372 &"
5373 zero
5374 one
5375 two
5376 three
5377 ˇfour
5378 five
5379 "
5380 .unindent(),
5381 );
5382}
5383
5384#[gpui::test]
5385fn test_transpose(cx: &mut TestAppContext) {
5386 init_test(cx, |_| {});
5387
5388 _ = cx.add_window(|window, cx| {
5389 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
5390 editor.set_style(EditorStyle::default(), window, cx);
5391 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5392 s.select_ranges([1..1])
5393 });
5394 editor.transpose(&Default::default(), window, cx);
5395 assert_eq!(editor.text(cx), "bac");
5396 assert_eq!(editor.selections.ranges(cx), [2..2]);
5397
5398 editor.transpose(&Default::default(), window, cx);
5399 assert_eq!(editor.text(cx), "bca");
5400 assert_eq!(editor.selections.ranges(cx), [3..3]);
5401
5402 editor.transpose(&Default::default(), window, cx);
5403 assert_eq!(editor.text(cx), "bac");
5404 assert_eq!(editor.selections.ranges(cx), [3..3]);
5405
5406 editor
5407 });
5408
5409 _ = cx.add_window(|window, cx| {
5410 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5411 editor.set_style(EditorStyle::default(), window, cx);
5412 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5413 s.select_ranges([3..3])
5414 });
5415 editor.transpose(&Default::default(), window, cx);
5416 assert_eq!(editor.text(cx), "acb\nde");
5417 assert_eq!(editor.selections.ranges(cx), [3..3]);
5418
5419 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5420 s.select_ranges([4..4])
5421 });
5422 editor.transpose(&Default::default(), window, cx);
5423 assert_eq!(editor.text(cx), "acbd\ne");
5424 assert_eq!(editor.selections.ranges(cx), [5..5]);
5425
5426 editor.transpose(&Default::default(), window, cx);
5427 assert_eq!(editor.text(cx), "acbde\n");
5428 assert_eq!(editor.selections.ranges(cx), [6..6]);
5429
5430 editor.transpose(&Default::default(), window, cx);
5431 assert_eq!(editor.text(cx), "acbd\ne");
5432 assert_eq!(editor.selections.ranges(cx), [6..6]);
5433
5434 editor
5435 });
5436
5437 _ = cx.add_window(|window, cx| {
5438 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5439 editor.set_style(EditorStyle::default(), window, cx);
5440 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5441 s.select_ranges([1..1, 2..2, 4..4])
5442 });
5443 editor.transpose(&Default::default(), window, cx);
5444 assert_eq!(editor.text(cx), "bacd\ne");
5445 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
5446
5447 editor.transpose(&Default::default(), window, cx);
5448 assert_eq!(editor.text(cx), "bcade\n");
5449 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
5450
5451 editor.transpose(&Default::default(), window, cx);
5452 assert_eq!(editor.text(cx), "bcda\ne");
5453 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5454
5455 editor.transpose(&Default::default(), window, cx);
5456 assert_eq!(editor.text(cx), "bcade\n");
5457 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5458
5459 editor.transpose(&Default::default(), window, cx);
5460 assert_eq!(editor.text(cx), "bcaed\n");
5461 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
5462
5463 editor
5464 });
5465
5466 _ = cx.add_window(|window, cx| {
5467 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
5468 editor.set_style(EditorStyle::default(), window, cx);
5469 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5470 s.select_ranges([4..4])
5471 });
5472 editor.transpose(&Default::default(), window, cx);
5473 assert_eq!(editor.text(cx), "🏀🍐✋");
5474 assert_eq!(editor.selections.ranges(cx), [8..8]);
5475
5476 editor.transpose(&Default::default(), window, cx);
5477 assert_eq!(editor.text(cx), "🏀✋🍐");
5478 assert_eq!(editor.selections.ranges(cx), [11..11]);
5479
5480 editor.transpose(&Default::default(), window, cx);
5481 assert_eq!(editor.text(cx), "🏀🍐✋");
5482 assert_eq!(editor.selections.ranges(cx), [11..11]);
5483
5484 editor
5485 });
5486}
5487
5488#[gpui::test]
5489async fn test_rewrap(cx: &mut TestAppContext) {
5490 init_test(cx, |settings| {
5491 settings.languages.0.extend([
5492 (
5493 "Markdown".into(),
5494 LanguageSettingsContent {
5495 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5496 preferred_line_length: Some(40),
5497 ..Default::default()
5498 },
5499 ),
5500 (
5501 "Plain Text".into(),
5502 LanguageSettingsContent {
5503 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5504 preferred_line_length: Some(40),
5505 ..Default::default()
5506 },
5507 ),
5508 (
5509 "C++".into(),
5510 LanguageSettingsContent {
5511 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5512 preferred_line_length: Some(40),
5513 ..Default::default()
5514 },
5515 ),
5516 (
5517 "Python".into(),
5518 LanguageSettingsContent {
5519 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5520 preferred_line_length: Some(40),
5521 ..Default::default()
5522 },
5523 ),
5524 (
5525 "Rust".into(),
5526 LanguageSettingsContent {
5527 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5528 preferred_line_length: Some(40),
5529 ..Default::default()
5530 },
5531 ),
5532 ])
5533 });
5534
5535 let mut cx = EditorTestContext::new(cx).await;
5536
5537 let cpp_language = Arc::new(Language::new(
5538 LanguageConfig {
5539 name: "C++".into(),
5540 line_comments: vec!["// ".into()],
5541 ..LanguageConfig::default()
5542 },
5543 None,
5544 ));
5545 let python_language = Arc::new(Language::new(
5546 LanguageConfig {
5547 name: "Python".into(),
5548 line_comments: vec!["# ".into()],
5549 ..LanguageConfig::default()
5550 },
5551 None,
5552 ));
5553 let markdown_language = Arc::new(Language::new(
5554 LanguageConfig {
5555 name: "Markdown".into(),
5556 rewrap_prefixes: vec![
5557 regex::Regex::new("\\d+\\.\\s+").unwrap(),
5558 regex::Regex::new("[-*+]\\s+").unwrap(),
5559 ],
5560 ..LanguageConfig::default()
5561 },
5562 None,
5563 ));
5564 let rust_language = Arc::new(Language::new(
5565 LanguageConfig {
5566 name: "Rust".into(),
5567 line_comments: vec!["// ".into(), "/// ".into()],
5568 ..LanguageConfig::default()
5569 },
5570 Some(tree_sitter_rust::LANGUAGE.into()),
5571 ));
5572
5573 let plaintext_language = Arc::new(Language::new(
5574 LanguageConfig {
5575 name: "Plain Text".into(),
5576 ..LanguageConfig::default()
5577 },
5578 None,
5579 ));
5580
5581 // Test basic rewrapping of a long line with a cursor
5582 assert_rewrap(
5583 indoc! {"
5584 // ˇThis is a long comment that needs to be wrapped.
5585 "},
5586 indoc! {"
5587 // ˇThis is a long comment that needs to
5588 // be wrapped.
5589 "},
5590 cpp_language.clone(),
5591 &mut cx,
5592 );
5593
5594 // Test rewrapping a full selection
5595 assert_rewrap(
5596 indoc! {"
5597 «// This selected long comment needs to be wrapped.ˇ»"
5598 },
5599 indoc! {"
5600 «// This selected long comment needs to
5601 // be wrapped.ˇ»"
5602 },
5603 cpp_language.clone(),
5604 &mut cx,
5605 );
5606
5607 // Test multiple cursors on different lines within the same paragraph are preserved after rewrapping
5608 assert_rewrap(
5609 indoc! {"
5610 // ˇThis is the first line.
5611 // Thisˇ is the second line.
5612 // This is the thirdˇ line, all part of one paragraph.
5613 "},
5614 indoc! {"
5615 // ˇThis is the first line. Thisˇ is the
5616 // second line. This is the thirdˇ line,
5617 // all part of one paragraph.
5618 "},
5619 cpp_language.clone(),
5620 &mut cx,
5621 );
5622
5623 // Test multiple cursors in different paragraphs trigger separate rewraps
5624 assert_rewrap(
5625 indoc! {"
5626 // ˇThis is the first paragraph, first line.
5627 // ˇThis is the first paragraph, second line.
5628
5629 // ˇThis is the second paragraph, first line.
5630 // ˇThis is the second paragraph, second line.
5631 "},
5632 indoc! {"
5633 // ˇThis is the first paragraph, first
5634 // line. ˇThis is the first paragraph,
5635 // second line.
5636
5637 // ˇThis is the second paragraph, first
5638 // line. ˇThis is the second paragraph,
5639 // second line.
5640 "},
5641 cpp_language.clone(),
5642 &mut cx,
5643 );
5644
5645 // Test that change in comment prefix (e.g., `//` to `///`) trigger seperate rewraps
5646 assert_rewrap(
5647 indoc! {"
5648 «// A regular long long comment to be wrapped.
5649 /// A documentation long comment to be wrapped.ˇ»
5650 "},
5651 indoc! {"
5652 «// A regular long long comment to be
5653 // wrapped.
5654 /// A documentation long comment to be
5655 /// wrapped.ˇ»
5656 "},
5657 rust_language.clone(),
5658 &mut cx,
5659 );
5660
5661 // Test that change in indentation level trigger seperate rewraps
5662 assert_rewrap(
5663 indoc! {"
5664 fn foo() {
5665 «// This is a long comment at the base indent.
5666 // This is a long comment at the next indent.ˇ»
5667 }
5668 "},
5669 indoc! {"
5670 fn foo() {
5671 «// This is a long comment at the
5672 // base indent.
5673 // This is a long comment at the
5674 // next indent.ˇ»
5675 }
5676 "},
5677 rust_language.clone(),
5678 &mut cx,
5679 );
5680
5681 // Test that different comment prefix characters (e.g., '#') are handled correctly
5682 assert_rewrap(
5683 indoc! {"
5684 # ˇThis is a long comment using a pound sign.
5685 "},
5686 indoc! {"
5687 # ˇThis is a long comment using a pound
5688 # sign.
5689 "},
5690 python_language,
5691 &mut cx,
5692 );
5693
5694 // Test rewrapping only affects comments, not code even when selected
5695 assert_rewrap(
5696 indoc! {"
5697 «/// This doc comment is long and should be wrapped.
5698 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
5699 "},
5700 indoc! {"
5701 «/// This doc comment is long and should
5702 /// be wrapped.
5703 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
5704 "},
5705 rust_language.clone(),
5706 &mut cx,
5707 );
5708
5709 // Test that rewrapping works in Markdown documents where `allow_rewrap` is `Anywhere`
5710 assert_rewrap(
5711 indoc! {"
5712 # Header
5713
5714 A long long long line of markdown text to wrap.ˇ
5715 "},
5716 indoc! {"
5717 # Header
5718
5719 A long long long line of markdown text
5720 to wrap.ˇ
5721 "},
5722 markdown_language.clone(),
5723 &mut cx,
5724 );
5725
5726 // Test that rewrapping boundary works and preserves relative indent for Markdown documents
5727 assert_rewrap(
5728 indoc! {"
5729 «1. This is a numbered list item that is very long and needs to be wrapped properly.
5730 2. This is a numbered list item that is very long and needs to be wrapped properly.
5731 - This is an unordered list item that is also very long and should not merge with the numbered item.ˇ»
5732 "},
5733 indoc! {"
5734 «1. This is a numbered list item that is
5735 very long and needs to be wrapped
5736 properly.
5737 2. This is a numbered list item that is
5738 very long and needs to be wrapped
5739 properly.
5740 - This is an unordered list item that is
5741 also very long and should not merge
5742 with the numbered item.ˇ»
5743 "},
5744 markdown_language.clone(),
5745 &mut cx,
5746 );
5747
5748 // Test that rewrapping add indents for rewrapping boundary if not exists already.
5749 assert_rewrap(
5750 indoc! {"
5751 «1. This is a numbered list item that is
5752 very long and needs to be wrapped
5753 properly.
5754 2. This is a numbered list item that is
5755 very long and needs to be wrapped
5756 properly.
5757 - This is an unordered list item that is
5758 also very long and should not merge with
5759 the numbered item.ˇ»
5760 "},
5761 indoc! {"
5762 «1. This is a numbered list item that is
5763 very long and needs to be wrapped
5764 properly.
5765 2. This is a numbered list item that is
5766 very long and needs to be wrapped
5767 properly.
5768 - This is an unordered list item that is
5769 also very long and should not merge
5770 with the numbered item.ˇ»
5771 "},
5772 markdown_language.clone(),
5773 &mut cx,
5774 );
5775
5776 // Test that rewrapping maintain indents even when they already exists.
5777 assert_rewrap(
5778 indoc! {"
5779 «1. This is a numbered list
5780 item that is very long and needs to be wrapped properly.
5781 2. This is a numbered list
5782 item that is very long and needs to be wrapped properly.
5783 - This is an unordered list item that is also very long and
5784 should not merge with the numbered item.ˇ»
5785 "},
5786 indoc! {"
5787 «1. This is a numbered list item that is
5788 very long and needs to be wrapped
5789 properly.
5790 2. This is a numbered list item that is
5791 very long and needs to be wrapped
5792 properly.
5793 - This is an unordered list item that is
5794 also very long and should not merge
5795 with the numbered item.ˇ»
5796 "},
5797 markdown_language,
5798 &mut cx,
5799 );
5800
5801 // Test that rewrapping works in plain text where `allow_rewrap` is `Anywhere`
5802 assert_rewrap(
5803 indoc! {"
5804 ˇThis is a very long line of plain text that will be wrapped.
5805 "},
5806 indoc! {"
5807 ˇThis is a very long line of plain text
5808 that will be wrapped.
5809 "},
5810 plaintext_language.clone(),
5811 &mut cx,
5812 );
5813
5814 // Test that non-commented code acts as a paragraph boundary within a selection
5815 assert_rewrap(
5816 indoc! {"
5817 «// This is the first long comment block to be wrapped.
5818 fn my_func(a: u32);
5819 // This is the second long comment block to be wrapped.ˇ»
5820 "},
5821 indoc! {"
5822 «// This is the first long comment block
5823 // to be wrapped.
5824 fn my_func(a: u32);
5825 // This is the second long comment block
5826 // to be wrapped.ˇ»
5827 "},
5828 rust_language,
5829 &mut cx,
5830 );
5831
5832 // Test rewrapping multiple selections, including ones with blank lines or tabs
5833 assert_rewrap(
5834 indoc! {"
5835 «ˇThis is a very long line that will be wrapped.
5836
5837 This is another paragraph in the same selection.»
5838
5839 «\tThis is a very long indented line that will be wrapped.ˇ»
5840 "},
5841 indoc! {"
5842 «ˇThis is a very long line that will be
5843 wrapped.
5844
5845 This is another paragraph in the same
5846 selection.»
5847
5848 «\tThis is a very long indented line
5849 \tthat will be wrapped.ˇ»
5850 "},
5851 plaintext_language,
5852 &mut cx,
5853 );
5854
5855 // Test that an empty comment line acts as a paragraph boundary
5856 assert_rewrap(
5857 indoc! {"
5858 // ˇThis is a long comment that will be wrapped.
5859 //
5860 // And this is another long comment that will also be wrapped.ˇ
5861 "},
5862 indoc! {"
5863 // ˇThis is a long comment that will be
5864 // wrapped.
5865 //
5866 // And this is another long comment that
5867 // will also be wrapped.ˇ
5868 "},
5869 cpp_language,
5870 &mut cx,
5871 );
5872
5873 #[track_caller]
5874 fn assert_rewrap(
5875 unwrapped_text: &str,
5876 wrapped_text: &str,
5877 language: Arc<Language>,
5878 cx: &mut EditorTestContext,
5879 ) {
5880 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
5881 cx.set_state(unwrapped_text);
5882 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
5883 cx.assert_editor_state(wrapped_text);
5884 }
5885}
5886
5887#[gpui::test]
5888async fn test_hard_wrap(cx: &mut TestAppContext) {
5889 init_test(cx, |_| {});
5890 let mut cx = EditorTestContext::new(cx).await;
5891
5892 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
5893 cx.update_editor(|editor, _, cx| {
5894 editor.set_hard_wrap(Some(14), cx);
5895 });
5896
5897 cx.set_state(indoc!(
5898 "
5899 one two three ˇ
5900 "
5901 ));
5902 cx.simulate_input("four");
5903 cx.run_until_parked();
5904
5905 cx.assert_editor_state(indoc!(
5906 "
5907 one two three
5908 fourˇ
5909 "
5910 ));
5911
5912 cx.update_editor(|editor, window, cx| {
5913 editor.newline(&Default::default(), window, cx);
5914 });
5915 cx.run_until_parked();
5916 cx.assert_editor_state(indoc!(
5917 "
5918 one two three
5919 four
5920 ˇ
5921 "
5922 ));
5923
5924 cx.simulate_input("five");
5925 cx.run_until_parked();
5926 cx.assert_editor_state(indoc!(
5927 "
5928 one two three
5929 four
5930 fiveˇ
5931 "
5932 ));
5933
5934 cx.update_editor(|editor, window, cx| {
5935 editor.newline(&Default::default(), window, cx);
5936 });
5937 cx.run_until_parked();
5938 cx.simulate_input("# ");
5939 cx.run_until_parked();
5940 cx.assert_editor_state(indoc!(
5941 "
5942 one two three
5943 four
5944 five
5945 # ˇ
5946 "
5947 ));
5948
5949 cx.update_editor(|editor, window, cx| {
5950 editor.newline(&Default::default(), window, cx);
5951 });
5952 cx.run_until_parked();
5953 cx.assert_editor_state(indoc!(
5954 "
5955 one two three
5956 four
5957 five
5958 #\x20
5959 #ˇ
5960 "
5961 ));
5962
5963 cx.simulate_input(" 6");
5964 cx.run_until_parked();
5965 cx.assert_editor_state(indoc!(
5966 "
5967 one two three
5968 four
5969 five
5970 #
5971 # 6ˇ
5972 "
5973 ));
5974}
5975
5976#[gpui::test]
5977async fn test_clipboard(cx: &mut TestAppContext) {
5978 init_test(cx, |_| {});
5979
5980 let mut cx = EditorTestContext::new(cx).await;
5981
5982 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
5983 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5984 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
5985
5986 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
5987 cx.set_state("two ˇfour ˇsix ˇ");
5988 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5989 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
5990
5991 // Paste again but with only two cursors. Since the number of cursors doesn't
5992 // match the number of slices in the clipboard, the entire clipboard text
5993 // is pasted at each cursor.
5994 cx.set_state("ˇtwo one✅ four three six five ˇ");
5995 cx.update_editor(|e, window, cx| {
5996 e.handle_input("( ", window, cx);
5997 e.paste(&Paste, window, cx);
5998 e.handle_input(") ", window, cx);
5999 });
6000 cx.assert_editor_state(
6001 &([
6002 "( one✅ ",
6003 "three ",
6004 "five ) ˇtwo one✅ four three six five ( one✅ ",
6005 "three ",
6006 "five ) ˇ",
6007 ]
6008 .join("\n")),
6009 );
6010
6011 // Cut with three selections, one of which is full-line.
6012 cx.set_state(indoc! {"
6013 1«2ˇ»3
6014 4ˇ567
6015 «8ˇ»9"});
6016 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6017 cx.assert_editor_state(indoc! {"
6018 1ˇ3
6019 ˇ9"});
6020
6021 // Paste with three selections, noticing how the copied selection that was full-line
6022 // gets inserted before the second cursor.
6023 cx.set_state(indoc! {"
6024 1ˇ3
6025 9ˇ
6026 «oˇ»ne"});
6027 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6028 cx.assert_editor_state(indoc! {"
6029 12ˇ3
6030 4567
6031 9ˇ
6032 8ˇne"});
6033
6034 // Copy with a single cursor only, which writes the whole line into the clipboard.
6035 cx.set_state(indoc! {"
6036 The quick brown
6037 fox juˇmps over
6038 the lazy dog"});
6039 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6040 assert_eq!(
6041 cx.read_from_clipboard()
6042 .and_then(|item| item.text().as_deref().map(str::to_string)),
6043 Some("fox jumps over\n".to_string())
6044 );
6045
6046 // Paste with three selections, noticing how the copied full-line selection is inserted
6047 // before the empty selections but replaces the selection that is non-empty.
6048 cx.set_state(indoc! {"
6049 Tˇhe quick brown
6050 «foˇ»x jumps over
6051 tˇhe lazy dog"});
6052 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6053 cx.assert_editor_state(indoc! {"
6054 fox jumps over
6055 Tˇhe quick brown
6056 fox jumps over
6057 ˇx jumps over
6058 fox jumps over
6059 tˇhe lazy dog"});
6060}
6061
6062#[gpui::test]
6063async fn test_copy_trim(cx: &mut TestAppContext) {
6064 init_test(cx, |_| {});
6065
6066 let mut cx = EditorTestContext::new(cx).await;
6067 cx.set_state(
6068 r#" «for selection in selections.iter() {
6069 let mut start = selection.start;
6070 let mut end = selection.end;
6071 let is_entire_line = selection.is_empty();
6072 if is_entire_line {
6073 start = Point::new(start.row, 0);ˇ»
6074 end = cmp::min(max_point, Point::new(end.row + 1, 0));
6075 }
6076 "#,
6077 );
6078 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6079 assert_eq!(
6080 cx.read_from_clipboard()
6081 .and_then(|item| item.text().as_deref().map(str::to_string)),
6082 Some(
6083 "for selection in selections.iter() {
6084 let mut start = selection.start;
6085 let mut end = selection.end;
6086 let is_entire_line = selection.is_empty();
6087 if is_entire_line {
6088 start = Point::new(start.row, 0);"
6089 .to_string()
6090 ),
6091 "Regular copying preserves all indentation selected",
6092 );
6093 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6094 assert_eq!(
6095 cx.read_from_clipboard()
6096 .and_then(|item| item.text().as_deref().map(str::to_string)),
6097 Some(
6098 "for selection in selections.iter() {
6099let mut start = selection.start;
6100let mut end = selection.end;
6101let is_entire_line = selection.is_empty();
6102if is_entire_line {
6103 start = Point::new(start.row, 0);"
6104 .to_string()
6105 ),
6106 "Copying with stripping should strip all leading whitespaces"
6107 );
6108
6109 cx.set_state(
6110 r#" « for selection in selections.iter() {
6111 let mut start = selection.start;
6112 let mut end = selection.end;
6113 let is_entire_line = selection.is_empty();
6114 if is_entire_line {
6115 start = Point::new(start.row, 0);ˇ»
6116 end = cmp::min(max_point, Point::new(end.row + 1, 0));
6117 }
6118 "#,
6119 );
6120 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6121 assert_eq!(
6122 cx.read_from_clipboard()
6123 .and_then(|item| item.text().as_deref().map(str::to_string)),
6124 Some(
6125 " for selection in selections.iter() {
6126 let mut start = selection.start;
6127 let mut end = selection.end;
6128 let is_entire_line = selection.is_empty();
6129 if is_entire_line {
6130 start = Point::new(start.row, 0);"
6131 .to_string()
6132 ),
6133 "Regular copying preserves all indentation selected",
6134 );
6135 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6136 assert_eq!(
6137 cx.read_from_clipboard()
6138 .and_then(|item| item.text().as_deref().map(str::to_string)),
6139 Some(
6140 "for selection in selections.iter() {
6141let mut start = selection.start;
6142let mut end = selection.end;
6143let is_entire_line = selection.is_empty();
6144if is_entire_line {
6145 start = Point::new(start.row, 0);"
6146 .to_string()
6147 ),
6148 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
6149 );
6150
6151 cx.set_state(
6152 r#" «ˇ for selection in selections.iter() {
6153 let mut start = selection.start;
6154 let mut end = selection.end;
6155 let is_entire_line = selection.is_empty();
6156 if is_entire_line {
6157 start = Point::new(start.row, 0);»
6158 end = cmp::min(max_point, Point::new(end.row + 1, 0));
6159 }
6160 "#,
6161 );
6162 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6163 assert_eq!(
6164 cx.read_from_clipboard()
6165 .and_then(|item| item.text().as_deref().map(str::to_string)),
6166 Some(
6167 " for selection in selections.iter() {
6168 let mut start = selection.start;
6169 let mut end = selection.end;
6170 let is_entire_line = selection.is_empty();
6171 if is_entire_line {
6172 start = Point::new(start.row, 0);"
6173 .to_string()
6174 ),
6175 "Regular copying for reverse selection works the same",
6176 );
6177 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6178 assert_eq!(
6179 cx.read_from_clipboard()
6180 .and_then(|item| item.text().as_deref().map(str::to_string)),
6181 Some(
6182 "for selection in selections.iter() {
6183let mut start = selection.start;
6184let mut end = selection.end;
6185let is_entire_line = selection.is_empty();
6186if is_entire_line {
6187 start = Point::new(start.row, 0);"
6188 .to_string()
6189 ),
6190 "Copying with stripping for reverse selection works the same"
6191 );
6192
6193 cx.set_state(
6194 r#" for selection «in selections.iter() {
6195 let mut start = selection.start;
6196 let mut end = selection.end;
6197 let is_entire_line = selection.is_empty();
6198 if is_entire_line {
6199 start = Point::new(start.row, 0);ˇ»
6200 end = cmp::min(max_point, Point::new(end.row + 1, 0));
6201 }
6202 "#,
6203 );
6204 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6205 assert_eq!(
6206 cx.read_from_clipboard()
6207 .and_then(|item| item.text().as_deref().map(str::to_string)),
6208 Some(
6209 "in selections.iter() {
6210 let mut start = selection.start;
6211 let mut end = selection.end;
6212 let is_entire_line = selection.is_empty();
6213 if is_entire_line {
6214 start = Point::new(start.row, 0);"
6215 .to_string()
6216 ),
6217 "When selecting past the indent, the copying works as usual",
6218 );
6219 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6220 assert_eq!(
6221 cx.read_from_clipboard()
6222 .and_then(|item| item.text().as_deref().map(str::to_string)),
6223 Some(
6224 "in selections.iter() {
6225 let mut start = selection.start;
6226 let mut end = selection.end;
6227 let is_entire_line = selection.is_empty();
6228 if is_entire_line {
6229 start = Point::new(start.row, 0);"
6230 .to_string()
6231 ),
6232 "When selecting past the indent, nothing is trimmed"
6233 );
6234
6235 cx.set_state(
6236 r#" «for selection in selections.iter() {
6237 let mut start = selection.start;
6238
6239 let mut end = selection.end;
6240 let is_entire_line = selection.is_empty();
6241 if is_entire_line {
6242 start = Point::new(start.row, 0);
6243ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
6244 }
6245 "#,
6246 );
6247 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6248 assert_eq!(
6249 cx.read_from_clipboard()
6250 .and_then(|item| item.text().as_deref().map(str::to_string)),
6251 Some(
6252 "for selection in selections.iter() {
6253let mut start = selection.start;
6254
6255let mut end = selection.end;
6256let is_entire_line = selection.is_empty();
6257if is_entire_line {
6258 start = Point::new(start.row, 0);
6259"
6260 .to_string()
6261 ),
6262 "Copying with stripping should ignore empty lines"
6263 );
6264}
6265
6266#[gpui::test]
6267async fn test_paste_multiline(cx: &mut TestAppContext) {
6268 init_test(cx, |_| {});
6269
6270 let mut cx = EditorTestContext::new(cx).await;
6271 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
6272
6273 // Cut an indented block, without the leading whitespace.
6274 cx.set_state(indoc! {"
6275 const a: B = (
6276 c(),
6277 «d(
6278 e,
6279 f
6280 )ˇ»
6281 );
6282 "});
6283 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6284 cx.assert_editor_state(indoc! {"
6285 const a: B = (
6286 c(),
6287 ˇ
6288 );
6289 "});
6290
6291 // Paste it at the same position.
6292 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6293 cx.assert_editor_state(indoc! {"
6294 const a: B = (
6295 c(),
6296 d(
6297 e,
6298 f
6299 )ˇ
6300 );
6301 "});
6302
6303 // Paste it at a line with a lower indent level.
6304 cx.set_state(indoc! {"
6305 ˇ
6306 const a: B = (
6307 c(),
6308 );
6309 "});
6310 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6311 cx.assert_editor_state(indoc! {"
6312 d(
6313 e,
6314 f
6315 )ˇ
6316 const a: B = (
6317 c(),
6318 );
6319 "});
6320
6321 // Cut an indented block, with the leading whitespace.
6322 cx.set_state(indoc! {"
6323 const a: B = (
6324 c(),
6325 « d(
6326 e,
6327 f
6328 )
6329 ˇ»);
6330 "});
6331 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6332 cx.assert_editor_state(indoc! {"
6333 const a: B = (
6334 c(),
6335 ˇ);
6336 "});
6337
6338 // Paste it at the same position.
6339 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6340 cx.assert_editor_state(indoc! {"
6341 const a: B = (
6342 c(),
6343 d(
6344 e,
6345 f
6346 )
6347 ˇ);
6348 "});
6349
6350 // Paste it at a line with a higher indent level.
6351 cx.set_state(indoc! {"
6352 const a: B = (
6353 c(),
6354 d(
6355 e,
6356 fˇ
6357 )
6358 );
6359 "});
6360 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6361 cx.assert_editor_state(indoc! {"
6362 const a: B = (
6363 c(),
6364 d(
6365 e,
6366 f d(
6367 e,
6368 f
6369 )
6370 ˇ
6371 )
6372 );
6373 "});
6374
6375 // Copy an indented block, starting mid-line
6376 cx.set_state(indoc! {"
6377 const a: B = (
6378 c(),
6379 somethin«g(
6380 e,
6381 f
6382 )ˇ»
6383 );
6384 "});
6385 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6386
6387 // Paste it on a line with a lower indent level
6388 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
6389 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6390 cx.assert_editor_state(indoc! {"
6391 const a: B = (
6392 c(),
6393 something(
6394 e,
6395 f
6396 )
6397 );
6398 g(
6399 e,
6400 f
6401 )ˇ"});
6402}
6403
6404#[gpui::test]
6405async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
6406 init_test(cx, |_| {});
6407
6408 cx.write_to_clipboard(ClipboardItem::new_string(
6409 " d(\n e\n );\n".into(),
6410 ));
6411
6412 let mut cx = EditorTestContext::new(cx).await;
6413 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
6414
6415 cx.set_state(indoc! {"
6416 fn a() {
6417 b();
6418 if c() {
6419 ˇ
6420 }
6421 }
6422 "});
6423
6424 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6425 cx.assert_editor_state(indoc! {"
6426 fn a() {
6427 b();
6428 if c() {
6429 d(
6430 e
6431 );
6432 ˇ
6433 }
6434 }
6435 "});
6436
6437 cx.set_state(indoc! {"
6438 fn a() {
6439 b();
6440 ˇ
6441 }
6442 "});
6443
6444 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6445 cx.assert_editor_state(indoc! {"
6446 fn a() {
6447 b();
6448 d(
6449 e
6450 );
6451 ˇ
6452 }
6453 "});
6454}
6455
6456#[gpui::test]
6457fn test_select_all(cx: &mut TestAppContext) {
6458 init_test(cx, |_| {});
6459
6460 let editor = cx.add_window(|window, cx| {
6461 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
6462 build_editor(buffer, window, cx)
6463 });
6464 _ = editor.update(cx, |editor, window, cx| {
6465 editor.select_all(&SelectAll, window, cx);
6466 assert_eq!(
6467 editor.selections.display_ranges(cx),
6468 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
6469 );
6470 });
6471}
6472
6473#[gpui::test]
6474fn test_select_line(cx: &mut TestAppContext) {
6475 init_test(cx, |_| {});
6476
6477 let editor = cx.add_window(|window, cx| {
6478 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
6479 build_editor(buffer, window, cx)
6480 });
6481 _ = editor.update(cx, |editor, window, cx| {
6482 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6483 s.select_display_ranges([
6484 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6485 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
6486 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
6487 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
6488 ])
6489 });
6490 editor.select_line(&SelectLine, window, cx);
6491 assert_eq!(
6492 editor.selections.display_ranges(cx),
6493 vec![
6494 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
6495 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
6496 ]
6497 );
6498 });
6499
6500 _ = editor.update(cx, |editor, window, cx| {
6501 editor.select_line(&SelectLine, window, cx);
6502 assert_eq!(
6503 editor.selections.display_ranges(cx),
6504 vec![
6505 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
6506 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
6507 ]
6508 );
6509 });
6510
6511 _ = editor.update(cx, |editor, window, cx| {
6512 editor.select_line(&SelectLine, window, cx);
6513 assert_eq!(
6514 editor.selections.display_ranges(cx),
6515 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
6516 );
6517 });
6518}
6519
6520#[gpui::test]
6521async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
6522 init_test(cx, |_| {});
6523 let mut cx = EditorTestContext::new(cx).await;
6524
6525 #[track_caller]
6526 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
6527 cx.set_state(initial_state);
6528 cx.update_editor(|e, window, cx| {
6529 e.split_selection_into_lines(&Default::default(), window, cx)
6530 });
6531 cx.assert_editor_state(expected_state);
6532 }
6533
6534 // Selection starts and ends at the middle of lines, left-to-right
6535 test(
6536 &mut cx,
6537 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
6538 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
6539 );
6540 // Same thing, right-to-left
6541 test(
6542 &mut cx,
6543 "aa\nb«b\ncc\ndd\neˇ»e\nff",
6544 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
6545 );
6546
6547 // Whole buffer, left-to-right, last line *doesn't* end with newline
6548 test(
6549 &mut cx,
6550 "«ˇaa\nbb\ncc\ndd\nee\nff»",
6551 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
6552 );
6553 // Same thing, right-to-left
6554 test(
6555 &mut cx,
6556 "«aa\nbb\ncc\ndd\nee\nffˇ»",
6557 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
6558 );
6559
6560 // Whole buffer, left-to-right, last line ends with newline
6561 test(
6562 &mut cx,
6563 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
6564 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
6565 );
6566 // Same thing, right-to-left
6567 test(
6568 &mut cx,
6569 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
6570 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
6571 );
6572
6573 // Starts at the end of a line, ends at the start of another
6574 test(
6575 &mut cx,
6576 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
6577 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
6578 );
6579}
6580
6581#[gpui::test]
6582async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
6583 init_test(cx, |_| {});
6584
6585 let editor = cx.add_window(|window, cx| {
6586 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
6587 build_editor(buffer, window, cx)
6588 });
6589
6590 // setup
6591 _ = editor.update(cx, |editor, window, cx| {
6592 editor.fold_creases(
6593 vec![
6594 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
6595 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
6596 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
6597 ],
6598 true,
6599 window,
6600 cx,
6601 );
6602 assert_eq!(
6603 editor.display_text(cx),
6604 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
6605 );
6606 });
6607
6608 _ = editor.update(cx, |editor, window, cx| {
6609 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6610 s.select_display_ranges([
6611 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6612 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
6613 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
6614 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
6615 ])
6616 });
6617 editor.split_selection_into_lines(&Default::default(), window, cx);
6618 assert_eq!(
6619 editor.display_text(cx),
6620 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
6621 );
6622 });
6623 EditorTestContext::for_editor(editor, cx)
6624 .await
6625 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
6626
6627 _ = editor.update(cx, |editor, window, cx| {
6628 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6629 s.select_display_ranges([
6630 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
6631 ])
6632 });
6633 editor.split_selection_into_lines(&Default::default(), window, cx);
6634 assert_eq!(
6635 editor.display_text(cx),
6636 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
6637 );
6638 assert_eq!(
6639 editor.selections.display_ranges(cx),
6640 [
6641 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
6642 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
6643 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
6644 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
6645 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
6646 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
6647 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
6648 ]
6649 );
6650 });
6651 EditorTestContext::for_editor(editor, cx)
6652 .await
6653 .assert_editor_state(
6654 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
6655 );
6656}
6657
6658#[gpui::test]
6659async fn test_add_selection_above_below(cx: &mut TestAppContext) {
6660 init_test(cx, |_| {});
6661
6662 let mut cx = EditorTestContext::new(cx).await;
6663
6664 cx.set_state(indoc!(
6665 r#"abc
6666 defˇghi
6667
6668 jk
6669 nlmo
6670 "#
6671 ));
6672
6673 cx.update_editor(|editor, window, cx| {
6674 editor.add_selection_above(&Default::default(), window, cx);
6675 });
6676
6677 cx.assert_editor_state(indoc!(
6678 r#"abcˇ
6679 defˇghi
6680
6681 jk
6682 nlmo
6683 "#
6684 ));
6685
6686 cx.update_editor(|editor, window, cx| {
6687 editor.add_selection_above(&Default::default(), window, cx);
6688 });
6689
6690 cx.assert_editor_state(indoc!(
6691 r#"abcˇ
6692 defˇghi
6693
6694 jk
6695 nlmo
6696 "#
6697 ));
6698
6699 cx.update_editor(|editor, window, cx| {
6700 editor.add_selection_below(&Default::default(), window, cx);
6701 });
6702
6703 cx.assert_editor_state(indoc!(
6704 r#"abc
6705 defˇghi
6706
6707 jk
6708 nlmo
6709 "#
6710 ));
6711
6712 cx.update_editor(|editor, window, cx| {
6713 editor.undo_selection(&Default::default(), window, cx);
6714 });
6715
6716 cx.assert_editor_state(indoc!(
6717 r#"abcˇ
6718 defˇghi
6719
6720 jk
6721 nlmo
6722 "#
6723 ));
6724
6725 cx.update_editor(|editor, window, cx| {
6726 editor.redo_selection(&Default::default(), window, cx);
6727 });
6728
6729 cx.assert_editor_state(indoc!(
6730 r#"abc
6731 defˇghi
6732
6733 jk
6734 nlmo
6735 "#
6736 ));
6737
6738 cx.update_editor(|editor, window, cx| {
6739 editor.add_selection_below(&Default::default(), window, cx);
6740 });
6741
6742 cx.assert_editor_state(indoc!(
6743 r#"abc
6744 defˇghi
6745 ˇ
6746 jk
6747 nlmo
6748 "#
6749 ));
6750
6751 cx.update_editor(|editor, window, cx| {
6752 editor.add_selection_below(&Default::default(), window, cx);
6753 });
6754
6755 cx.assert_editor_state(indoc!(
6756 r#"abc
6757 defˇghi
6758 ˇ
6759 jkˇ
6760 nlmo
6761 "#
6762 ));
6763
6764 cx.update_editor(|editor, window, cx| {
6765 editor.add_selection_below(&Default::default(), window, cx);
6766 });
6767
6768 cx.assert_editor_state(indoc!(
6769 r#"abc
6770 defˇghi
6771 ˇ
6772 jkˇ
6773 nlmˇo
6774 "#
6775 ));
6776
6777 cx.update_editor(|editor, window, cx| {
6778 editor.add_selection_below(&Default::default(), window, cx);
6779 });
6780
6781 cx.assert_editor_state(indoc!(
6782 r#"abc
6783 defˇghi
6784 ˇ
6785 jkˇ
6786 nlmˇo
6787 ˇ"#
6788 ));
6789
6790 // change selections
6791 cx.set_state(indoc!(
6792 r#"abc
6793 def«ˇg»hi
6794
6795 jk
6796 nlmo
6797 "#
6798 ));
6799
6800 cx.update_editor(|editor, window, cx| {
6801 editor.add_selection_below(&Default::default(), window, cx);
6802 });
6803
6804 cx.assert_editor_state(indoc!(
6805 r#"abc
6806 def«ˇg»hi
6807
6808 jk
6809 nlm«ˇo»
6810 "#
6811 ));
6812
6813 cx.update_editor(|editor, window, cx| {
6814 editor.add_selection_below(&Default::default(), window, cx);
6815 });
6816
6817 cx.assert_editor_state(indoc!(
6818 r#"abc
6819 def«ˇg»hi
6820
6821 jk
6822 nlm«ˇo»
6823 "#
6824 ));
6825
6826 cx.update_editor(|editor, window, cx| {
6827 editor.add_selection_above(&Default::default(), window, cx);
6828 });
6829
6830 cx.assert_editor_state(indoc!(
6831 r#"abc
6832 def«ˇg»hi
6833
6834 jk
6835 nlmo
6836 "#
6837 ));
6838
6839 cx.update_editor(|editor, window, cx| {
6840 editor.add_selection_above(&Default::default(), window, cx);
6841 });
6842
6843 cx.assert_editor_state(indoc!(
6844 r#"abc
6845 def«ˇg»hi
6846
6847 jk
6848 nlmo
6849 "#
6850 ));
6851
6852 // Change selections again
6853 cx.set_state(indoc!(
6854 r#"a«bc
6855 defgˇ»hi
6856
6857 jk
6858 nlmo
6859 "#
6860 ));
6861
6862 cx.update_editor(|editor, window, cx| {
6863 editor.add_selection_below(&Default::default(), window, cx);
6864 });
6865
6866 cx.assert_editor_state(indoc!(
6867 r#"a«bcˇ»
6868 d«efgˇ»hi
6869
6870 j«kˇ»
6871 nlmo
6872 "#
6873 ));
6874
6875 cx.update_editor(|editor, window, cx| {
6876 editor.add_selection_below(&Default::default(), window, cx);
6877 });
6878 cx.assert_editor_state(indoc!(
6879 r#"a«bcˇ»
6880 d«efgˇ»hi
6881
6882 j«kˇ»
6883 n«lmoˇ»
6884 "#
6885 ));
6886 cx.update_editor(|editor, window, cx| {
6887 editor.add_selection_above(&Default::default(), window, cx);
6888 });
6889
6890 cx.assert_editor_state(indoc!(
6891 r#"a«bcˇ»
6892 d«efgˇ»hi
6893
6894 j«kˇ»
6895 nlmo
6896 "#
6897 ));
6898
6899 // Change selections again
6900 cx.set_state(indoc!(
6901 r#"abc
6902 d«ˇefghi
6903
6904 jk
6905 nlm»o
6906 "#
6907 ));
6908
6909 cx.update_editor(|editor, window, cx| {
6910 editor.add_selection_above(&Default::default(), window, cx);
6911 });
6912
6913 cx.assert_editor_state(indoc!(
6914 r#"a«ˇbc»
6915 d«ˇef»ghi
6916
6917 j«ˇk»
6918 n«ˇlm»o
6919 "#
6920 ));
6921
6922 cx.update_editor(|editor, window, cx| {
6923 editor.add_selection_below(&Default::default(), window, cx);
6924 });
6925
6926 cx.assert_editor_state(indoc!(
6927 r#"abc
6928 d«ˇef»ghi
6929
6930 j«ˇk»
6931 n«ˇlm»o
6932 "#
6933 ));
6934}
6935
6936#[gpui::test]
6937async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
6938 init_test(cx, |_| {});
6939 let mut cx = EditorTestContext::new(cx).await;
6940
6941 cx.set_state(indoc!(
6942 r#"line onˇe
6943 liˇne two
6944 line three
6945 line four"#
6946 ));
6947
6948 cx.update_editor(|editor, window, cx| {
6949 editor.add_selection_below(&Default::default(), window, cx);
6950 });
6951
6952 // test multiple cursors expand in the same direction
6953 cx.assert_editor_state(indoc!(
6954 r#"line onˇe
6955 liˇne twˇo
6956 liˇne three
6957 line four"#
6958 ));
6959
6960 cx.update_editor(|editor, window, cx| {
6961 editor.add_selection_below(&Default::default(), window, cx);
6962 });
6963
6964 cx.update_editor(|editor, window, cx| {
6965 editor.add_selection_below(&Default::default(), window, cx);
6966 });
6967
6968 // test multiple cursors expand below overflow
6969 cx.assert_editor_state(indoc!(
6970 r#"line onˇe
6971 liˇne twˇo
6972 liˇne thˇree
6973 liˇne foˇur"#
6974 ));
6975
6976 cx.update_editor(|editor, window, cx| {
6977 editor.add_selection_above(&Default::default(), window, cx);
6978 });
6979
6980 // test multiple cursors retrieves back correctly
6981 cx.assert_editor_state(indoc!(
6982 r#"line onˇe
6983 liˇne twˇo
6984 liˇne thˇree
6985 line four"#
6986 ));
6987
6988 cx.update_editor(|editor, window, cx| {
6989 editor.add_selection_above(&Default::default(), window, cx);
6990 });
6991
6992 cx.update_editor(|editor, window, cx| {
6993 editor.add_selection_above(&Default::default(), window, cx);
6994 });
6995
6996 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
6997 cx.assert_editor_state(indoc!(
6998 r#"liˇne onˇe
6999 liˇne two
7000 line three
7001 line four"#
7002 ));
7003
7004 cx.update_editor(|editor, window, cx| {
7005 editor.undo_selection(&Default::default(), window, cx);
7006 });
7007
7008 // test undo
7009 cx.assert_editor_state(indoc!(
7010 r#"line onˇe
7011 liˇne twˇo
7012 line three
7013 line four"#
7014 ));
7015
7016 cx.update_editor(|editor, window, cx| {
7017 editor.redo_selection(&Default::default(), window, cx);
7018 });
7019
7020 // test redo
7021 cx.assert_editor_state(indoc!(
7022 r#"liˇne onˇe
7023 liˇne two
7024 line three
7025 line four"#
7026 ));
7027
7028 cx.set_state(indoc!(
7029 r#"abcd
7030 ef«ghˇ»
7031 ijkl
7032 «mˇ»nop"#
7033 ));
7034
7035 cx.update_editor(|editor, window, cx| {
7036 editor.add_selection_above(&Default::default(), window, cx);
7037 });
7038
7039 // test multiple selections expand in the same direction
7040 cx.assert_editor_state(indoc!(
7041 r#"ab«cdˇ»
7042 ef«ghˇ»
7043 «iˇ»jkl
7044 «mˇ»nop"#
7045 ));
7046
7047 cx.update_editor(|editor, window, cx| {
7048 editor.add_selection_above(&Default::default(), window, cx);
7049 });
7050
7051 // test multiple selection upward overflow
7052 cx.assert_editor_state(indoc!(
7053 r#"ab«cdˇ»
7054 «eˇ»f«ghˇ»
7055 «iˇ»jkl
7056 «mˇ»nop"#
7057 ));
7058
7059 cx.update_editor(|editor, window, cx| {
7060 editor.add_selection_below(&Default::default(), window, cx);
7061 });
7062
7063 // test multiple selection retrieves back correctly
7064 cx.assert_editor_state(indoc!(
7065 r#"abcd
7066 ef«ghˇ»
7067 «iˇ»jkl
7068 «mˇ»nop"#
7069 ));
7070
7071 cx.update_editor(|editor, window, cx| {
7072 editor.add_selection_below(&Default::default(), window, cx);
7073 });
7074
7075 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
7076 cx.assert_editor_state(indoc!(
7077 r#"abcd
7078 ef«ghˇ»
7079 ij«klˇ»
7080 «mˇ»nop"#
7081 ));
7082
7083 cx.update_editor(|editor, window, cx| {
7084 editor.undo_selection(&Default::default(), window, cx);
7085 });
7086
7087 // test undo
7088 cx.assert_editor_state(indoc!(
7089 r#"abcd
7090 ef«ghˇ»
7091 «iˇ»jkl
7092 «mˇ»nop"#
7093 ));
7094
7095 cx.update_editor(|editor, window, cx| {
7096 editor.redo_selection(&Default::default(), window, cx);
7097 });
7098
7099 // test redo
7100 cx.assert_editor_state(indoc!(
7101 r#"abcd
7102 ef«ghˇ»
7103 ij«klˇ»
7104 «mˇ»nop"#
7105 ));
7106}
7107
7108#[gpui::test]
7109async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
7110 init_test(cx, |_| {});
7111 let mut cx = EditorTestContext::new(cx).await;
7112
7113 cx.set_state(indoc!(
7114 r#"line onˇe
7115 liˇne two
7116 line three
7117 line four"#
7118 ));
7119
7120 cx.update_editor(|editor, window, cx| {
7121 editor.add_selection_below(&Default::default(), window, cx);
7122 editor.add_selection_below(&Default::default(), window, cx);
7123 editor.add_selection_below(&Default::default(), window, cx);
7124 });
7125
7126 // initial state with two multi cursor groups
7127 cx.assert_editor_state(indoc!(
7128 r#"line onˇe
7129 liˇne twˇo
7130 liˇne thˇree
7131 liˇne foˇur"#
7132 ));
7133
7134 // add single cursor in middle - simulate opt click
7135 cx.update_editor(|editor, window, cx| {
7136 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
7137 editor.begin_selection(new_cursor_point, true, 1, window, cx);
7138 editor.end_selection(window, cx);
7139 });
7140
7141 cx.assert_editor_state(indoc!(
7142 r#"line onˇe
7143 liˇne twˇo
7144 liˇneˇ thˇree
7145 liˇne foˇur"#
7146 ));
7147
7148 cx.update_editor(|editor, window, cx| {
7149 editor.add_selection_above(&Default::default(), window, cx);
7150 });
7151
7152 // test new added selection expands above and existing selection shrinks
7153 cx.assert_editor_state(indoc!(
7154 r#"line onˇe
7155 liˇneˇ twˇo
7156 liˇneˇ thˇree
7157 line four"#
7158 ));
7159
7160 cx.update_editor(|editor, window, cx| {
7161 editor.add_selection_above(&Default::default(), window, cx);
7162 });
7163
7164 // test new added selection expands above and existing selection shrinks
7165 cx.assert_editor_state(indoc!(
7166 r#"lineˇ onˇe
7167 liˇneˇ twˇo
7168 lineˇ three
7169 line four"#
7170 ));
7171
7172 // intial state with two selection groups
7173 cx.set_state(indoc!(
7174 r#"abcd
7175 ef«ghˇ»
7176 ijkl
7177 «mˇ»nop"#
7178 ));
7179
7180 cx.update_editor(|editor, window, cx| {
7181 editor.add_selection_above(&Default::default(), window, cx);
7182 editor.add_selection_above(&Default::default(), window, cx);
7183 });
7184
7185 cx.assert_editor_state(indoc!(
7186 r#"ab«cdˇ»
7187 «eˇ»f«ghˇ»
7188 «iˇ»jkl
7189 «mˇ»nop"#
7190 ));
7191
7192 // add single selection in middle - simulate opt drag
7193 cx.update_editor(|editor, window, cx| {
7194 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
7195 editor.begin_selection(new_cursor_point, true, 1, window, cx);
7196 editor.update_selection(
7197 DisplayPoint::new(DisplayRow(2), 4),
7198 0,
7199 gpui::Point::<f32>::default(),
7200 window,
7201 cx,
7202 );
7203 editor.end_selection(window, cx);
7204 });
7205
7206 cx.assert_editor_state(indoc!(
7207 r#"ab«cdˇ»
7208 «eˇ»f«ghˇ»
7209 «iˇ»jk«lˇ»
7210 «mˇ»nop"#
7211 ));
7212
7213 cx.update_editor(|editor, window, cx| {
7214 editor.add_selection_below(&Default::default(), window, cx);
7215 });
7216
7217 // test new added selection expands below, others shrinks from above
7218 cx.assert_editor_state(indoc!(
7219 r#"abcd
7220 ef«ghˇ»
7221 «iˇ»jk«lˇ»
7222 «mˇ»no«pˇ»"#
7223 ));
7224}
7225
7226#[gpui::test]
7227async fn test_select_next(cx: &mut TestAppContext) {
7228 init_test(cx, |_| {});
7229
7230 let mut cx = EditorTestContext::new(cx).await;
7231 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
7232
7233 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7234 .unwrap();
7235 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7236
7237 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7238 .unwrap();
7239 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
7240
7241 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7242 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7243
7244 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7245 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
7246
7247 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7248 .unwrap();
7249 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7250
7251 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7252 .unwrap();
7253 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7254
7255 // Test selection direction should be preserved
7256 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
7257
7258 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7259 .unwrap();
7260 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
7261}
7262
7263#[gpui::test]
7264async fn test_select_all_matches(cx: &mut TestAppContext) {
7265 init_test(cx, |_| {});
7266
7267 let mut cx = EditorTestContext::new(cx).await;
7268
7269 // Test caret-only selections
7270 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
7271 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7272 .unwrap();
7273 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7274
7275 // Test left-to-right selections
7276 cx.set_state("abc\n«abcˇ»\nabc");
7277 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7278 .unwrap();
7279 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
7280
7281 // Test right-to-left selections
7282 cx.set_state("abc\n«ˇabc»\nabc");
7283 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7284 .unwrap();
7285 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
7286
7287 // Test selecting whitespace with caret selection
7288 cx.set_state("abc\nˇ abc\nabc");
7289 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7290 .unwrap();
7291 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
7292
7293 // Test selecting whitespace with left-to-right selection
7294 cx.set_state("abc\n«ˇ »abc\nabc");
7295 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7296 .unwrap();
7297 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
7298
7299 // Test no matches with right-to-left selection
7300 cx.set_state("abc\n« ˇ»abc\nabc");
7301 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7302 .unwrap();
7303 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
7304
7305 // Test with a single word and clip_at_line_ends=true (#29823)
7306 cx.set_state("aˇbc");
7307 cx.update_editor(|e, window, cx| {
7308 e.set_clip_at_line_ends(true, cx);
7309 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
7310 e.set_clip_at_line_ends(false, cx);
7311 });
7312 cx.assert_editor_state("«abcˇ»");
7313}
7314
7315#[gpui::test]
7316async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
7317 init_test(cx, |_| {});
7318
7319 let mut cx = EditorTestContext::new(cx).await;
7320
7321 let large_body_1 = "\nd".repeat(200);
7322 let large_body_2 = "\ne".repeat(200);
7323
7324 cx.set_state(&format!(
7325 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
7326 ));
7327 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
7328 let scroll_position = editor.scroll_position(cx);
7329 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
7330 scroll_position
7331 });
7332
7333 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7334 .unwrap();
7335 cx.assert_editor_state(&format!(
7336 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
7337 ));
7338 let scroll_position_after_selection =
7339 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
7340 assert_eq!(
7341 initial_scroll_position, scroll_position_after_selection,
7342 "Scroll position should not change after selecting all matches"
7343 );
7344}
7345
7346#[gpui::test]
7347async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
7348 init_test(cx, |_| {});
7349
7350 let mut cx = EditorLspTestContext::new_rust(
7351 lsp::ServerCapabilities {
7352 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7353 ..Default::default()
7354 },
7355 cx,
7356 )
7357 .await;
7358
7359 cx.set_state(indoc! {"
7360 line 1
7361 line 2
7362 linˇe 3
7363 line 4
7364 line 5
7365 "});
7366
7367 // Make an edit
7368 cx.update_editor(|editor, window, cx| {
7369 editor.handle_input("X", window, cx);
7370 });
7371
7372 // Move cursor to a different position
7373 cx.update_editor(|editor, window, cx| {
7374 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7375 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
7376 });
7377 });
7378
7379 cx.assert_editor_state(indoc! {"
7380 line 1
7381 line 2
7382 linXe 3
7383 line 4
7384 liˇne 5
7385 "});
7386
7387 cx.lsp
7388 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
7389 Ok(Some(vec![lsp::TextEdit::new(
7390 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
7391 "PREFIX ".to_string(),
7392 )]))
7393 });
7394
7395 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
7396 .unwrap()
7397 .await
7398 .unwrap();
7399
7400 cx.assert_editor_state(indoc! {"
7401 PREFIX line 1
7402 line 2
7403 linXe 3
7404 line 4
7405 liˇne 5
7406 "});
7407
7408 // Undo formatting
7409 cx.update_editor(|editor, window, cx| {
7410 editor.undo(&Default::default(), window, cx);
7411 });
7412
7413 // Verify cursor moved back to position after edit
7414 cx.assert_editor_state(indoc! {"
7415 line 1
7416 line 2
7417 linXˇe 3
7418 line 4
7419 line 5
7420 "});
7421}
7422
7423#[gpui::test]
7424async fn test_undo_edit_prediction_scrolls_to_edit_pos(cx: &mut TestAppContext) {
7425 init_test(cx, |_| {});
7426
7427 let mut cx = EditorTestContext::new(cx).await;
7428
7429 let provider = cx.new(|_| FakeEditPredictionProvider::default());
7430 cx.update_editor(|editor, window, cx| {
7431 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
7432 });
7433
7434 cx.set_state(indoc! {"
7435 line 1
7436 line 2
7437 linˇe 3
7438 line 4
7439 line 5
7440 line 6
7441 line 7
7442 line 8
7443 line 9
7444 line 10
7445 "});
7446
7447 let snapshot = cx.buffer_snapshot();
7448 let edit_position = snapshot.anchor_after(Point::new(2, 4));
7449
7450 cx.update(|_, cx| {
7451 provider.update(cx, |provider, _| {
7452 provider.set_edit_prediction(Some(edit_prediction::EditPrediction {
7453 id: None,
7454 edits: vec![(edit_position..edit_position, "X".into())],
7455 edit_preview: None,
7456 }))
7457 })
7458 });
7459
7460 cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
7461 cx.update_editor(|editor, window, cx| {
7462 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
7463 });
7464
7465 cx.assert_editor_state(indoc! {"
7466 line 1
7467 line 2
7468 lineXˇ 3
7469 line 4
7470 line 5
7471 line 6
7472 line 7
7473 line 8
7474 line 9
7475 line 10
7476 "});
7477
7478 cx.update_editor(|editor, window, cx| {
7479 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7480 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
7481 });
7482 });
7483
7484 cx.assert_editor_state(indoc! {"
7485 line 1
7486 line 2
7487 lineX 3
7488 line 4
7489 line 5
7490 line 6
7491 line 7
7492 line 8
7493 line 9
7494 liˇne 10
7495 "});
7496
7497 cx.update_editor(|editor, window, cx| {
7498 editor.undo(&Default::default(), window, cx);
7499 });
7500
7501 cx.assert_editor_state(indoc! {"
7502 line 1
7503 line 2
7504 lineˇ 3
7505 line 4
7506 line 5
7507 line 6
7508 line 7
7509 line 8
7510 line 9
7511 line 10
7512 "});
7513}
7514
7515#[gpui::test]
7516async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
7517 init_test(cx, |_| {});
7518
7519 let mut cx = EditorTestContext::new(cx).await;
7520 cx.set_state(
7521 r#"let foo = 2;
7522lˇet foo = 2;
7523let fooˇ = 2;
7524let foo = 2;
7525let foo = ˇ2;"#,
7526 );
7527
7528 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7529 .unwrap();
7530 cx.assert_editor_state(
7531 r#"let foo = 2;
7532«letˇ» foo = 2;
7533let «fooˇ» = 2;
7534let foo = 2;
7535let foo = «2ˇ»;"#,
7536 );
7537
7538 // noop for multiple selections with different contents
7539 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7540 .unwrap();
7541 cx.assert_editor_state(
7542 r#"let foo = 2;
7543«letˇ» foo = 2;
7544let «fooˇ» = 2;
7545let foo = 2;
7546let foo = «2ˇ»;"#,
7547 );
7548
7549 // Test last selection direction should be preserved
7550 cx.set_state(
7551 r#"let foo = 2;
7552let foo = 2;
7553let «fooˇ» = 2;
7554let «ˇfoo» = 2;
7555let foo = 2;"#,
7556 );
7557
7558 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7559 .unwrap();
7560 cx.assert_editor_state(
7561 r#"let foo = 2;
7562let foo = 2;
7563let «fooˇ» = 2;
7564let «ˇfoo» = 2;
7565let «ˇfoo» = 2;"#,
7566 );
7567}
7568
7569#[gpui::test]
7570async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
7571 init_test(cx, |_| {});
7572
7573 let mut cx =
7574 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
7575
7576 cx.assert_editor_state(indoc! {"
7577 ˇbbb
7578 ccc
7579
7580 bbb
7581 ccc
7582 "});
7583 cx.dispatch_action(SelectPrevious::default());
7584 cx.assert_editor_state(indoc! {"
7585 «bbbˇ»
7586 ccc
7587
7588 bbb
7589 ccc
7590 "});
7591 cx.dispatch_action(SelectPrevious::default());
7592 cx.assert_editor_state(indoc! {"
7593 «bbbˇ»
7594 ccc
7595
7596 «bbbˇ»
7597 ccc
7598 "});
7599}
7600
7601#[gpui::test]
7602async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
7603 init_test(cx, |_| {});
7604
7605 let mut cx = EditorTestContext::new(cx).await;
7606 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
7607
7608 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7609 .unwrap();
7610 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7611
7612 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7613 .unwrap();
7614 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
7615
7616 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7617 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7618
7619 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7620 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
7621
7622 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7623 .unwrap();
7624 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
7625
7626 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7627 .unwrap();
7628 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7629}
7630
7631#[gpui::test]
7632async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
7633 init_test(cx, |_| {});
7634
7635 let mut cx = EditorTestContext::new(cx).await;
7636 cx.set_state("aˇ");
7637
7638 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7639 .unwrap();
7640 cx.assert_editor_state("«aˇ»");
7641 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7642 .unwrap();
7643 cx.assert_editor_state("«aˇ»");
7644}
7645
7646#[gpui::test]
7647async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
7648 init_test(cx, |_| {});
7649
7650 let mut cx = EditorTestContext::new(cx).await;
7651 cx.set_state(
7652 r#"let foo = 2;
7653lˇet foo = 2;
7654let fooˇ = 2;
7655let foo = 2;
7656let foo = ˇ2;"#,
7657 );
7658
7659 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7660 .unwrap();
7661 cx.assert_editor_state(
7662 r#"let foo = 2;
7663«letˇ» foo = 2;
7664let «fooˇ» = 2;
7665let foo = 2;
7666let foo = «2ˇ»;"#,
7667 );
7668
7669 // noop for multiple selections with different contents
7670 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7671 .unwrap();
7672 cx.assert_editor_state(
7673 r#"let foo = 2;
7674«letˇ» foo = 2;
7675let «fooˇ» = 2;
7676let foo = 2;
7677let foo = «2ˇ»;"#,
7678 );
7679}
7680
7681#[gpui::test]
7682async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
7683 init_test(cx, |_| {});
7684
7685 let mut cx = EditorTestContext::new(cx).await;
7686 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
7687
7688 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7689 .unwrap();
7690 // selection direction is preserved
7691 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
7692
7693 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7694 .unwrap();
7695 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
7696
7697 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7698 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
7699
7700 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7701 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
7702
7703 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7704 .unwrap();
7705 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
7706
7707 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7708 .unwrap();
7709 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
7710}
7711
7712#[gpui::test]
7713async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
7714 init_test(cx, |_| {});
7715
7716 let language = Arc::new(Language::new(
7717 LanguageConfig::default(),
7718 Some(tree_sitter_rust::LANGUAGE.into()),
7719 ));
7720
7721 let text = r#"
7722 use mod1::mod2::{mod3, mod4};
7723
7724 fn fn_1(param1: bool, param2: &str) {
7725 let var1 = "text";
7726 }
7727 "#
7728 .unindent();
7729
7730 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7731 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7732 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7733
7734 editor
7735 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7736 .await;
7737
7738 editor.update_in(cx, |editor, window, cx| {
7739 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7740 s.select_display_ranges([
7741 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
7742 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
7743 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
7744 ]);
7745 });
7746 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7747 });
7748 editor.update(cx, |editor, cx| {
7749 assert_text_with_selections(
7750 editor,
7751 indoc! {r#"
7752 use mod1::mod2::{mod3, «mod4ˇ»};
7753
7754 fn fn_1«ˇ(param1: bool, param2: &str)» {
7755 let var1 = "«ˇtext»";
7756 }
7757 "#},
7758 cx,
7759 );
7760 });
7761
7762 editor.update_in(cx, |editor, window, cx| {
7763 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7764 });
7765 editor.update(cx, |editor, cx| {
7766 assert_text_with_selections(
7767 editor,
7768 indoc! {r#"
7769 use mod1::mod2::«{mod3, mod4}ˇ»;
7770
7771 «ˇfn fn_1(param1: bool, param2: &str) {
7772 let var1 = "text";
7773 }»
7774 "#},
7775 cx,
7776 );
7777 });
7778
7779 editor.update_in(cx, |editor, window, cx| {
7780 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7781 });
7782 assert_eq!(
7783 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
7784 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
7785 );
7786
7787 // Trying to expand the selected syntax node one more time has no effect.
7788 editor.update_in(cx, |editor, window, cx| {
7789 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7790 });
7791 assert_eq!(
7792 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
7793 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
7794 );
7795
7796 editor.update_in(cx, |editor, window, cx| {
7797 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7798 });
7799 editor.update(cx, |editor, cx| {
7800 assert_text_with_selections(
7801 editor,
7802 indoc! {r#"
7803 use mod1::mod2::«{mod3, mod4}ˇ»;
7804
7805 «ˇfn fn_1(param1: bool, param2: &str) {
7806 let var1 = "text";
7807 }»
7808 "#},
7809 cx,
7810 );
7811 });
7812
7813 editor.update_in(cx, |editor, window, cx| {
7814 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7815 });
7816 editor.update(cx, |editor, cx| {
7817 assert_text_with_selections(
7818 editor,
7819 indoc! {r#"
7820 use mod1::mod2::{mod3, «mod4ˇ»};
7821
7822 fn fn_1«ˇ(param1: bool, param2: &str)» {
7823 let var1 = "«ˇtext»";
7824 }
7825 "#},
7826 cx,
7827 );
7828 });
7829
7830 editor.update_in(cx, |editor, window, cx| {
7831 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7832 });
7833 editor.update(cx, |editor, cx| {
7834 assert_text_with_selections(
7835 editor,
7836 indoc! {r#"
7837 use mod1::mod2::{mod3, mo«ˇ»d4};
7838
7839 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
7840 let var1 = "te«ˇ»xt";
7841 }
7842 "#},
7843 cx,
7844 );
7845 });
7846
7847 // Trying to shrink the selected syntax node one more time has no effect.
7848 editor.update_in(cx, |editor, window, cx| {
7849 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7850 });
7851 editor.update_in(cx, |editor, _, cx| {
7852 assert_text_with_selections(
7853 editor,
7854 indoc! {r#"
7855 use mod1::mod2::{mod3, mo«ˇ»d4};
7856
7857 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
7858 let var1 = "te«ˇ»xt";
7859 }
7860 "#},
7861 cx,
7862 );
7863 });
7864
7865 // Ensure that we keep expanding the selection if the larger selection starts or ends within
7866 // a fold.
7867 editor.update_in(cx, |editor, window, cx| {
7868 editor.fold_creases(
7869 vec![
7870 Crease::simple(
7871 Point::new(0, 21)..Point::new(0, 24),
7872 FoldPlaceholder::test(),
7873 ),
7874 Crease::simple(
7875 Point::new(3, 20)..Point::new(3, 22),
7876 FoldPlaceholder::test(),
7877 ),
7878 ],
7879 true,
7880 window,
7881 cx,
7882 );
7883 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7884 });
7885 editor.update(cx, |editor, cx| {
7886 assert_text_with_selections(
7887 editor,
7888 indoc! {r#"
7889 use mod1::mod2::«{mod3, mod4}ˇ»;
7890
7891 fn fn_1«ˇ(param1: bool, param2: &str)» {
7892 let var1 = "«ˇtext»";
7893 }
7894 "#},
7895 cx,
7896 );
7897 });
7898}
7899
7900#[gpui::test]
7901async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
7902 init_test(cx, |_| {});
7903
7904 let language = Arc::new(Language::new(
7905 LanguageConfig::default(),
7906 Some(tree_sitter_rust::LANGUAGE.into()),
7907 ));
7908
7909 let text = "let a = 2;";
7910
7911 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7912 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7913 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7914
7915 editor
7916 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7917 .await;
7918
7919 // Test case 1: Cursor at end of word
7920 editor.update_in(cx, |editor, window, cx| {
7921 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7922 s.select_display_ranges([
7923 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
7924 ]);
7925 });
7926 });
7927 editor.update(cx, |editor, cx| {
7928 assert_text_with_selections(editor, "let aˇ = 2;", cx);
7929 });
7930 editor.update_in(cx, |editor, window, cx| {
7931 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7932 });
7933 editor.update(cx, |editor, cx| {
7934 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
7935 });
7936 editor.update_in(cx, |editor, window, cx| {
7937 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7938 });
7939 editor.update(cx, |editor, cx| {
7940 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
7941 });
7942
7943 // Test case 2: Cursor at end of statement
7944 editor.update_in(cx, |editor, window, cx| {
7945 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7946 s.select_display_ranges([
7947 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
7948 ]);
7949 });
7950 });
7951 editor.update(cx, |editor, cx| {
7952 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
7953 });
7954 editor.update_in(cx, |editor, window, cx| {
7955 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7956 });
7957 editor.update(cx, |editor, cx| {
7958 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
7959 });
7960}
7961
7962#[gpui::test]
7963async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
7964 init_test(cx, |_| {});
7965
7966 let language = Arc::new(Language::new(
7967 LanguageConfig::default(),
7968 Some(tree_sitter_rust::LANGUAGE.into()),
7969 ));
7970
7971 let text = r#"
7972 use mod1::mod2::{mod3, mod4};
7973
7974 fn fn_1(param1: bool, param2: &str) {
7975 let var1 = "hello world";
7976 }
7977 "#
7978 .unindent();
7979
7980 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7981 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7982 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7983
7984 editor
7985 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7986 .await;
7987
7988 // Test 1: Cursor on a letter of a string word
7989 editor.update_in(cx, |editor, window, cx| {
7990 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7991 s.select_display_ranges([
7992 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
7993 ]);
7994 });
7995 });
7996 editor.update_in(cx, |editor, window, cx| {
7997 assert_text_with_selections(
7998 editor,
7999 indoc! {r#"
8000 use mod1::mod2::{mod3, mod4};
8001
8002 fn fn_1(param1: bool, param2: &str) {
8003 let var1 = "hˇello world";
8004 }
8005 "#},
8006 cx,
8007 );
8008 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8009 assert_text_with_selections(
8010 editor,
8011 indoc! {r#"
8012 use mod1::mod2::{mod3, mod4};
8013
8014 fn fn_1(param1: bool, param2: &str) {
8015 let var1 = "«ˇhello» world";
8016 }
8017 "#},
8018 cx,
8019 );
8020 });
8021
8022 // Test 2: Partial selection within a word
8023 editor.update_in(cx, |editor, window, cx| {
8024 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8025 s.select_display_ranges([
8026 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
8027 ]);
8028 });
8029 });
8030 editor.update_in(cx, |editor, window, cx| {
8031 assert_text_with_selections(
8032 editor,
8033 indoc! {r#"
8034 use mod1::mod2::{mod3, mod4};
8035
8036 fn fn_1(param1: bool, param2: &str) {
8037 let var1 = "h«elˇ»lo world";
8038 }
8039 "#},
8040 cx,
8041 );
8042 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8043 assert_text_with_selections(
8044 editor,
8045 indoc! {r#"
8046 use mod1::mod2::{mod3, mod4};
8047
8048 fn fn_1(param1: bool, param2: &str) {
8049 let var1 = "«ˇhello» world";
8050 }
8051 "#},
8052 cx,
8053 );
8054 });
8055
8056 // Test 3: Complete word already selected
8057 editor.update_in(cx, |editor, window, cx| {
8058 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8059 s.select_display_ranges([
8060 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
8061 ]);
8062 });
8063 });
8064 editor.update_in(cx, |editor, window, cx| {
8065 assert_text_with_selections(
8066 editor,
8067 indoc! {r#"
8068 use mod1::mod2::{mod3, mod4};
8069
8070 fn fn_1(param1: bool, param2: &str) {
8071 let var1 = "«helloˇ» world";
8072 }
8073 "#},
8074 cx,
8075 );
8076 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8077 assert_text_with_selections(
8078 editor,
8079 indoc! {r#"
8080 use mod1::mod2::{mod3, mod4};
8081
8082 fn fn_1(param1: bool, param2: &str) {
8083 let var1 = "«hello worldˇ»";
8084 }
8085 "#},
8086 cx,
8087 );
8088 });
8089
8090 // Test 4: Selection spanning across words
8091 editor.update_in(cx, |editor, window, cx| {
8092 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8093 s.select_display_ranges([
8094 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
8095 ]);
8096 });
8097 });
8098 editor.update_in(cx, |editor, window, cx| {
8099 assert_text_with_selections(
8100 editor,
8101 indoc! {r#"
8102 use mod1::mod2::{mod3, mod4};
8103
8104 fn fn_1(param1: bool, param2: &str) {
8105 let var1 = "hel«lo woˇ»rld";
8106 }
8107 "#},
8108 cx,
8109 );
8110 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8111 assert_text_with_selections(
8112 editor,
8113 indoc! {r#"
8114 use mod1::mod2::{mod3, mod4};
8115
8116 fn fn_1(param1: bool, param2: &str) {
8117 let var1 = "«ˇhello world»";
8118 }
8119 "#},
8120 cx,
8121 );
8122 });
8123
8124 // Test 5: Expansion beyond string
8125 editor.update_in(cx, |editor, window, cx| {
8126 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8127 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8128 assert_text_with_selections(
8129 editor,
8130 indoc! {r#"
8131 use mod1::mod2::{mod3, mod4};
8132
8133 fn fn_1(param1: bool, param2: &str) {
8134 «ˇlet var1 = "hello world";»
8135 }
8136 "#},
8137 cx,
8138 );
8139 });
8140}
8141
8142#[gpui::test]
8143async fn test_unwrap_syntax_nodes(cx: &mut gpui::TestAppContext) {
8144 init_test(cx, |_| {});
8145
8146 let mut cx = EditorTestContext::new(cx).await;
8147
8148 let language = Arc::new(Language::new(
8149 LanguageConfig::default(),
8150 Some(tree_sitter_rust::LANGUAGE.into()),
8151 ));
8152
8153 cx.update_buffer(|buffer, cx| {
8154 buffer.set_language(Some(language), cx);
8155 });
8156
8157 cx.set_state(indoc! { r#"use mod1::{mod2::{«mod3ˇ», mod4}, mod5::{mod6, «mod7ˇ»}};"# });
8158 cx.update_editor(|editor, window, cx| {
8159 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
8160 });
8161
8162 cx.assert_editor_state(indoc! { r#"use mod1::{mod2::«mod3ˇ», mod5::«mod7ˇ»};"# });
8163}
8164
8165#[gpui::test]
8166async fn test_fold_function_bodies(cx: &mut TestAppContext) {
8167 init_test(cx, |_| {});
8168
8169 let base_text = r#"
8170 impl A {
8171 // this is an uncommitted comment
8172
8173 fn b() {
8174 c();
8175 }
8176
8177 // this is another uncommitted comment
8178
8179 fn d() {
8180 // e
8181 // f
8182 }
8183 }
8184
8185 fn g() {
8186 // h
8187 }
8188 "#
8189 .unindent();
8190
8191 let text = r#"
8192 ˇimpl A {
8193
8194 fn b() {
8195 c();
8196 }
8197
8198 fn d() {
8199 // e
8200 // f
8201 }
8202 }
8203
8204 fn g() {
8205 // h
8206 }
8207 "#
8208 .unindent();
8209
8210 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
8211 cx.set_state(&text);
8212 cx.set_head_text(&base_text);
8213 cx.update_editor(|editor, window, cx| {
8214 editor.expand_all_diff_hunks(&Default::default(), window, cx);
8215 });
8216
8217 cx.assert_state_with_diff(
8218 "
8219 ˇimpl A {
8220 - // this is an uncommitted comment
8221
8222 fn b() {
8223 c();
8224 }
8225
8226 - // this is another uncommitted comment
8227 -
8228 fn d() {
8229 // e
8230 // f
8231 }
8232 }
8233
8234 fn g() {
8235 // h
8236 }
8237 "
8238 .unindent(),
8239 );
8240
8241 let expected_display_text = "
8242 impl A {
8243 // this is an uncommitted comment
8244
8245 fn b() {
8246 ⋯
8247 }
8248
8249 // this is another uncommitted comment
8250
8251 fn d() {
8252 ⋯
8253 }
8254 }
8255
8256 fn g() {
8257 ⋯
8258 }
8259 "
8260 .unindent();
8261
8262 cx.update_editor(|editor, window, cx| {
8263 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
8264 assert_eq!(editor.display_text(cx), expected_display_text);
8265 });
8266}
8267
8268#[gpui::test]
8269async fn test_autoindent(cx: &mut TestAppContext) {
8270 init_test(cx, |_| {});
8271
8272 let language = Arc::new(
8273 Language::new(
8274 LanguageConfig {
8275 brackets: BracketPairConfig {
8276 pairs: vec![
8277 BracketPair {
8278 start: "{".to_string(),
8279 end: "}".to_string(),
8280 close: false,
8281 surround: false,
8282 newline: true,
8283 },
8284 BracketPair {
8285 start: "(".to_string(),
8286 end: ")".to_string(),
8287 close: false,
8288 surround: false,
8289 newline: true,
8290 },
8291 ],
8292 ..Default::default()
8293 },
8294 ..Default::default()
8295 },
8296 Some(tree_sitter_rust::LANGUAGE.into()),
8297 )
8298 .with_indents_query(
8299 r#"
8300 (_ "(" ")" @end) @indent
8301 (_ "{" "}" @end) @indent
8302 "#,
8303 )
8304 .unwrap(),
8305 );
8306
8307 let text = "fn a() {}";
8308
8309 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8310 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8311 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8312 editor
8313 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8314 .await;
8315
8316 editor.update_in(cx, |editor, window, cx| {
8317 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8318 s.select_ranges([5..5, 8..8, 9..9])
8319 });
8320 editor.newline(&Newline, window, cx);
8321 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
8322 assert_eq!(
8323 editor.selections.ranges(cx),
8324 &[
8325 Point::new(1, 4)..Point::new(1, 4),
8326 Point::new(3, 4)..Point::new(3, 4),
8327 Point::new(5, 0)..Point::new(5, 0)
8328 ]
8329 );
8330 });
8331}
8332
8333#[gpui::test]
8334async fn test_autoindent_disabled(cx: &mut TestAppContext) {
8335 init_test(cx, |settings| settings.defaults.auto_indent = Some(false));
8336
8337 let language = Arc::new(
8338 Language::new(
8339 LanguageConfig {
8340 brackets: BracketPairConfig {
8341 pairs: vec![
8342 BracketPair {
8343 start: "{".to_string(),
8344 end: "}".to_string(),
8345 close: false,
8346 surround: false,
8347 newline: true,
8348 },
8349 BracketPair {
8350 start: "(".to_string(),
8351 end: ")".to_string(),
8352 close: false,
8353 surround: false,
8354 newline: true,
8355 },
8356 ],
8357 ..Default::default()
8358 },
8359 ..Default::default()
8360 },
8361 Some(tree_sitter_rust::LANGUAGE.into()),
8362 )
8363 .with_indents_query(
8364 r#"
8365 (_ "(" ")" @end) @indent
8366 (_ "{" "}" @end) @indent
8367 "#,
8368 )
8369 .unwrap(),
8370 );
8371
8372 let text = "fn a() {}";
8373
8374 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8375 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8376 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8377 editor
8378 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8379 .await;
8380
8381 editor.update_in(cx, |editor, window, cx| {
8382 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8383 s.select_ranges([5..5, 8..8, 9..9])
8384 });
8385 editor.newline(&Newline, window, cx);
8386 assert_eq!(
8387 editor.text(cx),
8388 indoc!(
8389 "
8390 fn a(
8391
8392 ) {
8393
8394 }
8395 "
8396 )
8397 );
8398 assert_eq!(
8399 editor.selections.ranges(cx),
8400 &[
8401 Point::new(1, 0)..Point::new(1, 0),
8402 Point::new(3, 0)..Point::new(3, 0),
8403 Point::new(5, 0)..Point::new(5, 0)
8404 ]
8405 );
8406 });
8407}
8408
8409#[gpui::test]
8410async fn test_autoindent_disabled_with_nested_language(cx: &mut TestAppContext) {
8411 init_test(cx, |settings| {
8412 settings.defaults.auto_indent = Some(true);
8413 settings.languages.0.insert(
8414 "python".into(),
8415 LanguageSettingsContent {
8416 auto_indent: Some(false),
8417 ..Default::default()
8418 },
8419 );
8420 });
8421
8422 let mut cx = EditorTestContext::new(cx).await;
8423
8424 let injected_language = Arc::new(
8425 Language::new(
8426 LanguageConfig {
8427 brackets: BracketPairConfig {
8428 pairs: vec![
8429 BracketPair {
8430 start: "{".to_string(),
8431 end: "}".to_string(),
8432 close: false,
8433 surround: false,
8434 newline: true,
8435 },
8436 BracketPair {
8437 start: "(".to_string(),
8438 end: ")".to_string(),
8439 close: true,
8440 surround: false,
8441 newline: true,
8442 },
8443 ],
8444 ..Default::default()
8445 },
8446 name: "python".into(),
8447 ..Default::default()
8448 },
8449 Some(tree_sitter_python::LANGUAGE.into()),
8450 )
8451 .with_indents_query(
8452 r#"
8453 (_ "(" ")" @end) @indent
8454 (_ "{" "}" @end) @indent
8455 "#,
8456 )
8457 .unwrap(),
8458 );
8459
8460 let language = Arc::new(
8461 Language::new(
8462 LanguageConfig {
8463 brackets: BracketPairConfig {
8464 pairs: vec![
8465 BracketPair {
8466 start: "{".to_string(),
8467 end: "}".to_string(),
8468 close: false,
8469 surround: false,
8470 newline: true,
8471 },
8472 BracketPair {
8473 start: "(".to_string(),
8474 end: ")".to_string(),
8475 close: true,
8476 surround: false,
8477 newline: true,
8478 },
8479 ],
8480 ..Default::default()
8481 },
8482 name: LanguageName::new("rust"),
8483 ..Default::default()
8484 },
8485 Some(tree_sitter_rust::LANGUAGE.into()),
8486 )
8487 .with_indents_query(
8488 r#"
8489 (_ "(" ")" @end) @indent
8490 (_ "{" "}" @end) @indent
8491 "#,
8492 )
8493 .unwrap()
8494 .with_injection_query(
8495 r#"
8496 (macro_invocation
8497 macro: (identifier) @_macro_name
8498 (token_tree) @injection.content
8499 (#set! injection.language "python"))
8500 "#,
8501 )
8502 .unwrap(),
8503 );
8504
8505 cx.language_registry().add(injected_language);
8506 cx.language_registry().add(language.clone());
8507
8508 cx.update_buffer(|buffer, cx| {
8509 buffer.set_language(Some(language), cx);
8510 });
8511
8512 cx.set_state(r#"struct A {ˇ}"#);
8513
8514 cx.update_editor(|editor, window, cx| {
8515 editor.newline(&Default::default(), window, cx);
8516 });
8517
8518 cx.assert_editor_state(indoc!(
8519 "struct A {
8520 ˇ
8521 }"
8522 ));
8523
8524 cx.set_state(r#"select_biased!(ˇ)"#);
8525
8526 cx.update_editor(|editor, window, cx| {
8527 editor.newline(&Default::default(), window, cx);
8528 editor.handle_input("def ", window, cx);
8529 editor.handle_input("(", window, cx);
8530 editor.newline(&Default::default(), window, cx);
8531 editor.handle_input("a", window, cx);
8532 });
8533
8534 cx.assert_editor_state(indoc!(
8535 "select_biased!(
8536 def (
8537 aˇ
8538 )
8539 )"
8540 ));
8541}
8542
8543#[gpui::test]
8544async fn test_autoindent_selections(cx: &mut TestAppContext) {
8545 init_test(cx, |_| {});
8546
8547 {
8548 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
8549 cx.set_state(indoc! {"
8550 impl A {
8551
8552 fn b() {}
8553
8554 «fn c() {
8555
8556 }ˇ»
8557 }
8558 "});
8559
8560 cx.update_editor(|editor, window, cx| {
8561 editor.autoindent(&Default::default(), window, cx);
8562 });
8563
8564 cx.assert_editor_state(indoc! {"
8565 impl A {
8566
8567 fn b() {}
8568
8569 «fn c() {
8570
8571 }ˇ»
8572 }
8573 "});
8574 }
8575
8576 {
8577 let mut cx = EditorTestContext::new_multibuffer(
8578 cx,
8579 [indoc! { "
8580 impl A {
8581 «
8582 // a
8583 fn b(){}
8584 »
8585 «
8586 }
8587 fn c(){}
8588 »
8589 "}],
8590 );
8591
8592 let buffer = cx.update_editor(|editor, _, cx| {
8593 let buffer = editor.buffer().update(cx, |buffer, _| {
8594 buffer.all_buffers().iter().next().unwrap().clone()
8595 });
8596 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
8597 buffer
8598 });
8599
8600 cx.run_until_parked();
8601 cx.update_editor(|editor, window, cx| {
8602 editor.select_all(&Default::default(), window, cx);
8603 editor.autoindent(&Default::default(), window, cx)
8604 });
8605 cx.run_until_parked();
8606
8607 cx.update(|_, cx| {
8608 assert_eq!(
8609 buffer.read(cx).text(),
8610 indoc! { "
8611 impl A {
8612
8613 // a
8614 fn b(){}
8615
8616
8617 }
8618 fn c(){}
8619
8620 " }
8621 )
8622 });
8623 }
8624}
8625
8626#[gpui::test]
8627async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
8628 init_test(cx, |_| {});
8629
8630 let mut cx = EditorTestContext::new(cx).await;
8631
8632 let language = Arc::new(Language::new(
8633 LanguageConfig {
8634 brackets: BracketPairConfig {
8635 pairs: vec![
8636 BracketPair {
8637 start: "{".to_string(),
8638 end: "}".to_string(),
8639 close: true,
8640 surround: true,
8641 newline: true,
8642 },
8643 BracketPair {
8644 start: "(".to_string(),
8645 end: ")".to_string(),
8646 close: true,
8647 surround: true,
8648 newline: true,
8649 },
8650 BracketPair {
8651 start: "/*".to_string(),
8652 end: " */".to_string(),
8653 close: true,
8654 surround: true,
8655 newline: true,
8656 },
8657 BracketPair {
8658 start: "[".to_string(),
8659 end: "]".to_string(),
8660 close: false,
8661 surround: false,
8662 newline: true,
8663 },
8664 BracketPair {
8665 start: "\"".to_string(),
8666 end: "\"".to_string(),
8667 close: true,
8668 surround: true,
8669 newline: false,
8670 },
8671 BracketPair {
8672 start: "<".to_string(),
8673 end: ">".to_string(),
8674 close: false,
8675 surround: true,
8676 newline: true,
8677 },
8678 ],
8679 ..Default::default()
8680 },
8681 autoclose_before: "})]".to_string(),
8682 ..Default::default()
8683 },
8684 Some(tree_sitter_rust::LANGUAGE.into()),
8685 ));
8686
8687 cx.language_registry().add(language.clone());
8688 cx.update_buffer(|buffer, cx| {
8689 buffer.set_language(Some(language), cx);
8690 });
8691
8692 cx.set_state(
8693 &r#"
8694 🏀ˇ
8695 εˇ
8696 ❤️ˇ
8697 "#
8698 .unindent(),
8699 );
8700
8701 // autoclose multiple nested brackets at multiple cursors
8702 cx.update_editor(|editor, window, cx| {
8703 editor.handle_input("{", window, cx);
8704 editor.handle_input("{", window, cx);
8705 editor.handle_input("{", window, cx);
8706 });
8707 cx.assert_editor_state(
8708 &"
8709 🏀{{{ˇ}}}
8710 ε{{{ˇ}}}
8711 ❤️{{{ˇ}}}
8712 "
8713 .unindent(),
8714 );
8715
8716 // insert a different closing bracket
8717 cx.update_editor(|editor, window, cx| {
8718 editor.handle_input(")", window, cx);
8719 });
8720 cx.assert_editor_state(
8721 &"
8722 🏀{{{)ˇ}}}
8723 ε{{{)ˇ}}}
8724 ❤️{{{)ˇ}}}
8725 "
8726 .unindent(),
8727 );
8728
8729 // skip over the auto-closed brackets when typing a closing bracket
8730 cx.update_editor(|editor, window, cx| {
8731 editor.move_right(&MoveRight, window, cx);
8732 editor.handle_input("}", window, cx);
8733 editor.handle_input("}", window, cx);
8734 editor.handle_input("}", window, cx);
8735 });
8736 cx.assert_editor_state(
8737 &"
8738 🏀{{{)}}}}ˇ
8739 ε{{{)}}}}ˇ
8740 ❤️{{{)}}}}ˇ
8741 "
8742 .unindent(),
8743 );
8744
8745 // autoclose multi-character pairs
8746 cx.set_state(
8747 &"
8748 ˇ
8749 ˇ
8750 "
8751 .unindent(),
8752 );
8753 cx.update_editor(|editor, window, cx| {
8754 editor.handle_input("/", window, cx);
8755 editor.handle_input("*", window, cx);
8756 });
8757 cx.assert_editor_state(
8758 &"
8759 /*ˇ */
8760 /*ˇ */
8761 "
8762 .unindent(),
8763 );
8764
8765 // one cursor autocloses a multi-character pair, one cursor
8766 // does not autoclose.
8767 cx.set_state(
8768 &"
8769 /ˇ
8770 ˇ
8771 "
8772 .unindent(),
8773 );
8774 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
8775 cx.assert_editor_state(
8776 &"
8777 /*ˇ */
8778 *ˇ
8779 "
8780 .unindent(),
8781 );
8782
8783 // Don't autoclose if the next character isn't whitespace and isn't
8784 // listed in the language's "autoclose_before" section.
8785 cx.set_state("ˇa b");
8786 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8787 cx.assert_editor_state("{ˇa b");
8788
8789 // Don't autoclose if `close` is false for the bracket pair
8790 cx.set_state("ˇ");
8791 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
8792 cx.assert_editor_state("[ˇ");
8793
8794 // Surround with brackets if text is selected
8795 cx.set_state("«aˇ» b");
8796 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8797 cx.assert_editor_state("{«aˇ»} b");
8798
8799 // Autoclose when not immediately after a word character
8800 cx.set_state("a ˇ");
8801 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8802 cx.assert_editor_state("a \"ˇ\"");
8803
8804 // Autoclose pair where the start and end characters are the same
8805 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8806 cx.assert_editor_state("a \"\"ˇ");
8807
8808 // Don't autoclose when immediately after a word character
8809 cx.set_state("aˇ");
8810 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8811 cx.assert_editor_state("a\"ˇ");
8812
8813 // Do autoclose when after a non-word character
8814 cx.set_state("{ˇ");
8815 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8816 cx.assert_editor_state("{\"ˇ\"");
8817
8818 // Non identical pairs autoclose regardless of preceding character
8819 cx.set_state("aˇ");
8820 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8821 cx.assert_editor_state("a{ˇ}");
8822
8823 // Don't autoclose pair if autoclose is disabled
8824 cx.set_state("ˇ");
8825 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
8826 cx.assert_editor_state("<ˇ");
8827
8828 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
8829 cx.set_state("«aˇ» b");
8830 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
8831 cx.assert_editor_state("<«aˇ»> b");
8832}
8833
8834#[gpui::test]
8835async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
8836 init_test(cx, |settings| {
8837 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
8838 });
8839
8840 let mut cx = EditorTestContext::new(cx).await;
8841
8842 let language = Arc::new(Language::new(
8843 LanguageConfig {
8844 brackets: BracketPairConfig {
8845 pairs: vec![
8846 BracketPair {
8847 start: "{".to_string(),
8848 end: "}".to_string(),
8849 close: true,
8850 surround: true,
8851 newline: true,
8852 },
8853 BracketPair {
8854 start: "(".to_string(),
8855 end: ")".to_string(),
8856 close: true,
8857 surround: true,
8858 newline: true,
8859 },
8860 BracketPair {
8861 start: "[".to_string(),
8862 end: "]".to_string(),
8863 close: false,
8864 surround: false,
8865 newline: true,
8866 },
8867 ],
8868 ..Default::default()
8869 },
8870 autoclose_before: "})]".to_string(),
8871 ..Default::default()
8872 },
8873 Some(tree_sitter_rust::LANGUAGE.into()),
8874 ));
8875
8876 cx.language_registry().add(language.clone());
8877 cx.update_buffer(|buffer, cx| {
8878 buffer.set_language(Some(language), cx);
8879 });
8880
8881 cx.set_state(
8882 &"
8883 ˇ
8884 ˇ
8885 ˇ
8886 "
8887 .unindent(),
8888 );
8889
8890 // ensure only matching closing brackets are skipped over
8891 cx.update_editor(|editor, window, cx| {
8892 editor.handle_input("}", window, cx);
8893 editor.move_left(&MoveLeft, window, cx);
8894 editor.handle_input(")", window, cx);
8895 editor.move_left(&MoveLeft, window, cx);
8896 });
8897 cx.assert_editor_state(
8898 &"
8899 ˇ)}
8900 ˇ)}
8901 ˇ)}
8902 "
8903 .unindent(),
8904 );
8905
8906 // skip-over closing brackets at multiple cursors
8907 cx.update_editor(|editor, window, cx| {
8908 editor.handle_input(")", window, cx);
8909 editor.handle_input("}", window, cx);
8910 });
8911 cx.assert_editor_state(
8912 &"
8913 )}ˇ
8914 )}ˇ
8915 )}ˇ
8916 "
8917 .unindent(),
8918 );
8919
8920 // ignore non-close brackets
8921 cx.update_editor(|editor, window, cx| {
8922 editor.handle_input("]", window, cx);
8923 editor.move_left(&MoveLeft, window, cx);
8924 editor.handle_input("]", window, cx);
8925 });
8926 cx.assert_editor_state(
8927 &"
8928 )}]ˇ]
8929 )}]ˇ]
8930 )}]ˇ]
8931 "
8932 .unindent(),
8933 );
8934}
8935
8936#[gpui::test]
8937async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
8938 init_test(cx, |_| {});
8939
8940 let mut cx = EditorTestContext::new(cx).await;
8941
8942 let html_language = Arc::new(
8943 Language::new(
8944 LanguageConfig {
8945 name: "HTML".into(),
8946 brackets: BracketPairConfig {
8947 pairs: vec![
8948 BracketPair {
8949 start: "<".into(),
8950 end: ">".into(),
8951 close: true,
8952 ..Default::default()
8953 },
8954 BracketPair {
8955 start: "{".into(),
8956 end: "}".into(),
8957 close: true,
8958 ..Default::default()
8959 },
8960 BracketPair {
8961 start: "(".into(),
8962 end: ")".into(),
8963 close: true,
8964 ..Default::default()
8965 },
8966 ],
8967 ..Default::default()
8968 },
8969 autoclose_before: "})]>".into(),
8970 ..Default::default()
8971 },
8972 Some(tree_sitter_html::LANGUAGE.into()),
8973 )
8974 .with_injection_query(
8975 r#"
8976 (script_element
8977 (raw_text) @injection.content
8978 (#set! injection.language "javascript"))
8979 "#,
8980 )
8981 .unwrap(),
8982 );
8983
8984 let javascript_language = Arc::new(Language::new(
8985 LanguageConfig {
8986 name: "JavaScript".into(),
8987 brackets: BracketPairConfig {
8988 pairs: vec![
8989 BracketPair {
8990 start: "/*".into(),
8991 end: " */".into(),
8992 close: true,
8993 ..Default::default()
8994 },
8995 BracketPair {
8996 start: "{".into(),
8997 end: "}".into(),
8998 close: true,
8999 ..Default::default()
9000 },
9001 BracketPair {
9002 start: "(".into(),
9003 end: ")".into(),
9004 close: true,
9005 ..Default::default()
9006 },
9007 ],
9008 ..Default::default()
9009 },
9010 autoclose_before: "})]>".into(),
9011 ..Default::default()
9012 },
9013 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
9014 ));
9015
9016 cx.language_registry().add(html_language.clone());
9017 cx.language_registry().add(javascript_language);
9018 cx.executor().run_until_parked();
9019
9020 cx.update_buffer(|buffer, cx| {
9021 buffer.set_language(Some(html_language), cx);
9022 });
9023
9024 cx.set_state(
9025 &r#"
9026 <body>ˇ
9027 <script>
9028 var x = 1;ˇ
9029 </script>
9030 </body>ˇ
9031 "#
9032 .unindent(),
9033 );
9034
9035 // Precondition: different languages are active at different locations.
9036 cx.update_editor(|editor, window, cx| {
9037 let snapshot = editor.snapshot(window, cx);
9038 let cursors = editor.selections.ranges::<usize>(cx);
9039 let languages = cursors
9040 .iter()
9041 .map(|c| snapshot.language_at(c.start).unwrap().name())
9042 .collect::<Vec<_>>();
9043 assert_eq!(
9044 languages,
9045 &["HTML".into(), "JavaScript".into(), "HTML".into()]
9046 );
9047 });
9048
9049 // Angle brackets autoclose in HTML, but not JavaScript.
9050 cx.update_editor(|editor, window, cx| {
9051 editor.handle_input("<", window, cx);
9052 editor.handle_input("a", window, cx);
9053 });
9054 cx.assert_editor_state(
9055 &r#"
9056 <body><aˇ>
9057 <script>
9058 var x = 1;<aˇ
9059 </script>
9060 </body><aˇ>
9061 "#
9062 .unindent(),
9063 );
9064
9065 // Curly braces and parens autoclose in both HTML and JavaScript.
9066 cx.update_editor(|editor, window, cx| {
9067 editor.handle_input(" b=", window, cx);
9068 editor.handle_input("{", window, cx);
9069 editor.handle_input("c", window, cx);
9070 editor.handle_input("(", window, cx);
9071 });
9072 cx.assert_editor_state(
9073 &r#"
9074 <body><a b={c(ˇ)}>
9075 <script>
9076 var x = 1;<a b={c(ˇ)}
9077 </script>
9078 </body><a b={c(ˇ)}>
9079 "#
9080 .unindent(),
9081 );
9082
9083 // Brackets that were already autoclosed are skipped.
9084 cx.update_editor(|editor, window, cx| {
9085 editor.handle_input(")", window, cx);
9086 editor.handle_input("d", window, cx);
9087 editor.handle_input("}", window, cx);
9088 });
9089 cx.assert_editor_state(
9090 &r#"
9091 <body><a b={c()d}ˇ>
9092 <script>
9093 var x = 1;<a b={c()d}ˇ
9094 </script>
9095 </body><a b={c()d}ˇ>
9096 "#
9097 .unindent(),
9098 );
9099 cx.update_editor(|editor, window, cx| {
9100 editor.handle_input(">", window, cx);
9101 });
9102 cx.assert_editor_state(
9103 &r#"
9104 <body><a b={c()d}>ˇ
9105 <script>
9106 var x = 1;<a b={c()d}>ˇ
9107 </script>
9108 </body><a b={c()d}>ˇ
9109 "#
9110 .unindent(),
9111 );
9112
9113 // Reset
9114 cx.set_state(
9115 &r#"
9116 <body>ˇ
9117 <script>
9118 var x = 1;ˇ
9119 </script>
9120 </body>ˇ
9121 "#
9122 .unindent(),
9123 );
9124
9125 cx.update_editor(|editor, window, cx| {
9126 editor.handle_input("<", window, cx);
9127 });
9128 cx.assert_editor_state(
9129 &r#"
9130 <body><ˇ>
9131 <script>
9132 var x = 1;<ˇ
9133 </script>
9134 </body><ˇ>
9135 "#
9136 .unindent(),
9137 );
9138
9139 // When backspacing, the closing angle brackets are removed.
9140 cx.update_editor(|editor, window, cx| {
9141 editor.backspace(&Backspace, window, cx);
9142 });
9143 cx.assert_editor_state(
9144 &r#"
9145 <body>ˇ
9146 <script>
9147 var x = 1;ˇ
9148 </script>
9149 </body>ˇ
9150 "#
9151 .unindent(),
9152 );
9153
9154 // Block comments autoclose in JavaScript, but not HTML.
9155 cx.update_editor(|editor, window, cx| {
9156 editor.handle_input("/", window, cx);
9157 editor.handle_input("*", window, cx);
9158 });
9159 cx.assert_editor_state(
9160 &r#"
9161 <body>/*ˇ
9162 <script>
9163 var x = 1;/*ˇ */
9164 </script>
9165 </body>/*ˇ
9166 "#
9167 .unindent(),
9168 );
9169}
9170
9171#[gpui::test]
9172async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
9173 init_test(cx, |_| {});
9174
9175 let mut cx = EditorTestContext::new(cx).await;
9176
9177 let rust_language = Arc::new(
9178 Language::new(
9179 LanguageConfig {
9180 name: "Rust".into(),
9181 brackets: serde_json::from_value(json!([
9182 { "start": "{", "end": "}", "close": true, "newline": true },
9183 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
9184 ]))
9185 .unwrap(),
9186 autoclose_before: "})]>".into(),
9187 ..Default::default()
9188 },
9189 Some(tree_sitter_rust::LANGUAGE.into()),
9190 )
9191 .with_override_query("(string_literal) @string")
9192 .unwrap(),
9193 );
9194
9195 cx.language_registry().add(rust_language.clone());
9196 cx.update_buffer(|buffer, cx| {
9197 buffer.set_language(Some(rust_language), cx);
9198 });
9199
9200 cx.set_state(
9201 &r#"
9202 let x = ˇ
9203 "#
9204 .unindent(),
9205 );
9206
9207 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
9208 cx.update_editor(|editor, window, cx| {
9209 editor.handle_input("\"", window, cx);
9210 });
9211 cx.assert_editor_state(
9212 &r#"
9213 let x = "ˇ"
9214 "#
9215 .unindent(),
9216 );
9217
9218 // Inserting another quotation mark. The cursor moves across the existing
9219 // automatically-inserted quotation mark.
9220 cx.update_editor(|editor, window, cx| {
9221 editor.handle_input("\"", window, cx);
9222 });
9223 cx.assert_editor_state(
9224 &r#"
9225 let x = ""ˇ
9226 "#
9227 .unindent(),
9228 );
9229
9230 // Reset
9231 cx.set_state(
9232 &r#"
9233 let x = ˇ
9234 "#
9235 .unindent(),
9236 );
9237
9238 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
9239 cx.update_editor(|editor, window, cx| {
9240 editor.handle_input("\"", window, cx);
9241 editor.handle_input(" ", window, cx);
9242 editor.move_left(&Default::default(), window, cx);
9243 editor.handle_input("\\", window, cx);
9244 editor.handle_input("\"", window, cx);
9245 });
9246 cx.assert_editor_state(
9247 &r#"
9248 let x = "\"ˇ "
9249 "#
9250 .unindent(),
9251 );
9252
9253 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
9254 // mark. Nothing is inserted.
9255 cx.update_editor(|editor, window, cx| {
9256 editor.move_right(&Default::default(), window, cx);
9257 editor.handle_input("\"", window, cx);
9258 });
9259 cx.assert_editor_state(
9260 &r#"
9261 let x = "\" "ˇ
9262 "#
9263 .unindent(),
9264 );
9265}
9266
9267#[gpui::test]
9268async fn test_surround_with_pair(cx: &mut TestAppContext) {
9269 init_test(cx, |_| {});
9270
9271 let language = Arc::new(Language::new(
9272 LanguageConfig {
9273 brackets: BracketPairConfig {
9274 pairs: vec![
9275 BracketPair {
9276 start: "{".to_string(),
9277 end: "}".to_string(),
9278 close: true,
9279 surround: true,
9280 newline: true,
9281 },
9282 BracketPair {
9283 start: "/* ".to_string(),
9284 end: "*/".to_string(),
9285 close: true,
9286 surround: true,
9287 ..Default::default()
9288 },
9289 ],
9290 ..Default::default()
9291 },
9292 ..Default::default()
9293 },
9294 Some(tree_sitter_rust::LANGUAGE.into()),
9295 ));
9296
9297 let text = r#"
9298 a
9299 b
9300 c
9301 "#
9302 .unindent();
9303
9304 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9305 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9306 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9307 editor
9308 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9309 .await;
9310
9311 editor.update_in(cx, |editor, window, cx| {
9312 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9313 s.select_display_ranges([
9314 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
9315 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
9316 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
9317 ])
9318 });
9319
9320 editor.handle_input("{", window, cx);
9321 editor.handle_input("{", window, cx);
9322 editor.handle_input("{", window, cx);
9323 assert_eq!(
9324 editor.text(cx),
9325 "
9326 {{{a}}}
9327 {{{b}}}
9328 {{{c}}}
9329 "
9330 .unindent()
9331 );
9332 assert_eq!(
9333 editor.selections.display_ranges(cx),
9334 [
9335 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
9336 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
9337 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
9338 ]
9339 );
9340
9341 editor.undo(&Undo, window, cx);
9342 editor.undo(&Undo, window, cx);
9343 editor.undo(&Undo, window, cx);
9344 assert_eq!(
9345 editor.text(cx),
9346 "
9347 a
9348 b
9349 c
9350 "
9351 .unindent()
9352 );
9353 assert_eq!(
9354 editor.selections.display_ranges(cx),
9355 [
9356 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
9357 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
9358 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
9359 ]
9360 );
9361
9362 // Ensure inserting the first character of a multi-byte bracket pair
9363 // doesn't surround the selections with the bracket.
9364 editor.handle_input("/", window, cx);
9365 assert_eq!(
9366 editor.text(cx),
9367 "
9368 /
9369 /
9370 /
9371 "
9372 .unindent()
9373 );
9374 assert_eq!(
9375 editor.selections.display_ranges(cx),
9376 [
9377 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
9378 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
9379 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
9380 ]
9381 );
9382
9383 editor.undo(&Undo, window, cx);
9384 assert_eq!(
9385 editor.text(cx),
9386 "
9387 a
9388 b
9389 c
9390 "
9391 .unindent()
9392 );
9393 assert_eq!(
9394 editor.selections.display_ranges(cx),
9395 [
9396 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
9397 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
9398 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
9399 ]
9400 );
9401
9402 // Ensure inserting the last character of a multi-byte bracket pair
9403 // doesn't surround the selections with the bracket.
9404 editor.handle_input("*", window, cx);
9405 assert_eq!(
9406 editor.text(cx),
9407 "
9408 *
9409 *
9410 *
9411 "
9412 .unindent()
9413 );
9414 assert_eq!(
9415 editor.selections.display_ranges(cx),
9416 [
9417 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
9418 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
9419 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
9420 ]
9421 );
9422 });
9423}
9424
9425#[gpui::test]
9426async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
9427 init_test(cx, |_| {});
9428
9429 let language = Arc::new(Language::new(
9430 LanguageConfig {
9431 brackets: BracketPairConfig {
9432 pairs: vec![BracketPair {
9433 start: "{".to_string(),
9434 end: "}".to_string(),
9435 close: true,
9436 surround: true,
9437 newline: true,
9438 }],
9439 ..Default::default()
9440 },
9441 autoclose_before: "}".to_string(),
9442 ..Default::default()
9443 },
9444 Some(tree_sitter_rust::LANGUAGE.into()),
9445 ));
9446
9447 let text = r#"
9448 a
9449 b
9450 c
9451 "#
9452 .unindent();
9453
9454 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9455 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9456 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9457 editor
9458 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9459 .await;
9460
9461 editor.update_in(cx, |editor, window, cx| {
9462 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9463 s.select_ranges([
9464 Point::new(0, 1)..Point::new(0, 1),
9465 Point::new(1, 1)..Point::new(1, 1),
9466 Point::new(2, 1)..Point::new(2, 1),
9467 ])
9468 });
9469
9470 editor.handle_input("{", window, cx);
9471 editor.handle_input("{", window, cx);
9472 editor.handle_input("_", window, cx);
9473 assert_eq!(
9474 editor.text(cx),
9475 "
9476 a{{_}}
9477 b{{_}}
9478 c{{_}}
9479 "
9480 .unindent()
9481 );
9482 assert_eq!(
9483 editor.selections.ranges::<Point>(cx),
9484 [
9485 Point::new(0, 4)..Point::new(0, 4),
9486 Point::new(1, 4)..Point::new(1, 4),
9487 Point::new(2, 4)..Point::new(2, 4)
9488 ]
9489 );
9490
9491 editor.backspace(&Default::default(), window, cx);
9492 editor.backspace(&Default::default(), window, cx);
9493 assert_eq!(
9494 editor.text(cx),
9495 "
9496 a{}
9497 b{}
9498 c{}
9499 "
9500 .unindent()
9501 );
9502 assert_eq!(
9503 editor.selections.ranges::<Point>(cx),
9504 [
9505 Point::new(0, 2)..Point::new(0, 2),
9506 Point::new(1, 2)..Point::new(1, 2),
9507 Point::new(2, 2)..Point::new(2, 2)
9508 ]
9509 );
9510
9511 editor.delete_to_previous_word_start(&Default::default(), window, cx);
9512 assert_eq!(
9513 editor.text(cx),
9514 "
9515 a
9516 b
9517 c
9518 "
9519 .unindent()
9520 );
9521 assert_eq!(
9522 editor.selections.ranges::<Point>(cx),
9523 [
9524 Point::new(0, 1)..Point::new(0, 1),
9525 Point::new(1, 1)..Point::new(1, 1),
9526 Point::new(2, 1)..Point::new(2, 1)
9527 ]
9528 );
9529 });
9530}
9531
9532#[gpui::test]
9533async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
9534 init_test(cx, |settings| {
9535 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
9536 });
9537
9538 let mut cx = EditorTestContext::new(cx).await;
9539
9540 let language = Arc::new(Language::new(
9541 LanguageConfig {
9542 brackets: BracketPairConfig {
9543 pairs: vec![
9544 BracketPair {
9545 start: "{".to_string(),
9546 end: "}".to_string(),
9547 close: true,
9548 surround: true,
9549 newline: true,
9550 },
9551 BracketPair {
9552 start: "(".to_string(),
9553 end: ")".to_string(),
9554 close: true,
9555 surround: true,
9556 newline: true,
9557 },
9558 BracketPair {
9559 start: "[".to_string(),
9560 end: "]".to_string(),
9561 close: false,
9562 surround: true,
9563 newline: true,
9564 },
9565 ],
9566 ..Default::default()
9567 },
9568 autoclose_before: "})]".to_string(),
9569 ..Default::default()
9570 },
9571 Some(tree_sitter_rust::LANGUAGE.into()),
9572 ));
9573
9574 cx.language_registry().add(language.clone());
9575 cx.update_buffer(|buffer, cx| {
9576 buffer.set_language(Some(language), cx);
9577 });
9578
9579 cx.set_state(
9580 &"
9581 {(ˇ)}
9582 [[ˇ]]
9583 {(ˇ)}
9584 "
9585 .unindent(),
9586 );
9587
9588 cx.update_editor(|editor, window, cx| {
9589 editor.backspace(&Default::default(), window, cx);
9590 editor.backspace(&Default::default(), window, cx);
9591 });
9592
9593 cx.assert_editor_state(
9594 &"
9595 ˇ
9596 ˇ]]
9597 ˇ
9598 "
9599 .unindent(),
9600 );
9601
9602 cx.update_editor(|editor, window, cx| {
9603 editor.handle_input("{", window, cx);
9604 editor.handle_input("{", window, cx);
9605 editor.move_right(&MoveRight, window, cx);
9606 editor.move_right(&MoveRight, window, cx);
9607 editor.move_left(&MoveLeft, window, cx);
9608 editor.move_left(&MoveLeft, window, cx);
9609 editor.backspace(&Default::default(), window, cx);
9610 });
9611
9612 cx.assert_editor_state(
9613 &"
9614 {ˇ}
9615 {ˇ}]]
9616 {ˇ}
9617 "
9618 .unindent(),
9619 );
9620
9621 cx.update_editor(|editor, window, cx| {
9622 editor.backspace(&Default::default(), window, cx);
9623 });
9624
9625 cx.assert_editor_state(
9626 &"
9627 ˇ
9628 ˇ]]
9629 ˇ
9630 "
9631 .unindent(),
9632 );
9633}
9634
9635#[gpui::test]
9636async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
9637 init_test(cx, |_| {});
9638
9639 let language = Arc::new(Language::new(
9640 LanguageConfig::default(),
9641 Some(tree_sitter_rust::LANGUAGE.into()),
9642 ));
9643
9644 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
9645 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9646 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9647 editor
9648 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9649 .await;
9650
9651 editor.update_in(cx, |editor, window, cx| {
9652 editor.set_auto_replace_emoji_shortcode(true);
9653
9654 editor.handle_input("Hello ", window, cx);
9655 editor.handle_input(":wave", window, cx);
9656 assert_eq!(editor.text(cx), "Hello :wave".unindent());
9657
9658 editor.handle_input(":", window, cx);
9659 assert_eq!(editor.text(cx), "Hello 👋".unindent());
9660
9661 editor.handle_input(" :smile", window, cx);
9662 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
9663
9664 editor.handle_input(":", window, cx);
9665 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
9666
9667 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
9668 editor.handle_input(":wave", window, cx);
9669 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
9670
9671 editor.handle_input(":", window, cx);
9672 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
9673
9674 editor.handle_input(":1", window, cx);
9675 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
9676
9677 editor.handle_input(":", window, cx);
9678 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
9679
9680 // Ensure shortcode does not get replaced when it is part of a word
9681 editor.handle_input(" Test:wave", window, cx);
9682 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
9683
9684 editor.handle_input(":", window, cx);
9685 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
9686
9687 editor.set_auto_replace_emoji_shortcode(false);
9688
9689 // Ensure shortcode does not get replaced when auto replace is off
9690 editor.handle_input(" :wave", window, cx);
9691 assert_eq!(
9692 editor.text(cx),
9693 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
9694 );
9695
9696 editor.handle_input(":", window, cx);
9697 assert_eq!(
9698 editor.text(cx),
9699 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
9700 );
9701 });
9702}
9703
9704#[gpui::test]
9705async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
9706 init_test(cx, |_| {});
9707
9708 let (text, insertion_ranges) = marked_text_ranges(
9709 indoc! {"
9710 ˇ
9711 "},
9712 false,
9713 );
9714
9715 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
9716 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9717
9718 _ = editor.update_in(cx, |editor, window, cx| {
9719 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
9720
9721 editor
9722 .insert_snippet(&insertion_ranges, snippet, window, cx)
9723 .unwrap();
9724
9725 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
9726 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
9727 assert_eq!(editor.text(cx), expected_text);
9728 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
9729 }
9730
9731 assert(
9732 editor,
9733 cx,
9734 indoc! {"
9735 type «» =•
9736 "},
9737 );
9738
9739 assert!(editor.context_menu_visible(), "There should be a matches");
9740 });
9741}
9742
9743#[gpui::test]
9744async fn test_snippets(cx: &mut TestAppContext) {
9745 init_test(cx, |_| {});
9746
9747 let mut cx = EditorTestContext::new(cx).await;
9748
9749 cx.set_state(indoc! {"
9750 a.ˇ b
9751 a.ˇ b
9752 a.ˇ b
9753 "});
9754
9755 cx.update_editor(|editor, window, cx| {
9756 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
9757 let insertion_ranges = editor
9758 .selections
9759 .all(cx)
9760 .iter()
9761 .map(|s| s.range())
9762 .collect::<Vec<_>>();
9763 editor
9764 .insert_snippet(&insertion_ranges, snippet, window, cx)
9765 .unwrap();
9766 });
9767
9768 cx.assert_editor_state(indoc! {"
9769 a.f(«oneˇ», two, «threeˇ») b
9770 a.f(«oneˇ», two, «threeˇ») b
9771 a.f(«oneˇ», two, «threeˇ») b
9772 "});
9773
9774 // Can't move earlier than the first tab stop
9775 cx.update_editor(|editor, window, cx| {
9776 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
9777 });
9778 cx.assert_editor_state(indoc! {"
9779 a.f(«oneˇ», two, «threeˇ») b
9780 a.f(«oneˇ», two, «threeˇ») b
9781 a.f(«oneˇ», two, «threeˇ») b
9782 "});
9783
9784 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9785 cx.assert_editor_state(indoc! {"
9786 a.f(one, «twoˇ», three) b
9787 a.f(one, «twoˇ», three) b
9788 a.f(one, «twoˇ», three) b
9789 "});
9790
9791 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
9792 cx.assert_editor_state(indoc! {"
9793 a.f(«oneˇ», two, «threeˇ») b
9794 a.f(«oneˇ», two, «threeˇ») b
9795 a.f(«oneˇ», two, «threeˇ») b
9796 "});
9797
9798 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9799 cx.assert_editor_state(indoc! {"
9800 a.f(one, «twoˇ», three) b
9801 a.f(one, «twoˇ», three) b
9802 a.f(one, «twoˇ», three) b
9803 "});
9804 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9805 cx.assert_editor_state(indoc! {"
9806 a.f(one, two, three)ˇ b
9807 a.f(one, two, three)ˇ b
9808 a.f(one, two, three)ˇ b
9809 "});
9810
9811 // As soon as the last tab stop is reached, snippet state is gone
9812 cx.update_editor(|editor, window, cx| {
9813 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
9814 });
9815 cx.assert_editor_state(indoc! {"
9816 a.f(one, two, three)ˇ b
9817 a.f(one, two, three)ˇ b
9818 a.f(one, two, three)ˇ b
9819 "});
9820}
9821
9822#[gpui::test]
9823async fn test_snippet_indentation(cx: &mut TestAppContext) {
9824 init_test(cx, |_| {});
9825
9826 let mut cx = EditorTestContext::new(cx).await;
9827
9828 cx.update_editor(|editor, window, cx| {
9829 let snippet = Snippet::parse(indoc! {"
9830 /*
9831 * Multiline comment with leading indentation
9832 *
9833 * $1
9834 */
9835 $0"})
9836 .unwrap();
9837 let insertion_ranges = editor
9838 .selections
9839 .all(cx)
9840 .iter()
9841 .map(|s| s.range())
9842 .collect::<Vec<_>>();
9843 editor
9844 .insert_snippet(&insertion_ranges, snippet, window, cx)
9845 .unwrap();
9846 });
9847
9848 cx.assert_editor_state(indoc! {"
9849 /*
9850 * Multiline comment with leading indentation
9851 *
9852 * ˇ
9853 */
9854 "});
9855
9856 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9857 cx.assert_editor_state(indoc! {"
9858 /*
9859 * Multiline comment with leading indentation
9860 *
9861 *•
9862 */
9863 ˇ"});
9864}
9865
9866#[gpui::test]
9867async fn test_document_format_during_save(cx: &mut TestAppContext) {
9868 init_test(cx, |_| {});
9869
9870 let fs = FakeFs::new(cx.executor());
9871 fs.insert_file(path!("/file.rs"), Default::default()).await;
9872
9873 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
9874
9875 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9876 language_registry.add(rust_lang());
9877 let mut fake_servers = language_registry.register_fake_lsp(
9878 "Rust",
9879 FakeLspAdapter {
9880 capabilities: lsp::ServerCapabilities {
9881 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9882 ..Default::default()
9883 },
9884 ..Default::default()
9885 },
9886 );
9887
9888 let buffer = project
9889 .update(cx, |project, cx| {
9890 project.open_local_buffer(path!("/file.rs"), cx)
9891 })
9892 .await
9893 .unwrap();
9894
9895 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9896 let (editor, cx) = cx.add_window_view(|window, cx| {
9897 build_editor_with_project(project.clone(), buffer, window, cx)
9898 });
9899 editor.update_in(cx, |editor, window, cx| {
9900 editor.set_text("one\ntwo\nthree\n", window, cx)
9901 });
9902 assert!(cx.read(|cx| editor.is_dirty(cx)));
9903
9904 cx.executor().start_waiting();
9905 let fake_server = fake_servers.next().await.unwrap();
9906
9907 {
9908 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9909 move |params, _| async move {
9910 assert_eq!(
9911 params.text_document.uri,
9912 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
9913 );
9914 assert_eq!(params.options.tab_size, 4);
9915 Ok(Some(vec![lsp::TextEdit::new(
9916 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9917 ", ".to_string(),
9918 )]))
9919 },
9920 );
9921 let save = editor
9922 .update_in(cx, |editor, window, cx| {
9923 editor.save(
9924 SaveOptions {
9925 format: true,
9926 autosave: false,
9927 },
9928 project.clone(),
9929 window,
9930 cx,
9931 )
9932 })
9933 .unwrap();
9934 cx.executor().start_waiting();
9935 save.await;
9936
9937 assert_eq!(
9938 editor.update(cx, |editor, cx| editor.text(cx)),
9939 "one, two\nthree\n"
9940 );
9941 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9942 }
9943
9944 {
9945 editor.update_in(cx, |editor, window, cx| {
9946 editor.set_text("one\ntwo\nthree\n", window, cx)
9947 });
9948 assert!(cx.read(|cx| editor.is_dirty(cx)));
9949
9950 // Ensure we can still save even if formatting hangs.
9951 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9952 move |params, _| async move {
9953 assert_eq!(
9954 params.text_document.uri,
9955 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
9956 );
9957 futures::future::pending::<()>().await;
9958 unreachable!()
9959 },
9960 );
9961 let save = editor
9962 .update_in(cx, |editor, window, cx| {
9963 editor.save(
9964 SaveOptions {
9965 format: true,
9966 autosave: false,
9967 },
9968 project.clone(),
9969 window,
9970 cx,
9971 )
9972 })
9973 .unwrap();
9974 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9975 cx.executor().start_waiting();
9976 save.await;
9977 assert_eq!(
9978 editor.update(cx, |editor, cx| editor.text(cx)),
9979 "one\ntwo\nthree\n"
9980 );
9981 }
9982
9983 // Set rust language override and assert overridden tabsize is sent to language server
9984 update_test_language_settings(cx, |settings| {
9985 settings.languages.0.insert(
9986 "Rust".into(),
9987 LanguageSettingsContent {
9988 tab_size: NonZeroU32::new(8),
9989 ..Default::default()
9990 },
9991 );
9992 });
9993
9994 {
9995 editor.update_in(cx, |editor, window, cx| {
9996 editor.set_text("somehting_new\n", window, cx)
9997 });
9998 assert!(cx.read(|cx| editor.is_dirty(cx)));
9999 let _formatting_request_signal = fake_server
10000 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
10001 assert_eq!(
10002 params.text_document.uri,
10003 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
10004 );
10005 assert_eq!(params.options.tab_size, 8);
10006 Ok(Some(vec![]))
10007 });
10008 let save = editor
10009 .update_in(cx, |editor, window, cx| {
10010 editor.save(
10011 SaveOptions {
10012 format: true,
10013 autosave: false,
10014 },
10015 project.clone(),
10016 window,
10017 cx,
10018 )
10019 })
10020 .unwrap();
10021 cx.executor().start_waiting();
10022 save.await;
10023 }
10024}
10025
10026#[gpui::test]
10027async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
10028 init_test(cx, |settings| {
10029 settings.defaults.ensure_final_newline_on_save = Some(false);
10030 });
10031
10032 let fs = FakeFs::new(cx.executor());
10033 fs.insert_file(path!("/file.txt"), "foo".into()).await;
10034
10035 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
10036
10037 let buffer = project
10038 .update(cx, |project, cx| {
10039 project.open_local_buffer(path!("/file.txt"), cx)
10040 })
10041 .await
10042 .unwrap();
10043
10044 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10045 let (editor, cx) = cx.add_window_view(|window, cx| {
10046 build_editor_with_project(project.clone(), buffer, window, cx)
10047 });
10048 editor.update_in(cx, |editor, window, cx| {
10049 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
10050 s.select_ranges([0..0])
10051 });
10052 });
10053 assert!(!cx.read(|cx| editor.is_dirty(cx)));
10054
10055 editor.update_in(cx, |editor, window, cx| {
10056 editor.handle_input("\n", window, cx)
10057 });
10058 cx.run_until_parked();
10059 save(&editor, &project, cx).await;
10060 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
10061
10062 editor.update_in(cx, |editor, window, cx| {
10063 editor.undo(&Default::default(), window, cx);
10064 });
10065 save(&editor, &project, cx).await;
10066 assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
10067
10068 editor.update_in(cx, |editor, window, cx| {
10069 editor.redo(&Default::default(), window, cx);
10070 });
10071 cx.run_until_parked();
10072 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
10073
10074 async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
10075 let save = editor
10076 .update_in(cx, |editor, window, cx| {
10077 editor.save(
10078 SaveOptions {
10079 format: true,
10080 autosave: false,
10081 },
10082 project.clone(),
10083 window,
10084 cx,
10085 )
10086 })
10087 .unwrap();
10088 cx.executor().start_waiting();
10089 save.await;
10090 assert!(!cx.read(|cx| editor.is_dirty(cx)));
10091 }
10092}
10093
10094#[gpui::test]
10095async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
10096 init_test(cx, |_| {});
10097
10098 let cols = 4;
10099 let rows = 10;
10100 let sample_text_1 = sample_text(rows, cols, 'a');
10101 assert_eq!(
10102 sample_text_1,
10103 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
10104 );
10105 let sample_text_2 = sample_text(rows, cols, 'l');
10106 assert_eq!(
10107 sample_text_2,
10108 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
10109 );
10110 let sample_text_3 = sample_text(rows, cols, 'v');
10111 assert_eq!(
10112 sample_text_3,
10113 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
10114 );
10115
10116 let fs = FakeFs::new(cx.executor());
10117 fs.insert_tree(
10118 path!("/a"),
10119 json!({
10120 "main.rs": sample_text_1,
10121 "other.rs": sample_text_2,
10122 "lib.rs": sample_text_3,
10123 }),
10124 )
10125 .await;
10126
10127 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
10128 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
10129 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
10130
10131 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10132 language_registry.add(rust_lang());
10133 let mut fake_servers = language_registry.register_fake_lsp(
10134 "Rust",
10135 FakeLspAdapter {
10136 capabilities: lsp::ServerCapabilities {
10137 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10138 ..Default::default()
10139 },
10140 ..Default::default()
10141 },
10142 );
10143
10144 let worktree = project.update(cx, |project, cx| {
10145 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
10146 assert_eq!(worktrees.len(), 1);
10147 worktrees.pop().unwrap()
10148 });
10149 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
10150
10151 let buffer_1 = project
10152 .update(cx, |project, cx| {
10153 project.open_buffer((worktree_id, "main.rs"), cx)
10154 })
10155 .await
10156 .unwrap();
10157 let buffer_2 = project
10158 .update(cx, |project, cx| {
10159 project.open_buffer((worktree_id, "other.rs"), cx)
10160 })
10161 .await
10162 .unwrap();
10163 let buffer_3 = project
10164 .update(cx, |project, cx| {
10165 project.open_buffer((worktree_id, "lib.rs"), cx)
10166 })
10167 .await
10168 .unwrap();
10169
10170 let multi_buffer = cx.new(|cx| {
10171 let mut multi_buffer = MultiBuffer::new(ReadWrite);
10172 multi_buffer.push_excerpts(
10173 buffer_1.clone(),
10174 [
10175 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
10176 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
10177 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
10178 ],
10179 cx,
10180 );
10181 multi_buffer.push_excerpts(
10182 buffer_2.clone(),
10183 [
10184 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
10185 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
10186 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
10187 ],
10188 cx,
10189 );
10190 multi_buffer.push_excerpts(
10191 buffer_3.clone(),
10192 [
10193 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
10194 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
10195 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
10196 ],
10197 cx,
10198 );
10199 multi_buffer
10200 });
10201 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
10202 Editor::new(
10203 EditorMode::full(),
10204 multi_buffer,
10205 Some(project.clone()),
10206 window,
10207 cx,
10208 )
10209 });
10210
10211 multi_buffer_editor.update_in(cx, |editor, window, cx| {
10212 editor.change_selections(
10213 SelectionEffects::scroll(Autoscroll::Next),
10214 window,
10215 cx,
10216 |s| s.select_ranges(Some(1..2)),
10217 );
10218 editor.insert("|one|two|three|", window, cx);
10219 });
10220 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
10221 multi_buffer_editor.update_in(cx, |editor, window, cx| {
10222 editor.change_selections(
10223 SelectionEffects::scroll(Autoscroll::Next),
10224 window,
10225 cx,
10226 |s| s.select_ranges(Some(60..70)),
10227 );
10228 editor.insert("|four|five|six|", window, cx);
10229 });
10230 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
10231
10232 // First two buffers should be edited, but not the third one.
10233 assert_eq!(
10234 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
10235 "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}",
10236 );
10237 buffer_1.update(cx, |buffer, _| {
10238 assert!(buffer.is_dirty());
10239 assert_eq!(
10240 buffer.text(),
10241 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
10242 )
10243 });
10244 buffer_2.update(cx, |buffer, _| {
10245 assert!(buffer.is_dirty());
10246 assert_eq!(
10247 buffer.text(),
10248 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
10249 )
10250 });
10251 buffer_3.update(cx, |buffer, _| {
10252 assert!(!buffer.is_dirty());
10253 assert_eq!(buffer.text(), sample_text_3,)
10254 });
10255 cx.executor().run_until_parked();
10256
10257 cx.executor().start_waiting();
10258 let save = multi_buffer_editor
10259 .update_in(cx, |editor, window, cx| {
10260 editor.save(
10261 SaveOptions {
10262 format: true,
10263 autosave: false,
10264 },
10265 project.clone(),
10266 window,
10267 cx,
10268 )
10269 })
10270 .unwrap();
10271
10272 let fake_server = fake_servers.next().await.unwrap();
10273 fake_server
10274 .server
10275 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
10276 Ok(Some(vec![lsp::TextEdit::new(
10277 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
10278 format!("[{} formatted]", params.text_document.uri),
10279 )]))
10280 })
10281 .detach();
10282 save.await;
10283
10284 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
10285 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
10286 assert_eq!(
10287 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
10288 uri!(
10289 "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}"
10290 ),
10291 );
10292 buffer_1.update(cx, |buffer, _| {
10293 assert!(!buffer.is_dirty());
10294 assert_eq!(
10295 buffer.text(),
10296 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
10297 )
10298 });
10299 buffer_2.update(cx, |buffer, _| {
10300 assert!(!buffer.is_dirty());
10301 assert_eq!(
10302 buffer.text(),
10303 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
10304 )
10305 });
10306 buffer_3.update(cx, |buffer, _| {
10307 assert!(!buffer.is_dirty());
10308 assert_eq!(buffer.text(), sample_text_3,)
10309 });
10310}
10311
10312#[gpui::test]
10313async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
10314 init_test(cx, |_| {});
10315
10316 let fs = FakeFs::new(cx.executor());
10317 fs.insert_tree(
10318 path!("/dir"),
10319 json!({
10320 "file1.rs": "fn main() { println!(\"hello\"); }",
10321 "file2.rs": "fn test() { println!(\"test\"); }",
10322 "file3.rs": "fn other() { println!(\"other\"); }\n",
10323 }),
10324 )
10325 .await;
10326
10327 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
10328 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
10329 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
10330
10331 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10332 language_registry.add(rust_lang());
10333
10334 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
10335 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
10336
10337 // Open three buffers
10338 let buffer_1 = project
10339 .update(cx, |project, cx| {
10340 project.open_buffer((worktree_id, "file1.rs"), cx)
10341 })
10342 .await
10343 .unwrap();
10344 let buffer_2 = project
10345 .update(cx, |project, cx| {
10346 project.open_buffer((worktree_id, "file2.rs"), cx)
10347 })
10348 .await
10349 .unwrap();
10350 let buffer_3 = project
10351 .update(cx, |project, cx| {
10352 project.open_buffer((worktree_id, "file3.rs"), cx)
10353 })
10354 .await
10355 .unwrap();
10356
10357 // Create a multi-buffer with all three buffers
10358 let multi_buffer = cx.new(|cx| {
10359 let mut multi_buffer = MultiBuffer::new(ReadWrite);
10360 multi_buffer.push_excerpts(
10361 buffer_1.clone(),
10362 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
10363 cx,
10364 );
10365 multi_buffer.push_excerpts(
10366 buffer_2.clone(),
10367 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
10368 cx,
10369 );
10370 multi_buffer.push_excerpts(
10371 buffer_3.clone(),
10372 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
10373 cx,
10374 );
10375 multi_buffer
10376 });
10377
10378 let editor = cx.new_window_entity(|window, cx| {
10379 Editor::new(
10380 EditorMode::full(),
10381 multi_buffer,
10382 Some(project.clone()),
10383 window,
10384 cx,
10385 )
10386 });
10387
10388 // Edit only the first buffer
10389 editor.update_in(cx, |editor, window, cx| {
10390 editor.change_selections(
10391 SelectionEffects::scroll(Autoscroll::Next),
10392 window,
10393 cx,
10394 |s| s.select_ranges(Some(10..10)),
10395 );
10396 editor.insert("// edited", window, cx);
10397 });
10398
10399 // Verify that only buffer 1 is dirty
10400 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
10401 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
10402 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
10403
10404 // Get write counts after file creation (files were created with initial content)
10405 // We expect each file to have been written once during creation
10406 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
10407 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
10408 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
10409
10410 // Perform autosave
10411 let save_task = editor.update_in(cx, |editor, window, cx| {
10412 editor.save(
10413 SaveOptions {
10414 format: true,
10415 autosave: true,
10416 },
10417 project.clone(),
10418 window,
10419 cx,
10420 )
10421 });
10422 save_task.await.unwrap();
10423
10424 // Only the dirty buffer should have been saved
10425 assert_eq!(
10426 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
10427 1,
10428 "Buffer 1 was dirty, so it should have been written once during autosave"
10429 );
10430 assert_eq!(
10431 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
10432 0,
10433 "Buffer 2 was clean, so it should not have been written during autosave"
10434 );
10435 assert_eq!(
10436 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
10437 0,
10438 "Buffer 3 was clean, so it should not have been written during autosave"
10439 );
10440
10441 // Verify buffer states after autosave
10442 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
10443 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
10444 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
10445
10446 // Now perform a manual save (format = true)
10447 let save_task = editor.update_in(cx, |editor, window, cx| {
10448 editor.save(
10449 SaveOptions {
10450 format: true,
10451 autosave: false,
10452 },
10453 project.clone(),
10454 window,
10455 cx,
10456 )
10457 });
10458 save_task.await.unwrap();
10459
10460 // During manual save, clean buffers don't get written to disk
10461 // They just get did_save called for language server notifications
10462 assert_eq!(
10463 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
10464 1,
10465 "Buffer 1 should only have been written once total (during autosave, not manual save)"
10466 );
10467 assert_eq!(
10468 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
10469 0,
10470 "Buffer 2 should not have been written at all"
10471 );
10472 assert_eq!(
10473 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
10474 0,
10475 "Buffer 3 should not have been written at all"
10476 );
10477}
10478
10479async fn setup_range_format_test(
10480 cx: &mut TestAppContext,
10481) -> (
10482 Entity<Project>,
10483 Entity<Editor>,
10484 &mut gpui::VisualTestContext,
10485 lsp::FakeLanguageServer,
10486) {
10487 init_test(cx, |_| {});
10488
10489 let fs = FakeFs::new(cx.executor());
10490 fs.insert_file(path!("/file.rs"), Default::default()).await;
10491
10492 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10493
10494 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10495 language_registry.add(rust_lang());
10496 let mut fake_servers = language_registry.register_fake_lsp(
10497 "Rust",
10498 FakeLspAdapter {
10499 capabilities: lsp::ServerCapabilities {
10500 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
10501 ..lsp::ServerCapabilities::default()
10502 },
10503 ..FakeLspAdapter::default()
10504 },
10505 );
10506
10507 let buffer = project
10508 .update(cx, |project, cx| {
10509 project.open_local_buffer(path!("/file.rs"), cx)
10510 })
10511 .await
10512 .unwrap();
10513
10514 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10515 let (editor, cx) = cx.add_window_view(|window, cx| {
10516 build_editor_with_project(project.clone(), buffer, window, cx)
10517 });
10518
10519 cx.executor().start_waiting();
10520 let fake_server = fake_servers.next().await.unwrap();
10521
10522 (project, editor, cx, fake_server)
10523}
10524
10525#[gpui::test]
10526async fn test_range_format_on_save_success(cx: &mut TestAppContext) {
10527 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
10528
10529 editor.update_in(cx, |editor, window, cx| {
10530 editor.set_text("one\ntwo\nthree\n", window, cx)
10531 });
10532 assert!(cx.read(|cx| editor.is_dirty(cx)));
10533
10534 let save = editor
10535 .update_in(cx, |editor, window, cx| {
10536 editor.save(
10537 SaveOptions {
10538 format: true,
10539 autosave: false,
10540 },
10541 project.clone(),
10542 window,
10543 cx,
10544 )
10545 })
10546 .unwrap();
10547 fake_server
10548 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
10549 assert_eq!(
10550 params.text_document.uri,
10551 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
10552 );
10553 assert_eq!(params.options.tab_size, 4);
10554 Ok(Some(vec![lsp::TextEdit::new(
10555 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
10556 ", ".to_string(),
10557 )]))
10558 })
10559 .next()
10560 .await;
10561 cx.executor().start_waiting();
10562 save.await;
10563 assert_eq!(
10564 editor.update(cx, |editor, cx| editor.text(cx)),
10565 "one, two\nthree\n"
10566 );
10567 assert!(!cx.read(|cx| editor.is_dirty(cx)));
10568}
10569
10570#[gpui::test]
10571async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) {
10572 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
10573
10574 editor.update_in(cx, |editor, window, cx| {
10575 editor.set_text("one\ntwo\nthree\n", window, cx)
10576 });
10577 assert!(cx.read(|cx| editor.is_dirty(cx)));
10578
10579 // Test that save still works when formatting hangs
10580 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
10581 move |params, _| async move {
10582 assert_eq!(
10583 params.text_document.uri,
10584 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
10585 );
10586 futures::future::pending::<()>().await;
10587 unreachable!()
10588 },
10589 );
10590 let save = editor
10591 .update_in(cx, |editor, window, cx| {
10592 editor.save(
10593 SaveOptions {
10594 format: true,
10595 autosave: false,
10596 },
10597 project.clone(),
10598 window,
10599 cx,
10600 )
10601 })
10602 .unwrap();
10603 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
10604 cx.executor().start_waiting();
10605 save.await;
10606 assert_eq!(
10607 editor.update(cx, |editor, cx| editor.text(cx)),
10608 "one\ntwo\nthree\n"
10609 );
10610 assert!(!cx.read(|cx| editor.is_dirty(cx)));
10611}
10612
10613#[gpui::test]
10614async fn test_range_format_not_called_for_clean_buffer(cx: &mut TestAppContext) {
10615 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
10616
10617 // Buffer starts clean, no formatting should be requested
10618 let save = editor
10619 .update_in(cx, |editor, window, cx| {
10620 editor.save(
10621 SaveOptions {
10622 format: false,
10623 autosave: false,
10624 },
10625 project.clone(),
10626 window,
10627 cx,
10628 )
10629 })
10630 .unwrap();
10631 let _pending_format_request = fake_server
10632 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
10633 panic!("Should not be invoked");
10634 })
10635 .next();
10636 cx.executor().start_waiting();
10637 save.await;
10638 cx.run_until_parked();
10639}
10640
10641#[gpui::test]
10642async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppContext) {
10643 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
10644
10645 // Set Rust language override and assert overridden tabsize is sent to language server
10646 update_test_language_settings(cx, |settings| {
10647 settings.languages.0.insert(
10648 "Rust".into(),
10649 LanguageSettingsContent {
10650 tab_size: NonZeroU32::new(8),
10651 ..Default::default()
10652 },
10653 );
10654 });
10655
10656 editor.update_in(cx, |editor, window, cx| {
10657 editor.set_text("something_new\n", window, cx)
10658 });
10659 assert!(cx.read(|cx| editor.is_dirty(cx)));
10660 let save = editor
10661 .update_in(cx, |editor, window, cx| {
10662 editor.save(
10663 SaveOptions {
10664 format: true,
10665 autosave: false,
10666 },
10667 project.clone(),
10668 window,
10669 cx,
10670 )
10671 })
10672 .unwrap();
10673 fake_server
10674 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
10675 assert_eq!(
10676 params.text_document.uri,
10677 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
10678 );
10679 assert_eq!(params.options.tab_size, 8);
10680 Ok(Some(Vec::new()))
10681 })
10682 .next()
10683 .await;
10684 save.await;
10685}
10686
10687#[gpui::test]
10688async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
10689 init_test(cx, |settings| {
10690 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
10691 Formatter::LanguageServer { name: None },
10692 )))
10693 });
10694
10695 let fs = FakeFs::new(cx.executor());
10696 fs.insert_file(path!("/file.rs"), Default::default()).await;
10697
10698 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10699
10700 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10701 language_registry.add(Arc::new(Language::new(
10702 LanguageConfig {
10703 name: "Rust".into(),
10704 matcher: LanguageMatcher {
10705 path_suffixes: vec!["rs".to_string()],
10706 ..Default::default()
10707 },
10708 ..LanguageConfig::default()
10709 },
10710 Some(tree_sitter_rust::LANGUAGE.into()),
10711 )));
10712 update_test_language_settings(cx, |settings| {
10713 // Enable Prettier formatting for the same buffer, and ensure
10714 // LSP is called instead of Prettier.
10715 settings.defaults.prettier = Some(PrettierSettings {
10716 allowed: true,
10717 ..PrettierSettings::default()
10718 });
10719 });
10720 let mut fake_servers = language_registry.register_fake_lsp(
10721 "Rust",
10722 FakeLspAdapter {
10723 capabilities: lsp::ServerCapabilities {
10724 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10725 ..Default::default()
10726 },
10727 ..Default::default()
10728 },
10729 );
10730
10731 let buffer = project
10732 .update(cx, |project, cx| {
10733 project.open_local_buffer(path!("/file.rs"), cx)
10734 })
10735 .await
10736 .unwrap();
10737
10738 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10739 let (editor, cx) = cx.add_window_view(|window, cx| {
10740 build_editor_with_project(project.clone(), buffer, window, cx)
10741 });
10742 editor.update_in(cx, |editor, window, cx| {
10743 editor.set_text("one\ntwo\nthree\n", window, cx)
10744 });
10745
10746 cx.executor().start_waiting();
10747 let fake_server = fake_servers.next().await.unwrap();
10748
10749 let format = editor
10750 .update_in(cx, |editor, window, cx| {
10751 editor.perform_format(
10752 project.clone(),
10753 FormatTrigger::Manual,
10754 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10755 window,
10756 cx,
10757 )
10758 })
10759 .unwrap();
10760 fake_server
10761 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
10762 assert_eq!(
10763 params.text_document.uri,
10764 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
10765 );
10766 assert_eq!(params.options.tab_size, 4);
10767 Ok(Some(vec![lsp::TextEdit::new(
10768 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
10769 ", ".to_string(),
10770 )]))
10771 })
10772 .next()
10773 .await;
10774 cx.executor().start_waiting();
10775 format.await;
10776 assert_eq!(
10777 editor.update(cx, |editor, cx| editor.text(cx)),
10778 "one, two\nthree\n"
10779 );
10780
10781 editor.update_in(cx, |editor, window, cx| {
10782 editor.set_text("one\ntwo\nthree\n", window, cx)
10783 });
10784 // Ensure we don't lock if formatting hangs.
10785 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10786 move |params, _| async move {
10787 assert_eq!(
10788 params.text_document.uri,
10789 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
10790 );
10791 futures::future::pending::<()>().await;
10792 unreachable!()
10793 },
10794 );
10795 let format = editor
10796 .update_in(cx, |editor, window, cx| {
10797 editor.perform_format(
10798 project,
10799 FormatTrigger::Manual,
10800 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10801 window,
10802 cx,
10803 )
10804 })
10805 .unwrap();
10806 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
10807 cx.executor().start_waiting();
10808 format.await;
10809 assert_eq!(
10810 editor.update(cx, |editor, cx| editor.text(cx)),
10811 "one\ntwo\nthree\n"
10812 );
10813}
10814
10815#[gpui::test]
10816async fn test_multiple_formatters(cx: &mut TestAppContext) {
10817 init_test(cx, |settings| {
10818 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
10819 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
10820 Formatter::LanguageServer { name: None },
10821 Formatter::CodeActions(
10822 [
10823 ("code-action-1".into(), true),
10824 ("code-action-2".into(), true),
10825 ]
10826 .into_iter()
10827 .collect(),
10828 ),
10829 ])))
10830 });
10831
10832 let fs = FakeFs::new(cx.executor());
10833 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
10834 .await;
10835
10836 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10837 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10838 language_registry.add(rust_lang());
10839
10840 let mut fake_servers = language_registry.register_fake_lsp(
10841 "Rust",
10842 FakeLspAdapter {
10843 capabilities: lsp::ServerCapabilities {
10844 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10845 execute_command_provider: Some(lsp::ExecuteCommandOptions {
10846 commands: vec!["the-command-for-code-action-1".into()],
10847 ..Default::default()
10848 }),
10849 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
10850 ..Default::default()
10851 },
10852 ..Default::default()
10853 },
10854 );
10855
10856 let buffer = project
10857 .update(cx, |project, cx| {
10858 project.open_local_buffer(path!("/file.rs"), cx)
10859 })
10860 .await
10861 .unwrap();
10862
10863 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10864 let (editor, cx) = cx.add_window_view(|window, cx| {
10865 build_editor_with_project(project.clone(), buffer, window, cx)
10866 });
10867
10868 cx.executor().start_waiting();
10869
10870 let fake_server = fake_servers.next().await.unwrap();
10871 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10872 move |_params, _| async move {
10873 Ok(Some(vec![lsp::TextEdit::new(
10874 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10875 "applied-formatting\n".to_string(),
10876 )]))
10877 },
10878 );
10879 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
10880 move |params, _| async move {
10881 assert_eq!(
10882 params.context.only,
10883 Some(vec!["code-action-1".into(), "code-action-2".into()])
10884 );
10885 let uri = lsp::Uri::from_file_path(path!("/file.rs")).unwrap();
10886 Ok(Some(vec![
10887 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
10888 kind: Some("code-action-1".into()),
10889 edit: Some(lsp::WorkspaceEdit::new(
10890 [(
10891 uri.clone(),
10892 vec![lsp::TextEdit::new(
10893 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10894 "applied-code-action-1-edit\n".to_string(),
10895 )],
10896 )]
10897 .into_iter()
10898 .collect(),
10899 )),
10900 command: Some(lsp::Command {
10901 command: "the-command-for-code-action-1".into(),
10902 ..Default::default()
10903 }),
10904 ..Default::default()
10905 }),
10906 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
10907 kind: Some("code-action-2".into()),
10908 edit: Some(lsp::WorkspaceEdit::new(
10909 [(
10910 uri,
10911 vec![lsp::TextEdit::new(
10912 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10913 "applied-code-action-2-edit\n".to_string(),
10914 )],
10915 )]
10916 .into_iter()
10917 .collect(),
10918 )),
10919 ..Default::default()
10920 }),
10921 ]))
10922 },
10923 );
10924
10925 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
10926 move |params, _| async move { Ok(params) }
10927 });
10928
10929 let command_lock = Arc::new(futures::lock::Mutex::new(()));
10930 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
10931 let fake = fake_server.clone();
10932 let lock = command_lock.clone();
10933 move |params, _| {
10934 assert_eq!(params.command, "the-command-for-code-action-1");
10935 let fake = fake.clone();
10936 let lock = lock.clone();
10937 async move {
10938 lock.lock().await;
10939 fake.server
10940 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
10941 label: None,
10942 edit: lsp::WorkspaceEdit {
10943 changes: Some(
10944 [(
10945 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
10946 vec![lsp::TextEdit {
10947 range: lsp::Range::new(
10948 lsp::Position::new(0, 0),
10949 lsp::Position::new(0, 0),
10950 ),
10951 new_text: "applied-code-action-1-command\n".into(),
10952 }],
10953 )]
10954 .into_iter()
10955 .collect(),
10956 ),
10957 ..Default::default()
10958 },
10959 })
10960 .await
10961 .into_response()
10962 .unwrap();
10963 Ok(Some(json!(null)))
10964 }
10965 }
10966 });
10967
10968 cx.executor().start_waiting();
10969 editor
10970 .update_in(cx, |editor, window, cx| {
10971 editor.perform_format(
10972 project.clone(),
10973 FormatTrigger::Manual,
10974 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10975 window,
10976 cx,
10977 )
10978 })
10979 .unwrap()
10980 .await;
10981 editor.update(cx, |editor, cx| {
10982 assert_eq!(
10983 editor.text(cx),
10984 r#"
10985 applied-code-action-2-edit
10986 applied-code-action-1-command
10987 applied-code-action-1-edit
10988 applied-formatting
10989 one
10990 two
10991 three
10992 "#
10993 .unindent()
10994 );
10995 });
10996
10997 editor.update_in(cx, |editor, window, cx| {
10998 editor.undo(&Default::default(), window, cx);
10999 assert_eq!(editor.text(cx), "one \ntwo \nthree");
11000 });
11001
11002 // Perform a manual edit while waiting for an LSP command
11003 // that's being run as part of a formatting code action.
11004 let lock_guard = command_lock.lock().await;
11005 let format = editor
11006 .update_in(cx, |editor, window, cx| {
11007 editor.perform_format(
11008 project.clone(),
11009 FormatTrigger::Manual,
11010 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
11011 window,
11012 cx,
11013 )
11014 })
11015 .unwrap();
11016 cx.run_until_parked();
11017 editor.update(cx, |editor, cx| {
11018 assert_eq!(
11019 editor.text(cx),
11020 r#"
11021 applied-code-action-1-edit
11022 applied-formatting
11023 one
11024 two
11025 three
11026 "#
11027 .unindent()
11028 );
11029
11030 editor.buffer.update(cx, |buffer, cx| {
11031 let ix = buffer.len(cx);
11032 buffer.edit([(ix..ix, "edited\n")], None, cx);
11033 });
11034 });
11035
11036 // Allow the LSP command to proceed. Because the buffer was edited,
11037 // the second code action will not be run.
11038 drop(lock_guard);
11039 format.await;
11040 editor.update_in(cx, |editor, window, cx| {
11041 assert_eq!(
11042 editor.text(cx),
11043 r#"
11044 applied-code-action-1-command
11045 applied-code-action-1-edit
11046 applied-formatting
11047 one
11048 two
11049 three
11050 edited
11051 "#
11052 .unindent()
11053 );
11054
11055 // The manual edit is undone first, because it is the last thing the user did
11056 // (even though the command completed afterwards).
11057 editor.undo(&Default::default(), window, cx);
11058 assert_eq!(
11059 editor.text(cx),
11060 r#"
11061 applied-code-action-1-command
11062 applied-code-action-1-edit
11063 applied-formatting
11064 one
11065 two
11066 three
11067 "#
11068 .unindent()
11069 );
11070
11071 // All the formatting (including the command, which completed after the manual edit)
11072 // is undone together.
11073 editor.undo(&Default::default(), window, cx);
11074 assert_eq!(editor.text(cx), "one \ntwo \nthree");
11075 });
11076}
11077
11078#[gpui::test]
11079async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
11080 init_test(cx, |settings| {
11081 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
11082 Formatter::LanguageServer { name: None },
11083 ])))
11084 });
11085
11086 let fs = FakeFs::new(cx.executor());
11087 fs.insert_file(path!("/file.ts"), Default::default()).await;
11088
11089 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11090
11091 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11092 language_registry.add(Arc::new(Language::new(
11093 LanguageConfig {
11094 name: "TypeScript".into(),
11095 matcher: LanguageMatcher {
11096 path_suffixes: vec!["ts".to_string()],
11097 ..Default::default()
11098 },
11099 ..LanguageConfig::default()
11100 },
11101 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
11102 )));
11103 update_test_language_settings(cx, |settings| {
11104 settings.defaults.prettier = Some(PrettierSettings {
11105 allowed: true,
11106 ..PrettierSettings::default()
11107 });
11108 });
11109 let mut fake_servers = language_registry.register_fake_lsp(
11110 "TypeScript",
11111 FakeLspAdapter {
11112 capabilities: lsp::ServerCapabilities {
11113 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
11114 ..Default::default()
11115 },
11116 ..Default::default()
11117 },
11118 );
11119
11120 let buffer = project
11121 .update(cx, |project, cx| {
11122 project.open_local_buffer(path!("/file.ts"), cx)
11123 })
11124 .await
11125 .unwrap();
11126
11127 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11128 let (editor, cx) = cx.add_window_view(|window, cx| {
11129 build_editor_with_project(project.clone(), buffer, window, cx)
11130 });
11131 editor.update_in(cx, |editor, window, cx| {
11132 editor.set_text(
11133 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
11134 window,
11135 cx,
11136 )
11137 });
11138
11139 cx.executor().start_waiting();
11140 let fake_server = fake_servers.next().await.unwrap();
11141
11142 let format = editor
11143 .update_in(cx, |editor, window, cx| {
11144 editor.perform_code_action_kind(
11145 project.clone(),
11146 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
11147 window,
11148 cx,
11149 )
11150 })
11151 .unwrap();
11152 fake_server
11153 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
11154 assert_eq!(
11155 params.text_document.uri,
11156 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
11157 );
11158 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
11159 lsp::CodeAction {
11160 title: "Organize Imports".to_string(),
11161 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
11162 edit: Some(lsp::WorkspaceEdit {
11163 changes: Some(
11164 [(
11165 params.text_document.uri.clone(),
11166 vec![lsp::TextEdit::new(
11167 lsp::Range::new(
11168 lsp::Position::new(1, 0),
11169 lsp::Position::new(2, 0),
11170 ),
11171 "".to_string(),
11172 )],
11173 )]
11174 .into_iter()
11175 .collect(),
11176 ),
11177 ..Default::default()
11178 }),
11179 ..Default::default()
11180 },
11181 )]))
11182 })
11183 .next()
11184 .await;
11185 cx.executor().start_waiting();
11186 format.await;
11187 assert_eq!(
11188 editor.update(cx, |editor, cx| editor.text(cx)),
11189 "import { a } from 'module';\n\nconst x = a;\n"
11190 );
11191
11192 editor.update_in(cx, |editor, window, cx| {
11193 editor.set_text(
11194 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
11195 window,
11196 cx,
11197 )
11198 });
11199 // Ensure we don't lock if code action hangs.
11200 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
11201 move |params, _| async move {
11202 assert_eq!(
11203 params.text_document.uri,
11204 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
11205 );
11206 futures::future::pending::<()>().await;
11207 unreachable!()
11208 },
11209 );
11210 let format = editor
11211 .update_in(cx, |editor, window, cx| {
11212 editor.perform_code_action_kind(
11213 project,
11214 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
11215 window,
11216 cx,
11217 )
11218 })
11219 .unwrap();
11220 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
11221 cx.executor().start_waiting();
11222 format.await;
11223 assert_eq!(
11224 editor.update(cx, |editor, cx| editor.text(cx)),
11225 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
11226 );
11227}
11228
11229#[gpui::test]
11230async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
11231 init_test(cx, |_| {});
11232
11233 let mut cx = EditorLspTestContext::new_rust(
11234 lsp::ServerCapabilities {
11235 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11236 ..Default::default()
11237 },
11238 cx,
11239 )
11240 .await;
11241
11242 cx.set_state(indoc! {"
11243 one.twoˇ
11244 "});
11245
11246 // The format request takes a long time. When it completes, it inserts
11247 // a newline and an indent before the `.`
11248 cx.lsp
11249 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
11250 let executor = cx.background_executor().clone();
11251 async move {
11252 executor.timer(Duration::from_millis(100)).await;
11253 Ok(Some(vec![lsp::TextEdit {
11254 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
11255 new_text: "\n ".into(),
11256 }]))
11257 }
11258 });
11259
11260 // Submit a format request.
11261 let format_1 = cx
11262 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
11263 .unwrap();
11264 cx.executor().run_until_parked();
11265
11266 // Submit a second format request.
11267 let format_2 = cx
11268 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
11269 .unwrap();
11270 cx.executor().run_until_parked();
11271
11272 // Wait for both format requests to complete
11273 cx.executor().advance_clock(Duration::from_millis(200));
11274 cx.executor().start_waiting();
11275 format_1.await.unwrap();
11276 cx.executor().start_waiting();
11277 format_2.await.unwrap();
11278
11279 // The formatting edits only happens once.
11280 cx.assert_editor_state(indoc! {"
11281 one
11282 .twoˇ
11283 "});
11284}
11285
11286#[gpui::test]
11287async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
11288 init_test(cx, |settings| {
11289 settings.defaults.formatter = Some(SelectedFormatter::Auto)
11290 });
11291
11292 let mut cx = EditorLspTestContext::new_rust(
11293 lsp::ServerCapabilities {
11294 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11295 ..Default::default()
11296 },
11297 cx,
11298 )
11299 .await;
11300
11301 // Set up a buffer white some trailing whitespace and no trailing newline.
11302 cx.set_state(
11303 &[
11304 "one ", //
11305 "twoˇ", //
11306 "three ", //
11307 "four", //
11308 ]
11309 .join("\n"),
11310 );
11311
11312 // Submit a format request.
11313 let format = cx
11314 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
11315 .unwrap();
11316
11317 // Record which buffer changes have been sent to the language server
11318 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
11319 cx.lsp
11320 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
11321 let buffer_changes = buffer_changes.clone();
11322 move |params, _| {
11323 buffer_changes.lock().extend(
11324 params
11325 .content_changes
11326 .into_iter()
11327 .map(|e| (e.range.unwrap(), e.text)),
11328 );
11329 }
11330 });
11331
11332 // Handle formatting requests to the language server.
11333 cx.lsp
11334 .set_request_handler::<lsp::request::Formatting, _, _>({
11335 let buffer_changes = buffer_changes.clone();
11336 move |_, _| {
11337 // When formatting is requested, trailing whitespace has already been stripped,
11338 // and the trailing newline has already been added.
11339 assert_eq!(
11340 &buffer_changes.lock()[1..],
11341 &[
11342 (
11343 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
11344 "".into()
11345 ),
11346 (
11347 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
11348 "".into()
11349 ),
11350 (
11351 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
11352 "\n".into()
11353 ),
11354 ]
11355 );
11356
11357 // Insert blank lines between each line of the buffer.
11358 async move {
11359 Ok(Some(vec![
11360 lsp::TextEdit {
11361 range: lsp::Range::new(
11362 lsp::Position::new(1, 0),
11363 lsp::Position::new(1, 0),
11364 ),
11365 new_text: "\n".into(),
11366 },
11367 lsp::TextEdit {
11368 range: lsp::Range::new(
11369 lsp::Position::new(2, 0),
11370 lsp::Position::new(2, 0),
11371 ),
11372 new_text: "\n".into(),
11373 },
11374 ]))
11375 }
11376 }
11377 });
11378
11379 // After formatting the buffer, the trailing whitespace is stripped,
11380 // a newline is appended, and the edits provided by the language server
11381 // have been applied.
11382 format.await.unwrap();
11383 cx.assert_editor_state(
11384 &[
11385 "one", //
11386 "", //
11387 "twoˇ", //
11388 "", //
11389 "three", //
11390 "four", //
11391 "", //
11392 ]
11393 .join("\n"),
11394 );
11395
11396 // Undoing the formatting undoes the trailing whitespace removal, the
11397 // trailing newline, and the LSP edits.
11398 cx.update_buffer(|buffer, cx| buffer.undo(cx));
11399 cx.assert_editor_state(
11400 &[
11401 "one ", //
11402 "twoˇ", //
11403 "three ", //
11404 "four", //
11405 ]
11406 .join("\n"),
11407 );
11408}
11409
11410#[gpui::test]
11411async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
11412 cx: &mut TestAppContext,
11413) {
11414 init_test(cx, |_| {});
11415
11416 cx.update(|cx| {
11417 cx.update_global::<SettingsStore, _>(|settings, cx| {
11418 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11419 settings.auto_signature_help = Some(true);
11420 });
11421 });
11422 });
11423
11424 let mut cx = EditorLspTestContext::new_rust(
11425 lsp::ServerCapabilities {
11426 signature_help_provider: Some(lsp::SignatureHelpOptions {
11427 ..Default::default()
11428 }),
11429 ..Default::default()
11430 },
11431 cx,
11432 )
11433 .await;
11434
11435 let language = Language::new(
11436 LanguageConfig {
11437 name: "Rust".into(),
11438 brackets: BracketPairConfig {
11439 pairs: vec![
11440 BracketPair {
11441 start: "{".to_string(),
11442 end: "}".to_string(),
11443 close: true,
11444 surround: true,
11445 newline: true,
11446 },
11447 BracketPair {
11448 start: "(".to_string(),
11449 end: ")".to_string(),
11450 close: true,
11451 surround: true,
11452 newline: true,
11453 },
11454 BracketPair {
11455 start: "/*".to_string(),
11456 end: " */".to_string(),
11457 close: true,
11458 surround: true,
11459 newline: true,
11460 },
11461 BracketPair {
11462 start: "[".to_string(),
11463 end: "]".to_string(),
11464 close: false,
11465 surround: false,
11466 newline: true,
11467 },
11468 BracketPair {
11469 start: "\"".to_string(),
11470 end: "\"".to_string(),
11471 close: true,
11472 surround: true,
11473 newline: false,
11474 },
11475 BracketPair {
11476 start: "<".to_string(),
11477 end: ">".to_string(),
11478 close: false,
11479 surround: true,
11480 newline: true,
11481 },
11482 ],
11483 ..Default::default()
11484 },
11485 autoclose_before: "})]".to_string(),
11486 ..Default::default()
11487 },
11488 Some(tree_sitter_rust::LANGUAGE.into()),
11489 );
11490 let language = Arc::new(language);
11491
11492 cx.language_registry().add(language.clone());
11493 cx.update_buffer(|buffer, cx| {
11494 buffer.set_language(Some(language), cx);
11495 });
11496
11497 cx.set_state(
11498 &r#"
11499 fn main() {
11500 sampleˇ
11501 }
11502 "#
11503 .unindent(),
11504 );
11505
11506 cx.update_editor(|editor, window, cx| {
11507 editor.handle_input("(", window, cx);
11508 });
11509 cx.assert_editor_state(
11510 &"
11511 fn main() {
11512 sample(ˇ)
11513 }
11514 "
11515 .unindent(),
11516 );
11517
11518 let mocked_response = lsp::SignatureHelp {
11519 signatures: vec![lsp::SignatureInformation {
11520 label: "fn sample(param1: u8, param2: u8)".to_string(),
11521 documentation: None,
11522 parameters: Some(vec![
11523 lsp::ParameterInformation {
11524 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11525 documentation: None,
11526 },
11527 lsp::ParameterInformation {
11528 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11529 documentation: None,
11530 },
11531 ]),
11532 active_parameter: None,
11533 }],
11534 active_signature: Some(0),
11535 active_parameter: Some(0),
11536 };
11537 handle_signature_help_request(&mut cx, mocked_response).await;
11538
11539 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11540 .await;
11541
11542 cx.editor(|editor, _, _| {
11543 let signature_help_state = editor.signature_help_state.popover().cloned();
11544 let signature = signature_help_state.unwrap();
11545 assert_eq!(
11546 signature.signatures[signature.current_signature].label,
11547 "fn sample(param1: u8, param2: u8)"
11548 );
11549 });
11550}
11551
11552#[gpui::test]
11553async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
11554 init_test(cx, |_| {});
11555
11556 cx.update(|cx| {
11557 cx.update_global::<SettingsStore, _>(|settings, cx| {
11558 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11559 settings.auto_signature_help = Some(false);
11560 settings.show_signature_help_after_edits = Some(false);
11561 });
11562 });
11563 });
11564
11565 let mut cx = EditorLspTestContext::new_rust(
11566 lsp::ServerCapabilities {
11567 signature_help_provider: Some(lsp::SignatureHelpOptions {
11568 ..Default::default()
11569 }),
11570 ..Default::default()
11571 },
11572 cx,
11573 )
11574 .await;
11575
11576 let language = Language::new(
11577 LanguageConfig {
11578 name: "Rust".into(),
11579 brackets: BracketPairConfig {
11580 pairs: vec![
11581 BracketPair {
11582 start: "{".to_string(),
11583 end: "}".to_string(),
11584 close: true,
11585 surround: true,
11586 newline: true,
11587 },
11588 BracketPair {
11589 start: "(".to_string(),
11590 end: ")".to_string(),
11591 close: true,
11592 surround: true,
11593 newline: true,
11594 },
11595 BracketPair {
11596 start: "/*".to_string(),
11597 end: " */".to_string(),
11598 close: true,
11599 surround: true,
11600 newline: true,
11601 },
11602 BracketPair {
11603 start: "[".to_string(),
11604 end: "]".to_string(),
11605 close: false,
11606 surround: false,
11607 newline: true,
11608 },
11609 BracketPair {
11610 start: "\"".to_string(),
11611 end: "\"".to_string(),
11612 close: true,
11613 surround: true,
11614 newline: false,
11615 },
11616 BracketPair {
11617 start: "<".to_string(),
11618 end: ">".to_string(),
11619 close: false,
11620 surround: true,
11621 newline: true,
11622 },
11623 ],
11624 ..Default::default()
11625 },
11626 autoclose_before: "})]".to_string(),
11627 ..Default::default()
11628 },
11629 Some(tree_sitter_rust::LANGUAGE.into()),
11630 );
11631 let language = Arc::new(language);
11632
11633 cx.language_registry().add(language.clone());
11634 cx.update_buffer(|buffer, cx| {
11635 buffer.set_language(Some(language), cx);
11636 });
11637
11638 // Ensure that signature_help is not called when no signature help is enabled.
11639 cx.set_state(
11640 &r#"
11641 fn main() {
11642 sampleˇ
11643 }
11644 "#
11645 .unindent(),
11646 );
11647 cx.update_editor(|editor, window, cx| {
11648 editor.handle_input("(", window, cx);
11649 });
11650 cx.assert_editor_state(
11651 &"
11652 fn main() {
11653 sample(ˇ)
11654 }
11655 "
11656 .unindent(),
11657 );
11658 cx.editor(|editor, _, _| {
11659 assert!(editor.signature_help_state.task().is_none());
11660 });
11661
11662 let mocked_response = lsp::SignatureHelp {
11663 signatures: vec![lsp::SignatureInformation {
11664 label: "fn sample(param1: u8, param2: u8)".to_string(),
11665 documentation: None,
11666 parameters: Some(vec![
11667 lsp::ParameterInformation {
11668 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11669 documentation: None,
11670 },
11671 lsp::ParameterInformation {
11672 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11673 documentation: None,
11674 },
11675 ]),
11676 active_parameter: None,
11677 }],
11678 active_signature: Some(0),
11679 active_parameter: Some(0),
11680 };
11681
11682 // Ensure that signature_help is called when enabled afte edits
11683 cx.update(|_, cx| {
11684 cx.update_global::<SettingsStore, _>(|settings, cx| {
11685 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11686 settings.auto_signature_help = Some(false);
11687 settings.show_signature_help_after_edits = Some(true);
11688 });
11689 });
11690 });
11691 cx.set_state(
11692 &r#"
11693 fn main() {
11694 sampleˇ
11695 }
11696 "#
11697 .unindent(),
11698 );
11699 cx.update_editor(|editor, window, cx| {
11700 editor.handle_input("(", window, cx);
11701 });
11702 cx.assert_editor_state(
11703 &"
11704 fn main() {
11705 sample(ˇ)
11706 }
11707 "
11708 .unindent(),
11709 );
11710 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11711 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11712 .await;
11713 cx.update_editor(|editor, _, _| {
11714 let signature_help_state = editor.signature_help_state.popover().cloned();
11715 assert!(signature_help_state.is_some());
11716 let signature = signature_help_state.unwrap();
11717 assert_eq!(
11718 signature.signatures[signature.current_signature].label,
11719 "fn sample(param1: u8, param2: u8)"
11720 );
11721 editor.signature_help_state = SignatureHelpState::default();
11722 });
11723
11724 // Ensure that signature_help is called when auto signature help override is enabled
11725 cx.update(|_, cx| {
11726 cx.update_global::<SettingsStore, _>(|settings, cx| {
11727 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11728 settings.auto_signature_help = Some(true);
11729 settings.show_signature_help_after_edits = Some(false);
11730 });
11731 });
11732 });
11733 cx.set_state(
11734 &r#"
11735 fn main() {
11736 sampleˇ
11737 }
11738 "#
11739 .unindent(),
11740 );
11741 cx.update_editor(|editor, window, cx| {
11742 editor.handle_input("(", window, cx);
11743 });
11744 cx.assert_editor_state(
11745 &"
11746 fn main() {
11747 sample(ˇ)
11748 }
11749 "
11750 .unindent(),
11751 );
11752 handle_signature_help_request(&mut cx, mocked_response).await;
11753 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11754 .await;
11755 cx.editor(|editor, _, _| {
11756 let signature_help_state = editor.signature_help_state.popover().cloned();
11757 assert!(signature_help_state.is_some());
11758 let signature = signature_help_state.unwrap();
11759 assert_eq!(
11760 signature.signatures[signature.current_signature].label,
11761 "fn sample(param1: u8, param2: u8)"
11762 );
11763 });
11764}
11765
11766#[gpui::test]
11767async fn test_signature_help(cx: &mut TestAppContext) {
11768 init_test(cx, |_| {});
11769 cx.update(|cx| {
11770 cx.update_global::<SettingsStore, _>(|settings, cx| {
11771 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11772 settings.auto_signature_help = Some(true);
11773 });
11774 });
11775 });
11776
11777 let mut cx = EditorLspTestContext::new_rust(
11778 lsp::ServerCapabilities {
11779 signature_help_provider: Some(lsp::SignatureHelpOptions {
11780 ..Default::default()
11781 }),
11782 ..Default::default()
11783 },
11784 cx,
11785 )
11786 .await;
11787
11788 // A test that directly calls `show_signature_help`
11789 cx.update_editor(|editor, window, cx| {
11790 editor.show_signature_help(&ShowSignatureHelp, window, cx);
11791 });
11792
11793 let mocked_response = lsp::SignatureHelp {
11794 signatures: vec![lsp::SignatureInformation {
11795 label: "fn sample(param1: u8, param2: u8)".to_string(),
11796 documentation: None,
11797 parameters: Some(vec![
11798 lsp::ParameterInformation {
11799 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11800 documentation: None,
11801 },
11802 lsp::ParameterInformation {
11803 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11804 documentation: None,
11805 },
11806 ]),
11807 active_parameter: None,
11808 }],
11809 active_signature: Some(0),
11810 active_parameter: Some(0),
11811 };
11812 handle_signature_help_request(&mut cx, mocked_response).await;
11813
11814 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11815 .await;
11816
11817 cx.editor(|editor, _, _| {
11818 let signature_help_state = editor.signature_help_state.popover().cloned();
11819 assert!(signature_help_state.is_some());
11820 let signature = signature_help_state.unwrap();
11821 assert_eq!(
11822 signature.signatures[signature.current_signature].label,
11823 "fn sample(param1: u8, param2: u8)"
11824 );
11825 });
11826
11827 // When exiting outside from inside the brackets, `signature_help` is closed.
11828 cx.set_state(indoc! {"
11829 fn main() {
11830 sample(ˇ);
11831 }
11832
11833 fn sample(param1: u8, param2: u8) {}
11834 "});
11835
11836 cx.update_editor(|editor, window, cx| {
11837 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11838 s.select_ranges([0..0])
11839 });
11840 });
11841
11842 let mocked_response = lsp::SignatureHelp {
11843 signatures: Vec::new(),
11844 active_signature: None,
11845 active_parameter: None,
11846 };
11847 handle_signature_help_request(&mut cx, mocked_response).await;
11848
11849 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
11850 .await;
11851
11852 cx.editor(|editor, _, _| {
11853 assert!(!editor.signature_help_state.is_shown());
11854 });
11855
11856 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
11857 cx.set_state(indoc! {"
11858 fn main() {
11859 sample(ˇ);
11860 }
11861
11862 fn sample(param1: u8, param2: u8) {}
11863 "});
11864
11865 let mocked_response = lsp::SignatureHelp {
11866 signatures: vec![lsp::SignatureInformation {
11867 label: "fn sample(param1: u8, param2: u8)".to_string(),
11868 documentation: None,
11869 parameters: Some(vec![
11870 lsp::ParameterInformation {
11871 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11872 documentation: None,
11873 },
11874 lsp::ParameterInformation {
11875 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11876 documentation: None,
11877 },
11878 ]),
11879 active_parameter: None,
11880 }],
11881 active_signature: Some(0),
11882 active_parameter: Some(0),
11883 };
11884 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11885 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11886 .await;
11887 cx.editor(|editor, _, _| {
11888 assert!(editor.signature_help_state.is_shown());
11889 });
11890
11891 // Restore the popover with more parameter input
11892 cx.set_state(indoc! {"
11893 fn main() {
11894 sample(param1, param2ˇ);
11895 }
11896
11897 fn sample(param1: u8, param2: u8) {}
11898 "});
11899
11900 let mocked_response = lsp::SignatureHelp {
11901 signatures: vec![lsp::SignatureInformation {
11902 label: "fn sample(param1: u8, param2: u8)".to_string(),
11903 documentation: None,
11904 parameters: Some(vec![
11905 lsp::ParameterInformation {
11906 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11907 documentation: None,
11908 },
11909 lsp::ParameterInformation {
11910 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11911 documentation: None,
11912 },
11913 ]),
11914 active_parameter: None,
11915 }],
11916 active_signature: Some(0),
11917 active_parameter: Some(1),
11918 };
11919 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11920 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11921 .await;
11922
11923 // When selecting a range, the popover is gone.
11924 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
11925 cx.update_editor(|editor, window, cx| {
11926 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11927 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
11928 })
11929 });
11930 cx.assert_editor_state(indoc! {"
11931 fn main() {
11932 sample(param1, «ˇparam2»);
11933 }
11934
11935 fn sample(param1: u8, param2: u8) {}
11936 "});
11937 cx.editor(|editor, _, _| {
11938 assert!(!editor.signature_help_state.is_shown());
11939 });
11940
11941 // When unselecting again, the popover is back if within the brackets.
11942 cx.update_editor(|editor, window, cx| {
11943 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11944 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11945 })
11946 });
11947 cx.assert_editor_state(indoc! {"
11948 fn main() {
11949 sample(param1, ˇparam2);
11950 }
11951
11952 fn sample(param1: u8, param2: u8) {}
11953 "});
11954 handle_signature_help_request(&mut cx, mocked_response).await;
11955 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11956 .await;
11957 cx.editor(|editor, _, _| {
11958 assert!(editor.signature_help_state.is_shown());
11959 });
11960
11961 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
11962 cx.update_editor(|editor, window, cx| {
11963 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11964 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
11965 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11966 })
11967 });
11968 cx.assert_editor_state(indoc! {"
11969 fn main() {
11970 sample(param1, ˇparam2);
11971 }
11972
11973 fn sample(param1: u8, param2: u8) {}
11974 "});
11975
11976 let mocked_response = lsp::SignatureHelp {
11977 signatures: vec![lsp::SignatureInformation {
11978 label: "fn sample(param1: u8, param2: u8)".to_string(),
11979 documentation: None,
11980 parameters: Some(vec![
11981 lsp::ParameterInformation {
11982 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11983 documentation: None,
11984 },
11985 lsp::ParameterInformation {
11986 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11987 documentation: None,
11988 },
11989 ]),
11990 active_parameter: None,
11991 }],
11992 active_signature: Some(0),
11993 active_parameter: Some(1),
11994 };
11995 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11996 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11997 .await;
11998 cx.update_editor(|editor, _, cx| {
11999 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
12000 });
12001 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
12002 .await;
12003 cx.update_editor(|editor, window, cx| {
12004 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12005 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
12006 })
12007 });
12008 cx.assert_editor_state(indoc! {"
12009 fn main() {
12010 sample(param1, «ˇparam2»);
12011 }
12012
12013 fn sample(param1: u8, param2: u8) {}
12014 "});
12015 cx.update_editor(|editor, window, cx| {
12016 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12017 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
12018 })
12019 });
12020 cx.assert_editor_state(indoc! {"
12021 fn main() {
12022 sample(param1, ˇparam2);
12023 }
12024
12025 fn sample(param1: u8, param2: u8) {}
12026 "});
12027 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
12028 .await;
12029}
12030
12031#[gpui::test]
12032async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
12033 init_test(cx, |_| {});
12034
12035 let mut cx = EditorLspTestContext::new_rust(
12036 lsp::ServerCapabilities {
12037 signature_help_provider: Some(lsp::SignatureHelpOptions {
12038 ..Default::default()
12039 }),
12040 ..Default::default()
12041 },
12042 cx,
12043 )
12044 .await;
12045
12046 cx.set_state(indoc! {"
12047 fn main() {
12048 overloadedˇ
12049 }
12050 "});
12051
12052 cx.update_editor(|editor, window, cx| {
12053 editor.handle_input("(", window, cx);
12054 editor.show_signature_help(&ShowSignatureHelp, window, cx);
12055 });
12056
12057 // Mock response with 3 signatures
12058 let mocked_response = lsp::SignatureHelp {
12059 signatures: vec![
12060 lsp::SignatureInformation {
12061 label: "fn overloaded(x: i32)".to_string(),
12062 documentation: None,
12063 parameters: Some(vec![lsp::ParameterInformation {
12064 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
12065 documentation: None,
12066 }]),
12067 active_parameter: None,
12068 },
12069 lsp::SignatureInformation {
12070 label: "fn overloaded(x: i32, y: i32)".to_string(),
12071 documentation: None,
12072 parameters: Some(vec![
12073 lsp::ParameterInformation {
12074 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
12075 documentation: None,
12076 },
12077 lsp::ParameterInformation {
12078 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
12079 documentation: None,
12080 },
12081 ]),
12082 active_parameter: None,
12083 },
12084 lsp::SignatureInformation {
12085 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
12086 documentation: None,
12087 parameters: Some(vec![
12088 lsp::ParameterInformation {
12089 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
12090 documentation: None,
12091 },
12092 lsp::ParameterInformation {
12093 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
12094 documentation: None,
12095 },
12096 lsp::ParameterInformation {
12097 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
12098 documentation: None,
12099 },
12100 ]),
12101 active_parameter: None,
12102 },
12103 ],
12104 active_signature: Some(1),
12105 active_parameter: Some(0),
12106 };
12107 handle_signature_help_request(&mut cx, mocked_response).await;
12108
12109 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12110 .await;
12111
12112 // Verify we have multiple signatures and the right one is selected
12113 cx.editor(|editor, _, _| {
12114 let popover = editor.signature_help_state.popover().cloned().unwrap();
12115 assert_eq!(popover.signatures.len(), 3);
12116 // active_signature was 1, so that should be the current
12117 assert_eq!(popover.current_signature, 1);
12118 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
12119 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
12120 assert_eq!(
12121 popover.signatures[2].label,
12122 "fn overloaded(x: i32, y: i32, z: i32)"
12123 );
12124 });
12125
12126 // Test navigation functionality
12127 cx.update_editor(|editor, window, cx| {
12128 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
12129 });
12130
12131 cx.editor(|editor, _, _| {
12132 let popover = editor.signature_help_state.popover().cloned().unwrap();
12133 assert_eq!(popover.current_signature, 2);
12134 });
12135
12136 // Test wrap around
12137 cx.update_editor(|editor, window, cx| {
12138 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
12139 });
12140
12141 cx.editor(|editor, _, _| {
12142 let popover = editor.signature_help_state.popover().cloned().unwrap();
12143 assert_eq!(popover.current_signature, 0);
12144 });
12145
12146 // Test previous navigation
12147 cx.update_editor(|editor, window, cx| {
12148 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
12149 });
12150
12151 cx.editor(|editor, _, _| {
12152 let popover = editor.signature_help_state.popover().cloned().unwrap();
12153 assert_eq!(popover.current_signature, 2);
12154 });
12155}
12156
12157#[gpui::test]
12158async fn test_completion_mode(cx: &mut TestAppContext) {
12159 init_test(cx, |_| {});
12160 let mut cx = EditorLspTestContext::new_rust(
12161 lsp::ServerCapabilities {
12162 completion_provider: Some(lsp::CompletionOptions {
12163 resolve_provider: Some(true),
12164 ..Default::default()
12165 }),
12166 ..Default::default()
12167 },
12168 cx,
12169 )
12170 .await;
12171
12172 struct Run {
12173 run_description: &'static str,
12174 initial_state: String,
12175 buffer_marked_text: String,
12176 completion_label: &'static str,
12177 completion_text: &'static str,
12178 expected_with_insert_mode: String,
12179 expected_with_replace_mode: String,
12180 expected_with_replace_subsequence_mode: String,
12181 expected_with_replace_suffix_mode: String,
12182 }
12183
12184 let runs = [
12185 Run {
12186 run_description: "Start of word matches completion text",
12187 initial_state: "before ediˇ after".into(),
12188 buffer_marked_text: "before <edi|> after".into(),
12189 completion_label: "editor",
12190 completion_text: "editor",
12191 expected_with_insert_mode: "before editorˇ after".into(),
12192 expected_with_replace_mode: "before editorˇ after".into(),
12193 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
12194 expected_with_replace_suffix_mode: "before editorˇ after".into(),
12195 },
12196 Run {
12197 run_description: "Accept same text at the middle of the word",
12198 initial_state: "before ediˇtor after".into(),
12199 buffer_marked_text: "before <edi|tor> after".into(),
12200 completion_label: "editor",
12201 completion_text: "editor",
12202 expected_with_insert_mode: "before editorˇtor after".into(),
12203 expected_with_replace_mode: "before editorˇ after".into(),
12204 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
12205 expected_with_replace_suffix_mode: "before editorˇ after".into(),
12206 },
12207 Run {
12208 run_description: "End of word matches completion text -- cursor at end",
12209 initial_state: "before torˇ after".into(),
12210 buffer_marked_text: "before <tor|> after".into(),
12211 completion_label: "editor",
12212 completion_text: "editor",
12213 expected_with_insert_mode: "before editorˇ after".into(),
12214 expected_with_replace_mode: "before editorˇ after".into(),
12215 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
12216 expected_with_replace_suffix_mode: "before editorˇ after".into(),
12217 },
12218 Run {
12219 run_description: "End of word matches completion text -- cursor at start",
12220 initial_state: "before ˇtor after".into(),
12221 buffer_marked_text: "before <|tor> after".into(),
12222 completion_label: "editor",
12223 completion_text: "editor",
12224 expected_with_insert_mode: "before editorˇtor after".into(),
12225 expected_with_replace_mode: "before editorˇ after".into(),
12226 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
12227 expected_with_replace_suffix_mode: "before editorˇ after".into(),
12228 },
12229 Run {
12230 run_description: "Prepend text containing whitespace",
12231 initial_state: "pˇfield: bool".into(),
12232 buffer_marked_text: "<p|field>: bool".into(),
12233 completion_label: "pub ",
12234 completion_text: "pub ",
12235 expected_with_insert_mode: "pub ˇfield: bool".into(),
12236 expected_with_replace_mode: "pub ˇ: bool".into(),
12237 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
12238 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
12239 },
12240 Run {
12241 run_description: "Add element to start of list",
12242 initial_state: "[element_ˇelement_2]".into(),
12243 buffer_marked_text: "[<element_|element_2>]".into(),
12244 completion_label: "element_1",
12245 completion_text: "element_1",
12246 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
12247 expected_with_replace_mode: "[element_1ˇ]".into(),
12248 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
12249 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
12250 },
12251 Run {
12252 run_description: "Add element to start of list -- first and second elements are equal",
12253 initial_state: "[elˇelement]".into(),
12254 buffer_marked_text: "[<el|element>]".into(),
12255 completion_label: "element",
12256 completion_text: "element",
12257 expected_with_insert_mode: "[elementˇelement]".into(),
12258 expected_with_replace_mode: "[elementˇ]".into(),
12259 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
12260 expected_with_replace_suffix_mode: "[elementˇ]".into(),
12261 },
12262 Run {
12263 run_description: "Ends with matching suffix",
12264 initial_state: "SubˇError".into(),
12265 buffer_marked_text: "<Sub|Error>".into(),
12266 completion_label: "SubscriptionError",
12267 completion_text: "SubscriptionError",
12268 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
12269 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
12270 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
12271 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
12272 },
12273 Run {
12274 run_description: "Suffix is a subsequence -- contiguous",
12275 initial_state: "SubˇErr".into(),
12276 buffer_marked_text: "<Sub|Err>".into(),
12277 completion_label: "SubscriptionError",
12278 completion_text: "SubscriptionError",
12279 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
12280 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
12281 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
12282 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
12283 },
12284 Run {
12285 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
12286 initial_state: "Suˇscrirr".into(),
12287 buffer_marked_text: "<Su|scrirr>".into(),
12288 completion_label: "SubscriptionError",
12289 completion_text: "SubscriptionError",
12290 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
12291 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
12292 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
12293 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
12294 },
12295 Run {
12296 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
12297 initial_state: "foo(indˇix)".into(),
12298 buffer_marked_text: "foo(<ind|ix>)".into(),
12299 completion_label: "node_index",
12300 completion_text: "node_index",
12301 expected_with_insert_mode: "foo(node_indexˇix)".into(),
12302 expected_with_replace_mode: "foo(node_indexˇ)".into(),
12303 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
12304 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
12305 },
12306 Run {
12307 run_description: "Replace range ends before cursor - should extend to cursor",
12308 initial_state: "before editˇo after".into(),
12309 buffer_marked_text: "before <{ed}>it|o after".into(),
12310 completion_label: "editor",
12311 completion_text: "editor",
12312 expected_with_insert_mode: "before editorˇo after".into(),
12313 expected_with_replace_mode: "before editorˇo after".into(),
12314 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
12315 expected_with_replace_suffix_mode: "before editorˇo after".into(),
12316 },
12317 Run {
12318 run_description: "Uses label for suffix matching",
12319 initial_state: "before ediˇtor after".into(),
12320 buffer_marked_text: "before <edi|tor> after".into(),
12321 completion_label: "editor",
12322 completion_text: "editor()",
12323 expected_with_insert_mode: "before editor()ˇtor after".into(),
12324 expected_with_replace_mode: "before editor()ˇ after".into(),
12325 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
12326 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
12327 },
12328 Run {
12329 run_description: "Case insensitive subsequence and suffix matching",
12330 initial_state: "before EDiˇtoR after".into(),
12331 buffer_marked_text: "before <EDi|toR> after".into(),
12332 completion_label: "editor",
12333 completion_text: "editor",
12334 expected_with_insert_mode: "before editorˇtoR after".into(),
12335 expected_with_replace_mode: "before editorˇ after".into(),
12336 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
12337 expected_with_replace_suffix_mode: "before editorˇ after".into(),
12338 },
12339 ];
12340
12341 for run in runs {
12342 let run_variations = [
12343 (LspInsertMode::Insert, run.expected_with_insert_mode),
12344 (LspInsertMode::Replace, run.expected_with_replace_mode),
12345 (
12346 LspInsertMode::ReplaceSubsequence,
12347 run.expected_with_replace_subsequence_mode,
12348 ),
12349 (
12350 LspInsertMode::ReplaceSuffix,
12351 run.expected_with_replace_suffix_mode,
12352 ),
12353 ];
12354
12355 for (lsp_insert_mode, expected_text) in run_variations {
12356 eprintln!(
12357 "run = {:?}, mode = {lsp_insert_mode:.?}",
12358 run.run_description,
12359 );
12360
12361 update_test_language_settings(&mut cx, |settings| {
12362 settings.defaults.completions = Some(CompletionSettings {
12363 lsp_insert_mode,
12364 words: WordsCompletionMode::Disabled,
12365 words_min_length: 0,
12366 lsp: true,
12367 lsp_fetch_timeout_ms: 0,
12368 });
12369 });
12370
12371 cx.set_state(&run.initial_state);
12372 cx.update_editor(|editor, window, cx| {
12373 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12374 });
12375
12376 let counter = Arc::new(AtomicUsize::new(0));
12377 handle_completion_request_with_insert_and_replace(
12378 &mut cx,
12379 &run.buffer_marked_text,
12380 vec![(run.completion_label, run.completion_text)],
12381 counter.clone(),
12382 )
12383 .await;
12384 cx.condition(|editor, _| editor.context_menu_visible())
12385 .await;
12386 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12387
12388 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12389 editor
12390 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12391 .unwrap()
12392 });
12393 cx.assert_editor_state(&expected_text);
12394 handle_resolve_completion_request(&mut cx, None).await;
12395 apply_additional_edits.await.unwrap();
12396 }
12397 }
12398}
12399
12400#[gpui::test]
12401async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
12402 init_test(cx, |_| {});
12403 let mut cx = EditorLspTestContext::new_rust(
12404 lsp::ServerCapabilities {
12405 completion_provider: Some(lsp::CompletionOptions {
12406 resolve_provider: Some(true),
12407 ..Default::default()
12408 }),
12409 ..Default::default()
12410 },
12411 cx,
12412 )
12413 .await;
12414
12415 let initial_state = "SubˇError";
12416 let buffer_marked_text = "<Sub|Error>";
12417 let completion_text = "SubscriptionError";
12418 let expected_with_insert_mode = "SubscriptionErrorˇError";
12419 let expected_with_replace_mode = "SubscriptionErrorˇ";
12420
12421 update_test_language_settings(&mut cx, |settings| {
12422 settings.defaults.completions = Some(CompletionSettings {
12423 words: WordsCompletionMode::Disabled,
12424 words_min_length: 0,
12425 // set the opposite here to ensure that the action is overriding the default behavior
12426 lsp_insert_mode: LspInsertMode::Insert,
12427 lsp: true,
12428 lsp_fetch_timeout_ms: 0,
12429 });
12430 });
12431
12432 cx.set_state(initial_state);
12433 cx.update_editor(|editor, window, cx| {
12434 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12435 });
12436
12437 let counter = Arc::new(AtomicUsize::new(0));
12438 handle_completion_request_with_insert_and_replace(
12439 &mut cx,
12440 buffer_marked_text,
12441 vec![(completion_text, completion_text)],
12442 counter.clone(),
12443 )
12444 .await;
12445 cx.condition(|editor, _| editor.context_menu_visible())
12446 .await;
12447 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12448
12449 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12450 editor
12451 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12452 .unwrap()
12453 });
12454 cx.assert_editor_state(expected_with_replace_mode);
12455 handle_resolve_completion_request(&mut cx, None).await;
12456 apply_additional_edits.await.unwrap();
12457
12458 update_test_language_settings(&mut cx, |settings| {
12459 settings.defaults.completions = Some(CompletionSettings {
12460 words: WordsCompletionMode::Disabled,
12461 words_min_length: 0,
12462 // set the opposite here to ensure that the action is overriding the default behavior
12463 lsp_insert_mode: LspInsertMode::Replace,
12464 lsp: true,
12465 lsp_fetch_timeout_ms: 0,
12466 });
12467 });
12468
12469 cx.set_state(initial_state);
12470 cx.update_editor(|editor, window, cx| {
12471 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12472 });
12473 handle_completion_request_with_insert_and_replace(
12474 &mut cx,
12475 buffer_marked_text,
12476 vec![(completion_text, completion_text)],
12477 counter.clone(),
12478 )
12479 .await;
12480 cx.condition(|editor, _| editor.context_menu_visible())
12481 .await;
12482 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12483
12484 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12485 editor
12486 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
12487 .unwrap()
12488 });
12489 cx.assert_editor_state(expected_with_insert_mode);
12490 handle_resolve_completion_request(&mut cx, None).await;
12491 apply_additional_edits.await.unwrap();
12492}
12493
12494#[gpui::test]
12495async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
12496 init_test(cx, |_| {});
12497 let mut cx = EditorLspTestContext::new_rust(
12498 lsp::ServerCapabilities {
12499 completion_provider: Some(lsp::CompletionOptions {
12500 resolve_provider: Some(true),
12501 ..Default::default()
12502 }),
12503 ..Default::default()
12504 },
12505 cx,
12506 )
12507 .await;
12508
12509 // scenario: surrounding text matches completion text
12510 let completion_text = "to_offset";
12511 let initial_state = indoc! {"
12512 1. buf.to_offˇsuffix
12513 2. buf.to_offˇsuf
12514 3. buf.to_offˇfix
12515 4. buf.to_offˇ
12516 5. into_offˇensive
12517 6. ˇsuffix
12518 7. let ˇ //
12519 8. aaˇzz
12520 9. buf.to_off«zzzzzˇ»suffix
12521 10. buf.«ˇzzzzz»suffix
12522 11. to_off«ˇzzzzz»
12523
12524 buf.to_offˇsuffix // newest cursor
12525 "};
12526 let completion_marked_buffer = indoc! {"
12527 1. buf.to_offsuffix
12528 2. buf.to_offsuf
12529 3. buf.to_offfix
12530 4. buf.to_off
12531 5. into_offensive
12532 6. suffix
12533 7. let //
12534 8. aazz
12535 9. buf.to_offzzzzzsuffix
12536 10. buf.zzzzzsuffix
12537 11. to_offzzzzz
12538
12539 buf.<to_off|suffix> // newest cursor
12540 "};
12541 let expected = indoc! {"
12542 1. buf.to_offsetˇ
12543 2. buf.to_offsetˇsuf
12544 3. buf.to_offsetˇfix
12545 4. buf.to_offsetˇ
12546 5. into_offsetˇensive
12547 6. to_offsetˇsuffix
12548 7. let to_offsetˇ //
12549 8. aato_offsetˇzz
12550 9. buf.to_offsetˇ
12551 10. buf.to_offsetˇsuffix
12552 11. to_offsetˇ
12553
12554 buf.to_offsetˇ // newest cursor
12555 "};
12556 cx.set_state(initial_state);
12557 cx.update_editor(|editor, window, cx| {
12558 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12559 });
12560 handle_completion_request_with_insert_and_replace(
12561 &mut cx,
12562 completion_marked_buffer,
12563 vec![(completion_text, completion_text)],
12564 Arc::new(AtomicUsize::new(0)),
12565 )
12566 .await;
12567 cx.condition(|editor, _| editor.context_menu_visible())
12568 .await;
12569 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12570 editor
12571 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12572 .unwrap()
12573 });
12574 cx.assert_editor_state(expected);
12575 handle_resolve_completion_request(&mut cx, None).await;
12576 apply_additional_edits.await.unwrap();
12577
12578 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
12579 let completion_text = "foo_and_bar";
12580 let initial_state = indoc! {"
12581 1. ooanbˇ
12582 2. zooanbˇ
12583 3. ooanbˇz
12584 4. zooanbˇz
12585 5. ooanˇ
12586 6. oanbˇ
12587
12588 ooanbˇ
12589 "};
12590 let completion_marked_buffer = indoc! {"
12591 1. ooanb
12592 2. zooanb
12593 3. ooanbz
12594 4. zooanbz
12595 5. ooan
12596 6. oanb
12597
12598 <ooanb|>
12599 "};
12600 let expected = indoc! {"
12601 1. foo_and_barˇ
12602 2. zfoo_and_barˇ
12603 3. foo_and_barˇz
12604 4. zfoo_and_barˇz
12605 5. ooanfoo_and_barˇ
12606 6. oanbfoo_and_barˇ
12607
12608 foo_and_barˇ
12609 "};
12610 cx.set_state(initial_state);
12611 cx.update_editor(|editor, window, cx| {
12612 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12613 });
12614 handle_completion_request_with_insert_and_replace(
12615 &mut cx,
12616 completion_marked_buffer,
12617 vec![(completion_text, completion_text)],
12618 Arc::new(AtomicUsize::new(0)),
12619 )
12620 .await;
12621 cx.condition(|editor, _| editor.context_menu_visible())
12622 .await;
12623 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12624 editor
12625 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12626 .unwrap()
12627 });
12628 cx.assert_editor_state(expected);
12629 handle_resolve_completion_request(&mut cx, None).await;
12630 apply_additional_edits.await.unwrap();
12631
12632 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
12633 // (expects the same as if it was inserted at the end)
12634 let completion_text = "foo_and_bar";
12635 let initial_state = indoc! {"
12636 1. ooˇanb
12637 2. zooˇanb
12638 3. ooˇanbz
12639 4. zooˇanbz
12640
12641 ooˇanb
12642 "};
12643 let completion_marked_buffer = indoc! {"
12644 1. ooanb
12645 2. zooanb
12646 3. ooanbz
12647 4. zooanbz
12648
12649 <oo|anb>
12650 "};
12651 let expected = indoc! {"
12652 1. foo_and_barˇ
12653 2. zfoo_and_barˇ
12654 3. foo_and_barˇz
12655 4. zfoo_and_barˇz
12656
12657 foo_and_barˇ
12658 "};
12659 cx.set_state(initial_state);
12660 cx.update_editor(|editor, window, cx| {
12661 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12662 });
12663 handle_completion_request_with_insert_and_replace(
12664 &mut cx,
12665 completion_marked_buffer,
12666 vec![(completion_text, completion_text)],
12667 Arc::new(AtomicUsize::new(0)),
12668 )
12669 .await;
12670 cx.condition(|editor, _| editor.context_menu_visible())
12671 .await;
12672 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12673 editor
12674 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12675 .unwrap()
12676 });
12677 cx.assert_editor_state(expected);
12678 handle_resolve_completion_request(&mut cx, None).await;
12679 apply_additional_edits.await.unwrap();
12680}
12681
12682// This used to crash
12683#[gpui::test]
12684async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
12685 init_test(cx, |_| {});
12686
12687 let buffer_text = indoc! {"
12688 fn main() {
12689 10.satu;
12690
12691 //
12692 // separate cursors so they open in different excerpts (manually reproducible)
12693 //
12694
12695 10.satu20;
12696 }
12697 "};
12698 let multibuffer_text_with_selections = indoc! {"
12699 fn main() {
12700 10.satuˇ;
12701
12702 //
12703
12704 //
12705
12706 10.satuˇ20;
12707 }
12708 "};
12709 let expected_multibuffer = indoc! {"
12710 fn main() {
12711 10.saturating_sub()ˇ;
12712
12713 //
12714
12715 //
12716
12717 10.saturating_sub()ˇ;
12718 }
12719 "};
12720
12721 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
12722 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
12723
12724 let fs = FakeFs::new(cx.executor());
12725 fs.insert_tree(
12726 path!("/a"),
12727 json!({
12728 "main.rs": buffer_text,
12729 }),
12730 )
12731 .await;
12732
12733 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12734 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12735 language_registry.add(rust_lang());
12736 let mut fake_servers = language_registry.register_fake_lsp(
12737 "Rust",
12738 FakeLspAdapter {
12739 capabilities: lsp::ServerCapabilities {
12740 completion_provider: Some(lsp::CompletionOptions {
12741 resolve_provider: None,
12742 ..lsp::CompletionOptions::default()
12743 }),
12744 ..lsp::ServerCapabilities::default()
12745 },
12746 ..FakeLspAdapter::default()
12747 },
12748 );
12749 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12750 let cx = &mut VisualTestContext::from_window(*workspace, cx);
12751 let buffer = project
12752 .update(cx, |project, cx| {
12753 project.open_local_buffer(path!("/a/main.rs"), cx)
12754 })
12755 .await
12756 .unwrap();
12757
12758 let multi_buffer = cx.new(|cx| {
12759 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
12760 multi_buffer.push_excerpts(
12761 buffer.clone(),
12762 [ExcerptRange::new(0..first_excerpt_end)],
12763 cx,
12764 );
12765 multi_buffer.push_excerpts(
12766 buffer.clone(),
12767 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
12768 cx,
12769 );
12770 multi_buffer
12771 });
12772
12773 let editor = workspace
12774 .update(cx, |_, window, cx| {
12775 cx.new(|cx| {
12776 Editor::new(
12777 EditorMode::Full {
12778 scale_ui_elements_with_buffer_font_size: false,
12779 show_active_line_background: false,
12780 sized_by_content: false,
12781 },
12782 multi_buffer.clone(),
12783 Some(project.clone()),
12784 window,
12785 cx,
12786 )
12787 })
12788 })
12789 .unwrap();
12790
12791 let pane = workspace
12792 .update(cx, |workspace, _, _| workspace.active_pane().clone())
12793 .unwrap();
12794 pane.update_in(cx, |pane, window, cx| {
12795 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
12796 });
12797
12798 let fake_server = fake_servers.next().await.unwrap();
12799
12800 editor.update_in(cx, |editor, window, cx| {
12801 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12802 s.select_ranges([
12803 Point::new(1, 11)..Point::new(1, 11),
12804 Point::new(7, 11)..Point::new(7, 11),
12805 ])
12806 });
12807
12808 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
12809 });
12810
12811 editor.update_in(cx, |editor, window, cx| {
12812 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12813 });
12814
12815 fake_server
12816 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
12817 let completion_item = lsp::CompletionItem {
12818 label: "saturating_sub()".into(),
12819 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
12820 lsp::InsertReplaceEdit {
12821 new_text: "saturating_sub()".to_owned(),
12822 insert: lsp::Range::new(
12823 lsp::Position::new(7, 7),
12824 lsp::Position::new(7, 11),
12825 ),
12826 replace: lsp::Range::new(
12827 lsp::Position::new(7, 7),
12828 lsp::Position::new(7, 13),
12829 ),
12830 },
12831 )),
12832 ..lsp::CompletionItem::default()
12833 };
12834
12835 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
12836 })
12837 .next()
12838 .await
12839 .unwrap();
12840
12841 cx.condition(&editor, |editor, _| editor.context_menu_visible())
12842 .await;
12843
12844 editor
12845 .update_in(cx, |editor, window, cx| {
12846 editor
12847 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12848 .unwrap()
12849 })
12850 .await
12851 .unwrap();
12852
12853 editor.update(cx, |editor, cx| {
12854 assert_text_with_selections(editor, expected_multibuffer, cx);
12855 })
12856}
12857
12858#[gpui::test]
12859async fn test_completion(cx: &mut TestAppContext) {
12860 init_test(cx, |_| {});
12861
12862 let mut cx = EditorLspTestContext::new_rust(
12863 lsp::ServerCapabilities {
12864 completion_provider: Some(lsp::CompletionOptions {
12865 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12866 resolve_provider: Some(true),
12867 ..Default::default()
12868 }),
12869 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12870 ..Default::default()
12871 },
12872 cx,
12873 )
12874 .await;
12875 let counter = Arc::new(AtomicUsize::new(0));
12876
12877 cx.set_state(indoc! {"
12878 oneˇ
12879 two
12880 three
12881 "});
12882 cx.simulate_keystroke(".");
12883 handle_completion_request(
12884 indoc! {"
12885 one.|<>
12886 two
12887 three
12888 "},
12889 vec!["first_completion", "second_completion"],
12890 true,
12891 counter.clone(),
12892 &mut cx,
12893 )
12894 .await;
12895 cx.condition(|editor, _| editor.context_menu_visible())
12896 .await;
12897 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12898
12899 let _handler = handle_signature_help_request(
12900 &mut cx,
12901 lsp::SignatureHelp {
12902 signatures: vec![lsp::SignatureInformation {
12903 label: "test signature".to_string(),
12904 documentation: None,
12905 parameters: Some(vec![lsp::ParameterInformation {
12906 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
12907 documentation: None,
12908 }]),
12909 active_parameter: None,
12910 }],
12911 active_signature: None,
12912 active_parameter: None,
12913 },
12914 );
12915 cx.update_editor(|editor, window, cx| {
12916 assert!(
12917 !editor.signature_help_state.is_shown(),
12918 "No signature help was called for"
12919 );
12920 editor.show_signature_help(&ShowSignatureHelp, window, cx);
12921 });
12922 cx.run_until_parked();
12923 cx.update_editor(|editor, _, _| {
12924 assert!(
12925 !editor.signature_help_state.is_shown(),
12926 "No signature help should be shown when completions menu is open"
12927 );
12928 });
12929
12930 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12931 editor.context_menu_next(&Default::default(), window, cx);
12932 editor
12933 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12934 .unwrap()
12935 });
12936 cx.assert_editor_state(indoc! {"
12937 one.second_completionˇ
12938 two
12939 three
12940 "});
12941
12942 handle_resolve_completion_request(
12943 &mut cx,
12944 Some(vec![
12945 (
12946 //This overlaps with the primary completion edit which is
12947 //misbehavior from the LSP spec, test that we filter it out
12948 indoc! {"
12949 one.second_ˇcompletion
12950 two
12951 threeˇ
12952 "},
12953 "overlapping additional edit",
12954 ),
12955 (
12956 indoc! {"
12957 one.second_completion
12958 two
12959 threeˇ
12960 "},
12961 "\nadditional edit",
12962 ),
12963 ]),
12964 )
12965 .await;
12966 apply_additional_edits.await.unwrap();
12967 cx.assert_editor_state(indoc! {"
12968 one.second_completionˇ
12969 two
12970 three
12971 additional edit
12972 "});
12973
12974 cx.set_state(indoc! {"
12975 one.second_completion
12976 twoˇ
12977 threeˇ
12978 additional edit
12979 "});
12980 cx.simulate_keystroke(" ");
12981 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12982 cx.simulate_keystroke("s");
12983 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12984
12985 cx.assert_editor_state(indoc! {"
12986 one.second_completion
12987 two sˇ
12988 three sˇ
12989 additional edit
12990 "});
12991 handle_completion_request(
12992 indoc! {"
12993 one.second_completion
12994 two s
12995 three <s|>
12996 additional edit
12997 "},
12998 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
12999 true,
13000 counter.clone(),
13001 &mut cx,
13002 )
13003 .await;
13004 cx.condition(|editor, _| editor.context_menu_visible())
13005 .await;
13006 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
13007
13008 cx.simulate_keystroke("i");
13009
13010 handle_completion_request(
13011 indoc! {"
13012 one.second_completion
13013 two si
13014 three <si|>
13015 additional edit
13016 "},
13017 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
13018 true,
13019 counter.clone(),
13020 &mut cx,
13021 )
13022 .await;
13023 cx.condition(|editor, _| editor.context_menu_visible())
13024 .await;
13025 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
13026
13027 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13028 editor
13029 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13030 .unwrap()
13031 });
13032 cx.assert_editor_state(indoc! {"
13033 one.second_completion
13034 two sixth_completionˇ
13035 three sixth_completionˇ
13036 additional edit
13037 "});
13038
13039 apply_additional_edits.await.unwrap();
13040
13041 update_test_language_settings(&mut cx, |settings| {
13042 settings.defaults.show_completions_on_input = Some(false);
13043 });
13044 cx.set_state("editorˇ");
13045 cx.simulate_keystroke(".");
13046 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
13047 cx.simulate_keystrokes("c l o");
13048 cx.assert_editor_state("editor.cloˇ");
13049 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
13050 cx.update_editor(|editor, window, cx| {
13051 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13052 });
13053 handle_completion_request(
13054 "editor.<clo|>",
13055 vec!["close", "clobber"],
13056 true,
13057 counter.clone(),
13058 &mut cx,
13059 )
13060 .await;
13061 cx.condition(|editor, _| editor.context_menu_visible())
13062 .await;
13063 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
13064
13065 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13066 editor
13067 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13068 .unwrap()
13069 });
13070 cx.assert_editor_state("editor.clobberˇ");
13071 handle_resolve_completion_request(&mut cx, None).await;
13072 apply_additional_edits.await.unwrap();
13073}
13074
13075#[gpui::test]
13076async fn test_completion_reuse(cx: &mut TestAppContext) {
13077 init_test(cx, |_| {});
13078
13079 let mut cx = EditorLspTestContext::new_rust(
13080 lsp::ServerCapabilities {
13081 completion_provider: Some(lsp::CompletionOptions {
13082 trigger_characters: Some(vec![".".to_string()]),
13083 ..Default::default()
13084 }),
13085 ..Default::default()
13086 },
13087 cx,
13088 )
13089 .await;
13090
13091 let counter = Arc::new(AtomicUsize::new(0));
13092 cx.set_state("objˇ");
13093 cx.simulate_keystroke(".");
13094
13095 // Initial completion request returns complete results
13096 let is_incomplete = false;
13097 handle_completion_request(
13098 "obj.|<>",
13099 vec!["a", "ab", "abc"],
13100 is_incomplete,
13101 counter.clone(),
13102 &mut cx,
13103 )
13104 .await;
13105 cx.run_until_parked();
13106 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13107 cx.assert_editor_state("obj.ˇ");
13108 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
13109
13110 // Type "a" - filters existing completions
13111 cx.simulate_keystroke("a");
13112 cx.run_until_parked();
13113 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13114 cx.assert_editor_state("obj.aˇ");
13115 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
13116
13117 // Type "b" - filters existing completions
13118 cx.simulate_keystroke("b");
13119 cx.run_until_parked();
13120 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13121 cx.assert_editor_state("obj.abˇ");
13122 check_displayed_completions(vec!["ab", "abc"], &mut cx);
13123
13124 // Type "c" - filters existing completions
13125 cx.simulate_keystroke("c");
13126 cx.run_until_parked();
13127 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13128 cx.assert_editor_state("obj.abcˇ");
13129 check_displayed_completions(vec!["abc"], &mut cx);
13130
13131 // Backspace to delete "c" - filters existing completions
13132 cx.update_editor(|editor, window, cx| {
13133 editor.backspace(&Backspace, window, cx);
13134 });
13135 cx.run_until_parked();
13136 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13137 cx.assert_editor_state("obj.abˇ");
13138 check_displayed_completions(vec!["ab", "abc"], &mut cx);
13139
13140 // Moving cursor to the left dismisses menu.
13141 cx.update_editor(|editor, window, cx| {
13142 editor.move_left(&MoveLeft, window, cx);
13143 });
13144 cx.run_until_parked();
13145 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13146 cx.assert_editor_state("obj.aˇb");
13147 cx.update_editor(|editor, _, _| {
13148 assert_eq!(editor.context_menu_visible(), false);
13149 });
13150
13151 // Type "b" - new request
13152 cx.simulate_keystroke("b");
13153 let is_incomplete = false;
13154 handle_completion_request(
13155 "obj.<ab|>a",
13156 vec!["ab", "abc"],
13157 is_incomplete,
13158 counter.clone(),
13159 &mut cx,
13160 )
13161 .await;
13162 cx.run_until_parked();
13163 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
13164 cx.assert_editor_state("obj.abˇb");
13165 check_displayed_completions(vec!["ab", "abc"], &mut cx);
13166
13167 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
13168 cx.update_editor(|editor, window, cx| {
13169 editor.backspace(&Backspace, window, cx);
13170 });
13171 let is_incomplete = false;
13172 handle_completion_request(
13173 "obj.<a|>b",
13174 vec!["a", "ab", "abc"],
13175 is_incomplete,
13176 counter.clone(),
13177 &mut cx,
13178 )
13179 .await;
13180 cx.run_until_parked();
13181 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
13182 cx.assert_editor_state("obj.aˇb");
13183 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
13184
13185 // Backspace to delete "a" - dismisses menu.
13186 cx.update_editor(|editor, window, cx| {
13187 editor.backspace(&Backspace, window, cx);
13188 });
13189 cx.run_until_parked();
13190 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
13191 cx.assert_editor_state("obj.ˇb");
13192 cx.update_editor(|editor, _, _| {
13193 assert_eq!(editor.context_menu_visible(), false);
13194 });
13195}
13196
13197#[gpui::test]
13198async fn test_word_completion(cx: &mut TestAppContext) {
13199 let lsp_fetch_timeout_ms = 10;
13200 init_test(cx, |language_settings| {
13201 language_settings.defaults.completions = Some(CompletionSettings {
13202 words: WordsCompletionMode::Fallback,
13203 words_min_length: 0,
13204 lsp: true,
13205 lsp_fetch_timeout_ms: 10,
13206 lsp_insert_mode: LspInsertMode::Insert,
13207 });
13208 });
13209
13210 let mut cx = EditorLspTestContext::new_rust(
13211 lsp::ServerCapabilities {
13212 completion_provider: Some(lsp::CompletionOptions {
13213 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
13214 ..lsp::CompletionOptions::default()
13215 }),
13216 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
13217 ..lsp::ServerCapabilities::default()
13218 },
13219 cx,
13220 )
13221 .await;
13222
13223 let throttle_completions = Arc::new(AtomicBool::new(false));
13224
13225 let lsp_throttle_completions = throttle_completions.clone();
13226 let _completion_requests_handler =
13227 cx.lsp
13228 .server
13229 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
13230 let lsp_throttle_completions = lsp_throttle_completions.clone();
13231 let cx = cx.clone();
13232 async move {
13233 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
13234 cx.background_executor()
13235 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
13236 .await;
13237 }
13238 Ok(Some(lsp::CompletionResponse::Array(vec![
13239 lsp::CompletionItem {
13240 label: "first".into(),
13241 ..lsp::CompletionItem::default()
13242 },
13243 lsp::CompletionItem {
13244 label: "last".into(),
13245 ..lsp::CompletionItem::default()
13246 },
13247 ])))
13248 }
13249 });
13250
13251 cx.set_state(indoc! {"
13252 oneˇ
13253 two
13254 three
13255 "});
13256 cx.simulate_keystroke(".");
13257 cx.executor().run_until_parked();
13258 cx.condition(|editor, _| editor.context_menu_visible())
13259 .await;
13260 cx.update_editor(|editor, window, cx| {
13261 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13262 {
13263 assert_eq!(
13264 completion_menu_entries(menu),
13265 &["first", "last"],
13266 "When LSP server is fast to reply, no fallback word completions are used"
13267 );
13268 } else {
13269 panic!("expected completion menu to be open");
13270 }
13271 editor.cancel(&Cancel, window, cx);
13272 });
13273 cx.executor().run_until_parked();
13274 cx.condition(|editor, _| !editor.context_menu_visible())
13275 .await;
13276
13277 throttle_completions.store(true, atomic::Ordering::Release);
13278 cx.simulate_keystroke(".");
13279 cx.executor()
13280 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
13281 cx.executor().run_until_parked();
13282 cx.condition(|editor, _| editor.context_menu_visible())
13283 .await;
13284 cx.update_editor(|editor, _, _| {
13285 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13286 {
13287 assert_eq!(completion_menu_entries(menu), &["one", "three", "two"],
13288 "When LSP server is slow, document words can be shown instead, if configured accordingly");
13289 } else {
13290 panic!("expected completion menu to be open");
13291 }
13292 });
13293}
13294
13295#[gpui::test]
13296async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
13297 init_test(cx, |language_settings| {
13298 language_settings.defaults.completions = Some(CompletionSettings {
13299 words: WordsCompletionMode::Enabled,
13300 words_min_length: 0,
13301 lsp: true,
13302 lsp_fetch_timeout_ms: 0,
13303 lsp_insert_mode: LspInsertMode::Insert,
13304 });
13305 });
13306
13307 let mut cx = EditorLspTestContext::new_rust(
13308 lsp::ServerCapabilities {
13309 completion_provider: Some(lsp::CompletionOptions {
13310 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
13311 ..lsp::CompletionOptions::default()
13312 }),
13313 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
13314 ..lsp::ServerCapabilities::default()
13315 },
13316 cx,
13317 )
13318 .await;
13319
13320 let _completion_requests_handler =
13321 cx.lsp
13322 .server
13323 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
13324 Ok(Some(lsp::CompletionResponse::Array(vec![
13325 lsp::CompletionItem {
13326 label: "first".into(),
13327 ..lsp::CompletionItem::default()
13328 },
13329 lsp::CompletionItem {
13330 label: "last".into(),
13331 ..lsp::CompletionItem::default()
13332 },
13333 ])))
13334 });
13335
13336 cx.set_state(indoc! {"ˇ
13337 first
13338 last
13339 second
13340 "});
13341 cx.simulate_keystroke(".");
13342 cx.executor().run_until_parked();
13343 cx.condition(|editor, _| editor.context_menu_visible())
13344 .await;
13345 cx.update_editor(|editor, _, _| {
13346 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13347 {
13348 assert_eq!(
13349 completion_menu_entries(menu),
13350 &["first", "last", "second"],
13351 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
13352 );
13353 } else {
13354 panic!("expected completion menu to be open");
13355 }
13356 });
13357}
13358
13359#[gpui::test]
13360async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
13361 init_test(cx, |language_settings| {
13362 language_settings.defaults.completions = Some(CompletionSettings {
13363 words: WordsCompletionMode::Disabled,
13364 words_min_length: 0,
13365 lsp: true,
13366 lsp_fetch_timeout_ms: 0,
13367 lsp_insert_mode: LspInsertMode::Insert,
13368 });
13369 });
13370
13371 let mut cx = EditorLspTestContext::new_rust(
13372 lsp::ServerCapabilities {
13373 completion_provider: Some(lsp::CompletionOptions {
13374 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
13375 ..lsp::CompletionOptions::default()
13376 }),
13377 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
13378 ..lsp::ServerCapabilities::default()
13379 },
13380 cx,
13381 )
13382 .await;
13383
13384 let _completion_requests_handler =
13385 cx.lsp
13386 .server
13387 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
13388 panic!("LSP completions should not be queried when dealing with word completions")
13389 });
13390
13391 cx.set_state(indoc! {"ˇ
13392 first
13393 last
13394 second
13395 "});
13396 cx.update_editor(|editor, window, cx| {
13397 editor.show_word_completions(&ShowWordCompletions, window, cx);
13398 });
13399 cx.executor().run_until_parked();
13400 cx.condition(|editor, _| editor.context_menu_visible())
13401 .await;
13402 cx.update_editor(|editor, _, _| {
13403 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13404 {
13405 assert_eq!(
13406 completion_menu_entries(menu),
13407 &["first", "last", "second"],
13408 "`ShowWordCompletions` action should show word completions"
13409 );
13410 } else {
13411 panic!("expected completion menu to be open");
13412 }
13413 });
13414
13415 cx.simulate_keystroke("l");
13416 cx.executor().run_until_parked();
13417 cx.condition(|editor, _| editor.context_menu_visible())
13418 .await;
13419 cx.update_editor(|editor, _, _| {
13420 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13421 {
13422 assert_eq!(
13423 completion_menu_entries(menu),
13424 &["last"],
13425 "After showing word completions, further editing should filter them and not query the LSP"
13426 );
13427 } else {
13428 panic!("expected completion menu to be open");
13429 }
13430 });
13431}
13432
13433#[gpui::test]
13434async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
13435 init_test(cx, |language_settings| {
13436 language_settings.defaults.completions = Some(CompletionSettings {
13437 words: WordsCompletionMode::Fallback,
13438 words_min_length: 0,
13439 lsp: false,
13440 lsp_fetch_timeout_ms: 0,
13441 lsp_insert_mode: LspInsertMode::Insert,
13442 });
13443 });
13444
13445 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
13446
13447 cx.set_state(indoc! {"ˇ
13448 0_usize
13449 let
13450 33
13451 4.5f32
13452 "});
13453 cx.update_editor(|editor, window, cx| {
13454 editor.show_completions(&ShowCompletions::default(), window, cx);
13455 });
13456 cx.executor().run_until_parked();
13457 cx.condition(|editor, _| editor.context_menu_visible())
13458 .await;
13459 cx.update_editor(|editor, window, cx| {
13460 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13461 {
13462 assert_eq!(
13463 completion_menu_entries(menu),
13464 &["let"],
13465 "With no digits in the completion query, no digits should be in the word completions"
13466 );
13467 } else {
13468 panic!("expected completion menu to be open");
13469 }
13470 editor.cancel(&Cancel, window, cx);
13471 });
13472
13473 cx.set_state(indoc! {"3ˇ
13474 0_usize
13475 let
13476 3
13477 33.35f32
13478 "});
13479 cx.update_editor(|editor, window, cx| {
13480 editor.show_completions(&ShowCompletions::default(), window, cx);
13481 });
13482 cx.executor().run_until_parked();
13483 cx.condition(|editor, _| editor.context_menu_visible())
13484 .await;
13485 cx.update_editor(|editor, _, _| {
13486 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13487 {
13488 assert_eq!(completion_menu_entries(menu), &["33", "35f32"], "The digit is in the completion query, \
13489 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
13490 } else {
13491 panic!("expected completion menu to be open");
13492 }
13493 });
13494}
13495
13496#[gpui::test]
13497async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) {
13498 init_test(cx, |language_settings| {
13499 language_settings.defaults.completions = Some(CompletionSettings {
13500 words: WordsCompletionMode::Enabled,
13501 words_min_length: 3,
13502 lsp: true,
13503 lsp_fetch_timeout_ms: 0,
13504 lsp_insert_mode: LspInsertMode::Insert,
13505 });
13506 });
13507
13508 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
13509 cx.set_state(indoc! {"ˇ
13510 wow
13511 wowen
13512 wowser
13513 "});
13514 cx.simulate_keystroke("w");
13515 cx.executor().run_until_parked();
13516 cx.update_editor(|editor, _, _| {
13517 if editor.context_menu.borrow_mut().is_some() {
13518 panic!(
13519 "expected completion menu to be hidden, as words completion threshold is not met"
13520 );
13521 }
13522 });
13523
13524 cx.simulate_keystroke("o");
13525 cx.executor().run_until_parked();
13526 cx.update_editor(|editor, _, _| {
13527 if editor.context_menu.borrow_mut().is_some() {
13528 panic!(
13529 "expected completion menu to be hidden, as words completion threshold is not met still"
13530 );
13531 }
13532 });
13533
13534 cx.simulate_keystroke("w");
13535 cx.executor().run_until_parked();
13536 cx.update_editor(|editor, _, _| {
13537 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13538 {
13539 assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word");
13540 } else {
13541 panic!("expected completion menu to be open after the word completions threshold is met");
13542 }
13543 });
13544}
13545
13546fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
13547 let position = || lsp::Position {
13548 line: params.text_document_position.position.line,
13549 character: params.text_document_position.position.character,
13550 };
13551 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13552 range: lsp::Range {
13553 start: position(),
13554 end: position(),
13555 },
13556 new_text: text.to_string(),
13557 }))
13558}
13559
13560#[gpui::test]
13561async fn test_multiline_completion(cx: &mut TestAppContext) {
13562 init_test(cx, |_| {});
13563
13564 let fs = FakeFs::new(cx.executor());
13565 fs.insert_tree(
13566 path!("/a"),
13567 json!({
13568 "main.ts": "a",
13569 }),
13570 )
13571 .await;
13572
13573 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13574 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13575 let typescript_language = Arc::new(Language::new(
13576 LanguageConfig {
13577 name: "TypeScript".into(),
13578 matcher: LanguageMatcher {
13579 path_suffixes: vec!["ts".to_string()],
13580 ..LanguageMatcher::default()
13581 },
13582 line_comments: vec!["// ".into()],
13583 ..LanguageConfig::default()
13584 },
13585 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
13586 ));
13587 language_registry.add(typescript_language.clone());
13588 let mut fake_servers = language_registry.register_fake_lsp(
13589 "TypeScript",
13590 FakeLspAdapter {
13591 capabilities: lsp::ServerCapabilities {
13592 completion_provider: Some(lsp::CompletionOptions {
13593 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
13594 ..lsp::CompletionOptions::default()
13595 }),
13596 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
13597 ..lsp::ServerCapabilities::default()
13598 },
13599 // Emulate vtsls label generation
13600 label_for_completion: Some(Box::new(|item, _| {
13601 let text = if let Some(description) = item
13602 .label_details
13603 .as_ref()
13604 .and_then(|label_details| label_details.description.as_ref())
13605 {
13606 format!("{} {}", item.label, description)
13607 } else if let Some(detail) = &item.detail {
13608 format!("{} {}", item.label, detail)
13609 } else {
13610 item.label.clone()
13611 };
13612 let len = text.len();
13613 Some(language::CodeLabel {
13614 text,
13615 runs: Vec::new(),
13616 filter_range: 0..len,
13617 })
13618 })),
13619 ..FakeLspAdapter::default()
13620 },
13621 );
13622 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13623 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13624 let worktree_id = workspace
13625 .update(cx, |workspace, _window, cx| {
13626 workspace.project().update(cx, |project, cx| {
13627 project.worktrees(cx).next().unwrap().read(cx).id()
13628 })
13629 })
13630 .unwrap();
13631 let _buffer = project
13632 .update(cx, |project, cx| {
13633 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
13634 })
13635 .await
13636 .unwrap();
13637 let editor = workspace
13638 .update(cx, |workspace, window, cx| {
13639 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
13640 })
13641 .unwrap()
13642 .await
13643 .unwrap()
13644 .downcast::<Editor>()
13645 .unwrap();
13646 let fake_server = fake_servers.next().await.unwrap();
13647
13648 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
13649 let multiline_label_2 = "a\nb\nc\n";
13650 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
13651 let multiline_description = "d\ne\nf\n";
13652 let multiline_detail_2 = "g\nh\ni\n";
13653
13654 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
13655 move |params, _| async move {
13656 Ok(Some(lsp::CompletionResponse::Array(vec![
13657 lsp::CompletionItem {
13658 label: multiline_label.to_string(),
13659 text_edit: gen_text_edit(¶ms, "new_text_1"),
13660 ..lsp::CompletionItem::default()
13661 },
13662 lsp::CompletionItem {
13663 label: "single line label 1".to_string(),
13664 detail: Some(multiline_detail.to_string()),
13665 text_edit: gen_text_edit(¶ms, "new_text_2"),
13666 ..lsp::CompletionItem::default()
13667 },
13668 lsp::CompletionItem {
13669 label: "single line label 2".to_string(),
13670 label_details: Some(lsp::CompletionItemLabelDetails {
13671 description: Some(multiline_description.to_string()),
13672 detail: None,
13673 }),
13674 text_edit: gen_text_edit(¶ms, "new_text_2"),
13675 ..lsp::CompletionItem::default()
13676 },
13677 lsp::CompletionItem {
13678 label: multiline_label_2.to_string(),
13679 detail: Some(multiline_detail_2.to_string()),
13680 text_edit: gen_text_edit(¶ms, "new_text_3"),
13681 ..lsp::CompletionItem::default()
13682 },
13683 lsp::CompletionItem {
13684 label: "Label with many spaces and \t but without newlines".to_string(),
13685 detail: Some(
13686 "Details with many spaces and \t but without newlines".to_string(),
13687 ),
13688 text_edit: gen_text_edit(¶ms, "new_text_4"),
13689 ..lsp::CompletionItem::default()
13690 },
13691 ])))
13692 },
13693 );
13694
13695 editor.update_in(cx, |editor, window, cx| {
13696 cx.focus_self(window);
13697 editor.move_to_end(&MoveToEnd, window, cx);
13698 editor.handle_input(".", window, cx);
13699 });
13700 cx.run_until_parked();
13701 completion_handle.next().await.unwrap();
13702
13703 editor.update(cx, |editor, _| {
13704 assert!(editor.context_menu_visible());
13705 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13706 {
13707 let completion_labels = menu
13708 .completions
13709 .borrow()
13710 .iter()
13711 .map(|c| c.label.text.clone())
13712 .collect::<Vec<_>>();
13713 assert_eq!(
13714 completion_labels,
13715 &[
13716 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
13717 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
13718 "single line label 2 d e f ",
13719 "a b c g h i ",
13720 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
13721 ],
13722 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
13723 );
13724
13725 for completion in menu
13726 .completions
13727 .borrow()
13728 .iter() {
13729 assert_eq!(
13730 completion.label.filter_range,
13731 0..completion.label.text.len(),
13732 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
13733 );
13734 }
13735 } else {
13736 panic!("expected completion menu to be open");
13737 }
13738 });
13739}
13740
13741#[gpui::test]
13742async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
13743 init_test(cx, |_| {});
13744 let mut cx = EditorLspTestContext::new_rust(
13745 lsp::ServerCapabilities {
13746 completion_provider: Some(lsp::CompletionOptions {
13747 trigger_characters: Some(vec![".".to_string()]),
13748 ..Default::default()
13749 }),
13750 ..Default::default()
13751 },
13752 cx,
13753 )
13754 .await;
13755 cx.lsp
13756 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13757 Ok(Some(lsp::CompletionResponse::Array(vec![
13758 lsp::CompletionItem {
13759 label: "first".into(),
13760 ..Default::default()
13761 },
13762 lsp::CompletionItem {
13763 label: "last".into(),
13764 ..Default::default()
13765 },
13766 ])))
13767 });
13768 cx.set_state("variableˇ");
13769 cx.simulate_keystroke(".");
13770 cx.executor().run_until_parked();
13771
13772 cx.update_editor(|editor, _, _| {
13773 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13774 {
13775 assert_eq!(completion_menu_entries(menu), &["first", "last"]);
13776 } else {
13777 panic!("expected completion menu to be open");
13778 }
13779 });
13780
13781 cx.update_editor(|editor, window, cx| {
13782 editor.move_page_down(&MovePageDown::default(), window, cx);
13783 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13784 {
13785 assert!(
13786 menu.selected_item == 1,
13787 "expected PageDown to select the last item from the context menu"
13788 );
13789 } else {
13790 panic!("expected completion menu to stay open after PageDown");
13791 }
13792 });
13793
13794 cx.update_editor(|editor, window, cx| {
13795 editor.move_page_up(&MovePageUp::default(), window, cx);
13796 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13797 {
13798 assert!(
13799 menu.selected_item == 0,
13800 "expected PageUp to select the first item from the context menu"
13801 );
13802 } else {
13803 panic!("expected completion menu to stay open after PageUp");
13804 }
13805 });
13806}
13807
13808#[gpui::test]
13809async fn test_as_is_completions(cx: &mut TestAppContext) {
13810 init_test(cx, |_| {});
13811 let mut cx = EditorLspTestContext::new_rust(
13812 lsp::ServerCapabilities {
13813 completion_provider: Some(lsp::CompletionOptions {
13814 ..Default::default()
13815 }),
13816 ..Default::default()
13817 },
13818 cx,
13819 )
13820 .await;
13821 cx.lsp
13822 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13823 Ok(Some(lsp::CompletionResponse::Array(vec![
13824 lsp::CompletionItem {
13825 label: "unsafe".into(),
13826 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13827 range: lsp::Range {
13828 start: lsp::Position {
13829 line: 1,
13830 character: 2,
13831 },
13832 end: lsp::Position {
13833 line: 1,
13834 character: 3,
13835 },
13836 },
13837 new_text: "unsafe".to_string(),
13838 })),
13839 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
13840 ..Default::default()
13841 },
13842 ])))
13843 });
13844 cx.set_state("fn a() {}\n nˇ");
13845 cx.executor().run_until_parked();
13846 cx.update_editor(|editor, window, cx| {
13847 editor.show_completions(
13848 &ShowCompletions {
13849 trigger: Some("\n".into()),
13850 },
13851 window,
13852 cx,
13853 );
13854 });
13855 cx.executor().run_until_parked();
13856
13857 cx.update_editor(|editor, window, cx| {
13858 editor.confirm_completion(&Default::default(), window, cx)
13859 });
13860 cx.executor().run_until_parked();
13861 cx.assert_editor_state("fn a() {}\n unsafeˇ");
13862}
13863
13864#[gpui::test]
13865async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
13866 init_test(cx, |_| {});
13867 let language =
13868 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
13869 let mut cx = EditorLspTestContext::new(
13870 language,
13871 lsp::ServerCapabilities {
13872 completion_provider: Some(lsp::CompletionOptions {
13873 ..lsp::CompletionOptions::default()
13874 }),
13875 ..lsp::ServerCapabilities::default()
13876 },
13877 cx,
13878 )
13879 .await;
13880
13881 cx.set_state(
13882 "#ifndef BAR_H
13883#define BAR_H
13884
13885#include <stdbool.h>
13886
13887int fn_branch(bool do_branch1, bool do_branch2);
13888
13889#endif // BAR_H
13890ˇ",
13891 );
13892 cx.executor().run_until_parked();
13893 cx.update_editor(|editor, window, cx| {
13894 editor.handle_input("#", window, cx);
13895 });
13896 cx.executor().run_until_parked();
13897 cx.update_editor(|editor, window, cx| {
13898 editor.handle_input("i", window, cx);
13899 });
13900 cx.executor().run_until_parked();
13901 cx.update_editor(|editor, window, cx| {
13902 editor.handle_input("n", window, cx);
13903 });
13904 cx.executor().run_until_parked();
13905 cx.assert_editor_state(
13906 "#ifndef BAR_H
13907#define BAR_H
13908
13909#include <stdbool.h>
13910
13911int fn_branch(bool do_branch1, bool do_branch2);
13912
13913#endif // BAR_H
13914#inˇ",
13915 );
13916
13917 cx.lsp
13918 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13919 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
13920 is_incomplete: false,
13921 item_defaults: None,
13922 items: vec![lsp::CompletionItem {
13923 kind: Some(lsp::CompletionItemKind::SNIPPET),
13924 label_details: Some(lsp::CompletionItemLabelDetails {
13925 detail: Some("header".to_string()),
13926 description: None,
13927 }),
13928 label: " include".to_string(),
13929 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13930 range: lsp::Range {
13931 start: lsp::Position {
13932 line: 8,
13933 character: 1,
13934 },
13935 end: lsp::Position {
13936 line: 8,
13937 character: 1,
13938 },
13939 },
13940 new_text: "include \"$0\"".to_string(),
13941 })),
13942 sort_text: Some("40b67681include".to_string()),
13943 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
13944 filter_text: Some("include".to_string()),
13945 insert_text: Some("include \"$0\"".to_string()),
13946 ..lsp::CompletionItem::default()
13947 }],
13948 })))
13949 });
13950 cx.update_editor(|editor, window, cx| {
13951 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13952 });
13953 cx.executor().run_until_parked();
13954 cx.update_editor(|editor, window, cx| {
13955 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
13956 });
13957 cx.executor().run_until_parked();
13958 cx.assert_editor_state(
13959 "#ifndef BAR_H
13960#define BAR_H
13961
13962#include <stdbool.h>
13963
13964int fn_branch(bool do_branch1, bool do_branch2);
13965
13966#endif // BAR_H
13967#include \"ˇ\"",
13968 );
13969
13970 cx.lsp
13971 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13972 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
13973 is_incomplete: true,
13974 item_defaults: None,
13975 items: vec![lsp::CompletionItem {
13976 kind: Some(lsp::CompletionItemKind::FILE),
13977 label: "AGL/".to_string(),
13978 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13979 range: lsp::Range {
13980 start: lsp::Position {
13981 line: 8,
13982 character: 10,
13983 },
13984 end: lsp::Position {
13985 line: 8,
13986 character: 11,
13987 },
13988 },
13989 new_text: "AGL/".to_string(),
13990 })),
13991 sort_text: Some("40b67681AGL/".to_string()),
13992 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
13993 filter_text: Some("AGL/".to_string()),
13994 insert_text: Some("AGL/".to_string()),
13995 ..lsp::CompletionItem::default()
13996 }],
13997 })))
13998 });
13999 cx.update_editor(|editor, window, cx| {
14000 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14001 });
14002 cx.executor().run_until_parked();
14003 cx.update_editor(|editor, window, cx| {
14004 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
14005 });
14006 cx.executor().run_until_parked();
14007 cx.assert_editor_state(
14008 r##"#ifndef BAR_H
14009#define BAR_H
14010
14011#include <stdbool.h>
14012
14013int fn_branch(bool do_branch1, bool do_branch2);
14014
14015#endif // BAR_H
14016#include "AGL/ˇ"##,
14017 );
14018
14019 cx.update_editor(|editor, window, cx| {
14020 editor.handle_input("\"", window, cx);
14021 });
14022 cx.executor().run_until_parked();
14023 cx.assert_editor_state(
14024 r##"#ifndef BAR_H
14025#define BAR_H
14026
14027#include <stdbool.h>
14028
14029int fn_branch(bool do_branch1, bool do_branch2);
14030
14031#endif // BAR_H
14032#include "AGL/"ˇ"##,
14033 );
14034}
14035
14036#[gpui::test]
14037async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
14038 init_test(cx, |_| {});
14039
14040 let mut cx = EditorLspTestContext::new_rust(
14041 lsp::ServerCapabilities {
14042 completion_provider: Some(lsp::CompletionOptions {
14043 trigger_characters: Some(vec![".".to_string()]),
14044 resolve_provider: Some(true),
14045 ..Default::default()
14046 }),
14047 ..Default::default()
14048 },
14049 cx,
14050 )
14051 .await;
14052
14053 cx.set_state("fn main() { let a = 2ˇ; }");
14054 cx.simulate_keystroke(".");
14055 let completion_item = lsp::CompletionItem {
14056 label: "Some".into(),
14057 kind: Some(lsp::CompletionItemKind::SNIPPET),
14058 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
14059 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
14060 kind: lsp::MarkupKind::Markdown,
14061 value: "```rust\nSome(2)\n```".to_string(),
14062 })),
14063 deprecated: Some(false),
14064 sort_text: Some("Some".to_string()),
14065 filter_text: Some("Some".to_string()),
14066 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
14067 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14068 range: lsp::Range {
14069 start: lsp::Position {
14070 line: 0,
14071 character: 22,
14072 },
14073 end: lsp::Position {
14074 line: 0,
14075 character: 22,
14076 },
14077 },
14078 new_text: "Some(2)".to_string(),
14079 })),
14080 additional_text_edits: Some(vec![lsp::TextEdit {
14081 range: lsp::Range {
14082 start: lsp::Position {
14083 line: 0,
14084 character: 20,
14085 },
14086 end: lsp::Position {
14087 line: 0,
14088 character: 22,
14089 },
14090 },
14091 new_text: "".to_string(),
14092 }]),
14093 ..Default::default()
14094 };
14095
14096 let closure_completion_item = completion_item.clone();
14097 let counter = Arc::new(AtomicUsize::new(0));
14098 let counter_clone = counter.clone();
14099 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14100 let task_completion_item = closure_completion_item.clone();
14101 counter_clone.fetch_add(1, atomic::Ordering::Release);
14102 async move {
14103 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
14104 is_incomplete: true,
14105 item_defaults: None,
14106 items: vec![task_completion_item],
14107 })))
14108 }
14109 });
14110
14111 cx.condition(|editor, _| editor.context_menu_visible())
14112 .await;
14113 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
14114 assert!(request.next().await.is_some());
14115 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14116
14117 cx.simulate_keystrokes("S o m");
14118 cx.condition(|editor, _| editor.context_menu_visible())
14119 .await;
14120 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
14121 assert!(request.next().await.is_some());
14122 assert!(request.next().await.is_some());
14123 assert!(request.next().await.is_some());
14124 request.close();
14125 assert!(request.next().await.is_none());
14126 assert_eq!(
14127 counter.load(atomic::Ordering::Acquire),
14128 4,
14129 "With the completions menu open, only one LSP request should happen per input"
14130 );
14131}
14132
14133#[gpui::test]
14134async fn test_toggle_comment(cx: &mut TestAppContext) {
14135 init_test(cx, |_| {});
14136 let mut cx = EditorTestContext::new(cx).await;
14137 let language = Arc::new(Language::new(
14138 LanguageConfig {
14139 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
14140 ..Default::default()
14141 },
14142 Some(tree_sitter_rust::LANGUAGE.into()),
14143 ));
14144 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
14145
14146 // If multiple selections intersect a line, the line is only toggled once.
14147 cx.set_state(indoc! {"
14148 fn a() {
14149 «//b();
14150 ˇ»// «c();
14151 //ˇ» d();
14152 }
14153 "});
14154
14155 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
14156
14157 cx.assert_editor_state(indoc! {"
14158 fn a() {
14159 «b();
14160 c();
14161 ˇ» d();
14162 }
14163 "});
14164
14165 // The comment prefix is inserted at the same column for every line in a
14166 // selection.
14167 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
14168
14169 cx.assert_editor_state(indoc! {"
14170 fn a() {
14171 // «b();
14172 // c();
14173 ˇ»// d();
14174 }
14175 "});
14176
14177 // If a selection ends at the beginning of a line, that line is not toggled.
14178 cx.set_selections_state(indoc! {"
14179 fn a() {
14180 // b();
14181 «// c();
14182 ˇ» // d();
14183 }
14184 "});
14185
14186 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
14187
14188 cx.assert_editor_state(indoc! {"
14189 fn a() {
14190 // b();
14191 «c();
14192 ˇ» // d();
14193 }
14194 "});
14195
14196 // If a selection span a single line and is empty, the line is toggled.
14197 cx.set_state(indoc! {"
14198 fn a() {
14199 a();
14200 b();
14201 ˇ
14202 }
14203 "});
14204
14205 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
14206
14207 cx.assert_editor_state(indoc! {"
14208 fn a() {
14209 a();
14210 b();
14211 //•ˇ
14212 }
14213 "});
14214
14215 // If a selection span multiple lines, empty lines are not toggled.
14216 cx.set_state(indoc! {"
14217 fn a() {
14218 «a();
14219
14220 c();ˇ»
14221 }
14222 "});
14223
14224 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
14225
14226 cx.assert_editor_state(indoc! {"
14227 fn a() {
14228 // «a();
14229
14230 // c();ˇ»
14231 }
14232 "});
14233
14234 // If a selection includes multiple comment prefixes, all lines are uncommented.
14235 cx.set_state(indoc! {"
14236 fn a() {
14237 «// a();
14238 /// b();
14239 //! c();ˇ»
14240 }
14241 "});
14242
14243 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
14244
14245 cx.assert_editor_state(indoc! {"
14246 fn a() {
14247 «a();
14248 b();
14249 c();ˇ»
14250 }
14251 "});
14252}
14253
14254#[gpui::test]
14255async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
14256 init_test(cx, |_| {});
14257 let mut cx = EditorTestContext::new(cx).await;
14258 let language = Arc::new(Language::new(
14259 LanguageConfig {
14260 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
14261 ..Default::default()
14262 },
14263 Some(tree_sitter_rust::LANGUAGE.into()),
14264 ));
14265 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
14266
14267 let toggle_comments = &ToggleComments {
14268 advance_downwards: false,
14269 ignore_indent: true,
14270 };
14271
14272 // If multiple selections intersect a line, the line is only toggled once.
14273 cx.set_state(indoc! {"
14274 fn a() {
14275 // «b();
14276 // c();
14277 // ˇ» d();
14278 }
14279 "});
14280
14281 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
14282
14283 cx.assert_editor_state(indoc! {"
14284 fn a() {
14285 «b();
14286 c();
14287 ˇ» d();
14288 }
14289 "});
14290
14291 // The comment prefix is inserted at the beginning of each line
14292 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
14293
14294 cx.assert_editor_state(indoc! {"
14295 fn a() {
14296 // «b();
14297 // c();
14298 // ˇ» d();
14299 }
14300 "});
14301
14302 // If a selection ends at the beginning of a line, that line is not toggled.
14303 cx.set_selections_state(indoc! {"
14304 fn a() {
14305 // b();
14306 // «c();
14307 ˇ»// d();
14308 }
14309 "});
14310
14311 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
14312
14313 cx.assert_editor_state(indoc! {"
14314 fn a() {
14315 // b();
14316 «c();
14317 ˇ»// d();
14318 }
14319 "});
14320
14321 // If a selection span a single line and is empty, the line is toggled.
14322 cx.set_state(indoc! {"
14323 fn a() {
14324 a();
14325 b();
14326 ˇ
14327 }
14328 "});
14329
14330 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
14331
14332 cx.assert_editor_state(indoc! {"
14333 fn a() {
14334 a();
14335 b();
14336 //ˇ
14337 }
14338 "});
14339
14340 // If a selection span multiple lines, empty lines are not toggled.
14341 cx.set_state(indoc! {"
14342 fn a() {
14343 «a();
14344
14345 c();ˇ»
14346 }
14347 "});
14348
14349 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
14350
14351 cx.assert_editor_state(indoc! {"
14352 fn a() {
14353 // «a();
14354
14355 // c();ˇ»
14356 }
14357 "});
14358
14359 // If a selection includes multiple comment prefixes, all lines are uncommented.
14360 cx.set_state(indoc! {"
14361 fn a() {
14362 // «a();
14363 /// b();
14364 //! c();ˇ»
14365 }
14366 "});
14367
14368 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
14369
14370 cx.assert_editor_state(indoc! {"
14371 fn a() {
14372 «a();
14373 b();
14374 c();ˇ»
14375 }
14376 "});
14377}
14378
14379#[gpui::test]
14380async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
14381 init_test(cx, |_| {});
14382
14383 let language = Arc::new(Language::new(
14384 LanguageConfig {
14385 line_comments: vec!["// ".into()],
14386 ..Default::default()
14387 },
14388 Some(tree_sitter_rust::LANGUAGE.into()),
14389 ));
14390
14391 let mut cx = EditorTestContext::new(cx).await;
14392
14393 cx.language_registry().add(language.clone());
14394 cx.update_buffer(|buffer, cx| {
14395 buffer.set_language(Some(language), cx);
14396 });
14397
14398 let toggle_comments = &ToggleComments {
14399 advance_downwards: true,
14400 ignore_indent: false,
14401 };
14402
14403 // Single cursor on one line -> advance
14404 // Cursor moves horizontally 3 characters as well on non-blank line
14405 cx.set_state(indoc!(
14406 "fn a() {
14407 ˇdog();
14408 cat();
14409 }"
14410 ));
14411 cx.update_editor(|editor, window, cx| {
14412 editor.toggle_comments(toggle_comments, window, cx);
14413 });
14414 cx.assert_editor_state(indoc!(
14415 "fn a() {
14416 // dog();
14417 catˇ();
14418 }"
14419 ));
14420
14421 // Single selection on one line -> don't advance
14422 cx.set_state(indoc!(
14423 "fn a() {
14424 «dog()ˇ»;
14425 cat();
14426 }"
14427 ));
14428 cx.update_editor(|editor, window, cx| {
14429 editor.toggle_comments(toggle_comments, window, cx);
14430 });
14431 cx.assert_editor_state(indoc!(
14432 "fn a() {
14433 // «dog()ˇ»;
14434 cat();
14435 }"
14436 ));
14437
14438 // Multiple cursors on one line -> advance
14439 cx.set_state(indoc!(
14440 "fn a() {
14441 ˇdˇog();
14442 cat();
14443 }"
14444 ));
14445 cx.update_editor(|editor, window, cx| {
14446 editor.toggle_comments(toggle_comments, window, cx);
14447 });
14448 cx.assert_editor_state(indoc!(
14449 "fn a() {
14450 // dog();
14451 catˇ(ˇ);
14452 }"
14453 ));
14454
14455 // Multiple cursors on one line, with selection -> don't advance
14456 cx.set_state(indoc!(
14457 "fn a() {
14458 ˇdˇog«()ˇ»;
14459 cat();
14460 }"
14461 ));
14462 cx.update_editor(|editor, window, cx| {
14463 editor.toggle_comments(toggle_comments, window, cx);
14464 });
14465 cx.assert_editor_state(indoc!(
14466 "fn a() {
14467 // ˇdˇog«()ˇ»;
14468 cat();
14469 }"
14470 ));
14471
14472 // Single cursor on one line -> advance
14473 // Cursor moves to column 0 on blank line
14474 cx.set_state(indoc!(
14475 "fn a() {
14476 ˇdog();
14477
14478 cat();
14479 }"
14480 ));
14481 cx.update_editor(|editor, window, cx| {
14482 editor.toggle_comments(toggle_comments, window, cx);
14483 });
14484 cx.assert_editor_state(indoc!(
14485 "fn a() {
14486 // dog();
14487 ˇ
14488 cat();
14489 }"
14490 ));
14491
14492 // Single cursor on one line -> advance
14493 // Cursor starts and ends at column 0
14494 cx.set_state(indoc!(
14495 "fn a() {
14496 ˇ dog();
14497 cat();
14498 }"
14499 ));
14500 cx.update_editor(|editor, window, cx| {
14501 editor.toggle_comments(toggle_comments, window, cx);
14502 });
14503 cx.assert_editor_state(indoc!(
14504 "fn a() {
14505 // dog();
14506 ˇ cat();
14507 }"
14508 ));
14509}
14510
14511#[gpui::test]
14512async fn test_toggle_block_comment(cx: &mut TestAppContext) {
14513 init_test(cx, |_| {});
14514
14515 let mut cx = EditorTestContext::new(cx).await;
14516
14517 let html_language = Arc::new(
14518 Language::new(
14519 LanguageConfig {
14520 name: "HTML".into(),
14521 block_comment: Some(BlockCommentConfig {
14522 start: "<!-- ".into(),
14523 prefix: "".into(),
14524 end: " -->".into(),
14525 tab_size: 0,
14526 }),
14527 ..Default::default()
14528 },
14529 Some(tree_sitter_html::LANGUAGE.into()),
14530 )
14531 .with_injection_query(
14532 r#"
14533 (script_element
14534 (raw_text) @injection.content
14535 (#set! injection.language "javascript"))
14536 "#,
14537 )
14538 .unwrap(),
14539 );
14540
14541 let javascript_language = Arc::new(Language::new(
14542 LanguageConfig {
14543 name: "JavaScript".into(),
14544 line_comments: vec!["// ".into()],
14545 ..Default::default()
14546 },
14547 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
14548 ));
14549
14550 cx.language_registry().add(html_language.clone());
14551 cx.language_registry().add(javascript_language);
14552 cx.update_buffer(|buffer, cx| {
14553 buffer.set_language(Some(html_language), cx);
14554 });
14555
14556 // Toggle comments for empty selections
14557 cx.set_state(
14558 &r#"
14559 <p>A</p>ˇ
14560 <p>B</p>ˇ
14561 <p>C</p>ˇ
14562 "#
14563 .unindent(),
14564 );
14565 cx.update_editor(|editor, window, cx| {
14566 editor.toggle_comments(&ToggleComments::default(), window, cx)
14567 });
14568 cx.assert_editor_state(
14569 &r#"
14570 <!-- <p>A</p>ˇ -->
14571 <!-- <p>B</p>ˇ -->
14572 <!-- <p>C</p>ˇ -->
14573 "#
14574 .unindent(),
14575 );
14576 cx.update_editor(|editor, window, cx| {
14577 editor.toggle_comments(&ToggleComments::default(), window, cx)
14578 });
14579 cx.assert_editor_state(
14580 &r#"
14581 <p>A</p>ˇ
14582 <p>B</p>ˇ
14583 <p>C</p>ˇ
14584 "#
14585 .unindent(),
14586 );
14587
14588 // Toggle comments for mixture of empty and non-empty selections, where
14589 // multiple selections occupy a given line.
14590 cx.set_state(
14591 &r#"
14592 <p>A«</p>
14593 <p>ˇ»B</p>ˇ
14594 <p>C«</p>
14595 <p>ˇ»D</p>ˇ
14596 "#
14597 .unindent(),
14598 );
14599
14600 cx.update_editor(|editor, window, cx| {
14601 editor.toggle_comments(&ToggleComments::default(), window, cx)
14602 });
14603 cx.assert_editor_state(
14604 &r#"
14605 <!-- <p>A«</p>
14606 <p>ˇ»B</p>ˇ -->
14607 <!-- <p>C«</p>
14608 <p>ˇ»D</p>ˇ -->
14609 "#
14610 .unindent(),
14611 );
14612 cx.update_editor(|editor, window, cx| {
14613 editor.toggle_comments(&ToggleComments::default(), window, cx)
14614 });
14615 cx.assert_editor_state(
14616 &r#"
14617 <p>A«</p>
14618 <p>ˇ»B</p>ˇ
14619 <p>C«</p>
14620 <p>ˇ»D</p>ˇ
14621 "#
14622 .unindent(),
14623 );
14624
14625 // Toggle comments when different languages are active for different
14626 // selections.
14627 cx.set_state(
14628 &r#"
14629 ˇ<script>
14630 ˇvar x = new Y();
14631 ˇ</script>
14632 "#
14633 .unindent(),
14634 );
14635 cx.executor().run_until_parked();
14636 cx.update_editor(|editor, window, cx| {
14637 editor.toggle_comments(&ToggleComments::default(), window, cx)
14638 });
14639 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
14640 // Uncommenting and commenting from this position brings in even more wrong artifacts.
14641 cx.assert_editor_state(
14642 &r#"
14643 <!-- ˇ<script> -->
14644 // ˇvar x = new Y();
14645 <!-- ˇ</script> -->
14646 "#
14647 .unindent(),
14648 );
14649}
14650
14651#[gpui::test]
14652fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
14653 init_test(cx, |_| {});
14654
14655 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
14656 let multibuffer = cx.new(|cx| {
14657 let mut multibuffer = MultiBuffer::new(ReadWrite);
14658 multibuffer.push_excerpts(
14659 buffer.clone(),
14660 [
14661 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
14662 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
14663 ],
14664 cx,
14665 );
14666 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
14667 multibuffer
14668 });
14669
14670 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
14671 editor.update_in(cx, |editor, window, cx| {
14672 assert_eq!(editor.text(cx), "aaaa\nbbbb");
14673 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14674 s.select_ranges([
14675 Point::new(0, 0)..Point::new(0, 0),
14676 Point::new(1, 0)..Point::new(1, 0),
14677 ])
14678 });
14679
14680 editor.handle_input("X", window, cx);
14681 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
14682 assert_eq!(
14683 editor.selections.ranges(cx),
14684 [
14685 Point::new(0, 1)..Point::new(0, 1),
14686 Point::new(1, 1)..Point::new(1, 1),
14687 ]
14688 );
14689
14690 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
14691 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14692 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
14693 });
14694 editor.backspace(&Default::default(), window, cx);
14695 assert_eq!(editor.text(cx), "Xa\nbbb");
14696 assert_eq!(
14697 editor.selections.ranges(cx),
14698 [Point::new(1, 0)..Point::new(1, 0)]
14699 );
14700
14701 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14702 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
14703 });
14704 editor.backspace(&Default::default(), window, cx);
14705 assert_eq!(editor.text(cx), "X\nbb");
14706 assert_eq!(
14707 editor.selections.ranges(cx),
14708 [Point::new(0, 1)..Point::new(0, 1)]
14709 );
14710 });
14711}
14712
14713#[gpui::test]
14714fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
14715 init_test(cx, |_| {});
14716
14717 let markers = vec![('[', ']').into(), ('(', ')').into()];
14718 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
14719 indoc! {"
14720 [aaaa
14721 (bbbb]
14722 cccc)",
14723 },
14724 markers.clone(),
14725 );
14726 let excerpt_ranges = markers.into_iter().map(|marker| {
14727 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
14728 ExcerptRange::new(context)
14729 });
14730 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
14731 let multibuffer = cx.new(|cx| {
14732 let mut multibuffer = MultiBuffer::new(ReadWrite);
14733 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
14734 multibuffer
14735 });
14736
14737 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
14738 editor.update_in(cx, |editor, window, cx| {
14739 let (expected_text, selection_ranges) = marked_text_ranges(
14740 indoc! {"
14741 aaaa
14742 bˇbbb
14743 bˇbbˇb
14744 cccc"
14745 },
14746 true,
14747 );
14748 assert_eq!(editor.text(cx), expected_text);
14749 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14750 s.select_ranges(selection_ranges)
14751 });
14752
14753 editor.handle_input("X", window, cx);
14754
14755 let (expected_text, expected_selections) = marked_text_ranges(
14756 indoc! {"
14757 aaaa
14758 bXˇbbXb
14759 bXˇbbXˇb
14760 cccc"
14761 },
14762 false,
14763 );
14764 assert_eq!(editor.text(cx), expected_text);
14765 assert_eq!(editor.selections.ranges(cx), expected_selections);
14766
14767 editor.newline(&Newline, window, cx);
14768 let (expected_text, expected_selections) = marked_text_ranges(
14769 indoc! {"
14770 aaaa
14771 bX
14772 ˇbbX
14773 b
14774 bX
14775 ˇbbX
14776 ˇb
14777 cccc"
14778 },
14779 false,
14780 );
14781 assert_eq!(editor.text(cx), expected_text);
14782 assert_eq!(editor.selections.ranges(cx), expected_selections);
14783 });
14784}
14785
14786#[gpui::test]
14787fn test_refresh_selections(cx: &mut TestAppContext) {
14788 init_test(cx, |_| {});
14789
14790 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
14791 let mut excerpt1_id = None;
14792 let multibuffer = cx.new(|cx| {
14793 let mut multibuffer = MultiBuffer::new(ReadWrite);
14794 excerpt1_id = multibuffer
14795 .push_excerpts(
14796 buffer.clone(),
14797 [
14798 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
14799 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
14800 ],
14801 cx,
14802 )
14803 .into_iter()
14804 .next();
14805 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
14806 multibuffer
14807 });
14808
14809 let editor = cx.add_window(|window, cx| {
14810 let mut editor = build_editor(multibuffer.clone(), window, cx);
14811 let snapshot = editor.snapshot(window, cx);
14812 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14813 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
14814 });
14815 editor.begin_selection(
14816 Point::new(2, 1).to_display_point(&snapshot),
14817 true,
14818 1,
14819 window,
14820 cx,
14821 );
14822 assert_eq!(
14823 editor.selections.ranges(cx),
14824 [
14825 Point::new(1, 3)..Point::new(1, 3),
14826 Point::new(2, 1)..Point::new(2, 1),
14827 ]
14828 );
14829 editor
14830 });
14831
14832 // Refreshing selections is a no-op when excerpts haven't changed.
14833 _ = editor.update(cx, |editor, window, cx| {
14834 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
14835 assert_eq!(
14836 editor.selections.ranges(cx),
14837 [
14838 Point::new(1, 3)..Point::new(1, 3),
14839 Point::new(2, 1)..Point::new(2, 1),
14840 ]
14841 );
14842 });
14843
14844 multibuffer.update(cx, |multibuffer, cx| {
14845 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
14846 });
14847 _ = editor.update(cx, |editor, window, cx| {
14848 // Removing an excerpt causes the first selection to become degenerate.
14849 assert_eq!(
14850 editor.selections.ranges(cx),
14851 [
14852 Point::new(0, 0)..Point::new(0, 0),
14853 Point::new(0, 1)..Point::new(0, 1)
14854 ]
14855 );
14856
14857 // Refreshing selections will relocate the first selection to the original buffer
14858 // location.
14859 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
14860 assert_eq!(
14861 editor.selections.ranges(cx),
14862 [
14863 Point::new(0, 1)..Point::new(0, 1),
14864 Point::new(0, 3)..Point::new(0, 3)
14865 ]
14866 );
14867 assert!(editor.selections.pending_anchor().is_some());
14868 });
14869}
14870
14871#[gpui::test]
14872fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
14873 init_test(cx, |_| {});
14874
14875 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
14876 let mut excerpt1_id = None;
14877 let multibuffer = cx.new(|cx| {
14878 let mut multibuffer = MultiBuffer::new(ReadWrite);
14879 excerpt1_id = multibuffer
14880 .push_excerpts(
14881 buffer.clone(),
14882 [
14883 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
14884 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
14885 ],
14886 cx,
14887 )
14888 .into_iter()
14889 .next();
14890 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
14891 multibuffer
14892 });
14893
14894 let editor = cx.add_window(|window, cx| {
14895 let mut editor = build_editor(multibuffer.clone(), window, cx);
14896 let snapshot = editor.snapshot(window, cx);
14897 editor.begin_selection(
14898 Point::new(1, 3).to_display_point(&snapshot),
14899 false,
14900 1,
14901 window,
14902 cx,
14903 );
14904 assert_eq!(
14905 editor.selections.ranges(cx),
14906 [Point::new(1, 3)..Point::new(1, 3)]
14907 );
14908 editor
14909 });
14910
14911 multibuffer.update(cx, |multibuffer, cx| {
14912 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
14913 });
14914 _ = editor.update(cx, |editor, window, cx| {
14915 assert_eq!(
14916 editor.selections.ranges(cx),
14917 [Point::new(0, 0)..Point::new(0, 0)]
14918 );
14919
14920 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
14921 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
14922 assert_eq!(
14923 editor.selections.ranges(cx),
14924 [Point::new(0, 3)..Point::new(0, 3)]
14925 );
14926 assert!(editor.selections.pending_anchor().is_some());
14927 });
14928}
14929
14930#[gpui::test]
14931async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
14932 init_test(cx, |_| {});
14933
14934 let language = Arc::new(
14935 Language::new(
14936 LanguageConfig {
14937 brackets: BracketPairConfig {
14938 pairs: vec![
14939 BracketPair {
14940 start: "{".to_string(),
14941 end: "}".to_string(),
14942 close: true,
14943 surround: true,
14944 newline: true,
14945 },
14946 BracketPair {
14947 start: "/* ".to_string(),
14948 end: " */".to_string(),
14949 close: true,
14950 surround: true,
14951 newline: true,
14952 },
14953 ],
14954 ..Default::default()
14955 },
14956 ..Default::default()
14957 },
14958 Some(tree_sitter_rust::LANGUAGE.into()),
14959 )
14960 .with_indents_query("")
14961 .unwrap(),
14962 );
14963
14964 let text = concat!(
14965 "{ }\n", //
14966 " x\n", //
14967 " /* */\n", //
14968 "x\n", //
14969 "{{} }\n", //
14970 );
14971
14972 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
14973 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
14974 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
14975 editor
14976 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
14977 .await;
14978
14979 editor.update_in(cx, |editor, window, cx| {
14980 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14981 s.select_display_ranges([
14982 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
14983 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
14984 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
14985 ])
14986 });
14987 editor.newline(&Newline, window, cx);
14988
14989 assert_eq!(
14990 editor.buffer().read(cx).read(cx).text(),
14991 concat!(
14992 "{ \n", // Suppress rustfmt
14993 "\n", //
14994 "}\n", //
14995 " x\n", //
14996 " /* \n", //
14997 " \n", //
14998 " */\n", //
14999 "x\n", //
15000 "{{} \n", //
15001 "}\n", //
15002 )
15003 );
15004 });
15005}
15006
15007#[gpui::test]
15008fn test_highlighted_ranges(cx: &mut TestAppContext) {
15009 init_test(cx, |_| {});
15010
15011 let editor = cx.add_window(|window, cx| {
15012 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
15013 build_editor(buffer, window, cx)
15014 });
15015
15016 _ = editor.update(cx, |editor, window, cx| {
15017 struct Type1;
15018 struct Type2;
15019
15020 let buffer = editor.buffer.read(cx).snapshot(cx);
15021
15022 let anchor_range =
15023 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
15024
15025 editor.highlight_background::<Type1>(
15026 &[
15027 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
15028 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
15029 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
15030 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
15031 ],
15032 |_| Hsla::red(),
15033 cx,
15034 );
15035 editor.highlight_background::<Type2>(
15036 &[
15037 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
15038 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
15039 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
15040 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
15041 ],
15042 |_| Hsla::green(),
15043 cx,
15044 );
15045
15046 let snapshot = editor.snapshot(window, cx);
15047 let mut highlighted_ranges = editor.background_highlights_in_range(
15048 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
15049 &snapshot,
15050 cx.theme(),
15051 );
15052 // Enforce a consistent ordering based on color without relying on the ordering of the
15053 // highlight's `TypeId` which is non-executor.
15054 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
15055 assert_eq!(
15056 highlighted_ranges,
15057 &[
15058 (
15059 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
15060 Hsla::red(),
15061 ),
15062 (
15063 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
15064 Hsla::red(),
15065 ),
15066 (
15067 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
15068 Hsla::green(),
15069 ),
15070 (
15071 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
15072 Hsla::green(),
15073 ),
15074 ]
15075 );
15076 assert_eq!(
15077 editor.background_highlights_in_range(
15078 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
15079 &snapshot,
15080 cx.theme(),
15081 ),
15082 &[(
15083 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
15084 Hsla::red(),
15085 )]
15086 );
15087 });
15088}
15089
15090#[gpui::test]
15091async fn test_following(cx: &mut TestAppContext) {
15092 init_test(cx, |_| {});
15093
15094 let fs = FakeFs::new(cx.executor());
15095 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
15096
15097 let buffer = project.update(cx, |project, cx| {
15098 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
15099 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
15100 });
15101 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
15102 let follower = cx.update(|cx| {
15103 cx.open_window(
15104 WindowOptions {
15105 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
15106 gpui::Point::new(px(0.), px(0.)),
15107 gpui::Point::new(px(10.), px(80.)),
15108 ))),
15109 ..Default::default()
15110 },
15111 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
15112 )
15113 .unwrap()
15114 });
15115
15116 let is_still_following = Rc::new(RefCell::new(true));
15117 let follower_edit_event_count = Rc::new(RefCell::new(0));
15118 let pending_update = Rc::new(RefCell::new(None));
15119 let leader_entity = leader.root(cx).unwrap();
15120 let follower_entity = follower.root(cx).unwrap();
15121 _ = follower.update(cx, {
15122 let update = pending_update.clone();
15123 let is_still_following = is_still_following.clone();
15124 let follower_edit_event_count = follower_edit_event_count.clone();
15125 |_, window, cx| {
15126 cx.subscribe_in(
15127 &leader_entity,
15128 window,
15129 move |_, leader, event, window, cx| {
15130 leader.read(cx).add_event_to_update_proto(
15131 event,
15132 &mut update.borrow_mut(),
15133 window,
15134 cx,
15135 );
15136 },
15137 )
15138 .detach();
15139
15140 cx.subscribe_in(
15141 &follower_entity,
15142 window,
15143 move |_, _, event: &EditorEvent, _window, _cx| {
15144 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
15145 *is_still_following.borrow_mut() = false;
15146 }
15147
15148 if let EditorEvent::BufferEdited = event {
15149 *follower_edit_event_count.borrow_mut() += 1;
15150 }
15151 },
15152 )
15153 .detach();
15154 }
15155 });
15156
15157 // Update the selections only
15158 _ = leader.update(cx, |leader, window, cx| {
15159 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15160 s.select_ranges([1..1])
15161 });
15162 });
15163 follower
15164 .update(cx, |follower, window, cx| {
15165 follower.apply_update_proto(
15166 &project,
15167 pending_update.borrow_mut().take().unwrap(),
15168 window,
15169 cx,
15170 )
15171 })
15172 .unwrap()
15173 .await
15174 .unwrap();
15175 _ = follower.update(cx, |follower, _, cx| {
15176 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
15177 });
15178 assert!(*is_still_following.borrow());
15179 assert_eq!(*follower_edit_event_count.borrow(), 0);
15180
15181 // Update the scroll position only
15182 _ = leader.update(cx, |leader, window, cx| {
15183 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
15184 });
15185 follower
15186 .update(cx, |follower, window, cx| {
15187 follower.apply_update_proto(
15188 &project,
15189 pending_update.borrow_mut().take().unwrap(),
15190 window,
15191 cx,
15192 )
15193 })
15194 .unwrap()
15195 .await
15196 .unwrap();
15197 assert_eq!(
15198 follower
15199 .update(cx, |follower, _, cx| follower.scroll_position(cx))
15200 .unwrap(),
15201 gpui::Point::new(1.5, 3.5)
15202 );
15203 assert!(*is_still_following.borrow());
15204 assert_eq!(*follower_edit_event_count.borrow(), 0);
15205
15206 // Update the selections and scroll position. The follower's scroll position is updated
15207 // via autoscroll, not via the leader's exact scroll position.
15208 _ = leader.update(cx, |leader, window, cx| {
15209 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15210 s.select_ranges([0..0])
15211 });
15212 leader.request_autoscroll(Autoscroll::newest(), cx);
15213 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
15214 });
15215 follower
15216 .update(cx, |follower, window, cx| {
15217 follower.apply_update_proto(
15218 &project,
15219 pending_update.borrow_mut().take().unwrap(),
15220 window,
15221 cx,
15222 )
15223 })
15224 .unwrap()
15225 .await
15226 .unwrap();
15227 _ = follower.update(cx, |follower, _, cx| {
15228 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
15229 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
15230 });
15231 assert!(*is_still_following.borrow());
15232
15233 // Creating a pending selection that precedes another selection
15234 _ = leader.update(cx, |leader, window, cx| {
15235 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15236 s.select_ranges([1..1])
15237 });
15238 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
15239 });
15240 follower
15241 .update(cx, |follower, window, cx| {
15242 follower.apply_update_proto(
15243 &project,
15244 pending_update.borrow_mut().take().unwrap(),
15245 window,
15246 cx,
15247 )
15248 })
15249 .unwrap()
15250 .await
15251 .unwrap();
15252 _ = follower.update(cx, |follower, _, cx| {
15253 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
15254 });
15255 assert!(*is_still_following.borrow());
15256
15257 // Extend the pending selection so that it surrounds another selection
15258 _ = leader.update(cx, |leader, window, cx| {
15259 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
15260 });
15261 follower
15262 .update(cx, |follower, window, cx| {
15263 follower.apply_update_proto(
15264 &project,
15265 pending_update.borrow_mut().take().unwrap(),
15266 window,
15267 cx,
15268 )
15269 })
15270 .unwrap()
15271 .await
15272 .unwrap();
15273 _ = follower.update(cx, |follower, _, cx| {
15274 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
15275 });
15276
15277 // Scrolling locally breaks the follow
15278 _ = follower.update(cx, |follower, window, cx| {
15279 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
15280 follower.set_scroll_anchor(
15281 ScrollAnchor {
15282 anchor: top_anchor,
15283 offset: gpui::Point::new(0.0, 0.5),
15284 },
15285 window,
15286 cx,
15287 );
15288 });
15289 assert!(!(*is_still_following.borrow()));
15290}
15291
15292#[gpui::test]
15293async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
15294 init_test(cx, |_| {});
15295
15296 let fs = FakeFs::new(cx.executor());
15297 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
15298 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15299 let pane = workspace
15300 .update(cx, |workspace, _, _| workspace.active_pane().clone())
15301 .unwrap();
15302
15303 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
15304
15305 let leader = pane.update_in(cx, |_, window, cx| {
15306 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
15307 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
15308 });
15309
15310 // Start following the editor when it has no excerpts.
15311 let mut state_message =
15312 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
15313 let workspace_entity = workspace.root(cx).unwrap();
15314 let follower_1 = cx
15315 .update_window(*workspace.deref(), |_, window, cx| {
15316 Editor::from_state_proto(
15317 workspace_entity,
15318 ViewId {
15319 creator: CollaboratorId::PeerId(PeerId::default()),
15320 id: 0,
15321 },
15322 &mut state_message,
15323 window,
15324 cx,
15325 )
15326 })
15327 .unwrap()
15328 .unwrap()
15329 .await
15330 .unwrap();
15331
15332 let update_message = Rc::new(RefCell::new(None));
15333 follower_1.update_in(cx, {
15334 let update = update_message.clone();
15335 |_, window, cx| {
15336 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
15337 leader.read(cx).add_event_to_update_proto(
15338 event,
15339 &mut update.borrow_mut(),
15340 window,
15341 cx,
15342 );
15343 })
15344 .detach();
15345 }
15346 });
15347
15348 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
15349 (
15350 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
15351 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
15352 )
15353 });
15354
15355 // Insert some excerpts.
15356 leader.update(cx, |leader, cx| {
15357 leader.buffer.update(cx, |multibuffer, cx| {
15358 multibuffer.set_excerpts_for_path(
15359 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
15360 buffer_1.clone(),
15361 vec![
15362 Point::row_range(0..3),
15363 Point::row_range(1..6),
15364 Point::row_range(12..15),
15365 ],
15366 0,
15367 cx,
15368 );
15369 multibuffer.set_excerpts_for_path(
15370 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
15371 buffer_2.clone(),
15372 vec![Point::row_range(0..6), Point::row_range(8..12)],
15373 0,
15374 cx,
15375 );
15376 });
15377 });
15378
15379 // Apply the update of adding the excerpts.
15380 follower_1
15381 .update_in(cx, |follower, window, cx| {
15382 follower.apply_update_proto(
15383 &project,
15384 update_message.borrow().clone().unwrap(),
15385 window,
15386 cx,
15387 )
15388 })
15389 .await
15390 .unwrap();
15391 assert_eq!(
15392 follower_1.update(cx, |editor, cx| editor.text(cx)),
15393 leader.update(cx, |editor, cx| editor.text(cx))
15394 );
15395 update_message.borrow_mut().take();
15396
15397 // Start following separately after it already has excerpts.
15398 let mut state_message =
15399 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
15400 let workspace_entity = workspace.root(cx).unwrap();
15401 let follower_2 = cx
15402 .update_window(*workspace.deref(), |_, window, cx| {
15403 Editor::from_state_proto(
15404 workspace_entity,
15405 ViewId {
15406 creator: CollaboratorId::PeerId(PeerId::default()),
15407 id: 0,
15408 },
15409 &mut state_message,
15410 window,
15411 cx,
15412 )
15413 })
15414 .unwrap()
15415 .unwrap()
15416 .await
15417 .unwrap();
15418 assert_eq!(
15419 follower_2.update(cx, |editor, cx| editor.text(cx)),
15420 leader.update(cx, |editor, cx| editor.text(cx))
15421 );
15422
15423 // Remove some excerpts.
15424 leader.update(cx, |leader, cx| {
15425 leader.buffer.update(cx, |multibuffer, cx| {
15426 let excerpt_ids = multibuffer.excerpt_ids();
15427 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
15428 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
15429 });
15430 });
15431
15432 // Apply the update of removing the excerpts.
15433 follower_1
15434 .update_in(cx, |follower, window, cx| {
15435 follower.apply_update_proto(
15436 &project,
15437 update_message.borrow().clone().unwrap(),
15438 window,
15439 cx,
15440 )
15441 })
15442 .await
15443 .unwrap();
15444 follower_2
15445 .update_in(cx, |follower, window, cx| {
15446 follower.apply_update_proto(
15447 &project,
15448 update_message.borrow().clone().unwrap(),
15449 window,
15450 cx,
15451 )
15452 })
15453 .await
15454 .unwrap();
15455 update_message.borrow_mut().take();
15456 assert_eq!(
15457 follower_1.update(cx, |editor, cx| editor.text(cx)),
15458 leader.update(cx, |editor, cx| editor.text(cx))
15459 );
15460}
15461
15462#[gpui::test]
15463async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
15464 init_test(cx, |_| {});
15465
15466 let mut cx = EditorTestContext::new(cx).await;
15467 let lsp_store =
15468 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
15469
15470 cx.set_state(indoc! {"
15471 ˇfn func(abc def: i32) -> u32 {
15472 }
15473 "});
15474
15475 cx.update(|_, cx| {
15476 lsp_store.update(cx, |lsp_store, cx| {
15477 lsp_store
15478 .update_diagnostics(
15479 LanguageServerId(0),
15480 lsp::PublishDiagnosticsParams {
15481 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
15482 version: None,
15483 diagnostics: vec![
15484 lsp::Diagnostic {
15485 range: lsp::Range::new(
15486 lsp::Position::new(0, 11),
15487 lsp::Position::new(0, 12),
15488 ),
15489 severity: Some(lsp::DiagnosticSeverity::ERROR),
15490 ..Default::default()
15491 },
15492 lsp::Diagnostic {
15493 range: lsp::Range::new(
15494 lsp::Position::new(0, 12),
15495 lsp::Position::new(0, 15),
15496 ),
15497 severity: Some(lsp::DiagnosticSeverity::ERROR),
15498 ..Default::default()
15499 },
15500 lsp::Diagnostic {
15501 range: lsp::Range::new(
15502 lsp::Position::new(0, 25),
15503 lsp::Position::new(0, 28),
15504 ),
15505 severity: Some(lsp::DiagnosticSeverity::ERROR),
15506 ..Default::default()
15507 },
15508 ],
15509 },
15510 None,
15511 DiagnosticSourceKind::Pushed,
15512 &[],
15513 cx,
15514 )
15515 .unwrap()
15516 });
15517 });
15518
15519 executor.run_until_parked();
15520
15521 cx.update_editor(|editor, window, cx| {
15522 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
15523 });
15524
15525 cx.assert_editor_state(indoc! {"
15526 fn func(abc def: i32) -> ˇu32 {
15527 }
15528 "});
15529
15530 cx.update_editor(|editor, window, cx| {
15531 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
15532 });
15533
15534 cx.assert_editor_state(indoc! {"
15535 fn func(abc ˇdef: i32) -> u32 {
15536 }
15537 "});
15538
15539 cx.update_editor(|editor, window, cx| {
15540 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
15541 });
15542
15543 cx.assert_editor_state(indoc! {"
15544 fn func(abcˇ def: i32) -> u32 {
15545 }
15546 "});
15547
15548 cx.update_editor(|editor, window, cx| {
15549 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
15550 });
15551
15552 cx.assert_editor_state(indoc! {"
15553 fn func(abc def: i32) -> ˇu32 {
15554 }
15555 "});
15556}
15557
15558#[gpui::test]
15559async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
15560 init_test(cx, |_| {});
15561
15562 let mut cx = EditorTestContext::new(cx).await;
15563
15564 let diff_base = r#"
15565 use some::mod;
15566
15567 const A: u32 = 42;
15568
15569 fn main() {
15570 println!("hello");
15571
15572 println!("world");
15573 }
15574 "#
15575 .unindent();
15576
15577 // Edits are modified, removed, modified, added
15578 cx.set_state(
15579 &r#"
15580 use some::modified;
15581
15582 ˇ
15583 fn main() {
15584 println!("hello there");
15585
15586 println!("around the");
15587 println!("world");
15588 }
15589 "#
15590 .unindent(),
15591 );
15592
15593 cx.set_head_text(&diff_base);
15594 executor.run_until_parked();
15595
15596 cx.update_editor(|editor, window, cx| {
15597 //Wrap around the bottom of the buffer
15598 for _ in 0..3 {
15599 editor.go_to_next_hunk(&GoToHunk, window, cx);
15600 }
15601 });
15602
15603 cx.assert_editor_state(
15604 &r#"
15605 ˇuse some::modified;
15606
15607
15608 fn main() {
15609 println!("hello there");
15610
15611 println!("around the");
15612 println!("world");
15613 }
15614 "#
15615 .unindent(),
15616 );
15617
15618 cx.update_editor(|editor, window, cx| {
15619 //Wrap around the top of the buffer
15620 for _ in 0..2 {
15621 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
15622 }
15623 });
15624
15625 cx.assert_editor_state(
15626 &r#"
15627 use some::modified;
15628
15629
15630 fn main() {
15631 ˇ println!("hello there");
15632
15633 println!("around the");
15634 println!("world");
15635 }
15636 "#
15637 .unindent(),
15638 );
15639
15640 cx.update_editor(|editor, window, cx| {
15641 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
15642 });
15643
15644 cx.assert_editor_state(
15645 &r#"
15646 use some::modified;
15647
15648 ˇ
15649 fn main() {
15650 println!("hello there");
15651
15652 println!("around the");
15653 println!("world");
15654 }
15655 "#
15656 .unindent(),
15657 );
15658
15659 cx.update_editor(|editor, window, cx| {
15660 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
15661 });
15662
15663 cx.assert_editor_state(
15664 &r#"
15665 ˇuse some::modified;
15666
15667
15668 fn main() {
15669 println!("hello there");
15670
15671 println!("around the");
15672 println!("world");
15673 }
15674 "#
15675 .unindent(),
15676 );
15677
15678 cx.update_editor(|editor, window, cx| {
15679 for _ in 0..2 {
15680 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
15681 }
15682 });
15683
15684 cx.assert_editor_state(
15685 &r#"
15686 use some::modified;
15687
15688
15689 fn main() {
15690 ˇ println!("hello there");
15691
15692 println!("around the");
15693 println!("world");
15694 }
15695 "#
15696 .unindent(),
15697 );
15698
15699 cx.update_editor(|editor, window, cx| {
15700 editor.fold(&Fold, window, cx);
15701 });
15702
15703 cx.update_editor(|editor, window, cx| {
15704 editor.go_to_next_hunk(&GoToHunk, window, cx);
15705 });
15706
15707 cx.assert_editor_state(
15708 &r#"
15709 ˇuse some::modified;
15710
15711
15712 fn main() {
15713 println!("hello there");
15714
15715 println!("around the");
15716 println!("world");
15717 }
15718 "#
15719 .unindent(),
15720 );
15721}
15722
15723#[test]
15724fn test_split_words() {
15725 fn split(text: &str) -> Vec<&str> {
15726 split_words(text).collect()
15727 }
15728
15729 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
15730 assert_eq!(split("hello_world"), &["hello_", "world"]);
15731 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
15732 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
15733 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
15734 assert_eq!(split("helloworld"), &["helloworld"]);
15735
15736 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
15737}
15738
15739#[gpui::test]
15740async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
15741 init_test(cx, |_| {});
15742
15743 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
15744 let mut assert = |before, after| {
15745 let _state_context = cx.set_state(before);
15746 cx.run_until_parked();
15747 cx.update_editor(|editor, window, cx| {
15748 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
15749 });
15750 cx.run_until_parked();
15751 cx.assert_editor_state(after);
15752 };
15753
15754 // Outside bracket jumps to outside of matching bracket
15755 assert("console.logˇ(var);", "console.log(var)ˇ;");
15756 assert("console.log(var)ˇ;", "console.logˇ(var);");
15757
15758 // Inside bracket jumps to inside of matching bracket
15759 assert("console.log(ˇvar);", "console.log(varˇ);");
15760 assert("console.log(varˇ);", "console.log(ˇvar);");
15761
15762 // When outside a bracket and inside, favor jumping to the inside bracket
15763 assert(
15764 "console.log('foo', [1, 2, 3]ˇ);",
15765 "console.log(ˇ'foo', [1, 2, 3]);",
15766 );
15767 assert(
15768 "console.log(ˇ'foo', [1, 2, 3]);",
15769 "console.log('foo', [1, 2, 3]ˇ);",
15770 );
15771
15772 // Bias forward if two options are equally likely
15773 assert(
15774 "let result = curried_fun()ˇ();",
15775 "let result = curried_fun()()ˇ;",
15776 );
15777
15778 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
15779 assert(
15780 indoc! {"
15781 function test() {
15782 console.log('test')ˇ
15783 }"},
15784 indoc! {"
15785 function test() {
15786 console.logˇ('test')
15787 }"},
15788 );
15789}
15790
15791#[gpui::test]
15792async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
15793 init_test(cx, |_| {});
15794
15795 let fs = FakeFs::new(cx.executor());
15796 fs.insert_tree(
15797 path!("/a"),
15798 json!({
15799 "main.rs": "fn main() { let a = 5; }",
15800 "other.rs": "// Test file",
15801 }),
15802 )
15803 .await;
15804 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15805
15806 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15807 language_registry.add(Arc::new(Language::new(
15808 LanguageConfig {
15809 name: "Rust".into(),
15810 matcher: LanguageMatcher {
15811 path_suffixes: vec!["rs".to_string()],
15812 ..Default::default()
15813 },
15814 brackets: BracketPairConfig {
15815 pairs: vec![BracketPair {
15816 start: "{".to_string(),
15817 end: "}".to_string(),
15818 close: true,
15819 surround: true,
15820 newline: true,
15821 }],
15822 disabled_scopes_by_bracket_ix: Vec::new(),
15823 },
15824 ..Default::default()
15825 },
15826 Some(tree_sitter_rust::LANGUAGE.into()),
15827 )));
15828 let mut fake_servers = language_registry.register_fake_lsp(
15829 "Rust",
15830 FakeLspAdapter {
15831 capabilities: lsp::ServerCapabilities {
15832 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
15833 first_trigger_character: "{".to_string(),
15834 more_trigger_character: None,
15835 }),
15836 ..Default::default()
15837 },
15838 ..Default::default()
15839 },
15840 );
15841
15842 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15843
15844 let cx = &mut VisualTestContext::from_window(*workspace, cx);
15845
15846 let worktree_id = workspace
15847 .update(cx, |workspace, _, cx| {
15848 workspace.project().update(cx, |project, cx| {
15849 project.worktrees(cx).next().unwrap().read(cx).id()
15850 })
15851 })
15852 .unwrap();
15853
15854 let buffer = project
15855 .update(cx, |project, cx| {
15856 project.open_local_buffer(path!("/a/main.rs"), cx)
15857 })
15858 .await
15859 .unwrap();
15860 let editor_handle = workspace
15861 .update(cx, |workspace, window, cx| {
15862 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
15863 })
15864 .unwrap()
15865 .await
15866 .unwrap()
15867 .downcast::<Editor>()
15868 .unwrap();
15869
15870 cx.executor().start_waiting();
15871 let fake_server = fake_servers.next().await.unwrap();
15872
15873 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
15874 |params, _| async move {
15875 assert_eq!(
15876 params.text_document_position.text_document.uri,
15877 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
15878 );
15879 assert_eq!(
15880 params.text_document_position.position,
15881 lsp::Position::new(0, 21),
15882 );
15883
15884 Ok(Some(vec![lsp::TextEdit {
15885 new_text: "]".to_string(),
15886 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15887 }]))
15888 },
15889 );
15890
15891 editor_handle.update_in(cx, |editor, window, cx| {
15892 window.focus(&editor.focus_handle(cx));
15893 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15894 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
15895 });
15896 editor.handle_input("{", window, cx);
15897 });
15898
15899 cx.executor().run_until_parked();
15900
15901 buffer.update(cx, |buffer, _| {
15902 assert_eq!(
15903 buffer.text(),
15904 "fn main() { let a = {5}; }",
15905 "No extra braces from on type formatting should appear in the buffer"
15906 )
15907 });
15908}
15909
15910#[gpui::test(iterations = 20, seeds(31))]
15911async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
15912 init_test(cx, |_| {});
15913
15914 let mut cx = EditorLspTestContext::new_rust(
15915 lsp::ServerCapabilities {
15916 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
15917 first_trigger_character: ".".to_string(),
15918 more_trigger_character: None,
15919 }),
15920 ..Default::default()
15921 },
15922 cx,
15923 )
15924 .await;
15925
15926 cx.update_buffer(|buffer, _| {
15927 // This causes autoindent to be async.
15928 buffer.set_sync_parse_timeout(Duration::ZERO)
15929 });
15930
15931 cx.set_state("fn c() {\n d()ˇ\n}\n");
15932 cx.simulate_keystroke("\n");
15933 cx.run_until_parked();
15934
15935 let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap());
15936 let mut request =
15937 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
15938 let buffer_cloned = buffer_cloned.clone();
15939 async move {
15940 buffer_cloned.update(&mut cx, |buffer, _| {
15941 assert_eq!(
15942 buffer.text(),
15943 "fn c() {\n d()\n .\n}\n",
15944 "OnTypeFormatting should triggered after autoindent applied"
15945 )
15946 })?;
15947
15948 Ok(Some(vec![]))
15949 }
15950 });
15951
15952 cx.simulate_keystroke(".");
15953 cx.run_until_parked();
15954
15955 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
15956 assert!(request.next().await.is_some());
15957 request.close();
15958 assert!(request.next().await.is_none());
15959}
15960
15961#[gpui::test]
15962async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
15963 init_test(cx, |_| {});
15964
15965 let fs = FakeFs::new(cx.executor());
15966 fs.insert_tree(
15967 path!("/a"),
15968 json!({
15969 "main.rs": "fn main() { let a = 5; }",
15970 "other.rs": "// Test file",
15971 }),
15972 )
15973 .await;
15974
15975 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15976
15977 let server_restarts = Arc::new(AtomicUsize::new(0));
15978 let closure_restarts = Arc::clone(&server_restarts);
15979 let language_server_name = "test language server";
15980 let language_name: LanguageName = "Rust".into();
15981
15982 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15983 language_registry.add(Arc::new(Language::new(
15984 LanguageConfig {
15985 name: language_name.clone(),
15986 matcher: LanguageMatcher {
15987 path_suffixes: vec!["rs".to_string()],
15988 ..Default::default()
15989 },
15990 ..Default::default()
15991 },
15992 Some(tree_sitter_rust::LANGUAGE.into()),
15993 )));
15994 let mut fake_servers = language_registry.register_fake_lsp(
15995 "Rust",
15996 FakeLspAdapter {
15997 name: language_server_name,
15998 initialization_options: Some(json!({
15999 "testOptionValue": true
16000 })),
16001 initializer: Some(Box::new(move |fake_server| {
16002 let task_restarts = Arc::clone(&closure_restarts);
16003 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
16004 task_restarts.fetch_add(1, atomic::Ordering::Release);
16005 futures::future::ready(Ok(()))
16006 });
16007 })),
16008 ..Default::default()
16009 },
16010 );
16011
16012 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16013 let _buffer = project
16014 .update(cx, |project, cx| {
16015 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
16016 })
16017 .await
16018 .unwrap();
16019 let _fake_server = fake_servers.next().await.unwrap();
16020 update_test_language_settings(cx, |language_settings| {
16021 language_settings.languages.0.insert(
16022 language_name.clone(),
16023 LanguageSettingsContent {
16024 tab_size: NonZeroU32::new(8),
16025 ..Default::default()
16026 },
16027 );
16028 });
16029 cx.executor().run_until_parked();
16030 assert_eq!(
16031 server_restarts.load(atomic::Ordering::Acquire),
16032 0,
16033 "Should not restart LSP server on an unrelated change"
16034 );
16035
16036 update_test_project_settings(cx, |project_settings| {
16037 project_settings.lsp.insert(
16038 "Some other server name".into(),
16039 LspSettings {
16040 binary: None,
16041 settings: None,
16042 initialization_options: Some(json!({
16043 "some other init value": false
16044 })),
16045 enable_lsp_tasks: false,
16046 },
16047 );
16048 });
16049 cx.executor().run_until_parked();
16050 assert_eq!(
16051 server_restarts.load(atomic::Ordering::Acquire),
16052 0,
16053 "Should not restart LSP server on an unrelated LSP settings change"
16054 );
16055
16056 update_test_project_settings(cx, |project_settings| {
16057 project_settings.lsp.insert(
16058 language_server_name.into(),
16059 LspSettings {
16060 binary: None,
16061 settings: None,
16062 initialization_options: Some(json!({
16063 "anotherInitValue": false
16064 })),
16065 enable_lsp_tasks: false,
16066 },
16067 );
16068 });
16069 cx.executor().run_until_parked();
16070 assert_eq!(
16071 server_restarts.load(atomic::Ordering::Acquire),
16072 1,
16073 "Should restart LSP server on a related LSP settings change"
16074 );
16075
16076 update_test_project_settings(cx, |project_settings| {
16077 project_settings.lsp.insert(
16078 language_server_name.into(),
16079 LspSettings {
16080 binary: None,
16081 settings: None,
16082 initialization_options: Some(json!({
16083 "anotherInitValue": false
16084 })),
16085 enable_lsp_tasks: false,
16086 },
16087 );
16088 });
16089 cx.executor().run_until_parked();
16090 assert_eq!(
16091 server_restarts.load(atomic::Ordering::Acquire),
16092 1,
16093 "Should not restart LSP server on a related LSP settings change that is the same"
16094 );
16095
16096 update_test_project_settings(cx, |project_settings| {
16097 project_settings.lsp.insert(
16098 language_server_name.into(),
16099 LspSettings {
16100 binary: None,
16101 settings: None,
16102 initialization_options: None,
16103 enable_lsp_tasks: false,
16104 },
16105 );
16106 });
16107 cx.executor().run_until_parked();
16108 assert_eq!(
16109 server_restarts.load(atomic::Ordering::Acquire),
16110 2,
16111 "Should restart LSP server on another related LSP settings change"
16112 );
16113}
16114
16115#[gpui::test]
16116async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
16117 init_test(cx, |_| {});
16118
16119 let mut cx = EditorLspTestContext::new_rust(
16120 lsp::ServerCapabilities {
16121 completion_provider: Some(lsp::CompletionOptions {
16122 trigger_characters: Some(vec![".".to_string()]),
16123 resolve_provider: Some(true),
16124 ..Default::default()
16125 }),
16126 ..Default::default()
16127 },
16128 cx,
16129 )
16130 .await;
16131
16132 cx.set_state("fn main() { let a = 2ˇ; }");
16133 cx.simulate_keystroke(".");
16134 let completion_item = lsp::CompletionItem {
16135 label: "some".into(),
16136 kind: Some(lsp::CompletionItemKind::SNIPPET),
16137 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
16138 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
16139 kind: lsp::MarkupKind::Markdown,
16140 value: "```rust\nSome(2)\n```".to_string(),
16141 })),
16142 deprecated: Some(false),
16143 sort_text: Some("fffffff2".to_string()),
16144 filter_text: Some("some".to_string()),
16145 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
16146 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16147 range: lsp::Range {
16148 start: lsp::Position {
16149 line: 0,
16150 character: 22,
16151 },
16152 end: lsp::Position {
16153 line: 0,
16154 character: 22,
16155 },
16156 },
16157 new_text: "Some(2)".to_string(),
16158 })),
16159 additional_text_edits: Some(vec![lsp::TextEdit {
16160 range: lsp::Range {
16161 start: lsp::Position {
16162 line: 0,
16163 character: 20,
16164 },
16165 end: lsp::Position {
16166 line: 0,
16167 character: 22,
16168 },
16169 },
16170 new_text: "".to_string(),
16171 }]),
16172 ..Default::default()
16173 };
16174
16175 let closure_completion_item = completion_item.clone();
16176 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
16177 let task_completion_item = closure_completion_item.clone();
16178 async move {
16179 Ok(Some(lsp::CompletionResponse::Array(vec![
16180 task_completion_item,
16181 ])))
16182 }
16183 });
16184
16185 request.next().await;
16186
16187 cx.condition(|editor, _| editor.context_menu_visible())
16188 .await;
16189 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
16190 editor
16191 .confirm_completion(&ConfirmCompletion::default(), window, cx)
16192 .unwrap()
16193 });
16194 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
16195
16196 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
16197 let task_completion_item = completion_item.clone();
16198 async move { Ok(task_completion_item) }
16199 })
16200 .next()
16201 .await
16202 .unwrap();
16203 apply_additional_edits.await.unwrap();
16204 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
16205}
16206
16207#[gpui::test]
16208async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
16209 init_test(cx, |_| {});
16210
16211 let mut cx = EditorLspTestContext::new_rust(
16212 lsp::ServerCapabilities {
16213 completion_provider: Some(lsp::CompletionOptions {
16214 trigger_characters: Some(vec![".".to_string()]),
16215 resolve_provider: Some(true),
16216 ..Default::default()
16217 }),
16218 ..Default::default()
16219 },
16220 cx,
16221 )
16222 .await;
16223
16224 cx.set_state("fn main() { let a = 2ˇ; }");
16225 cx.simulate_keystroke(".");
16226
16227 let item1 = lsp::CompletionItem {
16228 label: "method id()".to_string(),
16229 filter_text: Some("id".to_string()),
16230 detail: None,
16231 documentation: None,
16232 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16233 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
16234 new_text: ".id".to_string(),
16235 })),
16236 ..lsp::CompletionItem::default()
16237 };
16238
16239 let item2 = lsp::CompletionItem {
16240 label: "other".to_string(),
16241 filter_text: Some("other".to_string()),
16242 detail: None,
16243 documentation: None,
16244 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16245 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
16246 new_text: ".other".to_string(),
16247 })),
16248 ..lsp::CompletionItem::default()
16249 };
16250
16251 let item1 = item1.clone();
16252 cx.set_request_handler::<lsp::request::Completion, _, _>({
16253 let item1 = item1.clone();
16254 move |_, _, _| {
16255 let item1 = item1.clone();
16256 let item2 = item2.clone();
16257 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
16258 }
16259 })
16260 .next()
16261 .await;
16262
16263 cx.condition(|editor, _| editor.context_menu_visible())
16264 .await;
16265 cx.update_editor(|editor, _, _| {
16266 let context_menu = editor.context_menu.borrow_mut();
16267 let context_menu = context_menu
16268 .as_ref()
16269 .expect("Should have the context menu deployed");
16270 match context_menu {
16271 CodeContextMenu::Completions(completions_menu) => {
16272 let completions = completions_menu.completions.borrow_mut();
16273 assert_eq!(
16274 completions
16275 .iter()
16276 .map(|completion| &completion.label.text)
16277 .collect::<Vec<_>>(),
16278 vec!["method id()", "other"]
16279 )
16280 }
16281 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
16282 }
16283 });
16284
16285 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
16286 let item1 = item1.clone();
16287 move |_, item_to_resolve, _| {
16288 let item1 = item1.clone();
16289 async move {
16290 if item1 == item_to_resolve {
16291 Ok(lsp::CompletionItem {
16292 label: "method id()".to_string(),
16293 filter_text: Some("id".to_string()),
16294 detail: Some("Now resolved!".to_string()),
16295 documentation: Some(lsp::Documentation::String("Docs".to_string())),
16296 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16297 range: lsp::Range::new(
16298 lsp::Position::new(0, 22),
16299 lsp::Position::new(0, 22),
16300 ),
16301 new_text: ".id".to_string(),
16302 })),
16303 ..lsp::CompletionItem::default()
16304 })
16305 } else {
16306 Ok(item_to_resolve)
16307 }
16308 }
16309 }
16310 })
16311 .next()
16312 .await
16313 .unwrap();
16314 cx.run_until_parked();
16315
16316 cx.update_editor(|editor, window, cx| {
16317 editor.context_menu_next(&Default::default(), window, cx);
16318 });
16319
16320 cx.update_editor(|editor, _, _| {
16321 let context_menu = editor.context_menu.borrow_mut();
16322 let context_menu = context_menu
16323 .as_ref()
16324 .expect("Should have the context menu deployed");
16325 match context_menu {
16326 CodeContextMenu::Completions(completions_menu) => {
16327 let completions = completions_menu.completions.borrow_mut();
16328 assert_eq!(
16329 completions
16330 .iter()
16331 .map(|completion| &completion.label.text)
16332 .collect::<Vec<_>>(),
16333 vec!["method id() Now resolved!", "other"],
16334 "Should update first completion label, but not second as the filter text did not match."
16335 );
16336 }
16337 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
16338 }
16339 });
16340}
16341
16342#[gpui::test]
16343async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
16344 init_test(cx, |_| {});
16345 let mut cx = EditorLspTestContext::new_rust(
16346 lsp::ServerCapabilities {
16347 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
16348 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
16349 completion_provider: Some(lsp::CompletionOptions {
16350 resolve_provider: Some(true),
16351 ..Default::default()
16352 }),
16353 ..Default::default()
16354 },
16355 cx,
16356 )
16357 .await;
16358 cx.set_state(indoc! {"
16359 struct TestStruct {
16360 field: i32
16361 }
16362
16363 fn mainˇ() {
16364 let unused_var = 42;
16365 let test_struct = TestStruct { field: 42 };
16366 }
16367 "});
16368 let symbol_range = cx.lsp_range(indoc! {"
16369 struct TestStruct {
16370 field: i32
16371 }
16372
16373 «fn main»() {
16374 let unused_var = 42;
16375 let test_struct = TestStruct { field: 42 };
16376 }
16377 "});
16378 let mut hover_requests =
16379 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
16380 Ok(Some(lsp::Hover {
16381 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
16382 kind: lsp::MarkupKind::Markdown,
16383 value: "Function documentation".to_string(),
16384 }),
16385 range: Some(symbol_range),
16386 }))
16387 });
16388
16389 // Case 1: Test that code action menu hide hover popover
16390 cx.dispatch_action(Hover);
16391 hover_requests.next().await;
16392 cx.condition(|editor, _| editor.hover_state.visible()).await;
16393 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
16394 move |_, _, _| async move {
16395 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
16396 lsp::CodeAction {
16397 title: "Remove unused variable".to_string(),
16398 kind: Some(CodeActionKind::QUICKFIX),
16399 edit: Some(lsp::WorkspaceEdit {
16400 changes: Some(
16401 [(
16402 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
16403 vec![lsp::TextEdit {
16404 range: lsp::Range::new(
16405 lsp::Position::new(5, 4),
16406 lsp::Position::new(5, 27),
16407 ),
16408 new_text: "".to_string(),
16409 }],
16410 )]
16411 .into_iter()
16412 .collect(),
16413 ),
16414 ..Default::default()
16415 }),
16416 ..Default::default()
16417 },
16418 )]))
16419 },
16420 );
16421 cx.update_editor(|editor, window, cx| {
16422 editor.toggle_code_actions(
16423 &ToggleCodeActions {
16424 deployed_from: None,
16425 quick_launch: false,
16426 },
16427 window,
16428 cx,
16429 );
16430 });
16431 code_action_requests.next().await;
16432 cx.run_until_parked();
16433 cx.condition(|editor, _| editor.context_menu_visible())
16434 .await;
16435 cx.update_editor(|editor, _, _| {
16436 assert!(
16437 !editor.hover_state.visible(),
16438 "Hover popover should be hidden when code action menu is shown"
16439 );
16440 // Hide code actions
16441 editor.context_menu.take();
16442 });
16443
16444 // Case 2: Test that code completions hide hover popover
16445 cx.dispatch_action(Hover);
16446 hover_requests.next().await;
16447 cx.condition(|editor, _| editor.hover_state.visible()).await;
16448 let counter = Arc::new(AtomicUsize::new(0));
16449 let mut completion_requests =
16450 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
16451 let counter = counter.clone();
16452 async move {
16453 counter.fetch_add(1, atomic::Ordering::Release);
16454 Ok(Some(lsp::CompletionResponse::Array(vec![
16455 lsp::CompletionItem {
16456 label: "main".into(),
16457 kind: Some(lsp::CompletionItemKind::FUNCTION),
16458 detail: Some("() -> ()".to_string()),
16459 ..Default::default()
16460 },
16461 lsp::CompletionItem {
16462 label: "TestStruct".into(),
16463 kind: Some(lsp::CompletionItemKind::STRUCT),
16464 detail: Some("struct TestStruct".to_string()),
16465 ..Default::default()
16466 },
16467 ])))
16468 }
16469 });
16470 cx.update_editor(|editor, window, cx| {
16471 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
16472 });
16473 completion_requests.next().await;
16474 cx.condition(|editor, _| editor.context_menu_visible())
16475 .await;
16476 cx.update_editor(|editor, _, _| {
16477 assert!(
16478 !editor.hover_state.visible(),
16479 "Hover popover should be hidden when completion menu is shown"
16480 );
16481 });
16482}
16483
16484#[gpui::test]
16485async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
16486 init_test(cx, |_| {});
16487
16488 let mut cx = EditorLspTestContext::new_rust(
16489 lsp::ServerCapabilities {
16490 completion_provider: Some(lsp::CompletionOptions {
16491 trigger_characters: Some(vec![".".to_string()]),
16492 resolve_provider: Some(true),
16493 ..Default::default()
16494 }),
16495 ..Default::default()
16496 },
16497 cx,
16498 )
16499 .await;
16500
16501 cx.set_state("fn main() { let a = 2ˇ; }");
16502 cx.simulate_keystroke(".");
16503
16504 let unresolved_item_1 = lsp::CompletionItem {
16505 label: "id".to_string(),
16506 filter_text: Some("id".to_string()),
16507 detail: None,
16508 documentation: None,
16509 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16510 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
16511 new_text: ".id".to_string(),
16512 })),
16513 ..lsp::CompletionItem::default()
16514 };
16515 let resolved_item_1 = lsp::CompletionItem {
16516 additional_text_edits: Some(vec![lsp::TextEdit {
16517 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
16518 new_text: "!!".to_string(),
16519 }]),
16520 ..unresolved_item_1.clone()
16521 };
16522 let unresolved_item_2 = lsp::CompletionItem {
16523 label: "other".to_string(),
16524 filter_text: Some("other".to_string()),
16525 detail: None,
16526 documentation: None,
16527 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16528 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
16529 new_text: ".other".to_string(),
16530 })),
16531 ..lsp::CompletionItem::default()
16532 };
16533 let resolved_item_2 = lsp::CompletionItem {
16534 additional_text_edits: Some(vec![lsp::TextEdit {
16535 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
16536 new_text: "??".to_string(),
16537 }]),
16538 ..unresolved_item_2.clone()
16539 };
16540
16541 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
16542 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
16543 cx.lsp
16544 .server
16545 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
16546 let unresolved_item_1 = unresolved_item_1.clone();
16547 let resolved_item_1 = resolved_item_1.clone();
16548 let unresolved_item_2 = unresolved_item_2.clone();
16549 let resolved_item_2 = resolved_item_2.clone();
16550 let resolve_requests_1 = resolve_requests_1.clone();
16551 let resolve_requests_2 = resolve_requests_2.clone();
16552 move |unresolved_request, _| {
16553 let unresolved_item_1 = unresolved_item_1.clone();
16554 let resolved_item_1 = resolved_item_1.clone();
16555 let unresolved_item_2 = unresolved_item_2.clone();
16556 let resolved_item_2 = resolved_item_2.clone();
16557 let resolve_requests_1 = resolve_requests_1.clone();
16558 let resolve_requests_2 = resolve_requests_2.clone();
16559 async move {
16560 if unresolved_request == unresolved_item_1 {
16561 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
16562 Ok(resolved_item_1.clone())
16563 } else if unresolved_request == unresolved_item_2 {
16564 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
16565 Ok(resolved_item_2.clone())
16566 } else {
16567 panic!("Unexpected completion item {unresolved_request:?}")
16568 }
16569 }
16570 }
16571 })
16572 .detach();
16573
16574 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
16575 let unresolved_item_1 = unresolved_item_1.clone();
16576 let unresolved_item_2 = unresolved_item_2.clone();
16577 async move {
16578 Ok(Some(lsp::CompletionResponse::Array(vec![
16579 unresolved_item_1,
16580 unresolved_item_2,
16581 ])))
16582 }
16583 })
16584 .next()
16585 .await;
16586
16587 cx.condition(|editor, _| editor.context_menu_visible())
16588 .await;
16589 cx.update_editor(|editor, _, _| {
16590 let context_menu = editor.context_menu.borrow_mut();
16591 let context_menu = context_menu
16592 .as_ref()
16593 .expect("Should have the context menu deployed");
16594 match context_menu {
16595 CodeContextMenu::Completions(completions_menu) => {
16596 let completions = completions_menu.completions.borrow_mut();
16597 assert_eq!(
16598 completions
16599 .iter()
16600 .map(|completion| &completion.label.text)
16601 .collect::<Vec<_>>(),
16602 vec!["id", "other"]
16603 )
16604 }
16605 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
16606 }
16607 });
16608 cx.run_until_parked();
16609
16610 cx.update_editor(|editor, window, cx| {
16611 editor.context_menu_next(&ContextMenuNext, window, cx);
16612 });
16613 cx.run_until_parked();
16614 cx.update_editor(|editor, window, cx| {
16615 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
16616 });
16617 cx.run_until_parked();
16618 cx.update_editor(|editor, window, cx| {
16619 editor.context_menu_next(&ContextMenuNext, window, cx);
16620 });
16621 cx.run_until_parked();
16622 cx.update_editor(|editor, window, cx| {
16623 editor
16624 .compose_completion(&ComposeCompletion::default(), window, cx)
16625 .expect("No task returned")
16626 })
16627 .await
16628 .expect("Completion failed");
16629 cx.run_until_parked();
16630
16631 cx.update_editor(|editor, _, cx| {
16632 assert_eq!(
16633 resolve_requests_1.load(atomic::Ordering::Acquire),
16634 1,
16635 "Should always resolve once despite multiple selections"
16636 );
16637 assert_eq!(
16638 resolve_requests_2.load(atomic::Ordering::Acquire),
16639 1,
16640 "Should always resolve once after multiple selections and applying the completion"
16641 );
16642 assert_eq!(
16643 editor.text(cx),
16644 "fn main() { let a = ??.other; }",
16645 "Should use resolved data when applying the completion"
16646 );
16647 });
16648}
16649
16650#[gpui::test]
16651async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
16652 init_test(cx, |_| {});
16653
16654 let item_0 = lsp::CompletionItem {
16655 label: "abs".into(),
16656 insert_text: Some("abs".into()),
16657 data: Some(json!({ "very": "special"})),
16658 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
16659 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
16660 lsp::InsertReplaceEdit {
16661 new_text: "abs".to_string(),
16662 insert: lsp::Range::default(),
16663 replace: lsp::Range::default(),
16664 },
16665 )),
16666 ..lsp::CompletionItem::default()
16667 };
16668 let items = iter::once(item_0.clone())
16669 .chain((11..51).map(|i| lsp::CompletionItem {
16670 label: format!("item_{}", i),
16671 insert_text: Some(format!("item_{}", i)),
16672 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
16673 ..lsp::CompletionItem::default()
16674 }))
16675 .collect::<Vec<_>>();
16676
16677 let default_commit_characters = vec!["?".to_string()];
16678 let default_data = json!({ "default": "data"});
16679 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
16680 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
16681 let default_edit_range = lsp::Range {
16682 start: lsp::Position {
16683 line: 0,
16684 character: 5,
16685 },
16686 end: lsp::Position {
16687 line: 0,
16688 character: 5,
16689 },
16690 };
16691
16692 let mut cx = EditorLspTestContext::new_rust(
16693 lsp::ServerCapabilities {
16694 completion_provider: Some(lsp::CompletionOptions {
16695 trigger_characters: Some(vec![".".to_string()]),
16696 resolve_provider: Some(true),
16697 ..Default::default()
16698 }),
16699 ..Default::default()
16700 },
16701 cx,
16702 )
16703 .await;
16704
16705 cx.set_state("fn main() { let a = 2ˇ; }");
16706 cx.simulate_keystroke(".");
16707
16708 let completion_data = default_data.clone();
16709 let completion_characters = default_commit_characters.clone();
16710 let completion_items = items.clone();
16711 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
16712 let default_data = completion_data.clone();
16713 let default_commit_characters = completion_characters.clone();
16714 let items = completion_items.clone();
16715 async move {
16716 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
16717 items,
16718 item_defaults: Some(lsp::CompletionListItemDefaults {
16719 data: Some(default_data.clone()),
16720 commit_characters: Some(default_commit_characters.clone()),
16721 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
16722 default_edit_range,
16723 )),
16724 insert_text_format: Some(default_insert_text_format),
16725 insert_text_mode: Some(default_insert_text_mode),
16726 }),
16727 ..lsp::CompletionList::default()
16728 })))
16729 }
16730 })
16731 .next()
16732 .await;
16733
16734 let resolved_items = Arc::new(Mutex::new(Vec::new()));
16735 cx.lsp
16736 .server
16737 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
16738 let closure_resolved_items = resolved_items.clone();
16739 move |item_to_resolve, _| {
16740 let closure_resolved_items = closure_resolved_items.clone();
16741 async move {
16742 closure_resolved_items.lock().push(item_to_resolve.clone());
16743 Ok(item_to_resolve)
16744 }
16745 }
16746 })
16747 .detach();
16748
16749 cx.condition(|editor, _| editor.context_menu_visible())
16750 .await;
16751 cx.run_until_parked();
16752 cx.update_editor(|editor, _, _| {
16753 let menu = editor.context_menu.borrow_mut();
16754 match menu.as_ref().expect("should have the completions menu") {
16755 CodeContextMenu::Completions(completions_menu) => {
16756 assert_eq!(
16757 completions_menu
16758 .entries
16759 .borrow()
16760 .iter()
16761 .map(|mat| mat.string.clone())
16762 .collect::<Vec<String>>(),
16763 items
16764 .iter()
16765 .map(|completion| completion.label.clone())
16766 .collect::<Vec<String>>()
16767 );
16768 }
16769 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
16770 }
16771 });
16772 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
16773 // with 4 from the end.
16774 assert_eq!(
16775 *resolved_items.lock(),
16776 [&items[0..16], &items[items.len() - 4..items.len()]]
16777 .concat()
16778 .iter()
16779 .cloned()
16780 .map(|mut item| {
16781 if item.data.is_none() {
16782 item.data = Some(default_data.clone());
16783 }
16784 item
16785 })
16786 .collect::<Vec<lsp::CompletionItem>>(),
16787 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
16788 );
16789 resolved_items.lock().clear();
16790
16791 cx.update_editor(|editor, window, cx| {
16792 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
16793 });
16794 cx.run_until_parked();
16795 // Completions that have already been resolved are skipped.
16796 assert_eq!(
16797 *resolved_items.lock(),
16798 items[items.len() - 17..items.len() - 4]
16799 .iter()
16800 .cloned()
16801 .map(|mut item| {
16802 if item.data.is_none() {
16803 item.data = Some(default_data.clone());
16804 }
16805 item
16806 })
16807 .collect::<Vec<lsp::CompletionItem>>()
16808 );
16809 resolved_items.lock().clear();
16810}
16811
16812#[gpui::test]
16813async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
16814 init_test(cx, |_| {});
16815
16816 let mut cx = EditorLspTestContext::new(
16817 Language::new(
16818 LanguageConfig {
16819 matcher: LanguageMatcher {
16820 path_suffixes: vec!["jsx".into()],
16821 ..Default::default()
16822 },
16823 overrides: [(
16824 "element".into(),
16825 LanguageConfigOverride {
16826 completion_query_characters: Override::Set(['-'].into_iter().collect()),
16827 ..Default::default()
16828 },
16829 )]
16830 .into_iter()
16831 .collect(),
16832 ..Default::default()
16833 },
16834 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
16835 )
16836 .with_override_query("(jsx_self_closing_element) @element")
16837 .unwrap(),
16838 lsp::ServerCapabilities {
16839 completion_provider: Some(lsp::CompletionOptions {
16840 trigger_characters: Some(vec![":".to_string()]),
16841 ..Default::default()
16842 }),
16843 ..Default::default()
16844 },
16845 cx,
16846 )
16847 .await;
16848
16849 cx.lsp
16850 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
16851 Ok(Some(lsp::CompletionResponse::Array(vec![
16852 lsp::CompletionItem {
16853 label: "bg-blue".into(),
16854 ..Default::default()
16855 },
16856 lsp::CompletionItem {
16857 label: "bg-red".into(),
16858 ..Default::default()
16859 },
16860 lsp::CompletionItem {
16861 label: "bg-yellow".into(),
16862 ..Default::default()
16863 },
16864 ])))
16865 });
16866
16867 cx.set_state(r#"<p class="bgˇ" />"#);
16868
16869 // Trigger completion when typing a dash, because the dash is an extra
16870 // word character in the 'element' scope, which contains the cursor.
16871 cx.simulate_keystroke("-");
16872 cx.executor().run_until_parked();
16873 cx.update_editor(|editor, _, _| {
16874 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16875 {
16876 assert_eq!(
16877 completion_menu_entries(menu),
16878 &["bg-blue", "bg-red", "bg-yellow"]
16879 );
16880 } else {
16881 panic!("expected completion menu to be open");
16882 }
16883 });
16884
16885 cx.simulate_keystroke("l");
16886 cx.executor().run_until_parked();
16887 cx.update_editor(|editor, _, _| {
16888 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16889 {
16890 assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]);
16891 } else {
16892 panic!("expected completion menu to be open");
16893 }
16894 });
16895
16896 // When filtering completions, consider the character after the '-' to
16897 // be the start of a subword.
16898 cx.set_state(r#"<p class="yelˇ" />"#);
16899 cx.simulate_keystroke("l");
16900 cx.executor().run_until_parked();
16901 cx.update_editor(|editor, _, _| {
16902 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16903 {
16904 assert_eq!(completion_menu_entries(menu), &["bg-yellow"]);
16905 } else {
16906 panic!("expected completion menu to be open");
16907 }
16908 });
16909}
16910
16911fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
16912 let entries = menu.entries.borrow();
16913 entries.iter().map(|mat| mat.string.clone()).collect()
16914}
16915
16916#[gpui::test]
16917async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
16918 init_test(cx, |settings| {
16919 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
16920 Formatter::Prettier,
16921 )))
16922 });
16923
16924 let fs = FakeFs::new(cx.executor());
16925 fs.insert_file(path!("/file.ts"), Default::default()).await;
16926
16927 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
16928 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
16929
16930 language_registry.add(Arc::new(Language::new(
16931 LanguageConfig {
16932 name: "TypeScript".into(),
16933 matcher: LanguageMatcher {
16934 path_suffixes: vec!["ts".to_string()],
16935 ..Default::default()
16936 },
16937 ..Default::default()
16938 },
16939 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
16940 )));
16941 update_test_language_settings(cx, |settings| {
16942 settings.defaults.prettier = Some(PrettierSettings {
16943 allowed: true,
16944 ..PrettierSettings::default()
16945 });
16946 });
16947
16948 let test_plugin = "test_plugin";
16949 let _ = language_registry.register_fake_lsp(
16950 "TypeScript",
16951 FakeLspAdapter {
16952 prettier_plugins: vec![test_plugin],
16953 ..Default::default()
16954 },
16955 );
16956
16957 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
16958 let buffer = project
16959 .update(cx, |project, cx| {
16960 project.open_local_buffer(path!("/file.ts"), cx)
16961 })
16962 .await
16963 .unwrap();
16964
16965 let buffer_text = "one\ntwo\nthree\n";
16966 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
16967 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
16968 editor.update_in(cx, |editor, window, cx| {
16969 editor.set_text(buffer_text, window, cx)
16970 });
16971
16972 editor
16973 .update_in(cx, |editor, window, cx| {
16974 editor.perform_format(
16975 project.clone(),
16976 FormatTrigger::Manual,
16977 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
16978 window,
16979 cx,
16980 )
16981 })
16982 .unwrap()
16983 .await;
16984 assert_eq!(
16985 editor.update(cx, |editor, cx| editor.text(cx)),
16986 buffer_text.to_string() + prettier_format_suffix,
16987 "Test prettier formatting was not applied to the original buffer text",
16988 );
16989
16990 update_test_language_settings(cx, |settings| {
16991 settings.defaults.formatter = Some(SelectedFormatter::Auto)
16992 });
16993 let format = editor.update_in(cx, |editor, window, cx| {
16994 editor.perform_format(
16995 project.clone(),
16996 FormatTrigger::Manual,
16997 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
16998 window,
16999 cx,
17000 )
17001 });
17002 format.await.unwrap();
17003 assert_eq!(
17004 editor.update(cx, |editor, cx| editor.text(cx)),
17005 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
17006 "Autoformatting (via test prettier) was not applied to the original buffer text",
17007 );
17008}
17009
17010#[gpui::test]
17011async fn test_addition_reverts(cx: &mut TestAppContext) {
17012 init_test(cx, |_| {});
17013 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
17014 let base_text = indoc! {r#"
17015 struct Row;
17016 struct Row1;
17017 struct Row2;
17018
17019 struct Row4;
17020 struct Row5;
17021 struct Row6;
17022
17023 struct Row8;
17024 struct Row9;
17025 struct Row10;"#};
17026
17027 // When addition hunks are not adjacent to carets, no hunk revert is performed
17028 assert_hunk_revert(
17029 indoc! {r#"struct Row;
17030 struct Row1;
17031 struct Row1.1;
17032 struct Row1.2;
17033 struct Row2;ˇ
17034
17035 struct Row4;
17036 struct Row5;
17037 struct Row6;
17038
17039 struct Row8;
17040 ˇstruct Row9;
17041 struct Row9.1;
17042 struct Row9.2;
17043 struct Row9.3;
17044 struct Row10;"#},
17045 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
17046 indoc! {r#"struct Row;
17047 struct Row1;
17048 struct Row1.1;
17049 struct Row1.2;
17050 struct Row2;ˇ
17051
17052 struct Row4;
17053 struct Row5;
17054 struct Row6;
17055
17056 struct Row8;
17057 ˇstruct Row9;
17058 struct Row9.1;
17059 struct Row9.2;
17060 struct Row9.3;
17061 struct Row10;"#},
17062 base_text,
17063 &mut cx,
17064 );
17065 // Same for selections
17066 assert_hunk_revert(
17067 indoc! {r#"struct Row;
17068 struct Row1;
17069 struct Row2;
17070 struct Row2.1;
17071 struct Row2.2;
17072 «ˇ
17073 struct Row4;
17074 struct» Row5;
17075 «struct Row6;
17076 ˇ»
17077 struct Row9.1;
17078 struct Row9.2;
17079 struct Row9.3;
17080 struct Row8;
17081 struct Row9;
17082 struct Row10;"#},
17083 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
17084 indoc! {r#"struct Row;
17085 struct Row1;
17086 struct Row2;
17087 struct Row2.1;
17088 struct Row2.2;
17089 «ˇ
17090 struct Row4;
17091 struct» Row5;
17092 «struct Row6;
17093 ˇ»
17094 struct Row9.1;
17095 struct Row9.2;
17096 struct Row9.3;
17097 struct Row8;
17098 struct Row9;
17099 struct Row10;"#},
17100 base_text,
17101 &mut cx,
17102 );
17103
17104 // When carets and selections intersect the addition hunks, those are reverted.
17105 // Adjacent carets got merged.
17106 assert_hunk_revert(
17107 indoc! {r#"struct Row;
17108 ˇ// something on the top
17109 struct Row1;
17110 struct Row2;
17111 struct Roˇw3.1;
17112 struct Row2.2;
17113 struct Row2.3;ˇ
17114
17115 struct Row4;
17116 struct ˇRow5.1;
17117 struct Row5.2;
17118 struct «Rowˇ»5.3;
17119 struct Row5;
17120 struct Row6;
17121 ˇ
17122 struct Row9.1;
17123 struct «Rowˇ»9.2;
17124 struct «ˇRow»9.3;
17125 struct Row8;
17126 struct Row9;
17127 «ˇ// something on bottom»
17128 struct Row10;"#},
17129 vec![
17130 DiffHunkStatusKind::Added,
17131 DiffHunkStatusKind::Added,
17132 DiffHunkStatusKind::Added,
17133 DiffHunkStatusKind::Added,
17134 DiffHunkStatusKind::Added,
17135 ],
17136 indoc! {r#"struct Row;
17137 ˇstruct Row1;
17138 struct Row2;
17139 ˇ
17140 struct Row4;
17141 ˇstruct Row5;
17142 struct Row6;
17143 ˇ
17144 ˇstruct Row8;
17145 struct Row9;
17146 ˇstruct Row10;"#},
17147 base_text,
17148 &mut cx,
17149 );
17150}
17151
17152#[gpui::test]
17153async fn test_modification_reverts(cx: &mut TestAppContext) {
17154 init_test(cx, |_| {});
17155 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
17156 let base_text = indoc! {r#"
17157 struct Row;
17158 struct Row1;
17159 struct Row2;
17160
17161 struct Row4;
17162 struct Row5;
17163 struct Row6;
17164
17165 struct Row8;
17166 struct Row9;
17167 struct Row10;"#};
17168
17169 // Modification hunks behave the same as the addition ones.
17170 assert_hunk_revert(
17171 indoc! {r#"struct Row;
17172 struct Row1;
17173 struct Row33;
17174 ˇ
17175 struct Row4;
17176 struct Row5;
17177 struct Row6;
17178 ˇ
17179 struct Row99;
17180 struct Row9;
17181 struct Row10;"#},
17182 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
17183 indoc! {r#"struct Row;
17184 struct Row1;
17185 struct Row33;
17186 ˇ
17187 struct Row4;
17188 struct Row5;
17189 struct Row6;
17190 ˇ
17191 struct Row99;
17192 struct Row9;
17193 struct Row10;"#},
17194 base_text,
17195 &mut cx,
17196 );
17197 assert_hunk_revert(
17198 indoc! {r#"struct Row;
17199 struct Row1;
17200 struct Row33;
17201 «ˇ
17202 struct Row4;
17203 struct» Row5;
17204 «struct Row6;
17205 ˇ»
17206 struct Row99;
17207 struct Row9;
17208 struct Row10;"#},
17209 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
17210 indoc! {r#"struct Row;
17211 struct Row1;
17212 struct Row33;
17213 «ˇ
17214 struct Row4;
17215 struct» Row5;
17216 «struct Row6;
17217 ˇ»
17218 struct Row99;
17219 struct Row9;
17220 struct Row10;"#},
17221 base_text,
17222 &mut cx,
17223 );
17224
17225 assert_hunk_revert(
17226 indoc! {r#"ˇstruct Row1.1;
17227 struct Row1;
17228 «ˇstr»uct Row22;
17229
17230 struct ˇRow44;
17231 struct Row5;
17232 struct «Rˇ»ow66;ˇ
17233
17234 «struˇ»ct Row88;
17235 struct Row9;
17236 struct Row1011;ˇ"#},
17237 vec![
17238 DiffHunkStatusKind::Modified,
17239 DiffHunkStatusKind::Modified,
17240 DiffHunkStatusKind::Modified,
17241 DiffHunkStatusKind::Modified,
17242 DiffHunkStatusKind::Modified,
17243 DiffHunkStatusKind::Modified,
17244 ],
17245 indoc! {r#"struct Row;
17246 ˇstruct Row1;
17247 struct Row2;
17248 ˇ
17249 struct Row4;
17250 ˇstruct Row5;
17251 struct Row6;
17252 ˇ
17253 struct Row8;
17254 ˇstruct Row9;
17255 struct Row10;ˇ"#},
17256 base_text,
17257 &mut cx,
17258 );
17259}
17260
17261#[gpui::test]
17262async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
17263 init_test(cx, |_| {});
17264 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
17265 let base_text = indoc! {r#"
17266 one
17267
17268 two
17269 three
17270 "#};
17271
17272 cx.set_head_text(base_text);
17273 cx.set_state("\nˇ\n");
17274 cx.executor().run_until_parked();
17275 cx.update_editor(|editor, _window, cx| {
17276 editor.expand_selected_diff_hunks(cx);
17277 });
17278 cx.executor().run_until_parked();
17279 cx.update_editor(|editor, window, cx| {
17280 editor.backspace(&Default::default(), window, cx);
17281 });
17282 cx.run_until_parked();
17283 cx.assert_state_with_diff(
17284 indoc! {r#"
17285
17286 - two
17287 - threeˇ
17288 +
17289 "#}
17290 .to_string(),
17291 );
17292}
17293
17294#[gpui::test]
17295async fn test_deletion_reverts(cx: &mut TestAppContext) {
17296 init_test(cx, |_| {});
17297 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
17298 let base_text = indoc! {r#"struct Row;
17299struct Row1;
17300struct Row2;
17301
17302struct Row4;
17303struct Row5;
17304struct Row6;
17305
17306struct Row8;
17307struct Row9;
17308struct Row10;"#};
17309
17310 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
17311 assert_hunk_revert(
17312 indoc! {r#"struct Row;
17313 struct Row2;
17314
17315 ˇstruct Row4;
17316 struct Row5;
17317 struct Row6;
17318 ˇ
17319 struct Row8;
17320 struct Row10;"#},
17321 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
17322 indoc! {r#"struct Row;
17323 struct Row2;
17324
17325 ˇstruct Row4;
17326 struct Row5;
17327 struct Row6;
17328 ˇ
17329 struct Row8;
17330 struct Row10;"#},
17331 base_text,
17332 &mut cx,
17333 );
17334 assert_hunk_revert(
17335 indoc! {r#"struct Row;
17336 struct Row2;
17337
17338 «ˇstruct Row4;
17339 struct» Row5;
17340 «struct Row6;
17341 ˇ»
17342 struct Row8;
17343 struct Row10;"#},
17344 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
17345 indoc! {r#"struct Row;
17346 struct Row2;
17347
17348 «ˇstruct Row4;
17349 struct» Row5;
17350 «struct Row6;
17351 ˇ»
17352 struct Row8;
17353 struct Row10;"#},
17354 base_text,
17355 &mut cx,
17356 );
17357
17358 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
17359 assert_hunk_revert(
17360 indoc! {r#"struct Row;
17361 ˇstruct Row2;
17362
17363 struct Row4;
17364 struct Row5;
17365 struct Row6;
17366
17367 struct Row8;ˇ
17368 struct Row10;"#},
17369 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
17370 indoc! {r#"struct Row;
17371 struct Row1;
17372 ˇstruct Row2;
17373
17374 struct Row4;
17375 struct Row5;
17376 struct Row6;
17377
17378 struct Row8;ˇ
17379 struct Row9;
17380 struct Row10;"#},
17381 base_text,
17382 &mut cx,
17383 );
17384 assert_hunk_revert(
17385 indoc! {r#"struct Row;
17386 struct Row2«ˇ;
17387 struct Row4;
17388 struct» Row5;
17389 «struct Row6;
17390
17391 struct Row8;ˇ»
17392 struct Row10;"#},
17393 vec![
17394 DiffHunkStatusKind::Deleted,
17395 DiffHunkStatusKind::Deleted,
17396 DiffHunkStatusKind::Deleted,
17397 ],
17398 indoc! {r#"struct Row;
17399 struct Row1;
17400 struct Row2«ˇ;
17401
17402 struct Row4;
17403 struct» Row5;
17404 «struct Row6;
17405
17406 struct Row8;ˇ»
17407 struct Row9;
17408 struct Row10;"#},
17409 base_text,
17410 &mut cx,
17411 );
17412}
17413
17414#[gpui::test]
17415async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
17416 init_test(cx, |_| {});
17417
17418 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
17419 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
17420 let base_text_3 =
17421 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
17422
17423 let text_1 = edit_first_char_of_every_line(base_text_1);
17424 let text_2 = edit_first_char_of_every_line(base_text_2);
17425 let text_3 = edit_first_char_of_every_line(base_text_3);
17426
17427 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
17428 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
17429 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
17430
17431 let multibuffer = cx.new(|cx| {
17432 let mut multibuffer = MultiBuffer::new(ReadWrite);
17433 multibuffer.push_excerpts(
17434 buffer_1.clone(),
17435 [
17436 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17437 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17438 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17439 ],
17440 cx,
17441 );
17442 multibuffer.push_excerpts(
17443 buffer_2.clone(),
17444 [
17445 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17446 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17447 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17448 ],
17449 cx,
17450 );
17451 multibuffer.push_excerpts(
17452 buffer_3.clone(),
17453 [
17454 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17455 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17456 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17457 ],
17458 cx,
17459 );
17460 multibuffer
17461 });
17462
17463 let fs = FakeFs::new(cx.executor());
17464 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
17465 let (editor, cx) = cx
17466 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
17467 editor.update_in(cx, |editor, _window, cx| {
17468 for (buffer, diff_base) in [
17469 (buffer_1.clone(), base_text_1),
17470 (buffer_2.clone(), base_text_2),
17471 (buffer_3.clone(), base_text_3),
17472 ] {
17473 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
17474 editor
17475 .buffer
17476 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
17477 }
17478 });
17479 cx.executor().run_until_parked();
17480
17481 editor.update_in(cx, |editor, window, cx| {
17482 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}");
17483 editor.select_all(&SelectAll, window, cx);
17484 editor.git_restore(&Default::default(), window, cx);
17485 });
17486 cx.executor().run_until_parked();
17487
17488 // When all ranges are selected, all buffer hunks are reverted.
17489 editor.update(cx, |editor, cx| {
17490 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");
17491 });
17492 buffer_1.update(cx, |buffer, _| {
17493 assert_eq!(buffer.text(), base_text_1);
17494 });
17495 buffer_2.update(cx, |buffer, _| {
17496 assert_eq!(buffer.text(), base_text_2);
17497 });
17498 buffer_3.update(cx, |buffer, _| {
17499 assert_eq!(buffer.text(), base_text_3);
17500 });
17501
17502 editor.update_in(cx, |editor, window, cx| {
17503 editor.undo(&Default::default(), window, cx);
17504 });
17505
17506 editor.update_in(cx, |editor, window, cx| {
17507 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17508 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
17509 });
17510 editor.git_restore(&Default::default(), window, cx);
17511 });
17512
17513 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
17514 // but not affect buffer_2 and its related excerpts.
17515 editor.update(cx, |editor, cx| {
17516 assert_eq!(
17517 editor.text(cx),
17518 "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}"
17519 );
17520 });
17521 buffer_1.update(cx, |buffer, _| {
17522 assert_eq!(buffer.text(), base_text_1);
17523 });
17524 buffer_2.update(cx, |buffer, _| {
17525 assert_eq!(
17526 buffer.text(),
17527 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
17528 );
17529 });
17530 buffer_3.update(cx, |buffer, _| {
17531 assert_eq!(
17532 buffer.text(),
17533 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
17534 );
17535 });
17536
17537 fn edit_first_char_of_every_line(text: &str) -> String {
17538 text.split('\n')
17539 .map(|line| format!("X{}", &line[1..]))
17540 .collect::<Vec<_>>()
17541 .join("\n")
17542 }
17543}
17544
17545#[gpui::test]
17546async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
17547 init_test(cx, |_| {});
17548
17549 let cols = 4;
17550 let rows = 10;
17551 let sample_text_1 = sample_text(rows, cols, 'a');
17552 assert_eq!(
17553 sample_text_1,
17554 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
17555 );
17556 let sample_text_2 = sample_text(rows, cols, 'l');
17557 assert_eq!(
17558 sample_text_2,
17559 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
17560 );
17561 let sample_text_3 = sample_text(rows, cols, 'v');
17562 assert_eq!(
17563 sample_text_3,
17564 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
17565 );
17566
17567 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
17568 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
17569 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
17570
17571 let multi_buffer = cx.new(|cx| {
17572 let mut multibuffer = MultiBuffer::new(ReadWrite);
17573 multibuffer.push_excerpts(
17574 buffer_1.clone(),
17575 [
17576 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17577 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17578 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17579 ],
17580 cx,
17581 );
17582 multibuffer.push_excerpts(
17583 buffer_2.clone(),
17584 [
17585 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17586 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17587 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17588 ],
17589 cx,
17590 );
17591 multibuffer.push_excerpts(
17592 buffer_3.clone(),
17593 [
17594 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17595 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17596 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17597 ],
17598 cx,
17599 );
17600 multibuffer
17601 });
17602
17603 let fs = FakeFs::new(cx.executor());
17604 fs.insert_tree(
17605 "/a",
17606 json!({
17607 "main.rs": sample_text_1,
17608 "other.rs": sample_text_2,
17609 "lib.rs": sample_text_3,
17610 }),
17611 )
17612 .await;
17613 let project = Project::test(fs, ["/a".as_ref()], cx).await;
17614 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17615 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17616 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
17617 Editor::new(
17618 EditorMode::full(),
17619 multi_buffer,
17620 Some(project.clone()),
17621 window,
17622 cx,
17623 )
17624 });
17625 let multibuffer_item_id = workspace
17626 .update(cx, |workspace, window, cx| {
17627 assert!(
17628 workspace.active_item(cx).is_none(),
17629 "active item should be None before the first item is added"
17630 );
17631 workspace.add_item_to_active_pane(
17632 Box::new(multi_buffer_editor.clone()),
17633 None,
17634 true,
17635 window,
17636 cx,
17637 );
17638 let active_item = workspace
17639 .active_item(cx)
17640 .expect("should have an active item after adding the multi buffer");
17641 assert!(
17642 !active_item.is_singleton(cx),
17643 "A multi buffer was expected to active after adding"
17644 );
17645 active_item.item_id()
17646 })
17647 .unwrap();
17648 cx.executor().run_until_parked();
17649
17650 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17651 editor.change_selections(
17652 SelectionEffects::scroll(Autoscroll::Next),
17653 window,
17654 cx,
17655 |s| s.select_ranges(Some(1..2)),
17656 );
17657 editor.open_excerpts(&OpenExcerpts, window, cx);
17658 });
17659 cx.executor().run_until_parked();
17660 let first_item_id = workspace
17661 .update(cx, |workspace, window, cx| {
17662 let active_item = workspace
17663 .active_item(cx)
17664 .expect("should have an active item after navigating into the 1st buffer");
17665 let first_item_id = active_item.item_id();
17666 assert_ne!(
17667 first_item_id, multibuffer_item_id,
17668 "Should navigate into the 1st buffer and activate it"
17669 );
17670 assert!(
17671 active_item.is_singleton(cx),
17672 "New active item should be a singleton buffer"
17673 );
17674 assert_eq!(
17675 active_item
17676 .act_as::<Editor>(cx)
17677 .expect("should have navigated into an editor for the 1st buffer")
17678 .read(cx)
17679 .text(cx),
17680 sample_text_1
17681 );
17682
17683 workspace
17684 .go_back(workspace.active_pane().downgrade(), window, cx)
17685 .detach_and_log_err(cx);
17686
17687 first_item_id
17688 })
17689 .unwrap();
17690 cx.executor().run_until_parked();
17691 workspace
17692 .update(cx, |workspace, _, cx| {
17693 let active_item = workspace
17694 .active_item(cx)
17695 .expect("should have an active item after navigating back");
17696 assert_eq!(
17697 active_item.item_id(),
17698 multibuffer_item_id,
17699 "Should navigate back to the multi buffer"
17700 );
17701 assert!(!active_item.is_singleton(cx));
17702 })
17703 .unwrap();
17704
17705 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17706 editor.change_selections(
17707 SelectionEffects::scroll(Autoscroll::Next),
17708 window,
17709 cx,
17710 |s| s.select_ranges(Some(39..40)),
17711 );
17712 editor.open_excerpts(&OpenExcerpts, window, cx);
17713 });
17714 cx.executor().run_until_parked();
17715 let second_item_id = workspace
17716 .update(cx, |workspace, window, cx| {
17717 let active_item = workspace
17718 .active_item(cx)
17719 .expect("should have an active item after navigating into the 2nd buffer");
17720 let second_item_id = active_item.item_id();
17721 assert_ne!(
17722 second_item_id, multibuffer_item_id,
17723 "Should navigate away from the multibuffer"
17724 );
17725 assert_ne!(
17726 second_item_id, first_item_id,
17727 "Should navigate into the 2nd buffer and activate it"
17728 );
17729 assert!(
17730 active_item.is_singleton(cx),
17731 "New active item should be a singleton buffer"
17732 );
17733 assert_eq!(
17734 active_item
17735 .act_as::<Editor>(cx)
17736 .expect("should have navigated into an editor")
17737 .read(cx)
17738 .text(cx),
17739 sample_text_2
17740 );
17741
17742 workspace
17743 .go_back(workspace.active_pane().downgrade(), window, cx)
17744 .detach_and_log_err(cx);
17745
17746 second_item_id
17747 })
17748 .unwrap();
17749 cx.executor().run_until_parked();
17750 workspace
17751 .update(cx, |workspace, _, cx| {
17752 let active_item = workspace
17753 .active_item(cx)
17754 .expect("should have an active item after navigating back from the 2nd buffer");
17755 assert_eq!(
17756 active_item.item_id(),
17757 multibuffer_item_id,
17758 "Should navigate back from the 2nd buffer to the multi buffer"
17759 );
17760 assert!(!active_item.is_singleton(cx));
17761 })
17762 .unwrap();
17763
17764 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17765 editor.change_selections(
17766 SelectionEffects::scroll(Autoscroll::Next),
17767 window,
17768 cx,
17769 |s| s.select_ranges(Some(70..70)),
17770 );
17771 editor.open_excerpts(&OpenExcerpts, window, cx);
17772 });
17773 cx.executor().run_until_parked();
17774 workspace
17775 .update(cx, |workspace, window, cx| {
17776 let active_item = workspace
17777 .active_item(cx)
17778 .expect("should have an active item after navigating into the 3rd buffer");
17779 let third_item_id = active_item.item_id();
17780 assert_ne!(
17781 third_item_id, multibuffer_item_id,
17782 "Should navigate into the 3rd buffer and activate it"
17783 );
17784 assert_ne!(third_item_id, first_item_id);
17785 assert_ne!(third_item_id, second_item_id);
17786 assert!(
17787 active_item.is_singleton(cx),
17788 "New active item should be a singleton buffer"
17789 );
17790 assert_eq!(
17791 active_item
17792 .act_as::<Editor>(cx)
17793 .expect("should have navigated into an editor")
17794 .read(cx)
17795 .text(cx),
17796 sample_text_3
17797 );
17798
17799 workspace
17800 .go_back(workspace.active_pane().downgrade(), window, cx)
17801 .detach_and_log_err(cx);
17802 })
17803 .unwrap();
17804 cx.executor().run_until_parked();
17805 workspace
17806 .update(cx, |workspace, _, cx| {
17807 let active_item = workspace
17808 .active_item(cx)
17809 .expect("should have an active item after navigating back from the 3rd buffer");
17810 assert_eq!(
17811 active_item.item_id(),
17812 multibuffer_item_id,
17813 "Should navigate back from the 3rd buffer to the multi buffer"
17814 );
17815 assert!(!active_item.is_singleton(cx));
17816 })
17817 .unwrap();
17818}
17819
17820#[gpui::test]
17821async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17822 init_test(cx, |_| {});
17823
17824 let mut cx = EditorTestContext::new(cx).await;
17825
17826 let diff_base = r#"
17827 use some::mod;
17828
17829 const A: u32 = 42;
17830
17831 fn main() {
17832 println!("hello");
17833
17834 println!("world");
17835 }
17836 "#
17837 .unindent();
17838
17839 cx.set_state(
17840 &r#"
17841 use some::modified;
17842
17843 ˇ
17844 fn main() {
17845 println!("hello there");
17846
17847 println!("around the");
17848 println!("world");
17849 }
17850 "#
17851 .unindent(),
17852 );
17853
17854 cx.set_head_text(&diff_base);
17855 executor.run_until_parked();
17856
17857 cx.update_editor(|editor, window, cx| {
17858 editor.go_to_next_hunk(&GoToHunk, window, cx);
17859 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17860 });
17861 executor.run_until_parked();
17862 cx.assert_state_with_diff(
17863 r#"
17864 use some::modified;
17865
17866
17867 fn main() {
17868 - println!("hello");
17869 + ˇ println!("hello there");
17870
17871 println!("around the");
17872 println!("world");
17873 }
17874 "#
17875 .unindent(),
17876 );
17877
17878 cx.update_editor(|editor, window, cx| {
17879 for _ in 0..2 {
17880 editor.go_to_next_hunk(&GoToHunk, window, cx);
17881 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17882 }
17883 });
17884 executor.run_until_parked();
17885 cx.assert_state_with_diff(
17886 r#"
17887 - use some::mod;
17888 + ˇuse some::modified;
17889
17890
17891 fn main() {
17892 - println!("hello");
17893 + println!("hello there");
17894
17895 + println!("around the");
17896 println!("world");
17897 }
17898 "#
17899 .unindent(),
17900 );
17901
17902 cx.update_editor(|editor, window, cx| {
17903 editor.go_to_next_hunk(&GoToHunk, window, cx);
17904 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17905 });
17906 executor.run_until_parked();
17907 cx.assert_state_with_diff(
17908 r#"
17909 - use some::mod;
17910 + use some::modified;
17911
17912 - const A: u32 = 42;
17913 ˇ
17914 fn main() {
17915 - println!("hello");
17916 + println!("hello there");
17917
17918 + println!("around the");
17919 println!("world");
17920 }
17921 "#
17922 .unindent(),
17923 );
17924
17925 cx.update_editor(|editor, window, cx| {
17926 editor.cancel(&Cancel, window, cx);
17927 });
17928
17929 cx.assert_state_with_diff(
17930 r#"
17931 use some::modified;
17932
17933 ˇ
17934 fn main() {
17935 println!("hello there");
17936
17937 println!("around the");
17938 println!("world");
17939 }
17940 "#
17941 .unindent(),
17942 );
17943}
17944
17945#[gpui::test]
17946async fn test_diff_base_change_with_expanded_diff_hunks(
17947 executor: BackgroundExecutor,
17948 cx: &mut TestAppContext,
17949) {
17950 init_test(cx, |_| {});
17951
17952 let mut cx = EditorTestContext::new(cx).await;
17953
17954 let diff_base = r#"
17955 use some::mod1;
17956 use some::mod2;
17957
17958 const A: u32 = 42;
17959 const B: u32 = 42;
17960 const C: u32 = 42;
17961
17962 fn main() {
17963 println!("hello");
17964
17965 println!("world");
17966 }
17967 "#
17968 .unindent();
17969
17970 cx.set_state(
17971 &r#"
17972 use some::mod2;
17973
17974 const A: u32 = 42;
17975 const C: u32 = 42;
17976
17977 fn main(ˇ) {
17978 //println!("hello");
17979
17980 println!("world");
17981 //
17982 //
17983 }
17984 "#
17985 .unindent(),
17986 );
17987
17988 cx.set_head_text(&diff_base);
17989 executor.run_until_parked();
17990
17991 cx.update_editor(|editor, window, cx| {
17992 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17993 });
17994 executor.run_until_parked();
17995 cx.assert_state_with_diff(
17996 r#"
17997 - use some::mod1;
17998 use some::mod2;
17999
18000 const A: u32 = 42;
18001 - const B: u32 = 42;
18002 const C: u32 = 42;
18003
18004 fn main(ˇ) {
18005 - println!("hello");
18006 + //println!("hello");
18007
18008 println!("world");
18009 + //
18010 + //
18011 }
18012 "#
18013 .unindent(),
18014 );
18015
18016 cx.set_head_text("new diff base!");
18017 executor.run_until_parked();
18018 cx.assert_state_with_diff(
18019 r#"
18020 - new diff base!
18021 + use some::mod2;
18022 +
18023 + const A: u32 = 42;
18024 + const C: u32 = 42;
18025 +
18026 + fn main(ˇ) {
18027 + //println!("hello");
18028 +
18029 + println!("world");
18030 + //
18031 + //
18032 + }
18033 "#
18034 .unindent(),
18035 );
18036}
18037
18038#[gpui::test]
18039async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
18040 init_test(cx, |_| {});
18041
18042 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
18043 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
18044 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
18045 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
18046 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
18047 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
18048
18049 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
18050 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
18051 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
18052
18053 let multi_buffer = cx.new(|cx| {
18054 let mut multibuffer = MultiBuffer::new(ReadWrite);
18055 multibuffer.push_excerpts(
18056 buffer_1.clone(),
18057 [
18058 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18059 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18060 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
18061 ],
18062 cx,
18063 );
18064 multibuffer.push_excerpts(
18065 buffer_2.clone(),
18066 [
18067 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18068 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18069 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
18070 ],
18071 cx,
18072 );
18073 multibuffer.push_excerpts(
18074 buffer_3.clone(),
18075 [
18076 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18077 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18078 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
18079 ],
18080 cx,
18081 );
18082 multibuffer
18083 });
18084
18085 let editor =
18086 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
18087 editor
18088 .update(cx, |editor, _window, cx| {
18089 for (buffer, diff_base) in [
18090 (buffer_1.clone(), file_1_old),
18091 (buffer_2.clone(), file_2_old),
18092 (buffer_3.clone(), file_3_old),
18093 ] {
18094 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
18095 editor
18096 .buffer
18097 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
18098 }
18099 })
18100 .unwrap();
18101
18102 let mut cx = EditorTestContext::for_editor(editor, cx).await;
18103 cx.run_until_parked();
18104
18105 cx.assert_editor_state(
18106 &"
18107 ˇaaa
18108 ccc
18109 ddd
18110
18111 ggg
18112 hhh
18113
18114
18115 lll
18116 mmm
18117 NNN
18118
18119 qqq
18120 rrr
18121
18122 uuu
18123 111
18124 222
18125 333
18126
18127 666
18128 777
18129
18130 000
18131 !!!"
18132 .unindent(),
18133 );
18134
18135 cx.update_editor(|editor, window, cx| {
18136 editor.select_all(&SelectAll, window, cx);
18137 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
18138 });
18139 cx.executor().run_until_parked();
18140
18141 cx.assert_state_with_diff(
18142 "
18143 «aaa
18144 - bbb
18145 ccc
18146 ddd
18147
18148 ggg
18149 hhh
18150
18151
18152 lll
18153 mmm
18154 - nnn
18155 + NNN
18156
18157 qqq
18158 rrr
18159
18160 uuu
18161 111
18162 222
18163 333
18164
18165 + 666
18166 777
18167
18168 000
18169 !!!ˇ»"
18170 .unindent(),
18171 );
18172}
18173
18174#[gpui::test]
18175async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
18176 init_test(cx, |_| {});
18177
18178 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
18179 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
18180
18181 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
18182 let multi_buffer = cx.new(|cx| {
18183 let mut multibuffer = MultiBuffer::new(ReadWrite);
18184 multibuffer.push_excerpts(
18185 buffer.clone(),
18186 [
18187 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
18188 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
18189 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
18190 ],
18191 cx,
18192 );
18193 multibuffer
18194 });
18195
18196 let editor =
18197 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
18198 editor
18199 .update(cx, |editor, _window, cx| {
18200 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
18201 editor
18202 .buffer
18203 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
18204 })
18205 .unwrap();
18206
18207 let mut cx = EditorTestContext::for_editor(editor, cx).await;
18208 cx.run_until_parked();
18209
18210 cx.update_editor(|editor, window, cx| {
18211 editor.expand_all_diff_hunks(&Default::default(), window, cx)
18212 });
18213 cx.executor().run_until_parked();
18214
18215 // When the start of a hunk coincides with the start of its excerpt,
18216 // the hunk is expanded. When the start of a a hunk is earlier than
18217 // the start of its excerpt, the hunk is not expanded.
18218 cx.assert_state_with_diff(
18219 "
18220 ˇaaa
18221 - bbb
18222 + BBB
18223
18224 - ddd
18225 - eee
18226 + DDD
18227 + EEE
18228 fff
18229
18230 iii
18231 "
18232 .unindent(),
18233 );
18234}
18235
18236#[gpui::test]
18237async fn test_edits_around_expanded_insertion_hunks(
18238 executor: BackgroundExecutor,
18239 cx: &mut TestAppContext,
18240) {
18241 init_test(cx, |_| {});
18242
18243 let mut cx = EditorTestContext::new(cx).await;
18244
18245 let diff_base = r#"
18246 use some::mod1;
18247 use some::mod2;
18248
18249 const A: u32 = 42;
18250
18251 fn main() {
18252 println!("hello");
18253
18254 println!("world");
18255 }
18256 "#
18257 .unindent();
18258 executor.run_until_parked();
18259 cx.set_state(
18260 &r#"
18261 use some::mod1;
18262 use some::mod2;
18263
18264 const A: u32 = 42;
18265 const B: u32 = 42;
18266 const C: u32 = 42;
18267 ˇ
18268
18269 fn main() {
18270 println!("hello");
18271
18272 println!("world");
18273 }
18274 "#
18275 .unindent(),
18276 );
18277
18278 cx.set_head_text(&diff_base);
18279 executor.run_until_parked();
18280
18281 cx.update_editor(|editor, window, cx| {
18282 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18283 });
18284 executor.run_until_parked();
18285
18286 cx.assert_state_with_diff(
18287 r#"
18288 use some::mod1;
18289 use some::mod2;
18290
18291 const A: u32 = 42;
18292 + const B: u32 = 42;
18293 + const C: u32 = 42;
18294 + ˇ
18295
18296 fn main() {
18297 println!("hello");
18298
18299 println!("world");
18300 }
18301 "#
18302 .unindent(),
18303 );
18304
18305 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
18306 executor.run_until_parked();
18307
18308 cx.assert_state_with_diff(
18309 r#"
18310 use some::mod1;
18311 use some::mod2;
18312
18313 const A: u32 = 42;
18314 + const B: u32 = 42;
18315 + const C: u32 = 42;
18316 + const D: u32 = 42;
18317 + ˇ
18318
18319 fn main() {
18320 println!("hello");
18321
18322 println!("world");
18323 }
18324 "#
18325 .unindent(),
18326 );
18327
18328 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
18329 executor.run_until_parked();
18330
18331 cx.assert_state_with_diff(
18332 r#"
18333 use some::mod1;
18334 use some::mod2;
18335
18336 const A: u32 = 42;
18337 + const B: u32 = 42;
18338 + const C: u32 = 42;
18339 + const D: u32 = 42;
18340 + const E: u32 = 42;
18341 + ˇ
18342
18343 fn main() {
18344 println!("hello");
18345
18346 println!("world");
18347 }
18348 "#
18349 .unindent(),
18350 );
18351
18352 cx.update_editor(|editor, window, cx| {
18353 editor.delete_line(&DeleteLine, window, cx);
18354 });
18355 executor.run_until_parked();
18356
18357 cx.assert_state_with_diff(
18358 r#"
18359 use some::mod1;
18360 use some::mod2;
18361
18362 const A: u32 = 42;
18363 + const B: u32 = 42;
18364 + const C: u32 = 42;
18365 + const D: u32 = 42;
18366 + const E: u32 = 42;
18367 ˇ
18368 fn main() {
18369 println!("hello");
18370
18371 println!("world");
18372 }
18373 "#
18374 .unindent(),
18375 );
18376
18377 cx.update_editor(|editor, window, cx| {
18378 editor.move_up(&MoveUp, window, cx);
18379 editor.delete_line(&DeleteLine, window, cx);
18380 editor.move_up(&MoveUp, window, cx);
18381 editor.delete_line(&DeleteLine, window, cx);
18382 editor.move_up(&MoveUp, window, cx);
18383 editor.delete_line(&DeleteLine, window, cx);
18384 });
18385 executor.run_until_parked();
18386 cx.assert_state_with_diff(
18387 r#"
18388 use some::mod1;
18389 use some::mod2;
18390
18391 const A: u32 = 42;
18392 + const B: u32 = 42;
18393 ˇ
18394 fn main() {
18395 println!("hello");
18396
18397 println!("world");
18398 }
18399 "#
18400 .unindent(),
18401 );
18402
18403 cx.update_editor(|editor, window, cx| {
18404 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
18405 editor.delete_line(&DeleteLine, window, cx);
18406 });
18407 executor.run_until_parked();
18408 cx.assert_state_with_diff(
18409 r#"
18410 ˇ
18411 fn main() {
18412 println!("hello");
18413
18414 println!("world");
18415 }
18416 "#
18417 .unindent(),
18418 );
18419}
18420
18421#[gpui::test]
18422async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
18423 init_test(cx, |_| {});
18424
18425 let mut cx = EditorTestContext::new(cx).await;
18426 cx.set_head_text(indoc! { "
18427 one
18428 two
18429 three
18430 four
18431 five
18432 "
18433 });
18434 cx.set_state(indoc! { "
18435 one
18436 ˇthree
18437 five
18438 "});
18439 cx.run_until_parked();
18440 cx.update_editor(|editor, window, cx| {
18441 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
18442 });
18443 cx.assert_state_with_diff(
18444 indoc! { "
18445 one
18446 - two
18447 ˇthree
18448 - four
18449 five
18450 "}
18451 .to_string(),
18452 );
18453 cx.update_editor(|editor, window, cx| {
18454 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
18455 });
18456
18457 cx.assert_state_with_diff(
18458 indoc! { "
18459 one
18460 ˇthree
18461 five
18462 "}
18463 .to_string(),
18464 );
18465
18466 cx.set_state(indoc! { "
18467 one
18468 ˇTWO
18469 three
18470 four
18471 five
18472 "});
18473 cx.run_until_parked();
18474 cx.update_editor(|editor, window, cx| {
18475 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
18476 });
18477
18478 cx.assert_state_with_diff(
18479 indoc! { "
18480 one
18481 - two
18482 + ˇTWO
18483 three
18484 four
18485 five
18486 "}
18487 .to_string(),
18488 );
18489 cx.update_editor(|editor, window, cx| {
18490 editor.move_up(&Default::default(), window, cx);
18491 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
18492 });
18493 cx.assert_state_with_diff(
18494 indoc! { "
18495 one
18496 ˇTWO
18497 three
18498 four
18499 five
18500 "}
18501 .to_string(),
18502 );
18503}
18504
18505#[gpui::test]
18506async fn test_edits_around_expanded_deletion_hunks(
18507 executor: BackgroundExecutor,
18508 cx: &mut TestAppContext,
18509) {
18510 init_test(cx, |_| {});
18511
18512 let mut cx = EditorTestContext::new(cx).await;
18513
18514 let diff_base = r#"
18515 use some::mod1;
18516 use some::mod2;
18517
18518 const A: u32 = 42;
18519 const B: u32 = 42;
18520 const C: u32 = 42;
18521
18522
18523 fn main() {
18524 println!("hello");
18525
18526 println!("world");
18527 }
18528 "#
18529 .unindent();
18530 executor.run_until_parked();
18531 cx.set_state(
18532 &r#"
18533 use some::mod1;
18534 use some::mod2;
18535
18536 ˇconst B: u32 = 42;
18537 const C: u32 = 42;
18538
18539
18540 fn main() {
18541 println!("hello");
18542
18543 println!("world");
18544 }
18545 "#
18546 .unindent(),
18547 );
18548
18549 cx.set_head_text(&diff_base);
18550 executor.run_until_parked();
18551
18552 cx.update_editor(|editor, window, cx| {
18553 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18554 });
18555 executor.run_until_parked();
18556
18557 cx.assert_state_with_diff(
18558 r#"
18559 use some::mod1;
18560 use some::mod2;
18561
18562 - const A: u32 = 42;
18563 ˇconst B: u32 = 42;
18564 const C: u32 = 42;
18565
18566
18567 fn main() {
18568 println!("hello");
18569
18570 println!("world");
18571 }
18572 "#
18573 .unindent(),
18574 );
18575
18576 cx.update_editor(|editor, window, cx| {
18577 editor.delete_line(&DeleteLine, window, cx);
18578 });
18579 executor.run_until_parked();
18580 cx.assert_state_with_diff(
18581 r#"
18582 use some::mod1;
18583 use some::mod2;
18584
18585 - const A: u32 = 42;
18586 - const B: u32 = 42;
18587 ˇconst C: u32 = 42;
18588
18589
18590 fn main() {
18591 println!("hello");
18592
18593 println!("world");
18594 }
18595 "#
18596 .unindent(),
18597 );
18598
18599 cx.update_editor(|editor, window, cx| {
18600 editor.delete_line(&DeleteLine, window, cx);
18601 });
18602 executor.run_until_parked();
18603 cx.assert_state_with_diff(
18604 r#"
18605 use some::mod1;
18606 use some::mod2;
18607
18608 - const A: u32 = 42;
18609 - const B: u32 = 42;
18610 - const C: u32 = 42;
18611 ˇ
18612
18613 fn main() {
18614 println!("hello");
18615
18616 println!("world");
18617 }
18618 "#
18619 .unindent(),
18620 );
18621
18622 cx.update_editor(|editor, window, cx| {
18623 editor.handle_input("replacement", window, cx);
18624 });
18625 executor.run_until_parked();
18626 cx.assert_state_with_diff(
18627 r#"
18628 use some::mod1;
18629 use some::mod2;
18630
18631 - const A: u32 = 42;
18632 - const B: u32 = 42;
18633 - const C: u32 = 42;
18634 -
18635 + replacementˇ
18636
18637 fn main() {
18638 println!("hello");
18639
18640 println!("world");
18641 }
18642 "#
18643 .unindent(),
18644 );
18645}
18646
18647#[gpui::test]
18648async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
18649 init_test(cx, |_| {});
18650
18651 let mut cx = EditorTestContext::new(cx).await;
18652
18653 let base_text = r#"
18654 one
18655 two
18656 three
18657 four
18658 five
18659 "#
18660 .unindent();
18661 executor.run_until_parked();
18662 cx.set_state(
18663 &r#"
18664 one
18665 two
18666 fˇour
18667 five
18668 "#
18669 .unindent(),
18670 );
18671
18672 cx.set_head_text(&base_text);
18673 executor.run_until_parked();
18674
18675 cx.update_editor(|editor, window, cx| {
18676 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18677 });
18678 executor.run_until_parked();
18679
18680 cx.assert_state_with_diff(
18681 r#"
18682 one
18683 two
18684 - three
18685 fˇour
18686 five
18687 "#
18688 .unindent(),
18689 );
18690
18691 cx.update_editor(|editor, window, cx| {
18692 editor.backspace(&Backspace, window, cx);
18693 editor.backspace(&Backspace, window, cx);
18694 });
18695 executor.run_until_parked();
18696 cx.assert_state_with_diff(
18697 r#"
18698 one
18699 two
18700 - threeˇ
18701 - four
18702 + our
18703 five
18704 "#
18705 .unindent(),
18706 );
18707}
18708
18709#[gpui::test]
18710async fn test_edit_after_expanded_modification_hunk(
18711 executor: BackgroundExecutor,
18712 cx: &mut TestAppContext,
18713) {
18714 init_test(cx, |_| {});
18715
18716 let mut cx = EditorTestContext::new(cx).await;
18717
18718 let diff_base = r#"
18719 use some::mod1;
18720 use some::mod2;
18721
18722 const A: u32 = 42;
18723 const B: u32 = 42;
18724 const C: u32 = 42;
18725 const D: u32 = 42;
18726
18727
18728 fn main() {
18729 println!("hello");
18730
18731 println!("world");
18732 }"#
18733 .unindent();
18734
18735 cx.set_state(
18736 &r#"
18737 use some::mod1;
18738 use some::mod2;
18739
18740 const A: u32 = 42;
18741 const B: u32 = 42;
18742 const C: u32 = 43ˇ
18743 const D: u32 = 42;
18744
18745
18746 fn main() {
18747 println!("hello");
18748
18749 println!("world");
18750 }"#
18751 .unindent(),
18752 );
18753
18754 cx.set_head_text(&diff_base);
18755 executor.run_until_parked();
18756 cx.update_editor(|editor, window, cx| {
18757 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18758 });
18759 executor.run_until_parked();
18760
18761 cx.assert_state_with_diff(
18762 r#"
18763 use some::mod1;
18764 use some::mod2;
18765
18766 const A: u32 = 42;
18767 const B: u32 = 42;
18768 - const C: u32 = 42;
18769 + const C: u32 = 43ˇ
18770 const D: u32 = 42;
18771
18772
18773 fn main() {
18774 println!("hello");
18775
18776 println!("world");
18777 }"#
18778 .unindent(),
18779 );
18780
18781 cx.update_editor(|editor, window, cx| {
18782 editor.handle_input("\nnew_line\n", window, cx);
18783 });
18784 executor.run_until_parked();
18785
18786 cx.assert_state_with_diff(
18787 r#"
18788 use some::mod1;
18789 use some::mod2;
18790
18791 const A: u32 = 42;
18792 const B: u32 = 42;
18793 - const C: u32 = 42;
18794 + const C: u32 = 43
18795 + new_line
18796 + ˇ
18797 const D: u32 = 42;
18798
18799
18800 fn main() {
18801 println!("hello");
18802
18803 println!("world");
18804 }"#
18805 .unindent(),
18806 );
18807}
18808
18809#[gpui::test]
18810async fn test_stage_and_unstage_added_file_hunk(
18811 executor: BackgroundExecutor,
18812 cx: &mut TestAppContext,
18813) {
18814 init_test(cx, |_| {});
18815
18816 let mut cx = EditorTestContext::new(cx).await;
18817 cx.update_editor(|editor, _, cx| {
18818 editor.set_expand_all_diff_hunks(cx);
18819 });
18820
18821 let working_copy = r#"
18822 ˇfn main() {
18823 println!("hello, world!");
18824 }
18825 "#
18826 .unindent();
18827
18828 cx.set_state(&working_copy);
18829 executor.run_until_parked();
18830
18831 cx.assert_state_with_diff(
18832 r#"
18833 + ˇfn main() {
18834 + println!("hello, world!");
18835 + }
18836 "#
18837 .unindent(),
18838 );
18839 cx.assert_index_text(None);
18840
18841 cx.update_editor(|editor, window, cx| {
18842 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
18843 });
18844 executor.run_until_parked();
18845 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
18846 cx.assert_state_with_diff(
18847 r#"
18848 + ˇfn main() {
18849 + println!("hello, world!");
18850 + }
18851 "#
18852 .unindent(),
18853 );
18854
18855 cx.update_editor(|editor, window, cx| {
18856 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
18857 });
18858 executor.run_until_parked();
18859 cx.assert_index_text(None);
18860}
18861
18862async fn setup_indent_guides_editor(
18863 text: &str,
18864 cx: &mut TestAppContext,
18865) -> (BufferId, EditorTestContext) {
18866 init_test(cx, |_| {});
18867
18868 let mut cx = EditorTestContext::new(cx).await;
18869
18870 let buffer_id = cx.update_editor(|editor, window, cx| {
18871 editor.set_text(text, window, cx);
18872 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
18873
18874 buffer_ids[0]
18875 });
18876
18877 (buffer_id, cx)
18878}
18879
18880fn assert_indent_guides(
18881 range: Range<u32>,
18882 expected: Vec<IndentGuide>,
18883 active_indices: Option<Vec<usize>>,
18884 cx: &mut EditorTestContext,
18885) {
18886 let indent_guides = cx.update_editor(|editor, window, cx| {
18887 let snapshot = editor.snapshot(window, cx).display_snapshot;
18888 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
18889 editor,
18890 MultiBufferRow(range.start)..MultiBufferRow(range.end),
18891 true,
18892 &snapshot,
18893 cx,
18894 );
18895
18896 indent_guides.sort_by(|a, b| {
18897 a.depth.cmp(&b.depth).then(
18898 a.start_row
18899 .cmp(&b.start_row)
18900 .then(a.end_row.cmp(&b.end_row)),
18901 )
18902 });
18903 indent_guides
18904 });
18905
18906 if let Some(expected) = active_indices {
18907 let active_indices = cx.update_editor(|editor, window, cx| {
18908 let snapshot = editor.snapshot(window, cx).display_snapshot;
18909 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
18910 });
18911
18912 assert_eq!(
18913 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
18914 expected,
18915 "Active indent guide indices do not match"
18916 );
18917 }
18918
18919 assert_eq!(indent_guides, expected, "Indent guides do not match");
18920}
18921
18922fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
18923 IndentGuide {
18924 buffer_id,
18925 start_row: MultiBufferRow(start_row),
18926 end_row: MultiBufferRow(end_row),
18927 depth,
18928 tab_size: 4,
18929 settings: IndentGuideSettings {
18930 enabled: true,
18931 line_width: 1,
18932 active_line_width: 1,
18933 ..Default::default()
18934 },
18935 }
18936}
18937
18938#[gpui::test]
18939async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
18940 let (buffer_id, mut cx) = setup_indent_guides_editor(
18941 &"
18942 fn main() {
18943 let a = 1;
18944 }"
18945 .unindent(),
18946 cx,
18947 )
18948 .await;
18949
18950 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
18951}
18952
18953#[gpui::test]
18954async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
18955 let (buffer_id, mut cx) = setup_indent_guides_editor(
18956 &"
18957 fn main() {
18958 let a = 1;
18959 let b = 2;
18960 }"
18961 .unindent(),
18962 cx,
18963 )
18964 .await;
18965
18966 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
18967}
18968
18969#[gpui::test]
18970async fn test_indent_guide_nested(cx: &mut TestAppContext) {
18971 let (buffer_id, mut cx) = setup_indent_guides_editor(
18972 &"
18973 fn main() {
18974 let a = 1;
18975 if a == 3 {
18976 let b = 2;
18977 } else {
18978 let c = 3;
18979 }
18980 }"
18981 .unindent(),
18982 cx,
18983 )
18984 .await;
18985
18986 assert_indent_guides(
18987 0..8,
18988 vec![
18989 indent_guide(buffer_id, 1, 6, 0),
18990 indent_guide(buffer_id, 3, 3, 1),
18991 indent_guide(buffer_id, 5, 5, 1),
18992 ],
18993 None,
18994 &mut cx,
18995 );
18996}
18997
18998#[gpui::test]
18999async fn test_indent_guide_tab(cx: &mut TestAppContext) {
19000 let (buffer_id, mut cx) = setup_indent_guides_editor(
19001 &"
19002 fn main() {
19003 let a = 1;
19004 let b = 2;
19005 let c = 3;
19006 }"
19007 .unindent(),
19008 cx,
19009 )
19010 .await;
19011
19012 assert_indent_guides(
19013 0..5,
19014 vec![
19015 indent_guide(buffer_id, 1, 3, 0),
19016 indent_guide(buffer_id, 2, 2, 1),
19017 ],
19018 None,
19019 &mut cx,
19020 );
19021}
19022
19023#[gpui::test]
19024async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
19025 let (buffer_id, mut cx) = setup_indent_guides_editor(
19026 &"
19027 fn main() {
19028 let a = 1;
19029
19030 let c = 3;
19031 }"
19032 .unindent(),
19033 cx,
19034 )
19035 .await;
19036
19037 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
19038}
19039
19040#[gpui::test]
19041async fn test_indent_guide_complex(cx: &mut TestAppContext) {
19042 let (buffer_id, mut cx) = setup_indent_guides_editor(
19043 &"
19044 fn main() {
19045 let a = 1;
19046
19047 let c = 3;
19048
19049 if a == 3 {
19050 let b = 2;
19051 } else {
19052 let c = 3;
19053 }
19054 }"
19055 .unindent(),
19056 cx,
19057 )
19058 .await;
19059
19060 assert_indent_guides(
19061 0..11,
19062 vec![
19063 indent_guide(buffer_id, 1, 9, 0),
19064 indent_guide(buffer_id, 6, 6, 1),
19065 indent_guide(buffer_id, 8, 8, 1),
19066 ],
19067 None,
19068 &mut cx,
19069 );
19070}
19071
19072#[gpui::test]
19073async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
19074 let (buffer_id, mut cx) = setup_indent_guides_editor(
19075 &"
19076 fn main() {
19077 let a = 1;
19078
19079 let c = 3;
19080
19081 if a == 3 {
19082 let b = 2;
19083 } else {
19084 let c = 3;
19085 }
19086 }"
19087 .unindent(),
19088 cx,
19089 )
19090 .await;
19091
19092 assert_indent_guides(
19093 1..11,
19094 vec![
19095 indent_guide(buffer_id, 1, 9, 0),
19096 indent_guide(buffer_id, 6, 6, 1),
19097 indent_guide(buffer_id, 8, 8, 1),
19098 ],
19099 None,
19100 &mut cx,
19101 );
19102}
19103
19104#[gpui::test]
19105async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
19106 let (buffer_id, mut cx) = setup_indent_guides_editor(
19107 &"
19108 fn main() {
19109 let a = 1;
19110
19111 let c = 3;
19112
19113 if a == 3 {
19114 let b = 2;
19115 } else {
19116 let c = 3;
19117 }
19118 }"
19119 .unindent(),
19120 cx,
19121 )
19122 .await;
19123
19124 assert_indent_guides(
19125 1..10,
19126 vec![
19127 indent_guide(buffer_id, 1, 9, 0),
19128 indent_guide(buffer_id, 6, 6, 1),
19129 indent_guide(buffer_id, 8, 8, 1),
19130 ],
19131 None,
19132 &mut cx,
19133 );
19134}
19135
19136#[gpui::test]
19137async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
19138 let (buffer_id, mut cx) = setup_indent_guides_editor(
19139 &"
19140 fn main() {
19141 if a {
19142 b(
19143 c,
19144 d,
19145 )
19146 } else {
19147 e(
19148 f
19149 )
19150 }
19151 }"
19152 .unindent(),
19153 cx,
19154 )
19155 .await;
19156
19157 assert_indent_guides(
19158 0..11,
19159 vec![
19160 indent_guide(buffer_id, 1, 10, 0),
19161 indent_guide(buffer_id, 2, 5, 1),
19162 indent_guide(buffer_id, 7, 9, 1),
19163 indent_guide(buffer_id, 3, 4, 2),
19164 indent_guide(buffer_id, 8, 8, 2),
19165 ],
19166 None,
19167 &mut cx,
19168 );
19169
19170 cx.update_editor(|editor, window, cx| {
19171 editor.fold_at(MultiBufferRow(2), window, cx);
19172 assert_eq!(
19173 editor.display_text(cx),
19174 "
19175 fn main() {
19176 if a {
19177 b(⋯
19178 )
19179 } else {
19180 e(
19181 f
19182 )
19183 }
19184 }"
19185 .unindent()
19186 );
19187 });
19188
19189 assert_indent_guides(
19190 0..11,
19191 vec![
19192 indent_guide(buffer_id, 1, 10, 0),
19193 indent_guide(buffer_id, 2, 5, 1),
19194 indent_guide(buffer_id, 7, 9, 1),
19195 indent_guide(buffer_id, 8, 8, 2),
19196 ],
19197 None,
19198 &mut cx,
19199 );
19200}
19201
19202#[gpui::test]
19203async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
19204 let (buffer_id, mut cx) = setup_indent_guides_editor(
19205 &"
19206 block1
19207 block2
19208 block3
19209 block4
19210 block2
19211 block1
19212 block1"
19213 .unindent(),
19214 cx,
19215 )
19216 .await;
19217
19218 assert_indent_guides(
19219 1..10,
19220 vec![
19221 indent_guide(buffer_id, 1, 4, 0),
19222 indent_guide(buffer_id, 2, 3, 1),
19223 indent_guide(buffer_id, 3, 3, 2),
19224 ],
19225 None,
19226 &mut cx,
19227 );
19228}
19229
19230#[gpui::test]
19231async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
19232 let (buffer_id, mut cx) = setup_indent_guides_editor(
19233 &"
19234 block1
19235 block2
19236 block3
19237
19238 block1
19239 block1"
19240 .unindent(),
19241 cx,
19242 )
19243 .await;
19244
19245 assert_indent_guides(
19246 0..6,
19247 vec![
19248 indent_guide(buffer_id, 1, 2, 0),
19249 indent_guide(buffer_id, 2, 2, 1),
19250 ],
19251 None,
19252 &mut cx,
19253 );
19254}
19255
19256#[gpui::test]
19257async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
19258 let (buffer_id, mut cx) = setup_indent_guides_editor(
19259 &"
19260 function component() {
19261 \treturn (
19262 \t\t\t
19263 \t\t<div>
19264 \t\t\t<abc></abc>
19265 \t\t</div>
19266 \t)
19267 }"
19268 .unindent(),
19269 cx,
19270 )
19271 .await;
19272
19273 assert_indent_guides(
19274 0..8,
19275 vec![
19276 indent_guide(buffer_id, 1, 6, 0),
19277 indent_guide(buffer_id, 2, 5, 1),
19278 indent_guide(buffer_id, 4, 4, 2),
19279 ],
19280 None,
19281 &mut cx,
19282 );
19283}
19284
19285#[gpui::test]
19286async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
19287 let (buffer_id, mut cx) = setup_indent_guides_editor(
19288 &"
19289 function component() {
19290 \treturn (
19291 \t
19292 \t\t<div>
19293 \t\t\t<abc></abc>
19294 \t\t</div>
19295 \t)
19296 }"
19297 .unindent(),
19298 cx,
19299 )
19300 .await;
19301
19302 assert_indent_guides(
19303 0..8,
19304 vec![
19305 indent_guide(buffer_id, 1, 6, 0),
19306 indent_guide(buffer_id, 2, 5, 1),
19307 indent_guide(buffer_id, 4, 4, 2),
19308 ],
19309 None,
19310 &mut cx,
19311 );
19312}
19313
19314#[gpui::test]
19315async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
19316 let (buffer_id, mut cx) = setup_indent_guides_editor(
19317 &"
19318 block1
19319
19320
19321
19322 block2
19323 "
19324 .unindent(),
19325 cx,
19326 )
19327 .await;
19328
19329 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
19330}
19331
19332#[gpui::test]
19333async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
19334 let (buffer_id, mut cx) = setup_indent_guides_editor(
19335 &"
19336 def a:
19337 \tb = 3
19338 \tif True:
19339 \t\tc = 4
19340 \t\td = 5
19341 \tprint(b)
19342 "
19343 .unindent(),
19344 cx,
19345 )
19346 .await;
19347
19348 assert_indent_guides(
19349 0..6,
19350 vec![
19351 indent_guide(buffer_id, 1, 5, 0),
19352 indent_guide(buffer_id, 3, 4, 1),
19353 ],
19354 None,
19355 &mut cx,
19356 );
19357}
19358
19359#[gpui::test]
19360async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
19361 let (buffer_id, mut cx) = setup_indent_guides_editor(
19362 &"
19363 fn main() {
19364 let a = 1;
19365 }"
19366 .unindent(),
19367 cx,
19368 )
19369 .await;
19370
19371 cx.update_editor(|editor, window, cx| {
19372 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19373 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
19374 });
19375 });
19376
19377 assert_indent_guides(
19378 0..3,
19379 vec![indent_guide(buffer_id, 1, 1, 0)],
19380 Some(vec![0]),
19381 &mut cx,
19382 );
19383}
19384
19385#[gpui::test]
19386async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
19387 let (buffer_id, mut cx) = setup_indent_guides_editor(
19388 &"
19389 fn main() {
19390 if 1 == 2 {
19391 let a = 1;
19392 }
19393 }"
19394 .unindent(),
19395 cx,
19396 )
19397 .await;
19398
19399 cx.update_editor(|editor, window, cx| {
19400 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19401 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
19402 });
19403 });
19404
19405 assert_indent_guides(
19406 0..4,
19407 vec![
19408 indent_guide(buffer_id, 1, 3, 0),
19409 indent_guide(buffer_id, 2, 2, 1),
19410 ],
19411 Some(vec![1]),
19412 &mut cx,
19413 );
19414
19415 cx.update_editor(|editor, window, cx| {
19416 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19417 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
19418 });
19419 });
19420
19421 assert_indent_guides(
19422 0..4,
19423 vec![
19424 indent_guide(buffer_id, 1, 3, 0),
19425 indent_guide(buffer_id, 2, 2, 1),
19426 ],
19427 Some(vec![1]),
19428 &mut cx,
19429 );
19430
19431 cx.update_editor(|editor, window, cx| {
19432 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19433 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
19434 });
19435 });
19436
19437 assert_indent_guides(
19438 0..4,
19439 vec![
19440 indent_guide(buffer_id, 1, 3, 0),
19441 indent_guide(buffer_id, 2, 2, 1),
19442 ],
19443 Some(vec![0]),
19444 &mut cx,
19445 );
19446}
19447
19448#[gpui::test]
19449async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
19450 let (buffer_id, mut cx) = setup_indent_guides_editor(
19451 &"
19452 fn main() {
19453 let a = 1;
19454
19455 let b = 2;
19456 }"
19457 .unindent(),
19458 cx,
19459 )
19460 .await;
19461
19462 cx.update_editor(|editor, window, cx| {
19463 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19464 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
19465 });
19466 });
19467
19468 assert_indent_guides(
19469 0..5,
19470 vec![indent_guide(buffer_id, 1, 3, 0)],
19471 Some(vec![0]),
19472 &mut cx,
19473 );
19474}
19475
19476#[gpui::test]
19477async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
19478 let (buffer_id, mut cx) = setup_indent_guides_editor(
19479 &"
19480 def m:
19481 a = 1
19482 pass"
19483 .unindent(),
19484 cx,
19485 )
19486 .await;
19487
19488 cx.update_editor(|editor, window, cx| {
19489 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19490 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
19491 });
19492 });
19493
19494 assert_indent_guides(
19495 0..3,
19496 vec![indent_guide(buffer_id, 1, 2, 0)],
19497 Some(vec![0]),
19498 &mut cx,
19499 );
19500}
19501
19502#[gpui::test]
19503async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
19504 init_test(cx, |_| {});
19505 let mut cx = EditorTestContext::new(cx).await;
19506 let text = indoc! {
19507 "
19508 impl A {
19509 fn b() {
19510 0;
19511 3;
19512 5;
19513 6;
19514 7;
19515 }
19516 }
19517 "
19518 };
19519 let base_text = indoc! {
19520 "
19521 impl A {
19522 fn b() {
19523 0;
19524 1;
19525 2;
19526 3;
19527 4;
19528 }
19529 fn c() {
19530 5;
19531 6;
19532 7;
19533 }
19534 }
19535 "
19536 };
19537
19538 cx.update_editor(|editor, window, cx| {
19539 editor.set_text(text, window, cx);
19540
19541 editor.buffer().update(cx, |multibuffer, cx| {
19542 let buffer = multibuffer.as_singleton().unwrap();
19543 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
19544
19545 multibuffer.set_all_diff_hunks_expanded(cx);
19546 multibuffer.add_diff(diff, cx);
19547
19548 buffer.read(cx).remote_id()
19549 })
19550 });
19551 cx.run_until_parked();
19552
19553 cx.assert_state_with_diff(
19554 indoc! { "
19555 impl A {
19556 fn b() {
19557 0;
19558 - 1;
19559 - 2;
19560 3;
19561 - 4;
19562 - }
19563 - fn c() {
19564 5;
19565 6;
19566 7;
19567 }
19568 }
19569 ˇ"
19570 }
19571 .to_string(),
19572 );
19573
19574 let mut actual_guides = cx.update_editor(|editor, window, cx| {
19575 editor
19576 .snapshot(window, cx)
19577 .buffer_snapshot
19578 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
19579 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
19580 .collect::<Vec<_>>()
19581 });
19582 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
19583 assert_eq!(
19584 actual_guides,
19585 vec![
19586 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
19587 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
19588 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
19589 ]
19590 );
19591}
19592
19593#[gpui::test]
19594async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19595 init_test(cx, |_| {});
19596 let mut cx = EditorTestContext::new(cx).await;
19597
19598 let diff_base = r#"
19599 a
19600 b
19601 c
19602 "#
19603 .unindent();
19604
19605 cx.set_state(
19606 &r#"
19607 ˇA
19608 b
19609 C
19610 "#
19611 .unindent(),
19612 );
19613 cx.set_head_text(&diff_base);
19614 cx.update_editor(|editor, window, cx| {
19615 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19616 });
19617 executor.run_until_parked();
19618
19619 let both_hunks_expanded = r#"
19620 - a
19621 + ˇA
19622 b
19623 - c
19624 + C
19625 "#
19626 .unindent();
19627
19628 cx.assert_state_with_diff(both_hunks_expanded.clone());
19629
19630 let hunk_ranges = cx.update_editor(|editor, window, cx| {
19631 let snapshot = editor.snapshot(window, cx);
19632 let hunks = editor
19633 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
19634 .collect::<Vec<_>>();
19635 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
19636 let buffer_id = hunks[0].buffer_id;
19637 hunks
19638 .into_iter()
19639 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
19640 .collect::<Vec<_>>()
19641 });
19642 assert_eq!(hunk_ranges.len(), 2);
19643
19644 cx.update_editor(|editor, _, cx| {
19645 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
19646 });
19647 executor.run_until_parked();
19648
19649 let second_hunk_expanded = r#"
19650 ˇA
19651 b
19652 - c
19653 + C
19654 "#
19655 .unindent();
19656
19657 cx.assert_state_with_diff(second_hunk_expanded);
19658
19659 cx.update_editor(|editor, _, cx| {
19660 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
19661 });
19662 executor.run_until_parked();
19663
19664 cx.assert_state_with_diff(both_hunks_expanded.clone());
19665
19666 cx.update_editor(|editor, _, cx| {
19667 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
19668 });
19669 executor.run_until_parked();
19670
19671 let first_hunk_expanded = r#"
19672 - a
19673 + ˇA
19674 b
19675 C
19676 "#
19677 .unindent();
19678
19679 cx.assert_state_with_diff(first_hunk_expanded);
19680
19681 cx.update_editor(|editor, _, cx| {
19682 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
19683 });
19684 executor.run_until_parked();
19685
19686 cx.assert_state_with_diff(both_hunks_expanded);
19687
19688 cx.set_state(
19689 &r#"
19690 ˇA
19691 b
19692 "#
19693 .unindent(),
19694 );
19695 cx.run_until_parked();
19696
19697 // TODO this cursor position seems bad
19698 cx.assert_state_with_diff(
19699 r#"
19700 - ˇa
19701 + A
19702 b
19703 "#
19704 .unindent(),
19705 );
19706
19707 cx.update_editor(|editor, window, cx| {
19708 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19709 });
19710
19711 cx.assert_state_with_diff(
19712 r#"
19713 - ˇa
19714 + A
19715 b
19716 - c
19717 "#
19718 .unindent(),
19719 );
19720
19721 let hunk_ranges = cx.update_editor(|editor, window, cx| {
19722 let snapshot = editor.snapshot(window, cx);
19723 let hunks = editor
19724 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
19725 .collect::<Vec<_>>();
19726 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
19727 let buffer_id = hunks[0].buffer_id;
19728 hunks
19729 .into_iter()
19730 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
19731 .collect::<Vec<_>>()
19732 });
19733 assert_eq!(hunk_ranges.len(), 2);
19734
19735 cx.update_editor(|editor, _, cx| {
19736 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
19737 });
19738 executor.run_until_parked();
19739
19740 cx.assert_state_with_diff(
19741 r#"
19742 - ˇa
19743 + A
19744 b
19745 "#
19746 .unindent(),
19747 );
19748}
19749
19750#[gpui::test]
19751async fn test_toggle_deletion_hunk_at_start_of_file(
19752 executor: BackgroundExecutor,
19753 cx: &mut TestAppContext,
19754) {
19755 init_test(cx, |_| {});
19756 let mut cx = EditorTestContext::new(cx).await;
19757
19758 let diff_base = r#"
19759 a
19760 b
19761 c
19762 "#
19763 .unindent();
19764
19765 cx.set_state(
19766 &r#"
19767 ˇb
19768 c
19769 "#
19770 .unindent(),
19771 );
19772 cx.set_head_text(&diff_base);
19773 cx.update_editor(|editor, window, cx| {
19774 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19775 });
19776 executor.run_until_parked();
19777
19778 let hunk_expanded = r#"
19779 - a
19780 ˇb
19781 c
19782 "#
19783 .unindent();
19784
19785 cx.assert_state_with_diff(hunk_expanded.clone());
19786
19787 let hunk_ranges = cx.update_editor(|editor, window, cx| {
19788 let snapshot = editor.snapshot(window, cx);
19789 let hunks = editor
19790 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
19791 .collect::<Vec<_>>();
19792 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
19793 let buffer_id = hunks[0].buffer_id;
19794 hunks
19795 .into_iter()
19796 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
19797 .collect::<Vec<_>>()
19798 });
19799 assert_eq!(hunk_ranges.len(), 1);
19800
19801 cx.update_editor(|editor, _, cx| {
19802 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
19803 });
19804 executor.run_until_parked();
19805
19806 let hunk_collapsed = r#"
19807 ˇb
19808 c
19809 "#
19810 .unindent();
19811
19812 cx.assert_state_with_diff(hunk_collapsed);
19813
19814 cx.update_editor(|editor, _, cx| {
19815 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
19816 });
19817 executor.run_until_parked();
19818
19819 cx.assert_state_with_diff(hunk_expanded);
19820}
19821
19822#[gpui::test]
19823async fn test_display_diff_hunks(cx: &mut TestAppContext) {
19824 init_test(cx, |_| {});
19825
19826 let fs = FakeFs::new(cx.executor());
19827 fs.insert_tree(
19828 path!("/test"),
19829 json!({
19830 ".git": {},
19831 "file-1": "ONE\n",
19832 "file-2": "TWO\n",
19833 "file-3": "THREE\n",
19834 }),
19835 )
19836 .await;
19837
19838 fs.set_head_for_repo(
19839 path!("/test/.git").as_ref(),
19840 &[
19841 ("file-1".into(), "one\n".into()),
19842 ("file-2".into(), "two\n".into()),
19843 ("file-3".into(), "three\n".into()),
19844 ],
19845 "deadbeef",
19846 );
19847
19848 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
19849 let mut buffers = vec![];
19850 for i in 1..=3 {
19851 let buffer = project
19852 .update(cx, |project, cx| {
19853 let path = format!(path!("/test/file-{}"), i);
19854 project.open_local_buffer(path, cx)
19855 })
19856 .await
19857 .unwrap();
19858 buffers.push(buffer);
19859 }
19860
19861 let multibuffer = cx.new(|cx| {
19862 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
19863 multibuffer.set_all_diff_hunks_expanded(cx);
19864 for buffer in &buffers {
19865 let snapshot = buffer.read(cx).snapshot();
19866 multibuffer.set_excerpts_for_path(
19867 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
19868 buffer.clone(),
19869 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
19870 2,
19871 cx,
19872 );
19873 }
19874 multibuffer
19875 });
19876
19877 let editor = cx.add_window(|window, cx| {
19878 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
19879 });
19880 cx.run_until_parked();
19881
19882 let snapshot = editor
19883 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
19884 .unwrap();
19885 let hunks = snapshot
19886 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
19887 .map(|hunk| match hunk {
19888 DisplayDiffHunk::Unfolded {
19889 display_row_range, ..
19890 } => display_row_range,
19891 DisplayDiffHunk::Folded { .. } => unreachable!(),
19892 })
19893 .collect::<Vec<_>>();
19894 assert_eq!(
19895 hunks,
19896 [
19897 DisplayRow(2)..DisplayRow(4),
19898 DisplayRow(7)..DisplayRow(9),
19899 DisplayRow(12)..DisplayRow(14),
19900 ]
19901 );
19902}
19903
19904#[gpui::test]
19905async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
19906 init_test(cx, |_| {});
19907
19908 let mut cx = EditorTestContext::new(cx).await;
19909 cx.set_head_text(indoc! { "
19910 one
19911 two
19912 three
19913 four
19914 five
19915 "
19916 });
19917 cx.set_index_text(indoc! { "
19918 one
19919 two
19920 three
19921 four
19922 five
19923 "
19924 });
19925 cx.set_state(indoc! {"
19926 one
19927 TWO
19928 ˇTHREE
19929 FOUR
19930 five
19931 "});
19932 cx.run_until_parked();
19933 cx.update_editor(|editor, window, cx| {
19934 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19935 });
19936 cx.run_until_parked();
19937 cx.assert_index_text(Some(indoc! {"
19938 one
19939 TWO
19940 THREE
19941 FOUR
19942 five
19943 "}));
19944 cx.set_state(indoc! { "
19945 one
19946 TWO
19947 ˇTHREE-HUNDRED
19948 FOUR
19949 five
19950 "});
19951 cx.run_until_parked();
19952 cx.update_editor(|editor, window, cx| {
19953 let snapshot = editor.snapshot(window, cx);
19954 let hunks = editor
19955 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
19956 .collect::<Vec<_>>();
19957 assert_eq!(hunks.len(), 1);
19958 assert_eq!(
19959 hunks[0].status(),
19960 DiffHunkStatus {
19961 kind: DiffHunkStatusKind::Modified,
19962 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
19963 }
19964 );
19965
19966 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19967 });
19968 cx.run_until_parked();
19969 cx.assert_index_text(Some(indoc! {"
19970 one
19971 TWO
19972 THREE-HUNDRED
19973 FOUR
19974 five
19975 "}));
19976}
19977
19978#[gpui::test]
19979fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
19980 init_test(cx, |_| {});
19981
19982 let editor = cx.add_window(|window, cx| {
19983 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
19984 build_editor(buffer, window, cx)
19985 });
19986
19987 let render_args = Arc::new(Mutex::new(None));
19988 let snapshot = editor
19989 .update(cx, |editor, window, cx| {
19990 let snapshot = editor.buffer().read(cx).snapshot(cx);
19991 let range =
19992 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
19993
19994 struct RenderArgs {
19995 row: MultiBufferRow,
19996 folded: bool,
19997 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
19998 }
19999
20000 let crease = Crease::inline(
20001 range,
20002 FoldPlaceholder::test(),
20003 {
20004 let toggle_callback = render_args.clone();
20005 move |row, folded, callback, _window, _cx| {
20006 *toggle_callback.lock() = Some(RenderArgs {
20007 row,
20008 folded,
20009 callback,
20010 });
20011 div()
20012 }
20013 },
20014 |_row, _folded, _window, _cx| div(),
20015 );
20016
20017 editor.insert_creases(Some(crease), cx);
20018 let snapshot = editor.snapshot(window, cx);
20019 let _div =
20020 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
20021 snapshot
20022 })
20023 .unwrap();
20024
20025 let render_args = render_args.lock().take().unwrap();
20026 assert_eq!(render_args.row, MultiBufferRow(1));
20027 assert!(!render_args.folded);
20028 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
20029
20030 cx.update_window(*editor, |_, window, cx| {
20031 (render_args.callback)(true, window, cx)
20032 })
20033 .unwrap();
20034 let snapshot = editor
20035 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
20036 .unwrap();
20037 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
20038
20039 cx.update_window(*editor, |_, window, cx| {
20040 (render_args.callback)(false, window, cx)
20041 })
20042 .unwrap();
20043 let snapshot = editor
20044 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
20045 .unwrap();
20046 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
20047}
20048
20049#[gpui::test]
20050async fn test_input_text(cx: &mut TestAppContext) {
20051 init_test(cx, |_| {});
20052 let mut cx = EditorTestContext::new(cx).await;
20053
20054 cx.set_state(
20055 &r#"ˇone
20056 two
20057
20058 three
20059 fourˇ
20060 five
20061
20062 siˇx"#
20063 .unindent(),
20064 );
20065
20066 cx.dispatch_action(HandleInput(String::new()));
20067 cx.assert_editor_state(
20068 &r#"ˇone
20069 two
20070
20071 three
20072 fourˇ
20073 five
20074
20075 siˇx"#
20076 .unindent(),
20077 );
20078
20079 cx.dispatch_action(HandleInput("AAAA".to_string()));
20080 cx.assert_editor_state(
20081 &r#"AAAAˇone
20082 two
20083
20084 three
20085 fourAAAAˇ
20086 five
20087
20088 siAAAAˇx"#
20089 .unindent(),
20090 );
20091}
20092
20093#[gpui::test]
20094async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
20095 init_test(cx, |_| {});
20096
20097 let mut cx = EditorTestContext::new(cx).await;
20098 cx.set_state(
20099 r#"let foo = 1;
20100let foo = 2;
20101let foo = 3;
20102let fooˇ = 4;
20103let foo = 5;
20104let foo = 6;
20105let foo = 7;
20106let foo = 8;
20107let foo = 9;
20108let foo = 10;
20109let foo = 11;
20110let foo = 12;
20111let foo = 13;
20112let foo = 14;
20113let foo = 15;"#,
20114 );
20115
20116 cx.update_editor(|e, window, cx| {
20117 assert_eq!(
20118 e.next_scroll_position,
20119 NextScrollCursorCenterTopBottom::Center,
20120 "Default next scroll direction is center",
20121 );
20122
20123 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
20124 assert_eq!(
20125 e.next_scroll_position,
20126 NextScrollCursorCenterTopBottom::Top,
20127 "After center, next scroll direction should be top",
20128 );
20129
20130 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
20131 assert_eq!(
20132 e.next_scroll_position,
20133 NextScrollCursorCenterTopBottom::Bottom,
20134 "After top, next scroll direction should be bottom",
20135 );
20136
20137 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
20138 assert_eq!(
20139 e.next_scroll_position,
20140 NextScrollCursorCenterTopBottom::Center,
20141 "After bottom, scrolling should start over",
20142 );
20143
20144 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
20145 assert_eq!(
20146 e.next_scroll_position,
20147 NextScrollCursorCenterTopBottom::Top,
20148 "Scrolling continues if retriggered fast enough"
20149 );
20150 });
20151
20152 cx.executor()
20153 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
20154 cx.executor().run_until_parked();
20155 cx.update_editor(|e, _, _| {
20156 assert_eq!(
20157 e.next_scroll_position,
20158 NextScrollCursorCenterTopBottom::Center,
20159 "If scrolling is not triggered fast enough, it should reset"
20160 );
20161 });
20162}
20163
20164#[gpui::test]
20165async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
20166 init_test(cx, |_| {});
20167 let mut cx = EditorLspTestContext::new_rust(
20168 lsp::ServerCapabilities {
20169 definition_provider: Some(lsp::OneOf::Left(true)),
20170 references_provider: Some(lsp::OneOf::Left(true)),
20171 ..lsp::ServerCapabilities::default()
20172 },
20173 cx,
20174 )
20175 .await;
20176
20177 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
20178 let go_to_definition = cx
20179 .lsp
20180 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
20181 move |params, _| async move {
20182 if empty_go_to_definition {
20183 Ok(None)
20184 } else {
20185 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
20186 uri: params.text_document_position_params.text_document.uri,
20187 range: lsp::Range::new(
20188 lsp::Position::new(4, 3),
20189 lsp::Position::new(4, 6),
20190 ),
20191 })))
20192 }
20193 },
20194 );
20195 let references = cx
20196 .lsp
20197 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
20198 Ok(Some(vec![lsp::Location {
20199 uri: params.text_document_position.text_document.uri,
20200 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
20201 }]))
20202 });
20203 (go_to_definition, references)
20204 };
20205
20206 cx.set_state(
20207 &r#"fn one() {
20208 let mut a = ˇtwo();
20209 }
20210
20211 fn two() {}"#
20212 .unindent(),
20213 );
20214 set_up_lsp_handlers(false, &mut cx);
20215 let navigated = cx
20216 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
20217 .await
20218 .expect("Failed to navigate to definition");
20219 assert_eq!(
20220 navigated,
20221 Navigated::Yes,
20222 "Should have navigated to definition from the GetDefinition response"
20223 );
20224 cx.assert_editor_state(
20225 &r#"fn one() {
20226 let mut a = two();
20227 }
20228
20229 fn «twoˇ»() {}"#
20230 .unindent(),
20231 );
20232
20233 let editors = cx.update_workspace(|workspace, _, cx| {
20234 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
20235 });
20236 cx.update_editor(|_, _, test_editor_cx| {
20237 assert_eq!(
20238 editors.len(),
20239 1,
20240 "Initially, only one, test, editor should be open in the workspace"
20241 );
20242 assert_eq!(
20243 test_editor_cx.entity(),
20244 editors.last().expect("Asserted len is 1").clone()
20245 );
20246 });
20247
20248 set_up_lsp_handlers(true, &mut cx);
20249 let navigated = cx
20250 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
20251 .await
20252 .expect("Failed to navigate to lookup references");
20253 assert_eq!(
20254 navigated,
20255 Navigated::Yes,
20256 "Should have navigated to references as a fallback after empty GoToDefinition response"
20257 );
20258 // We should not change the selections in the existing file,
20259 // if opening another milti buffer with the references
20260 cx.assert_editor_state(
20261 &r#"fn one() {
20262 let mut a = two();
20263 }
20264
20265 fn «twoˇ»() {}"#
20266 .unindent(),
20267 );
20268 let editors = cx.update_workspace(|workspace, _, cx| {
20269 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
20270 });
20271 cx.update_editor(|_, _, test_editor_cx| {
20272 assert_eq!(
20273 editors.len(),
20274 2,
20275 "After falling back to references search, we open a new editor with the results"
20276 );
20277 let references_fallback_text = editors
20278 .into_iter()
20279 .find(|new_editor| *new_editor != test_editor_cx.entity())
20280 .expect("Should have one non-test editor now")
20281 .read(test_editor_cx)
20282 .text(test_editor_cx);
20283 assert_eq!(
20284 references_fallback_text, "fn one() {\n let mut a = two();\n}",
20285 "Should use the range from the references response and not the GoToDefinition one"
20286 );
20287 });
20288}
20289
20290#[gpui::test]
20291async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
20292 init_test(cx, |_| {});
20293 cx.update(|cx| {
20294 let mut editor_settings = EditorSettings::get_global(cx).clone();
20295 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
20296 EditorSettings::override_global(editor_settings, cx);
20297 });
20298 let mut cx = EditorLspTestContext::new_rust(
20299 lsp::ServerCapabilities {
20300 definition_provider: Some(lsp::OneOf::Left(true)),
20301 references_provider: Some(lsp::OneOf::Left(true)),
20302 ..lsp::ServerCapabilities::default()
20303 },
20304 cx,
20305 )
20306 .await;
20307 let original_state = r#"fn one() {
20308 let mut a = ˇtwo();
20309 }
20310
20311 fn two() {}"#
20312 .unindent();
20313 cx.set_state(&original_state);
20314
20315 let mut go_to_definition = cx
20316 .lsp
20317 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
20318 move |_, _| async move { Ok(None) },
20319 );
20320 let _references = cx
20321 .lsp
20322 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
20323 panic!("Should not call for references with no go to definition fallback")
20324 });
20325
20326 let navigated = cx
20327 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
20328 .await
20329 .expect("Failed to navigate to lookup references");
20330 go_to_definition
20331 .next()
20332 .await
20333 .expect("Should have called the go_to_definition handler");
20334
20335 assert_eq!(
20336 navigated,
20337 Navigated::No,
20338 "Should have navigated to references as a fallback after empty GoToDefinition response"
20339 );
20340 cx.assert_editor_state(&original_state);
20341 let editors = cx.update_workspace(|workspace, _, cx| {
20342 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
20343 });
20344 cx.update_editor(|_, _, _| {
20345 assert_eq!(
20346 editors.len(),
20347 1,
20348 "After unsuccessful fallback, no other editor should have been opened"
20349 );
20350 });
20351}
20352
20353#[gpui::test]
20354async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
20355 init_test(cx, |_| {});
20356
20357 let language = Arc::new(Language::new(
20358 LanguageConfig::default(),
20359 Some(tree_sitter_rust::LANGUAGE.into()),
20360 ));
20361
20362 let text = r#"
20363 #[cfg(test)]
20364 mod tests() {
20365 #[test]
20366 fn runnable_1() {
20367 let a = 1;
20368 }
20369
20370 #[test]
20371 fn runnable_2() {
20372 let a = 1;
20373 let b = 2;
20374 }
20375 }
20376 "#
20377 .unindent();
20378
20379 let fs = FakeFs::new(cx.executor());
20380 fs.insert_file("/file.rs", Default::default()).await;
20381
20382 let project = Project::test(fs, ["/a".as_ref()], cx).await;
20383 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20384 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20385 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
20386 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
20387
20388 let editor = cx.new_window_entity(|window, cx| {
20389 Editor::new(
20390 EditorMode::full(),
20391 multi_buffer,
20392 Some(project.clone()),
20393 window,
20394 cx,
20395 )
20396 });
20397
20398 editor.update_in(cx, |editor, window, cx| {
20399 let snapshot = editor.buffer().read(cx).snapshot(cx);
20400 editor.tasks.insert(
20401 (buffer.read(cx).remote_id(), 3),
20402 RunnableTasks {
20403 templates: vec![],
20404 offset: snapshot.anchor_before(43),
20405 column: 0,
20406 extra_variables: HashMap::default(),
20407 context_range: BufferOffset(43)..BufferOffset(85),
20408 },
20409 );
20410 editor.tasks.insert(
20411 (buffer.read(cx).remote_id(), 8),
20412 RunnableTasks {
20413 templates: vec![],
20414 offset: snapshot.anchor_before(86),
20415 column: 0,
20416 extra_variables: HashMap::default(),
20417 context_range: BufferOffset(86)..BufferOffset(191),
20418 },
20419 );
20420
20421 // Test finding task when cursor is inside function body
20422 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20423 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
20424 });
20425 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
20426 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
20427
20428 // Test finding task when cursor is on function name
20429 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20430 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
20431 });
20432 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
20433 assert_eq!(row, 8, "Should find task when cursor is on function name");
20434 });
20435}
20436
20437#[gpui::test]
20438async fn test_folding_buffers(cx: &mut TestAppContext) {
20439 init_test(cx, |_| {});
20440
20441 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
20442 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
20443 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
20444
20445 let fs = FakeFs::new(cx.executor());
20446 fs.insert_tree(
20447 path!("/a"),
20448 json!({
20449 "first.rs": sample_text_1,
20450 "second.rs": sample_text_2,
20451 "third.rs": sample_text_3,
20452 }),
20453 )
20454 .await;
20455 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20456 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20457 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20458 let worktree = project.update(cx, |project, cx| {
20459 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
20460 assert_eq!(worktrees.len(), 1);
20461 worktrees.pop().unwrap()
20462 });
20463 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
20464
20465 let buffer_1 = project
20466 .update(cx, |project, cx| {
20467 project.open_buffer((worktree_id, "first.rs"), cx)
20468 })
20469 .await
20470 .unwrap();
20471 let buffer_2 = project
20472 .update(cx, |project, cx| {
20473 project.open_buffer((worktree_id, "second.rs"), cx)
20474 })
20475 .await
20476 .unwrap();
20477 let buffer_3 = project
20478 .update(cx, |project, cx| {
20479 project.open_buffer((worktree_id, "third.rs"), cx)
20480 })
20481 .await
20482 .unwrap();
20483
20484 let multi_buffer = cx.new(|cx| {
20485 let mut multi_buffer = MultiBuffer::new(ReadWrite);
20486 multi_buffer.push_excerpts(
20487 buffer_1.clone(),
20488 [
20489 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20490 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20491 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20492 ],
20493 cx,
20494 );
20495 multi_buffer.push_excerpts(
20496 buffer_2.clone(),
20497 [
20498 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20499 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20500 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20501 ],
20502 cx,
20503 );
20504 multi_buffer.push_excerpts(
20505 buffer_3.clone(),
20506 [
20507 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20508 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20509 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20510 ],
20511 cx,
20512 );
20513 multi_buffer
20514 });
20515 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
20516 Editor::new(
20517 EditorMode::full(),
20518 multi_buffer.clone(),
20519 Some(project.clone()),
20520 window,
20521 cx,
20522 )
20523 });
20524
20525 assert_eq!(
20526 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20527 "\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",
20528 );
20529
20530 multi_buffer_editor.update(cx, |editor, cx| {
20531 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
20532 });
20533 assert_eq!(
20534 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20535 "\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",
20536 "After folding the first buffer, its text should not be displayed"
20537 );
20538
20539 multi_buffer_editor.update(cx, |editor, cx| {
20540 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
20541 });
20542 assert_eq!(
20543 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20544 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
20545 "After folding the second buffer, its text should not be displayed"
20546 );
20547
20548 multi_buffer_editor.update(cx, |editor, cx| {
20549 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
20550 });
20551 assert_eq!(
20552 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20553 "\n\n\n\n\n",
20554 "After folding the third buffer, its text should not be displayed"
20555 );
20556
20557 // Emulate selection inside the fold logic, that should work
20558 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20559 editor
20560 .snapshot(window, cx)
20561 .next_line_boundary(Point::new(0, 4));
20562 });
20563
20564 multi_buffer_editor.update(cx, |editor, cx| {
20565 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
20566 });
20567 assert_eq!(
20568 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20569 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
20570 "After unfolding the second buffer, its text should be displayed"
20571 );
20572
20573 // Typing inside of buffer 1 causes that buffer to be unfolded.
20574 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20575 assert_eq!(
20576 multi_buffer
20577 .read(cx)
20578 .snapshot(cx)
20579 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
20580 .collect::<String>(),
20581 "bbbb"
20582 );
20583 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
20584 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
20585 });
20586 editor.handle_input("B", window, cx);
20587 });
20588
20589 assert_eq!(
20590 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20591 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
20592 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
20593 );
20594
20595 multi_buffer_editor.update(cx, |editor, cx| {
20596 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
20597 });
20598 assert_eq!(
20599 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20600 "\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",
20601 "After unfolding the all buffers, all original text should be displayed"
20602 );
20603}
20604
20605#[gpui::test]
20606async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
20607 init_test(cx, |_| {});
20608
20609 let sample_text_1 = "1111\n2222\n3333".to_string();
20610 let sample_text_2 = "4444\n5555\n6666".to_string();
20611 let sample_text_3 = "7777\n8888\n9999".to_string();
20612
20613 let fs = FakeFs::new(cx.executor());
20614 fs.insert_tree(
20615 path!("/a"),
20616 json!({
20617 "first.rs": sample_text_1,
20618 "second.rs": sample_text_2,
20619 "third.rs": sample_text_3,
20620 }),
20621 )
20622 .await;
20623 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20624 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20625 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20626 let worktree = project.update(cx, |project, cx| {
20627 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
20628 assert_eq!(worktrees.len(), 1);
20629 worktrees.pop().unwrap()
20630 });
20631 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
20632
20633 let buffer_1 = project
20634 .update(cx, |project, cx| {
20635 project.open_buffer((worktree_id, "first.rs"), cx)
20636 })
20637 .await
20638 .unwrap();
20639 let buffer_2 = project
20640 .update(cx, |project, cx| {
20641 project.open_buffer((worktree_id, "second.rs"), cx)
20642 })
20643 .await
20644 .unwrap();
20645 let buffer_3 = project
20646 .update(cx, |project, cx| {
20647 project.open_buffer((worktree_id, "third.rs"), cx)
20648 })
20649 .await
20650 .unwrap();
20651
20652 let multi_buffer = cx.new(|cx| {
20653 let mut multi_buffer = MultiBuffer::new(ReadWrite);
20654 multi_buffer.push_excerpts(
20655 buffer_1.clone(),
20656 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
20657 cx,
20658 );
20659 multi_buffer.push_excerpts(
20660 buffer_2.clone(),
20661 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
20662 cx,
20663 );
20664 multi_buffer.push_excerpts(
20665 buffer_3.clone(),
20666 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
20667 cx,
20668 );
20669 multi_buffer
20670 });
20671
20672 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
20673 Editor::new(
20674 EditorMode::full(),
20675 multi_buffer,
20676 Some(project.clone()),
20677 window,
20678 cx,
20679 )
20680 });
20681
20682 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
20683 assert_eq!(
20684 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20685 full_text,
20686 );
20687
20688 multi_buffer_editor.update(cx, |editor, cx| {
20689 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
20690 });
20691 assert_eq!(
20692 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20693 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
20694 "After folding the first buffer, its text should not be displayed"
20695 );
20696
20697 multi_buffer_editor.update(cx, |editor, cx| {
20698 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
20699 });
20700
20701 assert_eq!(
20702 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20703 "\n\n\n\n\n\n7777\n8888\n9999",
20704 "After folding the second buffer, its text should not be displayed"
20705 );
20706
20707 multi_buffer_editor.update(cx, |editor, cx| {
20708 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
20709 });
20710 assert_eq!(
20711 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20712 "\n\n\n\n\n",
20713 "After folding the third buffer, its text should not be displayed"
20714 );
20715
20716 multi_buffer_editor.update(cx, |editor, cx| {
20717 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
20718 });
20719 assert_eq!(
20720 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20721 "\n\n\n\n4444\n5555\n6666\n\n",
20722 "After unfolding the second buffer, its text should be displayed"
20723 );
20724
20725 multi_buffer_editor.update(cx, |editor, cx| {
20726 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
20727 });
20728 assert_eq!(
20729 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20730 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
20731 "After unfolding the first buffer, its text should be displayed"
20732 );
20733
20734 multi_buffer_editor.update(cx, |editor, cx| {
20735 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
20736 });
20737 assert_eq!(
20738 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20739 full_text,
20740 "After unfolding all buffers, all original text should be displayed"
20741 );
20742}
20743
20744#[gpui::test]
20745async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
20746 init_test(cx, |_| {});
20747
20748 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
20749
20750 let fs = FakeFs::new(cx.executor());
20751 fs.insert_tree(
20752 path!("/a"),
20753 json!({
20754 "main.rs": sample_text,
20755 }),
20756 )
20757 .await;
20758 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20759 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20760 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20761 let worktree = project.update(cx, |project, cx| {
20762 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
20763 assert_eq!(worktrees.len(), 1);
20764 worktrees.pop().unwrap()
20765 });
20766 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
20767
20768 let buffer_1 = project
20769 .update(cx, |project, cx| {
20770 project.open_buffer((worktree_id, "main.rs"), cx)
20771 })
20772 .await
20773 .unwrap();
20774
20775 let multi_buffer = cx.new(|cx| {
20776 let mut multi_buffer = MultiBuffer::new(ReadWrite);
20777 multi_buffer.push_excerpts(
20778 buffer_1.clone(),
20779 [ExcerptRange::new(
20780 Point::new(0, 0)
20781 ..Point::new(
20782 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
20783 0,
20784 ),
20785 )],
20786 cx,
20787 );
20788 multi_buffer
20789 });
20790 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
20791 Editor::new(
20792 EditorMode::full(),
20793 multi_buffer,
20794 Some(project.clone()),
20795 window,
20796 cx,
20797 )
20798 });
20799
20800 let selection_range = Point::new(1, 0)..Point::new(2, 0);
20801 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20802 enum TestHighlight {}
20803 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
20804 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
20805 editor.highlight_text::<TestHighlight>(
20806 vec![highlight_range.clone()],
20807 HighlightStyle::color(Hsla::green()),
20808 cx,
20809 );
20810 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20811 s.select_ranges(Some(highlight_range))
20812 });
20813 });
20814
20815 let full_text = format!("\n\n{sample_text}");
20816 assert_eq!(
20817 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20818 full_text,
20819 );
20820}
20821
20822#[gpui::test]
20823async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
20824 init_test(cx, |_| {});
20825 cx.update(|cx| {
20826 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
20827 "keymaps/default-linux.json",
20828 cx,
20829 )
20830 .unwrap();
20831 cx.bind_keys(default_key_bindings);
20832 });
20833
20834 let (editor, cx) = cx.add_window_view(|window, cx| {
20835 let multi_buffer = MultiBuffer::build_multi(
20836 [
20837 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
20838 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
20839 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
20840 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
20841 ],
20842 cx,
20843 );
20844 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
20845
20846 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
20847 // fold all but the second buffer, so that we test navigating between two
20848 // adjacent folded buffers, as well as folded buffers at the start and
20849 // end the multibuffer
20850 editor.fold_buffer(buffer_ids[0], cx);
20851 editor.fold_buffer(buffer_ids[2], cx);
20852 editor.fold_buffer(buffer_ids[3], cx);
20853
20854 editor
20855 });
20856 cx.simulate_resize(size(px(1000.), px(1000.)));
20857
20858 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
20859 cx.assert_excerpts_with_selections(indoc! {"
20860 [EXCERPT]
20861 ˇ[FOLDED]
20862 [EXCERPT]
20863 a1
20864 b1
20865 [EXCERPT]
20866 [FOLDED]
20867 [EXCERPT]
20868 [FOLDED]
20869 "
20870 });
20871 cx.simulate_keystroke("down");
20872 cx.assert_excerpts_with_selections(indoc! {"
20873 [EXCERPT]
20874 [FOLDED]
20875 [EXCERPT]
20876 ˇa1
20877 b1
20878 [EXCERPT]
20879 [FOLDED]
20880 [EXCERPT]
20881 [FOLDED]
20882 "
20883 });
20884 cx.simulate_keystroke("down");
20885 cx.assert_excerpts_with_selections(indoc! {"
20886 [EXCERPT]
20887 [FOLDED]
20888 [EXCERPT]
20889 a1
20890 ˇb1
20891 [EXCERPT]
20892 [FOLDED]
20893 [EXCERPT]
20894 [FOLDED]
20895 "
20896 });
20897 cx.simulate_keystroke("down");
20898 cx.assert_excerpts_with_selections(indoc! {"
20899 [EXCERPT]
20900 [FOLDED]
20901 [EXCERPT]
20902 a1
20903 b1
20904 ˇ[EXCERPT]
20905 [FOLDED]
20906 [EXCERPT]
20907 [FOLDED]
20908 "
20909 });
20910 cx.simulate_keystroke("down");
20911 cx.assert_excerpts_with_selections(indoc! {"
20912 [EXCERPT]
20913 [FOLDED]
20914 [EXCERPT]
20915 a1
20916 b1
20917 [EXCERPT]
20918 ˇ[FOLDED]
20919 [EXCERPT]
20920 [FOLDED]
20921 "
20922 });
20923 for _ in 0..5 {
20924 cx.simulate_keystroke("down");
20925 cx.assert_excerpts_with_selections(indoc! {"
20926 [EXCERPT]
20927 [FOLDED]
20928 [EXCERPT]
20929 a1
20930 b1
20931 [EXCERPT]
20932 [FOLDED]
20933 [EXCERPT]
20934 ˇ[FOLDED]
20935 "
20936 });
20937 }
20938
20939 cx.simulate_keystroke("up");
20940 cx.assert_excerpts_with_selections(indoc! {"
20941 [EXCERPT]
20942 [FOLDED]
20943 [EXCERPT]
20944 a1
20945 b1
20946 [EXCERPT]
20947 ˇ[FOLDED]
20948 [EXCERPT]
20949 [FOLDED]
20950 "
20951 });
20952 cx.simulate_keystroke("up");
20953 cx.assert_excerpts_with_selections(indoc! {"
20954 [EXCERPT]
20955 [FOLDED]
20956 [EXCERPT]
20957 a1
20958 b1
20959 ˇ[EXCERPT]
20960 [FOLDED]
20961 [EXCERPT]
20962 [FOLDED]
20963 "
20964 });
20965 cx.simulate_keystroke("up");
20966 cx.assert_excerpts_with_selections(indoc! {"
20967 [EXCERPT]
20968 [FOLDED]
20969 [EXCERPT]
20970 a1
20971 ˇb1
20972 [EXCERPT]
20973 [FOLDED]
20974 [EXCERPT]
20975 [FOLDED]
20976 "
20977 });
20978 cx.simulate_keystroke("up");
20979 cx.assert_excerpts_with_selections(indoc! {"
20980 [EXCERPT]
20981 [FOLDED]
20982 [EXCERPT]
20983 ˇa1
20984 b1
20985 [EXCERPT]
20986 [FOLDED]
20987 [EXCERPT]
20988 [FOLDED]
20989 "
20990 });
20991 for _ in 0..5 {
20992 cx.simulate_keystroke("up");
20993 cx.assert_excerpts_with_selections(indoc! {"
20994 [EXCERPT]
20995 ˇ[FOLDED]
20996 [EXCERPT]
20997 a1
20998 b1
20999 [EXCERPT]
21000 [FOLDED]
21001 [EXCERPT]
21002 [FOLDED]
21003 "
21004 });
21005 }
21006}
21007
21008#[gpui::test]
21009async fn test_edit_prediction_text(cx: &mut TestAppContext) {
21010 init_test(cx, |_| {});
21011
21012 // Simple insertion
21013 assert_highlighted_edits(
21014 "Hello, world!",
21015 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
21016 true,
21017 cx,
21018 |highlighted_edits, cx| {
21019 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
21020 assert_eq!(highlighted_edits.highlights.len(), 1);
21021 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
21022 assert_eq!(
21023 highlighted_edits.highlights[0].1.background_color,
21024 Some(cx.theme().status().created_background)
21025 );
21026 },
21027 )
21028 .await;
21029
21030 // Replacement
21031 assert_highlighted_edits(
21032 "This is a test.",
21033 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
21034 false,
21035 cx,
21036 |highlighted_edits, cx| {
21037 assert_eq!(highlighted_edits.text, "That is a test.");
21038 assert_eq!(highlighted_edits.highlights.len(), 1);
21039 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
21040 assert_eq!(
21041 highlighted_edits.highlights[0].1.background_color,
21042 Some(cx.theme().status().created_background)
21043 );
21044 },
21045 )
21046 .await;
21047
21048 // Multiple edits
21049 assert_highlighted_edits(
21050 "Hello, world!",
21051 vec![
21052 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
21053 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
21054 ],
21055 false,
21056 cx,
21057 |highlighted_edits, cx| {
21058 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
21059 assert_eq!(highlighted_edits.highlights.len(), 2);
21060 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
21061 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
21062 assert_eq!(
21063 highlighted_edits.highlights[0].1.background_color,
21064 Some(cx.theme().status().created_background)
21065 );
21066 assert_eq!(
21067 highlighted_edits.highlights[1].1.background_color,
21068 Some(cx.theme().status().created_background)
21069 );
21070 },
21071 )
21072 .await;
21073
21074 // Multiple lines with edits
21075 assert_highlighted_edits(
21076 "First line\nSecond line\nThird line\nFourth line",
21077 vec![
21078 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
21079 (
21080 Point::new(2, 0)..Point::new(2, 10),
21081 "New third line".to_string(),
21082 ),
21083 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
21084 ],
21085 false,
21086 cx,
21087 |highlighted_edits, cx| {
21088 assert_eq!(
21089 highlighted_edits.text,
21090 "Second modified\nNew third line\nFourth updated line"
21091 );
21092 assert_eq!(highlighted_edits.highlights.len(), 3);
21093 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
21094 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
21095 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
21096 for highlight in &highlighted_edits.highlights {
21097 assert_eq!(
21098 highlight.1.background_color,
21099 Some(cx.theme().status().created_background)
21100 );
21101 }
21102 },
21103 )
21104 .await;
21105}
21106
21107#[gpui::test]
21108async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
21109 init_test(cx, |_| {});
21110
21111 // Deletion
21112 assert_highlighted_edits(
21113 "Hello, world!",
21114 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
21115 true,
21116 cx,
21117 |highlighted_edits, cx| {
21118 assert_eq!(highlighted_edits.text, "Hello, world!");
21119 assert_eq!(highlighted_edits.highlights.len(), 1);
21120 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
21121 assert_eq!(
21122 highlighted_edits.highlights[0].1.background_color,
21123 Some(cx.theme().status().deleted_background)
21124 );
21125 },
21126 )
21127 .await;
21128
21129 // Insertion
21130 assert_highlighted_edits(
21131 "Hello, world!",
21132 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
21133 true,
21134 cx,
21135 |highlighted_edits, cx| {
21136 assert_eq!(highlighted_edits.highlights.len(), 1);
21137 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
21138 assert_eq!(
21139 highlighted_edits.highlights[0].1.background_color,
21140 Some(cx.theme().status().created_background)
21141 );
21142 },
21143 )
21144 .await;
21145}
21146
21147async fn assert_highlighted_edits(
21148 text: &str,
21149 edits: Vec<(Range<Point>, String)>,
21150 include_deletions: bool,
21151 cx: &mut TestAppContext,
21152 assertion_fn: impl Fn(HighlightedText, &App),
21153) {
21154 let window = cx.add_window(|window, cx| {
21155 let buffer = MultiBuffer::build_simple(text, cx);
21156 Editor::new(EditorMode::full(), buffer, None, window, cx)
21157 });
21158 let cx = &mut VisualTestContext::from_window(*window, cx);
21159
21160 let (buffer, snapshot) = window
21161 .update(cx, |editor, _window, cx| {
21162 (
21163 editor.buffer().clone(),
21164 editor.buffer().read(cx).snapshot(cx),
21165 )
21166 })
21167 .unwrap();
21168
21169 let edits = edits
21170 .into_iter()
21171 .map(|(range, edit)| {
21172 (
21173 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
21174 edit,
21175 )
21176 })
21177 .collect::<Vec<_>>();
21178
21179 let text_anchor_edits = edits
21180 .clone()
21181 .into_iter()
21182 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
21183 .collect::<Vec<_>>();
21184
21185 let edit_preview = window
21186 .update(cx, |_, _window, cx| {
21187 buffer
21188 .read(cx)
21189 .as_singleton()
21190 .unwrap()
21191 .read(cx)
21192 .preview_edits(text_anchor_edits.into(), cx)
21193 })
21194 .unwrap()
21195 .await;
21196
21197 cx.update(|_window, cx| {
21198 let highlighted_edits = edit_prediction_edit_text(
21199 snapshot.as_singleton().unwrap().2,
21200 &edits,
21201 &edit_preview,
21202 include_deletions,
21203 cx,
21204 );
21205 assertion_fn(highlighted_edits, cx)
21206 });
21207}
21208
21209#[track_caller]
21210fn assert_breakpoint(
21211 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
21212 path: &Arc<Path>,
21213 expected: Vec<(u32, Breakpoint)>,
21214) {
21215 if expected.is_empty() {
21216 assert!(!breakpoints.contains_key(path), "{}", path.display());
21217 } else {
21218 let mut breakpoint = breakpoints
21219 .get(path)
21220 .unwrap()
21221 .iter()
21222 .map(|breakpoint| {
21223 (
21224 breakpoint.row,
21225 Breakpoint {
21226 message: breakpoint.message.clone(),
21227 state: breakpoint.state,
21228 condition: breakpoint.condition.clone(),
21229 hit_condition: breakpoint.hit_condition.clone(),
21230 },
21231 )
21232 })
21233 .collect::<Vec<_>>();
21234
21235 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
21236
21237 assert_eq!(expected, breakpoint);
21238 }
21239}
21240
21241fn add_log_breakpoint_at_cursor(
21242 editor: &mut Editor,
21243 log_message: &str,
21244 window: &mut Window,
21245 cx: &mut Context<Editor>,
21246) {
21247 let (anchor, bp) = editor
21248 .breakpoints_at_cursors(window, cx)
21249 .first()
21250 .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
21251 .unwrap_or_else(|| {
21252 let cursor_position: Point = editor.selections.newest(cx).head();
21253
21254 let breakpoint_position = editor
21255 .snapshot(window, cx)
21256 .display_snapshot
21257 .buffer_snapshot
21258 .anchor_before(Point::new(cursor_position.row, 0));
21259
21260 (breakpoint_position, Breakpoint::new_log(log_message))
21261 });
21262
21263 editor.edit_breakpoint_at_anchor(
21264 anchor,
21265 bp,
21266 BreakpointEditAction::EditLogMessage(log_message.into()),
21267 cx,
21268 );
21269}
21270
21271#[gpui::test]
21272async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
21273 init_test(cx, |_| {});
21274
21275 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
21276 let fs = FakeFs::new(cx.executor());
21277 fs.insert_tree(
21278 path!("/a"),
21279 json!({
21280 "main.rs": sample_text,
21281 }),
21282 )
21283 .await;
21284 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21285 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21286 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21287
21288 let fs = FakeFs::new(cx.executor());
21289 fs.insert_tree(
21290 path!("/a"),
21291 json!({
21292 "main.rs": sample_text,
21293 }),
21294 )
21295 .await;
21296 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21297 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21298 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21299 let worktree_id = workspace
21300 .update(cx, |workspace, _window, cx| {
21301 workspace.project().update(cx, |project, cx| {
21302 project.worktrees(cx).next().unwrap().read(cx).id()
21303 })
21304 })
21305 .unwrap();
21306
21307 let buffer = project
21308 .update(cx, |project, cx| {
21309 project.open_buffer((worktree_id, "main.rs"), cx)
21310 })
21311 .await
21312 .unwrap();
21313
21314 let (editor, cx) = cx.add_window_view(|window, cx| {
21315 Editor::new(
21316 EditorMode::full(),
21317 MultiBuffer::build_from_buffer(buffer, cx),
21318 Some(project.clone()),
21319 window,
21320 cx,
21321 )
21322 });
21323
21324 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
21325 let abs_path = project.read_with(cx, |project, cx| {
21326 project
21327 .absolute_path(&project_path, cx)
21328 .map(Arc::from)
21329 .unwrap()
21330 });
21331
21332 // assert we can add breakpoint on the first line
21333 editor.update_in(cx, |editor, window, cx| {
21334 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21335 editor.move_to_end(&MoveToEnd, window, cx);
21336 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21337 });
21338
21339 let breakpoints = editor.update(cx, |editor, cx| {
21340 editor
21341 .breakpoint_store()
21342 .as_ref()
21343 .unwrap()
21344 .read(cx)
21345 .all_source_breakpoints(cx)
21346 });
21347
21348 assert_eq!(1, breakpoints.len());
21349 assert_breakpoint(
21350 &breakpoints,
21351 &abs_path,
21352 vec![
21353 (0, Breakpoint::new_standard()),
21354 (3, Breakpoint::new_standard()),
21355 ],
21356 );
21357
21358 editor.update_in(cx, |editor, window, cx| {
21359 editor.move_to_beginning(&MoveToBeginning, window, cx);
21360 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21361 });
21362
21363 let breakpoints = editor.update(cx, |editor, cx| {
21364 editor
21365 .breakpoint_store()
21366 .as_ref()
21367 .unwrap()
21368 .read(cx)
21369 .all_source_breakpoints(cx)
21370 });
21371
21372 assert_eq!(1, breakpoints.len());
21373 assert_breakpoint(
21374 &breakpoints,
21375 &abs_path,
21376 vec![(3, Breakpoint::new_standard())],
21377 );
21378
21379 editor.update_in(cx, |editor, window, cx| {
21380 editor.move_to_end(&MoveToEnd, window, cx);
21381 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21382 });
21383
21384 let breakpoints = editor.update(cx, |editor, cx| {
21385 editor
21386 .breakpoint_store()
21387 .as_ref()
21388 .unwrap()
21389 .read(cx)
21390 .all_source_breakpoints(cx)
21391 });
21392
21393 assert_eq!(0, breakpoints.len());
21394 assert_breakpoint(&breakpoints, &abs_path, vec![]);
21395}
21396
21397#[gpui::test]
21398async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
21399 init_test(cx, |_| {});
21400
21401 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
21402
21403 let fs = FakeFs::new(cx.executor());
21404 fs.insert_tree(
21405 path!("/a"),
21406 json!({
21407 "main.rs": sample_text,
21408 }),
21409 )
21410 .await;
21411 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21412 let (workspace, cx) =
21413 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
21414
21415 let worktree_id = workspace.update(cx, |workspace, cx| {
21416 workspace.project().update(cx, |project, cx| {
21417 project.worktrees(cx).next().unwrap().read(cx).id()
21418 })
21419 });
21420
21421 let buffer = project
21422 .update(cx, |project, cx| {
21423 project.open_buffer((worktree_id, "main.rs"), cx)
21424 })
21425 .await
21426 .unwrap();
21427
21428 let (editor, cx) = cx.add_window_view(|window, cx| {
21429 Editor::new(
21430 EditorMode::full(),
21431 MultiBuffer::build_from_buffer(buffer, cx),
21432 Some(project.clone()),
21433 window,
21434 cx,
21435 )
21436 });
21437
21438 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
21439 let abs_path = project.read_with(cx, |project, cx| {
21440 project
21441 .absolute_path(&project_path, cx)
21442 .map(Arc::from)
21443 .unwrap()
21444 });
21445
21446 editor.update_in(cx, |editor, window, cx| {
21447 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
21448 });
21449
21450 let breakpoints = editor.update(cx, |editor, cx| {
21451 editor
21452 .breakpoint_store()
21453 .as_ref()
21454 .unwrap()
21455 .read(cx)
21456 .all_source_breakpoints(cx)
21457 });
21458
21459 assert_breakpoint(
21460 &breakpoints,
21461 &abs_path,
21462 vec![(0, Breakpoint::new_log("hello world"))],
21463 );
21464
21465 // Removing a log message from a log breakpoint should remove it
21466 editor.update_in(cx, |editor, window, cx| {
21467 add_log_breakpoint_at_cursor(editor, "", window, cx);
21468 });
21469
21470 let breakpoints = editor.update(cx, |editor, cx| {
21471 editor
21472 .breakpoint_store()
21473 .as_ref()
21474 .unwrap()
21475 .read(cx)
21476 .all_source_breakpoints(cx)
21477 });
21478
21479 assert_breakpoint(&breakpoints, &abs_path, vec![]);
21480
21481 editor.update_in(cx, |editor, window, cx| {
21482 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21483 editor.move_to_end(&MoveToEnd, window, cx);
21484 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21485 // Not adding a log message to a standard breakpoint shouldn't remove it
21486 add_log_breakpoint_at_cursor(editor, "", window, cx);
21487 });
21488
21489 let breakpoints = editor.update(cx, |editor, cx| {
21490 editor
21491 .breakpoint_store()
21492 .as_ref()
21493 .unwrap()
21494 .read(cx)
21495 .all_source_breakpoints(cx)
21496 });
21497
21498 assert_breakpoint(
21499 &breakpoints,
21500 &abs_path,
21501 vec![
21502 (0, Breakpoint::new_standard()),
21503 (3, Breakpoint::new_standard()),
21504 ],
21505 );
21506
21507 editor.update_in(cx, |editor, window, cx| {
21508 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
21509 });
21510
21511 let breakpoints = editor.update(cx, |editor, cx| {
21512 editor
21513 .breakpoint_store()
21514 .as_ref()
21515 .unwrap()
21516 .read(cx)
21517 .all_source_breakpoints(cx)
21518 });
21519
21520 assert_breakpoint(
21521 &breakpoints,
21522 &abs_path,
21523 vec![
21524 (0, Breakpoint::new_standard()),
21525 (3, Breakpoint::new_log("hello world")),
21526 ],
21527 );
21528
21529 editor.update_in(cx, |editor, window, cx| {
21530 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
21531 });
21532
21533 let breakpoints = editor.update(cx, |editor, cx| {
21534 editor
21535 .breakpoint_store()
21536 .as_ref()
21537 .unwrap()
21538 .read(cx)
21539 .all_source_breakpoints(cx)
21540 });
21541
21542 assert_breakpoint(
21543 &breakpoints,
21544 &abs_path,
21545 vec![
21546 (0, Breakpoint::new_standard()),
21547 (3, Breakpoint::new_log("hello Earth!!")),
21548 ],
21549 );
21550}
21551
21552/// This also tests that Editor::breakpoint_at_cursor_head is working properly
21553/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
21554/// or when breakpoints were placed out of order. This tests for a regression too
21555#[gpui::test]
21556async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
21557 init_test(cx, |_| {});
21558
21559 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
21560 let fs = FakeFs::new(cx.executor());
21561 fs.insert_tree(
21562 path!("/a"),
21563 json!({
21564 "main.rs": sample_text,
21565 }),
21566 )
21567 .await;
21568 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21569 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21570 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21571
21572 let fs = FakeFs::new(cx.executor());
21573 fs.insert_tree(
21574 path!("/a"),
21575 json!({
21576 "main.rs": sample_text,
21577 }),
21578 )
21579 .await;
21580 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21581 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21582 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21583 let worktree_id = workspace
21584 .update(cx, |workspace, _window, cx| {
21585 workspace.project().update(cx, |project, cx| {
21586 project.worktrees(cx).next().unwrap().read(cx).id()
21587 })
21588 })
21589 .unwrap();
21590
21591 let buffer = project
21592 .update(cx, |project, cx| {
21593 project.open_buffer((worktree_id, "main.rs"), cx)
21594 })
21595 .await
21596 .unwrap();
21597
21598 let (editor, cx) = cx.add_window_view(|window, cx| {
21599 Editor::new(
21600 EditorMode::full(),
21601 MultiBuffer::build_from_buffer(buffer, cx),
21602 Some(project.clone()),
21603 window,
21604 cx,
21605 )
21606 });
21607
21608 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
21609 let abs_path = project.read_with(cx, |project, cx| {
21610 project
21611 .absolute_path(&project_path, cx)
21612 .map(Arc::from)
21613 .unwrap()
21614 });
21615
21616 // assert we can add breakpoint on the first line
21617 editor.update_in(cx, |editor, window, cx| {
21618 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21619 editor.move_to_end(&MoveToEnd, window, cx);
21620 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21621 editor.move_up(&MoveUp, window, cx);
21622 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21623 });
21624
21625 let breakpoints = editor.update(cx, |editor, cx| {
21626 editor
21627 .breakpoint_store()
21628 .as_ref()
21629 .unwrap()
21630 .read(cx)
21631 .all_source_breakpoints(cx)
21632 });
21633
21634 assert_eq!(1, breakpoints.len());
21635 assert_breakpoint(
21636 &breakpoints,
21637 &abs_path,
21638 vec![
21639 (0, Breakpoint::new_standard()),
21640 (2, Breakpoint::new_standard()),
21641 (3, Breakpoint::new_standard()),
21642 ],
21643 );
21644
21645 editor.update_in(cx, |editor, window, cx| {
21646 editor.move_to_beginning(&MoveToBeginning, window, cx);
21647 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
21648 editor.move_to_end(&MoveToEnd, window, cx);
21649 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
21650 // Disabling a breakpoint that doesn't exist should do nothing
21651 editor.move_up(&MoveUp, window, cx);
21652 editor.move_up(&MoveUp, window, cx);
21653 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
21654 });
21655
21656 let breakpoints = editor.update(cx, |editor, cx| {
21657 editor
21658 .breakpoint_store()
21659 .as_ref()
21660 .unwrap()
21661 .read(cx)
21662 .all_source_breakpoints(cx)
21663 });
21664
21665 let disable_breakpoint = {
21666 let mut bp = Breakpoint::new_standard();
21667 bp.state = BreakpointState::Disabled;
21668 bp
21669 };
21670
21671 assert_eq!(1, breakpoints.len());
21672 assert_breakpoint(
21673 &breakpoints,
21674 &abs_path,
21675 vec![
21676 (0, disable_breakpoint.clone()),
21677 (2, Breakpoint::new_standard()),
21678 (3, disable_breakpoint.clone()),
21679 ],
21680 );
21681
21682 editor.update_in(cx, |editor, window, cx| {
21683 editor.move_to_beginning(&MoveToBeginning, window, cx);
21684 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
21685 editor.move_to_end(&MoveToEnd, window, cx);
21686 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
21687 editor.move_up(&MoveUp, window, cx);
21688 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
21689 });
21690
21691 let breakpoints = editor.update(cx, |editor, cx| {
21692 editor
21693 .breakpoint_store()
21694 .as_ref()
21695 .unwrap()
21696 .read(cx)
21697 .all_source_breakpoints(cx)
21698 });
21699
21700 assert_eq!(1, breakpoints.len());
21701 assert_breakpoint(
21702 &breakpoints,
21703 &abs_path,
21704 vec![
21705 (0, Breakpoint::new_standard()),
21706 (2, disable_breakpoint),
21707 (3, Breakpoint::new_standard()),
21708 ],
21709 );
21710}
21711
21712#[gpui::test]
21713async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
21714 init_test(cx, |_| {});
21715 let capabilities = lsp::ServerCapabilities {
21716 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
21717 prepare_provider: Some(true),
21718 work_done_progress_options: Default::default(),
21719 })),
21720 ..Default::default()
21721 };
21722 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
21723
21724 cx.set_state(indoc! {"
21725 struct Fˇoo {}
21726 "});
21727
21728 cx.update_editor(|editor, _, cx| {
21729 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
21730 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
21731 editor.highlight_background::<DocumentHighlightRead>(
21732 &[highlight_range],
21733 |theme| theme.colors().editor_document_highlight_read_background,
21734 cx,
21735 );
21736 });
21737
21738 let mut prepare_rename_handler = cx
21739 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
21740 move |_, _, _| async move {
21741 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
21742 start: lsp::Position {
21743 line: 0,
21744 character: 7,
21745 },
21746 end: lsp::Position {
21747 line: 0,
21748 character: 10,
21749 },
21750 })))
21751 },
21752 );
21753 let prepare_rename_task = cx
21754 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
21755 .expect("Prepare rename was not started");
21756 prepare_rename_handler.next().await.unwrap();
21757 prepare_rename_task.await.expect("Prepare rename failed");
21758
21759 let mut rename_handler =
21760 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
21761 let edit = lsp::TextEdit {
21762 range: lsp::Range {
21763 start: lsp::Position {
21764 line: 0,
21765 character: 7,
21766 },
21767 end: lsp::Position {
21768 line: 0,
21769 character: 10,
21770 },
21771 },
21772 new_text: "FooRenamed".to_string(),
21773 };
21774 Ok(Some(lsp::WorkspaceEdit::new(
21775 // Specify the same edit twice
21776 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
21777 )))
21778 });
21779 let rename_task = cx
21780 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
21781 .expect("Confirm rename was not started");
21782 rename_handler.next().await.unwrap();
21783 rename_task.await.expect("Confirm rename failed");
21784 cx.run_until_parked();
21785
21786 // Despite two edits, only one is actually applied as those are identical
21787 cx.assert_editor_state(indoc! {"
21788 struct FooRenamedˇ {}
21789 "});
21790}
21791
21792#[gpui::test]
21793async fn test_rename_without_prepare(cx: &mut TestAppContext) {
21794 init_test(cx, |_| {});
21795 // These capabilities indicate that the server does not support prepare rename.
21796 let capabilities = lsp::ServerCapabilities {
21797 rename_provider: Some(lsp::OneOf::Left(true)),
21798 ..Default::default()
21799 };
21800 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
21801
21802 cx.set_state(indoc! {"
21803 struct Fˇoo {}
21804 "});
21805
21806 cx.update_editor(|editor, _window, cx| {
21807 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
21808 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
21809 editor.highlight_background::<DocumentHighlightRead>(
21810 &[highlight_range],
21811 |theme| theme.colors().editor_document_highlight_read_background,
21812 cx,
21813 );
21814 });
21815
21816 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
21817 .expect("Prepare rename was not started")
21818 .await
21819 .expect("Prepare rename failed");
21820
21821 let mut rename_handler =
21822 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
21823 let edit = lsp::TextEdit {
21824 range: lsp::Range {
21825 start: lsp::Position {
21826 line: 0,
21827 character: 7,
21828 },
21829 end: lsp::Position {
21830 line: 0,
21831 character: 10,
21832 },
21833 },
21834 new_text: "FooRenamed".to_string(),
21835 };
21836 Ok(Some(lsp::WorkspaceEdit::new(
21837 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
21838 )))
21839 });
21840 let rename_task = cx
21841 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
21842 .expect("Confirm rename was not started");
21843 rename_handler.next().await.unwrap();
21844 rename_task.await.expect("Confirm rename failed");
21845 cx.run_until_parked();
21846
21847 // Correct range is renamed, as `surrounding_word` is used to find it.
21848 cx.assert_editor_state(indoc! {"
21849 struct FooRenamedˇ {}
21850 "});
21851}
21852
21853#[gpui::test]
21854async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
21855 init_test(cx, |_| {});
21856 let mut cx = EditorTestContext::new(cx).await;
21857
21858 let language = Arc::new(
21859 Language::new(
21860 LanguageConfig::default(),
21861 Some(tree_sitter_html::LANGUAGE.into()),
21862 )
21863 .with_brackets_query(
21864 r#"
21865 ("<" @open "/>" @close)
21866 ("</" @open ">" @close)
21867 ("<" @open ">" @close)
21868 ("\"" @open "\"" @close)
21869 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
21870 "#,
21871 )
21872 .unwrap(),
21873 );
21874 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
21875
21876 cx.set_state(indoc! {"
21877 <span>ˇ</span>
21878 "});
21879 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
21880 cx.assert_editor_state(indoc! {"
21881 <span>
21882 ˇ
21883 </span>
21884 "});
21885
21886 cx.set_state(indoc! {"
21887 <span><span></span>ˇ</span>
21888 "});
21889 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
21890 cx.assert_editor_state(indoc! {"
21891 <span><span></span>
21892 ˇ</span>
21893 "});
21894
21895 cx.set_state(indoc! {"
21896 <span>ˇ
21897 </span>
21898 "});
21899 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
21900 cx.assert_editor_state(indoc! {"
21901 <span>
21902 ˇ
21903 </span>
21904 "});
21905}
21906
21907#[gpui::test(iterations = 10)]
21908async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
21909 init_test(cx, |_| {});
21910
21911 let fs = FakeFs::new(cx.executor());
21912 fs.insert_tree(
21913 path!("/dir"),
21914 json!({
21915 "a.ts": "a",
21916 }),
21917 )
21918 .await;
21919
21920 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
21921 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21922 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21923
21924 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21925 language_registry.add(Arc::new(Language::new(
21926 LanguageConfig {
21927 name: "TypeScript".into(),
21928 matcher: LanguageMatcher {
21929 path_suffixes: vec!["ts".to_string()],
21930 ..Default::default()
21931 },
21932 ..Default::default()
21933 },
21934 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
21935 )));
21936 let mut fake_language_servers = language_registry.register_fake_lsp(
21937 "TypeScript",
21938 FakeLspAdapter {
21939 capabilities: lsp::ServerCapabilities {
21940 code_lens_provider: Some(lsp::CodeLensOptions {
21941 resolve_provider: Some(true),
21942 }),
21943 execute_command_provider: Some(lsp::ExecuteCommandOptions {
21944 commands: vec!["_the/command".to_string()],
21945 ..lsp::ExecuteCommandOptions::default()
21946 }),
21947 ..lsp::ServerCapabilities::default()
21948 },
21949 ..FakeLspAdapter::default()
21950 },
21951 );
21952
21953 let editor = workspace
21954 .update(cx, |workspace, window, cx| {
21955 workspace.open_abs_path(
21956 PathBuf::from(path!("/dir/a.ts")),
21957 OpenOptions::default(),
21958 window,
21959 cx,
21960 )
21961 })
21962 .unwrap()
21963 .await
21964 .unwrap()
21965 .downcast::<Editor>()
21966 .unwrap();
21967 cx.executor().run_until_parked();
21968
21969 let fake_server = fake_language_servers.next().await.unwrap();
21970
21971 let buffer = editor.update(cx, |editor, cx| {
21972 editor
21973 .buffer()
21974 .read(cx)
21975 .as_singleton()
21976 .expect("have opened a single file by path")
21977 });
21978
21979 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
21980 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
21981 drop(buffer_snapshot);
21982 let actions = cx
21983 .update_window(*workspace, |_, window, cx| {
21984 project.code_actions(&buffer, anchor..anchor, window, cx)
21985 })
21986 .unwrap();
21987
21988 fake_server
21989 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
21990 Ok(Some(vec![
21991 lsp::CodeLens {
21992 range: lsp::Range::default(),
21993 command: Some(lsp::Command {
21994 title: "Code lens command".to_owned(),
21995 command: "_the/command".to_owned(),
21996 arguments: None,
21997 }),
21998 data: None,
21999 },
22000 lsp::CodeLens {
22001 range: lsp::Range::default(),
22002 command: Some(lsp::Command {
22003 title: "Command not in capabilities".to_owned(),
22004 command: "not in capabilities".to_owned(),
22005 arguments: None,
22006 }),
22007 data: None,
22008 },
22009 lsp::CodeLens {
22010 range: lsp::Range {
22011 start: lsp::Position {
22012 line: 1,
22013 character: 1,
22014 },
22015 end: lsp::Position {
22016 line: 1,
22017 character: 1,
22018 },
22019 },
22020 command: Some(lsp::Command {
22021 title: "Command not in range".to_owned(),
22022 command: "_the/command".to_owned(),
22023 arguments: None,
22024 }),
22025 data: None,
22026 },
22027 ]))
22028 })
22029 .next()
22030 .await;
22031
22032 let actions = actions.await.unwrap();
22033 assert_eq!(
22034 actions.len(),
22035 1,
22036 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
22037 );
22038 let action = actions[0].clone();
22039 let apply = project.update(cx, |project, cx| {
22040 project.apply_code_action(buffer.clone(), action, true, cx)
22041 });
22042
22043 // Resolving the code action does not populate its edits. In absence of
22044 // edits, we must execute the given command.
22045 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
22046 |mut lens, _| async move {
22047 let lens_command = lens.command.as_mut().expect("should have a command");
22048 assert_eq!(lens_command.title, "Code lens command");
22049 lens_command.arguments = Some(vec![json!("the-argument")]);
22050 Ok(lens)
22051 },
22052 );
22053
22054 // While executing the command, the language server sends the editor
22055 // a `workspaceEdit` request.
22056 fake_server
22057 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
22058 let fake = fake_server.clone();
22059 move |params, _| {
22060 assert_eq!(params.command, "_the/command");
22061 let fake = fake.clone();
22062 async move {
22063 fake.server
22064 .request::<lsp::request::ApplyWorkspaceEdit>(
22065 lsp::ApplyWorkspaceEditParams {
22066 label: None,
22067 edit: lsp::WorkspaceEdit {
22068 changes: Some(
22069 [(
22070 lsp::Uri::from_file_path(path!("/dir/a.ts")).unwrap(),
22071 vec![lsp::TextEdit {
22072 range: lsp::Range::new(
22073 lsp::Position::new(0, 0),
22074 lsp::Position::new(0, 0),
22075 ),
22076 new_text: "X".into(),
22077 }],
22078 )]
22079 .into_iter()
22080 .collect(),
22081 ),
22082 ..lsp::WorkspaceEdit::default()
22083 },
22084 },
22085 )
22086 .await
22087 .into_response()
22088 .unwrap();
22089 Ok(Some(json!(null)))
22090 }
22091 }
22092 })
22093 .next()
22094 .await;
22095
22096 // Applying the code lens command returns a project transaction containing the edits
22097 // sent by the language server in its `workspaceEdit` request.
22098 let transaction = apply.await.unwrap();
22099 assert!(transaction.0.contains_key(&buffer));
22100 buffer.update(cx, |buffer, cx| {
22101 assert_eq!(buffer.text(), "Xa");
22102 buffer.undo(cx);
22103 assert_eq!(buffer.text(), "a");
22104 });
22105
22106 let actions_after_edits = cx
22107 .update_window(*workspace, |_, window, cx| {
22108 project.code_actions(&buffer, anchor..anchor, window, cx)
22109 })
22110 .unwrap()
22111 .await
22112 .unwrap();
22113 assert_eq!(
22114 actions, actions_after_edits,
22115 "For the same selection, same code lens actions should be returned"
22116 );
22117
22118 let _responses =
22119 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
22120 panic!("No more code lens requests are expected");
22121 });
22122 editor.update_in(cx, |editor, window, cx| {
22123 editor.select_all(&SelectAll, window, cx);
22124 });
22125 cx.executor().run_until_parked();
22126 let new_actions = cx
22127 .update_window(*workspace, |_, window, cx| {
22128 project.code_actions(&buffer, anchor..anchor, window, cx)
22129 })
22130 .unwrap()
22131 .await
22132 .unwrap();
22133 assert_eq!(
22134 actions, new_actions,
22135 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
22136 );
22137}
22138
22139#[gpui::test]
22140async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
22141 init_test(cx, |_| {});
22142
22143 let fs = FakeFs::new(cx.executor());
22144 let main_text = r#"fn main() {
22145println!("1");
22146println!("2");
22147println!("3");
22148println!("4");
22149println!("5");
22150}"#;
22151 let lib_text = "mod foo {}";
22152 fs.insert_tree(
22153 path!("/a"),
22154 json!({
22155 "lib.rs": lib_text,
22156 "main.rs": main_text,
22157 }),
22158 )
22159 .await;
22160
22161 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22162 let (workspace, cx) =
22163 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
22164 let worktree_id = workspace.update(cx, |workspace, cx| {
22165 workspace.project().update(cx, |project, cx| {
22166 project.worktrees(cx).next().unwrap().read(cx).id()
22167 })
22168 });
22169
22170 let expected_ranges = vec![
22171 Point::new(0, 0)..Point::new(0, 0),
22172 Point::new(1, 0)..Point::new(1, 1),
22173 Point::new(2, 0)..Point::new(2, 2),
22174 Point::new(3, 0)..Point::new(3, 3),
22175 ];
22176
22177 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
22178 let editor_1 = workspace
22179 .update_in(cx, |workspace, window, cx| {
22180 workspace.open_path(
22181 (worktree_id, "main.rs"),
22182 Some(pane_1.downgrade()),
22183 true,
22184 window,
22185 cx,
22186 )
22187 })
22188 .unwrap()
22189 .await
22190 .downcast::<Editor>()
22191 .unwrap();
22192 pane_1.update(cx, |pane, cx| {
22193 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22194 open_editor.update(cx, |editor, cx| {
22195 assert_eq!(
22196 editor.display_text(cx),
22197 main_text,
22198 "Original main.rs text on initial open",
22199 );
22200 assert_eq!(
22201 editor
22202 .selections
22203 .all::<Point>(cx)
22204 .into_iter()
22205 .map(|s| s.range())
22206 .collect::<Vec<_>>(),
22207 vec![Point::zero()..Point::zero()],
22208 "Default selections on initial open",
22209 );
22210 })
22211 });
22212 editor_1.update_in(cx, |editor, window, cx| {
22213 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22214 s.select_ranges(expected_ranges.clone());
22215 });
22216 });
22217
22218 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
22219 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
22220 });
22221 let editor_2 = workspace
22222 .update_in(cx, |workspace, window, cx| {
22223 workspace.open_path(
22224 (worktree_id, "main.rs"),
22225 Some(pane_2.downgrade()),
22226 true,
22227 window,
22228 cx,
22229 )
22230 })
22231 .unwrap()
22232 .await
22233 .downcast::<Editor>()
22234 .unwrap();
22235 pane_2.update(cx, |pane, cx| {
22236 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22237 open_editor.update(cx, |editor, cx| {
22238 assert_eq!(
22239 editor.display_text(cx),
22240 main_text,
22241 "Original main.rs text on initial open in another panel",
22242 );
22243 assert_eq!(
22244 editor
22245 .selections
22246 .all::<Point>(cx)
22247 .into_iter()
22248 .map(|s| s.range())
22249 .collect::<Vec<_>>(),
22250 vec![Point::zero()..Point::zero()],
22251 "Default selections on initial open in another panel",
22252 );
22253 })
22254 });
22255
22256 editor_2.update_in(cx, |editor, window, cx| {
22257 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
22258 });
22259
22260 let _other_editor_1 = workspace
22261 .update_in(cx, |workspace, window, cx| {
22262 workspace.open_path(
22263 (worktree_id, "lib.rs"),
22264 Some(pane_1.downgrade()),
22265 true,
22266 window,
22267 cx,
22268 )
22269 })
22270 .unwrap()
22271 .await
22272 .downcast::<Editor>()
22273 .unwrap();
22274 pane_1
22275 .update_in(cx, |pane, window, cx| {
22276 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
22277 })
22278 .await
22279 .unwrap();
22280 drop(editor_1);
22281 pane_1.update(cx, |pane, cx| {
22282 pane.active_item()
22283 .unwrap()
22284 .downcast::<Editor>()
22285 .unwrap()
22286 .update(cx, |editor, cx| {
22287 assert_eq!(
22288 editor.display_text(cx),
22289 lib_text,
22290 "Other file should be open and active",
22291 );
22292 });
22293 assert_eq!(pane.items().count(), 1, "No other editors should be open");
22294 });
22295
22296 let _other_editor_2 = workspace
22297 .update_in(cx, |workspace, window, cx| {
22298 workspace.open_path(
22299 (worktree_id, "lib.rs"),
22300 Some(pane_2.downgrade()),
22301 true,
22302 window,
22303 cx,
22304 )
22305 })
22306 .unwrap()
22307 .await
22308 .downcast::<Editor>()
22309 .unwrap();
22310 pane_2
22311 .update_in(cx, |pane, window, cx| {
22312 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
22313 })
22314 .await
22315 .unwrap();
22316 drop(editor_2);
22317 pane_2.update(cx, |pane, cx| {
22318 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22319 open_editor.update(cx, |editor, cx| {
22320 assert_eq!(
22321 editor.display_text(cx),
22322 lib_text,
22323 "Other file should be open and active in another panel too",
22324 );
22325 });
22326 assert_eq!(
22327 pane.items().count(),
22328 1,
22329 "No other editors should be open in another pane",
22330 );
22331 });
22332
22333 let _editor_1_reopened = workspace
22334 .update_in(cx, |workspace, window, cx| {
22335 workspace.open_path(
22336 (worktree_id, "main.rs"),
22337 Some(pane_1.downgrade()),
22338 true,
22339 window,
22340 cx,
22341 )
22342 })
22343 .unwrap()
22344 .await
22345 .downcast::<Editor>()
22346 .unwrap();
22347 let _editor_2_reopened = workspace
22348 .update_in(cx, |workspace, window, cx| {
22349 workspace.open_path(
22350 (worktree_id, "main.rs"),
22351 Some(pane_2.downgrade()),
22352 true,
22353 window,
22354 cx,
22355 )
22356 })
22357 .unwrap()
22358 .await
22359 .downcast::<Editor>()
22360 .unwrap();
22361 pane_1.update(cx, |pane, cx| {
22362 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22363 open_editor.update(cx, |editor, cx| {
22364 assert_eq!(
22365 editor.display_text(cx),
22366 main_text,
22367 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
22368 );
22369 assert_eq!(
22370 editor
22371 .selections
22372 .all::<Point>(cx)
22373 .into_iter()
22374 .map(|s| s.range())
22375 .collect::<Vec<_>>(),
22376 expected_ranges,
22377 "Previous editor in the 1st panel had selections and should get them restored on reopen",
22378 );
22379 })
22380 });
22381 pane_2.update(cx, |pane, cx| {
22382 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22383 open_editor.update(cx, |editor, cx| {
22384 assert_eq!(
22385 editor.display_text(cx),
22386 r#"fn main() {
22387⋯rintln!("1");
22388⋯intln!("2");
22389⋯ntln!("3");
22390println!("4");
22391println!("5");
22392}"#,
22393 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
22394 );
22395 assert_eq!(
22396 editor
22397 .selections
22398 .all::<Point>(cx)
22399 .into_iter()
22400 .map(|s| s.range())
22401 .collect::<Vec<_>>(),
22402 vec![Point::zero()..Point::zero()],
22403 "Previous editor in the 2nd pane had no selections changed hence should restore none",
22404 );
22405 })
22406 });
22407}
22408
22409#[gpui::test]
22410async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
22411 init_test(cx, |_| {});
22412
22413 let fs = FakeFs::new(cx.executor());
22414 let main_text = r#"fn main() {
22415println!("1");
22416println!("2");
22417println!("3");
22418println!("4");
22419println!("5");
22420}"#;
22421 let lib_text = "mod foo {}";
22422 fs.insert_tree(
22423 path!("/a"),
22424 json!({
22425 "lib.rs": lib_text,
22426 "main.rs": main_text,
22427 }),
22428 )
22429 .await;
22430
22431 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22432 let (workspace, cx) =
22433 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
22434 let worktree_id = workspace.update(cx, |workspace, cx| {
22435 workspace.project().update(cx, |project, cx| {
22436 project.worktrees(cx).next().unwrap().read(cx).id()
22437 })
22438 });
22439
22440 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
22441 let editor = workspace
22442 .update_in(cx, |workspace, window, cx| {
22443 workspace.open_path(
22444 (worktree_id, "main.rs"),
22445 Some(pane.downgrade()),
22446 true,
22447 window,
22448 cx,
22449 )
22450 })
22451 .unwrap()
22452 .await
22453 .downcast::<Editor>()
22454 .unwrap();
22455 pane.update(cx, |pane, cx| {
22456 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22457 open_editor.update(cx, |editor, cx| {
22458 assert_eq!(
22459 editor.display_text(cx),
22460 main_text,
22461 "Original main.rs text on initial open",
22462 );
22463 })
22464 });
22465 editor.update_in(cx, |editor, window, cx| {
22466 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
22467 });
22468
22469 cx.update_global(|store: &mut SettingsStore, cx| {
22470 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
22471 s.restore_on_file_reopen = Some(false);
22472 });
22473 });
22474 editor.update_in(cx, |editor, window, cx| {
22475 editor.fold_ranges(
22476 vec![
22477 Point::new(1, 0)..Point::new(1, 1),
22478 Point::new(2, 0)..Point::new(2, 2),
22479 Point::new(3, 0)..Point::new(3, 3),
22480 ],
22481 false,
22482 window,
22483 cx,
22484 );
22485 });
22486 pane.update_in(cx, |pane, window, cx| {
22487 pane.close_all_items(&CloseAllItems::default(), window, cx)
22488 })
22489 .await
22490 .unwrap();
22491 pane.update(cx, |pane, _| {
22492 assert!(pane.active_item().is_none());
22493 });
22494 cx.update_global(|store: &mut SettingsStore, cx| {
22495 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
22496 s.restore_on_file_reopen = Some(true);
22497 });
22498 });
22499
22500 let _editor_reopened = workspace
22501 .update_in(cx, |workspace, window, cx| {
22502 workspace.open_path(
22503 (worktree_id, "main.rs"),
22504 Some(pane.downgrade()),
22505 true,
22506 window,
22507 cx,
22508 )
22509 })
22510 .unwrap()
22511 .await
22512 .downcast::<Editor>()
22513 .unwrap();
22514 pane.update(cx, |pane, cx| {
22515 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22516 open_editor.update(cx, |editor, cx| {
22517 assert_eq!(
22518 editor.display_text(cx),
22519 main_text,
22520 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
22521 );
22522 })
22523 });
22524}
22525
22526#[gpui::test]
22527async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
22528 struct EmptyModalView {
22529 focus_handle: gpui::FocusHandle,
22530 }
22531 impl EventEmitter<DismissEvent> for EmptyModalView {}
22532 impl Render for EmptyModalView {
22533 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
22534 div()
22535 }
22536 }
22537 impl Focusable for EmptyModalView {
22538 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
22539 self.focus_handle.clone()
22540 }
22541 }
22542 impl workspace::ModalView for EmptyModalView {}
22543 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
22544 EmptyModalView {
22545 focus_handle: cx.focus_handle(),
22546 }
22547 }
22548
22549 init_test(cx, |_| {});
22550
22551 let fs = FakeFs::new(cx.executor());
22552 let project = Project::test(fs, [], cx).await;
22553 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22554 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
22555 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22556 let editor = cx.new_window_entity(|window, cx| {
22557 Editor::new(
22558 EditorMode::full(),
22559 buffer,
22560 Some(project.clone()),
22561 window,
22562 cx,
22563 )
22564 });
22565 workspace
22566 .update(cx, |workspace, window, cx| {
22567 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
22568 })
22569 .unwrap();
22570 editor.update_in(cx, |editor, window, cx| {
22571 editor.open_context_menu(&OpenContextMenu, window, cx);
22572 assert!(editor.mouse_context_menu.is_some());
22573 });
22574 workspace
22575 .update(cx, |workspace, window, cx| {
22576 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
22577 })
22578 .unwrap();
22579 cx.read(|cx| {
22580 assert!(editor.read(cx).mouse_context_menu.is_none());
22581 });
22582}
22583
22584#[gpui::test]
22585async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
22586 init_test(cx, |_| {});
22587
22588 let fs = FakeFs::new(cx.executor());
22589 fs.insert_file(path!("/file.html"), Default::default())
22590 .await;
22591
22592 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
22593
22594 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
22595 let html_language = Arc::new(Language::new(
22596 LanguageConfig {
22597 name: "HTML".into(),
22598 matcher: LanguageMatcher {
22599 path_suffixes: vec!["html".to_string()],
22600 ..LanguageMatcher::default()
22601 },
22602 brackets: BracketPairConfig {
22603 pairs: vec![BracketPair {
22604 start: "<".into(),
22605 end: ">".into(),
22606 close: true,
22607 ..Default::default()
22608 }],
22609 ..Default::default()
22610 },
22611 ..Default::default()
22612 },
22613 Some(tree_sitter_html::LANGUAGE.into()),
22614 ));
22615 language_registry.add(html_language);
22616 let mut fake_servers = language_registry.register_fake_lsp(
22617 "HTML",
22618 FakeLspAdapter {
22619 capabilities: lsp::ServerCapabilities {
22620 completion_provider: Some(lsp::CompletionOptions {
22621 resolve_provider: Some(true),
22622 ..Default::default()
22623 }),
22624 ..Default::default()
22625 },
22626 ..Default::default()
22627 },
22628 );
22629
22630 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22631 let cx = &mut VisualTestContext::from_window(*workspace, cx);
22632
22633 let worktree_id = workspace
22634 .update(cx, |workspace, _window, cx| {
22635 workspace.project().update(cx, |project, cx| {
22636 project.worktrees(cx).next().unwrap().read(cx).id()
22637 })
22638 })
22639 .unwrap();
22640 project
22641 .update(cx, |project, cx| {
22642 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
22643 })
22644 .await
22645 .unwrap();
22646 let editor = workspace
22647 .update(cx, |workspace, window, cx| {
22648 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
22649 })
22650 .unwrap()
22651 .await
22652 .unwrap()
22653 .downcast::<Editor>()
22654 .unwrap();
22655
22656 let fake_server = fake_servers.next().await.unwrap();
22657 editor.update_in(cx, |editor, window, cx| {
22658 editor.set_text("<ad></ad>", window, cx);
22659 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
22660 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
22661 });
22662 let Some((buffer, _)) = editor
22663 .buffer
22664 .read(cx)
22665 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
22666 else {
22667 panic!("Failed to get buffer for selection position");
22668 };
22669 let buffer = buffer.read(cx);
22670 let buffer_id = buffer.remote_id();
22671 let opening_range =
22672 buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3));
22673 let closing_range =
22674 buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
22675 let mut linked_ranges = HashMap::default();
22676 linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
22677 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
22678 });
22679 let mut completion_handle =
22680 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
22681 Ok(Some(lsp::CompletionResponse::Array(vec![
22682 lsp::CompletionItem {
22683 label: "head".to_string(),
22684 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
22685 lsp::InsertReplaceEdit {
22686 new_text: "head".to_string(),
22687 insert: lsp::Range::new(
22688 lsp::Position::new(0, 1),
22689 lsp::Position::new(0, 3),
22690 ),
22691 replace: lsp::Range::new(
22692 lsp::Position::new(0, 1),
22693 lsp::Position::new(0, 3),
22694 ),
22695 },
22696 )),
22697 ..Default::default()
22698 },
22699 ])))
22700 });
22701 editor.update_in(cx, |editor, window, cx| {
22702 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
22703 });
22704 cx.run_until_parked();
22705 completion_handle.next().await.unwrap();
22706 editor.update(cx, |editor, _| {
22707 assert!(
22708 editor.context_menu_visible(),
22709 "Completion menu should be visible"
22710 );
22711 });
22712 editor.update_in(cx, |editor, window, cx| {
22713 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
22714 });
22715 cx.executor().run_until_parked();
22716 editor.update(cx, |editor, cx| {
22717 assert_eq!(editor.text(cx), "<head></head>");
22718 });
22719}
22720
22721#[gpui::test]
22722async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
22723 init_test(cx, |_| {});
22724
22725 let fs = FakeFs::new(cx.executor());
22726 fs.insert_tree(
22727 path!("/root"),
22728 json!({
22729 "a": {
22730 "main.rs": "fn main() {}",
22731 },
22732 "foo": {
22733 "bar": {
22734 "external_file.rs": "pub mod external {}",
22735 }
22736 }
22737 }),
22738 )
22739 .await;
22740
22741 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
22742 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
22743 language_registry.add(rust_lang());
22744 let _fake_servers = language_registry.register_fake_lsp(
22745 "Rust",
22746 FakeLspAdapter {
22747 ..FakeLspAdapter::default()
22748 },
22749 );
22750 let (workspace, cx) =
22751 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
22752 let worktree_id = workspace.update(cx, |workspace, cx| {
22753 workspace.project().update(cx, |project, cx| {
22754 project.worktrees(cx).next().unwrap().read(cx).id()
22755 })
22756 });
22757
22758 let assert_language_servers_count =
22759 |expected: usize, context: &str, cx: &mut VisualTestContext| {
22760 project.update(cx, |project, cx| {
22761 let current = project
22762 .lsp_store()
22763 .read(cx)
22764 .as_local()
22765 .unwrap()
22766 .language_servers
22767 .len();
22768 assert_eq!(expected, current, "{context}");
22769 });
22770 };
22771
22772 assert_language_servers_count(
22773 0,
22774 "No servers should be running before any file is open",
22775 cx,
22776 );
22777 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
22778 let main_editor = workspace
22779 .update_in(cx, |workspace, window, cx| {
22780 workspace.open_path(
22781 (worktree_id, "main.rs"),
22782 Some(pane.downgrade()),
22783 true,
22784 window,
22785 cx,
22786 )
22787 })
22788 .unwrap()
22789 .await
22790 .downcast::<Editor>()
22791 .unwrap();
22792 pane.update(cx, |pane, cx| {
22793 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22794 open_editor.update(cx, |editor, cx| {
22795 assert_eq!(
22796 editor.display_text(cx),
22797 "fn main() {}",
22798 "Original main.rs text on initial open",
22799 );
22800 });
22801 assert_eq!(open_editor, main_editor);
22802 });
22803 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
22804
22805 let external_editor = workspace
22806 .update_in(cx, |workspace, window, cx| {
22807 workspace.open_abs_path(
22808 PathBuf::from("/root/foo/bar/external_file.rs"),
22809 OpenOptions::default(),
22810 window,
22811 cx,
22812 )
22813 })
22814 .await
22815 .expect("opening external file")
22816 .downcast::<Editor>()
22817 .expect("downcasted external file's open element to editor");
22818 pane.update(cx, |pane, cx| {
22819 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22820 open_editor.update(cx, |editor, cx| {
22821 assert_eq!(
22822 editor.display_text(cx),
22823 "pub mod external {}",
22824 "External file is open now",
22825 );
22826 });
22827 assert_eq!(open_editor, external_editor);
22828 });
22829 assert_language_servers_count(
22830 1,
22831 "Second, external, *.rs file should join the existing server",
22832 cx,
22833 );
22834
22835 pane.update_in(cx, |pane, window, cx| {
22836 pane.close_active_item(&CloseActiveItem::default(), window, cx)
22837 })
22838 .await
22839 .unwrap();
22840 pane.update_in(cx, |pane, window, cx| {
22841 pane.navigate_backward(&Default::default(), window, cx);
22842 });
22843 cx.run_until_parked();
22844 pane.update(cx, |pane, cx| {
22845 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22846 open_editor.update(cx, |editor, cx| {
22847 assert_eq!(
22848 editor.display_text(cx),
22849 "pub mod external {}",
22850 "External file is open now",
22851 );
22852 });
22853 });
22854 assert_language_servers_count(
22855 1,
22856 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
22857 cx,
22858 );
22859
22860 cx.update(|_, cx| {
22861 workspace::reload(cx);
22862 });
22863 assert_language_servers_count(
22864 1,
22865 "After reloading the worktree with local and external files opened, only one project should be started",
22866 cx,
22867 );
22868}
22869
22870#[gpui::test]
22871async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
22872 init_test(cx, |_| {});
22873
22874 let mut cx = EditorTestContext::new(cx).await;
22875 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
22876 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22877
22878 // test cursor move to start of each line on tab
22879 // for `if`, `elif`, `else`, `while`, `with` and `for`
22880 cx.set_state(indoc! {"
22881 def main():
22882 ˇ for item in items:
22883 ˇ while item.active:
22884 ˇ if item.value > 10:
22885 ˇ continue
22886 ˇ elif item.value < 0:
22887 ˇ break
22888 ˇ else:
22889 ˇ with item.context() as ctx:
22890 ˇ yield count
22891 ˇ else:
22892 ˇ log('while else')
22893 ˇ else:
22894 ˇ log('for else')
22895 "});
22896 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22897 cx.assert_editor_state(indoc! {"
22898 def main():
22899 ˇfor item in items:
22900 ˇwhile item.active:
22901 ˇif item.value > 10:
22902 ˇcontinue
22903 ˇelif item.value < 0:
22904 ˇbreak
22905 ˇelse:
22906 ˇwith item.context() as ctx:
22907 ˇyield count
22908 ˇelse:
22909 ˇlog('while else')
22910 ˇelse:
22911 ˇlog('for else')
22912 "});
22913 // test relative indent is preserved when tab
22914 // for `if`, `elif`, `else`, `while`, `with` and `for`
22915 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22916 cx.assert_editor_state(indoc! {"
22917 def main():
22918 ˇfor item in items:
22919 ˇwhile item.active:
22920 ˇif item.value > 10:
22921 ˇcontinue
22922 ˇelif item.value < 0:
22923 ˇbreak
22924 ˇelse:
22925 ˇwith item.context() as ctx:
22926 ˇyield count
22927 ˇelse:
22928 ˇlog('while else')
22929 ˇelse:
22930 ˇlog('for else')
22931 "});
22932
22933 // test cursor move to start of each line on tab
22934 // for `try`, `except`, `else`, `finally`, `match` and `def`
22935 cx.set_state(indoc! {"
22936 def main():
22937 ˇ try:
22938 ˇ fetch()
22939 ˇ except ValueError:
22940 ˇ handle_error()
22941 ˇ else:
22942 ˇ match value:
22943 ˇ case _:
22944 ˇ finally:
22945 ˇ def status():
22946 ˇ return 0
22947 "});
22948 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22949 cx.assert_editor_state(indoc! {"
22950 def main():
22951 ˇtry:
22952 ˇfetch()
22953 ˇexcept ValueError:
22954 ˇhandle_error()
22955 ˇelse:
22956 ˇmatch value:
22957 ˇcase _:
22958 ˇfinally:
22959 ˇdef status():
22960 ˇreturn 0
22961 "});
22962 // test relative indent is preserved when tab
22963 // for `try`, `except`, `else`, `finally`, `match` and `def`
22964 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22965 cx.assert_editor_state(indoc! {"
22966 def main():
22967 ˇtry:
22968 ˇfetch()
22969 ˇexcept ValueError:
22970 ˇhandle_error()
22971 ˇelse:
22972 ˇmatch value:
22973 ˇcase _:
22974 ˇfinally:
22975 ˇdef status():
22976 ˇreturn 0
22977 "});
22978}
22979
22980#[gpui::test]
22981async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
22982 init_test(cx, |_| {});
22983
22984 let mut cx = EditorTestContext::new(cx).await;
22985 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
22986 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22987
22988 // test `else` auto outdents when typed inside `if` block
22989 cx.set_state(indoc! {"
22990 def main():
22991 if i == 2:
22992 return
22993 ˇ
22994 "});
22995 cx.update_editor(|editor, window, cx| {
22996 editor.handle_input("else:", window, cx);
22997 });
22998 cx.assert_editor_state(indoc! {"
22999 def main():
23000 if i == 2:
23001 return
23002 else:ˇ
23003 "});
23004
23005 // test `except` auto outdents when typed inside `try` block
23006 cx.set_state(indoc! {"
23007 def main():
23008 try:
23009 i = 2
23010 ˇ
23011 "});
23012 cx.update_editor(|editor, window, cx| {
23013 editor.handle_input("except:", window, cx);
23014 });
23015 cx.assert_editor_state(indoc! {"
23016 def main():
23017 try:
23018 i = 2
23019 except:ˇ
23020 "});
23021
23022 // test `else` auto outdents when typed inside `except` block
23023 cx.set_state(indoc! {"
23024 def main():
23025 try:
23026 i = 2
23027 except:
23028 j = 2
23029 ˇ
23030 "});
23031 cx.update_editor(|editor, window, cx| {
23032 editor.handle_input("else:", window, cx);
23033 });
23034 cx.assert_editor_state(indoc! {"
23035 def main():
23036 try:
23037 i = 2
23038 except:
23039 j = 2
23040 else:ˇ
23041 "});
23042
23043 // test `finally` auto outdents when typed inside `else` block
23044 cx.set_state(indoc! {"
23045 def main():
23046 try:
23047 i = 2
23048 except:
23049 j = 2
23050 else:
23051 k = 2
23052 ˇ
23053 "});
23054 cx.update_editor(|editor, window, cx| {
23055 editor.handle_input("finally:", window, cx);
23056 });
23057 cx.assert_editor_state(indoc! {"
23058 def main():
23059 try:
23060 i = 2
23061 except:
23062 j = 2
23063 else:
23064 k = 2
23065 finally:ˇ
23066 "});
23067
23068 // test `else` does not outdents when typed inside `except` block right after for block
23069 cx.set_state(indoc! {"
23070 def main():
23071 try:
23072 i = 2
23073 except:
23074 for i in range(n):
23075 pass
23076 ˇ
23077 "});
23078 cx.update_editor(|editor, window, cx| {
23079 editor.handle_input("else:", window, cx);
23080 });
23081 cx.assert_editor_state(indoc! {"
23082 def main():
23083 try:
23084 i = 2
23085 except:
23086 for i in range(n):
23087 pass
23088 else:ˇ
23089 "});
23090
23091 // test `finally` auto outdents when typed inside `else` block right after for block
23092 cx.set_state(indoc! {"
23093 def main():
23094 try:
23095 i = 2
23096 except:
23097 j = 2
23098 else:
23099 for i in range(n):
23100 pass
23101 ˇ
23102 "});
23103 cx.update_editor(|editor, window, cx| {
23104 editor.handle_input("finally:", window, cx);
23105 });
23106 cx.assert_editor_state(indoc! {"
23107 def main():
23108 try:
23109 i = 2
23110 except:
23111 j = 2
23112 else:
23113 for i in range(n):
23114 pass
23115 finally:ˇ
23116 "});
23117
23118 // test `except` outdents to inner "try" block
23119 cx.set_state(indoc! {"
23120 def main():
23121 try:
23122 i = 2
23123 if i == 2:
23124 try:
23125 i = 3
23126 ˇ
23127 "});
23128 cx.update_editor(|editor, window, cx| {
23129 editor.handle_input("except:", window, cx);
23130 });
23131 cx.assert_editor_state(indoc! {"
23132 def main():
23133 try:
23134 i = 2
23135 if i == 2:
23136 try:
23137 i = 3
23138 except:ˇ
23139 "});
23140
23141 // test `except` outdents to outer "try" block
23142 cx.set_state(indoc! {"
23143 def main():
23144 try:
23145 i = 2
23146 if i == 2:
23147 try:
23148 i = 3
23149 ˇ
23150 "});
23151 cx.update_editor(|editor, window, cx| {
23152 editor.handle_input("except:", window, cx);
23153 });
23154 cx.assert_editor_state(indoc! {"
23155 def main():
23156 try:
23157 i = 2
23158 if i == 2:
23159 try:
23160 i = 3
23161 except:ˇ
23162 "});
23163
23164 // test `else` stays at correct indent when typed after `for` block
23165 cx.set_state(indoc! {"
23166 def main():
23167 for i in range(10):
23168 if i == 3:
23169 break
23170 ˇ
23171 "});
23172 cx.update_editor(|editor, window, cx| {
23173 editor.handle_input("else:", window, cx);
23174 });
23175 cx.assert_editor_state(indoc! {"
23176 def main():
23177 for i in range(10):
23178 if i == 3:
23179 break
23180 else:ˇ
23181 "});
23182
23183 // test does not outdent on typing after line with square brackets
23184 cx.set_state(indoc! {"
23185 def f() -> list[str]:
23186 ˇ
23187 "});
23188 cx.update_editor(|editor, window, cx| {
23189 editor.handle_input("a", window, cx);
23190 });
23191 cx.assert_editor_state(indoc! {"
23192 def f() -> list[str]:
23193 aˇ
23194 "});
23195
23196 // test does not outdent on typing : after case keyword
23197 cx.set_state(indoc! {"
23198 match 1:
23199 caseˇ
23200 "});
23201 cx.update_editor(|editor, window, cx| {
23202 editor.handle_input(":", window, cx);
23203 });
23204 cx.assert_editor_state(indoc! {"
23205 match 1:
23206 case:ˇ
23207 "});
23208}
23209
23210#[gpui::test]
23211async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
23212 init_test(cx, |_| {});
23213 update_test_language_settings(cx, |settings| {
23214 settings.defaults.extend_comment_on_newline = Some(false);
23215 });
23216 let mut cx = EditorTestContext::new(cx).await;
23217 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
23218 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23219
23220 // test correct indent after newline on comment
23221 cx.set_state(indoc! {"
23222 # COMMENT:ˇ
23223 "});
23224 cx.update_editor(|editor, window, cx| {
23225 editor.newline(&Newline, window, cx);
23226 });
23227 cx.assert_editor_state(indoc! {"
23228 # COMMENT:
23229 ˇ
23230 "});
23231
23232 // test correct indent after newline in brackets
23233 cx.set_state(indoc! {"
23234 {ˇ}
23235 "});
23236 cx.update_editor(|editor, window, cx| {
23237 editor.newline(&Newline, window, cx);
23238 });
23239 cx.run_until_parked();
23240 cx.assert_editor_state(indoc! {"
23241 {
23242 ˇ
23243 }
23244 "});
23245
23246 cx.set_state(indoc! {"
23247 (ˇ)
23248 "});
23249 cx.update_editor(|editor, window, cx| {
23250 editor.newline(&Newline, window, cx);
23251 });
23252 cx.run_until_parked();
23253 cx.assert_editor_state(indoc! {"
23254 (
23255 ˇ
23256 )
23257 "});
23258
23259 // do not indent after empty lists or dictionaries
23260 cx.set_state(indoc! {"
23261 a = []ˇ
23262 "});
23263 cx.update_editor(|editor, window, cx| {
23264 editor.newline(&Newline, window, cx);
23265 });
23266 cx.run_until_parked();
23267 cx.assert_editor_state(indoc! {"
23268 a = []
23269 ˇ
23270 "});
23271}
23272
23273#[gpui::test]
23274async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
23275 init_test(cx, |_| {});
23276
23277 let mut cx = EditorTestContext::new(cx).await;
23278 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
23279 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23280
23281 // test cursor move to start of each line on tab
23282 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
23283 cx.set_state(indoc! {"
23284 function main() {
23285 ˇ for item in $items; do
23286 ˇ while [ -n \"$item\" ]; do
23287 ˇ if [ \"$value\" -gt 10 ]; then
23288 ˇ continue
23289 ˇ elif [ \"$value\" -lt 0 ]; then
23290 ˇ break
23291 ˇ else
23292 ˇ echo \"$item\"
23293 ˇ fi
23294 ˇ done
23295 ˇ done
23296 ˇ}
23297 "});
23298 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
23299 cx.assert_editor_state(indoc! {"
23300 function main() {
23301 ˇfor item in $items; do
23302 ˇwhile [ -n \"$item\" ]; do
23303 ˇif [ \"$value\" -gt 10 ]; then
23304 ˇcontinue
23305 ˇelif [ \"$value\" -lt 0 ]; then
23306 ˇbreak
23307 ˇelse
23308 ˇecho \"$item\"
23309 ˇfi
23310 ˇdone
23311 ˇdone
23312 ˇ}
23313 "});
23314 // test relative indent is preserved when tab
23315 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
23316 cx.assert_editor_state(indoc! {"
23317 function main() {
23318 ˇfor item in $items; do
23319 ˇwhile [ -n \"$item\" ]; do
23320 ˇif [ \"$value\" -gt 10 ]; then
23321 ˇcontinue
23322 ˇelif [ \"$value\" -lt 0 ]; then
23323 ˇbreak
23324 ˇelse
23325 ˇecho \"$item\"
23326 ˇfi
23327 ˇdone
23328 ˇdone
23329 ˇ}
23330 "});
23331
23332 // test cursor move to start of each line on tab
23333 // for `case` statement with patterns
23334 cx.set_state(indoc! {"
23335 function handle() {
23336 ˇ case \"$1\" in
23337 ˇ start)
23338 ˇ echo \"a\"
23339 ˇ ;;
23340 ˇ stop)
23341 ˇ echo \"b\"
23342 ˇ ;;
23343 ˇ *)
23344 ˇ echo \"c\"
23345 ˇ ;;
23346 ˇ esac
23347 ˇ}
23348 "});
23349 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
23350 cx.assert_editor_state(indoc! {"
23351 function handle() {
23352 ˇcase \"$1\" in
23353 ˇstart)
23354 ˇecho \"a\"
23355 ˇ;;
23356 ˇstop)
23357 ˇecho \"b\"
23358 ˇ;;
23359 ˇ*)
23360 ˇecho \"c\"
23361 ˇ;;
23362 ˇesac
23363 ˇ}
23364 "});
23365}
23366
23367#[gpui::test]
23368async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
23369 init_test(cx, |_| {});
23370
23371 let mut cx = EditorTestContext::new(cx).await;
23372 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
23373 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23374
23375 // test indents on comment insert
23376 cx.set_state(indoc! {"
23377 function main() {
23378 ˇ for item in $items; do
23379 ˇ while [ -n \"$item\" ]; do
23380 ˇ if [ \"$value\" -gt 10 ]; then
23381 ˇ continue
23382 ˇ elif [ \"$value\" -lt 0 ]; then
23383 ˇ break
23384 ˇ else
23385 ˇ echo \"$item\"
23386 ˇ fi
23387 ˇ done
23388 ˇ done
23389 ˇ}
23390 "});
23391 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
23392 cx.assert_editor_state(indoc! {"
23393 function main() {
23394 #ˇ for item in $items; do
23395 #ˇ while [ -n \"$item\" ]; do
23396 #ˇ if [ \"$value\" -gt 10 ]; then
23397 #ˇ continue
23398 #ˇ elif [ \"$value\" -lt 0 ]; then
23399 #ˇ break
23400 #ˇ else
23401 #ˇ echo \"$item\"
23402 #ˇ fi
23403 #ˇ done
23404 #ˇ done
23405 #ˇ}
23406 "});
23407}
23408
23409#[gpui::test]
23410async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
23411 init_test(cx, |_| {});
23412
23413 let mut cx = EditorTestContext::new(cx).await;
23414 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
23415 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23416
23417 // test `else` auto outdents when typed inside `if` block
23418 cx.set_state(indoc! {"
23419 if [ \"$1\" = \"test\" ]; then
23420 echo \"foo bar\"
23421 ˇ
23422 "});
23423 cx.update_editor(|editor, window, cx| {
23424 editor.handle_input("else", window, cx);
23425 });
23426 cx.assert_editor_state(indoc! {"
23427 if [ \"$1\" = \"test\" ]; then
23428 echo \"foo bar\"
23429 elseˇ
23430 "});
23431
23432 // test `elif` auto outdents when typed inside `if` block
23433 cx.set_state(indoc! {"
23434 if [ \"$1\" = \"test\" ]; then
23435 echo \"foo bar\"
23436 ˇ
23437 "});
23438 cx.update_editor(|editor, window, cx| {
23439 editor.handle_input("elif", window, cx);
23440 });
23441 cx.assert_editor_state(indoc! {"
23442 if [ \"$1\" = \"test\" ]; then
23443 echo \"foo bar\"
23444 elifˇ
23445 "});
23446
23447 // test `fi` auto outdents when typed inside `else` block
23448 cx.set_state(indoc! {"
23449 if [ \"$1\" = \"test\" ]; then
23450 echo \"foo bar\"
23451 else
23452 echo \"bar baz\"
23453 ˇ
23454 "});
23455 cx.update_editor(|editor, window, cx| {
23456 editor.handle_input("fi", window, cx);
23457 });
23458 cx.assert_editor_state(indoc! {"
23459 if [ \"$1\" = \"test\" ]; then
23460 echo \"foo bar\"
23461 else
23462 echo \"bar baz\"
23463 fiˇ
23464 "});
23465
23466 // test `done` auto outdents when typed inside `while` block
23467 cx.set_state(indoc! {"
23468 while read line; do
23469 echo \"$line\"
23470 ˇ
23471 "});
23472 cx.update_editor(|editor, window, cx| {
23473 editor.handle_input("done", window, cx);
23474 });
23475 cx.assert_editor_state(indoc! {"
23476 while read line; do
23477 echo \"$line\"
23478 doneˇ
23479 "});
23480
23481 // test `done` auto outdents when typed inside `for` block
23482 cx.set_state(indoc! {"
23483 for file in *.txt; do
23484 cat \"$file\"
23485 ˇ
23486 "});
23487 cx.update_editor(|editor, window, cx| {
23488 editor.handle_input("done", window, cx);
23489 });
23490 cx.assert_editor_state(indoc! {"
23491 for file in *.txt; do
23492 cat \"$file\"
23493 doneˇ
23494 "});
23495
23496 // test `esac` auto outdents when typed inside `case` block
23497 cx.set_state(indoc! {"
23498 case \"$1\" in
23499 start)
23500 echo \"foo bar\"
23501 ;;
23502 stop)
23503 echo \"bar baz\"
23504 ;;
23505 ˇ
23506 "});
23507 cx.update_editor(|editor, window, cx| {
23508 editor.handle_input("esac", window, cx);
23509 });
23510 cx.assert_editor_state(indoc! {"
23511 case \"$1\" in
23512 start)
23513 echo \"foo bar\"
23514 ;;
23515 stop)
23516 echo \"bar baz\"
23517 ;;
23518 esacˇ
23519 "});
23520
23521 // test `*)` auto outdents when typed inside `case` block
23522 cx.set_state(indoc! {"
23523 case \"$1\" in
23524 start)
23525 echo \"foo bar\"
23526 ;;
23527 ˇ
23528 "});
23529 cx.update_editor(|editor, window, cx| {
23530 editor.handle_input("*)", window, cx);
23531 });
23532 cx.assert_editor_state(indoc! {"
23533 case \"$1\" in
23534 start)
23535 echo \"foo bar\"
23536 ;;
23537 *)ˇ
23538 "});
23539
23540 // test `fi` outdents to correct level with nested if blocks
23541 cx.set_state(indoc! {"
23542 if [ \"$1\" = \"test\" ]; then
23543 echo \"outer if\"
23544 if [ \"$2\" = \"debug\" ]; then
23545 echo \"inner if\"
23546 ˇ
23547 "});
23548 cx.update_editor(|editor, window, cx| {
23549 editor.handle_input("fi", window, cx);
23550 });
23551 cx.assert_editor_state(indoc! {"
23552 if [ \"$1\" = \"test\" ]; then
23553 echo \"outer if\"
23554 if [ \"$2\" = \"debug\" ]; then
23555 echo \"inner if\"
23556 fiˇ
23557 "});
23558}
23559
23560#[gpui::test]
23561async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
23562 init_test(cx, |_| {});
23563 update_test_language_settings(cx, |settings| {
23564 settings.defaults.extend_comment_on_newline = Some(false);
23565 });
23566 let mut cx = EditorTestContext::new(cx).await;
23567 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
23568 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23569
23570 // test correct indent after newline on comment
23571 cx.set_state(indoc! {"
23572 # COMMENT:ˇ
23573 "});
23574 cx.update_editor(|editor, window, cx| {
23575 editor.newline(&Newline, window, cx);
23576 });
23577 cx.assert_editor_state(indoc! {"
23578 # COMMENT:
23579 ˇ
23580 "});
23581
23582 // test correct indent after newline after `then`
23583 cx.set_state(indoc! {"
23584
23585 if [ \"$1\" = \"test\" ]; thenˇ
23586 "});
23587 cx.update_editor(|editor, window, cx| {
23588 editor.newline(&Newline, window, cx);
23589 });
23590 cx.run_until_parked();
23591 cx.assert_editor_state(indoc! {"
23592
23593 if [ \"$1\" = \"test\" ]; then
23594 ˇ
23595 "});
23596
23597 // test correct indent after newline after `else`
23598 cx.set_state(indoc! {"
23599 if [ \"$1\" = \"test\" ]; then
23600 elseˇ
23601 "});
23602 cx.update_editor(|editor, window, cx| {
23603 editor.newline(&Newline, window, cx);
23604 });
23605 cx.run_until_parked();
23606 cx.assert_editor_state(indoc! {"
23607 if [ \"$1\" = \"test\" ]; then
23608 else
23609 ˇ
23610 "});
23611
23612 // test correct indent after newline after `elif`
23613 cx.set_state(indoc! {"
23614 if [ \"$1\" = \"test\" ]; then
23615 elifˇ
23616 "});
23617 cx.update_editor(|editor, window, cx| {
23618 editor.newline(&Newline, window, cx);
23619 });
23620 cx.run_until_parked();
23621 cx.assert_editor_state(indoc! {"
23622 if [ \"$1\" = \"test\" ]; then
23623 elif
23624 ˇ
23625 "});
23626
23627 // test correct indent after newline after `do`
23628 cx.set_state(indoc! {"
23629 for file in *.txt; doˇ
23630 "});
23631 cx.update_editor(|editor, window, cx| {
23632 editor.newline(&Newline, window, cx);
23633 });
23634 cx.run_until_parked();
23635 cx.assert_editor_state(indoc! {"
23636 for file in *.txt; do
23637 ˇ
23638 "});
23639
23640 // test correct indent after newline after case pattern
23641 cx.set_state(indoc! {"
23642 case \"$1\" in
23643 start)ˇ
23644 "});
23645 cx.update_editor(|editor, window, cx| {
23646 editor.newline(&Newline, window, cx);
23647 });
23648 cx.run_until_parked();
23649 cx.assert_editor_state(indoc! {"
23650 case \"$1\" in
23651 start)
23652 ˇ
23653 "});
23654
23655 // test correct indent after newline after case pattern
23656 cx.set_state(indoc! {"
23657 case \"$1\" in
23658 start)
23659 ;;
23660 *)ˇ
23661 "});
23662 cx.update_editor(|editor, window, cx| {
23663 editor.newline(&Newline, window, cx);
23664 });
23665 cx.run_until_parked();
23666 cx.assert_editor_state(indoc! {"
23667 case \"$1\" in
23668 start)
23669 ;;
23670 *)
23671 ˇ
23672 "});
23673
23674 // test correct indent after newline after function opening brace
23675 cx.set_state(indoc! {"
23676 function test() {ˇ}
23677 "});
23678 cx.update_editor(|editor, window, cx| {
23679 editor.newline(&Newline, window, cx);
23680 });
23681 cx.run_until_parked();
23682 cx.assert_editor_state(indoc! {"
23683 function test() {
23684 ˇ
23685 }
23686 "});
23687
23688 // test no extra indent after semicolon on same line
23689 cx.set_state(indoc! {"
23690 echo \"test\";ˇ
23691 "});
23692 cx.update_editor(|editor, window, cx| {
23693 editor.newline(&Newline, window, cx);
23694 });
23695 cx.run_until_parked();
23696 cx.assert_editor_state(indoc! {"
23697 echo \"test\";
23698 ˇ
23699 "});
23700}
23701
23702fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
23703 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
23704 point..point
23705}
23706
23707#[track_caller]
23708fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
23709 let (text, ranges) = marked_text_ranges(marked_text, true);
23710 assert_eq!(editor.text(cx), text);
23711 assert_eq!(
23712 editor.selections.ranges(cx),
23713 ranges,
23714 "Assert selections are {}",
23715 marked_text
23716 );
23717}
23718
23719pub fn handle_signature_help_request(
23720 cx: &mut EditorLspTestContext,
23721 mocked_response: lsp::SignatureHelp,
23722) -> impl Future<Output = ()> + use<> {
23723 let mut request =
23724 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
23725 let mocked_response = mocked_response.clone();
23726 async move { Ok(Some(mocked_response)) }
23727 });
23728
23729 async move {
23730 request.next().await;
23731 }
23732}
23733
23734#[track_caller]
23735pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
23736 cx.update_editor(|editor, _, _| {
23737 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
23738 let entries = menu.entries.borrow();
23739 let entries = entries
23740 .iter()
23741 .map(|entry| entry.string.as_str())
23742 .collect::<Vec<_>>();
23743 assert_eq!(entries, expected);
23744 } else {
23745 panic!("Expected completions menu");
23746 }
23747 });
23748}
23749
23750/// Handle completion request passing a marked string specifying where the completion
23751/// should be triggered from using '|' character, what range should be replaced, and what completions
23752/// should be returned using '<' and '>' to delimit the range.
23753///
23754/// Also see `handle_completion_request_with_insert_and_replace`.
23755#[track_caller]
23756pub fn handle_completion_request(
23757 marked_string: &str,
23758 completions: Vec<&'static str>,
23759 is_incomplete: bool,
23760 counter: Arc<AtomicUsize>,
23761 cx: &mut EditorLspTestContext,
23762) -> impl Future<Output = ()> {
23763 let complete_from_marker: TextRangeMarker = '|'.into();
23764 let replace_range_marker: TextRangeMarker = ('<', '>').into();
23765 let (_, mut marked_ranges) = marked_text_ranges_by(
23766 marked_string,
23767 vec![complete_from_marker.clone(), replace_range_marker.clone()],
23768 );
23769
23770 let complete_from_position =
23771 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
23772 let replace_range =
23773 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
23774
23775 let mut request =
23776 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
23777 let completions = completions.clone();
23778 counter.fetch_add(1, atomic::Ordering::Release);
23779 async move {
23780 assert_eq!(params.text_document_position.text_document.uri, url.clone());
23781 assert_eq!(
23782 params.text_document_position.position,
23783 complete_from_position
23784 );
23785 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
23786 is_incomplete,
23787 item_defaults: None,
23788 items: completions
23789 .iter()
23790 .map(|completion_text| lsp::CompletionItem {
23791 label: completion_text.to_string(),
23792 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
23793 range: replace_range,
23794 new_text: completion_text.to_string(),
23795 })),
23796 ..Default::default()
23797 })
23798 .collect(),
23799 })))
23800 }
23801 });
23802
23803 async move {
23804 request.next().await;
23805 }
23806}
23807
23808/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
23809/// given instead, which also contains an `insert` range.
23810///
23811/// This function uses markers to define ranges:
23812/// - `|` marks the cursor position
23813/// - `<>` marks the replace range
23814/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
23815pub fn handle_completion_request_with_insert_and_replace(
23816 cx: &mut EditorLspTestContext,
23817 marked_string: &str,
23818 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
23819 counter: Arc<AtomicUsize>,
23820) -> impl Future<Output = ()> {
23821 let complete_from_marker: TextRangeMarker = '|'.into();
23822 let replace_range_marker: TextRangeMarker = ('<', '>').into();
23823 let insert_range_marker: TextRangeMarker = ('{', '}').into();
23824
23825 let (_, mut marked_ranges) = marked_text_ranges_by(
23826 marked_string,
23827 vec![
23828 complete_from_marker.clone(),
23829 replace_range_marker.clone(),
23830 insert_range_marker.clone(),
23831 ],
23832 );
23833
23834 let complete_from_position =
23835 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
23836 let replace_range =
23837 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
23838
23839 let insert_range = match marked_ranges.remove(&insert_range_marker) {
23840 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
23841 _ => lsp::Range {
23842 start: replace_range.start,
23843 end: complete_from_position,
23844 },
23845 };
23846
23847 let mut request =
23848 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
23849 let completions = completions.clone();
23850 counter.fetch_add(1, atomic::Ordering::Release);
23851 async move {
23852 assert_eq!(params.text_document_position.text_document.uri, url.clone());
23853 assert_eq!(
23854 params.text_document_position.position, complete_from_position,
23855 "marker `|` position doesn't match",
23856 );
23857 Ok(Some(lsp::CompletionResponse::Array(
23858 completions
23859 .iter()
23860 .map(|(label, new_text)| lsp::CompletionItem {
23861 label: label.to_string(),
23862 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
23863 lsp::InsertReplaceEdit {
23864 insert: insert_range,
23865 replace: replace_range,
23866 new_text: new_text.to_string(),
23867 },
23868 )),
23869 ..Default::default()
23870 })
23871 .collect(),
23872 )))
23873 }
23874 });
23875
23876 async move {
23877 request.next().await;
23878 }
23879}
23880
23881fn handle_resolve_completion_request(
23882 cx: &mut EditorLspTestContext,
23883 edits: Option<Vec<(&'static str, &'static str)>>,
23884) -> impl Future<Output = ()> {
23885 let edits = edits.map(|edits| {
23886 edits
23887 .iter()
23888 .map(|(marked_string, new_text)| {
23889 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
23890 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
23891 lsp::TextEdit::new(replace_range, new_text.to_string())
23892 })
23893 .collect::<Vec<_>>()
23894 });
23895
23896 let mut request =
23897 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
23898 let edits = edits.clone();
23899 async move {
23900 Ok(lsp::CompletionItem {
23901 additional_text_edits: edits,
23902 ..Default::default()
23903 })
23904 }
23905 });
23906
23907 async move {
23908 request.next().await;
23909 }
23910}
23911
23912pub(crate) fn update_test_language_settings(
23913 cx: &mut TestAppContext,
23914 f: impl Fn(&mut AllLanguageSettingsContent),
23915) {
23916 cx.update(|cx| {
23917 SettingsStore::update_global(cx, |store, cx| {
23918 store.update_user_settings::<AllLanguageSettings>(cx, f);
23919 });
23920 });
23921}
23922
23923pub(crate) fn update_test_project_settings(
23924 cx: &mut TestAppContext,
23925 f: impl Fn(&mut ProjectSettings),
23926) {
23927 cx.update(|cx| {
23928 SettingsStore::update_global(cx, |store, cx| {
23929 store.update_user_settings::<ProjectSettings>(cx, f);
23930 });
23931 });
23932}
23933
23934pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
23935 cx.update(|cx| {
23936 assets::Assets.load_test_fonts(cx);
23937 let store = SettingsStore::test(cx);
23938 cx.set_global(store);
23939 theme::init(theme::LoadThemes::JustBase, cx);
23940 release_channel::init(SemanticVersion::default(), cx);
23941 client::init_settings(cx);
23942 language::init(cx);
23943 Project::init_settings(cx);
23944 workspace::init_settings(cx);
23945 crate::init(cx);
23946 });
23947 zlog::init_test();
23948 update_test_language_settings(cx, f);
23949}
23950
23951#[track_caller]
23952fn assert_hunk_revert(
23953 not_reverted_text_with_selections: &str,
23954 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
23955 expected_reverted_text_with_selections: &str,
23956 base_text: &str,
23957 cx: &mut EditorLspTestContext,
23958) {
23959 cx.set_state(not_reverted_text_with_selections);
23960 cx.set_head_text(base_text);
23961 cx.executor().run_until_parked();
23962
23963 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
23964 let snapshot = editor.snapshot(window, cx);
23965 let reverted_hunk_statuses = snapshot
23966 .buffer_snapshot
23967 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
23968 .map(|hunk| hunk.status().kind)
23969 .collect::<Vec<_>>();
23970
23971 editor.git_restore(&Default::default(), window, cx);
23972 reverted_hunk_statuses
23973 });
23974 cx.executor().run_until_parked();
23975 cx.assert_editor_state(expected_reverted_text_with_selections);
23976 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
23977}
23978
23979#[gpui::test(iterations = 10)]
23980async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
23981 init_test(cx, |_| {});
23982
23983 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
23984 let counter = diagnostic_requests.clone();
23985
23986 let fs = FakeFs::new(cx.executor());
23987 fs.insert_tree(
23988 path!("/a"),
23989 json!({
23990 "first.rs": "fn main() { let a = 5; }",
23991 "second.rs": "// Test file",
23992 }),
23993 )
23994 .await;
23995
23996 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23997 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23998 let cx = &mut VisualTestContext::from_window(*workspace, cx);
23999
24000 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24001 language_registry.add(rust_lang());
24002 let mut fake_servers = language_registry.register_fake_lsp(
24003 "Rust",
24004 FakeLspAdapter {
24005 capabilities: lsp::ServerCapabilities {
24006 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
24007 lsp::DiagnosticOptions {
24008 identifier: None,
24009 inter_file_dependencies: true,
24010 workspace_diagnostics: true,
24011 work_done_progress_options: Default::default(),
24012 },
24013 )),
24014 ..Default::default()
24015 },
24016 ..Default::default()
24017 },
24018 );
24019
24020 let editor = workspace
24021 .update(cx, |workspace, window, cx| {
24022 workspace.open_abs_path(
24023 PathBuf::from(path!("/a/first.rs")),
24024 OpenOptions::default(),
24025 window,
24026 cx,
24027 )
24028 })
24029 .unwrap()
24030 .await
24031 .unwrap()
24032 .downcast::<Editor>()
24033 .unwrap();
24034 let fake_server = fake_servers.next().await.unwrap();
24035 let server_id = fake_server.server.server_id();
24036 let mut first_request = fake_server
24037 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
24038 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
24039 let result_id = Some(new_result_id.to_string());
24040 assert_eq!(
24041 params.text_document.uri,
24042 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
24043 );
24044 async move {
24045 Ok(lsp::DocumentDiagnosticReportResult::Report(
24046 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
24047 related_documents: None,
24048 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
24049 items: Vec::new(),
24050 result_id,
24051 },
24052 }),
24053 ))
24054 }
24055 });
24056
24057 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
24058 project.update(cx, |project, cx| {
24059 let buffer_id = editor
24060 .read(cx)
24061 .buffer()
24062 .read(cx)
24063 .as_singleton()
24064 .expect("created a singleton buffer")
24065 .read(cx)
24066 .remote_id();
24067 let buffer_result_id = project
24068 .lsp_store()
24069 .read(cx)
24070 .result_id(server_id, buffer_id, cx);
24071 assert_eq!(expected, buffer_result_id);
24072 });
24073 };
24074
24075 ensure_result_id(None, cx);
24076 cx.executor().advance_clock(Duration::from_millis(60));
24077 cx.executor().run_until_parked();
24078 assert_eq!(
24079 diagnostic_requests.load(atomic::Ordering::Acquire),
24080 1,
24081 "Opening file should trigger diagnostic request"
24082 );
24083 first_request
24084 .next()
24085 .await
24086 .expect("should have sent the first diagnostics pull request");
24087 ensure_result_id(Some("1".to_string()), cx);
24088
24089 // Editing should trigger diagnostics
24090 editor.update_in(cx, |editor, window, cx| {
24091 editor.handle_input("2", window, cx)
24092 });
24093 cx.executor().advance_clock(Duration::from_millis(60));
24094 cx.executor().run_until_parked();
24095 assert_eq!(
24096 diagnostic_requests.load(atomic::Ordering::Acquire),
24097 2,
24098 "Editing should trigger diagnostic request"
24099 );
24100 ensure_result_id(Some("2".to_string()), cx);
24101
24102 // Moving cursor should not trigger diagnostic request
24103 editor.update_in(cx, |editor, window, cx| {
24104 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
24105 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
24106 });
24107 });
24108 cx.executor().advance_clock(Duration::from_millis(60));
24109 cx.executor().run_until_parked();
24110 assert_eq!(
24111 diagnostic_requests.load(atomic::Ordering::Acquire),
24112 2,
24113 "Cursor movement should not trigger diagnostic request"
24114 );
24115 ensure_result_id(Some("2".to_string()), cx);
24116 // Multiple rapid edits should be debounced
24117 for _ in 0..5 {
24118 editor.update_in(cx, |editor, window, cx| {
24119 editor.handle_input("x", window, cx)
24120 });
24121 }
24122 cx.executor().advance_clock(Duration::from_millis(60));
24123 cx.executor().run_until_parked();
24124
24125 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
24126 assert!(
24127 final_requests <= 4,
24128 "Multiple rapid edits should be debounced (got {final_requests} requests)",
24129 );
24130 ensure_result_id(Some(final_requests.to_string()), cx);
24131}
24132
24133#[gpui::test]
24134async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
24135 // Regression test for issue #11671
24136 // Previously, adding a cursor after moving multiple cursors would reset
24137 // the cursor count instead of adding to the existing cursors.
24138 init_test(cx, |_| {});
24139 let mut cx = EditorTestContext::new(cx).await;
24140
24141 // Create a simple buffer with cursor at start
24142 cx.set_state(indoc! {"
24143 ˇaaaa
24144 bbbb
24145 cccc
24146 dddd
24147 eeee
24148 ffff
24149 gggg
24150 hhhh"});
24151
24152 // Add 2 cursors below (so we have 3 total)
24153 cx.update_editor(|editor, window, cx| {
24154 editor.add_selection_below(&Default::default(), window, cx);
24155 editor.add_selection_below(&Default::default(), window, cx);
24156 });
24157
24158 // Verify we have 3 cursors
24159 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
24160 assert_eq!(
24161 initial_count, 3,
24162 "Should have 3 cursors after adding 2 below"
24163 );
24164
24165 // Move down one line
24166 cx.update_editor(|editor, window, cx| {
24167 editor.move_down(&MoveDown, window, cx);
24168 });
24169
24170 // Add another cursor below
24171 cx.update_editor(|editor, window, cx| {
24172 editor.add_selection_below(&Default::default(), window, cx);
24173 });
24174
24175 // Should now have 4 cursors (3 original + 1 new)
24176 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
24177 assert_eq!(
24178 final_count, 4,
24179 "Should have 4 cursors after moving and adding another"
24180 );
24181}
24182
24183#[gpui::test(iterations = 10)]
24184async fn test_document_colors(cx: &mut TestAppContext) {
24185 let expected_color = Rgba {
24186 r: 0.33,
24187 g: 0.33,
24188 b: 0.33,
24189 a: 0.33,
24190 };
24191
24192 init_test(cx, |_| {});
24193
24194 let fs = FakeFs::new(cx.executor());
24195 fs.insert_tree(
24196 path!("/a"),
24197 json!({
24198 "first.rs": "fn main() { let a = 5; }",
24199 }),
24200 )
24201 .await;
24202
24203 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24204 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24205 let cx = &mut VisualTestContext::from_window(*workspace, cx);
24206
24207 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24208 language_registry.add(rust_lang());
24209 let mut fake_servers = language_registry.register_fake_lsp(
24210 "Rust",
24211 FakeLspAdapter {
24212 capabilities: lsp::ServerCapabilities {
24213 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
24214 ..lsp::ServerCapabilities::default()
24215 },
24216 name: "rust-analyzer",
24217 ..FakeLspAdapter::default()
24218 },
24219 );
24220 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
24221 "Rust",
24222 FakeLspAdapter {
24223 capabilities: lsp::ServerCapabilities {
24224 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
24225 ..lsp::ServerCapabilities::default()
24226 },
24227 name: "not-rust-analyzer",
24228 ..FakeLspAdapter::default()
24229 },
24230 );
24231
24232 let editor = workspace
24233 .update(cx, |workspace, window, cx| {
24234 workspace.open_abs_path(
24235 PathBuf::from(path!("/a/first.rs")),
24236 OpenOptions::default(),
24237 window,
24238 cx,
24239 )
24240 })
24241 .unwrap()
24242 .await
24243 .unwrap()
24244 .downcast::<Editor>()
24245 .unwrap();
24246 let fake_language_server = fake_servers.next().await.unwrap();
24247 let fake_language_server_without_capabilities =
24248 fake_servers_without_capabilities.next().await.unwrap();
24249 let requests_made = Arc::new(AtomicUsize::new(0));
24250 let closure_requests_made = Arc::clone(&requests_made);
24251 let mut color_request_handle = fake_language_server
24252 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
24253 let requests_made = Arc::clone(&closure_requests_made);
24254 async move {
24255 assert_eq!(
24256 params.text_document.uri,
24257 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
24258 );
24259 requests_made.fetch_add(1, atomic::Ordering::Release);
24260 Ok(vec![
24261 lsp::ColorInformation {
24262 range: lsp::Range {
24263 start: lsp::Position {
24264 line: 0,
24265 character: 0,
24266 },
24267 end: lsp::Position {
24268 line: 0,
24269 character: 1,
24270 },
24271 },
24272 color: lsp::Color {
24273 red: 0.33,
24274 green: 0.33,
24275 blue: 0.33,
24276 alpha: 0.33,
24277 },
24278 },
24279 lsp::ColorInformation {
24280 range: lsp::Range {
24281 start: lsp::Position {
24282 line: 0,
24283 character: 0,
24284 },
24285 end: lsp::Position {
24286 line: 0,
24287 character: 1,
24288 },
24289 },
24290 color: lsp::Color {
24291 red: 0.33,
24292 green: 0.33,
24293 blue: 0.33,
24294 alpha: 0.33,
24295 },
24296 },
24297 ])
24298 }
24299 });
24300
24301 let _handle = fake_language_server_without_capabilities
24302 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
24303 panic!("Should not be called");
24304 });
24305 cx.executor().advance_clock(Duration::from_millis(100));
24306 color_request_handle.next().await.unwrap();
24307 cx.run_until_parked();
24308 assert_eq!(
24309 1,
24310 requests_made.load(atomic::Ordering::Acquire),
24311 "Should query for colors once per editor open"
24312 );
24313 editor.update_in(cx, |editor, _, cx| {
24314 assert_eq!(
24315 vec![expected_color],
24316 extract_color_inlays(editor, cx),
24317 "Should have an initial inlay"
24318 );
24319 });
24320
24321 // opening another file in a split should not influence the LSP query counter
24322 workspace
24323 .update(cx, |workspace, window, cx| {
24324 assert_eq!(
24325 workspace.panes().len(),
24326 1,
24327 "Should have one pane with one editor"
24328 );
24329 workspace.move_item_to_pane_in_direction(
24330 &MoveItemToPaneInDirection {
24331 direction: SplitDirection::Right,
24332 focus: false,
24333 clone: true,
24334 },
24335 window,
24336 cx,
24337 );
24338 })
24339 .unwrap();
24340 cx.run_until_parked();
24341 workspace
24342 .update(cx, |workspace, _, cx| {
24343 let panes = workspace.panes();
24344 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
24345 for pane in panes {
24346 let editor = pane
24347 .read(cx)
24348 .active_item()
24349 .and_then(|item| item.downcast::<Editor>())
24350 .expect("Should have opened an editor in each split");
24351 let editor_file = editor
24352 .read(cx)
24353 .buffer()
24354 .read(cx)
24355 .as_singleton()
24356 .expect("test deals with singleton buffers")
24357 .read(cx)
24358 .file()
24359 .expect("test buffese should have a file")
24360 .path();
24361 assert_eq!(
24362 editor_file.as_ref(),
24363 Path::new("first.rs"),
24364 "Both editors should be opened for the same file"
24365 )
24366 }
24367 })
24368 .unwrap();
24369
24370 cx.executor().advance_clock(Duration::from_millis(500));
24371 let save = editor.update_in(cx, |editor, window, cx| {
24372 editor.move_to_end(&MoveToEnd, window, cx);
24373 editor.handle_input("dirty", window, cx);
24374 editor.save(
24375 SaveOptions {
24376 format: true,
24377 autosave: true,
24378 },
24379 project.clone(),
24380 window,
24381 cx,
24382 )
24383 });
24384 save.await.unwrap();
24385
24386 color_request_handle.next().await.unwrap();
24387 cx.run_until_parked();
24388 assert_eq!(
24389 3,
24390 requests_made.load(atomic::Ordering::Acquire),
24391 "Should query for colors once per save and once per formatting after save"
24392 );
24393
24394 drop(editor);
24395 let close = workspace
24396 .update(cx, |workspace, window, cx| {
24397 workspace.active_pane().update(cx, |pane, cx| {
24398 pane.close_active_item(&CloseActiveItem::default(), window, cx)
24399 })
24400 })
24401 .unwrap();
24402 close.await.unwrap();
24403 let close = workspace
24404 .update(cx, |workspace, window, cx| {
24405 workspace.active_pane().update(cx, |pane, cx| {
24406 pane.close_active_item(&CloseActiveItem::default(), window, cx)
24407 })
24408 })
24409 .unwrap();
24410 close.await.unwrap();
24411 assert_eq!(
24412 3,
24413 requests_made.load(atomic::Ordering::Acquire),
24414 "After saving and closing all editors, no extra requests should be made"
24415 );
24416 workspace
24417 .update(cx, |workspace, _, cx| {
24418 assert!(
24419 workspace.active_item(cx).is_none(),
24420 "Should close all editors"
24421 )
24422 })
24423 .unwrap();
24424
24425 workspace
24426 .update(cx, |workspace, window, cx| {
24427 workspace.active_pane().update(cx, |pane, cx| {
24428 pane.navigate_backward(&Default::default(), window, cx);
24429 })
24430 })
24431 .unwrap();
24432 cx.executor().advance_clock(Duration::from_millis(100));
24433 cx.run_until_parked();
24434 let editor = workspace
24435 .update(cx, |workspace, _, cx| {
24436 workspace
24437 .active_item(cx)
24438 .expect("Should have reopened the editor again after navigating back")
24439 .downcast::<Editor>()
24440 .expect("Should be an editor")
24441 })
24442 .unwrap();
24443 color_request_handle.next().await.unwrap();
24444 assert_eq!(
24445 3,
24446 requests_made.load(atomic::Ordering::Acquire),
24447 "Cache should be reused on buffer close and reopen"
24448 );
24449 editor.update(cx, |editor, cx| {
24450 assert_eq!(
24451 vec![expected_color],
24452 extract_color_inlays(editor, cx),
24453 "Should have an initial inlay"
24454 );
24455 });
24456}
24457
24458#[gpui::test]
24459async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
24460 init_test(cx, |_| {});
24461 let (editor, cx) = cx.add_window_view(Editor::single_line);
24462 editor.update_in(cx, |editor, window, cx| {
24463 editor.set_text("oops\n\nwow\n", window, cx)
24464 });
24465 cx.run_until_parked();
24466 editor.update(cx, |editor, cx| {
24467 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
24468 });
24469 editor.update(cx, |editor, cx| editor.edit([(3..5, "")], cx));
24470 cx.run_until_parked();
24471 editor.update(cx, |editor, cx| {
24472 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
24473 });
24474}
24475
24476#[gpui::test]
24477async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
24478 init_test(cx, |_| {});
24479
24480 cx.update(|cx| {
24481 register_project_item::<Editor>(cx);
24482 });
24483
24484 let fs = FakeFs::new(cx.executor());
24485 fs.insert_tree("/root1", json!({})).await;
24486 fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
24487 .await;
24488
24489 let project = Project::test(fs, ["/root1".as_ref()], cx).await;
24490 let (workspace, cx) =
24491 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24492
24493 let worktree_id = project.update(cx, |project, cx| {
24494 project.worktrees(cx).next().unwrap().read(cx).id()
24495 });
24496
24497 let handle = workspace
24498 .update_in(cx, |workspace, window, cx| {
24499 let project_path = (worktree_id, "one.pdf");
24500 workspace.open_path(project_path, None, true, window, cx)
24501 })
24502 .await
24503 .unwrap();
24504
24505 assert_eq!(
24506 handle.to_any().entity_type(),
24507 TypeId::of::<InvalidBufferView>()
24508 );
24509}
24510
24511#[track_caller]
24512fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
24513 editor
24514 .all_inlays(cx)
24515 .into_iter()
24516 .filter_map(|inlay| inlay.get_color())
24517 .map(Rgba::from)
24518 .collect()
24519}