1use super::*;
2use crate::{
3 JoinLines,
4 code_context_menus::CodeContextMenu,
5 edit_prediction_tests::FakeEditPredictionProvider,
6 linked_editing_ranges::LinkedEditingRanges,
7 scroll::scroll_amount::ScrollAmount,
8 test::{
9 assert_text_with_selections, build_editor,
10 editor_lsp_test_context::{EditorLspTestContext, git_commit_lang},
11 editor_test_context::EditorTestContext,
12 select_ranges,
13 },
14};
15use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
16use collections::HashMap;
17use futures::StreamExt;
18use gpui::{
19 BackgroundExecutor, DismissEvent, Rgba, SemanticVersion, TestAppContext, UpdateGlobal,
20 VisualTestContext, WindowBounds, WindowOptions, div,
21};
22use indoc::indoc;
23use language::{
24 BracketPairConfig,
25 Capability::ReadWrite,
26 DiagnosticSourceKind, FakeLspAdapter, IndentGuideSettings, LanguageConfig,
27 LanguageConfigOverride, LanguageMatcher, LanguageName, Override, Point,
28 language_settings::{
29 CompletionSettingsContent, FormatterList, LanguageSettingsContent, LspInsertMode,
30 SelectedFormatter,
31 },
32 tree_sitter_python,
33};
34use language_settings::Formatter;
35use lsp::CompletionParams;
36use multi_buffer::{IndentGuide, PathKey};
37use parking_lot::Mutex;
38use pretty_assertions::{assert_eq, assert_ne};
39use project::{
40 FakeFs,
41 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
42 project_settings::LspSettings,
43};
44use serde_json::{self, json};
45use settings::{
46 AllLanguageSettingsContent, IndentGuideBackgroundColoring, IndentGuideColoring,
47 ProjectSettingsContent,
48};
49use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
50use std::{
51 iter,
52 sync::atomic::{self, AtomicUsize},
53};
54use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
55use text::ToPoint as _;
56use unindent::Unindent;
57use util::{
58 assert_set_eq, path,
59 rel_path::rel_path,
60 test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
61 uri,
62};
63use workspace::{
64 CloseActiveItem, CloseAllItems, CloseOtherItems, MoveItemToPaneInDirection, NavigationEntry,
65 OpenOptions, ViewId,
66 invalid_buffer_view::InvalidBufferView,
67 item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
68 register_project_item,
69};
70
71#[gpui::test]
72fn test_edit_events(cx: &mut TestAppContext) {
73 init_test(cx, |_| {});
74
75 let buffer = cx.new(|cx| {
76 let mut buffer = language::Buffer::local("123456", cx);
77 buffer.set_group_interval(Duration::from_secs(1));
78 buffer
79 });
80
81 let events = Rc::new(RefCell::new(Vec::new()));
82 let editor1 = cx.add_window({
83 let events = events.clone();
84 |window, cx| {
85 let entity = cx.entity();
86 cx.subscribe_in(
87 &entity,
88 window,
89 move |_, _, event: &EditorEvent, _, _| match event {
90 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
91 EditorEvent::BufferEdited => {
92 events.borrow_mut().push(("editor1", "buffer edited"))
93 }
94 _ => {}
95 },
96 )
97 .detach();
98 Editor::for_buffer(buffer.clone(), None, window, cx)
99 }
100 });
101
102 let editor2 = cx.add_window({
103 let events = events.clone();
104 |window, cx| {
105 cx.subscribe_in(
106 &cx.entity(),
107 window,
108 move |_, _, event: &EditorEvent, _, _| match event {
109 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
110 EditorEvent::BufferEdited => {
111 events.borrow_mut().push(("editor2", "buffer edited"))
112 }
113 _ => {}
114 },
115 )
116 .detach();
117 Editor::for_buffer(buffer.clone(), None, window, cx)
118 }
119 });
120
121 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
122
123 // Mutating editor 1 will emit an `Edited` event only for that editor.
124 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
125 assert_eq!(
126 mem::take(&mut *events.borrow_mut()),
127 [
128 ("editor1", "edited"),
129 ("editor1", "buffer edited"),
130 ("editor2", "buffer edited"),
131 ]
132 );
133
134 // Mutating editor 2 will emit an `Edited` event only for that editor.
135 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
136 assert_eq!(
137 mem::take(&mut *events.borrow_mut()),
138 [
139 ("editor2", "edited"),
140 ("editor1", "buffer edited"),
141 ("editor2", "buffer edited"),
142 ]
143 );
144
145 // Undoing on editor 1 will emit an `Edited` event only for that editor.
146 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
147 assert_eq!(
148 mem::take(&mut *events.borrow_mut()),
149 [
150 ("editor1", "edited"),
151 ("editor1", "buffer edited"),
152 ("editor2", "buffer edited"),
153 ]
154 );
155
156 // Redoing on editor 1 will emit an `Edited` event only for that editor.
157 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
158 assert_eq!(
159 mem::take(&mut *events.borrow_mut()),
160 [
161 ("editor1", "edited"),
162 ("editor1", "buffer edited"),
163 ("editor2", "buffer edited"),
164 ]
165 );
166
167 // Undoing on editor 2 will emit an `Edited` event only for that editor.
168 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
169 assert_eq!(
170 mem::take(&mut *events.borrow_mut()),
171 [
172 ("editor2", "edited"),
173 ("editor1", "buffer edited"),
174 ("editor2", "buffer edited"),
175 ]
176 );
177
178 // Redoing on editor 2 will emit an `Edited` event only for that editor.
179 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
180 assert_eq!(
181 mem::take(&mut *events.borrow_mut()),
182 [
183 ("editor2", "edited"),
184 ("editor1", "buffer edited"),
185 ("editor2", "buffer edited"),
186 ]
187 );
188
189 // No event is emitted when the mutation is a no-op.
190 _ = editor2.update(cx, |editor, window, cx| {
191 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
192 s.select_ranges([0..0])
193 });
194
195 editor.backspace(&Backspace, window, cx);
196 });
197 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
198}
199
200#[gpui::test]
201fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
202 init_test(cx, |_| {});
203
204 let mut now = Instant::now();
205 let group_interval = Duration::from_millis(1);
206 let buffer = cx.new(|cx| {
207 let mut buf = language::Buffer::local("123456", cx);
208 buf.set_group_interval(group_interval);
209 buf
210 });
211 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
212 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
213
214 _ = editor.update(cx, |editor, window, cx| {
215 editor.start_transaction_at(now, window, cx);
216 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
217 s.select_ranges([2..4])
218 });
219
220 editor.insert("cd", window, cx);
221 editor.end_transaction_at(now, cx);
222 assert_eq!(editor.text(cx), "12cd56");
223 assert_eq!(editor.selections.ranges(cx), vec![4..4]);
224
225 editor.start_transaction_at(now, window, cx);
226 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
227 s.select_ranges([4..5])
228 });
229 editor.insert("e", window, cx);
230 editor.end_transaction_at(now, cx);
231 assert_eq!(editor.text(cx), "12cde6");
232 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
233
234 now += group_interval + Duration::from_millis(1);
235 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
236 s.select_ranges([2..2])
237 });
238
239 // Simulate an edit in another editor
240 buffer.update(cx, |buffer, cx| {
241 buffer.start_transaction_at(now, cx);
242 buffer.edit([(0..1, "a")], None, cx);
243 buffer.edit([(1..1, "b")], None, cx);
244 buffer.end_transaction_at(now, cx);
245 });
246
247 assert_eq!(editor.text(cx), "ab2cde6");
248 assert_eq!(editor.selections.ranges(cx), vec![3..3]);
249
250 // Last transaction happened past the group interval in a different editor.
251 // Undo it individually and don't restore selections.
252 editor.undo(&Undo, window, cx);
253 assert_eq!(editor.text(cx), "12cde6");
254 assert_eq!(editor.selections.ranges(cx), vec![2..2]);
255
256 // First two transactions happened within the group interval in this editor.
257 // Undo them together and restore selections.
258 editor.undo(&Undo, window, cx);
259 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
260 assert_eq!(editor.text(cx), "123456");
261 assert_eq!(editor.selections.ranges(cx), vec![0..0]);
262
263 // Redo the first two transactions together.
264 editor.redo(&Redo, window, cx);
265 assert_eq!(editor.text(cx), "12cde6");
266 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
267
268 // Redo the last transaction on its own.
269 editor.redo(&Redo, window, cx);
270 assert_eq!(editor.text(cx), "ab2cde6");
271 assert_eq!(editor.selections.ranges(cx), vec![6..6]);
272
273 // Test empty transactions.
274 editor.start_transaction_at(now, window, cx);
275 editor.end_transaction_at(now, cx);
276 editor.undo(&Undo, window, cx);
277 assert_eq!(editor.text(cx), "12cde6");
278 });
279}
280
281#[gpui::test]
282fn test_ime_composition(cx: &mut TestAppContext) {
283 init_test(cx, |_| {});
284
285 let buffer = cx.new(|cx| {
286 let mut buffer = language::Buffer::local("abcde", cx);
287 // Ensure automatic grouping doesn't occur.
288 buffer.set_group_interval(Duration::ZERO);
289 buffer
290 });
291
292 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
293 cx.add_window(|window, cx| {
294 let mut editor = build_editor(buffer.clone(), window, cx);
295
296 // Start a new IME composition.
297 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
298 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
299 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
300 assert_eq!(editor.text(cx), "äbcde");
301 assert_eq!(
302 editor.marked_text_ranges(cx),
303 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
304 );
305
306 // Finalize IME composition.
307 editor.replace_text_in_range(None, "ā", window, cx);
308 assert_eq!(editor.text(cx), "ābcde");
309 assert_eq!(editor.marked_text_ranges(cx), None);
310
311 // IME composition edits are grouped and are undone/redone at once.
312 editor.undo(&Default::default(), window, cx);
313 assert_eq!(editor.text(cx), "abcde");
314 assert_eq!(editor.marked_text_ranges(cx), None);
315 editor.redo(&Default::default(), window, cx);
316 assert_eq!(editor.text(cx), "ābcde");
317 assert_eq!(editor.marked_text_ranges(cx), None);
318
319 // Start a new IME composition.
320 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
321 assert_eq!(
322 editor.marked_text_ranges(cx),
323 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
324 );
325
326 // Undoing during an IME composition cancels it.
327 editor.undo(&Default::default(), window, cx);
328 assert_eq!(editor.text(cx), "ābcde");
329 assert_eq!(editor.marked_text_ranges(cx), None);
330
331 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
332 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
333 assert_eq!(editor.text(cx), "ābcdè");
334 assert_eq!(
335 editor.marked_text_ranges(cx),
336 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
337 );
338
339 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
340 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
341 assert_eq!(editor.text(cx), "ābcdę");
342 assert_eq!(editor.marked_text_ranges(cx), None);
343
344 // Start a new IME composition with multiple cursors.
345 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
346 s.select_ranges([
347 OffsetUtf16(1)..OffsetUtf16(1),
348 OffsetUtf16(3)..OffsetUtf16(3),
349 OffsetUtf16(5)..OffsetUtf16(5),
350 ])
351 });
352 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
353 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
354 assert_eq!(
355 editor.marked_text_ranges(cx),
356 Some(vec![
357 OffsetUtf16(0)..OffsetUtf16(3),
358 OffsetUtf16(4)..OffsetUtf16(7),
359 OffsetUtf16(8)..OffsetUtf16(11)
360 ])
361 );
362
363 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
364 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
365 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
366 assert_eq!(
367 editor.marked_text_ranges(cx),
368 Some(vec![
369 OffsetUtf16(1)..OffsetUtf16(2),
370 OffsetUtf16(5)..OffsetUtf16(6),
371 OffsetUtf16(9)..OffsetUtf16(10)
372 ])
373 );
374
375 // Finalize IME composition with multiple cursors.
376 editor.replace_text_in_range(Some(9..10), "2", window, cx);
377 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
378 assert_eq!(editor.marked_text_ranges(cx), None);
379
380 editor
381 });
382}
383
384#[gpui::test]
385fn test_selection_with_mouse(cx: &mut TestAppContext) {
386 init_test(cx, |_| {});
387
388 let editor = cx.add_window(|window, cx| {
389 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
390 build_editor(buffer, window, cx)
391 });
392
393 _ = editor.update(cx, |editor, window, cx| {
394 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
395 });
396 assert_eq!(
397 editor
398 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
399 .unwrap(),
400 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
401 );
402
403 _ = editor.update(cx, |editor, window, cx| {
404 editor.update_selection(
405 DisplayPoint::new(DisplayRow(3), 3),
406 0,
407 gpui::Point::<f32>::default(),
408 window,
409 cx,
410 );
411 });
412
413 assert_eq!(
414 editor
415 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
416 .unwrap(),
417 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
418 );
419
420 _ = editor.update(cx, |editor, window, cx| {
421 editor.update_selection(
422 DisplayPoint::new(DisplayRow(1), 1),
423 0,
424 gpui::Point::<f32>::default(),
425 window,
426 cx,
427 );
428 });
429
430 assert_eq!(
431 editor
432 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
433 .unwrap(),
434 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
435 );
436
437 _ = editor.update(cx, |editor, window, cx| {
438 editor.end_selection(window, cx);
439 editor.update_selection(
440 DisplayPoint::new(DisplayRow(3), 3),
441 0,
442 gpui::Point::<f32>::default(),
443 window,
444 cx,
445 );
446 });
447
448 assert_eq!(
449 editor
450 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
451 .unwrap(),
452 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
453 );
454
455 _ = editor.update(cx, |editor, window, cx| {
456 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
457 editor.update_selection(
458 DisplayPoint::new(DisplayRow(0), 0),
459 0,
460 gpui::Point::<f32>::default(),
461 window,
462 cx,
463 );
464 });
465
466 assert_eq!(
467 editor
468 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
469 .unwrap(),
470 [
471 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
472 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
473 ]
474 );
475
476 _ = editor.update(cx, |editor, window, cx| {
477 editor.end_selection(window, cx);
478 });
479
480 assert_eq!(
481 editor
482 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
483 .unwrap(),
484 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
485 );
486}
487
488#[gpui::test]
489fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
490 init_test(cx, |_| {});
491
492 let editor = cx.add_window(|window, cx| {
493 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
494 build_editor(buffer, window, cx)
495 });
496
497 _ = editor.update(cx, |editor, window, cx| {
498 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
499 });
500
501 _ = editor.update(cx, |editor, window, cx| {
502 editor.end_selection(window, cx);
503 });
504
505 _ = editor.update(cx, |editor, window, cx| {
506 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
507 });
508
509 _ = editor.update(cx, |editor, window, cx| {
510 editor.end_selection(window, cx);
511 });
512
513 assert_eq!(
514 editor
515 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
516 .unwrap(),
517 [
518 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
519 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
520 ]
521 );
522
523 _ = editor.update(cx, |editor, window, cx| {
524 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
525 });
526
527 _ = editor.update(cx, |editor, window, cx| {
528 editor.end_selection(window, cx);
529 });
530
531 assert_eq!(
532 editor
533 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
534 .unwrap(),
535 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
536 );
537}
538
539#[gpui::test]
540fn test_canceling_pending_selection(cx: &mut TestAppContext) {
541 init_test(cx, |_| {});
542
543 let editor = cx.add_window(|window, cx| {
544 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
545 build_editor(buffer, window, cx)
546 });
547
548 _ = editor.update(cx, |editor, window, cx| {
549 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
550 assert_eq!(
551 editor.selections.display_ranges(cx),
552 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
553 );
554 });
555
556 _ = editor.update(cx, |editor, window, cx| {
557 editor.update_selection(
558 DisplayPoint::new(DisplayRow(3), 3),
559 0,
560 gpui::Point::<f32>::default(),
561 window,
562 cx,
563 );
564 assert_eq!(
565 editor.selections.display_ranges(cx),
566 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
567 );
568 });
569
570 _ = editor.update(cx, |editor, window, cx| {
571 editor.cancel(&Cancel, window, cx);
572 editor.update_selection(
573 DisplayPoint::new(DisplayRow(1), 1),
574 0,
575 gpui::Point::<f32>::default(),
576 window,
577 cx,
578 );
579 assert_eq!(
580 editor.selections.display_ranges(cx),
581 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
582 );
583 });
584}
585
586#[gpui::test]
587fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
588 init_test(cx, |_| {});
589
590 let editor = cx.add_window(|window, cx| {
591 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
592 build_editor(buffer, window, cx)
593 });
594
595 _ = editor.update(cx, |editor, window, cx| {
596 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
597 assert_eq!(
598 editor.selections.display_ranges(cx),
599 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
600 );
601
602 editor.move_down(&Default::default(), window, cx);
603 assert_eq!(
604 editor.selections.display_ranges(cx),
605 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
606 );
607
608 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
609 assert_eq!(
610 editor.selections.display_ranges(cx),
611 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
612 );
613
614 editor.move_up(&Default::default(), window, cx);
615 assert_eq!(
616 editor.selections.display_ranges(cx),
617 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
618 );
619 });
620}
621
622#[gpui::test]
623fn test_clone(cx: &mut TestAppContext) {
624 init_test(cx, |_| {});
625
626 let (text, selection_ranges) = marked_text_ranges(
627 indoc! {"
628 one
629 two
630 threeˇ
631 four
632 fiveˇ
633 "},
634 true,
635 );
636
637 let editor = cx.add_window(|window, cx| {
638 let buffer = MultiBuffer::build_simple(&text, cx);
639 build_editor(buffer, window, cx)
640 });
641
642 _ = editor.update(cx, |editor, window, cx| {
643 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
644 s.select_ranges(selection_ranges.clone())
645 });
646 editor.fold_creases(
647 vec![
648 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
649 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
650 ],
651 true,
652 window,
653 cx,
654 );
655 });
656
657 let cloned_editor = editor
658 .update(cx, |editor, _, cx| {
659 cx.open_window(Default::default(), |window, cx| {
660 cx.new(|cx| editor.clone(window, cx))
661 })
662 })
663 .unwrap()
664 .unwrap();
665
666 let snapshot = editor
667 .update(cx, |e, window, cx| e.snapshot(window, cx))
668 .unwrap();
669 let cloned_snapshot = cloned_editor
670 .update(cx, |e, window, cx| e.snapshot(window, cx))
671 .unwrap();
672
673 assert_eq!(
674 cloned_editor
675 .update(cx, |e, _, cx| e.display_text(cx))
676 .unwrap(),
677 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
678 );
679 assert_eq!(
680 cloned_snapshot
681 .folds_in_range(0..text.len())
682 .collect::<Vec<_>>(),
683 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
684 );
685 assert_set_eq!(
686 cloned_editor
687 .update(cx, |editor, _, cx| editor.selections.ranges::<Point>(cx))
688 .unwrap(),
689 editor
690 .update(cx, |editor, _, cx| editor.selections.ranges(cx))
691 .unwrap()
692 );
693 assert_set_eq!(
694 cloned_editor
695 .update(cx, |e, _window, cx| e.selections.display_ranges(cx))
696 .unwrap(),
697 editor
698 .update(cx, |e, _, cx| e.selections.display_ranges(cx))
699 .unwrap()
700 );
701}
702
703#[gpui::test]
704async fn test_navigation_history(cx: &mut TestAppContext) {
705 init_test(cx, |_| {});
706
707 use workspace::item::Item;
708
709 let fs = FakeFs::new(cx.executor());
710 let project = Project::test(fs, [], cx).await;
711 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
712 let pane = workspace
713 .update(cx, |workspace, _, _| workspace.active_pane().clone())
714 .unwrap();
715
716 _ = workspace.update(cx, |_v, window, cx| {
717 cx.new(|cx| {
718 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
719 let mut editor = build_editor(buffer, window, cx);
720 let handle = cx.entity();
721 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
722
723 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
724 editor.nav_history.as_mut().unwrap().pop_backward(cx)
725 }
726
727 // Move the cursor a small distance.
728 // Nothing is added to the navigation history.
729 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
730 s.select_display_ranges([
731 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
732 ])
733 });
734 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
735 s.select_display_ranges([
736 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
737 ])
738 });
739 assert!(pop_history(&mut editor, cx).is_none());
740
741 // Move the cursor a large distance.
742 // The history can jump back to the previous position.
743 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
744 s.select_display_ranges([
745 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
746 ])
747 });
748 let nav_entry = pop_history(&mut editor, cx).unwrap();
749 editor.navigate(nav_entry.data.unwrap(), window, cx);
750 assert_eq!(nav_entry.item.id(), cx.entity_id());
751 assert_eq!(
752 editor.selections.display_ranges(cx),
753 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
754 );
755 assert!(pop_history(&mut editor, cx).is_none());
756
757 // Move the cursor a small distance via the mouse.
758 // Nothing is added to the navigation history.
759 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
760 editor.end_selection(window, cx);
761 assert_eq!(
762 editor.selections.display_ranges(cx),
763 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
764 );
765 assert!(pop_history(&mut editor, cx).is_none());
766
767 // Move the cursor a large distance via the mouse.
768 // The history can jump back to the previous position.
769 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
770 editor.end_selection(window, cx);
771 assert_eq!(
772 editor.selections.display_ranges(cx),
773 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
774 );
775 let nav_entry = pop_history(&mut editor, cx).unwrap();
776 editor.navigate(nav_entry.data.unwrap(), window, cx);
777 assert_eq!(nav_entry.item.id(), cx.entity_id());
778 assert_eq!(
779 editor.selections.display_ranges(cx),
780 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
781 );
782 assert!(pop_history(&mut editor, cx).is_none());
783
784 // Set scroll position to check later
785 editor.set_scroll_position(gpui::Point::<f32>::new(5.5, 5.5), window, cx);
786 let original_scroll_position = editor.scroll_manager.anchor();
787
788 // Jump to the end of the document and adjust scroll
789 editor.move_to_end(&MoveToEnd, window, cx);
790 editor.set_scroll_position(gpui::Point::<f32>::new(-2.5, -0.5), window, cx);
791 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
792
793 let nav_entry = pop_history(&mut editor, cx).unwrap();
794 editor.navigate(nav_entry.data.unwrap(), window, cx);
795 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
796
797 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
798 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
799 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
800 let invalid_point = Point::new(9999, 0);
801 editor.navigate(
802 Box::new(NavigationData {
803 cursor_anchor: invalid_anchor,
804 cursor_position: invalid_point,
805 scroll_anchor: ScrollAnchor {
806 anchor: invalid_anchor,
807 offset: Default::default(),
808 },
809 scroll_top_row: invalid_point.row,
810 }),
811 window,
812 cx,
813 );
814 assert_eq!(
815 editor.selections.display_ranges(cx),
816 &[editor.max_point(cx)..editor.max_point(cx)]
817 );
818 assert_eq!(
819 editor.scroll_position(cx),
820 gpui::Point::new(0., editor.max_point(cx).row().as_f32())
821 );
822
823 editor
824 })
825 });
826}
827
828#[gpui::test]
829fn test_cancel(cx: &mut TestAppContext) {
830 init_test(cx, |_| {});
831
832 let editor = cx.add_window(|window, cx| {
833 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
834 build_editor(buffer, window, cx)
835 });
836
837 _ = editor.update(cx, |editor, window, cx| {
838 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
839 editor.update_selection(
840 DisplayPoint::new(DisplayRow(1), 1),
841 0,
842 gpui::Point::<f32>::default(),
843 window,
844 cx,
845 );
846 editor.end_selection(window, cx);
847
848 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
849 editor.update_selection(
850 DisplayPoint::new(DisplayRow(0), 3),
851 0,
852 gpui::Point::<f32>::default(),
853 window,
854 cx,
855 );
856 editor.end_selection(window, cx);
857 assert_eq!(
858 editor.selections.display_ranges(cx),
859 [
860 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
861 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
862 ]
863 );
864 });
865
866 _ = editor.update(cx, |editor, window, cx| {
867 editor.cancel(&Cancel, window, cx);
868 assert_eq!(
869 editor.selections.display_ranges(cx),
870 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
871 );
872 });
873
874 _ = editor.update(cx, |editor, window, cx| {
875 editor.cancel(&Cancel, window, cx);
876 assert_eq!(
877 editor.selections.display_ranges(cx),
878 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
879 );
880 });
881}
882
883#[gpui::test]
884fn test_fold_action(cx: &mut TestAppContext) {
885 init_test(cx, |_| {});
886
887 let editor = cx.add_window(|window, cx| {
888 let buffer = MultiBuffer::build_simple(
889 &"
890 impl Foo {
891 // Hello!
892
893 fn a() {
894 1
895 }
896
897 fn b() {
898 2
899 }
900
901 fn c() {
902 3
903 }
904 }
905 "
906 .unindent(),
907 cx,
908 );
909 build_editor(buffer, window, cx)
910 });
911
912 _ = editor.update(cx, |editor, window, cx| {
913 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
914 s.select_display_ranges([
915 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
916 ]);
917 });
918 editor.fold(&Fold, window, cx);
919 assert_eq!(
920 editor.display_text(cx),
921 "
922 impl Foo {
923 // Hello!
924
925 fn a() {
926 1
927 }
928
929 fn b() {⋯
930 }
931
932 fn c() {⋯
933 }
934 }
935 "
936 .unindent(),
937 );
938
939 editor.fold(&Fold, window, cx);
940 assert_eq!(
941 editor.display_text(cx),
942 "
943 impl Foo {⋯
944 }
945 "
946 .unindent(),
947 );
948
949 editor.unfold_lines(&UnfoldLines, window, cx);
950 assert_eq!(
951 editor.display_text(cx),
952 "
953 impl Foo {
954 // Hello!
955
956 fn a() {
957 1
958 }
959
960 fn b() {⋯
961 }
962
963 fn c() {⋯
964 }
965 }
966 "
967 .unindent(),
968 );
969
970 editor.unfold_lines(&UnfoldLines, window, cx);
971 assert_eq!(
972 editor.display_text(cx),
973 editor.buffer.read(cx).read(cx).text()
974 );
975 });
976}
977
978#[gpui::test]
979fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
980 init_test(cx, |_| {});
981
982 let editor = cx.add_window(|window, cx| {
983 let buffer = MultiBuffer::build_simple(
984 &"
985 class Foo:
986 # Hello!
987
988 def a():
989 print(1)
990
991 def b():
992 print(2)
993
994 def c():
995 print(3)
996 "
997 .unindent(),
998 cx,
999 );
1000 build_editor(buffer, window, cx)
1001 });
1002
1003 _ = editor.update(cx, |editor, window, cx| {
1004 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1005 s.select_display_ranges([
1006 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
1007 ]);
1008 });
1009 editor.fold(&Fold, window, cx);
1010 assert_eq!(
1011 editor.display_text(cx),
1012 "
1013 class Foo:
1014 # Hello!
1015
1016 def a():
1017 print(1)
1018
1019 def b():⋯
1020
1021 def c():⋯
1022 "
1023 .unindent(),
1024 );
1025
1026 editor.fold(&Fold, window, cx);
1027 assert_eq!(
1028 editor.display_text(cx),
1029 "
1030 class Foo:⋯
1031 "
1032 .unindent(),
1033 );
1034
1035 editor.unfold_lines(&UnfoldLines, window, cx);
1036 assert_eq!(
1037 editor.display_text(cx),
1038 "
1039 class Foo:
1040 # Hello!
1041
1042 def a():
1043 print(1)
1044
1045 def b():⋯
1046
1047 def c():⋯
1048 "
1049 .unindent(),
1050 );
1051
1052 editor.unfold_lines(&UnfoldLines, window, cx);
1053 assert_eq!(
1054 editor.display_text(cx),
1055 editor.buffer.read(cx).read(cx).text()
1056 );
1057 });
1058}
1059
1060#[gpui::test]
1061fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1062 init_test(cx, |_| {});
1063
1064 let editor = cx.add_window(|window, cx| {
1065 let buffer = MultiBuffer::build_simple(
1066 &"
1067 class Foo:
1068 # Hello!
1069
1070 def a():
1071 print(1)
1072
1073 def b():
1074 print(2)
1075
1076
1077 def c():
1078 print(3)
1079
1080
1081 "
1082 .unindent(),
1083 cx,
1084 );
1085 build_editor(buffer, window, cx)
1086 });
1087
1088 _ = editor.update(cx, |editor, window, cx| {
1089 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1090 s.select_display_ranges([
1091 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1092 ]);
1093 });
1094 editor.fold(&Fold, window, cx);
1095 assert_eq!(
1096 editor.display_text(cx),
1097 "
1098 class Foo:
1099 # Hello!
1100
1101 def a():
1102 print(1)
1103
1104 def b():⋯
1105
1106
1107 def c():⋯
1108
1109
1110 "
1111 .unindent(),
1112 );
1113
1114 editor.fold(&Fold, window, cx);
1115 assert_eq!(
1116 editor.display_text(cx),
1117 "
1118 class Foo:⋯
1119
1120
1121 "
1122 .unindent(),
1123 );
1124
1125 editor.unfold_lines(&UnfoldLines, window, cx);
1126 assert_eq!(
1127 editor.display_text(cx),
1128 "
1129 class Foo:
1130 # Hello!
1131
1132 def a():
1133 print(1)
1134
1135 def b():⋯
1136
1137
1138 def c():⋯
1139
1140
1141 "
1142 .unindent(),
1143 );
1144
1145 editor.unfold_lines(&UnfoldLines, window, cx);
1146 assert_eq!(
1147 editor.display_text(cx),
1148 editor.buffer.read(cx).read(cx).text()
1149 );
1150 });
1151}
1152
1153#[gpui::test]
1154fn test_fold_at_level(cx: &mut TestAppContext) {
1155 init_test(cx, |_| {});
1156
1157 let editor = cx.add_window(|window, cx| {
1158 let buffer = MultiBuffer::build_simple(
1159 &"
1160 class Foo:
1161 # Hello!
1162
1163 def a():
1164 print(1)
1165
1166 def b():
1167 print(2)
1168
1169
1170 class Bar:
1171 # World!
1172
1173 def a():
1174 print(1)
1175
1176 def b():
1177 print(2)
1178
1179
1180 "
1181 .unindent(),
1182 cx,
1183 );
1184 build_editor(buffer, window, cx)
1185 });
1186
1187 _ = editor.update(cx, |editor, window, cx| {
1188 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1189 assert_eq!(
1190 editor.display_text(cx),
1191 "
1192 class Foo:
1193 # Hello!
1194
1195 def a():⋯
1196
1197 def b():⋯
1198
1199
1200 class Bar:
1201 # World!
1202
1203 def a():⋯
1204
1205 def b():⋯
1206
1207
1208 "
1209 .unindent(),
1210 );
1211
1212 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1213 assert_eq!(
1214 editor.display_text(cx),
1215 "
1216 class Foo:⋯
1217
1218
1219 class Bar:⋯
1220
1221
1222 "
1223 .unindent(),
1224 );
1225
1226 editor.unfold_all(&UnfoldAll, window, cx);
1227 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1228 assert_eq!(
1229 editor.display_text(cx),
1230 "
1231 class Foo:
1232 # Hello!
1233
1234 def a():
1235 print(1)
1236
1237 def b():
1238 print(2)
1239
1240
1241 class Bar:
1242 # World!
1243
1244 def a():
1245 print(1)
1246
1247 def b():
1248 print(2)
1249
1250
1251 "
1252 .unindent(),
1253 );
1254
1255 assert_eq!(
1256 editor.display_text(cx),
1257 editor.buffer.read(cx).read(cx).text()
1258 );
1259 });
1260}
1261
1262#[gpui::test]
1263fn test_move_cursor(cx: &mut TestAppContext) {
1264 init_test(cx, |_| {});
1265
1266 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1267 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1268
1269 buffer.update(cx, |buffer, cx| {
1270 buffer.edit(
1271 vec![
1272 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1273 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1274 ],
1275 None,
1276 cx,
1277 );
1278 });
1279 _ = editor.update(cx, |editor, window, cx| {
1280 assert_eq!(
1281 editor.selections.display_ranges(cx),
1282 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1283 );
1284
1285 editor.move_down(&MoveDown, window, cx);
1286 assert_eq!(
1287 editor.selections.display_ranges(cx),
1288 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1289 );
1290
1291 editor.move_right(&MoveRight, window, cx);
1292 assert_eq!(
1293 editor.selections.display_ranges(cx),
1294 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1295 );
1296
1297 editor.move_left(&MoveLeft, window, cx);
1298 assert_eq!(
1299 editor.selections.display_ranges(cx),
1300 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1301 );
1302
1303 editor.move_up(&MoveUp, window, cx);
1304 assert_eq!(
1305 editor.selections.display_ranges(cx),
1306 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1307 );
1308
1309 editor.move_to_end(&MoveToEnd, window, cx);
1310 assert_eq!(
1311 editor.selections.display_ranges(cx),
1312 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1313 );
1314
1315 editor.move_to_beginning(&MoveToBeginning, window, cx);
1316 assert_eq!(
1317 editor.selections.display_ranges(cx),
1318 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1319 );
1320
1321 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1322 s.select_display_ranges([
1323 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1324 ]);
1325 });
1326 editor.select_to_beginning(&SelectToBeginning, window, cx);
1327 assert_eq!(
1328 editor.selections.display_ranges(cx),
1329 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1330 );
1331
1332 editor.select_to_end(&SelectToEnd, window, cx);
1333 assert_eq!(
1334 editor.selections.display_ranges(cx),
1335 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1336 );
1337 });
1338}
1339
1340#[gpui::test]
1341fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1342 init_test(cx, |_| {});
1343
1344 let editor = cx.add_window(|window, cx| {
1345 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1346 build_editor(buffer, window, cx)
1347 });
1348
1349 assert_eq!('🟥'.len_utf8(), 4);
1350 assert_eq!('α'.len_utf8(), 2);
1351
1352 _ = editor.update(cx, |editor, window, cx| {
1353 editor.fold_creases(
1354 vec![
1355 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1356 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1357 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1358 ],
1359 true,
1360 window,
1361 cx,
1362 );
1363 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1364
1365 editor.move_right(&MoveRight, window, cx);
1366 assert_eq!(
1367 editor.selections.display_ranges(cx),
1368 &[empty_range(0, "🟥".len())]
1369 );
1370 editor.move_right(&MoveRight, window, cx);
1371 assert_eq!(
1372 editor.selections.display_ranges(cx),
1373 &[empty_range(0, "🟥🟧".len())]
1374 );
1375 editor.move_right(&MoveRight, window, cx);
1376 assert_eq!(
1377 editor.selections.display_ranges(cx),
1378 &[empty_range(0, "🟥🟧⋯".len())]
1379 );
1380
1381 editor.move_down(&MoveDown, window, cx);
1382 assert_eq!(
1383 editor.selections.display_ranges(cx),
1384 &[empty_range(1, "ab⋯e".len())]
1385 );
1386 editor.move_left(&MoveLeft, window, cx);
1387 assert_eq!(
1388 editor.selections.display_ranges(cx),
1389 &[empty_range(1, "ab⋯".len())]
1390 );
1391 editor.move_left(&MoveLeft, window, cx);
1392 assert_eq!(
1393 editor.selections.display_ranges(cx),
1394 &[empty_range(1, "ab".len())]
1395 );
1396 editor.move_left(&MoveLeft, window, cx);
1397 assert_eq!(
1398 editor.selections.display_ranges(cx),
1399 &[empty_range(1, "a".len())]
1400 );
1401
1402 editor.move_down(&MoveDown, window, cx);
1403 assert_eq!(
1404 editor.selections.display_ranges(cx),
1405 &[empty_range(2, "α".len())]
1406 );
1407 editor.move_right(&MoveRight, window, cx);
1408 assert_eq!(
1409 editor.selections.display_ranges(cx),
1410 &[empty_range(2, "αβ".len())]
1411 );
1412 editor.move_right(&MoveRight, window, cx);
1413 assert_eq!(
1414 editor.selections.display_ranges(cx),
1415 &[empty_range(2, "αβ⋯".len())]
1416 );
1417 editor.move_right(&MoveRight, window, cx);
1418 assert_eq!(
1419 editor.selections.display_ranges(cx),
1420 &[empty_range(2, "αβ⋯ε".len())]
1421 );
1422
1423 editor.move_up(&MoveUp, window, cx);
1424 assert_eq!(
1425 editor.selections.display_ranges(cx),
1426 &[empty_range(1, "ab⋯e".len())]
1427 );
1428 editor.move_down(&MoveDown, window, cx);
1429 assert_eq!(
1430 editor.selections.display_ranges(cx),
1431 &[empty_range(2, "αβ⋯ε".len())]
1432 );
1433 editor.move_up(&MoveUp, window, cx);
1434 assert_eq!(
1435 editor.selections.display_ranges(cx),
1436 &[empty_range(1, "ab⋯e".len())]
1437 );
1438
1439 editor.move_up(&MoveUp, window, cx);
1440 assert_eq!(
1441 editor.selections.display_ranges(cx),
1442 &[empty_range(0, "🟥🟧".len())]
1443 );
1444 editor.move_left(&MoveLeft, window, cx);
1445 assert_eq!(
1446 editor.selections.display_ranges(cx),
1447 &[empty_range(0, "🟥".len())]
1448 );
1449 editor.move_left(&MoveLeft, window, cx);
1450 assert_eq!(
1451 editor.selections.display_ranges(cx),
1452 &[empty_range(0, "".len())]
1453 );
1454 });
1455}
1456
1457#[gpui::test]
1458fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1459 init_test(cx, |_| {});
1460
1461 let editor = cx.add_window(|window, cx| {
1462 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1463 build_editor(buffer, window, cx)
1464 });
1465 _ = editor.update(cx, |editor, window, cx| {
1466 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1467 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1468 });
1469
1470 // moving above start of document should move selection to start of document,
1471 // but the next move down should still be at the original goal_x
1472 editor.move_up(&MoveUp, window, cx);
1473 assert_eq!(
1474 editor.selections.display_ranges(cx),
1475 &[empty_range(0, "".len())]
1476 );
1477
1478 editor.move_down(&MoveDown, window, cx);
1479 assert_eq!(
1480 editor.selections.display_ranges(cx),
1481 &[empty_range(1, "abcd".len())]
1482 );
1483
1484 editor.move_down(&MoveDown, window, cx);
1485 assert_eq!(
1486 editor.selections.display_ranges(cx),
1487 &[empty_range(2, "αβγ".len())]
1488 );
1489
1490 editor.move_down(&MoveDown, window, cx);
1491 assert_eq!(
1492 editor.selections.display_ranges(cx),
1493 &[empty_range(3, "abcd".len())]
1494 );
1495
1496 editor.move_down(&MoveDown, window, cx);
1497 assert_eq!(
1498 editor.selections.display_ranges(cx),
1499 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1500 );
1501
1502 // moving past end of document should not change goal_x
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_down(&MoveDown, window, cx);
1510 assert_eq!(
1511 editor.selections.display_ranges(cx),
1512 &[empty_range(5, "".len())]
1513 );
1514
1515 editor.move_up(&MoveUp, window, cx);
1516 assert_eq!(
1517 editor.selections.display_ranges(cx),
1518 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1519 );
1520
1521 editor.move_up(&MoveUp, window, cx);
1522 assert_eq!(
1523 editor.selections.display_ranges(cx),
1524 &[empty_range(3, "abcd".len())]
1525 );
1526
1527 editor.move_up(&MoveUp, window, cx);
1528 assert_eq!(
1529 editor.selections.display_ranges(cx),
1530 &[empty_range(2, "αβγ".len())]
1531 );
1532 });
1533}
1534
1535#[gpui::test]
1536fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1537 init_test(cx, |_| {});
1538 let move_to_beg = MoveToBeginningOfLine {
1539 stop_at_soft_wraps: true,
1540 stop_at_indent: true,
1541 };
1542
1543 let delete_to_beg = DeleteToBeginningOfLine {
1544 stop_at_indent: false,
1545 };
1546
1547 let move_to_end = MoveToEndOfLine {
1548 stop_at_soft_wraps: true,
1549 };
1550
1551 let editor = cx.add_window(|window, cx| {
1552 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1553 build_editor(buffer, window, cx)
1554 });
1555 _ = editor.update(cx, |editor, window, cx| {
1556 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1557 s.select_display_ranges([
1558 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1559 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1560 ]);
1561 });
1562 });
1563
1564 _ = editor.update(cx, |editor, window, cx| {
1565 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1566 assert_eq!(
1567 editor.selections.display_ranges(cx),
1568 &[
1569 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1570 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1571 ]
1572 );
1573 });
1574
1575 _ = editor.update(cx, |editor, window, cx| {
1576 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1577 assert_eq!(
1578 editor.selections.display_ranges(cx),
1579 &[
1580 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1581 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1582 ]
1583 );
1584 });
1585
1586 _ = editor.update(cx, |editor, window, cx| {
1587 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1588 assert_eq!(
1589 editor.selections.display_ranges(cx),
1590 &[
1591 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1592 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1593 ]
1594 );
1595 });
1596
1597 _ = editor.update(cx, |editor, window, cx| {
1598 editor.move_to_end_of_line(&move_to_end, window, cx);
1599 assert_eq!(
1600 editor.selections.display_ranges(cx),
1601 &[
1602 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1603 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1604 ]
1605 );
1606 });
1607
1608 // Moving to the end of line again is a no-op.
1609 _ = editor.update(cx, |editor, window, cx| {
1610 editor.move_to_end_of_line(&move_to_end, window, cx);
1611 assert_eq!(
1612 editor.selections.display_ranges(cx),
1613 &[
1614 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1615 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1616 ]
1617 );
1618 });
1619
1620 _ = editor.update(cx, |editor, window, cx| {
1621 editor.move_left(&MoveLeft, window, cx);
1622 editor.select_to_beginning_of_line(
1623 &SelectToBeginningOfLine {
1624 stop_at_soft_wraps: true,
1625 stop_at_indent: true,
1626 },
1627 window,
1628 cx,
1629 );
1630 assert_eq!(
1631 editor.selections.display_ranges(cx),
1632 &[
1633 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1634 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1635 ]
1636 );
1637 });
1638
1639 _ = editor.update(cx, |editor, window, cx| {
1640 editor.select_to_beginning_of_line(
1641 &SelectToBeginningOfLine {
1642 stop_at_soft_wraps: true,
1643 stop_at_indent: true,
1644 },
1645 window,
1646 cx,
1647 );
1648 assert_eq!(
1649 editor.selections.display_ranges(cx),
1650 &[
1651 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1652 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1653 ]
1654 );
1655 });
1656
1657 _ = editor.update(cx, |editor, window, cx| {
1658 editor.select_to_beginning_of_line(
1659 &SelectToBeginningOfLine {
1660 stop_at_soft_wraps: true,
1661 stop_at_indent: true,
1662 },
1663 window,
1664 cx,
1665 );
1666 assert_eq!(
1667 editor.selections.display_ranges(cx),
1668 &[
1669 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1670 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1671 ]
1672 );
1673 });
1674
1675 _ = editor.update(cx, |editor, window, cx| {
1676 editor.select_to_end_of_line(
1677 &SelectToEndOfLine {
1678 stop_at_soft_wraps: true,
1679 },
1680 window,
1681 cx,
1682 );
1683 assert_eq!(
1684 editor.selections.display_ranges(cx),
1685 &[
1686 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1687 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1688 ]
1689 );
1690 });
1691
1692 _ = editor.update(cx, |editor, window, cx| {
1693 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1694 assert_eq!(editor.display_text(cx), "ab\n de");
1695 assert_eq!(
1696 editor.selections.display_ranges(cx),
1697 &[
1698 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1699 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1700 ]
1701 );
1702 });
1703
1704 _ = editor.update(cx, |editor, window, cx| {
1705 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1706 assert_eq!(editor.display_text(cx), "\n");
1707 assert_eq!(
1708 editor.selections.display_ranges(cx),
1709 &[
1710 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1711 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1712 ]
1713 );
1714 });
1715}
1716
1717#[gpui::test]
1718fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1719 init_test(cx, |_| {});
1720 let move_to_beg = MoveToBeginningOfLine {
1721 stop_at_soft_wraps: false,
1722 stop_at_indent: false,
1723 };
1724
1725 let move_to_end = MoveToEndOfLine {
1726 stop_at_soft_wraps: false,
1727 };
1728
1729 let editor = cx.add_window(|window, cx| {
1730 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1731 build_editor(buffer, window, cx)
1732 });
1733
1734 _ = editor.update(cx, |editor, window, cx| {
1735 editor.set_wrap_width(Some(140.0.into()), cx);
1736
1737 // We expect the following lines after wrapping
1738 // ```
1739 // thequickbrownfox
1740 // jumpedoverthelazydo
1741 // gs
1742 // ```
1743 // The final `gs` was soft-wrapped onto a new line.
1744 assert_eq!(
1745 "thequickbrownfox\njumpedoverthelaz\nydogs",
1746 editor.display_text(cx),
1747 );
1748
1749 // First, let's assert behavior on the first line, that was not soft-wrapped.
1750 // Start the cursor at the `k` on the first line
1751 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1752 s.select_display_ranges([
1753 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1754 ]);
1755 });
1756
1757 // Moving to the beginning of the line should put us at the beginning of the line.
1758 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1759 assert_eq!(
1760 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1761 editor.selections.display_ranges(cx)
1762 );
1763
1764 // Moving to the end of the line should put us at the end of the line.
1765 editor.move_to_end_of_line(&move_to_end, window, cx);
1766 assert_eq!(
1767 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1768 editor.selections.display_ranges(cx)
1769 );
1770
1771 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1772 // Start the cursor at the last line (`y` that was wrapped to a new line)
1773 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1774 s.select_display_ranges([
1775 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1776 ]);
1777 });
1778
1779 // Moving to the beginning of the line should put us at the start of the second line of
1780 // display text, i.e., the `j`.
1781 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1782 assert_eq!(
1783 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1784 editor.selections.display_ranges(cx)
1785 );
1786
1787 // Moving to the beginning of the line again should be a no-op.
1788 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1789 assert_eq!(
1790 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1791 editor.selections.display_ranges(cx)
1792 );
1793
1794 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1795 // next display line.
1796 editor.move_to_end_of_line(&move_to_end, window, cx);
1797 assert_eq!(
1798 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1799 editor.selections.display_ranges(cx)
1800 );
1801
1802 // Moving to the end of the line again should be a no-op.
1803 editor.move_to_end_of_line(&move_to_end, window, cx);
1804 assert_eq!(
1805 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1806 editor.selections.display_ranges(cx)
1807 );
1808 });
1809}
1810
1811#[gpui::test]
1812fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1813 init_test(cx, |_| {});
1814
1815 let move_to_beg = MoveToBeginningOfLine {
1816 stop_at_soft_wraps: true,
1817 stop_at_indent: true,
1818 };
1819
1820 let select_to_beg = SelectToBeginningOfLine {
1821 stop_at_soft_wraps: true,
1822 stop_at_indent: true,
1823 };
1824
1825 let delete_to_beg = DeleteToBeginningOfLine {
1826 stop_at_indent: true,
1827 };
1828
1829 let move_to_end = MoveToEndOfLine {
1830 stop_at_soft_wraps: false,
1831 };
1832
1833 let editor = cx.add_window(|window, cx| {
1834 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1835 build_editor(buffer, window, cx)
1836 });
1837
1838 _ = editor.update(cx, |editor, window, cx| {
1839 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1840 s.select_display_ranges([
1841 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1842 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1843 ]);
1844 });
1845
1846 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1847 // and the second cursor at the first non-whitespace character in the line.
1848 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1849 assert_eq!(
1850 editor.selections.display_ranges(cx),
1851 &[
1852 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1853 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1854 ]
1855 );
1856
1857 // Moving to the beginning of the line again should be a no-op for the first cursor,
1858 // and should move the second cursor to the beginning of the line.
1859 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1860 assert_eq!(
1861 editor.selections.display_ranges(cx),
1862 &[
1863 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1864 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1865 ]
1866 );
1867
1868 // Moving to the beginning of the line again should still be a no-op for the first cursor,
1869 // and should move the second cursor back to the first non-whitespace character in the line.
1870 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1871 assert_eq!(
1872 editor.selections.display_ranges(cx),
1873 &[
1874 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1875 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1876 ]
1877 );
1878
1879 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
1880 // and to the first non-whitespace character in the line for the second cursor.
1881 editor.move_to_end_of_line(&move_to_end, window, cx);
1882 editor.move_left(&MoveLeft, window, cx);
1883 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1884 assert_eq!(
1885 editor.selections.display_ranges(cx),
1886 &[
1887 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1888 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1889 ]
1890 );
1891
1892 // Selecting to the beginning of the line again should be a no-op for the first cursor,
1893 // and should select to the beginning of the line for the second cursor.
1894 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1895 assert_eq!(
1896 editor.selections.display_ranges(cx),
1897 &[
1898 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1899 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1900 ]
1901 );
1902
1903 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
1904 // and should delete to the first non-whitespace character in the line for the second cursor.
1905 editor.move_to_end_of_line(&move_to_end, window, cx);
1906 editor.move_left(&MoveLeft, window, cx);
1907 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1908 assert_eq!(editor.text(cx), "c\n f");
1909 });
1910}
1911
1912#[gpui::test]
1913fn test_beginning_of_line_with_cursor_between_line_start_and_indent(cx: &mut TestAppContext) {
1914 init_test(cx, |_| {});
1915
1916 let move_to_beg = MoveToBeginningOfLine {
1917 stop_at_soft_wraps: true,
1918 stop_at_indent: true,
1919 };
1920
1921 let editor = cx.add_window(|window, cx| {
1922 let buffer = MultiBuffer::build_simple(" hello\nworld", cx);
1923 build_editor(buffer, window, cx)
1924 });
1925
1926 _ = editor.update(cx, |editor, window, cx| {
1927 // test cursor between line_start and indent_start
1928 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1929 s.select_display_ranges([
1930 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3)
1931 ]);
1932 });
1933
1934 // cursor should move to line_start
1935 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1936 assert_eq!(
1937 editor.selections.display_ranges(cx),
1938 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1939 );
1940
1941 // cursor should move to indent_start
1942 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1943 assert_eq!(
1944 editor.selections.display_ranges(cx),
1945 &[DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 4)]
1946 );
1947
1948 // cursor should move to back to line_start
1949 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1950 assert_eq!(
1951 editor.selections.display_ranges(cx),
1952 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1953 );
1954 });
1955}
1956
1957#[gpui::test]
1958fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
1959 init_test(cx, |_| {});
1960
1961 let editor = cx.add_window(|window, cx| {
1962 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
1963 build_editor(buffer, window, cx)
1964 });
1965 _ = editor.update(cx, |editor, window, cx| {
1966 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1967 s.select_display_ranges([
1968 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
1969 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
1970 ])
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_previous_word_start(&MoveToPreviousWordStart, window, cx);
1982 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1983
1984 editor.move_to_previous_word_start(&MoveToPreviousWordStart, 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_to_next_word_end(&MoveToNextWordEnd, window, cx);
1991 assert_selection_ranges("use stdˇ::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
1992
1993 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1994 assert_selection_ranges("use std::ˇstr::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1995
1996 editor.move_right(&MoveRight, window, cx);
1997 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1998 assert_selection_ranges(
1999 "use std::«ˇs»tr::{foo, bar}\n«ˇ\n» {baz.qux()}",
2000 editor,
2001 cx,
2002 );
2003
2004 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
2005 assert_selection_ranges(
2006 "use std«ˇ::s»tr::{foo, bar«ˇ}\n\n» {baz.qux()}",
2007 editor,
2008 cx,
2009 );
2010
2011 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
2012 assert_selection_ranges(
2013 "use std::«ˇs»tr::{foo, bar}«ˇ\n\n» {baz.qux()}",
2014 editor,
2015 cx,
2016 );
2017 });
2018}
2019
2020#[gpui::test]
2021fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
2022 init_test(cx, |_| {});
2023
2024 let editor = cx.add_window(|window, cx| {
2025 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
2026 build_editor(buffer, window, cx)
2027 });
2028
2029 _ = editor.update(cx, |editor, window, cx| {
2030 editor.set_wrap_width(Some(140.0.into()), cx);
2031 assert_eq!(
2032 editor.display_text(cx),
2033 "use one::{\n two::three::\n four::five\n};"
2034 );
2035
2036 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2037 s.select_display_ranges([
2038 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
2039 ]);
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), 9)..DisplayPoint::new(DisplayRow(1), 9)]
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(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
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), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2058 );
2059
2060 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2061 assert_eq!(
2062 editor.selections.display_ranges(cx),
2063 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
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(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2070 );
2071
2072 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2073 assert_eq!(
2074 editor.selections.display_ranges(cx),
2075 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2076 );
2077 });
2078}
2079
2080#[gpui::test]
2081async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2082 init_test(cx, |_| {});
2083 let mut cx = EditorTestContext::new(cx).await;
2084
2085 let line_height = cx.editor(|editor, window, _| {
2086 editor
2087 .style()
2088 .unwrap()
2089 .text
2090 .line_height_in_pixels(window.rem_size())
2091 });
2092 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2093
2094 cx.set_state(
2095 &r#"ˇone
2096 two
2097
2098 three
2099 fourˇ
2100 five
2101
2102 six"#
2103 .unindent(),
2104 );
2105
2106 cx.update_editor(|editor, window, cx| {
2107 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2108 });
2109 cx.assert_editor_state(
2110 &r#"one
2111 two
2112 ˇ
2113 three
2114 four
2115 five
2116 ˇ
2117 six"#
2118 .unindent(),
2119 );
2120
2121 cx.update_editor(|editor, window, cx| {
2122 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2123 });
2124 cx.assert_editor_state(
2125 &r#"one
2126 two
2127
2128 three
2129 four
2130 five
2131 ˇ
2132 sixˇ"#
2133 .unindent(),
2134 );
2135
2136 cx.update_editor(|editor, window, cx| {
2137 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2138 });
2139 cx.assert_editor_state(
2140 &r#"one
2141 two
2142
2143 three
2144 four
2145 five
2146
2147 sixˇ"#
2148 .unindent(),
2149 );
2150
2151 cx.update_editor(|editor, window, cx| {
2152 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2153 });
2154 cx.assert_editor_state(
2155 &r#"one
2156 two
2157
2158 three
2159 four
2160 five
2161 ˇ
2162 six"#
2163 .unindent(),
2164 );
2165
2166 cx.update_editor(|editor, window, cx| {
2167 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2168 });
2169 cx.assert_editor_state(
2170 &r#"one
2171 two
2172 ˇ
2173 three
2174 four
2175 five
2176
2177 six"#
2178 .unindent(),
2179 );
2180
2181 cx.update_editor(|editor, window, cx| {
2182 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2183 });
2184 cx.assert_editor_state(
2185 &r#"ˇone
2186 two
2187
2188 three
2189 four
2190 five
2191
2192 six"#
2193 .unindent(),
2194 );
2195}
2196
2197#[gpui::test]
2198async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2199 init_test(cx, |_| {});
2200 let mut cx = EditorTestContext::new(cx).await;
2201 let line_height = cx.editor(|editor, window, _| {
2202 editor
2203 .style()
2204 .unwrap()
2205 .text
2206 .line_height_in_pixels(window.rem_size())
2207 });
2208 let window = cx.window;
2209 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2210
2211 cx.set_state(
2212 r#"ˇone
2213 two
2214 three
2215 four
2216 five
2217 six
2218 seven
2219 eight
2220 nine
2221 ten
2222 "#,
2223 );
2224
2225 cx.update_editor(|editor, window, cx| {
2226 assert_eq!(
2227 editor.snapshot(window, cx).scroll_position(),
2228 gpui::Point::new(0., 0.)
2229 );
2230 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2231 assert_eq!(
2232 editor.snapshot(window, cx).scroll_position(),
2233 gpui::Point::new(0., 3.)
2234 );
2235 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2236 assert_eq!(
2237 editor.snapshot(window, cx).scroll_position(),
2238 gpui::Point::new(0., 6.)
2239 );
2240 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2241 assert_eq!(
2242 editor.snapshot(window, cx).scroll_position(),
2243 gpui::Point::new(0., 3.)
2244 );
2245
2246 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2247 assert_eq!(
2248 editor.snapshot(window, cx).scroll_position(),
2249 gpui::Point::new(0., 1.)
2250 );
2251 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2252 assert_eq!(
2253 editor.snapshot(window, cx).scroll_position(),
2254 gpui::Point::new(0., 3.)
2255 );
2256 });
2257}
2258
2259#[gpui::test]
2260async fn test_autoscroll(cx: &mut TestAppContext) {
2261 init_test(cx, |_| {});
2262 let mut cx = EditorTestContext::new(cx).await;
2263
2264 let line_height = cx.update_editor(|editor, window, cx| {
2265 editor.set_vertical_scroll_margin(2, cx);
2266 editor
2267 .style()
2268 .unwrap()
2269 .text
2270 .line_height_in_pixels(window.rem_size())
2271 });
2272 let window = cx.window;
2273 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2274
2275 cx.set_state(
2276 r#"ˇone
2277 two
2278 three
2279 four
2280 five
2281 six
2282 seven
2283 eight
2284 nine
2285 ten
2286 "#,
2287 );
2288 cx.update_editor(|editor, window, cx| {
2289 assert_eq!(
2290 editor.snapshot(window, cx).scroll_position(),
2291 gpui::Point::new(0., 0.0)
2292 );
2293 });
2294
2295 // Add a cursor below the visible area. Since both cursors cannot fit
2296 // on screen, the editor autoscrolls to reveal the newest cursor, and
2297 // allows the vertical scroll margin below that cursor.
2298 cx.update_editor(|editor, window, cx| {
2299 editor.change_selections(Default::default(), window, cx, |selections| {
2300 selections.select_ranges([
2301 Point::new(0, 0)..Point::new(0, 0),
2302 Point::new(6, 0)..Point::new(6, 0),
2303 ]);
2304 })
2305 });
2306 cx.update_editor(|editor, window, cx| {
2307 assert_eq!(
2308 editor.snapshot(window, cx).scroll_position(),
2309 gpui::Point::new(0., 3.0)
2310 );
2311 });
2312
2313 // Move down. The editor cursor scrolls down to track the newest cursor.
2314 cx.update_editor(|editor, window, cx| {
2315 editor.move_down(&Default::default(), window, cx);
2316 });
2317 cx.update_editor(|editor, window, cx| {
2318 assert_eq!(
2319 editor.snapshot(window, cx).scroll_position(),
2320 gpui::Point::new(0., 4.0)
2321 );
2322 });
2323
2324 // Add a cursor above the visible area. Since both cursors fit on screen,
2325 // the editor scrolls to show both.
2326 cx.update_editor(|editor, window, cx| {
2327 editor.change_selections(Default::default(), window, cx, |selections| {
2328 selections.select_ranges([
2329 Point::new(1, 0)..Point::new(1, 0),
2330 Point::new(6, 0)..Point::new(6, 0),
2331 ]);
2332 })
2333 });
2334 cx.update_editor(|editor, window, cx| {
2335 assert_eq!(
2336 editor.snapshot(window, cx).scroll_position(),
2337 gpui::Point::new(0., 1.0)
2338 );
2339 });
2340}
2341
2342#[gpui::test]
2343async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2344 init_test(cx, |_| {});
2345 let mut cx = EditorTestContext::new(cx).await;
2346
2347 let line_height = cx.editor(|editor, window, _cx| {
2348 editor
2349 .style()
2350 .unwrap()
2351 .text
2352 .line_height_in_pixels(window.rem_size())
2353 });
2354 let window = cx.window;
2355 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2356 cx.set_state(
2357 &r#"
2358 ˇone
2359 two
2360 threeˇ
2361 four
2362 five
2363 six
2364 seven
2365 eight
2366 nine
2367 ten
2368 "#
2369 .unindent(),
2370 );
2371
2372 cx.update_editor(|editor, window, cx| {
2373 editor.move_page_down(&MovePageDown::default(), window, cx)
2374 });
2375 cx.assert_editor_state(
2376 &r#"
2377 one
2378 two
2379 three
2380 ˇfour
2381 five
2382 sixˇ
2383 seven
2384 eight
2385 nine
2386 ten
2387 "#
2388 .unindent(),
2389 );
2390
2391 cx.update_editor(|editor, window, cx| {
2392 editor.move_page_down(&MovePageDown::default(), window, cx)
2393 });
2394 cx.assert_editor_state(
2395 &r#"
2396 one
2397 two
2398 three
2399 four
2400 five
2401 six
2402 ˇseven
2403 eight
2404 nineˇ
2405 ten
2406 "#
2407 .unindent(),
2408 );
2409
2410 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2411 cx.assert_editor_state(
2412 &r#"
2413 one
2414 two
2415 three
2416 ˇfour
2417 five
2418 sixˇ
2419 seven
2420 eight
2421 nine
2422 ten
2423 "#
2424 .unindent(),
2425 );
2426
2427 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2428 cx.assert_editor_state(
2429 &r#"
2430 ˇone
2431 two
2432 threeˇ
2433 four
2434 five
2435 six
2436 seven
2437 eight
2438 nine
2439 ten
2440 "#
2441 .unindent(),
2442 );
2443
2444 // Test select collapsing
2445 cx.update_editor(|editor, window, cx| {
2446 editor.move_page_down(&MovePageDown::default(), window, cx);
2447 editor.move_page_down(&MovePageDown::default(), window, cx);
2448 editor.move_page_down(&MovePageDown::default(), window, cx);
2449 });
2450 cx.assert_editor_state(
2451 &r#"
2452 one
2453 two
2454 three
2455 four
2456 five
2457 six
2458 seven
2459 eight
2460 nine
2461 ˇten
2462 ˇ"#
2463 .unindent(),
2464 );
2465}
2466
2467#[gpui::test]
2468async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2469 init_test(cx, |_| {});
2470 let mut cx = EditorTestContext::new(cx).await;
2471 cx.set_state("one «two threeˇ» four");
2472 cx.update_editor(|editor, window, cx| {
2473 editor.delete_to_beginning_of_line(
2474 &DeleteToBeginningOfLine {
2475 stop_at_indent: false,
2476 },
2477 window,
2478 cx,
2479 );
2480 assert_eq!(editor.text(cx), " four");
2481 });
2482}
2483
2484#[gpui::test]
2485async fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2486 init_test(cx, |_| {});
2487
2488 let mut cx = EditorTestContext::new(cx).await;
2489
2490 // For an empty selection, the preceding word fragment is deleted.
2491 // For non-empty selections, only selected characters are deleted.
2492 cx.set_state("onˇe two t«hreˇ»e four");
2493 cx.update_editor(|editor, window, cx| {
2494 editor.delete_to_previous_word_start(
2495 &DeleteToPreviousWordStart {
2496 ignore_newlines: false,
2497 ignore_brackets: false,
2498 },
2499 window,
2500 cx,
2501 );
2502 });
2503 cx.assert_editor_state("ˇe two tˇe four");
2504
2505 cx.set_state("e tˇwo te «fˇ»our");
2506 cx.update_editor(|editor, window, cx| {
2507 editor.delete_to_next_word_end(
2508 &DeleteToNextWordEnd {
2509 ignore_newlines: false,
2510 ignore_brackets: false,
2511 },
2512 window,
2513 cx,
2514 );
2515 });
2516 cx.assert_editor_state("e tˇ te ˇour");
2517}
2518
2519#[gpui::test]
2520async fn test_delete_whitespaces(cx: &mut TestAppContext) {
2521 init_test(cx, |_| {});
2522
2523 let mut cx = EditorTestContext::new(cx).await;
2524
2525 cx.set_state("here is some text ˇwith a space");
2526 cx.update_editor(|editor, window, cx| {
2527 editor.delete_to_previous_word_start(
2528 &DeleteToPreviousWordStart {
2529 ignore_newlines: false,
2530 ignore_brackets: true,
2531 },
2532 window,
2533 cx,
2534 );
2535 });
2536 // Continuous whitespace sequences are removed entirely, words behind them are not affected by the deletion action.
2537 cx.assert_editor_state("here is some textˇwith a space");
2538
2539 cx.set_state("here is some text ˇwith a space");
2540 cx.update_editor(|editor, window, cx| {
2541 editor.delete_to_previous_word_start(
2542 &DeleteToPreviousWordStart {
2543 ignore_newlines: false,
2544 ignore_brackets: false,
2545 },
2546 window,
2547 cx,
2548 );
2549 });
2550 cx.assert_editor_state("here is some textˇwith a space");
2551
2552 cx.set_state("here is some textˇ with a space");
2553 cx.update_editor(|editor, window, cx| {
2554 editor.delete_to_next_word_end(
2555 &DeleteToNextWordEnd {
2556 ignore_newlines: false,
2557 ignore_brackets: true,
2558 },
2559 window,
2560 cx,
2561 );
2562 });
2563 // Same happens in the other direction.
2564 cx.assert_editor_state("here is some textˇwith a space");
2565
2566 cx.set_state("here is some textˇ with a space");
2567 cx.update_editor(|editor, window, cx| {
2568 editor.delete_to_next_word_end(
2569 &DeleteToNextWordEnd {
2570 ignore_newlines: false,
2571 ignore_brackets: false,
2572 },
2573 window,
2574 cx,
2575 );
2576 });
2577 cx.assert_editor_state("here is some textˇwith a space");
2578
2579 cx.set_state("here is some textˇ with a space");
2580 cx.update_editor(|editor, window, cx| {
2581 editor.delete_to_next_word_end(
2582 &DeleteToNextWordEnd {
2583 ignore_newlines: true,
2584 ignore_brackets: false,
2585 },
2586 window,
2587 cx,
2588 );
2589 });
2590 cx.assert_editor_state("here is some textˇwith a space");
2591 cx.update_editor(|editor, window, cx| {
2592 editor.delete_to_previous_word_start(
2593 &DeleteToPreviousWordStart {
2594 ignore_newlines: true,
2595 ignore_brackets: false,
2596 },
2597 window,
2598 cx,
2599 );
2600 });
2601 cx.assert_editor_state("here is some ˇwith a space");
2602 cx.update_editor(|editor, window, cx| {
2603 editor.delete_to_previous_word_start(
2604 &DeleteToPreviousWordStart {
2605 ignore_newlines: true,
2606 ignore_brackets: false,
2607 },
2608 window,
2609 cx,
2610 );
2611 });
2612 // Single whitespaces are removed with the word behind them.
2613 cx.assert_editor_state("here is ˇwith a space");
2614 cx.update_editor(|editor, window, cx| {
2615 editor.delete_to_previous_word_start(
2616 &DeleteToPreviousWordStart {
2617 ignore_newlines: true,
2618 ignore_brackets: false,
2619 },
2620 window,
2621 cx,
2622 );
2623 });
2624 cx.assert_editor_state("here ˇwith a space");
2625 cx.update_editor(|editor, window, cx| {
2626 editor.delete_to_previous_word_start(
2627 &DeleteToPreviousWordStart {
2628 ignore_newlines: true,
2629 ignore_brackets: false,
2630 },
2631 window,
2632 cx,
2633 );
2634 });
2635 cx.assert_editor_state("ˇwith a space");
2636 cx.update_editor(|editor, window, cx| {
2637 editor.delete_to_previous_word_start(
2638 &DeleteToPreviousWordStart {
2639 ignore_newlines: true,
2640 ignore_brackets: false,
2641 },
2642 window,
2643 cx,
2644 );
2645 });
2646 cx.assert_editor_state("ˇwith a space");
2647 cx.update_editor(|editor, window, cx| {
2648 editor.delete_to_next_word_end(
2649 &DeleteToNextWordEnd {
2650 ignore_newlines: true,
2651 ignore_brackets: false,
2652 },
2653 window,
2654 cx,
2655 );
2656 });
2657 // Same happens in the other direction.
2658 cx.assert_editor_state("ˇ a space");
2659 cx.update_editor(|editor, window, cx| {
2660 editor.delete_to_next_word_end(
2661 &DeleteToNextWordEnd {
2662 ignore_newlines: true,
2663 ignore_brackets: false,
2664 },
2665 window,
2666 cx,
2667 );
2668 });
2669 cx.assert_editor_state("ˇ space");
2670 cx.update_editor(|editor, window, cx| {
2671 editor.delete_to_next_word_end(
2672 &DeleteToNextWordEnd {
2673 ignore_newlines: true,
2674 ignore_brackets: false,
2675 },
2676 window,
2677 cx,
2678 );
2679 });
2680 cx.assert_editor_state("ˇ");
2681 cx.update_editor(|editor, window, cx| {
2682 editor.delete_to_next_word_end(
2683 &DeleteToNextWordEnd {
2684 ignore_newlines: true,
2685 ignore_brackets: false,
2686 },
2687 window,
2688 cx,
2689 );
2690 });
2691 cx.assert_editor_state("ˇ");
2692 cx.update_editor(|editor, window, cx| {
2693 editor.delete_to_previous_word_start(
2694 &DeleteToPreviousWordStart {
2695 ignore_newlines: true,
2696 ignore_brackets: false,
2697 },
2698 window,
2699 cx,
2700 );
2701 });
2702 cx.assert_editor_state("ˇ");
2703}
2704
2705#[gpui::test]
2706async fn test_delete_to_bracket(cx: &mut TestAppContext) {
2707 init_test(cx, |_| {});
2708
2709 let language = Arc::new(
2710 Language::new(
2711 LanguageConfig {
2712 brackets: BracketPairConfig {
2713 pairs: vec![
2714 BracketPair {
2715 start: "\"".to_string(),
2716 end: "\"".to_string(),
2717 close: true,
2718 surround: true,
2719 newline: false,
2720 },
2721 BracketPair {
2722 start: "(".to_string(),
2723 end: ")".to_string(),
2724 close: true,
2725 surround: true,
2726 newline: true,
2727 },
2728 ],
2729 ..BracketPairConfig::default()
2730 },
2731 ..LanguageConfig::default()
2732 },
2733 Some(tree_sitter_rust::LANGUAGE.into()),
2734 )
2735 .with_brackets_query(
2736 r#"
2737 ("(" @open ")" @close)
2738 ("\"" @open "\"" @close)
2739 "#,
2740 )
2741 .unwrap(),
2742 );
2743
2744 let mut cx = EditorTestContext::new(cx).await;
2745 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2746
2747 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2748 cx.update_editor(|editor, window, cx| {
2749 editor.delete_to_previous_word_start(
2750 &DeleteToPreviousWordStart {
2751 ignore_newlines: true,
2752 ignore_brackets: false,
2753 },
2754 window,
2755 cx,
2756 );
2757 });
2758 // Deletion stops before brackets if asked to not ignore them.
2759 cx.assert_editor_state(r#"macro!("ˇCOMMENT");"#);
2760 cx.update_editor(|editor, window, cx| {
2761 editor.delete_to_previous_word_start(
2762 &DeleteToPreviousWordStart {
2763 ignore_newlines: true,
2764 ignore_brackets: false,
2765 },
2766 window,
2767 cx,
2768 );
2769 });
2770 // Deletion has to remove a single bracket and then stop again.
2771 cx.assert_editor_state(r#"macro!(ˇCOMMENT");"#);
2772
2773 cx.update_editor(|editor, window, cx| {
2774 editor.delete_to_previous_word_start(
2775 &DeleteToPreviousWordStart {
2776 ignore_newlines: true,
2777 ignore_brackets: false,
2778 },
2779 window,
2780 cx,
2781 );
2782 });
2783 cx.assert_editor_state(r#"macro!ˇCOMMENT");"#);
2784
2785 cx.update_editor(|editor, window, cx| {
2786 editor.delete_to_previous_word_start(
2787 &DeleteToPreviousWordStart {
2788 ignore_newlines: true,
2789 ignore_brackets: false,
2790 },
2791 window,
2792 cx,
2793 );
2794 });
2795 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2796
2797 cx.update_editor(|editor, window, cx| {
2798 editor.delete_to_previous_word_start(
2799 &DeleteToPreviousWordStart {
2800 ignore_newlines: true,
2801 ignore_brackets: false,
2802 },
2803 window,
2804 cx,
2805 );
2806 });
2807 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2808
2809 cx.update_editor(|editor, window, cx| {
2810 editor.delete_to_next_word_end(
2811 &DeleteToNextWordEnd {
2812 ignore_newlines: true,
2813 ignore_brackets: false,
2814 },
2815 window,
2816 cx,
2817 );
2818 });
2819 // Brackets on the right are not paired anymore, hence deletion does not stop at them
2820 cx.assert_editor_state(r#"ˇ");"#);
2821
2822 cx.update_editor(|editor, window, cx| {
2823 editor.delete_to_next_word_end(
2824 &DeleteToNextWordEnd {
2825 ignore_newlines: true,
2826 ignore_brackets: false,
2827 },
2828 window,
2829 cx,
2830 );
2831 });
2832 cx.assert_editor_state(r#"ˇ"#);
2833
2834 cx.update_editor(|editor, window, cx| {
2835 editor.delete_to_next_word_end(
2836 &DeleteToNextWordEnd {
2837 ignore_newlines: true,
2838 ignore_brackets: false,
2839 },
2840 window,
2841 cx,
2842 );
2843 });
2844 cx.assert_editor_state(r#"ˇ"#);
2845
2846 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2847 cx.update_editor(|editor, window, cx| {
2848 editor.delete_to_previous_word_start(
2849 &DeleteToPreviousWordStart {
2850 ignore_newlines: true,
2851 ignore_brackets: true,
2852 },
2853 window,
2854 cx,
2855 );
2856 });
2857 cx.assert_editor_state(r#"macroˇCOMMENT");"#);
2858}
2859
2860#[gpui::test]
2861fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2862 init_test(cx, |_| {});
2863
2864 let editor = cx.add_window(|window, cx| {
2865 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2866 build_editor(buffer, window, cx)
2867 });
2868 let del_to_prev_word_start = DeleteToPreviousWordStart {
2869 ignore_newlines: false,
2870 ignore_brackets: false,
2871 };
2872 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2873 ignore_newlines: true,
2874 ignore_brackets: false,
2875 };
2876
2877 _ = editor.update(cx, |editor, window, cx| {
2878 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2879 s.select_display_ranges([
2880 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2881 ])
2882 });
2883 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2884 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
2885 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2886 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
2887 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2888 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
2889 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2890 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
2891 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2892 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
2893 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2894 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2895 });
2896}
2897
2898#[gpui::test]
2899fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
2900 init_test(cx, |_| {});
2901
2902 let editor = cx.add_window(|window, cx| {
2903 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
2904 build_editor(buffer, window, cx)
2905 });
2906 let del_to_next_word_end = DeleteToNextWordEnd {
2907 ignore_newlines: false,
2908 ignore_brackets: false,
2909 };
2910 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
2911 ignore_newlines: true,
2912 ignore_brackets: false,
2913 };
2914
2915 _ = editor.update(cx, |editor, window, cx| {
2916 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2917 s.select_display_ranges([
2918 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2919 ])
2920 });
2921 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2922 assert_eq!(
2923 editor.buffer.read(cx).read(cx).text(),
2924 "one\n two\nthree\n four"
2925 );
2926 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2927 assert_eq!(
2928 editor.buffer.read(cx).read(cx).text(),
2929 "\n two\nthree\n four"
2930 );
2931 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2932 assert_eq!(
2933 editor.buffer.read(cx).read(cx).text(),
2934 "two\nthree\n four"
2935 );
2936 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2937 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
2938 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2939 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
2940 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2941 assert_eq!(editor.buffer.read(cx).read(cx).text(), "four");
2942 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2943 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2944 });
2945}
2946
2947#[gpui::test]
2948fn test_newline(cx: &mut TestAppContext) {
2949 init_test(cx, |_| {});
2950
2951 let editor = cx.add_window(|window, cx| {
2952 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
2953 build_editor(buffer, window, cx)
2954 });
2955
2956 _ = editor.update(cx, |editor, window, cx| {
2957 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2958 s.select_display_ranges([
2959 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2960 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2961 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
2962 ])
2963 });
2964
2965 editor.newline(&Newline, window, cx);
2966 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
2967 });
2968}
2969
2970#[gpui::test]
2971fn test_newline_with_old_selections(cx: &mut TestAppContext) {
2972 init_test(cx, |_| {});
2973
2974 let editor = cx.add_window(|window, cx| {
2975 let buffer = MultiBuffer::build_simple(
2976 "
2977 a
2978 b(
2979 X
2980 )
2981 c(
2982 X
2983 )
2984 "
2985 .unindent()
2986 .as_str(),
2987 cx,
2988 );
2989 let mut editor = build_editor(buffer, window, cx);
2990 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2991 s.select_ranges([
2992 Point::new(2, 4)..Point::new(2, 5),
2993 Point::new(5, 4)..Point::new(5, 5),
2994 ])
2995 });
2996 editor
2997 });
2998
2999 _ = editor.update(cx, |editor, window, cx| {
3000 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3001 editor.buffer.update(cx, |buffer, cx| {
3002 buffer.edit(
3003 [
3004 (Point::new(1, 2)..Point::new(3, 0), ""),
3005 (Point::new(4, 2)..Point::new(6, 0), ""),
3006 ],
3007 None,
3008 cx,
3009 );
3010 assert_eq!(
3011 buffer.read(cx).text(),
3012 "
3013 a
3014 b()
3015 c()
3016 "
3017 .unindent()
3018 );
3019 });
3020 assert_eq!(
3021 editor.selections.ranges(cx),
3022 &[
3023 Point::new(1, 2)..Point::new(1, 2),
3024 Point::new(2, 2)..Point::new(2, 2),
3025 ],
3026 );
3027
3028 editor.newline(&Newline, window, cx);
3029 assert_eq!(
3030 editor.text(cx),
3031 "
3032 a
3033 b(
3034 )
3035 c(
3036 )
3037 "
3038 .unindent()
3039 );
3040
3041 // The selections are moved after the inserted newlines
3042 assert_eq!(
3043 editor.selections.ranges(cx),
3044 &[
3045 Point::new(2, 0)..Point::new(2, 0),
3046 Point::new(4, 0)..Point::new(4, 0),
3047 ],
3048 );
3049 });
3050}
3051
3052#[gpui::test]
3053async fn test_newline_above(cx: &mut TestAppContext) {
3054 init_test(cx, |settings| {
3055 settings.defaults.tab_size = NonZeroU32::new(4)
3056 });
3057
3058 let language = Arc::new(
3059 Language::new(
3060 LanguageConfig::default(),
3061 Some(tree_sitter_rust::LANGUAGE.into()),
3062 )
3063 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3064 .unwrap(),
3065 );
3066
3067 let mut cx = EditorTestContext::new(cx).await;
3068 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3069 cx.set_state(indoc! {"
3070 const a: ˇA = (
3071 (ˇ
3072 «const_functionˇ»(ˇ),
3073 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3074 )ˇ
3075 ˇ);ˇ
3076 "});
3077
3078 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
3079 cx.assert_editor_state(indoc! {"
3080 ˇ
3081 const a: A = (
3082 ˇ
3083 (
3084 ˇ
3085 ˇ
3086 const_function(),
3087 ˇ
3088 ˇ
3089 ˇ
3090 ˇ
3091 something_else,
3092 ˇ
3093 )
3094 ˇ
3095 ˇ
3096 );
3097 "});
3098}
3099
3100#[gpui::test]
3101async fn test_newline_below(cx: &mut TestAppContext) {
3102 init_test(cx, |settings| {
3103 settings.defaults.tab_size = NonZeroU32::new(4)
3104 });
3105
3106 let language = Arc::new(
3107 Language::new(
3108 LanguageConfig::default(),
3109 Some(tree_sitter_rust::LANGUAGE.into()),
3110 )
3111 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3112 .unwrap(),
3113 );
3114
3115 let mut cx = EditorTestContext::new(cx).await;
3116 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3117 cx.set_state(indoc! {"
3118 const a: ˇA = (
3119 (ˇ
3120 «const_functionˇ»(ˇ),
3121 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3122 )ˇ
3123 ˇ);ˇ
3124 "});
3125
3126 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
3127 cx.assert_editor_state(indoc! {"
3128 const a: A = (
3129 ˇ
3130 (
3131 ˇ
3132 const_function(),
3133 ˇ
3134 ˇ
3135 something_else,
3136 ˇ
3137 ˇ
3138 ˇ
3139 ˇ
3140 )
3141 ˇ
3142 );
3143 ˇ
3144 ˇ
3145 "});
3146}
3147
3148#[gpui::test]
3149async fn test_newline_comments(cx: &mut TestAppContext) {
3150 init_test(cx, |settings| {
3151 settings.defaults.tab_size = NonZeroU32::new(4)
3152 });
3153
3154 let language = Arc::new(Language::new(
3155 LanguageConfig {
3156 line_comments: vec!["// ".into()],
3157 ..LanguageConfig::default()
3158 },
3159 None,
3160 ));
3161 {
3162 let mut cx = EditorTestContext::new(cx).await;
3163 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3164 cx.set_state(indoc! {"
3165 // Fooˇ
3166 "});
3167
3168 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3169 cx.assert_editor_state(indoc! {"
3170 // Foo
3171 // ˇ
3172 "});
3173 // Ensure that we add comment prefix when existing line contains space
3174 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3175 cx.assert_editor_state(
3176 indoc! {"
3177 // Foo
3178 //s
3179 // ˇ
3180 "}
3181 .replace("s", " ") // s is used as space placeholder to prevent format on save
3182 .as_str(),
3183 );
3184 // Ensure that we add comment prefix when existing line does not contain space
3185 cx.set_state(indoc! {"
3186 // Foo
3187 //ˇ
3188 "});
3189 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3190 cx.assert_editor_state(indoc! {"
3191 // Foo
3192 //
3193 // ˇ
3194 "});
3195 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
3196 cx.set_state(indoc! {"
3197 ˇ// Foo
3198 "});
3199 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3200 cx.assert_editor_state(indoc! {"
3201
3202 ˇ// Foo
3203 "});
3204 }
3205 // Ensure that comment continuations can be disabled.
3206 update_test_language_settings(cx, |settings| {
3207 settings.defaults.extend_comment_on_newline = Some(false);
3208 });
3209 let mut cx = EditorTestContext::new(cx).await;
3210 cx.set_state(indoc! {"
3211 // Fooˇ
3212 "});
3213 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3214 cx.assert_editor_state(indoc! {"
3215 // Foo
3216 ˇ
3217 "});
3218}
3219
3220#[gpui::test]
3221async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
3222 init_test(cx, |settings| {
3223 settings.defaults.tab_size = NonZeroU32::new(4)
3224 });
3225
3226 let language = Arc::new(Language::new(
3227 LanguageConfig {
3228 line_comments: vec!["// ".into(), "/// ".into()],
3229 ..LanguageConfig::default()
3230 },
3231 None,
3232 ));
3233 {
3234 let mut cx = EditorTestContext::new(cx).await;
3235 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3236 cx.set_state(indoc! {"
3237 //ˇ
3238 "});
3239 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3240 cx.assert_editor_state(indoc! {"
3241 //
3242 // ˇ
3243 "});
3244
3245 cx.set_state(indoc! {"
3246 ///ˇ
3247 "});
3248 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3249 cx.assert_editor_state(indoc! {"
3250 ///
3251 /// ˇ
3252 "});
3253 }
3254}
3255
3256#[gpui::test]
3257async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
3258 init_test(cx, |settings| {
3259 settings.defaults.tab_size = NonZeroU32::new(4)
3260 });
3261
3262 let language = Arc::new(
3263 Language::new(
3264 LanguageConfig {
3265 documentation_comment: Some(language::BlockCommentConfig {
3266 start: "/**".into(),
3267 end: "*/".into(),
3268 prefix: "* ".into(),
3269 tab_size: 1,
3270 }),
3271
3272 ..LanguageConfig::default()
3273 },
3274 Some(tree_sitter_rust::LANGUAGE.into()),
3275 )
3276 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
3277 .unwrap(),
3278 );
3279
3280 {
3281 let mut cx = EditorTestContext::new(cx).await;
3282 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3283 cx.set_state(indoc! {"
3284 /**ˇ
3285 "});
3286
3287 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3288 cx.assert_editor_state(indoc! {"
3289 /**
3290 * ˇ
3291 "});
3292 // Ensure that if cursor is before the comment start,
3293 // we do not actually insert a comment prefix.
3294 cx.set_state(indoc! {"
3295 ˇ/**
3296 "});
3297 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3298 cx.assert_editor_state(indoc! {"
3299
3300 ˇ/**
3301 "});
3302 // Ensure that if cursor is between it doesn't add comment prefix.
3303 cx.set_state(indoc! {"
3304 /*ˇ*
3305 "});
3306 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3307 cx.assert_editor_state(indoc! {"
3308 /*
3309 ˇ*
3310 "});
3311 // Ensure that if suffix exists on same line after cursor it adds new line.
3312 cx.set_state(indoc! {"
3313 /**ˇ*/
3314 "});
3315 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3316 cx.assert_editor_state(indoc! {"
3317 /**
3318 * ˇ
3319 */
3320 "});
3321 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3322 cx.set_state(indoc! {"
3323 /**ˇ */
3324 "});
3325 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3326 cx.assert_editor_state(indoc! {"
3327 /**
3328 * ˇ
3329 */
3330 "});
3331 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3332 cx.set_state(indoc! {"
3333 /** ˇ*/
3334 "});
3335 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3336 cx.assert_editor_state(
3337 indoc! {"
3338 /**s
3339 * ˇ
3340 */
3341 "}
3342 .replace("s", " ") // s is used as space placeholder to prevent format on save
3343 .as_str(),
3344 );
3345 // Ensure that delimiter space is preserved when newline on already
3346 // spaced delimiter.
3347 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3348 cx.assert_editor_state(
3349 indoc! {"
3350 /**s
3351 *s
3352 * ˇ
3353 */
3354 "}
3355 .replace("s", " ") // s is used as space placeholder to prevent format on save
3356 .as_str(),
3357 );
3358 // Ensure that delimiter space is preserved when space is not
3359 // on existing delimiter.
3360 cx.set_state(indoc! {"
3361 /**
3362 *ˇ
3363 */
3364 "});
3365 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3366 cx.assert_editor_state(indoc! {"
3367 /**
3368 *
3369 * ˇ
3370 */
3371 "});
3372 // Ensure that if suffix exists on same line after cursor it
3373 // doesn't add extra new line if prefix is not on same line.
3374 cx.set_state(indoc! {"
3375 /**
3376 ˇ*/
3377 "});
3378 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3379 cx.assert_editor_state(indoc! {"
3380 /**
3381
3382 ˇ*/
3383 "});
3384 // Ensure that it detects suffix after existing prefix.
3385 cx.set_state(indoc! {"
3386 /**ˇ/
3387 "});
3388 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3389 cx.assert_editor_state(indoc! {"
3390 /**
3391 ˇ/
3392 "});
3393 // Ensure that if suffix exists on same line before
3394 // cursor it does not add comment prefix.
3395 cx.set_state(indoc! {"
3396 /** */ˇ
3397 "});
3398 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3399 cx.assert_editor_state(indoc! {"
3400 /** */
3401 ˇ
3402 "});
3403 // Ensure that if suffix exists on same line before
3404 // cursor it does not add comment prefix.
3405 cx.set_state(indoc! {"
3406 /**
3407 *
3408 */ˇ
3409 "});
3410 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3411 cx.assert_editor_state(indoc! {"
3412 /**
3413 *
3414 */
3415 ˇ
3416 "});
3417
3418 // Ensure that inline comment followed by code
3419 // doesn't add comment prefix on newline
3420 cx.set_state(indoc! {"
3421 /** */ textˇ
3422 "});
3423 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3424 cx.assert_editor_state(indoc! {"
3425 /** */ text
3426 ˇ
3427 "});
3428
3429 // Ensure that text after comment end tag
3430 // doesn't add comment prefix on newline
3431 cx.set_state(indoc! {"
3432 /**
3433 *
3434 */ˇtext
3435 "});
3436 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3437 cx.assert_editor_state(indoc! {"
3438 /**
3439 *
3440 */
3441 ˇtext
3442 "});
3443
3444 // Ensure if not comment block it doesn't
3445 // add comment prefix on newline
3446 cx.set_state(indoc! {"
3447 * textˇ
3448 "});
3449 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3450 cx.assert_editor_state(indoc! {"
3451 * text
3452 ˇ
3453 "});
3454 }
3455 // Ensure that comment continuations can be disabled.
3456 update_test_language_settings(cx, |settings| {
3457 settings.defaults.extend_comment_on_newline = Some(false);
3458 });
3459 let mut cx = EditorTestContext::new(cx).await;
3460 cx.set_state(indoc! {"
3461 /**ˇ
3462 "});
3463 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3464 cx.assert_editor_state(indoc! {"
3465 /**
3466 ˇ
3467 "});
3468}
3469
3470#[gpui::test]
3471async fn test_newline_comments_with_block_comment(cx: &mut TestAppContext) {
3472 init_test(cx, |settings| {
3473 settings.defaults.tab_size = NonZeroU32::new(4)
3474 });
3475
3476 let lua_language = Arc::new(Language::new(
3477 LanguageConfig {
3478 line_comments: vec!["--".into()],
3479 block_comment: Some(language::BlockCommentConfig {
3480 start: "--[[".into(),
3481 prefix: "".into(),
3482 end: "]]".into(),
3483 tab_size: 0,
3484 }),
3485 ..LanguageConfig::default()
3486 },
3487 None,
3488 ));
3489
3490 let mut cx = EditorTestContext::new(cx).await;
3491 cx.update_buffer(|buffer, cx| buffer.set_language(Some(lua_language), cx));
3492
3493 // Line with line comment should extend
3494 cx.set_state(indoc! {"
3495 --ˇ
3496 "});
3497 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3498 cx.assert_editor_state(indoc! {"
3499 --
3500 --ˇ
3501 "});
3502
3503 // Line with block comment that matches line comment should not extend
3504 cx.set_state(indoc! {"
3505 --[[ˇ
3506 "});
3507 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3508 cx.assert_editor_state(indoc! {"
3509 --[[
3510 ˇ
3511 "});
3512}
3513
3514#[gpui::test]
3515fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3516 init_test(cx, |_| {});
3517
3518 let editor = cx.add_window(|window, cx| {
3519 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3520 let mut editor = build_editor(buffer, window, cx);
3521 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3522 s.select_ranges([3..4, 11..12, 19..20])
3523 });
3524 editor
3525 });
3526
3527 _ = editor.update(cx, |editor, window, cx| {
3528 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3529 editor.buffer.update(cx, |buffer, cx| {
3530 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
3531 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3532 });
3533 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
3534
3535 editor.insert("Z", window, cx);
3536 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3537
3538 // The selections are moved after the inserted characters
3539 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
3540 });
3541}
3542
3543#[gpui::test]
3544async fn test_tab(cx: &mut TestAppContext) {
3545 init_test(cx, |settings| {
3546 settings.defaults.tab_size = NonZeroU32::new(3)
3547 });
3548
3549 let mut cx = EditorTestContext::new(cx).await;
3550 cx.set_state(indoc! {"
3551 ˇabˇc
3552 ˇ🏀ˇ🏀ˇefg
3553 dˇ
3554 "});
3555 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3556 cx.assert_editor_state(indoc! {"
3557 ˇab ˇc
3558 ˇ🏀 ˇ🏀 ˇefg
3559 d ˇ
3560 "});
3561
3562 cx.set_state(indoc! {"
3563 a
3564 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3565 "});
3566 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3567 cx.assert_editor_state(indoc! {"
3568 a
3569 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3570 "});
3571}
3572
3573#[gpui::test]
3574async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3575 init_test(cx, |_| {});
3576
3577 let mut cx = EditorTestContext::new(cx).await;
3578 let language = Arc::new(
3579 Language::new(
3580 LanguageConfig::default(),
3581 Some(tree_sitter_rust::LANGUAGE.into()),
3582 )
3583 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3584 .unwrap(),
3585 );
3586 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3587
3588 // test when all cursors are not at suggested indent
3589 // then simply move to their suggested indent location
3590 cx.set_state(indoc! {"
3591 const a: B = (
3592 c(
3593 ˇ
3594 ˇ )
3595 );
3596 "});
3597 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3598 cx.assert_editor_state(indoc! {"
3599 const a: B = (
3600 c(
3601 ˇ
3602 ˇ)
3603 );
3604 "});
3605
3606 // test cursor already at suggested indent not moving when
3607 // other cursors are yet to reach their suggested indents
3608 cx.set_state(indoc! {"
3609 ˇ
3610 const a: B = (
3611 c(
3612 d(
3613 ˇ
3614 )
3615 ˇ
3616 ˇ )
3617 );
3618 "});
3619 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3620 cx.assert_editor_state(indoc! {"
3621 ˇ
3622 const a: B = (
3623 c(
3624 d(
3625 ˇ
3626 )
3627 ˇ
3628 ˇ)
3629 );
3630 "});
3631 // test when all cursors are at suggested indent then tab is inserted
3632 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3633 cx.assert_editor_state(indoc! {"
3634 ˇ
3635 const a: B = (
3636 c(
3637 d(
3638 ˇ
3639 )
3640 ˇ
3641 ˇ)
3642 );
3643 "});
3644
3645 // test when current indent is less than suggested indent,
3646 // we adjust line to match suggested indent and move cursor to it
3647 //
3648 // when no other cursor is at word boundary, all of them should move
3649 cx.set_state(indoc! {"
3650 const a: B = (
3651 c(
3652 d(
3653 ˇ
3654 ˇ )
3655 ˇ )
3656 );
3657 "});
3658 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3659 cx.assert_editor_state(indoc! {"
3660 const a: B = (
3661 c(
3662 d(
3663 ˇ
3664 ˇ)
3665 ˇ)
3666 );
3667 "});
3668
3669 // test when current indent is less than suggested indent,
3670 // we adjust line to match suggested indent and move cursor to it
3671 //
3672 // when some other cursor is at word boundary, it should not move
3673 cx.set_state(indoc! {"
3674 const a: B = (
3675 c(
3676 d(
3677 ˇ
3678 ˇ )
3679 ˇ)
3680 );
3681 "});
3682 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3683 cx.assert_editor_state(indoc! {"
3684 const a: B = (
3685 c(
3686 d(
3687 ˇ
3688 ˇ)
3689 ˇ)
3690 );
3691 "});
3692
3693 // test when current indent is more than suggested indent,
3694 // we just move cursor to current indent instead of suggested indent
3695 //
3696 // when no other cursor is at word boundary, all of them should move
3697 cx.set_state(indoc! {"
3698 const a: B = (
3699 c(
3700 d(
3701 ˇ
3702 ˇ )
3703 ˇ )
3704 );
3705 "});
3706 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3707 cx.assert_editor_state(indoc! {"
3708 const a: B = (
3709 c(
3710 d(
3711 ˇ
3712 ˇ)
3713 ˇ)
3714 );
3715 "});
3716 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3717 cx.assert_editor_state(indoc! {"
3718 const a: B = (
3719 c(
3720 d(
3721 ˇ
3722 ˇ)
3723 ˇ)
3724 );
3725 "});
3726
3727 // test when current indent is more than suggested indent,
3728 // we just move cursor to current indent instead of suggested indent
3729 //
3730 // when some other cursor is at word boundary, it doesn't move
3731 cx.set_state(indoc! {"
3732 const a: B = (
3733 c(
3734 d(
3735 ˇ
3736 ˇ )
3737 ˇ)
3738 );
3739 "});
3740 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3741 cx.assert_editor_state(indoc! {"
3742 const a: B = (
3743 c(
3744 d(
3745 ˇ
3746 ˇ)
3747 ˇ)
3748 );
3749 "});
3750
3751 // handle auto-indent when there are multiple cursors on the same line
3752 cx.set_state(indoc! {"
3753 const a: B = (
3754 c(
3755 ˇ ˇ
3756 ˇ )
3757 );
3758 "});
3759 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3760 cx.assert_editor_state(indoc! {"
3761 const a: B = (
3762 c(
3763 ˇ
3764 ˇ)
3765 );
3766 "});
3767}
3768
3769#[gpui::test]
3770async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3771 init_test(cx, |settings| {
3772 settings.defaults.tab_size = NonZeroU32::new(3)
3773 });
3774
3775 let mut cx = EditorTestContext::new(cx).await;
3776 cx.set_state(indoc! {"
3777 ˇ
3778 \t ˇ
3779 \t ˇ
3780 \t ˇ
3781 \t \t\t \t \t\t \t\t \t \t ˇ
3782 "});
3783
3784 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3785 cx.assert_editor_state(indoc! {"
3786 ˇ
3787 \t ˇ
3788 \t ˇ
3789 \t ˇ
3790 \t \t\t \t \t\t \t\t \t \t ˇ
3791 "});
3792}
3793
3794#[gpui::test]
3795async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3796 init_test(cx, |settings| {
3797 settings.defaults.tab_size = NonZeroU32::new(4)
3798 });
3799
3800 let language = Arc::new(
3801 Language::new(
3802 LanguageConfig::default(),
3803 Some(tree_sitter_rust::LANGUAGE.into()),
3804 )
3805 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3806 .unwrap(),
3807 );
3808
3809 let mut cx = EditorTestContext::new(cx).await;
3810 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3811 cx.set_state(indoc! {"
3812 fn a() {
3813 if b {
3814 \t ˇc
3815 }
3816 }
3817 "});
3818
3819 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3820 cx.assert_editor_state(indoc! {"
3821 fn a() {
3822 if b {
3823 ˇc
3824 }
3825 }
3826 "});
3827}
3828
3829#[gpui::test]
3830async fn test_indent_outdent(cx: &mut TestAppContext) {
3831 init_test(cx, |settings| {
3832 settings.defaults.tab_size = NonZeroU32::new(4);
3833 });
3834
3835 let mut cx = EditorTestContext::new(cx).await;
3836
3837 cx.set_state(indoc! {"
3838 «oneˇ» «twoˇ»
3839 three
3840 four
3841 "});
3842 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3843 cx.assert_editor_state(indoc! {"
3844 «oneˇ» «twoˇ»
3845 three
3846 four
3847 "});
3848
3849 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3850 cx.assert_editor_state(indoc! {"
3851 «oneˇ» «twoˇ»
3852 three
3853 four
3854 "});
3855
3856 // select across line ending
3857 cx.set_state(indoc! {"
3858 one two
3859 t«hree
3860 ˇ» four
3861 "});
3862 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3863 cx.assert_editor_state(indoc! {"
3864 one two
3865 t«hree
3866 ˇ» four
3867 "});
3868
3869 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3870 cx.assert_editor_state(indoc! {"
3871 one two
3872 t«hree
3873 ˇ» four
3874 "});
3875
3876 // Ensure that indenting/outdenting works when the cursor is at column 0.
3877 cx.set_state(indoc! {"
3878 one two
3879 ˇthree
3880 four
3881 "});
3882 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3883 cx.assert_editor_state(indoc! {"
3884 one two
3885 ˇthree
3886 four
3887 "});
3888
3889 cx.set_state(indoc! {"
3890 one two
3891 ˇ three
3892 four
3893 "});
3894 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3895 cx.assert_editor_state(indoc! {"
3896 one two
3897 ˇthree
3898 four
3899 "});
3900}
3901
3902#[gpui::test]
3903async fn test_indent_yaml_comments_with_multiple_cursors(cx: &mut TestAppContext) {
3904 // This is a regression test for issue #33761
3905 init_test(cx, |_| {});
3906
3907 let mut cx = EditorTestContext::new(cx).await;
3908 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3909 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3910
3911 cx.set_state(
3912 r#"ˇ# ingress:
3913ˇ# api:
3914ˇ# enabled: false
3915ˇ# pathType: Prefix
3916ˇ# console:
3917ˇ# enabled: false
3918ˇ# pathType: Prefix
3919"#,
3920 );
3921
3922 // Press tab to indent all lines
3923 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3924
3925 cx.assert_editor_state(
3926 r#" ˇ# ingress:
3927 ˇ# api:
3928 ˇ# enabled: false
3929 ˇ# pathType: Prefix
3930 ˇ# console:
3931 ˇ# enabled: false
3932 ˇ# pathType: Prefix
3933"#,
3934 );
3935}
3936
3937#[gpui::test]
3938async fn test_indent_yaml_non_comments_with_multiple_cursors(cx: &mut TestAppContext) {
3939 // This is a test to make sure our fix for issue #33761 didn't break anything
3940 init_test(cx, |_| {});
3941
3942 let mut cx = EditorTestContext::new(cx).await;
3943 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3944 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3945
3946 cx.set_state(
3947 r#"ˇingress:
3948ˇ api:
3949ˇ enabled: false
3950ˇ pathType: Prefix
3951"#,
3952 );
3953
3954 // Press tab to indent all lines
3955 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3956
3957 cx.assert_editor_state(
3958 r#"ˇingress:
3959 ˇapi:
3960 ˇenabled: false
3961 ˇpathType: Prefix
3962"#,
3963 );
3964}
3965
3966#[gpui::test]
3967async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
3968 init_test(cx, |settings| {
3969 settings.defaults.hard_tabs = Some(true);
3970 });
3971
3972 let mut cx = EditorTestContext::new(cx).await;
3973
3974 // select two ranges on one line
3975 cx.set_state(indoc! {"
3976 «oneˇ» «twoˇ»
3977 three
3978 four
3979 "});
3980 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3981 cx.assert_editor_state(indoc! {"
3982 \t«oneˇ» «twoˇ»
3983 three
3984 four
3985 "});
3986 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3987 cx.assert_editor_state(indoc! {"
3988 \t\t«oneˇ» «twoˇ»
3989 three
3990 four
3991 "});
3992 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3993 cx.assert_editor_state(indoc! {"
3994 \t«oneˇ» «twoˇ»
3995 three
3996 four
3997 "});
3998 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3999 cx.assert_editor_state(indoc! {"
4000 «oneˇ» «twoˇ»
4001 three
4002 four
4003 "});
4004
4005 // select across a line ending
4006 cx.set_state(indoc! {"
4007 one two
4008 t«hree
4009 ˇ»four
4010 "});
4011 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4012 cx.assert_editor_state(indoc! {"
4013 one two
4014 \tt«hree
4015 ˇ»four
4016 "});
4017 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4018 cx.assert_editor_state(indoc! {"
4019 one two
4020 \t\tt«hree
4021 ˇ»four
4022 "});
4023 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4024 cx.assert_editor_state(indoc! {"
4025 one two
4026 \tt«hree
4027 ˇ»four
4028 "});
4029 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4030 cx.assert_editor_state(indoc! {"
4031 one two
4032 t«hree
4033 ˇ»four
4034 "});
4035
4036 // Ensure that indenting/outdenting works when the cursor is at column 0.
4037 cx.set_state(indoc! {"
4038 one two
4039 ˇthree
4040 four
4041 "});
4042 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4043 cx.assert_editor_state(indoc! {"
4044 one two
4045 ˇthree
4046 four
4047 "});
4048 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4049 cx.assert_editor_state(indoc! {"
4050 one two
4051 \tˇthree
4052 four
4053 "});
4054 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4055 cx.assert_editor_state(indoc! {"
4056 one two
4057 ˇthree
4058 four
4059 "});
4060}
4061
4062#[gpui::test]
4063fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
4064 init_test(cx, |settings| {
4065 settings.languages.0.extend([
4066 (
4067 "TOML".into(),
4068 LanguageSettingsContent {
4069 tab_size: NonZeroU32::new(2),
4070 ..Default::default()
4071 },
4072 ),
4073 (
4074 "Rust".into(),
4075 LanguageSettingsContent {
4076 tab_size: NonZeroU32::new(4),
4077 ..Default::default()
4078 },
4079 ),
4080 ]);
4081 });
4082
4083 let toml_language = Arc::new(Language::new(
4084 LanguageConfig {
4085 name: "TOML".into(),
4086 ..Default::default()
4087 },
4088 None,
4089 ));
4090 let rust_language = Arc::new(Language::new(
4091 LanguageConfig {
4092 name: "Rust".into(),
4093 ..Default::default()
4094 },
4095 None,
4096 ));
4097
4098 let toml_buffer =
4099 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
4100 let rust_buffer =
4101 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
4102 let multibuffer = cx.new(|cx| {
4103 let mut multibuffer = MultiBuffer::new(ReadWrite);
4104 multibuffer.push_excerpts(
4105 toml_buffer.clone(),
4106 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
4107 cx,
4108 );
4109 multibuffer.push_excerpts(
4110 rust_buffer.clone(),
4111 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
4112 cx,
4113 );
4114 multibuffer
4115 });
4116
4117 cx.add_window(|window, cx| {
4118 let mut editor = build_editor(multibuffer, window, cx);
4119
4120 assert_eq!(
4121 editor.text(cx),
4122 indoc! {"
4123 a = 1
4124 b = 2
4125
4126 const c: usize = 3;
4127 "}
4128 );
4129
4130 select_ranges(
4131 &mut editor,
4132 indoc! {"
4133 «aˇ» = 1
4134 b = 2
4135
4136 «const c:ˇ» usize = 3;
4137 "},
4138 window,
4139 cx,
4140 );
4141
4142 editor.tab(&Tab, window, cx);
4143 assert_text_with_selections(
4144 &mut editor,
4145 indoc! {"
4146 «aˇ» = 1
4147 b = 2
4148
4149 «const c:ˇ» usize = 3;
4150 "},
4151 cx,
4152 );
4153 editor.backtab(&Backtab, window, cx);
4154 assert_text_with_selections(
4155 &mut editor,
4156 indoc! {"
4157 «aˇ» = 1
4158 b = 2
4159
4160 «const c:ˇ» usize = 3;
4161 "},
4162 cx,
4163 );
4164
4165 editor
4166 });
4167}
4168
4169#[gpui::test]
4170async fn test_backspace(cx: &mut TestAppContext) {
4171 init_test(cx, |_| {});
4172
4173 let mut cx = EditorTestContext::new(cx).await;
4174
4175 // Basic backspace
4176 cx.set_state(indoc! {"
4177 onˇe two three
4178 fou«rˇ» five six
4179 seven «ˇeight nine
4180 »ten
4181 "});
4182 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4183 cx.assert_editor_state(indoc! {"
4184 oˇe two three
4185 fouˇ five six
4186 seven ˇten
4187 "});
4188
4189 // Test backspace inside and around indents
4190 cx.set_state(indoc! {"
4191 zero
4192 ˇone
4193 ˇtwo
4194 ˇ ˇ ˇ three
4195 ˇ ˇ four
4196 "});
4197 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4198 cx.assert_editor_state(indoc! {"
4199 zero
4200 ˇone
4201 ˇtwo
4202 ˇ threeˇ four
4203 "});
4204}
4205
4206#[gpui::test]
4207async fn test_delete(cx: &mut TestAppContext) {
4208 init_test(cx, |_| {});
4209
4210 let mut cx = EditorTestContext::new(cx).await;
4211 cx.set_state(indoc! {"
4212 onˇe two three
4213 fou«rˇ» five six
4214 seven «ˇeight nine
4215 »ten
4216 "});
4217 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
4218 cx.assert_editor_state(indoc! {"
4219 onˇ two three
4220 fouˇ five six
4221 seven ˇten
4222 "});
4223}
4224
4225#[gpui::test]
4226fn test_delete_line(cx: &mut TestAppContext) {
4227 init_test(cx, |_| {});
4228
4229 let editor = cx.add_window(|window, cx| {
4230 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4231 build_editor(buffer, window, cx)
4232 });
4233 _ = editor.update(cx, |editor, window, cx| {
4234 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4235 s.select_display_ranges([
4236 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4237 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4238 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4239 ])
4240 });
4241 editor.delete_line(&DeleteLine, window, cx);
4242 assert_eq!(editor.display_text(cx), "ghi");
4243 assert_eq!(
4244 editor.selections.display_ranges(cx),
4245 vec![
4246 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4247 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
4248 ]
4249 );
4250 });
4251
4252 let editor = cx.add_window(|window, cx| {
4253 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4254 build_editor(buffer, window, cx)
4255 });
4256 _ = editor.update(cx, |editor, window, cx| {
4257 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4258 s.select_display_ranges([
4259 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
4260 ])
4261 });
4262 editor.delete_line(&DeleteLine, window, cx);
4263 assert_eq!(editor.display_text(cx), "ghi\n");
4264 assert_eq!(
4265 editor.selections.display_ranges(cx),
4266 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
4267 );
4268 });
4269}
4270
4271#[gpui::test]
4272fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
4273 init_test(cx, |_| {});
4274
4275 cx.add_window(|window, cx| {
4276 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4277 let mut editor = build_editor(buffer.clone(), window, cx);
4278 let buffer = buffer.read(cx).as_singleton().unwrap();
4279
4280 assert_eq!(
4281 editor.selections.ranges::<Point>(cx),
4282 &[Point::new(0, 0)..Point::new(0, 0)]
4283 );
4284
4285 // When on single line, replace newline at end by space
4286 editor.join_lines(&JoinLines, window, cx);
4287 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4288 assert_eq!(
4289 editor.selections.ranges::<Point>(cx),
4290 &[Point::new(0, 3)..Point::new(0, 3)]
4291 );
4292
4293 // When multiple lines are selected, remove newlines that are spanned by the selection
4294 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4295 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
4296 });
4297 editor.join_lines(&JoinLines, window, cx);
4298 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
4299 assert_eq!(
4300 editor.selections.ranges::<Point>(cx),
4301 &[Point::new(0, 11)..Point::new(0, 11)]
4302 );
4303
4304 // Undo should be transactional
4305 editor.undo(&Undo, window, cx);
4306 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4307 assert_eq!(
4308 editor.selections.ranges::<Point>(cx),
4309 &[Point::new(0, 5)..Point::new(2, 2)]
4310 );
4311
4312 // When joining an empty line don't insert a space
4313 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4314 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
4315 });
4316 editor.join_lines(&JoinLines, window, cx);
4317 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
4318 assert_eq!(
4319 editor.selections.ranges::<Point>(cx),
4320 [Point::new(2, 3)..Point::new(2, 3)]
4321 );
4322
4323 // We can remove trailing newlines
4324 editor.join_lines(&JoinLines, window, cx);
4325 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4326 assert_eq!(
4327 editor.selections.ranges::<Point>(cx),
4328 [Point::new(2, 3)..Point::new(2, 3)]
4329 );
4330
4331 // We don't blow up on the last line
4332 editor.join_lines(&JoinLines, window, cx);
4333 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4334 assert_eq!(
4335 editor.selections.ranges::<Point>(cx),
4336 [Point::new(2, 3)..Point::new(2, 3)]
4337 );
4338
4339 // reset to test indentation
4340 editor.buffer.update(cx, |buffer, cx| {
4341 buffer.edit(
4342 [
4343 (Point::new(1, 0)..Point::new(1, 2), " "),
4344 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
4345 ],
4346 None,
4347 cx,
4348 )
4349 });
4350
4351 // We remove any leading spaces
4352 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
4353 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4354 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
4355 });
4356 editor.join_lines(&JoinLines, window, cx);
4357 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
4358
4359 // We don't insert a space for a line containing only spaces
4360 editor.join_lines(&JoinLines, window, cx);
4361 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
4362
4363 // We ignore any leading tabs
4364 editor.join_lines(&JoinLines, window, cx);
4365 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
4366
4367 editor
4368 });
4369}
4370
4371#[gpui::test]
4372fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
4373 init_test(cx, |_| {});
4374
4375 cx.add_window(|window, cx| {
4376 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4377 let mut editor = build_editor(buffer.clone(), window, cx);
4378 let buffer = buffer.read(cx).as_singleton().unwrap();
4379
4380 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4381 s.select_ranges([
4382 Point::new(0, 2)..Point::new(1, 1),
4383 Point::new(1, 2)..Point::new(1, 2),
4384 Point::new(3, 1)..Point::new(3, 2),
4385 ])
4386 });
4387
4388 editor.join_lines(&JoinLines, window, cx);
4389 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
4390
4391 assert_eq!(
4392 editor.selections.ranges::<Point>(cx),
4393 [
4394 Point::new(0, 7)..Point::new(0, 7),
4395 Point::new(1, 3)..Point::new(1, 3)
4396 ]
4397 );
4398 editor
4399 });
4400}
4401
4402#[gpui::test]
4403async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
4404 init_test(cx, |_| {});
4405
4406 let mut cx = EditorTestContext::new(cx).await;
4407
4408 let diff_base = r#"
4409 Line 0
4410 Line 1
4411 Line 2
4412 Line 3
4413 "#
4414 .unindent();
4415
4416 cx.set_state(
4417 &r#"
4418 ˇLine 0
4419 Line 1
4420 Line 2
4421 Line 3
4422 "#
4423 .unindent(),
4424 );
4425
4426 cx.set_head_text(&diff_base);
4427 executor.run_until_parked();
4428
4429 // Join lines
4430 cx.update_editor(|editor, window, cx| {
4431 editor.join_lines(&JoinLines, window, cx);
4432 });
4433 executor.run_until_parked();
4434
4435 cx.assert_editor_state(
4436 &r#"
4437 Line 0ˇ Line 1
4438 Line 2
4439 Line 3
4440 "#
4441 .unindent(),
4442 );
4443 // Join again
4444 cx.update_editor(|editor, window, cx| {
4445 editor.join_lines(&JoinLines, window, cx);
4446 });
4447 executor.run_until_parked();
4448
4449 cx.assert_editor_state(
4450 &r#"
4451 Line 0 Line 1ˇ Line 2
4452 Line 3
4453 "#
4454 .unindent(),
4455 );
4456}
4457
4458#[gpui::test]
4459async fn test_custom_newlines_cause_no_false_positive_diffs(
4460 executor: BackgroundExecutor,
4461 cx: &mut TestAppContext,
4462) {
4463 init_test(cx, |_| {});
4464 let mut cx = EditorTestContext::new(cx).await;
4465 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
4466 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
4467 executor.run_until_parked();
4468
4469 cx.update_editor(|editor, window, cx| {
4470 let snapshot = editor.snapshot(window, cx);
4471 assert_eq!(
4472 snapshot
4473 .buffer_snapshot
4474 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
4475 .collect::<Vec<_>>(),
4476 Vec::new(),
4477 "Should not have any diffs for files with custom newlines"
4478 );
4479 });
4480}
4481
4482#[gpui::test]
4483async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppContext) {
4484 init_test(cx, |_| {});
4485
4486 let mut cx = EditorTestContext::new(cx).await;
4487
4488 // Test sort_lines_case_insensitive()
4489 cx.set_state(indoc! {"
4490 «z
4491 y
4492 x
4493 Z
4494 Y
4495 Xˇ»
4496 "});
4497 cx.update_editor(|e, window, cx| {
4498 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
4499 });
4500 cx.assert_editor_state(indoc! {"
4501 «x
4502 X
4503 y
4504 Y
4505 z
4506 Zˇ»
4507 "});
4508
4509 // Test sort_lines_by_length()
4510 //
4511 // Demonstrates:
4512 // - ∞ is 3 bytes UTF-8, but sorted by its char count (1)
4513 // - sort is stable
4514 cx.set_state(indoc! {"
4515 «123
4516 æ
4517 12
4518 ∞
4519 1
4520 æˇ»
4521 "});
4522 cx.update_editor(|e, window, cx| e.sort_lines_by_length(&SortLinesByLength, window, cx));
4523 cx.assert_editor_state(indoc! {"
4524 «æ
4525 ∞
4526 1
4527 æ
4528 12
4529 123ˇ»
4530 "});
4531
4532 // Test reverse_lines()
4533 cx.set_state(indoc! {"
4534 «5
4535 4
4536 3
4537 2
4538 1ˇ»
4539 "});
4540 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4541 cx.assert_editor_state(indoc! {"
4542 «1
4543 2
4544 3
4545 4
4546 5ˇ»
4547 "});
4548
4549 // Skip testing shuffle_line()
4550
4551 // From here on out, test more complex cases of manipulate_immutable_lines() with a single driver method: sort_lines_case_sensitive()
4552 // Since all methods calling manipulate_immutable_lines() are doing the exact same general thing (reordering lines)
4553
4554 // Don't manipulate when cursor is on single line, but expand the selection
4555 cx.set_state(indoc! {"
4556 ddˇdd
4557 ccc
4558 bb
4559 a
4560 "});
4561 cx.update_editor(|e, window, cx| {
4562 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4563 });
4564 cx.assert_editor_state(indoc! {"
4565 «ddddˇ»
4566 ccc
4567 bb
4568 a
4569 "});
4570
4571 // Basic manipulate case
4572 // Start selection moves to column 0
4573 // End of selection shrinks to fit shorter line
4574 cx.set_state(indoc! {"
4575 dd«d
4576 ccc
4577 bb
4578 aaaaaˇ»
4579 "});
4580 cx.update_editor(|e, window, cx| {
4581 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4582 });
4583 cx.assert_editor_state(indoc! {"
4584 «aaaaa
4585 bb
4586 ccc
4587 dddˇ»
4588 "});
4589
4590 // Manipulate case with newlines
4591 cx.set_state(indoc! {"
4592 dd«d
4593 ccc
4594
4595 bb
4596 aaaaa
4597
4598 ˇ»
4599 "});
4600 cx.update_editor(|e, window, cx| {
4601 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4602 });
4603 cx.assert_editor_state(indoc! {"
4604 «
4605
4606 aaaaa
4607 bb
4608 ccc
4609 dddˇ»
4610
4611 "});
4612
4613 // Adding new line
4614 cx.set_state(indoc! {"
4615 aa«a
4616 bbˇ»b
4617 "});
4618 cx.update_editor(|e, window, cx| {
4619 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added_line"))
4620 });
4621 cx.assert_editor_state(indoc! {"
4622 «aaa
4623 bbb
4624 added_lineˇ»
4625 "});
4626
4627 // Removing line
4628 cx.set_state(indoc! {"
4629 aa«a
4630 bbbˇ»
4631 "});
4632 cx.update_editor(|e, window, cx| {
4633 e.manipulate_immutable_lines(window, cx, |lines| {
4634 lines.pop();
4635 })
4636 });
4637 cx.assert_editor_state(indoc! {"
4638 «aaaˇ»
4639 "});
4640
4641 // Removing all lines
4642 cx.set_state(indoc! {"
4643 aa«a
4644 bbbˇ»
4645 "});
4646 cx.update_editor(|e, window, cx| {
4647 e.manipulate_immutable_lines(window, cx, |lines| {
4648 lines.drain(..);
4649 })
4650 });
4651 cx.assert_editor_state(indoc! {"
4652 ˇ
4653 "});
4654}
4655
4656#[gpui::test]
4657async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4658 init_test(cx, |_| {});
4659
4660 let mut cx = EditorTestContext::new(cx).await;
4661
4662 // Consider continuous selection as single selection
4663 cx.set_state(indoc! {"
4664 Aaa«aa
4665 cˇ»c«c
4666 bb
4667 aaaˇ»aa
4668 "});
4669 cx.update_editor(|e, window, cx| {
4670 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4671 });
4672 cx.assert_editor_state(indoc! {"
4673 «Aaaaa
4674 ccc
4675 bb
4676 aaaaaˇ»
4677 "});
4678
4679 cx.set_state(indoc! {"
4680 Aaa«aa
4681 cˇ»c«c
4682 bb
4683 aaaˇ»aa
4684 "});
4685 cx.update_editor(|e, window, cx| {
4686 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4687 });
4688 cx.assert_editor_state(indoc! {"
4689 «Aaaaa
4690 ccc
4691 bbˇ»
4692 "});
4693
4694 // Consider non continuous selection as distinct dedup operations
4695 cx.set_state(indoc! {"
4696 «aaaaa
4697 bb
4698 aaaaa
4699 aaaaaˇ»
4700
4701 aaa«aaˇ»
4702 "});
4703 cx.update_editor(|e, window, cx| {
4704 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4705 });
4706 cx.assert_editor_state(indoc! {"
4707 «aaaaa
4708 bbˇ»
4709
4710 «aaaaaˇ»
4711 "});
4712}
4713
4714#[gpui::test]
4715async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4716 init_test(cx, |_| {});
4717
4718 let mut cx = EditorTestContext::new(cx).await;
4719
4720 cx.set_state(indoc! {"
4721 «Aaa
4722 aAa
4723 Aaaˇ»
4724 "});
4725 cx.update_editor(|e, window, cx| {
4726 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4727 });
4728 cx.assert_editor_state(indoc! {"
4729 «Aaa
4730 aAaˇ»
4731 "});
4732
4733 cx.set_state(indoc! {"
4734 «Aaa
4735 aAa
4736 aaAˇ»
4737 "});
4738 cx.update_editor(|e, window, cx| {
4739 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4740 });
4741 cx.assert_editor_state(indoc! {"
4742 «Aaaˇ»
4743 "});
4744}
4745
4746#[gpui::test]
4747async fn test_wrap_in_tag_single_selection(cx: &mut TestAppContext) {
4748 init_test(cx, |_| {});
4749
4750 let mut cx = EditorTestContext::new(cx).await;
4751
4752 let js_language = Arc::new(Language::new(
4753 LanguageConfig {
4754 name: "JavaScript".into(),
4755 wrap_characters: Some(language::WrapCharactersConfig {
4756 start_prefix: "<".into(),
4757 start_suffix: ">".into(),
4758 end_prefix: "</".into(),
4759 end_suffix: ">".into(),
4760 }),
4761 ..LanguageConfig::default()
4762 },
4763 None,
4764 ));
4765
4766 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
4767
4768 cx.set_state(indoc! {"
4769 «testˇ»
4770 "});
4771 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4772 cx.assert_editor_state(indoc! {"
4773 <«ˇ»>test</«ˇ»>
4774 "});
4775
4776 cx.set_state(indoc! {"
4777 «test
4778 testˇ»
4779 "});
4780 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4781 cx.assert_editor_state(indoc! {"
4782 <«ˇ»>test
4783 test</«ˇ»>
4784 "});
4785
4786 cx.set_state(indoc! {"
4787 teˇst
4788 "});
4789 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4790 cx.assert_editor_state(indoc! {"
4791 te<«ˇ»></«ˇ»>st
4792 "});
4793}
4794
4795#[gpui::test]
4796async fn test_wrap_in_tag_multi_selection(cx: &mut TestAppContext) {
4797 init_test(cx, |_| {});
4798
4799 let mut cx = EditorTestContext::new(cx).await;
4800
4801 let js_language = Arc::new(Language::new(
4802 LanguageConfig {
4803 name: "JavaScript".into(),
4804 wrap_characters: Some(language::WrapCharactersConfig {
4805 start_prefix: "<".into(),
4806 start_suffix: ">".into(),
4807 end_prefix: "</".into(),
4808 end_suffix: ">".into(),
4809 }),
4810 ..LanguageConfig::default()
4811 },
4812 None,
4813 ));
4814
4815 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
4816
4817 cx.set_state(indoc! {"
4818 «testˇ»
4819 «testˇ» «testˇ»
4820 «testˇ»
4821 "});
4822 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4823 cx.assert_editor_state(indoc! {"
4824 <«ˇ»>test</«ˇ»>
4825 <«ˇ»>test</«ˇ»> <«ˇ»>test</«ˇ»>
4826 <«ˇ»>test</«ˇ»>
4827 "});
4828
4829 cx.set_state(indoc! {"
4830 «test
4831 testˇ»
4832 «test
4833 testˇ»
4834 "});
4835 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4836 cx.assert_editor_state(indoc! {"
4837 <«ˇ»>test
4838 test</«ˇ»>
4839 <«ˇ»>test
4840 test</«ˇ»>
4841 "});
4842}
4843
4844#[gpui::test]
4845async fn test_wrap_in_tag_does_nothing_in_unsupported_languages(cx: &mut TestAppContext) {
4846 init_test(cx, |_| {});
4847
4848 let mut cx = EditorTestContext::new(cx).await;
4849
4850 let plaintext_language = Arc::new(Language::new(
4851 LanguageConfig {
4852 name: "Plain Text".into(),
4853 ..LanguageConfig::default()
4854 },
4855 None,
4856 ));
4857
4858 cx.update_buffer(|buffer, cx| buffer.set_language(Some(plaintext_language), cx));
4859
4860 cx.set_state(indoc! {"
4861 «testˇ»
4862 "});
4863 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4864 cx.assert_editor_state(indoc! {"
4865 «testˇ»
4866 "});
4867}
4868
4869#[gpui::test]
4870async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
4871 init_test(cx, |_| {});
4872
4873 let mut cx = EditorTestContext::new(cx).await;
4874
4875 // Manipulate with multiple selections on a single line
4876 cx.set_state(indoc! {"
4877 dd«dd
4878 cˇ»c«c
4879 bb
4880 aaaˇ»aa
4881 "});
4882 cx.update_editor(|e, window, cx| {
4883 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4884 });
4885 cx.assert_editor_state(indoc! {"
4886 «aaaaa
4887 bb
4888 ccc
4889 ddddˇ»
4890 "});
4891
4892 // Manipulate with multiple disjoin selections
4893 cx.set_state(indoc! {"
4894 5«
4895 4
4896 3
4897 2
4898 1ˇ»
4899
4900 dd«dd
4901 ccc
4902 bb
4903 aaaˇ»aa
4904 "});
4905 cx.update_editor(|e, window, cx| {
4906 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4907 });
4908 cx.assert_editor_state(indoc! {"
4909 «1
4910 2
4911 3
4912 4
4913 5ˇ»
4914
4915 «aaaaa
4916 bb
4917 ccc
4918 ddddˇ»
4919 "});
4920
4921 // Adding lines on each selection
4922 cx.set_state(indoc! {"
4923 2«
4924 1ˇ»
4925
4926 bb«bb
4927 aaaˇ»aa
4928 "});
4929 cx.update_editor(|e, window, cx| {
4930 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added line"))
4931 });
4932 cx.assert_editor_state(indoc! {"
4933 «2
4934 1
4935 added lineˇ»
4936
4937 «bbbb
4938 aaaaa
4939 added lineˇ»
4940 "});
4941
4942 // Removing lines on each selection
4943 cx.set_state(indoc! {"
4944 2«
4945 1ˇ»
4946
4947 bb«bb
4948 aaaˇ»aa
4949 "});
4950 cx.update_editor(|e, window, cx| {
4951 e.manipulate_immutable_lines(window, cx, |lines| {
4952 lines.pop();
4953 })
4954 });
4955 cx.assert_editor_state(indoc! {"
4956 «2ˇ»
4957
4958 «bbbbˇ»
4959 "});
4960}
4961
4962#[gpui::test]
4963async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) {
4964 init_test(cx, |settings| {
4965 settings.defaults.tab_size = NonZeroU32::new(3)
4966 });
4967
4968 let mut cx = EditorTestContext::new(cx).await;
4969
4970 // MULTI SELECTION
4971 // Ln.1 "«" tests empty lines
4972 // Ln.9 tests just leading whitespace
4973 cx.set_state(indoc! {"
4974 «
4975 abc // No indentationˇ»
4976 «\tabc // 1 tabˇ»
4977 \t\tabc « ˇ» // 2 tabs
4978 \t ab«c // Tab followed by space
4979 \tabc // Space followed by tab (3 spaces should be the result)
4980 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4981 abˇ»ˇc ˇ ˇ // Already space indented«
4982 \t
4983 \tabc\tdef // Only the leading tab is manipulatedˇ»
4984 "});
4985 cx.update_editor(|e, window, cx| {
4986 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4987 });
4988 cx.assert_editor_state(
4989 indoc! {"
4990 «
4991 abc // No indentation
4992 abc // 1 tab
4993 abc // 2 tabs
4994 abc // Tab followed by space
4995 abc // Space followed by tab (3 spaces should be the result)
4996 abc // Mixed indentation (tab conversion depends on the column)
4997 abc // Already space indented
4998 ·
4999 abc\tdef // Only the leading tab is manipulatedˇ»
5000 "}
5001 .replace("·", "")
5002 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5003 );
5004
5005 // Test on just a few lines, the others should remain unchanged
5006 // Only lines (3, 5, 10, 11) should change
5007 cx.set_state(
5008 indoc! {"
5009 ·
5010 abc // No indentation
5011 \tabcˇ // 1 tab
5012 \t\tabc // 2 tabs
5013 \t abcˇ // Tab followed by space
5014 \tabc // Space followed by tab (3 spaces should be the result)
5015 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5016 abc // Already space indented
5017 «\t
5018 \tabc\tdef // Only the leading tab is manipulatedˇ»
5019 "}
5020 .replace("·", "")
5021 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5022 );
5023 cx.update_editor(|e, window, cx| {
5024 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5025 });
5026 cx.assert_editor_state(
5027 indoc! {"
5028 ·
5029 abc // No indentation
5030 « abc // 1 tabˇ»
5031 \t\tabc // 2 tabs
5032 « abc // Tab followed by spaceˇ»
5033 \tabc // Space followed by tab (3 spaces should be the result)
5034 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5035 abc // Already space indented
5036 « ·
5037 abc\tdef // Only the leading tab is manipulatedˇ»
5038 "}
5039 .replace("·", "")
5040 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5041 );
5042
5043 // SINGLE SELECTION
5044 // Ln.1 "«" tests empty lines
5045 // Ln.9 tests just leading whitespace
5046 cx.set_state(indoc! {"
5047 «
5048 abc // No indentation
5049 \tabc // 1 tab
5050 \t\tabc // 2 tabs
5051 \t abc // Tab followed by space
5052 \tabc // Space followed by tab (3 spaces should be the result)
5053 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5054 abc // Already space indented
5055 \t
5056 \tabc\tdef // Only the leading tab is manipulatedˇ»
5057 "});
5058 cx.update_editor(|e, window, cx| {
5059 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5060 });
5061 cx.assert_editor_state(
5062 indoc! {"
5063 «
5064 abc // No indentation
5065 abc // 1 tab
5066 abc // 2 tabs
5067 abc // Tab followed by space
5068 abc // Space followed by tab (3 spaces should be the result)
5069 abc // Mixed indentation (tab conversion depends on the column)
5070 abc // Already space indented
5071 ·
5072 abc\tdef // Only the leading tab is manipulatedˇ»
5073 "}
5074 .replace("·", "")
5075 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5076 );
5077}
5078
5079#[gpui::test]
5080async fn test_convert_indentation_to_tabs(cx: &mut TestAppContext) {
5081 init_test(cx, |settings| {
5082 settings.defaults.tab_size = NonZeroU32::new(3)
5083 });
5084
5085 let mut cx = EditorTestContext::new(cx).await;
5086
5087 // MULTI SELECTION
5088 // Ln.1 "«" tests empty lines
5089 // Ln.11 tests just leading whitespace
5090 cx.set_state(indoc! {"
5091 «
5092 abˇ»ˇc // No indentation
5093 abc ˇ ˇ // 1 space (< 3 so dont convert)
5094 abc « // 2 spaces (< 3 so dont convert)
5095 abc // 3 spaces (convert)
5096 abc ˇ» // 5 spaces (1 tab + 2 spaces)
5097 «\tˇ»\t«\tˇ»abc // Already tab indented
5098 «\t abc // Tab followed by space
5099 \tabc // Space followed by tab (should be consumed due to tab)
5100 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5101 \tˇ» «\t
5102 abcˇ» \t ˇˇˇ // Only the leading spaces should be converted
5103 "});
5104 cx.update_editor(|e, window, cx| {
5105 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5106 });
5107 cx.assert_editor_state(indoc! {"
5108 «
5109 abc // No indentation
5110 abc // 1 space (< 3 so dont convert)
5111 abc // 2 spaces (< 3 so dont convert)
5112 \tabc // 3 spaces (convert)
5113 \t abc // 5 spaces (1 tab + 2 spaces)
5114 \t\t\tabc // Already tab indented
5115 \t abc // Tab followed by space
5116 \tabc // Space followed by tab (should be consumed due to tab)
5117 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5118 \t\t\t
5119 \tabc \t // Only the leading spaces should be convertedˇ»
5120 "});
5121
5122 // Test on just a few lines, the other should remain unchanged
5123 // Only lines (4, 8, 11, 12) should change
5124 cx.set_state(
5125 indoc! {"
5126 ·
5127 abc // No indentation
5128 abc // 1 space (< 3 so dont convert)
5129 abc // 2 spaces (< 3 so dont convert)
5130 « abc // 3 spaces (convert)ˇ»
5131 abc // 5 spaces (1 tab + 2 spaces)
5132 \t\t\tabc // Already tab indented
5133 \t abc // Tab followed by space
5134 \tabc ˇ // Space followed by tab (should be consumed due to tab)
5135 \t\t \tabc // Mixed indentation
5136 \t \t \t \tabc // Mixed indentation
5137 \t \tˇ
5138 « abc \t // Only the leading spaces should be convertedˇ»
5139 "}
5140 .replace("·", "")
5141 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5142 );
5143 cx.update_editor(|e, window, cx| {
5144 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5145 });
5146 cx.assert_editor_state(
5147 indoc! {"
5148 ·
5149 abc // No indentation
5150 abc // 1 space (< 3 so dont convert)
5151 abc // 2 spaces (< 3 so dont convert)
5152 «\tabc // 3 spaces (convert)ˇ»
5153 abc // 5 spaces (1 tab + 2 spaces)
5154 \t\t\tabc // Already tab indented
5155 \t abc // Tab followed by space
5156 «\tabc // Space followed by tab (should be consumed due to tab)ˇ»
5157 \t\t \tabc // Mixed indentation
5158 \t \t \t \tabc // Mixed indentation
5159 «\t\t\t
5160 \tabc \t // Only the leading spaces should be convertedˇ»
5161 "}
5162 .replace("·", "")
5163 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5164 );
5165
5166 // SINGLE SELECTION
5167 // Ln.1 "«" tests empty lines
5168 // Ln.11 tests just leading whitespace
5169 cx.set_state(indoc! {"
5170 «
5171 abc // No indentation
5172 abc // 1 space (< 3 so dont convert)
5173 abc // 2 spaces (< 3 so dont convert)
5174 abc // 3 spaces (convert)
5175 abc // 5 spaces (1 tab + 2 spaces)
5176 \t\t\tabc // Already tab indented
5177 \t abc // Tab followed by space
5178 \tabc // Space followed by tab (should be consumed due to tab)
5179 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5180 \t \t
5181 abc \t // Only the leading spaces should be convertedˇ»
5182 "});
5183 cx.update_editor(|e, window, cx| {
5184 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5185 });
5186 cx.assert_editor_state(indoc! {"
5187 «
5188 abc // No indentation
5189 abc // 1 space (< 3 so dont convert)
5190 abc // 2 spaces (< 3 so dont convert)
5191 \tabc // 3 spaces (convert)
5192 \t abc // 5 spaces (1 tab + 2 spaces)
5193 \t\t\tabc // Already tab indented
5194 \t abc // Tab followed by space
5195 \tabc // Space followed by tab (should be consumed due to tab)
5196 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5197 \t\t\t
5198 \tabc \t // Only the leading spaces should be convertedˇ»
5199 "});
5200}
5201
5202#[gpui::test]
5203async fn test_toggle_case(cx: &mut TestAppContext) {
5204 init_test(cx, |_| {});
5205
5206 let mut cx = EditorTestContext::new(cx).await;
5207
5208 // If all lower case -> upper case
5209 cx.set_state(indoc! {"
5210 «hello worldˇ»
5211 "});
5212 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5213 cx.assert_editor_state(indoc! {"
5214 «HELLO WORLDˇ»
5215 "});
5216
5217 // If all upper case -> lower case
5218 cx.set_state(indoc! {"
5219 «HELLO WORLDˇ»
5220 "});
5221 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5222 cx.assert_editor_state(indoc! {"
5223 «hello worldˇ»
5224 "});
5225
5226 // If any upper case characters are identified -> lower case
5227 // This matches JetBrains IDEs
5228 cx.set_state(indoc! {"
5229 «hEllo worldˇ»
5230 "});
5231 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5232 cx.assert_editor_state(indoc! {"
5233 «hello worldˇ»
5234 "});
5235}
5236
5237#[gpui::test]
5238async fn test_convert_to_sentence_case(cx: &mut TestAppContext) {
5239 init_test(cx, |_| {});
5240
5241 let mut cx = EditorTestContext::new(cx).await;
5242
5243 cx.set_state(indoc! {"
5244 «implement-windows-supportˇ»
5245 "});
5246 cx.update_editor(|e, window, cx| {
5247 e.convert_to_sentence_case(&ConvertToSentenceCase, window, cx)
5248 });
5249 cx.assert_editor_state(indoc! {"
5250 «Implement windows supportˇ»
5251 "});
5252}
5253
5254#[gpui::test]
5255async fn test_manipulate_text(cx: &mut TestAppContext) {
5256 init_test(cx, |_| {});
5257
5258 let mut cx = EditorTestContext::new(cx).await;
5259
5260 // Test convert_to_upper_case()
5261 cx.set_state(indoc! {"
5262 «hello worldˇ»
5263 "});
5264 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5265 cx.assert_editor_state(indoc! {"
5266 «HELLO WORLDˇ»
5267 "});
5268
5269 // Test convert_to_lower_case()
5270 cx.set_state(indoc! {"
5271 «HELLO WORLDˇ»
5272 "});
5273 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
5274 cx.assert_editor_state(indoc! {"
5275 «hello worldˇ»
5276 "});
5277
5278 // Test multiple line, single selection case
5279 cx.set_state(indoc! {"
5280 «The quick brown
5281 fox jumps over
5282 the lazy dogˇ»
5283 "});
5284 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
5285 cx.assert_editor_state(indoc! {"
5286 «The Quick Brown
5287 Fox Jumps Over
5288 The Lazy Dogˇ»
5289 "});
5290
5291 // Test multiple line, single selection case
5292 cx.set_state(indoc! {"
5293 «The quick brown
5294 fox jumps over
5295 the lazy dogˇ»
5296 "});
5297 cx.update_editor(|e, window, cx| {
5298 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
5299 });
5300 cx.assert_editor_state(indoc! {"
5301 «TheQuickBrown
5302 FoxJumpsOver
5303 TheLazyDogˇ»
5304 "});
5305
5306 // From here on out, test more complex cases of manipulate_text()
5307
5308 // Test no selection case - should affect words cursors are in
5309 // Cursor at beginning, middle, and end of word
5310 cx.set_state(indoc! {"
5311 ˇhello big beauˇtiful worldˇ
5312 "});
5313 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5314 cx.assert_editor_state(indoc! {"
5315 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
5316 "});
5317
5318 // Test multiple selections on a single line and across multiple lines
5319 cx.set_state(indoc! {"
5320 «Theˇ» quick «brown
5321 foxˇ» jumps «overˇ»
5322 the «lazyˇ» dog
5323 "});
5324 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5325 cx.assert_editor_state(indoc! {"
5326 «THEˇ» quick «BROWN
5327 FOXˇ» jumps «OVERˇ»
5328 the «LAZYˇ» dog
5329 "});
5330
5331 // Test case where text length grows
5332 cx.set_state(indoc! {"
5333 «tschüߡ»
5334 "});
5335 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5336 cx.assert_editor_state(indoc! {"
5337 «TSCHÜSSˇ»
5338 "});
5339
5340 // Test to make sure we don't crash when text shrinks
5341 cx.set_state(indoc! {"
5342 aaa_bbbˇ
5343 "});
5344 cx.update_editor(|e, window, cx| {
5345 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5346 });
5347 cx.assert_editor_state(indoc! {"
5348 «aaaBbbˇ»
5349 "});
5350
5351 // Test to make sure we all aware of the fact that each word can grow and shrink
5352 // Final selections should be aware of this fact
5353 cx.set_state(indoc! {"
5354 aaa_bˇbb bbˇb_ccc ˇccc_ddd
5355 "});
5356 cx.update_editor(|e, window, cx| {
5357 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5358 });
5359 cx.assert_editor_state(indoc! {"
5360 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
5361 "});
5362
5363 cx.set_state(indoc! {"
5364 «hElLo, WoRld!ˇ»
5365 "});
5366 cx.update_editor(|e, window, cx| {
5367 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
5368 });
5369 cx.assert_editor_state(indoc! {"
5370 «HeLlO, wOrLD!ˇ»
5371 "});
5372
5373 // Test selections with `line_mode() = true`.
5374 cx.update_editor(|editor, _window, _cx| editor.selections.set_line_mode(true));
5375 cx.set_state(indoc! {"
5376 «The quick brown
5377 fox jumps over
5378 tˇ»he lazy dog
5379 "});
5380 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5381 cx.assert_editor_state(indoc! {"
5382 «THE QUICK BROWN
5383 FOX JUMPS OVER
5384 THE LAZY DOGˇ»
5385 "});
5386}
5387
5388#[gpui::test]
5389fn test_duplicate_line(cx: &mut TestAppContext) {
5390 init_test(cx, |_| {});
5391
5392 let editor = cx.add_window(|window, cx| {
5393 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5394 build_editor(buffer, window, cx)
5395 });
5396 _ = editor.update(cx, |editor, window, cx| {
5397 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5398 s.select_display_ranges([
5399 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5400 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5401 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5402 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5403 ])
5404 });
5405 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5406 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5407 assert_eq!(
5408 editor.selections.display_ranges(cx),
5409 vec![
5410 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
5411 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
5412 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5413 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
5414 ]
5415 );
5416 });
5417
5418 let editor = cx.add_window(|window, cx| {
5419 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5420 build_editor(buffer, window, cx)
5421 });
5422 _ = editor.update(cx, |editor, window, cx| {
5423 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5424 s.select_display_ranges([
5425 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5426 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5427 ])
5428 });
5429 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5430 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5431 assert_eq!(
5432 editor.selections.display_ranges(cx),
5433 vec![
5434 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
5435 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
5436 ]
5437 );
5438 });
5439
5440 // With `move_upwards` the selections stay in place, except for
5441 // the lines inserted above them
5442 let editor = cx.add_window(|window, cx| {
5443 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5444 build_editor(buffer, window, cx)
5445 });
5446 _ = editor.update(cx, |editor, window, cx| {
5447 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5448 s.select_display_ranges([
5449 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5450 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5451 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5452 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5453 ])
5454 });
5455 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5456 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5457 assert_eq!(
5458 editor.selections.display_ranges(cx),
5459 vec![
5460 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5461 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5462 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
5463 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
5464 ]
5465 );
5466 });
5467
5468 let editor = cx.add_window(|window, cx| {
5469 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5470 build_editor(buffer, window, cx)
5471 });
5472 _ = editor.update(cx, |editor, window, cx| {
5473 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5474 s.select_display_ranges([
5475 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5476 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5477 ])
5478 });
5479 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5480 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5481 assert_eq!(
5482 editor.selections.display_ranges(cx),
5483 vec![
5484 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5485 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5486 ]
5487 );
5488 });
5489
5490 let editor = cx.add_window(|window, cx| {
5491 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5492 build_editor(buffer, window, cx)
5493 });
5494 _ = editor.update(cx, |editor, window, cx| {
5495 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5496 s.select_display_ranges([
5497 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5498 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5499 ])
5500 });
5501 editor.duplicate_selection(&DuplicateSelection, window, cx);
5502 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
5503 assert_eq!(
5504 editor.selections.display_ranges(cx),
5505 vec![
5506 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5507 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
5508 ]
5509 );
5510 });
5511}
5512
5513#[gpui::test]
5514fn test_move_line_up_down(cx: &mut TestAppContext) {
5515 init_test(cx, |_| {});
5516
5517 let editor = cx.add_window(|window, cx| {
5518 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5519 build_editor(buffer, window, cx)
5520 });
5521 _ = editor.update(cx, |editor, window, cx| {
5522 editor.fold_creases(
5523 vec![
5524 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5525 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5526 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5527 ],
5528 true,
5529 window,
5530 cx,
5531 );
5532 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5533 s.select_display_ranges([
5534 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5535 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5536 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5537 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
5538 ])
5539 });
5540 assert_eq!(
5541 editor.display_text(cx),
5542 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
5543 );
5544
5545 editor.move_line_up(&MoveLineUp, window, cx);
5546 assert_eq!(
5547 editor.display_text(cx),
5548 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
5549 );
5550 assert_eq!(
5551 editor.selections.display_ranges(cx),
5552 vec![
5553 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5554 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5555 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5556 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5557 ]
5558 );
5559 });
5560
5561 _ = editor.update(cx, |editor, window, cx| {
5562 editor.move_line_down(&MoveLineDown, window, cx);
5563 assert_eq!(
5564 editor.display_text(cx),
5565 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
5566 );
5567 assert_eq!(
5568 editor.selections.display_ranges(cx),
5569 vec![
5570 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5571 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5572 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5573 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5574 ]
5575 );
5576 });
5577
5578 _ = editor.update(cx, |editor, window, cx| {
5579 editor.move_line_down(&MoveLineDown, window, cx);
5580 assert_eq!(
5581 editor.display_text(cx),
5582 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
5583 );
5584 assert_eq!(
5585 editor.selections.display_ranges(cx),
5586 vec![
5587 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5588 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5589 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5590 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5591 ]
5592 );
5593 });
5594
5595 _ = editor.update(cx, |editor, window, cx| {
5596 editor.move_line_up(&MoveLineUp, window, cx);
5597 assert_eq!(
5598 editor.display_text(cx),
5599 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
5600 );
5601 assert_eq!(
5602 editor.selections.display_ranges(cx),
5603 vec![
5604 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5605 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5606 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5607 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5608 ]
5609 );
5610 });
5611}
5612
5613#[gpui::test]
5614fn test_move_line_up_selection_at_end_of_fold(cx: &mut TestAppContext) {
5615 init_test(cx, |_| {});
5616 let editor = cx.add_window(|window, cx| {
5617 let buffer = MultiBuffer::build_simple("\n\n\n\n\n\naaaa\nbbbb\ncccc", cx);
5618 build_editor(buffer, window, cx)
5619 });
5620 _ = editor.update(cx, |editor, window, cx| {
5621 editor.fold_creases(
5622 vec![Crease::simple(
5623 Point::new(6, 4)..Point::new(7, 4),
5624 FoldPlaceholder::test(),
5625 )],
5626 true,
5627 window,
5628 cx,
5629 );
5630 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5631 s.select_ranges([Point::new(7, 4)..Point::new(7, 4)])
5632 });
5633 assert_eq!(editor.display_text(cx), "\n\n\n\n\n\naaaa⋯\ncccc");
5634 editor.move_line_up(&MoveLineUp, window, cx);
5635 let buffer_text = editor.buffer.read(cx).snapshot(cx).text();
5636 assert_eq!(buffer_text, "\n\n\n\n\naaaa\nbbbb\n\ncccc");
5637 });
5638}
5639
5640#[gpui::test]
5641fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
5642 init_test(cx, |_| {});
5643
5644 let editor = cx.add_window(|window, cx| {
5645 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5646 build_editor(buffer, window, cx)
5647 });
5648 _ = editor.update(cx, |editor, window, cx| {
5649 let snapshot = editor.buffer.read(cx).snapshot(cx);
5650 editor.insert_blocks(
5651 [BlockProperties {
5652 style: BlockStyle::Fixed,
5653 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
5654 height: Some(1),
5655 render: Arc::new(|_| div().into_any()),
5656 priority: 0,
5657 }],
5658 Some(Autoscroll::fit()),
5659 cx,
5660 );
5661 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5662 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
5663 });
5664 editor.move_line_down(&MoveLineDown, window, cx);
5665 });
5666}
5667
5668#[gpui::test]
5669async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
5670 init_test(cx, |_| {});
5671
5672 let mut cx = EditorTestContext::new(cx).await;
5673 cx.set_state(
5674 &"
5675 ˇzero
5676 one
5677 two
5678 three
5679 four
5680 five
5681 "
5682 .unindent(),
5683 );
5684
5685 // Create a four-line block that replaces three lines of text.
5686 cx.update_editor(|editor, window, cx| {
5687 let snapshot = editor.snapshot(window, cx);
5688 let snapshot = &snapshot.buffer_snapshot;
5689 let placement = BlockPlacement::Replace(
5690 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
5691 );
5692 editor.insert_blocks(
5693 [BlockProperties {
5694 placement,
5695 height: Some(4),
5696 style: BlockStyle::Sticky,
5697 render: Arc::new(|_| gpui::div().into_any_element()),
5698 priority: 0,
5699 }],
5700 None,
5701 cx,
5702 );
5703 });
5704
5705 // Move down so that the cursor touches the block.
5706 cx.update_editor(|editor, window, cx| {
5707 editor.move_down(&Default::default(), window, cx);
5708 });
5709 cx.assert_editor_state(
5710 &"
5711 zero
5712 «one
5713 two
5714 threeˇ»
5715 four
5716 five
5717 "
5718 .unindent(),
5719 );
5720
5721 // Move down past the block.
5722 cx.update_editor(|editor, window, cx| {
5723 editor.move_down(&Default::default(), window, cx);
5724 });
5725 cx.assert_editor_state(
5726 &"
5727 zero
5728 one
5729 two
5730 three
5731 ˇfour
5732 five
5733 "
5734 .unindent(),
5735 );
5736}
5737
5738#[gpui::test]
5739fn test_transpose(cx: &mut TestAppContext) {
5740 init_test(cx, |_| {});
5741
5742 _ = cx.add_window(|window, cx| {
5743 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
5744 editor.set_style(EditorStyle::default(), window, cx);
5745 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5746 s.select_ranges([1..1])
5747 });
5748 editor.transpose(&Default::default(), window, cx);
5749 assert_eq!(editor.text(cx), "bac");
5750 assert_eq!(editor.selections.ranges(cx), [2..2]);
5751
5752 editor.transpose(&Default::default(), window, cx);
5753 assert_eq!(editor.text(cx), "bca");
5754 assert_eq!(editor.selections.ranges(cx), [3..3]);
5755
5756 editor.transpose(&Default::default(), window, cx);
5757 assert_eq!(editor.text(cx), "bac");
5758 assert_eq!(editor.selections.ranges(cx), [3..3]);
5759
5760 editor
5761 });
5762
5763 _ = cx.add_window(|window, cx| {
5764 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5765 editor.set_style(EditorStyle::default(), window, cx);
5766 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5767 s.select_ranges([3..3])
5768 });
5769 editor.transpose(&Default::default(), window, cx);
5770 assert_eq!(editor.text(cx), "acb\nde");
5771 assert_eq!(editor.selections.ranges(cx), [3..3]);
5772
5773 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5774 s.select_ranges([4..4])
5775 });
5776 editor.transpose(&Default::default(), window, cx);
5777 assert_eq!(editor.text(cx), "acbd\ne");
5778 assert_eq!(editor.selections.ranges(cx), [5..5]);
5779
5780 editor.transpose(&Default::default(), window, cx);
5781 assert_eq!(editor.text(cx), "acbde\n");
5782 assert_eq!(editor.selections.ranges(cx), [6..6]);
5783
5784 editor.transpose(&Default::default(), window, cx);
5785 assert_eq!(editor.text(cx), "acbd\ne");
5786 assert_eq!(editor.selections.ranges(cx), [6..6]);
5787
5788 editor
5789 });
5790
5791 _ = cx.add_window(|window, cx| {
5792 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5793 editor.set_style(EditorStyle::default(), window, cx);
5794 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5795 s.select_ranges([1..1, 2..2, 4..4])
5796 });
5797 editor.transpose(&Default::default(), window, cx);
5798 assert_eq!(editor.text(cx), "bacd\ne");
5799 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
5800
5801 editor.transpose(&Default::default(), window, cx);
5802 assert_eq!(editor.text(cx), "bcade\n");
5803 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
5804
5805 editor.transpose(&Default::default(), window, cx);
5806 assert_eq!(editor.text(cx), "bcda\ne");
5807 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5808
5809 editor.transpose(&Default::default(), window, cx);
5810 assert_eq!(editor.text(cx), "bcade\n");
5811 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5812
5813 editor.transpose(&Default::default(), window, cx);
5814 assert_eq!(editor.text(cx), "bcaed\n");
5815 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
5816
5817 editor
5818 });
5819
5820 _ = cx.add_window(|window, cx| {
5821 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
5822 editor.set_style(EditorStyle::default(), window, cx);
5823 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5824 s.select_ranges([4..4])
5825 });
5826 editor.transpose(&Default::default(), window, cx);
5827 assert_eq!(editor.text(cx), "🏀🍐✋");
5828 assert_eq!(editor.selections.ranges(cx), [8..8]);
5829
5830 editor.transpose(&Default::default(), window, cx);
5831 assert_eq!(editor.text(cx), "🏀✋🍐");
5832 assert_eq!(editor.selections.ranges(cx), [11..11]);
5833
5834 editor.transpose(&Default::default(), window, cx);
5835 assert_eq!(editor.text(cx), "🏀🍐✋");
5836 assert_eq!(editor.selections.ranges(cx), [11..11]);
5837
5838 editor
5839 });
5840}
5841
5842#[gpui::test]
5843async fn test_rewrap(cx: &mut TestAppContext) {
5844 init_test(cx, |settings| {
5845 settings.languages.0.extend([
5846 (
5847 "Markdown".into(),
5848 LanguageSettingsContent {
5849 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5850 preferred_line_length: Some(40),
5851 ..Default::default()
5852 },
5853 ),
5854 (
5855 "Plain Text".into(),
5856 LanguageSettingsContent {
5857 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5858 preferred_line_length: Some(40),
5859 ..Default::default()
5860 },
5861 ),
5862 (
5863 "C++".into(),
5864 LanguageSettingsContent {
5865 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5866 preferred_line_length: Some(40),
5867 ..Default::default()
5868 },
5869 ),
5870 (
5871 "Python".into(),
5872 LanguageSettingsContent {
5873 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5874 preferred_line_length: Some(40),
5875 ..Default::default()
5876 },
5877 ),
5878 (
5879 "Rust".into(),
5880 LanguageSettingsContent {
5881 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5882 preferred_line_length: Some(40),
5883 ..Default::default()
5884 },
5885 ),
5886 ])
5887 });
5888
5889 let mut cx = EditorTestContext::new(cx).await;
5890
5891 let cpp_language = Arc::new(Language::new(
5892 LanguageConfig {
5893 name: "C++".into(),
5894 line_comments: vec!["// ".into()],
5895 ..LanguageConfig::default()
5896 },
5897 None,
5898 ));
5899 let python_language = Arc::new(Language::new(
5900 LanguageConfig {
5901 name: "Python".into(),
5902 line_comments: vec!["# ".into()],
5903 ..LanguageConfig::default()
5904 },
5905 None,
5906 ));
5907 let markdown_language = Arc::new(Language::new(
5908 LanguageConfig {
5909 name: "Markdown".into(),
5910 rewrap_prefixes: vec![
5911 regex::Regex::new("\\d+\\.\\s+").unwrap(),
5912 regex::Regex::new("[-*+]\\s+").unwrap(),
5913 ],
5914 ..LanguageConfig::default()
5915 },
5916 None,
5917 ));
5918 let rust_language = Arc::new(
5919 Language::new(
5920 LanguageConfig {
5921 name: "Rust".into(),
5922 line_comments: vec!["// ".into(), "/// ".into()],
5923 ..LanguageConfig::default()
5924 },
5925 Some(tree_sitter_rust::LANGUAGE.into()),
5926 )
5927 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
5928 .unwrap(),
5929 );
5930
5931 let plaintext_language = Arc::new(Language::new(
5932 LanguageConfig {
5933 name: "Plain Text".into(),
5934 ..LanguageConfig::default()
5935 },
5936 None,
5937 ));
5938
5939 // Test basic rewrapping of a long line with a cursor
5940 assert_rewrap(
5941 indoc! {"
5942 // ˇThis is a long comment that needs to be wrapped.
5943 "},
5944 indoc! {"
5945 // ˇThis is a long comment that needs to
5946 // be wrapped.
5947 "},
5948 cpp_language.clone(),
5949 &mut cx,
5950 );
5951
5952 // Test rewrapping a full selection
5953 assert_rewrap(
5954 indoc! {"
5955 «// This selected long comment needs to be wrapped.ˇ»"
5956 },
5957 indoc! {"
5958 «// This selected long comment needs to
5959 // be wrapped.ˇ»"
5960 },
5961 cpp_language.clone(),
5962 &mut cx,
5963 );
5964
5965 // Test multiple cursors on different lines within the same paragraph are preserved after rewrapping
5966 assert_rewrap(
5967 indoc! {"
5968 // ˇThis is the first line.
5969 // Thisˇ is the second line.
5970 // This is the thirdˇ line, all part of one paragraph.
5971 "},
5972 indoc! {"
5973 // ˇThis is the first line. Thisˇ is the
5974 // second line. This is the thirdˇ line,
5975 // all part of one paragraph.
5976 "},
5977 cpp_language.clone(),
5978 &mut cx,
5979 );
5980
5981 // Test multiple cursors in different paragraphs trigger separate rewraps
5982 assert_rewrap(
5983 indoc! {"
5984 // ˇThis is the first paragraph, first line.
5985 // ˇThis is the first paragraph, second line.
5986
5987 // ˇThis is the second paragraph, first line.
5988 // ˇThis is the second paragraph, second line.
5989 "},
5990 indoc! {"
5991 // ˇThis is the first paragraph, first
5992 // line. ˇThis is the first paragraph,
5993 // second line.
5994
5995 // ˇThis is the second paragraph, first
5996 // line. ˇThis is the second paragraph,
5997 // second line.
5998 "},
5999 cpp_language.clone(),
6000 &mut cx,
6001 );
6002
6003 // Test that change in comment prefix (e.g., `//` to `///`) trigger seperate rewraps
6004 assert_rewrap(
6005 indoc! {"
6006 «// A regular long long comment to be wrapped.
6007 /// A documentation long comment to be wrapped.ˇ»
6008 "},
6009 indoc! {"
6010 «// A regular long long comment to be
6011 // wrapped.
6012 /// A documentation long comment to be
6013 /// wrapped.ˇ»
6014 "},
6015 rust_language.clone(),
6016 &mut cx,
6017 );
6018
6019 // Test that change in indentation level trigger seperate rewraps
6020 assert_rewrap(
6021 indoc! {"
6022 fn foo() {
6023 «// This is a long comment at the base indent.
6024 // This is a long comment at the next indent.ˇ»
6025 }
6026 "},
6027 indoc! {"
6028 fn foo() {
6029 «// This is a long comment at the
6030 // base indent.
6031 // This is a long comment at the
6032 // next indent.ˇ»
6033 }
6034 "},
6035 rust_language.clone(),
6036 &mut cx,
6037 );
6038
6039 // Test that different comment prefix characters (e.g., '#') are handled correctly
6040 assert_rewrap(
6041 indoc! {"
6042 # ˇThis is a long comment using a pound sign.
6043 "},
6044 indoc! {"
6045 # ˇThis is a long comment using a pound
6046 # sign.
6047 "},
6048 python_language,
6049 &mut cx,
6050 );
6051
6052 // Test rewrapping only affects comments, not code even when selected
6053 assert_rewrap(
6054 indoc! {"
6055 «/// This doc comment is long and should be wrapped.
6056 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6057 "},
6058 indoc! {"
6059 «/// This doc comment is long and should
6060 /// be wrapped.
6061 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6062 "},
6063 rust_language.clone(),
6064 &mut cx,
6065 );
6066
6067 // Test that rewrapping works in Markdown documents where `allow_rewrap` is `Anywhere`
6068 assert_rewrap(
6069 indoc! {"
6070 # Header
6071
6072 A long long long line of markdown text to wrap.ˇ
6073 "},
6074 indoc! {"
6075 # Header
6076
6077 A long long long line of markdown text
6078 to wrap.ˇ
6079 "},
6080 markdown_language.clone(),
6081 &mut cx,
6082 );
6083
6084 // Test that rewrapping boundary works and preserves relative indent for Markdown documents
6085 assert_rewrap(
6086 indoc! {"
6087 «1. This is a numbered list item that is very long and needs to be wrapped properly.
6088 2. This is a numbered list item that is very long and needs to be wrapped properly.
6089 - This is an unordered list item that is also very long and should not merge with the numbered item.ˇ»
6090 "},
6091 indoc! {"
6092 «1. This is a numbered list item that is
6093 very long and needs to be wrapped
6094 properly.
6095 2. This is a numbered list item that is
6096 very long and needs to be wrapped
6097 properly.
6098 - This is an unordered list item that is
6099 also very long and should not merge
6100 with the numbered item.ˇ»
6101 "},
6102 markdown_language.clone(),
6103 &mut cx,
6104 );
6105
6106 // Test that rewrapping add indents for rewrapping boundary if not exists already.
6107 assert_rewrap(
6108 indoc! {"
6109 «1. This is a numbered list item that is
6110 very long and needs to be wrapped
6111 properly.
6112 2. This is a numbered list item that is
6113 very long and needs to be wrapped
6114 properly.
6115 - This is an unordered list item that is
6116 also very long and should not merge with
6117 the numbered item.ˇ»
6118 "},
6119 indoc! {"
6120 «1. This is a numbered list item that is
6121 very long and needs to be wrapped
6122 properly.
6123 2. This is a numbered list item that is
6124 very long and needs to be wrapped
6125 properly.
6126 - This is an unordered list item that is
6127 also very long and should not merge
6128 with the numbered item.ˇ»
6129 "},
6130 markdown_language.clone(),
6131 &mut cx,
6132 );
6133
6134 // Test that rewrapping maintain indents even when they already exists.
6135 assert_rewrap(
6136 indoc! {"
6137 «1. This is a numbered list
6138 item that is very long and needs to be wrapped properly.
6139 2. This is a numbered list
6140 item that is very long and needs to be wrapped properly.
6141 - This is an unordered list item that is also very long and
6142 should not merge with the numbered item.ˇ»
6143 "},
6144 indoc! {"
6145 «1. This is a numbered list item that is
6146 very long and needs to be wrapped
6147 properly.
6148 2. This is a numbered list item that is
6149 very long and needs to be wrapped
6150 properly.
6151 - This is an unordered list item that is
6152 also very long and should not merge
6153 with the numbered item.ˇ»
6154 "},
6155 markdown_language,
6156 &mut cx,
6157 );
6158
6159 // Test that rewrapping works in plain text where `allow_rewrap` is `Anywhere`
6160 assert_rewrap(
6161 indoc! {"
6162 ˇThis is a very long line of plain text that will be wrapped.
6163 "},
6164 indoc! {"
6165 ˇThis is a very long line of plain text
6166 that will be wrapped.
6167 "},
6168 plaintext_language.clone(),
6169 &mut cx,
6170 );
6171
6172 // Test that non-commented code acts as a paragraph boundary within a selection
6173 assert_rewrap(
6174 indoc! {"
6175 «// This is the first long comment block to be wrapped.
6176 fn my_func(a: u32);
6177 // This is the second long comment block to be wrapped.ˇ»
6178 "},
6179 indoc! {"
6180 «// This is the first long comment block
6181 // to be wrapped.
6182 fn my_func(a: u32);
6183 // This is the second long comment block
6184 // to be wrapped.ˇ»
6185 "},
6186 rust_language,
6187 &mut cx,
6188 );
6189
6190 // Test rewrapping multiple selections, including ones with blank lines or tabs
6191 assert_rewrap(
6192 indoc! {"
6193 «ˇThis is a very long line that will be wrapped.
6194
6195 This is another paragraph in the same selection.»
6196
6197 «\tThis is a very long indented line that will be wrapped.ˇ»
6198 "},
6199 indoc! {"
6200 «ˇThis is a very long line that will be
6201 wrapped.
6202
6203 This is another paragraph in the same
6204 selection.»
6205
6206 «\tThis is a very long indented line
6207 \tthat will be wrapped.ˇ»
6208 "},
6209 plaintext_language,
6210 &mut cx,
6211 );
6212
6213 // Test that an empty comment line acts as a paragraph boundary
6214 assert_rewrap(
6215 indoc! {"
6216 // ˇThis is a long comment that will be wrapped.
6217 //
6218 // And this is another long comment that will also be wrapped.ˇ
6219 "},
6220 indoc! {"
6221 // ˇThis is a long comment that will be
6222 // wrapped.
6223 //
6224 // And this is another long comment that
6225 // will also be wrapped.ˇ
6226 "},
6227 cpp_language,
6228 &mut cx,
6229 );
6230
6231 #[track_caller]
6232 fn assert_rewrap(
6233 unwrapped_text: &str,
6234 wrapped_text: &str,
6235 language: Arc<Language>,
6236 cx: &mut EditorTestContext,
6237 ) {
6238 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6239 cx.set_state(unwrapped_text);
6240 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6241 cx.assert_editor_state(wrapped_text);
6242 }
6243}
6244
6245#[gpui::test]
6246async fn test_rewrap_block_comments(cx: &mut TestAppContext) {
6247 init_test(cx, |settings| {
6248 settings.languages.0.extend([(
6249 "Rust".into(),
6250 LanguageSettingsContent {
6251 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6252 preferred_line_length: Some(40),
6253 ..Default::default()
6254 },
6255 )])
6256 });
6257
6258 let mut cx = EditorTestContext::new(cx).await;
6259
6260 let rust_lang = Arc::new(
6261 Language::new(
6262 LanguageConfig {
6263 name: "Rust".into(),
6264 line_comments: vec!["// ".into()],
6265 block_comment: Some(BlockCommentConfig {
6266 start: "/*".into(),
6267 end: "*/".into(),
6268 prefix: "* ".into(),
6269 tab_size: 1,
6270 }),
6271 documentation_comment: Some(BlockCommentConfig {
6272 start: "/**".into(),
6273 end: "*/".into(),
6274 prefix: "* ".into(),
6275 tab_size: 1,
6276 }),
6277
6278 ..LanguageConfig::default()
6279 },
6280 Some(tree_sitter_rust::LANGUAGE.into()),
6281 )
6282 .with_override_query("[(line_comment) (block_comment)] @comment.inclusive")
6283 .unwrap(),
6284 );
6285
6286 // regular block comment
6287 assert_rewrap(
6288 indoc! {"
6289 /*
6290 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6291 */
6292 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6293 "},
6294 indoc! {"
6295 /*
6296 *ˇ Lorem ipsum dolor sit amet,
6297 * consectetur adipiscing elit.
6298 */
6299 /*
6300 *ˇ Lorem ipsum dolor sit amet,
6301 * consectetur adipiscing elit.
6302 */
6303 "},
6304 rust_lang.clone(),
6305 &mut cx,
6306 );
6307
6308 // indent is respected
6309 assert_rewrap(
6310 indoc! {"
6311 {}
6312 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6313 "},
6314 indoc! {"
6315 {}
6316 /*
6317 *ˇ Lorem ipsum dolor sit amet,
6318 * consectetur adipiscing elit.
6319 */
6320 "},
6321 rust_lang.clone(),
6322 &mut cx,
6323 );
6324
6325 // short block comments with inline delimiters
6326 assert_rewrap(
6327 indoc! {"
6328 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6329 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6330 */
6331 /*
6332 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6333 "},
6334 indoc! {"
6335 /*
6336 *ˇ Lorem ipsum dolor sit amet,
6337 * consectetur adipiscing elit.
6338 */
6339 /*
6340 *ˇ Lorem ipsum dolor sit amet,
6341 * consectetur adipiscing elit.
6342 */
6343 /*
6344 *ˇ Lorem ipsum dolor sit amet,
6345 * consectetur adipiscing elit.
6346 */
6347 "},
6348 rust_lang.clone(),
6349 &mut cx,
6350 );
6351
6352 // multiline block comment with inline start/end delimiters
6353 assert_rewrap(
6354 indoc! {"
6355 /*ˇ Lorem ipsum dolor sit amet,
6356 * consectetur adipiscing elit. */
6357 "},
6358 indoc! {"
6359 /*
6360 *ˇ Lorem ipsum dolor sit amet,
6361 * consectetur adipiscing elit.
6362 */
6363 "},
6364 rust_lang.clone(),
6365 &mut cx,
6366 );
6367
6368 // block comment rewrap still respects paragraph bounds
6369 assert_rewrap(
6370 indoc! {"
6371 /*
6372 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6373 *
6374 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6375 */
6376 "},
6377 indoc! {"
6378 /*
6379 *ˇ Lorem ipsum dolor sit amet,
6380 * consectetur adipiscing elit.
6381 *
6382 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6383 */
6384 "},
6385 rust_lang.clone(),
6386 &mut cx,
6387 );
6388
6389 // documentation comments
6390 assert_rewrap(
6391 indoc! {"
6392 /**ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6393 /**
6394 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6395 */
6396 "},
6397 indoc! {"
6398 /**
6399 *ˇ Lorem ipsum dolor sit amet,
6400 * consectetur adipiscing elit.
6401 */
6402 /**
6403 *ˇ Lorem ipsum dolor sit amet,
6404 * consectetur adipiscing elit.
6405 */
6406 "},
6407 rust_lang.clone(),
6408 &mut cx,
6409 );
6410
6411 // different, adjacent comments
6412 assert_rewrap(
6413 indoc! {"
6414 /**
6415 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6416 */
6417 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6418 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6419 "},
6420 indoc! {"
6421 /**
6422 *ˇ Lorem ipsum dolor sit amet,
6423 * consectetur adipiscing elit.
6424 */
6425 /*
6426 *ˇ Lorem ipsum dolor sit amet,
6427 * consectetur adipiscing elit.
6428 */
6429 //ˇ Lorem ipsum dolor sit amet,
6430 // consectetur adipiscing elit.
6431 "},
6432 rust_lang.clone(),
6433 &mut cx,
6434 );
6435
6436 // selection w/ single short block comment
6437 assert_rewrap(
6438 indoc! {"
6439 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6440 "},
6441 indoc! {"
6442 «/*
6443 * Lorem ipsum dolor sit amet,
6444 * consectetur adipiscing elit.
6445 */ˇ»
6446 "},
6447 rust_lang.clone(),
6448 &mut cx,
6449 );
6450
6451 // rewrapping a single comment w/ abutting comments
6452 assert_rewrap(
6453 indoc! {"
6454 /* ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. */
6455 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6456 "},
6457 indoc! {"
6458 /*
6459 * ˇLorem ipsum dolor sit amet,
6460 * consectetur adipiscing elit.
6461 */
6462 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6463 "},
6464 rust_lang.clone(),
6465 &mut cx,
6466 );
6467
6468 // selection w/ non-abutting short block comments
6469 assert_rewrap(
6470 indoc! {"
6471 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6472
6473 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6474 "},
6475 indoc! {"
6476 «/*
6477 * Lorem ipsum dolor sit amet,
6478 * consectetur adipiscing elit.
6479 */
6480
6481 /*
6482 * Lorem ipsum dolor sit amet,
6483 * consectetur adipiscing elit.
6484 */ˇ»
6485 "},
6486 rust_lang.clone(),
6487 &mut cx,
6488 );
6489
6490 // selection of multiline block comments
6491 assert_rewrap(
6492 indoc! {"
6493 «/* Lorem ipsum dolor sit amet,
6494 * consectetur adipiscing elit. */ˇ»
6495 "},
6496 indoc! {"
6497 «/*
6498 * Lorem ipsum dolor sit amet,
6499 * consectetur adipiscing elit.
6500 */ˇ»
6501 "},
6502 rust_lang.clone(),
6503 &mut cx,
6504 );
6505
6506 // partial selection of multiline block comments
6507 assert_rewrap(
6508 indoc! {"
6509 «/* Lorem ipsum dolor sit amet,ˇ»
6510 * consectetur adipiscing elit. */
6511 /* Lorem ipsum dolor sit amet,
6512 «* consectetur adipiscing elit. */ˇ»
6513 "},
6514 indoc! {"
6515 «/*
6516 * Lorem ipsum dolor sit amet,ˇ»
6517 * consectetur adipiscing elit. */
6518 /* Lorem ipsum dolor sit amet,
6519 «* consectetur adipiscing elit.
6520 */ˇ»
6521 "},
6522 rust_lang.clone(),
6523 &mut cx,
6524 );
6525
6526 // selection w/ abutting short block comments
6527 // TODO: should not be combined; should rewrap as 2 comments
6528 assert_rewrap(
6529 indoc! {"
6530 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6531 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6532 "},
6533 // desired behavior:
6534 // indoc! {"
6535 // «/*
6536 // * Lorem ipsum dolor sit amet,
6537 // * consectetur adipiscing elit.
6538 // */
6539 // /*
6540 // * Lorem ipsum dolor sit amet,
6541 // * consectetur adipiscing elit.
6542 // */ˇ»
6543 // "},
6544 // actual behaviour:
6545 indoc! {"
6546 «/*
6547 * Lorem ipsum dolor sit amet,
6548 * consectetur adipiscing elit. Lorem
6549 * ipsum dolor sit amet, consectetur
6550 * adipiscing elit.
6551 */ˇ»
6552 "},
6553 rust_lang.clone(),
6554 &mut cx,
6555 );
6556
6557 // TODO: same as above, but with delimiters on separate line
6558 // assert_rewrap(
6559 // indoc! {"
6560 // «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6561 // */
6562 // /*
6563 // * Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6564 // "},
6565 // // desired:
6566 // // indoc! {"
6567 // // «/*
6568 // // * Lorem ipsum dolor sit amet,
6569 // // * consectetur adipiscing elit.
6570 // // */
6571 // // /*
6572 // // * Lorem ipsum dolor sit amet,
6573 // // * consectetur adipiscing elit.
6574 // // */ˇ»
6575 // // "},
6576 // // actual: (but with trailing w/s on the empty lines)
6577 // indoc! {"
6578 // «/*
6579 // * Lorem ipsum dolor sit amet,
6580 // * consectetur adipiscing elit.
6581 // *
6582 // */
6583 // /*
6584 // *
6585 // * Lorem ipsum dolor sit amet,
6586 // * consectetur adipiscing elit.
6587 // */ˇ»
6588 // "},
6589 // rust_lang.clone(),
6590 // &mut cx,
6591 // );
6592
6593 // TODO these are unhandled edge cases; not correct, just documenting known issues
6594 assert_rewrap(
6595 indoc! {"
6596 /*
6597 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6598 */
6599 /*
6600 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6601 /*ˇ Lorem ipsum dolor sit amet */ /* consectetur adipiscing elit. */
6602 "},
6603 // desired:
6604 // indoc! {"
6605 // /*
6606 // *ˇ Lorem ipsum dolor sit amet,
6607 // * consectetur adipiscing elit.
6608 // */
6609 // /*
6610 // *ˇ Lorem ipsum dolor sit amet,
6611 // * consectetur adipiscing elit.
6612 // */
6613 // /*
6614 // *ˇ Lorem ipsum dolor sit amet
6615 // */ /* consectetur adipiscing elit. */
6616 // "},
6617 // actual:
6618 indoc! {"
6619 /*
6620 //ˇ Lorem ipsum dolor sit amet,
6621 // consectetur adipiscing elit.
6622 */
6623 /*
6624 * //ˇ Lorem ipsum dolor sit amet,
6625 * consectetur adipiscing elit.
6626 */
6627 /*
6628 *ˇ Lorem ipsum dolor sit amet */ /*
6629 * consectetur adipiscing elit.
6630 */
6631 "},
6632 rust_lang,
6633 &mut cx,
6634 );
6635
6636 #[track_caller]
6637 fn assert_rewrap(
6638 unwrapped_text: &str,
6639 wrapped_text: &str,
6640 language: Arc<Language>,
6641 cx: &mut EditorTestContext,
6642 ) {
6643 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6644 cx.set_state(unwrapped_text);
6645 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6646 cx.assert_editor_state(wrapped_text);
6647 }
6648}
6649
6650#[gpui::test]
6651async fn test_hard_wrap(cx: &mut TestAppContext) {
6652 init_test(cx, |_| {});
6653 let mut cx = EditorTestContext::new(cx).await;
6654
6655 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
6656 cx.update_editor(|editor, _, cx| {
6657 editor.set_hard_wrap(Some(14), cx);
6658 });
6659
6660 cx.set_state(indoc!(
6661 "
6662 one two three ˇ
6663 "
6664 ));
6665 cx.simulate_input("four");
6666 cx.run_until_parked();
6667
6668 cx.assert_editor_state(indoc!(
6669 "
6670 one two three
6671 fourˇ
6672 "
6673 ));
6674
6675 cx.update_editor(|editor, window, cx| {
6676 editor.newline(&Default::default(), window, cx);
6677 });
6678 cx.run_until_parked();
6679 cx.assert_editor_state(indoc!(
6680 "
6681 one two three
6682 four
6683 ˇ
6684 "
6685 ));
6686
6687 cx.simulate_input("five");
6688 cx.run_until_parked();
6689 cx.assert_editor_state(indoc!(
6690 "
6691 one two three
6692 four
6693 fiveˇ
6694 "
6695 ));
6696
6697 cx.update_editor(|editor, window, cx| {
6698 editor.newline(&Default::default(), window, cx);
6699 });
6700 cx.run_until_parked();
6701 cx.simulate_input("# ");
6702 cx.run_until_parked();
6703 cx.assert_editor_state(indoc!(
6704 "
6705 one two three
6706 four
6707 five
6708 # ˇ
6709 "
6710 ));
6711
6712 cx.update_editor(|editor, window, cx| {
6713 editor.newline(&Default::default(), window, cx);
6714 });
6715 cx.run_until_parked();
6716 cx.assert_editor_state(indoc!(
6717 "
6718 one two three
6719 four
6720 five
6721 #\x20
6722 #ˇ
6723 "
6724 ));
6725
6726 cx.simulate_input(" 6");
6727 cx.run_until_parked();
6728 cx.assert_editor_state(indoc!(
6729 "
6730 one two three
6731 four
6732 five
6733 #
6734 # 6ˇ
6735 "
6736 ));
6737}
6738
6739#[gpui::test]
6740async fn test_cut_line_ends(cx: &mut TestAppContext) {
6741 init_test(cx, |_| {});
6742
6743 let mut cx = EditorTestContext::new(cx).await;
6744
6745 cx.set_state(indoc! {"The quick brownˇ"});
6746 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
6747 cx.assert_editor_state(indoc! {"The quick brownˇ"});
6748
6749 cx.set_state(indoc! {"The emacs foxˇ"});
6750 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
6751 cx.assert_editor_state(indoc! {"The emacs foxˇ"});
6752
6753 cx.set_state(indoc! {"
6754 The quick« brownˇ»
6755 fox jumps overˇ
6756 the lazy dog"});
6757 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6758 cx.assert_editor_state(indoc! {"
6759 The quickˇ
6760 ˇthe lazy dog"});
6761
6762 cx.set_state(indoc! {"
6763 The quick« brownˇ»
6764 fox jumps overˇ
6765 the lazy dog"});
6766 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
6767 cx.assert_editor_state(indoc! {"
6768 The quickˇ
6769 fox jumps overˇthe lazy dog"});
6770
6771 cx.set_state(indoc! {"
6772 The quick« brownˇ»
6773 fox jumps overˇ
6774 the lazy dog"});
6775 cx.update_editor(|e, window, cx| {
6776 e.cut_to_end_of_line(
6777 &CutToEndOfLine {
6778 stop_at_newlines: true,
6779 },
6780 window,
6781 cx,
6782 )
6783 });
6784 cx.assert_editor_state(indoc! {"
6785 The quickˇ
6786 fox jumps overˇ
6787 the lazy dog"});
6788
6789 cx.set_state(indoc! {"
6790 The quick« brownˇ»
6791 fox jumps overˇ
6792 the lazy dog"});
6793 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
6794 cx.assert_editor_state(indoc! {"
6795 The quickˇ
6796 fox jumps overˇthe lazy dog"});
6797}
6798
6799#[gpui::test]
6800async fn test_clipboard(cx: &mut TestAppContext) {
6801 init_test(cx, |_| {});
6802
6803 let mut cx = EditorTestContext::new(cx).await;
6804
6805 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
6806 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6807 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
6808
6809 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
6810 cx.set_state("two ˇfour ˇsix ˇ");
6811 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6812 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
6813
6814 // Paste again but with only two cursors. Since the number of cursors doesn't
6815 // match the number of slices in the clipboard, the entire clipboard text
6816 // is pasted at each cursor.
6817 cx.set_state("ˇtwo one✅ four three six five ˇ");
6818 cx.update_editor(|e, window, cx| {
6819 e.handle_input("( ", window, cx);
6820 e.paste(&Paste, window, cx);
6821 e.handle_input(") ", window, cx);
6822 });
6823 cx.assert_editor_state(
6824 &([
6825 "( one✅ ",
6826 "three ",
6827 "five ) ˇtwo one✅ four three six five ( one✅ ",
6828 "three ",
6829 "five ) ˇ",
6830 ]
6831 .join("\n")),
6832 );
6833
6834 // Cut with three selections, one of which is full-line.
6835 cx.set_state(indoc! {"
6836 1«2ˇ»3
6837 4ˇ567
6838 «8ˇ»9"});
6839 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6840 cx.assert_editor_state(indoc! {"
6841 1ˇ3
6842 ˇ9"});
6843
6844 // Paste with three selections, noticing how the copied selection that was full-line
6845 // gets inserted before the second cursor.
6846 cx.set_state(indoc! {"
6847 1ˇ3
6848 9ˇ
6849 «oˇ»ne"});
6850 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6851 cx.assert_editor_state(indoc! {"
6852 12ˇ3
6853 4567
6854 9ˇ
6855 8ˇne"});
6856
6857 // Copy with a single cursor only, which writes the whole line into the clipboard.
6858 cx.set_state(indoc! {"
6859 The quick brown
6860 fox juˇmps over
6861 the lazy dog"});
6862 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6863 assert_eq!(
6864 cx.read_from_clipboard()
6865 .and_then(|item| item.text().as_deref().map(str::to_string)),
6866 Some("fox jumps over\n".to_string())
6867 );
6868
6869 // Paste with three selections, noticing how the copied full-line selection is inserted
6870 // before the empty selections but replaces the selection that is non-empty.
6871 cx.set_state(indoc! {"
6872 Tˇhe quick brown
6873 «foˇ»x jumps over
6874 tˇhe lazy dog"});
6875 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6876 cx.assert_editor_state(indoc! {"
6877 fox jumps over
6878 Tˇhe quick brown
6879 fox jumps over
6880 ˇx jumps over
6881 fox jumps over
6882 tˇhe lazy dog"});
6883}
6884
6885#[gpui::test]
6886async fn test_copy_trim(cx: &mut TestAppContext) {
6887 init_test(cx, |_| {});
6888
6889 let mut cx = EditorTestContext::new(cx).await;
6890 cx.set_state(
6891 r#" «for selection in selections.iter() {
6892 let mut start = selection.start;
6893 let mut end = selection.end;
6894 let is_entire_line = selection.is_empty();
6895 if is_entire_line {
6896 start = Point::new(start.row, 0);ˇ»
6897 end = cmp::min(max_point, Point::new(end.row + 1, 0));
6898 }
6899 "#,
6900 );
6901 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6902 assert_eq!(
6903 cx.read_from_clipboard()
6904 .and_then(|item| item.text().as_deref().map(str::to_string)),
6905 Some(
6906 "for selection in selections.iter() {
6907 let mut start = selection.start;
6908 let mut end = selection.end;
6909 let is_entire_line = selection.is_empty();
6910 if is_entire_line {
6911 start = Point::new(start.row, 0);"
6912 .to_string()
6913 ),
6914 "Regular copying preserves all indentation selected",
6915 );
6916 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6917 assert_eq!(
6918 cx.read_from_clipboard()
6919 .and_then(|item| item.text().as_deref().map(str::to_string)),
6920 Some(
6921 "for selection in selections.iter() {
6922let mut start = selection.start;
6923let mut end = selection.end;
6924let is_entire_line = selection.is_empty();
6925if is_entire_line {
6926 start = Point::new(start.row, 0);"
6927 .to_string()
6928 ),
6929 "Copying with stripping should strip all leading whitespaces"
6930 );
6931
6932 cx.set_state(
6933 r#" « for selection in selections.iter() {
6934 let mut start = selection.start;
6935 let mut end = selection.end;
6936 let is_entire_line = selection.is_empty();
6937 if is_entire_line {
6938 start = Point::new(start.row, 0);ˇ»
6939 end = cmp::min(max_point, Point::new(end.row + 1, 0));
6940 }
6941 "#,
6942 );
6943 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6944 assert_eq!(
6945 cx.read_from_clipboard()
6946 .and_then(|item| item.text().as_deref().map(str::to_string)),
6947 Some(
6948 " for selection in selections.iter() {
6949 let mut start = selection.start;
6950 let mut end = selection.end;
6951 let is_entire_line = selection.is_empty();
6952 if is_entire_line {
6953 start = Point::new(start.row, 0);"
6954 .to_string()
6955 ),
6956 "Regular copying preserves all indentation selected",
6957 );
6958 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6959 assert_eq!(
6960 cx.read_from_clipboard()
6961 .and_then(|item| item.text().as_deref().map(str::to_string)),
6962 Some(
6963 "for selection in selections.iter() {
6964let mut start = selection.start;
6965let mut end = selection.end;
6966let is_entire_line = selection.is_empty();
6967if is_entire_line {
6968 start = Point::new(start.row, 0);"
6969 .to_string()
6970 ),
6971 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
6972 );
6973
6974 cx.set_state(
6975 r#" «ˇ for selection in selections.iter() {
6976 let mut start = selection.start;
6977 let mut end = selection.end;
6978 let is_entire_line = selection.is_empty();
6979 if is_entire_line {
6980 start = Point::new(start.row, 0);»
6981 end = cmp::min(max_point, Point::new(end.row + 1, 0));
6982 }
6983 "#,
6984 );
6985 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6986 assert_eq!(
6987 cx.read_from_clipboard()
6988 .and_then(|item| item.text().as_deref().map(str::to_string)),
6989 Some(
6990 " for selection in selections.iter() {
6991 let mut start = selection.start;
6992 let mut end = selection.end;
6993 let is_entire_line = selection.is_empty();
6994 if is_entire_line {
6995 start = Point::new(start.row, 0);"
6996 .to_string()
6997 ),
6998 "Regular copying for reverse selection works the same",
6999 );
7000 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7001 assert_eq!(
7002 cx.read_from_clipboard()
7003 .and_then(|item| item.text().as_deref().map(str::to_string)),
7004 Some(
7005 "for selection in selections.iter() {
7006let mut start = selection.start;
7007let mut end = selection.end;
7008let is_entire_line = selection.is_empty();
7009if is_entire_line {
7010 start = Point::new(start.row, 0);"
7011 .to_string()
7012 ),
7013 "Copying with stripping for reverse selection works the same"
7014 );
7015
7016 cx.set_state(
7017 r#" for selection «in selections.iter() {
7018 let mut start = selection.start;
7019 let mut end = selection.end;
7020 let is_entire_line = selection.is_empty();
7021 if is_entire_line {
7022 start = Point::new(start.row, 0);ˇ»
7023 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7024 }
7025 "#,
7026 );
7027 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7028 assert_eq!(
7029 cx.read_from_clipboard()
7030 .and_then(|item| item.text().as_deref().map(str::to_string)),
7031 Some(
7032 "in selections.iter() {
7033 let mut start = selection.start;
7034 let mut end = selection.end;
7035 let is_entire_line = selection.is_empty();
7036 if is_entire_line {
7037 start = Point::new(start.row, 0);"
7038 .to_string()
7039 ),
7040 "When selecting past the indent, the copying works as usual",
7041 );
7042 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7043 assert_eq!(
7044 cx.read_from_clipboard()
7045 .and_then(|item| item.text().as_deref().map(str::to_string)),
7046 Some(
7047 "in selections.iter() {
7048 let mut start = selection.start;
7049 let mut end = selection.end;
7050 let is_entire_line = selection.is_empty();
7051 if is_entire_line {
7052 start = Point::new(start.row, 0);"
7053 .to_string()
7054 ),
7055 "When selecting past the indent, nothing is trimmed"
7056 );
7057
7058 cx.set_state(
7059 r#" «for selection in selections.iter() {
7060 let mut start = selection.start;
7061
7062 let mut end = selection.end;
7063 let is_entire_line = selection.is_empty();
7064 if is_entire_line {
7065 start = Point::new(start.row, 0);
7066ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
7067 }
7068 "#,
7069 );
7070 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7071 assert_eq!(
7072 cx.read_from_clipboard()
7073 .and_then(|item| item.text().as_deref().map(str::to_string)),
7074 Some(
7075 "for selection in selections.iter() {
7076let mut start = selection.start;
7077
7078let mut end = selection.end;
7079let is_entire_line = selection.is_empty();
7080if is_entire_line {
7081 start = Point::new(start.row, 0);
7082"
7083 .to_string()
7084 ),
7085 "Copying with stripping should ignore empty lines"
7086 );
7087}
7088
7089#[gpui::test]
7090async fn test_paste_multiline(cx: &mut TestAppContext) {
7091 init_test(cx, |_| {});
7092
7093 let mut cx = EditorTestContext::new(cx).await;
7094 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7095
7096 // Cut an indented block, without the leading whitespace.
7097 cx.set_state(indoc! {"
7098 const a: B = (
7099 c(),
7100 «d(
7101 e,
7102 f
7103 )ˇ»
7104 );
7105 "});
7106 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7107 cx.assert_editor_state(indoc! {"
7108 const a: B = (
7109 c(),
7110 ˇ
7111 );
7112 "});
7113
7114 // Paste it at the same position.
7115 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7116 cx.assert_editor_state(indoc! {"
7117 const a: B = (
7118 c(),
7119 d(
7120 e,
7121 f
7122 )ˇ
7123 );
7124 "});
7125
7126 // Paste it at a line with a lower indent level.
7127 cx.set_state(indoc! {"
7128 ˇ
7129 const a: B = (
7130 c(),
7131 );
7132 "});
7133 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7134 cx.assert_editor_state(indoc! {"
7135 d(
7136 e,
7137 f
7138 )ˇ
7139 const a: B = (
7140 c(),
7141 );
7142 "});
7143
7144 // Cut an indented block, with the leading whitespace.
7145 cx.set_state(indoc! {"
7146 const a: B = (
7147 c(),
7148 « d(
7149 e,
7150 f
7151 )
7152 ˇ»);
7153 "});
7154 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7155 cx.assert_editor_state(indoc! {"
7156 const a: B = (
7157 c(),
7158 ˇ);
7159 "});
7160
7161 // Paste it at the same position.
7162 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7163 cx.assert_editor_state(indoc! {"
7164 const a: B = (
7165 c(),
7166 d(
7167 e,
7168 f
7169 )
7170 ˇ);
7171 "});
7172
7173 // Paste it at a line with a higher indent level.
7174 cx.set_state(indoc! {"
7175 const a: B = (
7176 c(),
7177 d(
7178 e,
7179 fˇ
7180 )
7181 );
7182 "});
7183 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7184 cx.assert_editor_state(indoc! {"
7185 const a: B = (
7186 c(),
7187 d(
7188 e,
7189 f d(
7190 e,
7191 f
7192 )
7193 ˇ
7194 )
7195 );
7196 "});
7197
7198 // Copy an indented block, starting mid-line
7199 cx.set_state(indoc! {"
7200 const a: B = (
7201 c(),
7202 somethin«g(
7203 e,
7204 f
7205 )ˇ»
7206 );
7207 "});
7208 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7209
7210 // Paste it on a line with a lower indent level
7211 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
7212 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7213 cx.assert_editor_state(indoc! {"
7214 const a: B = (
7215 c(),
7216 something(
7217 e,
7218 f
7219 )
7220 );
7221 g(
7222 e,
7223 f
7224 )ˇ"});
7225}
7226
7227#[gpui::test]
7228async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
7229 init_test(cx, |_| {});
7230
7231 cx.write_to_clipboard(ClipboardItem::new_string(
7232 " d(\n e\n );\n".into(),
7233 ));
7234
7235 let mut cx = EditorTestContext::new(cx).await;
7236 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7237
7238 cx.set_state(indoc! {"
7239 fn a() {
7240 b();
7241 if c() {
7242 ˇ
7243 }
7244 }
7245 "});
7246
7247 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7248 cx.assert_editor_state(indoc! {"
7249 fn a() {
7250 b();
7251 if c() {
7252 d(
7253 e
7254 );
7255 ˇ
7256 }
7257 }
7258 "});
7259
7260 cx.set_state(indoc! {"
7261 fn a() {
7262 b();
7263 ˇ
7264 }
7265 "});
7266
7267 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7268 cx.assert_editor_state(indoc! {"
7269 fn a() {
7270 b();
7271 d(
7272 e
7273 );
7274 ˇ
7275 }
7276 "});
7277}
7278
7279#[gpui::test]
7280fn test_select_all(cx: &mut TestAppContext) {
7281 init_test(cx, |_| {});
7282
7283 let editor = cx.add_window(|window, cx| {
7284 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
7285 build_editor(buffer, window, cx)
7286 });
7287 _ = editor.update(cx, |editor, window, cx| {
7288 editor.select_all(&SelectAll, window, cx);
7289 assert_eq!(
7290 editor.selections.display_ranges(cx),
7291 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
7292 );
7293 });
7294}
7295
7296#[gpui::test]
7297fn test_select_line(cx: &mut TestAppContext) {
7298 init_test(cx, |_| {});
7299
7300 let editor = cx.add_window(|window, cx| {
7301 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
7302 build_editor(buffer, window, cx)
7303 });
7304 _ = editor.update(cx, |editor, window, cx| {
7305 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7306 s.select_display_ranges([
7307 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7308 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7309 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7310 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
7311 ])
7312 });
7313 editor.select_line(&SelectLine, window, cx);
7314 assert_eq!(
7315 editor.selections.display_ranges(cx),
7316 vec![
7317 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
7318 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
7319 ]
7320 );
7321 });
7322
7323 _ = editor.update(cx, |editor, window, cx| {
7324 editor.select_line(&SelectLine, window, cx);
7325 assert_eq!(
7326 editor.selections.display_ranges(cx),
7327 vec![
7328 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
7329 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
7330 ]
7331 );
7332 });
7333
7334 _ = editor.update(cx, |editor, window, cx| {
7335 editor.select_line(&SelectLine, window, cx);
7336 assert_eq!(
7337 editor.selections.display_ranges(cx),
7338 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
7339 );
7340 });
7341}
7342
7343#[gpui::test]
7344async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
7345 init_test(cx, |_| {});
7346 let mut cx = EditorTestContext::new(cx).await;
7347
7348 #[track_caller]
7349 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
7350 cx.set_state(initial_state);
7351 cx.update_editor(|e, window, cx| {
7352 e.split_selection_into_lines(&Default::default(), window, cx)
7353 });
7354 cx.assert_editor_state(expected_state);
7355 }
7356
7357 // Selection starts and ends at the middle of lines, left-to-right
7358 test(
7359 &mut cx,
7360 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
7361 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7362 );
7363 // Same thing, right-to-left
7364 test(
7365 &mut cx,
7366 "aa\nb«b\ncc\ndd\neˇ»e\nff",
7367 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7368 );
7369
7370 // Whole buffer, left-to-right, last line *doesn't* end with newline
7371 test(
7372 &mut cx,
7373 "«ˇaa\nbb\ncc\ndd\nee\nff»",
7374 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7375 );
7376 // Same thing, right-to-left
7377 test(
7378 &mut cx,
7379 "«aa\nbb\ncc\ndd\nee\nffˇ»",
7380 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7381 );
7382
7383 // Whole buffer, left-to-right, last line ends with newline
7384 test(
7385 &mut cx,
7386 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
7387 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7388 );
7389 // Same thing, right-to-left
7390 test(
7391 &mut cx,
7392 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
7393 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7394 );
7395
7396 // Starts at the end of a line, ends at the start of another
7397 test(
7398 &mut cx,
7399 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
7400 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
7401 );
7402}
7403
7404#[gpui::test]
7405async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
7406 init_test(cx, |_| {});
7407
7408 let editor = cx.add_window(|window, cx| {
7409 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
7410 build_editor(buffer, window, cx)
7411 });
7412
7413 // setup
7414 _ = editor.update(cx, |editor, window, cx| {
7415 editor.fold_creases(
7416 vec![
7417 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
7418 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
7419 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
7420 ],
7421 true,
7422 window,
7423 cx,
7424 );
7425 assert_eq!(
7426 editor.display_text(cx),
7427 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7428 );
7429 });
7430
7431 _ = editor.update(cx, |editor, window, cx| {
7432 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7433 s.select_display_ranges([
7434 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7435 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7436 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7437 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
7438 ])
7439 });
7440 editor.split_selection_into_lines(&Default::default(), window, cx);
7441 assert_eq!(
7442 editor.display_text(cx),
7443 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7444 );
7445 });
7446 EditorTestContext::for_editor(editor, cx)
7447 .await
7448 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
7449
7450 _ = editor.update(cx, |editor, window, cx| {
7451 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7452 s.select_display_ranges([
7453 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
7454 ])
7455 });
7456 editor.split_selection_into_lines(&Default::default(), window, cx);
7457 assert_eq!(
7458 editor.display_text(cx),
7459 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
7460 );
7461 assert_eq!(
7462 editor.selections.display_ranges(cx),
7463 [
7464 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
7465 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
7466 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
7467 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
7468 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
7469 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
7470 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
7471 ]
7472 );
7473 });
7474 EditorTestContext::for_editor(editor, cx)
7475 .await
7476 .assert_editor_state(
7477 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
7478 );
7479}
7480
7481#[gpui::test]
7482async fn test_add_selection_above_below(cx: &mut TestAppContext) {
7483 init_test(cx, |_| {});
7484
7485 let mut cx = EditorTestContext::new(cx).await;
7486
7487 cx.set_state(indoc!(
7488 r#"abc
7489 defˇghi
7490
7491 jk
7492 nlmo
7493 "#
7494 ));
7495
7496 cx.update_editor(|editor, window, cx| {
7497 editor.add_selection_above(&Default::default(), window, cx);
7498 });
7499
7500 cx.assert_editor_state(indoc!(
7501 r#"abcˇ
7502 defˇghi
7503
7504 jk
7505 nlmo
7506 "#
7507 ));
7508
7509 cx.update_editor(|editor, window, cx| {
7510 editor.add_selection_above(&Default::default(), window, cx);
7511 });
7512
7513 cx.assert_editor_state(indoc!(
7514 r#"abcˇ
7515 defˇghi
7516
7517 jk
7518 nlmo
7519 "#
7520 ));
7521
7522 cx.update_editor(|editor, window, cx| {
7523 editor.add_selection_below(&Default::default(), window, cx);
7524 });
7525
7526 cx.assert_editor_state(indoc!(
7527 r#"abc
7528 defˇghi
7529
7530 jk
7531 nlmo
7532 "#
7533 ));
7534
7535 cx.update_editor(|editor, window, cx| {
7536 editor.undo_selection(&Default::default(), window, cx);
7537 });
7538
7539 cx.assert_editor_state(indoc!(
7540 r#"abcˇ
7541 defˇghi
7542
7543 jk
7544 nlmo
7545 "#
7546 ));
7547
7548 cx.update_editor(|editor, window, cx| {
7549 editor.redo_selection(&Default::default(), window, cx);
7550 });
7551
7552 cx.assert_editor_state(indoc!(
7553 r#"abc
7554 defˇghi
7555
7556 jk
7557 nlmo
7558 "#
7559 ));
7560
7561 cx.update_editor(|editor, window, cx| {
7562 editor.add_selection_below(&Default::default(), window, cx);
7563 });
7564
7565 cx.assert_editor_state(indoc!(
7566 r#"abc
7567 defˇghi
7568 ˇ
7569 jk
7570 nlmo
7571 "#
7572 ));
7573
7574 cx.update_editor(|editor, window, cx| {
7575 editor.add_selection_below(&Default::default(), window, cx);
7576 });
7577
7578 cx.assert_editor_state(indoc!(
7579 r#"abc
7580 defˇghi
7581 ˇ
7582 jkˇ
7583 nlmo
7584 "#
7585 ));
7586
7587 cx.update_editor(|editor, window, cx| {
7588 editor.add_selection_below(&Default::default(), window, cx);
7589 });
7590
7591 cx.assert_editor_state(indoc!(
7592 r#"abc
7593 defˇghi
7594 ˇ
7595 jkˇ
7596 nlmˇo
7597 "#
7598 ));
7599
7600 cx.update_editor(|editor, window, cx| {
7601 editor.add_selection_below(&Default::default(), window, cx);
7602 });
7603
7604 cx.assert_editor_state(indoc!(
7605 r#"abc
7606 defˇghi
7607 ˇ
7608 jkˇ
7609 nlmˇo
7610 ˇ"#
7611 ));
7612
7613 // change selections
7614 cx.set_state(indoc!(
7615 r#"abc
7616 def«ˇg»hi
7617
7618 jk
7619 nlmo
7620 "#
7621 ));
7622
7623 cx.update_editor(|editor, window, cx| {
7624 editor.add_selection_below(&Default::default(), window, cx);
7625 });
7626
7627 cx.assert_editor_state(indoc!(
7628 r#"abc
7629 def«ˇg»hi
7630
7631 jk
7632 nlm«ˇo»
7633 "#
7634 ));
7635
7636 cx.update_editor(|editor, window, cx| {
7637 editor.add_selection_below(&Default::default(), window, cx);
7638 });
7639
7640 cx.assert_editor_state(indoc!(
7641 r#"abc
7642 def«ˇg»hi
7643
7644 jk
7645 nlm«ˇo»
7646 "#
7647 ));
7648
7649 cx.update_editor(|editor, window, cx| {
7650 editor.add_selection_above(&Default::default(), window, cx);
7651 });
7652
7653 cx.assert_editor_state(indoc!(
7654 r#"abc
7655 def«ˇg»hi
7656
7657 jk
7658 nlmo
7659 "#
7660 ));
7661
7662 cx.update_editor(|editor, window, cx| {
7663 editor.add_selection_above(&Default::default(), window, cx);
7664 });
7665
7666 cx.assert_editor_state(indoc!(
7667 r#"abc
7668 def«ˇg»hi
7669
7670 jk
7671 nlmo
7672 "#
7673 ));
7674
7675 // Change selections again
7676 cx.set_state(indoc!(
7677 r#"a«bc
7678 defgˇ»hi
7679
7680 jk
7681 nlmo
7682 "#
7683 ));
7684
7685 cx.update_editor(|editor, window, cx| {
7686 editor.add_selection_below(&Default::default(), window, cx);
7687 });
7688
7689 cx.assert_editor_state(indoc!(
7690 r#"a«bcˇ»
7691 d«efgˇ»hi
7692
7693 j«kˇ»
7694 nlmo
7695 "#
7696 ));
7697
7698 cx.update_editor(|editor, window, cx| {
7699 editor.add_selection_below(&Default::default(), window, cx);
7700 });
7701 cx.assert_editor_state(indoc!(
7702 r#"a«bcˇ»
7703 d«efgˇ»hi
7704
7705 j«kˇ»
7706 n«lmoˇ»
7707 "#
7708 ));
7709 cx.update_editor(|editor, window, cx| {
7710 editor.add_selection_above(&Default::default(), window, cx);
7711 });
7712
7713 cx.assert_editor_state(indoc!(
7714 r#"a«bcˇ»
7715 d«efgˇ»hi
7716
7717 j«kˇ»
7718 nlmo
7719 "#
7720 ));
7721
7722 // Change selections again
7723 cx.set_state(indoc!(
7724 r#"abc
7725 d«ˇefghi
7726
7727 jk
7728 nlm»o
7729 "#
7730 ));
7731
7732 cx.update_editor(|editor, window, cx| {
7733 editor.add_selection_above(&Default::default(), window, cx);
7734 });
7735
7736 cx.assert_editor_state(indoc!(
7737 r#"a«ˇbc»
7738 d«ˇef»ghi
7739
7740 j«ˇk»
7741 n«ˇlm»o
7742 "#
7743 ));
7744
7745 cx.update_editor(|editor, window, cx| {
7746 editor.add_selection_below(&Default::default(), window, cx);
7747 });
7748
7749 cx.assert_editor_state(indoc!(
7750 r#"abc
7751 d«ˇef»ghi
7752
7753 j«ˇk»
7754 n«ˇlm»o
7755 "#
7756 ));
7757}
7758
7759#[gpui::test]
7760async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
7761 init_test(cx, |_| {});
7762 let mut cx = EditorTestContext::new(cx).await;
7763
7764 cx.set_state(indoc!(
7765 r#"line onˇe
7766 liˇne two
7767 line three
7768 line four"#
7769 ));
7770
7771 cx.update_editor(|editor, window, cx| {
7772 editor.add_selection_below(&Default::default(), window, cx);
7773 });
7774
7775 // test multiple cursors expand in the same direction
7776 cx.assert_editor_state(indoc!(
7777 r#"line onˇe
7778 liˇne twˇo
7779 liˇne three
7780 line four"#
7781 ));
7782
7783 cx.update_editor(|editor, window, cx| {
7784 editor.add_selection_below(&Default::default(), window, cx);
7785 });
7786
7787 cx.update_editor(|editor, window, cx| {
7788 editor.add_selection_below(&Default::default(), window, cx);
7789 });
7790
7791 // test multiple cursors expand below overflow
7792 cx.assert_editor_state(indoc!(
7793 r#"line onˇe
7794 liˇne twˇo
7795 liˇne thˇree
7796 liˇne foˇur"#
7797 ));
7798
7799 cx.update_editor(|editor, window, cx| {
7800 editor.add_selection_above(&Default::default(), window, cx);
7801 });
7802
7803 // test multiple cursors retrieves back correctly
7804 cx.assert_editor_state(indoc!(
7805 r#"line onˇe
7806 liˇne twˇo
7807 liˇne thˇree
7808 line four"#
7809 ));
7810
7811 cx.update_editor(|editor, window, cx| {
7812 editor.add_selection_above(&Default::default(), window, cx);
7813 });
7814
7815 cx.update_editor(|editor, window, cx| {
7816 editor.add_selection_above(&Default::default(), window, cx);
7817 });
7818
7819 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
7820 cx.assert_editor_state(indoc!(
7821 r#"liˇne onˇe
7822 liˇne two
7823 line three
7824 line four"#
7825 ));
7826
7827 cx.update_editor(|editor, window, cx| {
7828 editor.undo_selection(&Default::default(), window, cx);
7829 });
7830
7831 // test undo
7832 cx.assert_editor_state(indoc!(
7833 r#"line onˇe
7834 liˇne twˇo
7835 line three
7836 line four"#
7837 ));
7838
7839 cx.update_editor(|editor, window, cx| {
7840 editor.redo_selection(&Default::default(), window, cx);
7841 });
7842
7843 // test redo
7844 cx.assert_editor_state(indoc!(
7845 r#"liˇne onˇe
7846 liˇne two
7847 line three
7848 line four"#
7849 ));
7850
7851 cx.set_state(indoc!(
7852 r#"abcd
7853 ef«ghˇ»
7854 ijkl
7855 «mˇ»nop"#
7856 ));
7857
7858 cx.update_editor(|editor, window, cx| {
7859 editor.add_selection_above(&Default::default(), window, cx);
7860 });
7861
7862 // test multiple selections expand in the same direction
7863 cx.assert_editor_state(indoc!(
7864 r#"ab«cdˇ»
7865 ef«ghˇ»
7866 «iˇ»jkl
7867 «mˇ»nop"#
7868 ));
7869
7870 cx.update_editor(|editor, window, cx| {
7871 editor.add_selection_above(&Default::default(), window, cx);
7872 });
7873
7874 // test multiple selection upward overflow
7875 cx.assert_editor_state(indoc!(
7876 r#"ab«cdˇ»
7877 «eˇ»f«ghˇ»
7878 «iˇ»jkl
7879 «mˇ»nop"#
7880 ));
7881
7882 cx.update_editor(|editor, window, cx| {
7883 editor.add_selection_below(&Default::default(), window, cx);
7884 });
7885
7886 // test multiple selection retrieves back correctly
7887 cx.assert_editor_state(indoc!(
7888 r#"abcd
7889 ef«ghˇ»
7890 «iˇ»jkl
7891 «mˇ»nop"#
7892 ));
7893
7894 cx.update_editor(|editor, window, cx| {
7895 editor.add_selection_below(&Default::default(), window, cx);
7896 });
7897
7898 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
7899 cx.assert_editor_state(indoc!(
7900 r#"abcd
7901 ef«ghˇ»
7902 ij«klˇ»
7903 «mˇ»nop"#
7904 ));
7905
7906 cx.update_editor(|editor, window, cx| {
7907 editor.undo_selection(&Default::default(), window, cx);
7908 });
7909
7910 // test undo
7911 cx.assert_editor_state(indoc!(
7912 r#"abcd
7913 ef«ghˇ»
7914 «iˇ»jkl
7915 «mˇ»nop"#
7916 ));
7917
7918 cx.update_editor(|editor, window, cx| {
7919 editor.redo_selection(&Default::default(), window, cx);
7920 });
7921
7922 // test redo
7923 cx.assert_editor_state(indoc!(
7924 r#"abcd
7925 ef«ghˇ»
7926 ij«klˇ»
7927 «mˇ»nop"#
7928 ));
7929}
7930
7931#[gpui::test]
7932async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
7933 init_test(cx, |_| {});
7934 let mut cx = EditorTestContext::new(cx).await;
7935
7936 cx.set_state(indoc!(
7937 r#"line onˇe
7938 liˇne two
7939 line three
7940 line four"#
7941 ));
7942
7943 cx.update_editor(|editor, window, cx| {
7944 editor.add_selection_below(&Default::default(), window, cx);
7945 editor.add_selection_below(&Default::default(), window, cx);
7946 editor.add_selection_below(&Default::default(), window, cx);
7947 });
7948
7949 // initial state with two multi cursor groups
7950 cx.assert_editor_state(indoc!(
7951 r#"line onˇe
7952 liˇne twˇo
7953 liˇne thˇree
7954 liˇne foˇur"#
7955 ));
7956
7957 // add single cursor in middle - simulate opt click
7958 cx.update_editor(|editor, window, cx| {
7959 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
7960 editor.begin_selection(new_cursor_point, true, 1, window, cx);
7961 editor.end_selection(window, cx);
7962 });
7963
7964 cx.assert_editor_state(indoc!(
7965 r#"line onˇe
7966 liˇne twˇo
7967 liˇneˇ thˇree
7968 liˇne foˇur"#
7969 ));
7970
7971 cx.update_editor(|editor, window, cx| {
7972 editor.add_selection_above(&Default::default(), window, cx);
7973 });
7974
7975 // test new added selection expands above and existing selection shrinks
7976 cx.assert_editor_state(indoc!(
7977 r#"line onˇe
7978 liˇneˇ twˇo
7979 liˇneˇ thˇree
7980 line four"#
7981 ));
7982
7983 cx.update_editor(|editor, window, cx| {
7984 editor.add_selection_above(&Default::default(), window, cx);
7985 });
7986
7987 // test new added selection expands above and existing selection shrinks
7988 cx.assert_editor_state(indoc!(
7989 r#"lineˇ onˇe
7990 liˇneˇ twˇo
7991 lineˇ three
7992 line four"#
7993 ));
7994
7995 // intial state with two selection groups
7996 cx.set_state(indoc!(
7997 r#"abcd
7998 ef«ghˇ»
7999 ijkl
8000 «mˇ»nop"#
8001 ));
8002
8003 cx.update_editor(|editor, window, cx| {
8004 editor.add_selection_above(&Default::default(), window, cx);
8005 editor.add_selection_above(&Default::default(), window, cx);
8006 });
8007
8008 cx.assert_editor_state(indoc!(
8009 r#"ab«cdˇ»
8010 «eˇ»f«ghˇ»
8011 «iˇ»jkl
8012 «mˇ»nop"#
8013 ));
8014
8015 // add single selection in middle - simulate opt drag
8016 cx.update_editor(|editor, window, cx| {
8017 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
8018 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8019 editor.update_selection(
8020 DisplayPoint::new(DisplayRow(2), 4),
8021 0,
8022 gpui::Point::<f32>::default(),
8023 window,
8024 cx,
8025 );
8026 editor.end_selection(window, cx);
8027 });
8028
8029 cx.assert_editor_state(indoc!(
8030 r#"ab«cdˇ»
8031 «eˇ»f«ghˇ»
8032 «iˇ»jk«lˇ»
8033 «mˇ»nop"#
8034 ));
8035
8036 cx.update_editor(|editor, window, cx| {
8037 editor.add_selection_below(&Default::default(), window, cx);
8038 });
8039
8040 // test new added selection expands below, others shrinks from above
8041 cx.assert_editor_state(indoc!(
8042 r#"abcd
8043 ef«ghˇ»
8044 «iˇ»jk«lˇ»
8045 «mˇ»no«pˇ»"#
8046 ));
8047}
8048
8049#[gpui::test]
8050async fn test_select_next(cx: &mut TestAppContext) {
8051 init_test(cx, |_| {});
8052
8053 let mut cx = EditorTestContext::new(cx).await;
8054 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8055
8056 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8057 .unwrap();
8058 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8059
8060 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8061 .unwrap();
8062 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8063
8064 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8065 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8066
8067 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8068 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8069
8070 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8071 .unwrap();
8072 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8073
8074 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8075 .unwrap();
8076 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8077
8078 // Test selection direction should be preserved
8079 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8080
8081 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8082 .unwrap();
8083 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
8084}
8085
8086#[gpui::test]
8087async fn test_select_all_matches(cx: &mut TestAppContext) {
8088 init_test(cx, |_| {});
8089
8090 let mut cx = EditorTestContext::new(cx).await;
8091
8092 // Test caret-only selections
8093 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8094 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8095 .unwrap();
8096 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8097
8098 // Test left-to-right selections
8099 cx.set_state("abc\n«abcˇ»\nabc");
8100 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8101 .unwrap();
8102 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
8103
8104 // Test right-to-left selections
8105 cx.set_state("abc\n«ˇabc»\nabc");
8106 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8107 .unwrap();
8108 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
8109
8110 // Test selecting whitespace with caret selection
8111 cx.set_state("abc\nˇ abc\nabc");
8112 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8113 .unwrap();
8114 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8115
8116 // Test selecting whitespace with left-to-right selection
8117 cx.set_state("abc\n«ˇ »abc\nabc");
8118 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8119 .unwrap();
8120 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
8121
8122 // Test no matches with right-to-left selection
8123 cx.set_state("abc\n« ˇ»abc\nabc");
8124 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8125 .unwrap();
8126 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8127
8128 // Test with a single word and clip_at_line_ends=true (#29823)
8129 cx.set_state("aˇbc");
8130 cx.update_editor(|e, window, cx| {
8131 e.set_clip_at_line_ends(true, cx);
8132 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8133 e.set_clip_at_line_ends(false, cx);
8134 });
8135 cx.assert_editor_state("«abcˇ»");
8136}
8137
8138#[gpui::test]
8139async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
8140 init_test(cx, |_| {});
8141
8142 let mut cx = EditorTestContext::new(cx).await;
8143
8144 let large_body_1 = "\nd".repeat(200);
8145 let large_body_2 = "\ne".repeat(200);
8146
8147 cx.set_state(&format!(
8148 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
8149 ));
8150 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
8151 let scroll_position = editor.scroll_position(cx);
8152 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
8153 scroll_position
8154 });
8155
8156 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8157 .unwrap();
8158 cx.assert_editor_state(&format!(
8159 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
8160 ));
8161 let scroll_position_after_selection =
8162 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
8163 assert_eq!(
8164 initial_scroll_position, scroll_position_after_selection,
8165 "Scroll position should not change after selecting all matches"
8166 );
8167}
8168
8169#[gpui::test]
8170async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
8171 init_test(cx, |_| {});
8172
8173 let mut cx = EditorLspTestContext::new_rust(
8174 lsp::ServerCapabilities {
8175 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8176 ..Default::default()
8177 },
8178 cx,
8179 )
8180 .await;
8181
8182 cx.set_state(indoc! {"
8183 line 1
8184 line 2
8185 linˇe 3
8186 line 4
8187 line 5
8188 "});
8189
8190 // Make an edit
8191 cx.update_editor(|editor, window, cx| {
8192 editor.handle_input("X", window, cx);
8193 });
8194
8195 // Move cursor to a different position
8196 cx.update_editor(|editor, window, cx| {
8197 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8198 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
8199 });
8200 });
8201
8202 cx.assert_editor_state(indoc! {"
8203 line 1
8204 line 2
8205 linXe 3
8206 line 4
8207 liˇne 5
8208 "});
8209
8210 cx.lsp
8211 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
8212 Ok(Some(vec![lsp::TextEdit::new(
8213 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8214 "PREFIX ".to_string(),
8215 )]))
8216 });
8217
8218 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
8219 .unwrap()
8220 .await
8221 .unwrap();
8222
8223 cx.assert_editor_state(indoc! {"
8224 PREFIX line 1
8225 line 2
8226 linXe 3
8227 line 4
8228 liˇne 5
8229 "});
8230
8231 // Undo formatting
8232 cx.update_editor(|editor, window, cx| {
8233 editor.undo(&Default::default(), window, cx);
8234 });
8235
8236 // Verify cursor moved back to position after edit
8237 cx.assert_editor_state(indoc! {"
8238 line 1
8239 line 2
8240 linXˇe 3
8241 line 4
8242 line 5
8243 "});
8244}
8245
8246#[gpui::test]
8247async fn test_undo_edit_prediction_scrolls_to_edit_pos(cx: &mut TestAppContext) {
8248 init_test(cx, |_| {});
8249
8250 let mut cx = EditorTestContext::new(cx).await;
8251
8252 let provider = cx.new(|_| FakeEditPredictionProvider::default());
8253 cx.update_editor(|editor, window, cx| {
8254 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
8255 });
8256
8257 cx.set_state(indoc! {"
8258 line 1
8259 line 2
8260 linˇe 3
8261 line 4
8262 line 5
8263 line 6
8264 line 7
8265 line 8
8266 line 9
8267 line 10
8268 "});
8269
8270 let snapshot = cx.buffer_snapshot();
8271 let edit_position = snapshot.anchor_after(Point::new(2, 4));
8272
8273 cx.update(|_, cx| {
8274 provider.update(cx, |provider, _| {
8275 provider.set_edit_prediction(Some(edit_prediction::EditPrediction::Local {
8276 id: None,
8277 edits: vec![(edit_position..edit_position, "X".into())],
8278 edit_preview: None,
8279 }))
8280 })
8281 });
8282
8283 cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
8284 cx.update_editor(|editor, window, cx| {
8285 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
8286 });
8287
8288 cx.assert_editor_state(indoc! {"
8289 line 1
8290 line 2
8291 lineXˇ 3
8292 line 4
8293 line 5
8294 line 6
8295 line 7
8296 line 8
8297 line 9
8298 line 10
8299 "});
8300
8301 cx.update_editor(|editor, window, cx| {
8302 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8303 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
8304 });
8305 });
8306
8307 cx.assert_editor_state(indoc! {"
8308 line 1
8309 line 2
8310 lineX 3
8311 line 4
8312 line 5
8313 line 6
8314 line 7
8315 line 8
8316 line 9
8317 liˇne 10
8318 "});
8319
8320 cx.update_editor(|editor, window, cx| {
8321 editor.undo(&Default::default(), window, cx);
8322 });
8323
8324 cx.assert_editor_state(indoc! {"
8325 line 1
8326 line 2
8327 lineˇ 3
8328 line 4
8329 line 5
8330 line 6
8331 line 7
8332 line 8
8333 line 9
8334 line 10
8335 "});
8336}
8337
8338#[gpui::test]
8339async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
8340 init_test(cx, |_| {});
8341
8342 let mut cx = EditorTestContext::new(cx).await;
8343 cx.set_state(
8344 r#"let foo = 2;
8345lˇet foo = 2;
8346let fooˇ = 2;
8347let foo = 2;
8348let foo = ˇ2;"#,
8349 );
8350
8351 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8352 .unwrap();
8353 cx.assert_editor_state(
8354 r#"let foo = 2;
8355«letˇ» foo = 2;
8356let «fooˇ» = 2;
8357let foo = 2;
8358let foo = «2ˇ»;"#,
8359 );
8360
8361 // noop for multiple selections with different contents
8362 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8363 .unwrap();
8364 cx.assert_editor_state(
8365 r#"let foo = 2;
8366«letˇ» foo = 2;
8367let «fooˇ» = 2;
8368let foo = 2;
8369let foo = «2ˇ»;"#,
8370 );
8371
8372 // Test last selection direction should be preserved
8373 cx.set_state(
8374 r#"let foo = 2;
8375let foo = 2;
8376let «fooˇ» = 2;
8377let «ˇfoo» = 2;
8378let foo = 2;"#,
8379 );
8380
8381 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8382 .unwrap();
8383 cx.assert_editor_state(
8384 r#"let foo = 2;
8385let foo = 2;
8386let «fooˇ» = 2;
8387let «ˇfoo» = 2;
8388let «ˇfoo» = 2;"#,
8389 );
8390}
8391
8392#[gpui::test]
8393async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
8394 init_test(cx, |_| {});
8395
8396 let mut cx =
8397 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
8398
8399 cx.assert_editor_state(indoc! {"
8400 ˇbbb
8401 ccc
8402
8403 bbb
8404 ccc
8405 "});
8406 cx.dispatch_action(SelectPrevious::default());
8407 cx.assert_editor_state(indoc! {"
8408 «bbbˇ»
8409 ccc
8410
8411 bbb
8412 ccc
8413 "});
8414 cx.dispatch_action(SelectPrevious::default());
8415 cx.assert_editor_state(indoc! {"
8416 «bbbˇ»
8417 ccc
8418
8419 «bbbˇ»
8420 ccc
8421 "});
8422}
8423
8424#[gpui::test]
8425async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
8426 init_test(cx, |_| {});
8427
8428 let mut cx = EditorTestContext::new(cx).await;
8429 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8430
8431 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8432 .unwrap();
8433 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8434
8435 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8436 .unwrap();
8437 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8438
8439 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8440 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8441
8442 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8443 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8444
8445 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8446 .unwrap();
8447 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
8448
8449 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8450 .unwrap();
8451 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8452}
8453
8454#[gpui::test]
8455async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
8456 init_test(cx, |_| {});
8457
8458 let mut cx = EditorTestContext::new(cx).await;
8459 cx.set_state("aˇ");
8460
8461 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8462 .unwrap();
8463 cx.assert_editor_state("«aˇ»");
8464 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8465 .unwrap();
8466 cx.assert_editor_state("«aˇ»");
8467}
8468
8469#[gpui::test]
8470async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
8471 init_test(cx, |_| {});
8472
8473 let mut cx = EditorTestContext::new(cx).await;
8474 cx.set_state(
8475 r#"let foo = 2;
8476lˇet foo = 2;
8477let fooˇ = 2;
8478let foo = 2;
8479let foo = ˇ2;"#,
8480 );
8481
8482 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8483 .unwrap();
8484 cx.assert_editor_state(
8485 r#"let foo = 2;
8486«letˇ» foo = 2;
8487let «fooˇ» = 2;
8488let foo = 2;
8489let foo = «2ˇ»;"#,
8490 );
8491
8492 // noop for multiple selections with different contents
8493 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8494 .unwrap();
8495 cx.assert_editor_state(
8496 r#"let foo = 2;
8497«letˇ» foo = 2;
8498let «fooˇ» = 2;
8499let foo = 2;
8500let foo = «2ˇ»;"#,
8501 );
8502}
8503
8504#[gpui::test]
8505async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
8506 init_test(cx, |_| {});
8507
8508 let mut cx = EditorTestContext::new(cx).await;
8509 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8510
8511 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8512 .unwrap();
8513 // selection direction is preserved
8514 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8515
8516 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8517 .unwrap();
8518 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8519
8520 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8521 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8522
8523 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8524 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8525
8526 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8527 .unwrap();
8528 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
8529
8530 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8531 .unwrap();
8532 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
8533}
8534
8535#[gpui::test]
8536async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
8537 init_test(cx, |_| {});
8538
8539 let language = Arc::new(Language::new(
8540 LanguageConfig::default(),
8541 Some(tree_sitter_rust::LANGUAGE.into()),
8542 ));
8543
8544 let text = r#"
8545 use mod1::mod2::{mod3, mod4};
8546
8547 fn fn_1(param1: bool, param2: &str) {
8548 let var1 = "text";
8549 }
8550 "#
8551 .unindent();
8552
8553 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8554 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8555 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8556
8557 editor
8558 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8559 .await;
8560
8561 editor.update_in(cx, |editor, window, cx| {
8562 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8563 s.select_display_ranges([
8564 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
8565 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
8566 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
8567 ]);
8568 });
8569 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8570 });
8571 editor.update(cx, |editor, cx| {
8572 assert_text_with_selections(
8573 editor,
8574 indoc! {r#"
8575 use mod1::mod2::{mod3, «mod4ˇ»};
8576
8577 fn fn_1«ˇ(param1: bool, param2: &str)» {
8578 let var1 = "«ˇtext»";
8579 }
8580 "#},
8581 cx,
8582 );
8583 });
8584
8585 editor.update_in(cx, |editor, window, cx| {
8586 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8587 });
8588 editor.update(cx, |editor, cx| {
8589 assert_text_with_selections(
8590 editor,
8591 indoc! {r#"
8592 use mod1::mod2::«{mod3, mod4}ˇ»;
8593
8594 «ˇfn fn_1(param1: bool, param2: &str) {
8595 let var1 = "text";
8596 }»
8597 "#},
8598 cx,
8599 );
8600 });
8601
8602 editor.update_in(cx, |editor, window, cx| {
8603 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8604 });
8605 assert_eq!(
8606 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
8607 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
8608 );
8609
8610 // Trying to expand the selected syntax node one more time has no effect.
8611 editor.update_in(cx, |editor, window, cx| {
8612 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8613 });
8614 assert_eq!(
8615 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
8616 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
8617 );
8618
8619 editor.update_in(cx, |editor, window, cx| {
8620 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8621 });
8622 editor.update(cx, |editor, cx| {
8623 assert_text_with_selections(
8624 editor,
8625 indoc! {r#"
8626 use mod1::mod2::«{mod3, mod4}ˇ»;
8627
8628 «ˇfn fn_1(param1: bool, param2: &str) {
8629 let var1 = "text";
8630 }»
8631 "#},
8632 cx,
8633 );
8634 });
8635
8636 editor.update_in(cx, |editor, window, cx| {
8637 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8638 });
8639 editor.update(cx, |editor, cx| {
8640 assert_text_with_selections(
8641 editor,
8642 indoc! {r#"
8643 use mod1::mod2::{mod3, «mod4ˇ»};
8644
8645 fn fn_1«ˇ(param1: bool, param2: &str)» {
8646 let var1 = "«ˇtext»";
8647 }
8648 "#},
8649 cx,
8650 );
8651 });
8652
8653 editor.update_in(cx, |editor, window, cx| {
8654 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8655 });
8656 editor.update(cx, |editor, cx| {
8657 assert_text_with_selections(
8658 editor,
8659 indoc! {r#"
8660 use mod1::mod2::{mod3, moˇd4};
8661
8662 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
8663 let var1 = "teˇxt";
8664 }
8665 "#},
8666 cx,
8667 );
8668 });
8669
8670 // Trying to shrink the selected syntax node one more time has no effect.
8671 editor.update_in(cx, |editor, window, cx| {
8672 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8673 });
8674 editor.update_in(cx, |editor, _, cx| {
8675 assert_text_with_selections(
8676 editor,
8677 indoc! {r#"
8678 use mod1::mod2::{mod3, moˇd4};
8679
8680 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
8681 let var1 = "teˇxt";
8682 }
8683 "#},
8684 cx,
8685 );
8686 });
8687
8688 // Ensure that we keep expanding the selection if the larger selection starts or ends within
8689 // a fold.
8690 editor.update_in(cx, |editor, window, cx| {
8691 editor.fold_creases(
8692 vec![
8693 Crease::simple(
8694 Point::new(0, 21)..Point::new(0, 24),
8695 FoldPlaceholder::test(),
8696 ),
8697 Crease::simple(
8698 Point::new(3, 20)..Point::new(3, 22),
8699 FoldPlaceholder::test(),
8700 ),
8701 ],
8702 true,
8703 window,
8704 cx,
8705 );
8706 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8707 });
8708 editor.update(cx, |editor, cx| {
8709 assert_text_with_selections(
8710 editor,
8711 indoc! {r#"
8712 use mod1::mod2::«{mod3, mod4}ˇ»;
8713
8714 fn fn_1«ˇ(param1: bool, param2: &str)» {
8715 let var1 = "«ˇtext»";
8716 }
8717 "#},
8718 cx,
8719 );
8720 });
8721}
8722
8723#[gpui::test]
8724async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
8725 init_test(cx, |_| {});
8726
8727 let language = Arc::new(Language::new(
8728 LanguageConfig::default(),
8729 Some(tree_sitter_rust::LANGUAGE.into()),
8730 ));
8731
8732 let text = "let a = 2;";
8733
8734 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8735 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8736 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8737
8738 editor
8739 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8740 .await;
8741
8742 // Test case 1: Cursor at end of word
8743 editor.update_in(cx, |editor, window, cx| {
8744 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8745 s.select_display_ranges([
8746 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
8747 ]);
8748 });
8749 });
8750 editor.update(cx, |editor, cx| {
8751 assert_text_with_selections(editor, "let aˇ = 2;", cx);
8752 });
8753 editor.update_in(cx, |editor, window, cx| {
8754 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8755 });
8756 editor.update(cx, |editor, cx| {
8757 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
8758 });
8759 editor.update_in(cx, |editor, window, cx| {
8760 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8761 });
8762 editor.update(cx, |editor, cx| {
8763 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
8764 });
8765
8766 // Test case 2: Cursor at end of statement
8767 editor.update_in(cx, |editor, window, cx| {
8768 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8769 s.select_display_ranges([
8770 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
8771 ]);
8772 });
8773 });
8774 editor.update(cx, |editor, cx| {
8775 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
8776 });
8777 editor.update_in(cx, |editor, window, cx| {
8778 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8779 });
8780 editor.update(cx, |editor, cx| {
8781 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
8782 });
8783}
8784
8785#[gpui::test]
8786async fn test_select_larger_syntax_node_for_cursor_at_symbol(cx: &mut TestAppContext) {
8787 init_test(cx, |_| {});
8788
8789 let language = Arc::new(Language::new(
8790 LanguageConfig {
8791 name: "JavaScript".into(),
8792 ..Default::default()
8793 },
8794 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
8795 ));
8796
8797 let text = r#"
8798 let a = {
8799 key: "value",
8800 };
8801 "#
8802 .unindent();
8803
8804 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8805 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8806 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8807
8808 editor
8809 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8810 .await;
8811
8812 // Test case 1: Cursor after '{'
8813 editor.update_in(cx, |editor, window, cx| {
8814 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8815 s.select_display_ranges([
8816 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 9)
8817 ]);
8818 });
8819 });
8820 editor.update(cx, |editor, cx| {
8821 assert_text_with_selections(
8822 editor,
8823 indoc! {r#"
8824 let a = {ˇ
8825 key: "value",
8826 };
8827 "#},
8828 cx,
8829 );
8830 });
8831 editor.update_in(cx, |editor, window, cx| {
8832 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8833 });
8834 editor.update(cx, |editor, cx| {
8835 assert_text_with_selections(
8836 editor,
8837 indoc! {r#"
8838 let a = «ˇ{
8839 key: "value",
8840 }»;
8841 "#},
8842 cx,
8843 );
8844 });
8845
8846 // Test case 2: Cursor after ':'
8847 editor.update_in(cx, |editor, window, cx| {
8848 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8849 s.select_display_ranges([
8850 DisplayPoint::new(DisplayRow(1), 8)..DisplayPoint::new(DisplayRow(1), 8)
8851 ]);
8852 });
8853 });
8854 editor.update(cx, |editor, cx| {
8855 assert_text_with_selections(
8856 editor,
8857 indoc! {r#"
8858 let a = {
8859 key:ˇ "value",
8860 };
8861 "#},
8862 cx,
8863 );
8864 });
8865 editor.update_in(cx, |editor, window, cx| {
8866 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8867 });
8868 editor.update(cx, |editor, cx| {
8869 assert_text_with_selections(
8870 editor,
8871 indoc! {r#"
8872 let a = {
8873 «ˇkey: "value"»,
8874 };
8875 "#},
8876 cx,
8877 );
8878 });
8879 editor.update_in(cx, |editor, window, cx| {
8880 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8881 });
8882 editor.update(cx, |editor, cx| {
8883 assert_text_with_selections(
8884 editor,
8885 indoc! {r#"
8886 let a = «ˇ{
8887 key: "value",
8888 }»;
8889 "#},
8890 cx,
8891 );
8892 });
8893
8894 // Test case 3: Cursor after ','
8895 editor.update_in(cx, |editor, window, cx| {
8896 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8897 s.select_display_ranges([
8898 DisplayPoint::new(DisplayRow(1), 17)..DisplayPoint::new(DisplayRow(1), 17)
8899 ]);
8900 });
8901 });
8902 editor.update(cx, |editor, cx| {
8903 assert_text_with_selections(
8904 editor,
8905 indoc! {r#"
8906 let a = {
8907 key: "value",ˇ
8908 };
8909 "#},
8910 cx,
8911 );
8912 });
8913 editor.update_in(cx, |editor, window, cx| {
8914 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8915 });
8916 editor.update(cx, |editor, cx| {
8917 assert_text_with_selections(
8918 editor,
8919 indoc! {r#"
8920 let a = «ˇ{
8921 key: "value",
8922 }»;
8923 "#},
8924 cx,
8925 );
8926 });
8927
8928 // Test case 4: Cursor after ';'
8929 editor.update_in(cx, |editor, window, cx| {
8930 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8931 s.select_display_ranges([
8932 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)
8933 ]);
8934 });
8935 });
8936 editor.update(cx, |editor, cx| {
8937 assert_text_with_selections(
8938 editor,
8939 indoc! {r#"
8940 let a = {
8941 key: "value",
8942 };ˇ
8943 "#},
8944 cx,
8945 );
8946 });
8947 editor.update_in(cx, |editor, window, cx| {
8948 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8949 });
8950 editor.update(cx, |editor, cx| {
8951 assert_text_with_selections(
8952 editor,
8953 indoc! {r#"
8954 «ˇlet a = {
8955 key: "value",
8956 };
8957 »"#},
8958 cx,
8959 );
8960 });
8961}
8962
8963#[gpui::test]
8964async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
8965 init_test(cx, |_| {});
8966
8967 let language = Arc::new(Language::new(
8968 LanguageConfig::default(),
8969 Some(tree_sitter_rust::LANGUAGE.into()),
8970 ));
8971
8972 let text = r#"
8973 use mod1::mod2::{mod3, mod4};
8974
8975 fn fn_1(param1: bool, param2: &str) {
8976 let var1 = "hello world";
8977 }
8978 "#
8979 .unindent();
8980
8981 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8982 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8983 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8984
8985 editor
8986 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8987 .await;
8988
8989 // Test 1: Cursor on a letter of a string word
8990 editor.update_in(cx, |editor, window, cx| {
8991 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8992 s.select_display_ranges([
8993 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
8994 ]);
8995 });
8996 });
8997 editor.update_in(cx, |editor, window, cx| {
8998 assert_text_with_selections(
8999 editor,
9000 indoc! {r#"
9001 use mod1::mod2::{mod3, mod4};
9002
9003 fn fn_1(param1: bool, param2: &str) {
9004 let var1 = "hˇello world";
9005 }
9006 "#},
9007 cx,
9008 );
9009 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9010 assert_text_with_selections(
9011 editor,
9012 indoc! {r#"
9013 use mod1::mod2::{mod3, mod4};
9014
9015 fn fn_1(param1: bool, param2: &str) {
9016 let var1 = "«ˇhello» world";
9017 }
9018 "#},
9019 cx,
9020 );
9021 });
9022
9023 // Test 2: Partial selection within a word
9024 editor.update_in(cx, |editor, window, cx| {
9025 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9026 s.select_display_ranges([
9027 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
9028 ]);
9029 });
9030 });
9031 editor.update_in(cx, |editor, window, cx| {
9032 assert_text_with_selections(
9033 editor,
9034 indoc! {r#"
9035 use mod1::mod2::{mod3, mod4};
9036
9037 fn fn_1(param1: bool, param2: &str) {
9038 let var1 = "h«elˇ»lo world";
9039 }
9040 "#},
9041 cx,
9042 );
9043 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9044 assert_text_with_selections(
9045 editor,
9046 indoc! {r#"
9047 use mod1::mod2::{mod3, mod4};
9048
9049 fn fn_1(param1: bool, param2: &str) {
9050 let var1 = "«ˇhello» world";
9051 }
9052 "#},
9053 cx,
9054 );
9055 });
9056
9057 // Test 3: Complete word already selected
9058 editor.update_in(cx, |editor, window, cx| {
9059 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9060 s.select_display_ranges([
9061 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
9062 ]);
9063 });
9064 });
9065 editor.update_in(cx, |editor, window, cx| {
9066 assert_text_with_selections(
9067 editor,
9068 indoc! {r#"
9069 use mod1::mod2::{mod3, mod4};
9070
9071 fn fn_1(param1: bool, param2: &str) {
9072 let var1 = "«helloˇ» world";
9073 }
9074 "#},
9075 cx,
9076 );
9077 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9078 assert_text_with_selections(
9079 editor,
9080 indoc! {r#"
9081 use mod1::mod2::{mod3, mod4};
9082
9083 fn fn_1(param1: bool, param2: &str) {
9084 let var1 = "«hello worldˇ»";
9085 }
9086 "#},
9087 cx,
9088 );
9089 });
9090
9091 // Test 4: Selection spanning across words
9092 editor.update_in(cx, |editor, window, cx| {
9093 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9094 s.select_display_ranges([
9095 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
9096 ]);
9097 });
9098 });
9099 editor.update_in(cx, |editor, window, cx| {
9100 assert_text_with_selections(
9101 editor,
9102 indoc! {r#"
9103 use mod1::mod2::{mod3, mod4};
9104
9105 fn fn_1(param1: bool, param2: &str) {
9106 let var1 = "hel«lo woˇ»rld";
9107 }
9108 "#},
9109 cx,
9110 );
9111 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9112 assert_text_with_selections(
9113 editor,
9114 indoc! {r#"
9115 use mod1::mod2::{mod3, mod4};
9116
9117 fn fn_1(param1: bool, param2: &str) {
9118 let var1 = "«ˇhello world»";
9119 }
9120 "#},
9121 cx,
9122 );
9123 });
9124
9125 // Test 5: Expansion beyond string
9126 editor.update_in(cx, |editor, window, cx| {
9127 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9128 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9129 assert_text_with_selections(
9130 editor,
9131 indoc! {r#"
9132 use mod1::mod2::{mod3, mod4};
9133
9134 fn fn_1(param1: bool, param2: &str) {
9135 «ˇlet var1 = "hello world";»
9136 }
9137 "#},
9138 cx,
9139 );
9140 });
9141}
9142
9143#[gpui::test]
9144async fn test_unwrap_syntax_nodes(cx: &mut gpui::TestAppContext) {
9145 init_test(cx, |_| {});
9146
9147 let mut cx = EditorTestContext::new(cx).await;
9148
9149 let language = Arc::new(Language::new(
9150 LanguageConfig::default(),
9151 Some(tree_sitter_rust::LANGUAGE.into()),
9152 ));
9153
9154 cx.update_buffer(|buffer, cx| {
9155 buffer.set_language(Some(language), cx);
9156 });
9157
9158 cx.set_state(indoc! { r#"use mod1::{mod2::{«mod3ˇ», mod4}, mod5::{mod6, «mod7ˇ»}};"# });
9159 cx.update_editor(|editor, window, cx| {
9160 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9161 });
9162
9163 cx.assert_editor_state(indoc! { r#"use mod1::{mod2::«mod3ˇ», mod5::«mod7ˇ»};"# });
9164
9165 cx.set_state(indoc! { r#"fn a() {
9166 // what
9167 // a
9168 // ˇlong
9169 // method
9170 // I
9171 // sure
9172 // hope
9173 // it
9174 // works
9175 }"# });
9176
9177 let buffer = cx.update_multibuffer(|multibuffer, _| multibuffer.as_singleton().unwrap());
9178 let multi_buffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
9179 cx.update(|_, cx| {
9180 multi_buffer.update(cx, |multi_buffer, cx| {
9181 multi_buffer.set_excerpts_for_path(
9182 PathKey::for_buffer(&buffer, cx),
9183 buffer,
9184 [Point::new(1, 0)..Point::new(1, 0)],
9185 3,
9186 cx,
9187 );
9188 });
9189 });
9190
9191 let editor2 = cx.new_window_entity(|window, cx| {
9192 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
9193 });
9194
9195 let mut cx = EditorTestContext::for_editor_in(editor2, &mut cx).await;
9196 cx.update_editor(|editor, window, cx| {
9197 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
9198 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)]);
9199 })
9200 });
9201
9202 cx.assert_editor_state(indoc! { "
9203 fn a() {
9204 // what
9205 // a
9206 ˇ // long
9207 // method"});
9208
9209 cx.update_editor(|editor, window, cx| {
9210 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9211 });
9212
9213 // Although we could potentially make the action work when the syntax node
9214 // is half-hidden, it seems a bit dangerous as you can't easily tell what it
9215 // did. Maybe we could also expand the excerpt to contain the range?
9216 cx.assert_editor_state(indoc! { "
9217 fn a() {
9218 // what
9219 // a
9220 ˇ // long
9221 // method"});
9222}
9223
9224#[gpui::test]
9225async fn test_fold_function_bodies(cx: &mut TestAppContext) {
9226 init_test(cx, |_| {});
9227
9228 let base_text = r#"
9229 impl A {
9230 // this is an uncommitted comment
9231
9232 fn b() {
9233 c();
9234 }
9235
9236 // this is another uncommitted comment
9237
9238 fn d() {
9239 // e
9240 // f
9241 }
9242 }
9243
9244 fn g() {
9245 // h
9246 }
9247 "#
9248 .unindent();
9249
9250 let text = r#"
9251 ˇimpl A {
9252
9253 fn b() {
9254 c();
9255 }
9256
9257 fn d() {
9258 // e
9259 // f
9260 }
9261 }
9262
9263 fn g() {
9264 // h
9265 }
9266 "#
9267 .unindent();
9268
9269 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9270 cx.set_state(&text);
9271 cx.set_head_text(&base_text);
9272 cx.update_editor(|editor, window, cx| {
9273 editor.expand_all_diff_hunks(&Default::default(), window, cx);
9274 });
9275
9276 cx.assert_state_with_diff(
9277 "
9278 ˇimpl A {
9279 - // this is an uncommitted comment
9280
9281 fn b() {
9282 c();
9283 }
9284
9285 - // this is another uncommitted comment
9286 -
9287 fn d() {
9288 // e
9289 // f
9290 }
9291 }
9292
9293 fn g() {
9294 // h
9295 }
9296 "
9297 .unindent(),
9298 );
9299
9300 let expected_display_text = "
9301 impl A {
9302 // this is an uncommitted comment
9303
9304 fn b() {
9305 ⋯
9306 }
9307
9308 // this is another uncommitted comment
9309
9310 fn d() {
9311 ⋯
9312 }
9313 }
9314
9315 fn g() {
9316 ⋯
9317 }
9318 "
9319 .unindent();
9320
9321 cx.update_editor(|editor, window, cx| {
9322 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
9323 assert_eq!(editor.display_text(cx), expected_display_text);
9324 });
9325}
9326
9327#[gpui::test]
9328async fn test_autoindent(cx: &mut TestAppContext) {
9329 init_test(cx, |_| {});
9330
9331 let language = Arc::new(
9332 Language::new(
9333 LanguageConfig {
9334 brackets: BracketPairConfig {
9335 pairs: vec![
9336 BracketPair {
9337 start: "{".to_string(),
9338 end: "}".to_string(),
9339 close: false,
9340 surround: false,
9341 newline: true,
9342 },
9343 BracketPair {
9344 start: "(".to_string(),
9345 end: ")".to_string(),
9346 close: false,
9347 surround: false,
9348 newline: true,
9349 },
9350 ],
9351 ..Default::default()
9352 },
9353 ..Default::default()
9354 },
9355 Some(tree_sitter_rust::LANGUAGE.into()),
9356 )
9357 .with_indents_query(
9358 r#"
9359 (_ "(" ")" @end) @indent
9360 (_ "{" "}" @end) @indent
9361 "#,
9362 )
9363 .unwrap(),
9364 );
9365
9366 let text = "fn a() {}";
9367
9368 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9369 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9370 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9371 editor
9372 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9373 .await;
9374
9375 editor.update_in(cx, |editor, window, cx| {
9376 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9377 s.select_ranges([5..5, 8..8, 9..9])
9378 });
9379 editor.newline(&Newline, window, cx);
9380 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
9381 assert_eq!(
9382 editor.selections.ranges(cx),
9383 &[
9384 Point::new(1, 4)..Point::new(1, 4),
9385 Point::new(3, 4)..Point::new(3, 4),
9386 Point::new(5, 0)..Point::new(5, 0)
9387 ]
9388 );
9389 });
9390}
9391
9392#[gpui::test]
9393async fn test_autoindent_disabled(cx: &mut TestAppContext) {
9394 init_test(cx, |settings| settings.defaults.auto_indent = Some(false));
9395
9396 let language = Arc::new(
9397 Language::new(
9398 LanguageConfig {
9399 brackets: BracketPairConfig {
9400 pairs: vec![
9401 BracketPair {
9402 start: "{".to_string(),
9403 end: "}".to_string(),
9404 close: false,
9405 surround: false,
9406 newline: true,
9407 },
9408 BracketPair {
9409 start: "(".to_string(),
9410 end: ")".to_string(),
9411 close: false,
9412 surround: false,
9413 newline: true,
9414 },
9415 ],
9416 ..Default::default()
9417 },
9418 ..Default::default()
9419 },
9420 Some(tree_sitter_rust::LANGUAGE.into()),
9421 )
9422 .with_indents_query(
9423 r#"
9424 (_ "(" ")" @end) @indent
9425 (_ "{" "}" @end) @indent
9426 "#,
9427 )
9428 .unwrap(),
9429 );
9430
9431 let text = "fn a() {}";
9432
9433 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9434 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9435 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9436 editor
9437 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9438 .await;
9439
9440 editor.update_in(cx, |editor, window, cx| {
9441 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9442 s.select_ranges([5..5, 8..8, 9..9])
9443 });
9444 editor.newline(&Newline, window, cx);
9445 assert_eq!(
9446 editor.text(cx),
9447 indoc!(
9448 "
9449 fn a(
9450
9451 ) {
9452
9453 }
9454 "
9455 )
9456 );
9457 assert_eq!(
9458 editor.selections.ranges(cx),
9459 &[
9460 Point::new(1, 0)..Point::new(1, 0),
9461 Point::new(3, 0)..Point::new(3, 0),
9462 Point::new(5, 0)..Point::new(5, 0)
9463 ]
9464 );
9465 });
9466}
9467
9468#[gpui::test]
9469async fn test_autoindent_disabled_with_nested_language(cx: &mut TestAppContext) {
9470 init_test(cx, |settings| {
9471 settings.defaults.auto_indent = Some(true);
9472 settings.languages.0.insert(
9473 "python".into(),
9474 LanguageSettingsContent {
9475 auto_indent: Some(false),
9476 ..Default::default()
9477 },
9478 );
9479 });
9480
9481 let mut cx = EditorTestContext::new(cx).await;
9482
9483 let injected_language = Arc::new(
9484 Language::new(
9485 LanguageConfig {
9486 brackets: BracketPairConfig {
9487 pairs: vec![
9488 BracketPair {
9489 start: "{".to_string(),
9490 end: "}".to_string(),
9491 close: false,
9492 surround: false,
9493 newline: true,
9494 },
9495 BracketPair {
9496 start: "(".to_string(),
9497 end: ")".to_string(),
9498 close: true,
9499 surround: false,
9500 newline: true,
9501 },
9502 ],
9503 ..Default::default()
9504 },
9505 name: "python".into(),
9506 ..Default::default()
9507 },
9508 Some(tree_sitter_python::LANGUAGE.into()),
9509 )
9510 .with_indents_query(
9511 r#"
9512 (_ "(" ")" @end) @indent
9513 (_ "{" "}" @end) @indent
9514 "#,
9515 )
9516 .unwrap(),
9517 );
9518
9519 let language = Arc::new(
9520 Language::new(
9521 LanguageConfig {
9522 brackets: BracketPairConfig {
9523 pairs: vec![
9524 BracketPair {
9525 start: "{".to_string(),
9526 end: "}".to_string(),
9527 close: false,
9528 surround: false,
9529 newline: true,
9530 },
9531 BracketPair {
9532 start: "(".to_string(),
9533 end: ")".to_string(),
9534 close: true,
9535 surround: false,
9536 newline: true,
9537 },
9538 ],
9539 ..Default::default()
9540 },
9541 name: LanguageName::new("rust"),
9542 ..Default::default()
9543 },
9544 Some(tree_sitter_rust::LANGUAGE.into()),
9545 )
9546 .with_indents_query(
9547 r#"
9548 (_ "(" ")" @end) @indent
9549 (_ "{" "}" @end) @indent
9550 "#,
9551 )
9552 .unwrap()
9553 .with_injection_query(
9554 r#"
9555 (macro_invocation
9556 macro: (identifier) @_macro_name
9557 (token_tree) @injection.content
9558 (#set! injection.language "python"))
9559 "#,
9560 )
9561 .unwrap(),
9562 );
9563
9564 cx.language_registry().add(injected_language);
9565 cx.language_registry().add(language.clone());
9566
9567 cx.update_buffer(|buffer, cx| {
9568 buffer.set_language(Some(language), cx);
9569 });
9570
9571 cx.set_state(r#"struct A {ˇ}"#);
9572
9573 cx.update_editor(|editor, window, cx| {
9574 editor.newline(&Default::default(), window, cx);
9575 });
9576
9577 cx.assert_editor_state(indoc!(
9578 "struct A {
9579 ˇ
9580 }"
9581 ));
9582
9583 cx.set_state(r#"select_biased!(ˇ)"#);
9584
9585 cx.update_editor(|editor, window, cx| {
9586 editor.newline(&Default::default(), window, cx);
9587 editor.handle_input("def ", window, cx);
9588 editor.handle_input("(", window, cx);
9589 editor.newline(&Default::default(), window, cx);
9590 editor.handle_input("a", window, cx);
9591 });
9592
9593 cx.assert_editor_state(indoc!(
9594 "select_biased!(
9595 def (
9596 aˇ
9597 )
9598 )"
9599 ));
9600}
9601
9602#[gpui::test]
9603async fn test_autoindent_selections(cx: &mut TestAppContext) {
9604 init_test(cx, |_| {});
9605
9606 {
9607 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9608 cx.set_state(indoc! {"
9609 impl A {
9610
9611 fn b() {}
9612
9613 «fn c() {
9614
9615 }ˇ»
9616 }
9617 "});
9618
9619 cx.update_editor(|editor, window, cx| {
9620 editor.autoindent(&Default::default(), window, cx);
9621 });
9622
9623 cx.assert_editor_state(indoc! {"
9624 impl A {
9625
9626 fn b() {}
9627
9628 «fn c() {
9629
9630 }ˇ»
9631 }
9632 "});
9633 }
9634
9635 {
9636 let mut cx = EditorTestContext::new_multibuffer(
9637 cx,
9638 [indoc! { "
9639 impl A {
9640 «
9641 // a
9642 fn b(){}
9643 »
9644 «
9645 }
9646 fn c(){}
9647 »
9648 "}],
9649 );
9650
9651 let buffer = cx.update_editor(|editor, _, cx| {
9652 let buffer = editor.buffer().update(cx, |buffer, _| {
9653 buffer.all_buffers().iter().next().unwrap().clone()
9654 });
9655 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
9656 buffer
9657 });
9658
9659 cx.run_until_parked();
9660 cx.update_editor(|editor, window, cx| {
9661 editor.select_all(&Default::default(), window, cx);
9662 editor.autoindent(&Default::default(), window, cx)
9663 });
9664 cx.run_until_parked();
9665
9666 cx.update(|_, cx| {
9667 assert_eq!(
9668 buffer.read(cx).text(),
9669 indoc! { "
9670 impl A {
9671
9672 // a
9673 fn b(){}
9674
9675
9676 }
9677 fn c(){}
9678
9679 " }
9680 )
9681 });
9682 }
9683}
9684
9685#[gpui::test]
9686async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
9687 init_test(cx, |_| {});
9688
9689 let mut cx = EditorTestContext::new(cx).await;
9690
9691 let language = Arc::new(Language::new(
9692 LanguageConfig {
9693 brackets: BracketPairConfig {
9694 pairs: vec![
9695 BracketPair {
9696 start: "{".to_string(),
9697 end: "}".to_string(),
9698 close: true,
9699 surround: true,
9700 newline: true,
9701 },
9702 BracketPair {
9703 start: "(".to_string(),
9704 end: ")".to_string(),
9705 close: true,
9706 surround: true,
9707 newline: true,
9708 },
9709 BracketPair {
9710 start: "/*".to_string(),
9711 end: " */".to_string(),
9712 close: true,
9713 surround: true,
9714 newline: true,
9715 },
9716 BracketPair {
9717 start: "[".to_string(),
9718 end: "]".to_string(),
9719 close: false,
9720 surround: false,
9721 newline: true,
9722 },
9723 BracketPair {
9724 start: "\"".to_string(),
9725 end: "\"".to_string(),
9726 close: true,
9727 surround: true,
9728 newline: false,
9729 },
9730 BracketPair {
9731 start: "<".to_string(),
9732 end: ">".to_string(),
9733 close: false,
9734 surround: true,
9735 newline: true,
9736 },
9737 ],
9738 ..Default::default()
9739 },
9740 autoclose_before: "})]".to_string(),
9741 ..Default::default()
9742 },
9743 Some(tree_sitter_rust::LANGUAGE.into()),
9744 ));
9745
9746 cx.language_registry().add(language.clone());
9747 cx.update_buffer(|buffer, cx| {
9748 buffer.set_language(Some(language), cx);
9749 });
9750
9751 cx.set_state(
9752 &r#"
9753 🏀ˇ
9754 εˇ
9755 ❤️ˇ
9756 "#
9757 .unindent(),
9758 );
9759
9760 // autoclose multiple nested brackets at multiple cursors
9761 cx.update_editor(|editor, window, cx| {
9762 editor.handle_input("{", window, cx);
9763 editor.handle_input("{", window, cx);
9764 editor.handle_input("{", window, cx);
9765 });
9766 cx.assert_editor_state(
9767 &"
9768 🏀{{{ˇ}}}
9769 ε{{{ˇ}}}
9770 ❤️{{{ˇ}}}
9771 "
9772 .unindent(),
9773 );
9774
9775 // insert a different closing bracket
9776 cx.update_editor(|editor, window, cx| {
9777 editor.handle_input(")", window, cx);
9778 });
9779 cx.assert_editor_state(
9780 &"
9781 🏀{{{)ˇ}}}
9782 ε{{{)ˇ}}}
9783 ❤️{{{)ˇ}}}
9784 "
9785 .unindent(),
9786 );
9787
9788 // skip over the auto-closed brackets when typing a closing bracket
9789 cx.update_editor(|editor, window, cx| {
9790 editor.move_right(&MoveRight, window, cx);
9791 editor.handle_input("}", window, cx);
9792 editor.handle_input("}", window, cx);
9793 editor.handle_input("}", window, cx);
9794 });
9795 cx.assert_editor_state(
9796 &"
9797 🏀{{{)}}}}ˇ
9798 ε{{{)}}}}ˇ
9799 ❤️{{{)}}}}ˇ
9800 "
9801 .unindent(),
9802 );
9803
9804 // autoclose multi-character pairs
9805 cx.set_state(
9806 &"
9807 ˇ
9808 ˇ
9809 "
9810 .unindent(),
9811 );
9812 cx.update_editor(|editor, window, cx| {
9813 editor.handle_input("/", window, cx);
9814 editor.handle_input("*", window, cx);
9815 });
9816 cx.assert_editor_state(
9817 &"
9818 /*ˇ */
9819 /*ˇ */
9820 "
9821 .unindent(),
9822 );
9823
9824 // one cursor autocloses a multi-character pair, one cursor
9825 // does not autoclose.
9826 cx.set_state(
9827 &"
9828 /ˇ
9829 ˇ
9830 "
9831 .unindent(),
9832 );
9833 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
9834 cx.assert_editor_state(
9835 &"
9836 /*ˇ */
9837 *ˇ
9838 "
9839 .unindent(),
9840 );
9841
9842 // Don't autoclose if the next character isn't whitespace and isn't
9843 // listed in the language's "autoclose_before" section.
9844 cx.set_state("ˇa b");
9845 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
9846 cx.assert_editor_state("{ˇa b");
9847
9848 // Don't autoclose if `close` is false for the bracket pair
9849 cx.set_state("ˇ");
9850 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
9851 cx.assert_editor_state("[ˇ");
9852
9853 // Surround with brackets if text is selected
9854 cx.set_state("«aˇ» b");
9855 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
9856 cx.assert_editor_state("{«aˇ»} b");
9857
9858 // Autoclose when not immediately after a word character
9859 cx.set_state("a ˇ");
9860 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
9861 cx.assert_editor_state("a \"ˇ\"");
9862
9863 // Autoclose pair where the start and end characters are the same
9864 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
9865 cx.assert_editor_state("a \"\"ˇ");
9866
9867 // Don't autoclose when immediately after a word character
9868 cx.set_state("aˇ");
9869 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
9870 cx.assert_editor_state("a\"ˇ");
9871
9872 // Do autoclose when after a non-word character
9873 cx.set_state("{ˇ");
9874 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
9875 cx.assert_editor_state("{\"ˇ\"");
9876
9877 // Non identical pairs autoclose regardless of preceding character
9878 cx.set_state("aˇ");
9879 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
9880 cx.assert_editor_state("a{ˇ}");
9881
9882 // Don't autoclose pair if autoclose is disabled
9883 cx.set_state("ˇ");
9884 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
9885 cx.assert_editor_state("<ˇ");
9886
9887 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
9888 cx.set_state("«aˇ» b");
9889 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
9890 cx.assert_editor_state("<«aˇ»> b");
9891}
9892
9893#[gpui::test]
9894async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
9895 init_test(cx, |settings| {
9896 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
9897 });
9898
9899 let mut cx = EditorTestContext::new(cx).await;
9900
9901 let language = Arc::new(Language::new(
9902 LanguageConfig {
9903 brackets: BracketPairConfig {
9904 pairs: vec![
9905 BracketPair {
9906 start: "{".to_string(),
9907 end: "}".to_string(),
9908 close: true,
9909 surround: true,
9910 newline: true,
9911 },
9912 BracketPair {
9913 start: "(".to_string(),
9914 end: ")".to_string(),
9915 close: true,
9916 surround: true,
9917 newline: true,
9918 },
9919 BracketPair {
9920 start: "[".to_string(),
9921 end: "]".to_string(),
9922 close: false,
9923 surround: false,
9924 newline: true,
9925 },
9926 ],
9927 ..Default::default()
9928 },
9929 autoclose_before: "})]".to_string(),
9930 ..Default::default()
9931 },
9932 Some(tree_sitter_rust::LANGUAGE.into()),
9933 ));
9934
9935 cx.language_registry().add(language.clone());
9936 cx.update_buffer(|buffer, cx| {
9937 buffer.set_language(Some(language), cx);
9938 });
9939
9940 cx.set_state(
9941 &"
9942 ˇ
9943 ˇ
9944 ˇ
9945 "
9946 .unindent(),
9947 );
9948
9949 // ensure only matching closing brackets are skipped over
9950 cx.update_editor(|editor, window, cx| {
9951 editor.handle_input("}", window, cx);
9952 editor.move_left(&MoveLeft, window, cx);
9953 editor.handle_input(")", window, cx);
9954 editor.move_left(&MoveLeft, window, cx);
9955 });
9956 cx.assert_editor_state(
9957 &"
9958 ˇ)}
9959 ˇ)}
9960 ˇ)}
9961 "
9962 .unindent(),
9963 );
9964
9965 // skip-over closing brackets at multiple cursors
9966 cx.update_editor(|editor, window, cx| {
9967 editor.handle_input(")", window, cx);
9968 editor.handle_input("}", window, cx);
9969 });
9970 cx.assert_editor_state(
9971 &"
9972 )}ˇ
9973 )}ˇ
9974 )}ˇ
9975 "
9976 .unindent(),
9977 );
9978
9979 // ignore non-close brackets
9980 cx.update_editor(|editor, window, cx| {
9981 editor.handle_input("]", window, cx);
9982 editor.move_left(&MoveLeft, window, cx);
9983 editor.handle_input("]", window, cx);
9984 });
9985 cx.assert_editor_state(
9986 &"
9987 )}]ˇ]
9988 )}]ˇ]
9989 )}]ˇ]
9990 "
9991 .unindent(),
9992 );
9993}
9994
9995#[gpui::test]
9996async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
9997 init_test(cx, |_| {});
9998
9999 let mut cx = EditorTestContext::new(cx).await;
10000
10001 let html_language = Arc::new(
10002 Language::new(
10003 LanguageConfig {
10004 name: "HTML".into(),
10005 brackets: BracketPairConfig {
10006 pairs: vec![
10007 BracketPair {
10008 start: "<".into(),
10009 end: ">".into(),
10010 close: true,
10011 ..Default::default()
10012 },
10013 BracketPair {
10014 start: "{".into(),
10015 end: "}".into(),
10016 close: true,
10017 ..Default::default()
10018 },
10019 BracketPair {
10020 start: "(".into(),
10021 end: ")".into(),
10022 close: true,
10023 ..Default::default()
10024 },
10025 ],
10026 ..Default::default()
10027 },
10028 autoclose_before: "})]>".into(),
10029 ..Default::default()
10030 },
10031 Some(tree_sitter_html::LANGUAGE.into()),
10032 )
10033 .with_injection_query(
10034 r#"
10035 (script_element
10036 (raw_text) @injection.content
10037 (#set! injection.language "javascript"))
10038 "#,
10039 )
10040 .unwrap(),
10041 );
10042
10043 let javascript_language = Arc::new(Language::new(
10044 LanguageConfig {
10045 name: "JavaScript".into(),
10046 brackets: BracketPairConfig {
10047 pairs: vec![
10048 BracketPair {
10049 start: "/*".into(),
10050 end: " */".into(),
10051 close: true,
10052 ..Default::default()
10053 },
10054 BracketPair {
10055 start: "{".into(),
10056 end: "}".into(),
10057 close: true,
10058 ..Default::default()
10059 },
10060 BracketPair {
10061 start: "(".into(),
10062 end: ")".into(),
10063 close: true,
10064 ..Default::default()
10065 },
10066 ],
10067 ..Default::default()
10068 },
10069 autoclose_before: "})]>".into(),
10070 ..Default::default()
10071 },
10072 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
10073 ));
10074
10075 cx.language_registry().add(html_language.clone());
10076 cx.language_registry().add(javascript_language);
10077 cx.executor().run_until_parked();
10078
10079 cx.update_buffer(|buffer, cx| {
10080 buffer.set_language(Some(html_language), cx);
10081 });
10082
10083 cx.set_state(
10084 &r#"
10085 <body>ˇ
10086 <script>
10087 var x = 1;ˇ
10088 </script>
10089 </body>ˇ
10090 "#
10091 .unindent(),
10092 );
10093
10094 // Precondition: different languages are active at different locations.
10095 cx.update_editor(|editor, window, cx| {
10096 let snapshot = editor.snapshot(window, cx);
10097 let cursors = editor.selections.ranges::<usize>(cx);
10098 let languages = cursors
10099 .iter()
10100 .map(|c| snapshot.language_at(c.start).unwrap().name())
10101 .collect::<Vec<_>>();
10102 assert_eq!(
10103 languages,
10104 &["HTML".into(), "JavaScript".into(), "HTML".into()]
10105 );
10106 });
10107
10108 // Angle brackets autoclose in HTML, but not JavaScript.
10109 cx.update_editor(|editor, window, cx| {
10110 editor.handle_input("<", window, cx);
10111 editor.handle_input("a", window, cx);
10112 });
10113 cx.assert_editor_state(
10114 &r#"
10115 <body><aˇ>
10116 <script>
10117 var x = 1;<aˇ
10118 </script>
10119 </body><aˇ>
10120 "#
10121 .unindent(),
10122 );
10123
10124 // Curly braces and parens autoclose in both HTML and JavaScript.
10125 cx.update_editor(|editor, window, cx| {
10126 editor.handle_input(" b=", window, cx);
10127 editor.handle_input("{", window, cx);
10128 editor.handle_input("c", window, cx);
10129 editor.handle_input("(", window, cx);
10130 });
10131 cx.assert_editor_state(
10132 &r#"
10133 <body><a b={c(ˇ)}>
10134 <script>
10135 var x = 1;<a b={c(ˇ)}
10136 </script>
10137 </body><a b={c(ˇ)}>
10138 "#
10139 .unindent(),
10140 );
10141
10142 // Brackets that were already autoclosed are skipped.
10143 cx.update_editor(|editor, window, cx| {
10144 editor.handle_input(")", window, cx);
10145 editor.handle_input("d", window, cx);
10146 editor.handle_input("}", window, cx);
10147 });
10148 cx.assert_editor_state(
10149 &r#"
10150 <body><a b={c()d}ˇ>
10151 <script>
10152 var x = 1;<a b={c()d}ˇ
10153 </script>
10154 </body><a b={c()d}ˇ>
10155 "#
10156 .unindent(),
10157 );
10158 cx.update_editor(|editor, window, cx| {
10159 editor.handle_input(">", window, cx);
10160 });
10161 cx.assert_editor_state(
10162 &r#"
10163 <body><a b={c()d}>ˇ
10164 <script>
10165 var x = 1;<a b={c()d}>ˇ
10166 </script>
10167 </body><a b={c()d}>ˇ
10168 "#
10169 .unindent(),
10170 );
10171
10172 // Reset
10173 cx.set_state(
10174 &r#"
10175 <body>ˇ
10176 <script>
10177 var x = 1;ˇ
10178 </script>
10179 </body>ˇ
10180 "#
10181 .unindent(),
10182 );
10183
10184 cx.update_editor(|editor, window, cx| {
10185 editor.handle_input("<", window, cx);
10186 });
10187 cx.assert_editor_state(
10188 &r#"
10189 <body><ˇ>
10190 <script>
10191 var x = 1;<ˇ
10192 </script>
10193 </body><ˇ>
10194 "#
10195 .unindent(),
10196 );
10197
10198 // When backspacing, the closing angle brackets are removed.
10199 cx.update_editor(|editor, window, cx| {
10200 editor.backspace(&Backspace, window, cx);
10201 });
10202 cx.assert_editor_state(
10203 &r#"
10204 <body>ˇ
10205 <script>
10206 var x = 1;ˇ
10207 </script>
10208 </body>ˇ
10209 "#
10210 .unindent(),
10211 );
10212
10213 // Block comments autoclose in JavaScript, but not HTML.
10214 cx.update_editor(|editor, window, cx| {
10215 editor.handle_input("/", window, cx);
10216 editor.handle_input("*", window, cx);
10217 });
10218 cx.assert_editor_state(
10219 &r#"
10220 <body>/*ˇ
10221 <script>
10222 var x = 1;/*ˇ */
10223 </script>
10224 </body>/*ˇ
10225 "#
10226 .unindent(),
10227 );
10228}
10229
10230#[gpui::test]
10231async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
10232 init_test(cx, |_| {});
10233
10234 let mut cx = EditorTestContext::new(cx).await;
10235
10236 let rust_language = Arc::new(
10237 Language::new(
10238 LanguageConfig {
10239 name: "Rust".into(),
10240 brackets: serde_json::from_value(json!([
10241 { "start": "{", "end": "}", "close": true, "newline": true },
10242 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
10243 ]))
10244 .unwrap(),
10245 autoclose_before: "})]>".into(),
10246 ..Default::default()
10247 },
10248 Some(tree_sitter_rust::LANGUAGE.into()),
10249 )
10250 .with_override_query("(string_literal) @string")
10251 .unwrap(),
10252 );
10253
10254 cx.language_registry().add(rust_language.clone());
10255 cx.update_buffer(|buffer, cx| {
10256 buffer.set_language(Some(rust_language), cx);
10257 });
10258
10259 cx.set_state(
10260 &r#"
10261 let x = ˇ
10262 "#
10263 .unindent(),
10264 );
10265
10266 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
10267 cx.update_editor(|editor, window, cx| {
10268 editor.handle_input("\"", window, cx);
10269 });
10270 cx.assert_editor_state(
10271 &r#"
10272 let x = "ˇ"
10273 "#
10274 .unindent(),
10275 );
10276
10277 // Inserting another quotation mark. The cursor moves across the existing
10278 // automatically-inserted quotation mark.
10279 cx.update_editor(|editor, window, cx| {
10280 editor.handle_input("\"", window, cx);
10281 });
10282 cx.assert_editor_state(
10283 &r#"
10284 let x = ""ˇ
10285 "#
10286 .unindent(),
10287 );
10288
10289 // Reset
10290 cx.set_state(
10291 &r#"
10292 let x = ˇ
10293 "#
10294 .unindent(),
10295 );
10296
10297 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
10298 cx.update_editor(|editor, window, cx| {
10299 editor.handle_input("\"", window, cx);
10300 editor.handle_input(" ", window, cx);
10301 editor.move_left(&Default::default(), window, cx);
10302 editor.handle_input("\\", window, cx);
10303 editor.handle_input("\"", window, cx);
10304 });
10305 cx.assert_editor_state(
10306 &r#"
10307 let x = "\"ˇ "
10308 "#
10309 .unindent(),
10310 );
10311
10312 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
10313 // mark. Nothing is inserted.
10314 cx.update_editor(|editor, window, cx| {
10315 editor.move_right(&Default::default(), window, cx);
10316 editor.handle_input("\"", window, cx);
10317 });
10318 cx.assert_editor_state(
10319 &r#"
10320 let x = "\" "ˇ
10321 "#
10322 .unindent(),
10323 );
10324}
10325
10326#[gpui::test]
10327async fn test_surround_with_pair(cx: &mut TestAppContext) {
10328 init_test(cx, |_| {});
10329
10330 let language = Arc::new(Language::new(
10331 LanguageConfig {
10332 brackets: BracketPairConfig {
10333 pairs: vec![
10334 BracketPair {
10335 start: "{".to_string(),
10336 end: "}".to_string(),
10337 close: true,
10338 surround: true,
10339 newline: true,
10340 },
10341 BracketPair {
10342 start: "/* ".to_string(),
10343 end: "*/".to_string(),
10344 close: true,
10345 surround: true,
10346 ..Default::default()
10347 },
10348 ],
10349 ..Default::default()
10350 },
10351 ..Default::default()
10352 },
10353 Some(tree_sitter_rust::LANGUAGE.into()),
10354 ));
10355
10356 let text = r#"
10357 a
10358 b
10359 c
10360 "#
10361 .unindent();
10362
10363 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10364 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10365 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10366 editor
10367 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10368 .await;
10369
10370 editor.update_in(cx, |editor, window, cx| {
10371 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10372 s.select_display_ranges([
10373 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10374 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10375 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
10376 ])
10377 });
10378
10379 editor.handle_input("{", window, cx);
10380 editor.handle_input("{", window, cx);
10381 editor.handle_input("{", window, cx);
10382 assert_eq!(
10383 editor.text(cx),
10384 "
10385 {{{a}}}
10386 {{{b}}}
10387 {{{c}}}
10388 "
10389 .unindent()
10390 );
10391 assert_eq!(
10392 editor.selections.display_ranges(cx),
10393 [
10394 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
10395 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
10396 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
10397 ]
10398 );
10399
10400 editor.undo(&Undo, window, cx);
10401 editor.undo(&Undo, window, cx);
10402 editor.undo(&Undo, window, cx);
10403 assert_eq!(
10404 editor.text(cx),
10405 "
10406 a
10407 b
10408 c
10409 "
10410 .unindent()
10411 );
10412 assert_eq!(
10413 editor.selections.display_ranges(cx),
10414 [
10415 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10416 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10417 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10418 ]
10419 );
10420
10421 // Ensure inserting the first character of a multi-byte bracket pair
10422 // doesn't surround the selections with the bracket.
10423 editor.handle_input("/", window, cx);
10424 assert_eq!(
10425 editor.text(cx),
10426 "
10427 /
10428 /
10429 /
10430 "
10431 .unindent()
10432 );
10433 assert_eq!(
10434 editor.selections.display_ranges(cx),
10435 [
10436 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10437 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10438 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10439 ]
10440 );
10441
10442 editor.undo(&Undo, window, cx);
10443 assert_eq!(
10444 editor.text(cx),
10445 "
10446 a
10447 b
10448 c
10449 "
10450 .unindent()
10451 );
10452 assert_eq!(
10453 editor.selections.display_ranges(cx),
10454 [
10455 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10456 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10457 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10458 ]
10459 );
10460
10461 // Ensure inserting the last character of a multi-byte bracket pair
10462 // doesn't surround the selections with the bracket.
10463 editor.handle_input("*", window, cx);
10464 assert_eq!(
10465 editor.text(cx),
10466 "
10467 *
10468 *
10469 *
10470 "
10471 .unindent()
10472 );
10473 assert_eq!(
10474 editor.selections.display_ranges(cx),
10475 [
10476 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10477 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10478 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10479 ]
10480 );
10481 });
10482}
10483
10484#[gpui::test]
10485async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
10486 init_test(cx, |_| {});
10487
10488 let language = Arc::new(Language::new(
10489 LanguageConfig {
10490 brackets: BracketPairConfig {
10491 pairs: vec![BracketPair {
10492 start: "{".to_string(),
10493 end: "}".to_string(),
10494 close: true,
10495 surround: true,
10496 newline: true,
10497 }],
10498 ..Default::default()
10499 },
10500 autoclose_before: "}".to_string(),
10501 ..Default::default()
10502 },
10503 Some(tree_sitter_rust::LANGUAGE.into()),
10504 ));
10505
10506 let text = r#"
10507 a
10508 b
10509 c
10510 "#
10511 .unindent();
10512
10513 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10514 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10515 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10516 editor
10517 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10518 .await;
10519
10520 editor.update_in(cx, |editor, window, cx| {
10521 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10522 s.select_ranges([
10523 Point::new(0, 1)..Point::new(0, 1),
10524 Point::new(1, 1)..Point::new(1, 1),
10525 Point::new(2, 1)..Point::new(2, 1),
10526 ])
10527 });
10528
10529 editor.handle_input("{", window, cx);
10530 editor.handle_input("{", window, cx);
10531 editor.handle_input("_", window, cx);
10532 assert_eq!(
10533 editor.text(cx),
10534 "
10535 a{{_}}
10536 b{{_}}
10537 c{{_}}
10538 "
10539 .unindent()
10540 );
10541 assert_eq!(
10542 editor.selections.ranges::<Point>(cx),
10543 [
10544 Point::new(0, 4)..Point::new(0, 4),
10545 Point::new(1, 4)..Point::new(1, 4),
10546 Point::new(2, 4)..Point::new(2, 4)
10547 ]
10548 );
10549
10550 editor.backspace(&Default::default(), window, cx);
10551 editor.backspace(&Default::default(), window, cx);
10552 assert_eq!(
10553 editor.text(cx),
10554 "
10555 a{}
10556 b{}
10557 c{}
10558 "
10559 .unindent()
10560 );
10561 assert_eq!(
10562 editor.selections.ranges::<Point>(cx),
10563 [
10564 Point::new(0, 2)..Point::new(0, 2),
10565 Point::new(1, 2)..Point::new(1, 2),
10566 Point::new(2, 2)..Point::new(2, 2)
10567 ]
10568 );
10569
10570 editor.delete_to_previous_word_start(&Default::default(), window, cx);
10571 assert_eq!(
10572 editor.text(cx),
10573 "
10574 a
10575 b
10576 c
10577 "
10578 .unindent()
10579 );
10580 assert_eq!(
10581 editor.selections.ranges::<Point>(cx),
10582 [
10583 Point::new(0, 1)..Point::new(0, 1),
10584 Point::new(1, 1)..Point::new(1, 1),
10585 Point::new(2, 1)..Point::new(2, 1)
10586 ]
10587 );
10588 });
10589}
10590
10591#[gpui::test]
10592async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
10593 init_test(cx, |settings| {
10594 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
10595 });
10596
10597 let mut cx = EditorTestContext::new(cx).await;
10598
10599 let language = Arc::new(Language::new(
10600 LanguageConfig {
10601 brackets: BracketPairConfig {
10602 pairs: vec![
10603 BracketPair {
10604 start: "{".to_string(),
10605 end: "}".to_string(),
10606 close: true,
10607 surround: true,
10608 newline: true,
10609 },
10610 BracketPair {
10611 start: "(".to_string(),
10612 end: ")".to_string(),
10613 close: true,
10614 surround: true,
10615 newline: true,
10616 },
10617 BracketPair {
10618 start: "[".to_string(),
10619 end: "]".to_string(),
10620 close: false,
10621 surround: true,
10622 newline: true,
10623 },
10624 ],
10625 ..Default::default()
10626 },
10627 autoclose_before: "})]".to_string(),
10628 ..Default::default()
10629 },
10630 Some(tree_sitter_rust::LANGUAGE.into()),
10631 ));
10632
10633 cx.language_registry().add(language.clone());
10634 cx.update_buffer(|buffer, cx| {
10635 buffer.set_language(Some(language), cx);
10636 });
10637
10638 cx.set_state(
10639 &"
10640 {(ˇ)}
10641 [[ˇ]]
10642 {(ˇ)}
10643 "
10644 .unindent(),
10645 );
10646
10647 cx.update_editor(|editor, window, cx| {
10648 editor.backspace(&Default::default(), window, cx);
10649 editor.backspace(&Default::default(), window, cx);
10650 });
10651
10652 cx.assert_editor_state(
10653 &"
10654 ˇ
10655 ˇ]]
10656 ˇ
10657 "
10658 .unindent(),
10659 );
10660
10661 cx.update_editor(|editor, window, cx| {
10662 editor.handle_input("{", window, cx);
10663 editor.handle_input("{", window, cx);
10664 editor.move_right(&MoveRight, window, cx);
10665 editor.move_right(&MoveRight, window, cx);
10666 editor.move_left(&MoveLeft, window, cx);
10667 editor.move_left(&MoveLeft, window, cx);
10668 editor.backspace(&Default::default(), window, cx);
10669 });
10670
10671 cx.assert_editor_state(
10672 &"
10673 {ˇ}
10674 {ˇ}]]
10675 {ˇ}
10676 "
10677 .unindent(),
10678 );
10679
10680 cx.update_editor(|editor, window, cx| {
10681 editor.backspace(&Default::default(), window, cx);
10682 });
10683
10684 cx.assert_editor_state(
10685 &"
10686 ˇ
10687 ˇ]]
10688 ˇ
10689 "
10690 .unindent(),
10691 );
10692}
10693
10694#[gpui::test]
10695async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
10696 init_test(cx, |_| {});
10697
10698 let language = Arc::new(Language::new(
10699 LanguageConfig::default(),
10700 Some(tree_sitter_rust::LANGUAGE.into()),
10701 ));
10702
10703 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
10704 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10705 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10706 editor
10707 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10708 .await;
10709
10710 editor.update_in(cx, |editor, window, cx| {
10711 editor.set_auto_replace_emoji_shortcode(true);
10712
10713 editor.handle_input("Hello ", window, cx);
10714 editor.handle_input(":wave", window, cx);
10715 assert_eq!(editor.text(cx), "Hello :wave".unindent());
10716
10717 editor.handle_input(":", window, cx);
10718 assert_eq!(editor.text(cx), "Hello 👋".unindent());
10719
10720 editor.handle_input(" :smile", window, cx);
10721 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
10722
10723 editor.handle_input(":", window, cx);
10724 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
10725
10726 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
10727 editor.handle_input(":wave", window, cx);
10728 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
10729
10730 editor.handle_input(":", window, cx);
10731 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
10732
10733 editor.handle_input(":1", window, cx);
10734 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
10735
10736 editor.handle_input(":", window, cx);
10737 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
10738
10739 // Ensure shortcode does not get replaced when it is part of a word
10740 editor.handle_input(" Test:wave", window, cx);
10741 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
10742
10743 editor.handle_input(":", window, cx);
10744 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
10745
10746 editor.set_auto_replace_emoji_shortcode(false);
10747
10748 // Ensure shortcode does not get replaced when auto replace is off
10749 editor.handle_input(" :wave", window, cx);
10750 assert_eq!(
10751 editor.text(cx),
10752 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
10753 );
10754
10755 editor.handle_input(":", window, cx);
10756 assert_eq!(
10757 editor.text(cx),
10758 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
10759 );
10760 });
10761}
10762
10763#[gpui::test]
10764async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
10765 init_test(cx, |_| {});
10766
10767 let (text, insertion_ranges) = marked_text_ranges(
10768 indoc! {"
10769 ˇ
10770 "},
10771 false,
10772 );
10773
10774 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
10775 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10776
10777 _ = editor.update_in(cx, |editor, window, cx| {
10778 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
10779
10780 editor
10781 .insert_snippet(&insertion_ranges, snippet, window, cx)
10782 .unwrap();
10783
10784 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
10785 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
10786 assert_eq!(editor.text(cx), expected_text);
10787 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
10788 }
10789
10790 assert(
10791 editor,
10792 cx,
10793 indoc! {"
10794 type «» =•
10795 "},
10796 );
10797
10798 assert!(editor.context_menu_visible(), "There should be a matches");
10799 });
10800}
10801
10802#[gpui::test]
10803async fn test_snippets(cx: &mut TestAppContext) {
10804 init_test(cx, |_| {});
10805
10806 let mut cx = EditorTestContext::new(cx).await;
10807
10808 cx.set_state(indoc! {"
10809 a.ˇ b
10810 a.ˇ b
10811 a.ˇ b
10812 "});
10813
10814 cx.update_editor(|editor, window, cx| {
10815 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
10816 let insertion_ranges = editor
10817 .selections
10818 .all(cx)
10819 .iter()
10820 .map(|s| s.range())
10821 .collect::<Vec<_>>();
10822 editor
10823 .insert_snippet(&insertion_ranges, snippet, window, cx)
10824 .unwrap();
10825 });
10826
10827 cx.assert_editor_state(indoc! {"
10828 a.f(«oneˇ», two, «threeˇ») b
10829 a.f(«oneˇ», two, «threeˇ») b
10830 a.f(«oneˇ», two, «threeˇ») b
10831 "});
10832
10833 // Can't move earlier than the first tab stop
10834 cx.update_editor(|editor, window, cx| {
10835 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
10836 });
10837 cx.assert_editor_state(indoc! {"
10838 a.f(«oneˇ», two, «threeˇ») b
10839 a.f(«oneˇ», two, «threeˇ») b
10840 a.f(«oneˇ», two, «threeˇ») b
10841 "});
10842
10843 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
10844 cx.assert_editor_state(indoc! {"
10845 a.f(one, «twoˇ», three) b
10846 a.f(one, «twoˇ», three) b
10847 a.f(one, «twoˇ», three) b
10848 "});
10849
10850 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
10851 cx.assert_editor_state(indoc! {"
10852 a.f(«oneˇ», two, «threeˇ») b
10853 a.f(«oneˇ», two, «threeˇ») b
10854 a.f(«oneˇ», two, «threeˇ») b
10855 "});
10856
10857 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
10858 cx.assert_editor_state(indoc! {"
10859 a.f(one, «twoˇ», three) b
10860 a.f(one, «twoˇ», three) b
10861 a.f(one, «twoˇ», three) b
10862 "});
10863 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
10864 cx.assert_editor_state(indoc! {"
10865 a.f(one, two, three)ˇ b
10866 a.f(one, two, three)ˇ b
10867 a.f(one, two, three)ˇ b
10868 "});
10869
10870 // As soon as the last tab stop is reached, snippet state is gone
10871 cx.update_editor(|editor, window, cx| {
10872 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
10873 });
10874 cx.assert_editor_state(indoc! {"
10875 a.f(one, two, three)ˇ b
10876 a.f(one, two, three)ˇ b
10877 a.f(one, two, three)ˇ b
10878 "});
10879}
10880
10881#[gpui::test]
10882async fn test_snippet_indentation(cx: &mut TestAppContext) {
10883 init_test(cx, |_| {});
10884
10885 let mut cx = EditorTestContext::new(cx).await;
10886
10887 cx.update_editor(|editor, window, cx| {
10888 let snippet = Snippet::parse(indoc! {"
10889 /*
10890 * Multiline comment with leading indentation
10891 *
10892 * $1
10893 */
10894 $0"})
10895 .unwrap();
10896 let insertion_ranges = editor
10897 .selections
10898 .all(cx)
10899 .iter()
10900 .map(|s| s.range())
10901 .collect::<Vec<_>>();
10902 editor
10903 .insert_snippet(&insertion_ranges, snippet, window, cx)
10904 .unwrap();
10905 });
10906
10907 cx.assert_editor_state(indoc! {"
10908 /*
10909 * Multiline comment with leading indentation
10910 *
10911 * ˇ
10912 */
10913 "});
10914
10915 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
10916 cx.assert_editor_state(indoc! {"
10917 /*
10918 * Multiline comment with leading indentation
10919 *
10920 *•
10921 */
10922 ˇ"});
10923}
10924
10925#[gpui::test]
10926async fn test_document_format_during_save(cx: &mut TestAppContext) {
10927 init_test(cx, |_| {});
10928
10929 let fs = FakeFs::new(cx.executor());
10930 fs.insert_file(path!("/file.rs"), Default::default()).await;
10931
10932 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
10933
10934 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10935 language_registry.add(rust_lang());
10936 let mut fake_servers = language_registry.register_fake_lsp(
10937 "Rust",
10938 FakeLspAdapter {
10939 capabilities: lsp::ServerCapabilities {
10940 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10941 ..Default::default()
10942 },
10943 ..Default::default()
10944 },
10945 );
10946
10947 let buffer = project
10948 .update(cx, |project, cx| {
10949 project.open_local_buffer(path!("/file.rs"), cx)
10950 })
10951 .await
10952 .unwrap();
10953
10954 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10955 let (editor, cx) = cx.add_window_view(|window, cx| {
10956 build_editor_with_project(project.clone(), buffer, window, cx)
10957 });
10958 editor.update_in(cx, |editor, window, cx| {
10959 editor.set_text("one\ntwo\nthree\n", window, cx)
10960 });
10961 assert!(cx.read(|cx| editor.is_dirty(cx)));
10962
10963 cx.executor().start_waiting();
10964 let fake_server = fake_servers.next().await.unwrap();
10965
10966 {
10967 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10968 move |params, _| async move {
10969 assert_eq!(
10970 params.text_document.uri,
10971 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
10972 );
10973 assert_eq!(params.options.tab_size, 4);
10974 Ok(Some(vec![lsp::TextEdit::new(
10975 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
10976 ", ".to_string(),
10977 )]))
10978 },
10979 );
10980 let save = editor
10981 .update_in(cx, |editor, window, cx| {
10982 editor.save(
10983 SaveOptions {
10984 format: true,
10985 autosave: false,
10986 },
10987 project.clone(),
10988 window,
10989 cx,
10990 )
10991 })
10992 .unwrap();
10993 cx.executor().start_waiting();
10994 save.await;
10995
10996 assert_eq!(
10997 editor.update(cx, |editor, cx| editor.text(cx)),
10998 "one, two\nthree\n"
10999 );
11000 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11001 }
11002
11003 {
11004 editor.update_in(cx, |editor, window, cx| {
11005 editor.set_text("one\ntwo\nthree\n", window, cx)
11006 });
11007 assert!(cx.read(|cx| editor.is_dirty(cx)));
11008
11009 // Ensure we can still save even if formatting hangs.
11010 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11011 move |params, _| async move {
11012 assert_eq!(
11013 params.text_document.uri,
11014 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11015 );
11016 futures::future::pending::<()>().await;
11017 unreachable!()
11018 },
11019 );
11020 let save = editor
11021 .update_in(cx, |editor, window, cx| {
11022 editor.save(
11023 SaveOptions {
11024 format: true,
11025 autosave: false,
11026 },
11027 project.clone(),
11028 window,
11029 cx,
11030 )
11031 })
11032 .unwrap();
11033 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11034 cx.executor().start_waiting();
11035 save.await;
11036 assert_eq!(
11037 editor.update(cx, |editor, cx| editor.text(cx)),
11038 "one\ntwo\nthree\n"
11039 );
11040 }
11041
11042 // Set rust language override and assert overridden tabsize is sent to language server
11043 update_test_language_settings(cx, |settings| {
11044 settings.languages.0.insert(
11045 "Rust".into(),
11046 LanguageSettingsContent {
11047 tab_size: NonZeroU32::new(8),
11048 ..Default::default()
11049 },
11050 );
11051 });
11052
11053 {
11054 editor.update_in(cx, |editor, window, cx| {
11055 editor.set_text("somehting_new\n", window, cx)
11056 });
11057 assert!(cx.read(|cx| editor.is_dirty(cx)));
11058 let _formatting_request_signal = fake_server
11059 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
11060 assert_eq!(
11061 params.text_document.uri,
11062 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11063 );
11064 assert_eq!(params.options.tab_size, 8);
11065 Ok(Some(vec![]))
11066 });
11067 let save = editor
11068 .update_in(cx, |editor, window, cx| {
11069 editor.save(
11070 SaveOptions {
11071 format: true,
11072 autosave: false,
11073 },
11074 project.clone(),
11075 window,
11076 cx,
11077 )
11078 })
11079 .unwrap();
11080 cx.executor().start_waiting();
11081 save.await;
11082 }
11083}
11084
11085#[gpui::test]
11086async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
11087 init_test(cx, |settings| {
11088 settings.defaults.ensure_final_newline_on_save = Some(false);
11089 });
11090
11091 let fs = FakeFs::new(cx.executor());
11092 fs.insert_file(path!("/file.txt"), "foo".into()).await;
11093
11094 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
11095
11096 let buffer = project
11097 .update(cx, |project, cx| {
11098 project.open_local_buffer(path!("/file.txt"), cx)
11099 })
11100 .await
11101 .unwrap();
11102
11103 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11104 let (editor, cx) = cx.add_window_view(|window, cx| {
11105 build_editor_with_project(project.clone(), buffer, window, cx)
11106 });
11107 editor.update_in(cx, |editor, window, cx| {
11108 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
11109 s.select_ranges([0..0])
11110 });
11111 });
11112 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11113
11114 editor.update_in(cx, |editor, window, cx| {
11115 editor.handle_input("\n", window, cx)
11116 });
11117 cx.run_until_parked();
11118 save(&editor, &project, cx).await;
11119 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11120
11121 editor.update_in(cx, |editor, window, cx| {
11122 editor.undo(&Default::default(), window, cx);
11123 });
11124 save(&editor, &project, cx).await;
11125 assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11126
11127 editor.update_in(cx, |editor, window, cx| {
11128 editor.redo(&Default::default(), window, cx);
11129 });
11130 cx.run_until_parked();
11131 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11132
11133 async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
11134 let save = editor
11135 .update_in(cx, |editor, window, cx| {
11136 editor.save(
11137 SaveOptions {
11138 format: true,
11139 autosave: false,
11140 },
11141 project.clone(),
11142 window,
11143 cx,
11144 )
11145 })
11146 .unwrap();
11147 cx.executor().start_waiting();
11148 save.await;
11149 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11150 }
11151}
11152
11153#[gpui::test]
11154async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
11155 init_test(cx, |_| {});
11156
11157 let cols = 4;
11158 let rows = 10;
11159 let sample_text_1 = sample_text(rows, cols, 'a');
11160 assert_eq!(
11161 sample_text_1,
11162 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
11163 );
11164 let sample_text_2 = sample_text(rows, cols, 'l');
11165 assert_eq!(
11166 sample_text_2,
11167 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
11168 );
11169 let sample_text_3 = sample_text(rows, cols, 'v');
11170 assert_eq!(
11171 sample_text_3,
11172 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
11173 );
11174
11175 let fs = FakeFs::new(cx.executor());
11176 fs.insert_tree(
11177 path!("/a"),
11178 json!({
11179 "main.rs": sample_text_1,
11180 "other.rs": sample_text_2,
11181 "lib.rs": sample_text_3,
11182 }),
11183 )
11184 .await;
11185
11186 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11187 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11188 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11189
11190 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11191 language_registry.add(rust_lang());
11192 let mut fake_servers = language_registry.register_fake_lsp(
11193 "Rust",
11194 FakeLspAdapter {
11195 capabilities: lsp::ServerCapabilities {
11196 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11197 ..Default::default()
11198 },
11199 ..Default::default()
11200 },
11201 );
11202
11203 let worktree = project.update(cx, |project, cx| {
11204 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
11205 assert_eq!(worktrees.len(), 1);
11206 worktrees.pop().unwrap()
11207 });
11208 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
11209
11210 let buffer_1 = project
11211 .update(cx, |project, cx| {
11212 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
11213 })
11214 .await
11215 .unwrap();
11216 let buffer_2 = project
11217 .update(cx, |project, cx| {
11218 project.open_buffer((worktree_id, rel_path("other.rs")), cx)
11219 })
11220 .await
11221 .unwrap();
11222 let buffer_3 = project
11223 .update(cx, |project, cx| {
11224 project.open_buffer((worktree_id, rel_path("lib.rs")), cx)
11225 })
11226 .await
11227 .unwrap();
11228
11229 let multi_buffer = cx.new(|cx| {
11230 let mut multi_buffer = MultiBuffer::new(ReadWrite);
11231 multi_buffer.push_excerpts(
11232 buffer_1.clone(),
11233 [
11234 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11235 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11236 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11237 ],
11238 cx,
11239 );
11240 multi_buffer.push_excerpts(
11241 buffer_2.clone(),
11242 [
11243 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11244 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11245 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11246 ],
11247 cx,
11248 );
11249 multi_buffer.push_excerpts(
11250 buffer_3.clone(),
11251 [
11252 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11253 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11254 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11255 ],
11256 cx,
11257 );
11258 multi_buffer
11259 });
11260 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
11261 Editor::new(
11262 EditorMode::full(),
11263 multi_buffer,
11264 Some(project.clone()),
11265 window,
11266 cx,
11267 )
11268 });
11269
11270 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11271 editor.change_selections(
11272 SelectionEffects::scroll(Autoscroll::Next),
11273 window,
11274 cx,
11275 |s| s.select_ranges(Some(1..2)),
11276 );
11277 editor.insert("|one|two|three|", window, cx);
11278 });
11279 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11280 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11281 editor.change_selections(
11282 SelectionEffects::scroll(Autoscroll::Next),
11283 window,
11284 cx,
11285 |s| s.select_ranges(Some(60..70)),
11286 );
11287 editor.insert("|four|five|six|", window, cx);
11288 });
11289 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11290
11291 // First two buffers should be edited, but not the third one.
11292 assert_eq!(
11293 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11294 "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}",
11295 );
11296 buffer_1.update(cx, |buffer, _| {
11297 assert!(buffer.is_dirty());
11298 assert_eq!(
11299 buffer.text(),
11300 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
11301 )
11302 });
11303 buffer_2.update(cx, |buffer, _| {
11304 assert!(buffer.is_dirty());
11305 assert_eq!(
11306 buffer.text(),
11307 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
11308 )
11309 });
11310 buffer_3.update(cx, |buffer, _| {
11311 assert!(!buffer.is_dirty());
11312 assert_eq!(buffer.text(), sample_text_3,)
11313 });
11314 cx.executor().run_until_parked();
11315
11316 cx.executor().start_waiting();
11317 let save = multi_buffer_editor
11318 .update_in(cx, |editor, window, cx| {
11319 editor.save(
11320 SaveOptions {
11321 format: true,
11322 autosave: false,
11323 },
11324 project.clone(),
11325 window,
11326 cx,
11327 )
11328 })
11329 .unwrap();
11330
11331 let fake_server = fake_servers.next().await.unwrap();
11332 fake_server
11333 .server
11334 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
11335 Ok(Some(vec![lsp::TextEdit::new(
11336 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11337 format!("[{} formatted]", params.text_document.uri),
11338 )]))
11339 })
11340 .detach();
11341 save.await;
11342
11343 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
11344 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
11345 assert_eq!(
11346 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11347 uri!(
11348 "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}"
11349 ),
11350 );
11351 buffer_1.update(cx, |buffer, _| {
11352 assert!(!buffer.is_dirty());
11353 assert_eq!(
11354 buffer.text(),
11355 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
11356 )
11357 });
11358 buffer_2.update(cx, |buffer, _| {
11359 assert!(!buffer.is_dirty());
11360 assert_eq!(
11361 buffer.text(),
11362 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
11363 )
11364 });
11365 buffer_3.update(cx, |buffer, _| {
11366 assert!(!buffer.is_dirty());
11367 assert_eq!(buffer.text(), sample_text_3,)
11368 });
11369}
11370
11371#[gpui::test]
11372async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
11373 init_test(cx, |_| {});
11374
11375 let fs = FakeFs::new(cx.executor());
11376 fs.insert_tree(
11377 path!("/dir"),
11378 json!({
11379 "file1.rs": "fn main() { println!(\"hello\"); }",
11380 "file2.rs": "fn test() { println!(\"test\"); }",
11381 "file3.rs": "fn other() { println!(\"other\"); }\n",
11382 }),
11383 )
11384 .await;
11385
11386 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
11387 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11388 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11389
11390 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11391 language_registry.add(rust_lang());
11392
11393 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
11394 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
11395
11396 // Open three buffers
11397 let buffer_1 = project
11398 .update(cx, |project, cx| {
11399 project.open_buffer((worktree_id, rel_path("file1.rs")), cx)
11400 })
11401 .await
11402 .unwrap();
11403 let buffer_2 = project
11404 .update(cx, |project, cx| {
11405 project.open_buffer((worktree_id, rel_path("file2.rs")), cx)
11406 })
11407 .await
11408 .unwrap();
11409 let buffer_3 = project
11410 .update(cx, |project, cx| {
11411 project.open_buffer((worktree_id, rel_path("file3.rs")), cx)
11412 })
11413 .await
11414 .unwrap();
11415
11416 // Create a multi-buffer with all three buffers
11417 let multi_buffer = cx.new(|cx| {
11418 let mut multi_buffer = MultiBuffer::new(ReadWrite);
11419 multi_buffer.push_excerpts(
11420 buffer_1.clone(),
11421 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11422 cx,
11423 );
11424 multi_buffer.push_excerpts(
11425 buffer_2.clone(),
11426 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11427 cx,
11428 );
11429 multi_buffer.push_excerpts(
11430 buffer_3.clone(),
11431 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11432 cx,
11433 );
11434 multi_buffer
11435 });
11436
11437 let editor = cx.new_window_entity(|window, cx| {
11438 Editor::new(
11439 EditorMode::full(),
11440 multi_buffer,
11441 Some(project.clone()),
11442 window,
11443 cx,
11444 )
11445 });
11446
11447 // Edit only the first buffer
11448 editor.update_in(cx, |editor, window, cx| {
11449 editor.change_selections(
11450 SelectionEffects::scroll(Autoscroll::Next),
11451 window,
11452 cx,
11453 |s| s.select_ranges(Some(10..10)),
11454 );
11455 editor.insert("// edited", window, cx);
11456 });
11457
11458 // Verify that only buffer 1 is dirty
11459 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
11460 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11461 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11462
11463 // Get write counts after file creation (files were created with initial content)
11464 // We expect each file to have been written once during creation
11465 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
11466 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
11467 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
11468
11469 // Perform autosave
11470 let save_task = editor.update_in(cx, |editor, window, cx| {
11471 editor.save(
11472 SaveOptions {
11473 format: true,
11474 autosave: true,
11475 },
11476 project.clone(),
11477 window,
11478 cx,
11479 )
11480 });
11481 save_task.await.unwrap();
11482
11483 // Only the dirty buffer should have been saved
11484 assert_eq!(
11485 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
11486 1,
11487 "Buffer 1 was dirty, so it should have been written once during autosave"
11488 );
11489 assert_eq!(
11490 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
11491 0,
11492 "Buffer 2 was clean, so it should not have been written during autosave"
11493 );
11494 assert_eq!(
11495 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
11496 0,
11497 "Buffer 3 was clean, so it should not have been written during autosave"
11498 );
11499
11500 // Verify buffer states after autosave
11501 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11502 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11503 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11504
11505 // Now perform a manual save (format = true)
11506 let save_task = editor.update_in(cx, |editor, window, cx| {
11507 editor.save(
11508 SaveOptions {
11509 format: true,
11510 autosave: false,
11511 },
11512 project.clone(),
11513 window,
11514 cx,
11515 )
11516 });
11517 save_task.await.unwrap();
11518
11519 // During manual save, clean buffers don't get written to disk
11520 // They just get did_save called for language server notifications
11521 assert_eq!(
11522 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
11523 1,
11524 "Buffer 1 should only have been written once total (during autosave, not manual save)"
11525 );
11526 assert_eq!(
11527 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
11528 0,
11529 "Buffer 2 should not have been written at all"
11530 );
11531 assert_eq!(
11532 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
11533 0,
11534 "Buffer 3 should not have been written at all"
11535 );
11536}
11537
11538async fn setup_range_format_test(
11539 cx: &mut TestAppContext,
11540) -> (
11541 Entity<Project>,
11542 Entity<Editor>,
11543 &mut gpui::VisualTestContext,
11544 lsp::FakeLanguageServer,
11545) {
11546 init_test(cx, |_| {});
11547
11548 let fs = FakeFs::new(cx.executor());
11549 fs.insert_file(path!("/file.rs"), Default::default()).await;
11550
11551 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11552
11553 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11554 language_registry.add(rust_lang());
11555 let mut fake_servers = language_registry.register_fake_lsp(
11556 "Rust",
11557 FakeLspAdapter {
11558 capabilities: lsp::ServerCapabilities {
11559 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
11560 ..lsp::ServerCapabilities::default()
11561 },
11562 ..FakeLspAdapter::default()
11563 },
11564 );
11565
11566 let buffer = project
11567 .update(cx, |project, cx| {
11568 project.open_local_buffer(path!("/file.rs"), cx)
11569 })
11570 .await
11571 .unwrap();
11572
11573 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11574 let (editor, cx) = cx.add_window_view(|window, cx| {
11575 build_editor_with_project(project.clone(), buffer, window, cx)
11576 });
11577
11578 cx.executor().start_waiting();
11579 let fake_server = fake_servers.next().await.unwrap();
11580
11581 (project, editor, cx, fake_server)
11582}
11583
11584#[gpui::test]
11585async fn test_range_format_on_save_success(cx: &mut TestAppContext) {
11586 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11587
11588 editor.update_in(cx, |editor, window, cx| {
11589 editor.set_text("one\ntwo\nthree\n", window, cx)
11590 });
11591 assert!(cx.read(|cx| editor.is_dirty(cx)));
11592
11593 let save = editor
11594 .update_in(cx, |editor, window, cx| {
11595 editor.save(
11596 SaveOptions {
11597 format: true,
11598 autosave: false,
11599 },
11600 project.clone(),
11601 window,
11602 cx,
11603 )
11604 })
11605 .unwrap();
11606 fake_server
11607 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
11608 assert_eq!(
11609 params.text_document.uri,
11610 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11611 );
11612 assert_eq!(params.options.tab_size, 4);
11613 Ok(Some(vec![lsp::TextEdit::new(
11614 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11615 ", ".to_string(),
11616 )]))
11617 })
11618 .next()
11619 .await;
11620 cx.executor().start_waiting();
11621 save.await;
11622 assert_eq!(
11623 editor.update(cx, |editor, cx| editor.text(cx)),
11624 "one, two\nthree\n"
11625 );
11626 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11627}
11628
11629#[gpui::test]
11630async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) {
11631 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11632
11633 editor.update_in(cx, |editor, window, cx| {
11634 editor.set_text("one\ntwo\nthree\n", window, cx)
11635 });
11636 assert!(cx.read(|cx| editor.is_dirty(cx)));
11637
11638 // Test that save still works when formatting hangs
11639 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
11640 move |params, _| async move {
11641 assert_eq!(
11642 params.text_document.uri,
11643 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11644 );
11645 futures::future::pending::<()>().await;
11646 unreachable!()
11647 },
11648 );
11649 let save = editor
11650 .update_in(cx, |editor, window, cx| {
11651 editor.save(
11652 SaveOptions {
11653 format: true,
11654 autosave: false,
11655 },
11656 project.clone(),
11657 window,
11658 cx,
11659 )
11660 })
11661 .unwrap();
11662 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11663 cx.executor().start_waiting();
11664 save.await;
11665 assert_eq!(
11666 editor.update(cx, |editor, cx| editor.text(cx)),
11667 "one\ntwo\nthree\n"
11668 );
11669 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11670}
11671
11672#[gpui::test]
11673async fn test_range_format_not_called_for_clean_buffer(cx: &mut TestAppContext) {
11674 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11675
11676 // Buffer starts clean, no formatting should be requested
11677 let save = editor
11678 .update_in(cx, |editor, window, cx| {
11679 editor.save(
11680 SaveOptions {
11681 format: false,
11682 autosave: false,
11683 },
11684 project.clone(),
11685 window,
11686 cx,
11687 )
11688 })
11689 .unwrap();
11690 let _pending_format_request = fake_server
11691 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
11692 panic!("Should not be invoked");
11693 })
11694 .next();
11695 cx.executor().start_waiting();
11696 save.await;
11697 cx.run_until_parked();
11698}
11699
11700#[gpui::test]
11701async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppContext) {
11702 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11703
11704 // Set Rust language override and assert overridden tabsize is sent to language server
11705 update_test_language_settings(cx, |settings| {
11706 settings.languages.0.insert(
11707 "Rust".into(),
11708 LanguageSettingsContent {
11709 tab_size: NonZeroU32::new(8),
11710 ..Default::default()
11711 },
11712 );
11713 });
11714
11715 editor.update_in(cx, |editor, window, cx| {
11716 editor.set_text("something_new\n", window, cx)
11717 });
11718 assert!(cx.read(|cx| editor.is_dirty(cx)));
11719 let save = editor
11720 .update_in(cx, |editor, window, cx| {
11721 editor.save(
11722 SaveOptions {
11723 format: true,
11724 autosave: false,
11725 },
11726 project.clone(),
11727 window,
11728 cx,
11729 )
11730 })
11731 .unwrap();
11732 fake_server
11733 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
11734 assert_eq!(
11735 params.text_document.uri,
11736 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11737 );
11738 assert_eq!(params.options.tab_size, 8);
11739 Ok(Some(Vec::new()))
11740 })
11741 .next()
11742 .await;
11743 save.await;
11744}
11745
11746#[gpui::test]
11747async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
11748 init_test(cx, |settings| {
11749 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
11750 Formatter::LanguageServer { name: None },
11751 )))
11752 });
11753
11754 let fs = FakeFs::new(cx.executor());
11755 fs.insert_file(path!("/file.rs"), Default::default()).await;
11756
11757 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11758
11759 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11760 language_registry.add(Arc::new(Language::new(
11761 LanguageConfig {
11762 name: "Rust".into(),
11763 matcher: LanguageMatcher {
11764 path_suffixes: vec!["rs".to_string()],
11765 ..Default::default()
11766 },
11767 ..LanguageConfig::default()
11768 },
11769 Some(tree_sitter_rust::LANGUAGE.into()),
11770 )));
11771 update_test_language_settings(cx, |settings| {
11772 // Enable Prettier formatting for the same buffer, and ensure
11773 // LSP is called instead of Prettier.
11774 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
11775 });
11776 let mut fake_servers = language_registry.register_fake_lsp(
11777 "Rust",
11778 FakeLspAdapter {
11779 capabilities: lsp::ServerCapabilities {
11780 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11781 ..Default::default()
11782 },
11783 ..Default::default()
11784 },
11785 );
11786
11787 let buffer = project
11788 .update(cx, |project, cx| {
11789 project.open_local_buffer(path!("/file.rs"), cx)
11790 })
11791 .await
11792 .unwrap();
11793
11794 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11795 let (editor, cx) = cx.add_window_view(|window, cx| {
11796 build_editor_with_project(project.clone(), buffer, window, cx)
11797 });
11798 editor.update_in(cx, |editor, window, cx| {
11799 editor.set_text("one\ntwo\nthree\n", window, cx)
11800 });
11801
11802 cx.executor().start_waiting();
11803 let fake_server = fake_servers.next().await.unwrap();
11804
11805 let format = editor
11806 .update_in(cx, |editor, window, cx| {
11807 editor.perform_format(
11808 project.clone(),
11809 FormatTrigger::Manual,
11810 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
11811 window,
11812 cx,
11813 )
11814 })
11815 .unwrap();
11816 fake_server
11817 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
11818 assert_eq!(
11819 params.text_document.uri,
11820 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11821 );
11822 assert_eq!(params.options.tab_size, 4);
11823 Ok(Some(vec![lsp::TextEdit::new(
11824 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11825 ", ".to_string(),
11826 )]))
11827 })
11828 .next()
11829 .await;
11830 cx.executor().start_waiting();
11831 format.await;
11832 assert_eq!(
11833 editor.update(cx, |editor, cx| editor.text(cx)),
11834 "one, two\nthree\n"
11835 );
11836
11837 editor.update_in(cx, |editor, window, cx| {
11838 editor.set_text("one\ntwo\nthree\n", window, cx)
11839 });
11840 // Ensure we don't lock if formatting hangs.
11841 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11842 move |params, _| async move {
11843 assert_eq!(
11844 params.text_document.uri,
11845 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11846 );
11847 futures::future::pending::<()>().await;
11848 unreachable!()
11849 },
11850 );
11851 let format = editor
11852 .update_in(cx, |editor, window, cx| {
11853 editor.perform_format(
11854 project,
11855 FormatTrigger::Manual,
11856 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
11857 window,
11858 cx,
11859 )
11860 })
11861 .unwrap();
11862 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11863 cx.executor().start_waiting();
11864 format.await;
11865 assert_eq!(
11866 editor.update(cx, |editor, cx| editor.text(cx)),
11867 "one\ntwo\nthree\n"
11868 );
11869}
11870
11871#[gpui::test]
11872async fn test_multiple_formatters(cx: &mut TestAppContext) {
11873 init_test(cx, |settings| {
11874 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
11875 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
11876 Formatter::LanguageServer { name: None },
11877 Formatter::CodeActions(
11878 [
11879 ("code-action-1".into(), true),
11880 ("code-action-2".into(), true),
11881 ]
11882 .into_iter()
11883 .collect(),
11884 ),
11885 ])))
11886 });
11887
11888 let fs = FakeFs::new(cx.executor());
11889 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
11890 .await;
11891
11892 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11893 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11894 language_registry.add(rust_lang());
11895
11896 let mut fake_servers = language_registry.register_fake_lsp(
11897 "Rust",
11898 FakeLspAdapter {
11899 capabilities: lsp::ServerCapabilities {
11900 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11901 execute_command_provider: Some(lsp::ExecuteCommandOptions {
11902 commands: vec!["the-command-for-code-action-1".into()],
11903 ..Default::default()
11904 }),
11905 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
11906 ..Default::default()
11907 },
11908 ..Default::default()
11909 },
11910 );
11911
11912 let buffer = project
11913 .update(cx, |project, cx| {
11914 project.open_local_buffer(path!("/file.rs"), cx)
11915 })
11916 .await
11917 .unwrap();
11918
11919 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11920 let (editor, cx) = cx.add_window_view(|window, cx| {
11921 build_editor_with_project(project.clone(), buffer, window, cx)
11922 });
11923
11924 cx.executor().start_waiting();
11925
11926 let fake_server = fake_servers.next().await.unwrap();
11927 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11928 move |_params, _| async move {
11929 Ok(Some(vec![lsp::TextEdit::new(
11930 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
11931 "applied-formatting\n".to_string(),
11932 )]))
11933 },
11934 );
11935 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
11936 move |params, _| async move {
11937 let requested_code_actions = params.context.only.expect("Expected code action request");
11938 assert_eq!(requested_code_actions.len(), 1);
11939
11940 let uri = lsp::Uri::from_file_path(path!("/file.rs")).unwrap();
11941 let code_action = match requested_code_actions[0].as_str() {
11942 "code-action-1" => lsp::CodeAction {
11943 kind: Some("code-action-1".into()),
11944 edit: Some(lsp::WorkspaceEdit::new(
11945 [(
11946 uri,
11947 vec![lsp::TextEdit::new(
11948 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
11949 "applied-code-action-1-edit\n".to_string(),
11950 )],
11951 )]
11952 .into_iter()
11953 .collect(),
11954 )),
11955 command: Some(lsp::Command {
11956 command: "the-command-for-code-action-1".into(),
11957 ..Default::default()
11958 }),
11959 ..Default::default()
11960 },
11961 "code-action-2" => lsp::CodeAction {
11962 kind: Some("code-action-2".into()),
11963 edit: Some(lsp::WorkspaceEdit::new(
11964 [(
11965 uri,
11966 vec![lsp::TextEdit::new(
11967 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
11968 "applied-code-action-2-edit\n".to_string(),
11969 )],
11970 )]
11971 .into_iter()
11972 .collect(),
11973 )),
11974 ..Default::default()
11975 },
11976 req => panic!("Unexpected code action request: {:?}", req),
11977 };
11978 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
11979 code_action,
11980 )]))
11981 },
11982 );
11983
11984 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
11985 move |params, _| async move { Ok(params) }
11986 });
11987
11988 let command_lock = Arc::new(futures::lock::Mutex::new(()));
11989 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
11990 let fake = fake_server.clone();
11991 let lock = command_lock.clone();
11992 move |params, _| {
11993 assert_eq!(params.command, "the-command-for-code-action-1");
11994 let fake = fake.clone();
11995 let lock = lock.clone();
11996 async move {
11997 lock.lock().await;
11998 fake.server
11999 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
12000 label: None,
12001 edit: lsp::WorkspaceEdit {
12002 changes: Some(
12003 [(
12004 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
12005 vec![lsp::TextEdit {
12006 range: lsp::Range::new(
12007 lsp::Position::new(0, 0),
12008 lsp::Position::new(0, 0),
12009 ),
12010 new_text: "applied-code-action-1-command\n".into(),
12011 }],
12012 )]
12013 .into_iter()
12014 .collect(),
12015 ),
12016 ..Default::default()
12017 },
12018 })
12019 .await
12020 .into_response()
12021 .unwrap();
12022 Ok(Some(json!(null)))
12023 }
12024 }
12025 });
12026
12027 cx.executor().start_waiting();
12028 editor
12029 .update_in(cx, |editor, window, cx| {
12030 editor.perform_format(
12031 project.clone(),
12032 FormatTrigger::Manual,
12033 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12034 window,
12035 cx,
12036 )
12037 })
12038 .unwrap()
12039 .await;
12040 editor.update(cx, |editor, cx| {
12041 assert_eq!(
12042 editor.text(cx),
12043 r#"
12044 applied-code-action-2-edit
12045 applied-code-action-1-command
12046 applied-code-action-1-edit
12047 applied-formatting
12048 one
12049 two
12050 three
12051 "#
12052 .unindent()
12053 );
12054 });
12055
12056 editor.update_in(cx, |editor, window, cx| {
12057 editor.undo(&Default::default(), window, cx);
12058 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12059 });
12060
12061 // Perform a manual edit while waiting for an LSP command
12062 // that's being run as part of a formatting code action.
12063 let lock_guard = command_lock.lock().await;
12064 let format = editor
12065 .update_in(cx, |editor, window, cx| {
12066 editor.perform_format(
12067 project.clone(),
12068 FormatTrigger::Manual,
12069 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12070 window,
12071 cx,
12072 )
12073 })
12074 .unwrap();
12075 cx.run_until_parked();
12076 editor.update(cx, |editor, cx| {
12077 assert_eq!(
12078 editor.text(cx),
12079 r#"
12080 applied-code-action-1-edit
12081 applied-formatting
12082 one
12083 two
12084 three
12085 "#
12086 .unindent()
12087 );
12088
12089 editor.buffer.update(cx, |buffer, cx| {
12090 let ix = buffer.len(cx);
12091 buffer.edit([(ix..ix, "edited\n")], None, cx);
12092 });
12093 });
12094
12095 // Allow the LSP command to proceed. Because the buffer was edited,
12096 // the second code action will not be run.
12097 drop(lock_guard);
12098 format.await;
12099 editor.update_in(cx, |editor, window, cx| {
12100 assert_eq!(
12101 editor.text(cx),
12102 r#"
12103 applied-code-action-1-command
12104 applied-code-action-1-edit
12105 applied-formatting
12106 one
12107 two
12108 three
12109 edited
12110 "#
12111 .unindent()
12112 );
12113
12114 // The manual edit is undone first, because it is the last thing the user did
12115 // (even though the command completed afterwards).
12116 editor.undo(&Default::default(), window, cx);
12117 assert_eq!(
12118 editor.text(cx),
12119 r#"
12120 applied-code-action-1-command
12121 applied-code-action-1-edit
12122 applied-formatting
12123 one
12124 two
12125 three
12126 "#
12127 .unindent()
12128 );
12129
12130 // All the formatting (including the command, which completed after the manual edit)
12131 // is undone together.
12132 editor.undo(&Default::default(), window, cx);
12133 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12134 });
12135}
12136
12137#[gpui::test]
12138async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
12139 init_test(cx, |settings| {
12140 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
12141 Formatter::LanguageServer { name: None },
12142 ])))
12143 });
12144
12145 let fs = FakeFs::new(cx.executor());
12146 fs.insert_file(path!("/file.ts"), Default::default()).await;
12147
12148 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12149
12150 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12151 language_registry.add(Arc::new(Language::new(
12152 LanguageConfig {
12153 name: "TypeScript".into(),
12154 matcher: LanguageMatcher {
12155 path_suffixes: vec!["ts".to_string()],
12156 ..Default::default()
12157 },
12158 ..LanguageConfig::default()
12159 },
12160 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
12161 )));
12162 update_test_language_settings(cx, |settings| {
12163 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
12164 });
12165 let mut fake_servers = language_registry.register_fake_lsp(
12166 "TypeScript",
12167 FakeLspAdapter {
12168 capabilities: lsp::ServerCapabilities {
12169 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
12170 ..Default::default()
12171 },
12172 ..Default::default()
12173 },
12174 );
12175
12176 let buffer = project
12177 .update(cx, |project, cx| {
12178 project.open_local_buffer(path!("/file.ts"), cx)
12179 })
12180 .await
12181 .unwrap();
12182
12183 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12184 let (editor, cx) = cx.add_window_view(|window, cx| {
12185 build_editor_with_project(project.clone(), buffer, window, cx)
12186 });
12187 editor.update_in(cx, |editor, window, cx| {
12188 editor.set_text(
12189 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12190 window,
12191 cx,
12192 )
12193 });
12194
12195 cx.executor().start_waiting();
12196 let fake_server = fake_servers.next().await.unwrap();
12197
12198 let format = editor
12199 .update_in(cx, |editor, window, cx| {
12200 editor.perform_code_action_kind(
12201 project.clone(),
12202 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12203 window,
12204 cx,
12205 )
12206 })
12207 .unwrap();
12208 fake_server
12209 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
12210 assert_eq!(
12211 params.text_document.uri,
12212 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12213 );
12214 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
12215 lsp::CodeAction {
12216 title: "Organize Imports".to_string(),
12217 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
12218 edit: Some(lsp::WorkspaceEdit {
12219 changes: Some(
12220 [(
12221 params.text_document.uri.clone(),
12222 vec![lsp::TextEdit::new(
12223 lsp::Range::new(
12224 lsp::Position::new(1, 0),
12225 lsp::Position::new(2, 0),
12226 ),
12227 "".to_string(),
12228 )],
12229 )]
12230 .into_iter()
12231 .collect(),
12232 ),
12233 ..Default::default()
12234 }),
12235 ..Default::default()
12236 },
12237 )]))
12238 })
12239 .next()
12240 .await;
12241 cx.executor().start_waiting();
12242 format.await;
12243 assert_eq!(
12244 editor.update(cx, |editor, cx| editor.text(cx)),
12245 "import { a } from 'module';\n\nconst x = a;\n"
12246 );
12247
12248 editor.update_in(cx, |editor, window, cx| {
12249 editor.set_text(
12250 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12251 window,
12252 cx,
12253 )
12254 });
12255 // Ensure we don't lock if code action hangs.
12256 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
12257 move |params, _| async move {
12258 assert_eq!(
12259 params.text_document.uri,
12260 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12261 );
12262 futures::future::pending::<()>().await;
12263 unreachable!()
12264 },
12265 );
12266 let format = editor
12267 .update_in(cx, |editor, window, cx| {
12268 editor.perform_code_action_kind(
12269 project,
12270 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12271 window,
12272 cx,
12273 )
12274 })
12275 .unwrap();
12276 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
12277 cx.executor().start_waiting();
12278 format.await;
12279 assert_eq!(
12280 editor.update(cx, |editor, cx| editor.text(cx)),
12281 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
12282 );
12283}
12284
12285#[gpui::test]
12286async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
12287 init_test(cx, |_| {});
12288
12289 let mut cx = EditorLspTestContext::new_rust(
12290 lsp::ServerCapabilities {
12291 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12292 ..Default::default()
12293 },
12294 cx,
12295 )
12296 .await;
12297
12298 cx.set_state(indoc! {"
12299 one.twoˇ
12300 "});
12301
12302 // The format request takes a long time. When it completes, it inserts
12303 // a newline and an indent before the `.`
12304 cx.lsp
12305 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
12306 let executor = cx.background_executor().clone();
12307 async move {
12308 executor.timer(Duration::from_millis(100)).await;
12309 Ok(Some(vec![lsp::TextEdit {
12310 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
12311 new_text: "\n ".into(),
12312 }]))
12313 }
12314 });
12315
12316 // Submit a format request.
12317 let format_1 = cx
12318 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12319 .unwrap();
12320 cx.executor().run_until_parked();
12321
12322 // Submit a second format request.
12323 let format_2 = cx
12324 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12325 .unwrap();
12326 cx.executor().run_until_parked();
12327
12328 // Wait for both format requests to complete
12329 cx.executor().advance_clock(Duration::from_millis(200));
12330 cx.executor().start_waiting();
12331 format_1.await.unwrap();
12332 cx.executor().start_waiting();
12333 format_2.await.unwrap();
12334
12335 // The formatting edits only happens once.
12336 cx.assert_editor_state(indoc! {"
12337 one
12338 .twoˇ
12339 "});
12340}
12341
12342#[gpui::test]
12343async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
12344 init_test(cx, |settings| {
12345 settings.defaults.formatter = Some(SelectedFormatter::Auto)
12346 });
12347
12348 let mut cx = EditorLspTestContext::new_rust(
12349 lsp::ServerCapabilities {
12350 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12351 ..Default::default()
12352 },
12353 cx,
12354 )
12355 .await;
12356
12357 // Set up a buffer white some trailing whitespace and no trailing newline.
12358 cx.set_state(
12359 &[
12360 "one ", //
12361 "twoˇ", //
12362 "three ", //
12363 "four", //
12364 ]
12365 .join("\n"),
12366 );
12367
12368 // Submit a format request.
12369 let format = cx
12370 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12371 .unwrap();
12372
12373 // Record which buffer changes have been sent to the language server
12374 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
12375 cx.lsp
12376 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
12377 let buffer_changes = buffer_changes.clone();
12378 move |params, _| {
12379 buffer_changes.lock().extend(
12380 params
12381 .content_changes
12382 .into_iter()
12383 .map(|e| (e.range.unwrap(), e.text)),
12384 );
12385 }
12386 });
12387
12388 // Handle formatting requests to the language server.
12389 cx.lsp
12390 .set_request_handler::<lsp::request::Formatting, _, _>({
12391 let buffer_changes = buffer_changes.clone();
12392 move |_, _| {
12393 // When formatting is requested, trailing whitespace has already been stripped,
12394 // and the trailing newline has already been added.
12395 assert_eq!(
12396 &buffer_changes.lock()[1..],
12397 &[
12398 (
12399 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
12400 "".into()
12401 ),
12402 (
12403 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
12404 "".into()
12405 ),
12406 (
12407 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
12408 "\n".into()
12409 ),
12410 ]
12411 );
12412
12413 // Insert blank lines between each line of the buffer.
12414 async move {
12415 Ok(Some(vec![
12416 lsp::TextEdit {
12417 range: lsp::Range::new(
12418 lsp::Position::new(1, 0),
12419 lsp::Position::new(1, 0),
12420 ),
12421 new_text: "\n".into(),
12422 },
12423 lsp::TextEdit {
12424 range: lsp::Range::new(
12425 lsp::Position::new(2, 0),
12426 lsp::Position::new(2, 0),
12427 ),
12428 new_text: "\n".into(),
12429 },
12430 ]))
12431 }
12432 }
12433 });
12434
12435 // After formatting the buffer, the trailing whitespace is stripped,
12436 // a newline is appended, and the edits provided by the language server
12437 // have been applied.
12438 format.await.unwrap();
12439 cx.assert_editor_state(
12440 &[
12441 "one", //
12442 "", //
12443 "twoˇ", //
12444 "", //
12445 "three", //
12446 "four", //
12447 "", //
12448 ]
12449 .join("\n"),
12450 );
12451
12452 // Undoing the formatting undoes the trailing whitespace removal, the
12453 // trailing newline, and the LSP edits.
12454 cx.update_buffer(|buffer, cx| buffer.undo(cx));
12455 cx.assert_editor_state(
12456 &[
12457 "one ", //
12458 "twoˇ", //
12459 "three ", //
12460 "four", //
12461 ]
12462 .join("\n"),
12463 );
12464}
12465
12466#[gpui::test]
12467async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
12468 cx: &mut TestAppContext,
12469) {
12470 init_test(cx, |_| {});
12471
12472 cx.update(|cx| {
12473 cx.update_global::<SettingsStore, _>(|settings, cx| {
12474 settings.update_user_settings(cx, |settings| {
12475 settings.editor.auto_signature_help = Some(true);
12476 });
12477 });
12478 });
12479
12480 let mut cx = EditorLspTestContext::new_rust(
12481 lsp::ServerCapabilities {
12482 signature_help_provider: Some(lsp::SignatureHelpOptions {
12483 ..Default::default()
12484 }),
12485 ..Default::default()
12486 },
12487 cx,
12488 )
12489 .await;
12490
12491 let language = Language::new(
12492 LanguageConfig {
12493 name: "Rust".into(),
12494 brackets: BracketPairConfig {
12495 pairs: vec![
12496 BracketPair {
12497 start: "{".to_string(),
12498 end: "}".to_string(),
12499 close: true,
12500 surround: true,
12501 newline: true,
12502 },
12503 BracketPair {
12504 start: "(".to_string(),
12505 end: ")".to_string(),
12506 close: true,
12507 surround: true,
12508 newline: true,
12509 },
12510 BracketPair {
12511 start: "/*".to_string(),
12512 end: " */".to_string(),
12513 close: true,
12514 surround: true,
12515 newline: true,
12516 },
12517 BracketPair {
12518 start: "[".to_string(),
12519 end: "]".to_string(),
12520 close: false,
12521 surround: false,
12522 newline: true,
12523 },
12524 BracketPair {
12525 start: "\"".to_string(),
12526 end: "\"".to_string(),
12527 close: true,
12528 surround: true,
12529 newline: false,
12530 },
12531 BracketPair {
12532 start: "<".to_string(),
12533 end: ">".to_string(),
12534 close: false,
12535 surround: true,
12536 newline: true,
12537 },
12538 ],
12539 ..Default::default()
12540 },
12541 autoclose_before: "})]".to_string(),
12542 ..Default::default()
12543 },
12544 Some(tree_sitter_rust::LANGUAGE.into()),
12545 );
12546 let language = Arc::new(language);
12547
12548 cx.language_registry().add(language.clone());
12549 cx.update_buffer(|buffer, cx| {
12550 buffer.set_language(Some(language), cx);
12551 });
12552
12553 cx.set_state(
12554 &r#"
12555 fn main() {
12556 sampleˇ
12557 }
12558 "#
12559 .unindent(),
12560 );
12561
12562 cx.update_editor(|editor, window, cx| {
12563 editor.handle_input("(", window, cx);
12564 });
12565 cx.assert_editor_state(
12566 &"
12567 fn main() {
12568 sample(ˇ)
12569 }
12570 "
12571 .unindent(),
12572 );
12573
12574 let mocked_response = lsp::SignatureHelp {
12575 signatures: vec![lsp::SignatureInformation {
12576 label: "fn sample(param1: u8, param2: u8)".to_string(),
12577 documentation: None,
12578 parameters: Some(vec![
12579 lsp::ParameterInformation {
12580 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12581 documentation: None,
12582 },
12583 lsp::ParameterInformation {
12584 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12585 documentation: None,
12586 },
12587 ]),
12588 active_parameter: None,
12589 }],
12590 active_signature: Some(0),
12591 active_parameter: Some(0),
12592 };
12593 handle_signature_help_request(&mut cx, mocked_response).await;
12594
12595 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12596 .await;
12597
12598 cx.editor(|editor, _, _| {
12599 let signature_help_state = editor.signature_help_state.popover().cloned();
12600 let signature = signature_help_state.unwrap();
12601 assert_eq!(
12602 signature.signatures[signature.current_signature].label,
12603 "fn sample(param1: u8, param2: u8)"
12604 );
12605 });
12606}
12607
12608#[gpui::test]
12609async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
12610 init_test(cx, |_| {});
12611
12612 cx.update(|cx| {
12613 cx.update_global::<SettingsStore, _>(|settings, cx| {
12614 settings.update_user_settings(cx, |settings| {
12615 settings.editor.auto_signature_help = Some(false);
12616 settings.editor.show_signature_help_after_edits = Some(false);
12617 });
12618 });
12619 });
12620
12621 let mut cx = EditorLspTestContext::new_rust(
12622 lsp::ServerCapabilities {
12623 signature_help_provider: Some(lsp::SignatureHelpOptions {
12624 ..Default::default()
12625 }),
12626 ..Default::default()
12627 },
12628 cx,
12629 )
12630 .await;
12631
12632 let language = Language::new(
12633 LanguageConfig {
12634 name: "Rust".into(),
12635 brackets: BracketPairConfig {
12636 pairs: vec![
12637 BracketPair {
12638 start: "{".to_string(),
12639 end: "}".to_string(),
12640 close: true,
12641 surround: true,
12642 newline: true,
12643 },
12644 BracketPair {
12645 start: "(".to_string(),
12646 end: ")".to_string(),
12647 close: true,
12648 surround: true,
12649 newline: true,
12650 },
12651 BracketPair {
12652 start: "/*".to_string(),
12653 end: " */".to_string(),
12654 close: true,
12655 surround: true,
12656 newline: true,
12657 },
12658 BracketPair {
12659 start: "[".to_string(),
12660 end: "]".to_string(),
12661 close: false,
12662 surround: false,
12663 newline: true,
12664 },
12665 BracketPair {
12666 start: "\"".to_string(),
12667 end: "\"".to_string(),
12668 close: true,
12669 surround: true,
12670 newline: false,
12671 },
12672 BracketPair {
12673 start: "<".to_string(),
12674 end: ">".to_string(),
12675 close: false,
12676 surround: true,
12677 newline: true,
12678 },
12679 ],
12680 ..Default::default()
12681 },
12682 autoclose_before: "})]".to_string(),
12683 ..Default::default()
12684 },
12685 Some(tree_sitter_rust::LANGUAGE.into()),
12686 );
12687 let language = Arc::new(language);
12688
12689 cx.language_registry().add(language.clone());
12690 cx.update_buffer(|buffer, cx| {
12691 buffer.set_language(Some(language), cx);
12692 });
12693
12694 // Ensure that signature_help is not called when no signature help is enabled.
12695 cx.set_state(
12696 &r#"
12697 fn main() {
12698 sampleˇ
12699 }
12700 "#
12701 .unindent(),
12702 );
12703 cx.update_editor(|editor, window, cx| {
12704 editor.handle_input("(", window, cx);
12705 });
12706 cx.assert_editor_state(
12707 &"
12708 fn main() {
12709 sample(ˇ)
12710 }
12711 "
12712 .unindent(),
12713 );
12714 cx.editor(|editor, _, _| {
12715 assert!(editor.signature_help_state.task().is_none());
12716 });
12717
12718 let mocked_response = lsp::SignatureHelp {
12719 signatures: vec![lsp::SignatureInformation {
12720 label: "fn sample(param1: u8, param2: u8)".to_string(),
12721 documentation: None,
12722 parameters: Some(vec![
12723 lsp::ParameterInformation {
12724 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12725 documentation: None,
12726 },
12727 lsp::ParameterInformation {
12728 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12729 documentation: None,
12730 },
12731 ]),
12732 active_parameter: None,
12733 }],
12734 active_signature: Some(0),
12735 active_parameter: Some(0),
12736 };
12737
12738 // Ensure that signature_help is called when enabled afte edits
12739 cx.update(|_, cx| {
12740 cx.update_global::<SettingsStore, _>(|settings, cx| {
12741 settings.update_user_settings(cx, |settings| {
12742 settings.editor.auto_signature_help = Some(false);
12743 settings.editor.show_signature_help_after_edits = Some(true);
12744 });
12745 });
12746 });
12747 cx.set_state(
12748 &r#"
12749 fn main() {
12750 sampleˇ
12751 }
12752 "#
12753 .unindent(),
12754 );
12755 cx.update_editor(|editor, window, cx| {
12756 editor.handle_input("(", window, cx);
12757 });
12758 cx.assert_editor_state(
12759 &"
12760 fn main() {
12761 sample(ˇ)
12762 }
12763 "
12764 .unindent(),
12765 );
12766 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12767 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12768 .await;
12769 cx.update_editor(|editor, _, _| {
12770 let signature_help_state = editor.signature_help_state.popover().cloned();
12771 assert!(signature_help_state.is_some());
12772 let signature = signature_help_state.unwrap();
12773 assert_eq!(
12774 signature.signatures[signature.current_signature].label,
12775 "fn sample(param1: u8, param2: u8)"
12776 );
12777 editor.signature_help_state = SignatureHelpState::default();
12778 });
12779
12780 // Ensure that signature_help is called when auto signature help override is enabled
12781 cx.update(|_, cx| {
12782 cx.update_global::<SettingsStore, _>(|settings, cx| {
12783 settings.update_user_settings(cx, |settings| {
12784 settings.editor.auto_signature_help = Some(true);
12785 settings.editor.show_signature_help_after_edits = Some(false);
12786 });
12787 });
12788 });
12789 cx.set_state(
12790 &r#"
12791 fn main() {
12792 sampleˇ
12793 }
12794 "#
12795 .unindent(),
12796 );
12797 cx.update_editor(|editor, window, cx| {
12798 editor.handle_input("(", window, cx);
12799 });
12800 cx.assert_editor_state(
12801 &"
12802 fn main() {
12803 sample(ˇ)
12804 }
12805 "
12806 .unindent(),
12807 );
12808 handle_signature_help_request(&mut cx, mocked_response).await;
12809 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12810 .await;
12811 cx.editor(|editor, _, _| {
12812 let signature_help_state = editor.signature_help_state.popover().cloned();
12813 assert!(signature_help_state.is_some());
12814 let signature = signature_help_state.unwrap();
12815 assert_eq!(
12816 signature.signatures[signature.current_signature].label,
12817 "fn sample(param1: u8, param2: u8)"
12818 );
12819 });
12820}
12821
12822#[gpui::test]
12823async fn test_signature_help(cx: &mut TestAppContext) {
12824 init_test(cx, |_| {});
12825 cx.update(|cx| {
12826 cx.update_global::<SettingsStore, _>(|settings, cx| {
12827 settings.update_user_settings(cx, |settings| {
12828 settings.editor.auto_signature_help = Some(true);
12829 });
12830 });
12831 });
12832
12833 let mut cx = EditorLspTestContext::new_rust(
12834 lsp::ServerCapabilities {
12835 signature_help_provider: Some(lsp::SignatureHelpOptions {
12836 ..Default::default()
12837 }),
12838 ..Default::default()
12839 },
12840 cx,
12841 )
12842 .await;
12843
12844 // A test that directly calls `show_signature_help`
12845 cx.update_editor(|editor, window, cx| {
12846 editor.show_signature_help(&ShowSignatureHelp, window, cx);
12847 });
12848
12849 let mocked_response = lsp::SignatureHelp {
12850 signatures: vec![lsp::SignatureInformation {
12851 label: "fn sample(param1: u8, param2: u8)".to_string(),
12852 documentation: None,
12853 parameters: Some(vec![
12854 lsp::ParameterInformation {
12855 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12856 documentation: None,
12857 },
12858 lsp::ParameterInformation {
12859 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12860 documentation: None,
12861 },
12862 ]),
12863 active_parameter: None,
12864 }],
12865 active_signature: Some(0),
12866 active_parameter: Some(0),
12867 };
12868 handle_signature_help_request(&mut cx, mocked_response).await;
12869
12870 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12871 .await;
12872
12873 cx.editor(|editor, _, _| {
12874 let signature_help_state = editor.signature_help_state.popover().cloned();
12875 assert!(signature_help_state.is_some());
12876 let signature = signature_help_state.unwrap();
12877 assert_eq!(
12878 signature.signatures[signature.current_signature].label,
12879 "fn sample(param1: u8, param2: u8)"
12880 );
12881 });
12882
12883 // When exiting outside from inside the brackets, `signature_help` is closed.
12884 cx.set_state(indoc! {"
12885 fn main() {
12886 sample(ˇ);
12887 }
12888
12889 fn sample(param1: u8, param2: u8) {}
12890 "});
12891
12892 cx.update_editor(|editor, window, cx| {
12893 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12894 s.select_ranges([0..0])
12895 });
12896 });
12897
12898 let mocked_response = lsp::SignatureHelp {
12899 signatures: Vec::new(),
12900 active_signature: None,
12901 active_parameter: None,
12902 };
12903 handle_signature_help_request(&mut cx, mocked_response).await;
12904
12905 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
12906 .await;
12907
12908 cx.editor(|editor, _, _| {
12909 assert!(!editor.signature_help_state.is_shown());
12910 });
12911
12912 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
12913 cx.set_state(indoc! {"
12914 fn main() {
12915 sample(ˇ);
12916 }
12917
12918 fn sample(param1: u8, param2: u8) {}
12919 "});
12920
12921 let mocked_response = lsp::SignatureHelp {
12922 signatures: vec![lsp::SignatureInformation {
12923 label: "fn sample(param1: u8, param2: u8)".to_string(),
12924 documentation: None,
12925 parameters: Some(vec![
12926 lsp::ParameterInformation {
12927 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12928 documentation: None,
12929 },
12930 lsp::ParameterInformation {
12931 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12932 documentation: None,
12933 },
12934 ]),
12935 active_parameter: None,
12936 }],
12937 active_signature: Some(0),
12938 active_parameter: Some(0),
12939 };
12940 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12941 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12942 .await;
12943 cx.editor(|editor, _, _| {
12944 assert!(editor.signature_help_state.is_shown());
12945 });
12946
12947 // Restore the popover with more parameter input
12948 cx.set_state(indoc! {"
12949 fn main() {
12950 sample(param1, param2ˇ);
12951 }
12952
12953 fn sample(param1: u8, param2: u8) {}
12954 "});
12955
12956 let mocked_response = lsp::SignatureHelp {
12957 signatures: vec![lsp::SignatureInformation {
12958 label: "fn sample(param1: u8, param2: u8)".to_string(),
12959 documentation: None,
12960 parameters: Some(vec![
12961 lsp::ParameterInformation {
12962 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12963 documentation: None,
12964 },
12965 lsp::ParameterInformation {
12966 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12967 documentation: None,
12968 },
12969 ]),
12970 active_parameter: None,
12971 }],
12972 active_signature: Some(0),
12973 active_parameter: Some(1),
12974 };
12975 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12976 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12977 .await;
12978
12979 // When selecting a range, the popover is gone.
12980 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
12981 cx.update_editor(|editor, window, cx| {
12982 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12983 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
12984 })
12985 });
12986 cx.assert_editor_state(indoc! {"
12987 fn main() {
12988 sample(param1, «ˇparam2»);
12989 }
12990
12991 fn sample(param1: u8, param2: u8) {}
12992 "});
12993 cx.editor(|editor, _, _| {
12994 assert!(!editor.signature_help_state.is_shown());
12995 });
12996
12997 // When unselecting again, the popover is back if within the brackets.
12998 cx.update_editor(|editor, window, cx| {
12999 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13000 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13001 })
13002 });
13003 cx.assert_editor_state(indoc! {"
13004 fn main() {
13005 sample(param1, ˇparam2);
13006 }
13007
13008 fn sample(param1: u8, param2: u8) {}
13009 "});
13010 handle_signature_help_request(&mut cx, mocked_response).await;
13011 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13012 .await;
13013 cx.editor(|editor, _, _| {
13014 assert!(editor.signature_help_state.is_shown());
13015 });
13016
13017 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
13018 cx.update_editor(|editor, window, cx| {
13019 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13020 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
13021 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13022 })
13023 });
13024 cx.assert_editor_state(indoc! {"
13025 fn main() {
13026 sample(param1, ˇparam2);
13027 }
13028
13029 fn sample(param1: u8, param2: u8) {}
13030 "});
13031
13032 let mocked_response = lsp::SignatureHelp {
13033 signatures: vec![lsp::SignatureInformation {
13034 label: "fn sample(param1: u8, param2: u8)".to_string(),
13035 documentation: None,
13036 parameters: Some(vec![
13037 lsp::ParameterInformation {
13038 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13039 documentation: None,
13040 },
13041 lsp::ParameterInformation {
13042 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13043 documentation: None,
13044 },
13045 ]),
13046 active_parameter: None,
13047 }],
13048 active_signature: Some(0),
13049 active_parameter: Some(1),
13050 };
13051 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13052 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13053 .await;
13054 cx.update_editor(|editor, _, cx| {
13055 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
13056 });
13057 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13058 .await;
13059 cx.update_editor(|editor, window, cx| {
13060 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13061 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13062 })
13063 });
13064 cx.assert_editor_state(indoc! {"
13065 fn main() {
13066 sample(param1, «ˇparam2»);
13067 }
13068
13069 fn sample(param1: u8, param2: u8) {}
13070 "});
13071 cx.update_editor(|editor, window, cx| {
13072 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13073 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13074 })
13075 });
13076 cx.assert_editor_state(indoc! {"
13077 fn main() {
13078 sample(param1, ˇparam2);
13079 }
13080
13081 fn sample(param1: u8, param2: u8) {}
13082 "});
13083 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
13084 .await;
13085}
13086
13087#[gpui::test]
13088async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
13089 init_test(cx, |_| {});
13090
13091 let mut cx = EditorLspTestContext::new_rust(
13092 lsp::ServerCapabilities {
13093 signature_help_provider: Some(lsp::SignatureHelpOptions {
13094 ..Default::default()
13095 }),
13096 ..Default::default()
13097 },
13098 cx,
13099 )
13100 .await;
13101
13102 cx.set_state(indoc! {"
13103 fn main() {
13104 overloadedˇ
13105 }
13106 "});
13107
13108 cx.update_editor(|editor, window, cx| {
13109 editor.handle_input("(", window, cx);
13110 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13111 });
13112
13113 // Mock response with 3 signatures
13114 let mocked_response = lsp::SignatureHelp {
13115 signatures: vec![
13116 lsp::SignatureInformation {
13117 label: "fn overloaded(x: i32)".to_string(),
13118 documentation: None,
13119 parameters: Some(vec![lsp::ParameterInformation {
13120 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13121 documentation: None,
13122 }]),
13123 active_parameter: None,
13124 },
13125 lsp::SignatureInformation {
13126 label: "fn overloaded(x: i32, y: i32)".to_string(),
13127 documentation: None,
13128 parameters: Some(vec![
13129 lsp::ParameterInformation {
13130 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13131 documentation: None,
13132 },
13133 lsp::ParameterInformation {
13134 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13135 documentation: None,
13136 },
13137 ]),
13138 active_parameter: None,
13139 },
13140 lsp::SignatureInformation {
13141 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
13142 documentation: None,
13143 parameters: Some(vec![
13144 lsp::ParameterInformation {
13145 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13146 documentation: None,
13147 },
13148 lsp::ParameterInformation {
13149 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13150 documentation: None,
13151 },
13152 lsp::ParameterInformation {
13153 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
13154 documentation: None,
13155 },
13156 ]),
13157 active_parameter: None,
13158 },
13159 ],
13160 active_signature: Some(1),
13161 active_parameter: Some(0),
13162 };
13163 handle_signature_help_request(&mut cx, mocked_response).await;
13164
13165 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13166 .await;
13167
13168 // Verify we have multiple signatures and the right one is selected
13169 cx.editor(|editor, _, _| {
13170 let popover = editor.signature_help_state.popover().cloned().unwrap();
13171 assert_eq!(popover.signatures.len(), 3);
13172 // active_signature was 1, so that should be the current
13173 assert_eq!(popover.current_signature, 1);
13174 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
13175 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
13176 assert_eq!(
13177 popover.signatures[2].label,
13178 "fn overloaded(x: i32, y: i32, z: i32)"
13179 );
13180 });
13181
13182 // Test navigation functionality
13183 cx.update_editor(|editor, window, cx| {
13184 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13185 });
13186
13187 cx.editor(|editor, _, _| {
13188 let popover = editor.signature_help_state.popover().cloned().unwrap();
13189 assert_eq!(popover.current_signature, 2);
13190 });
13191
13192 // Test wrap around
13193 cx.update_editor(|editor, window, cx| {
13194 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13195 });
13196
13197 cx.editor(|editor, _, _| {
13198 let popover = editor.signature_help_state.popover().cloned().unwrap();
13199 assert_eq!(popover.current_signature, 0);
13200 });
13201
13202 // Test previous navigation
13203 cx.update_editor(|editor, window, cx| {
13204 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
13205 });
13206
13207 cx.editor(|editor, _, _| {
13208 let popover = editor.signature_help_state.popover().cloned().unwrap();
13209 assert_eq!(popover.current_signature, 2);
13210 });
13211}
13212
13213#[gpui::test]
13214async fn test_completion_mode(cx: &mut TestAppContext) {
13215 init_test(cx, |_| {});
13216 let mut cx = EditorLspTestContext::new_rust(
13217 lsp::ServerCapabilities {
13218 completion_provider: Some(lsp::CompletionOptions {
13219 resolve_provider: Some(true),
13220 ..Default::default()
13221 }),
13222 ..Default::default()
13223 },
13224 cx,
13225 )
13226 .await;
13227
13228 struct Run {
13229 run_description: &'static str,
13230 initial_state: String,
13231 buffer_marked_text: String,
13232 completion_label: &'static str,
13233 completion_text: &'static str,
13234 expected_with_insert_mode: String,
13235 expected_with_replace_mode: String,
13236 expected_with_replace_subsequence_mode: String,
13237 expected_with_replace_suffix_mode: String,
13238 }
13239
13240 let runs = [
13241 Run {
13242 run_description: "Start of word matches completion text",
13243 initial_state: "before ediˇ after".into(),
13244 buffer_marked_text: "before <edi|> after".into(),
13245 completion_label: "editor",
13246 completion_text: "editor",
13247 expected_with_insert_mode: "before editorˇ after".into(),
13248 expected_with_replace_mode: "before editorˇ after".into(),
13249 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13250 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13251 },
13252 Run {
13253 run_description: "Accept same text at the middle of the word",
13254 initial_state: "before ediˇtor after".into(),
13255 buffer_marked_text: "before <edi|tor> after".into(),
13256 completion_label: "editor",
13257 completion_text: "editor",
13258 expected_with_insert_mode: "before editorˇtor after".into(),
13259 expected_with_replace_mode: "before editorˇ after".into(),
13260 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13261 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13262 },
13263 Run {
13264 run_description: "End of word matches completion text -- cursor at end",
13265 initial_state: "before torˇ after".into(),
13266 buffer_marked_text: "before <tor|> after".into(),
13267 completion_label: "editor",
13268 completion_text: "editor",
13269 expected_with_insert_mode: "before editorˇ after".into(),
13270 expected_with_replace_mode: "before editorˇ after".into(),
13271 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13272 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13273 },
13274 Run {
13275 run_description: "End of word matches completion text -- cursor at start",
13276 initial_state: "before ˇtor after".into(),
13277 buffer_marked_text: "before <|tor> after".into(),
13278 completion_label: "editor",
13279 completion_text: "editor",
13280 expected_with_insert_mode: "before editorˇtor after".into(),
13281 expected_with_replace_mode: "before editorˇ after".into(),
13282 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13283 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13284 },
13285 Run {
13286 run_description: "Prepend text containing whitespace",
13287 initial_state: "pˇfield: bool".into(),
13288 buffer_marked_text: "<p|field>: bool".into(),
13289 completion_label: "pub ",
13290 completion_text: "pub ",
13291 expected_with_insert_mode: "pub ˇfield: bool".into(),
13292 expected_with_replace_mode: "pub ˇ: bool".into(),
13293 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
13294 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
13295 },
13296 Run {
13297 run_description: "Add element to start of list",
13298 initial_state: "[element_ˇelement_2]".into(),
13299 buffer_marked_text: "[<element_|element_2>]".into(),
13300 completion_label: "element_1",
13301 completion_text: "element_1",
13302 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
13303 expected_with_replace_mode: "[element_1ˇ]".into(),
13304 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
13305 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
13306 },
13307 Run {
13308 run_description: "Add element to start of list -- first and second elements are equal",
13309 initial_state: "[elˇelement]".into(),
13310 buffer_marked_text: "[<el|element>]".into(),
13311 completion_label: "element",
13312 completion_text: "element",
13313 expected_with_insert_mode: "[elementˇelement]".into(),
13314 expected_with_replace_mode: "[elementˇ]".into(),
13315 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
13316 expected_with_replace_suffix_mode: "[elementˇ]".into(),
13317 },
13318 Run {
13319 run_description: "Ends with matching suffix",
13320 initial_state: "SubˇError".into(),
13321 buffer_marked_text: "<Sub|Error>".into(),
13322 completion_label: "SubscriptionError",
13323 completion_text: "SubscriptionError",
13324 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
13325 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13326 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13327 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
13328 },
13329 Run {
13330 run_description: "Suffix is a subsequence -- contiguous",
13331 initial_state: "SubˇErr".into(),
13332 buffer_marked_text: "<Sub|Err>".into(),
13333 completion_label: "SubscriptionError",
13334 completion_text: "SubscriptionError",
13335 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
13336 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13337 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13338 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
13339 },
13340 Run {
13341 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
13342 initial_state: "Suˇscrirr".into(),
13343 buffer_marked_text: "<Su|scrirr>".into(),
13344 completion_label: "SubscriptionError",
13345 completion_text: "SubscriptionError",
13346 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
13347 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13348 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13349 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
13350 },
13351 Run {
13352 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
13353 initial_state: "foo(indˇix)".into(),
13354 buffer_marked_text: "foo(<ind|ix>)".into(),
13355 completion_label: "node_index",
13356 completion_text: "node_index",
13357 expected_with_insert_mode: "foo(node_indexˇix)".into(),
13358 expected_with_replace_mode: "foo(node_indexˇ)".into(),
13359 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
13360 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
13361 },
13362 Run {
13363 run_description: "Replace range ends before cursor - should extend to cursor",
13364 initial_state: "before editˇo after".into(),
13365 buffer_marked_text: "before <{ed}>it|o after".into(),
13366 completion_label: "editor",
13367 completion_text: "editor",
13368 expected_with_insert_mode: "before editorˇo after".into(),
13369 expected_with_replace_mode: "before editorˇo after".into(),
13370 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
13371 expected_with_replace_suffix_mode: "before editorˇo after".into(),
13372 },
13373 Run {
13374 run_description: "Uses label for suffix matching",
13375 initial_state: "before ediˇtor after".into(),
13376 buffer_marked_text: "before <edi|tor> after".into(),
13377 completion_label: "editor",
13378 completion_text: "editor()",
13379 expected_with_insert_mode: "before editor()ˇtor after".into(),
13380 expected_with_replace_mode: "before editor()ˇ after".into(),
13381 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
13382 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
13383 },
13384 Run {
13385 run_description: "Case insensitive subsequence and suffix matching",
13386 initial_state: "before EDiˇtoR after".into(),
13387 buffer_marked_text: "before <EDi|toR> after".into(),
13388 completion_label: "editor",
13389 completion_text: "editor",
13390 expected_with_insert_mode: "before editorˇtoR after".into(),
13391 expected_with_replace_mode: "before editorˇ after".into(),
13392 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13393 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13394 },
13395 ];
13396
13397 for run in runs {
13398 let run_variations = [
13399 (LspInsertMode::Insert, run.expected_with_insert_mode),
13400 (LspInsertMode::Replace, run.expected_with_replace_mode),
13401 (
13402 LspInsertMode::ReplaceSubsequence,
13403 run.expected_with_replace_subsequence_mode,
13404 ),
13405 (
13406 LspInsertMode::ReplaceSuffix,
13407 run.expected_with_replace_suffix_mode,
13408 ),
13409 ];
13410
13411 for (lsp_insert_mode, expected_text) in run_variations {
13412 eprintln!(
13413 "run = {:?}, mode = {lsp_insert_mode:.?}",
13414 run.run_description,
13415 );
13416
13417 update_test_language_settings(&mut cx, |settings| {
13418 settings.defaults.completions = Some(CompletionSettingsContent {
13419 lsp_insert_mode: Some(lsp_insert_mode),
13420 words: Some(WordsCompletionMode::Disabled),
13421 words_min_length: Some(0),
13422 ..Default::default()
13423 });
13424 });
13425
13426 cx.set_state(&run.initial_state);
13427 cx.update_editor(|editor, window, cx| {
13428 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13429 });
13430
13431 let counter = Arc::new(AtomicUsize::new(0));
13432 handle_completion_request_with_insert_and_replace(
13433 &mut cx,
13434 &run.buffer_marked_text,
13435 vec![(run.completion_label, run.completion_text)],
13436 counter.clone(),
13437 )
13438 .await;
13439 cx.condition(|editor, _| editor.context_menu_visible())
13440 .await;
13441 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13442
13443 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13444 editor
13445 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13446 .unwrap()
13447 });
13448 cx.assert_editor_state(&expected_text);
13449 handle_resolve_completion_request(&mut cx, None).await;
13450 apply_additional_edits.await.unwrap();
13451 }
13452 }
13453}
13454
13455#[gpui::test]
13456async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
13457 init_test(cx, |_| {});
13458 let mut cx = EditorLspTestContext::new_rust(
13459 lsp::ServerCapabilities {
13460 completion_provider: Some(lsp::CompletionOptions {
13461 resolve_provider: Some(true),
13462 ..Default::default()
13463 }),
13464 ..Default::default()
13465 },
13466 cx,
13467 )
13468 .await;
13469
13470 let initial_state = "SubˇError";
13471 let buffer_marked_text = "<Sub|Error>";
13472 let completion_text = "SubscriptionError";
13473 let expected_with_insert_mode = "SubscriptionErrorˇError";
13474 let expected_with_replace_mode = "SubscriptionErrorˇ";
13475
13476 update_test_language_settings(&mut cx, |settings| {
13477 settings.defaults.completions = Some(CompletionSettingsContent {
13478 words: Some(WordsCompletionMode::Disabled),
13479 words_min_length: Some(0),
13480 // set the opposite here to ensure that the action is overriding the default behavior
13481 lsp_insert_mode: Some(LspInsertMode::Insert),
13482 ..Default::default()
13483 });
13484 });
13485
13486 cx.set_state(initial_state);
13487 cx.update_editor(|editor, window, cx| {
13488 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13489 });
13490
13491 let counter = Arc::new(AtomicUsize::new(0));
13492 handle_completion_request_with_insert_and_replace(
13493 &mut cx,
13494 buffer_marked_text,
13495 vec![(completion_text, completion_text)],
13496 counter.clone(),
13497 )
13498 .await;
13499 cx.condition(|editor, _| editor.context_menu_visible())
13500 .await;
13501 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13502
13503 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13504 editor
13505 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13506 .unwrap()
13507 });
13508 cx.assert_editor_state(expected_with_replace_mode);
13509 handle_resolve_completion_request(&mut cx, None).await;
13510 apply_additional_edits.await.unwrap();
13511
13512 update_test_language_settings(&mut cx, |settings| {
13513 settings.defaults.completions = Some(CompletionSettingsContent {
13514 words: Some(WordsCompletionMode::Disabled),
13515 words_min_length: Some(0),
13516 // set the opposite here to ensure that the action is overriding the default behavior
13517 lsp_insert_mode: Some(LspInsertMode::Replace),
13518 ..Default::default()
13519 });
13520 });
13521
13522 cx.set_state(initial_state);
13523 cx.update_editor(|editor, window, cx| {
13524 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13525 });
13526 handle_completion_request_with_insert_and_replace(
13527 &mut cx,
13528 buffer_marked_text,
13529 vec![(completion_text, completion_text)],
13530 counter.clone(),
13531 )
13532 .await;
13533 cx.condition(|editor, _| editor.context_menu_visible())
13534 .await;
13535 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
13536
13537 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13538 editor
13539 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
13540 .unwrap()
13541 });
13542 cx.assert_editor_state(expected_with_insert_mode);
13543 handle_resolve_completion_request(&mut cx, None).await;
13544 apply_additional_edits.await.unwrap();
13545}
13546
13547#[gpui::test]
13548async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
13549 init_test(cx, |_| {});
13550 let mut cx = EditorLspTestContext::new_rust(
13551 lsp::ServerCapabilities {
13552 completion_provider: Some(lsp::CompletionOptions {
13553 resolve_provider: Some(true),
13554 ..Default::default()
13555 }),
13556 ..Default::default()
13557 },
13558 cx,
13559 )
13560 .await;
13561
13562 // scenario: surrounding text matches completion text
13563 let completion_text = "to_offset";
13564 let initial_state = indoc! {"
13565 1. buf.to_offˇsuffix
13566 2. buf.to_offˇsuf
13567 3. buf.to_offˇfix
13568 4. buf.to_offˇ
13569 5. into_offˇensive
13570 6. ˇsuffix
13571 7. let ˇ //
13572 8. aaˇzz
13573 9. buf.to_off«zzzzzˇ»suffix
13574 10. buf.«ˇzzzzz»suffix
13575 11. to_off«ˇzzzzz»
13576
13577 buf.to_offˇsuffix // newest cursor
13578 "};
13579 let completion_marked_buffer = indoc! {"
13580 1. buf.to_offsuffix
13581 2. buf.to_offsuf
13582 3. buf.to_offfix
13583 4. buf.to_off
13584 5. into_offensive
13585 6. suffix
13586 7. let //
13587 8. aazz
13588 9. buf.to_offzzzzzsuffix
13589 10. buf.zzzzzsuffix
13590 11. to_offzzzzz
13591
13592 buf.<to_off|suffix> // newest cursor
13593 "};
13594 let expected = indoc! {"
13595 1. buf.to_offsetˇ
13596 2. buf.to_offsetˇsuf
13597 3. buf.to_offsetˇfix
13598 4. buf.to_offsetˇ
13599 5. into_offsetˇensive
13600 6. to_offsetˇsuffix
13601 7. let to_offsetˇ //
13602 8. aato_offsetˇzz
13603 9. buf.to_offsetˇ
13604 10. buf.to_offsetˇsuffix
13605 11. to_offsetˇ
13606
13607 buf.to_offsetˇ // newest cursor
13608 "};
13609 cx.set_state(initial_state);
13610 cx.update_editor(|editor, window, cx| {
13611 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13612 });
13613 handle_completion_request_with_insert_and_replace(
13614 &mut cx,
13615 completion_marked_buffer,
13616 vec![(completion_text, completion_text)],
13617 Arc::new(AtomicUsize::new(0)),
13618 )
13619 .await;
13620 cx.condition(|editor, _| editor.context_menu_visible())
13621 .await;
13622 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13623 editor
13624 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13625 .unwrap()
13626 });
13627 cx.assert_editor_state(expected);
13628 handle_resolve_completion_request(&mut cx, None).await;
13629 apply_additional_edits.await.unwrap();
13630
13631 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
13632 let completion_text = "foo_and_bar";
13633 let initial_state = indoc! {"
13634 1. ooanbˇ
13635 2. zooanbˇ
13636 3. ooanbˇz
13637 4. zooanbˇz
13638 5. ooanˇ
13639 6. oanbˇ
13640
13641 ooanbˇ
13642 "};
13643 let completion_marked_buffer = indoc! {"
13644 1. ooanb
13645 2. zooanb
13646 3. ooanbz
13647 4. zooanbz
13648 5. ooan
13649 6. oanb
13650
13651 <ooanb|>
13652 "};
13653 let expected = indoc! {"
13654 1. foo_and_barˇ
13655 2. zfoo_and_barˇ
13656 3. foo_and_barˇz
13657 4. zfoo_and_barˇz
13658 5. ooanfoo_and_barˇ
13659 6. oanbfoo_and_barˇ
13660
13661 foo_and_barˇ
13662 "};
13663 cx.set_state(initial_state);
13664 cx.update_editor(|editor, window, cx| {
13665 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13666 });
13667 handle_completion_request_with_insert_and_replace(
13668 &mut cx,
13669 completion_marked_buffer,
13670 vec![(completion_text, completion_text)],
13671 Arc::new(AtomicUsize::new(0)),
13672 )
13673 .await;
13674 cx.condition(|editor, _| editor.context_menu_visible())
13675 .await;
13676 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13677 editor
13678 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13679 .unwrap()
13680 });
13681 cx.assert_editor_state(expected);
13682 handle_resolve_completion_request(&mut cx, None).await;
13683 apply_additional_edits.await.unwrap();
13684
13685 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
13686 // (expects the same as if it was inserted at the end)
13687 let completion_text = "foo_and_bar";
13688 let initial_state = indoc! {"
13689 1. ooˇanb
13690 2. zooˇanb
13691 3. ooˇanbz
13692 4. zooˇanbz
13693
13694 ooˇanb
13695 "};
13696 let completion_marked_buffer = indoc! {"
13697 1. ooanb
13698 2. zooanb
13699 3. ooanbz
13700 4. zooanbz
13701
13702 <oo|anb>
13703 "};
13704 let expected = indoc! {"
13705 1. foo_and_barˇ
13706 2. zfoo_and_barˇ
13707 3. foo_and_barˇz
13708 4. zfoo_and_barˇz
13709
13710 foo_and_barˇ
13711 "};
13712 cx.set_state(initial_state);
13713 cx.update_editor(|editor, window, cx| {
13714 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13715 });
13716 handle_completion_request_with_insert_and_replace(
13717 &mut cx,
13718 completion_marked_buffer,
13719 vec![(completion_text, completion_text)],
13720 Arc::new(AtomicUsize::new(0)),
13721 )
13722 .await;
13723 cx.condition(|editor, _| editor.context_menu_visible())
13724 .await;
13725 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13726 editor
13727 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13728 .unwrap()
13729 });
13730 cx.assert_editor_state(expected);
13731 handle_resolve_completion_request(&mut cx, None).await;
13732 apply_additional_edits.await.unwrap();
13733}
13734
13735// This used to crash
13736#[gpui::test]
13737async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
13738 init_test(cx, |_| {});
13739
13740 let buffer_text = indoc! {"
13741 fn main() {
13742 10.satu;
13743
13744 //
13745 // separate cursors so they open in different excerpts (manually reproducible)
13746 //
13747
13748 10.satu20;
13749 }
13750 "};
13751 let multibuffer_text_with_selections = indoc! {"
13752 fn main() {
13753 10.satuˇ;
13754
13755 //
13756
13757 //
13758
13759 10.satuˇ20;
13760 }
13761 "};
13762 let expected_multibuffer = indoc! {"
13763 fn main() {
13764 10.saturating_sub()ˇ;
13765
13766 //
13767
13768 //
13769
13770 10.saturating_sub()ˇ;
13771 }
13772 "};
13773
13774 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
13775 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
13776
13777 let fs = FakeFs::new(cx.executor());
13778 fs.insert_tree(
13779 path!("/a"),
13780 json!({
13781 "main.rs": buffer_text,
13782 }),
13783 )
13784 .await;
13785
13786 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13787 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13788 language_registry.add(rust_lang());
13789 let mut fake_servers = language_registry.register_fake_lsp(
13790 "Rust",
13791 FakeLspAdapter {
13792 capabilities: lsp::ServerCapabilities {
13793 completion_provider: Some(lsp::CompletionOptions {
13794 resolve_provider: None,
13795 ..lsp::CompletionOptions::default()
13796 }),
13797 ..lsp::ServerCapabilities::default()
13798 },
13799 ..FakeLspAdapter::default()
13800 },
13801 );
13802 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13803 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13804 let buffer = project
13805 .update(cx, |project, cx| {
13806 project.open_local_buffer(path!("/a/main.rs"), cx)
13807 })
13808 .await
13809 .unwrap();
13810
13811 let multi_buffer = cx.new(|cx| {
13812 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
13813 multi_buffer.push_excerpts(
13814 buffer.clone(),
13815 [ExcerptRange::new(0..first_excerpt_end)],
13816 cx,
13817 );
13818 multi_buffer.push_excerpts(
13819 buffer.clone(),
13820 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
13821 cx,
13822 );
13823 multi_buffer
13824 });
13825
13826 let editor = workspace
13827 .update(cx, |_, window, cx| {
13828 cx.new(|cx| {
13829 Editor::new(
13830 EditorMode::Full {
13831 scale_ui_elements_with_buffer_font_size: false,
13832 show_active_line_background: false,
13833 sized_by_content: false,
13834 },
13835 multi_buffer.clone(),
13836 Some(project.clone()),
13837 window,
13838 cx,
13839 )
13840 })
13841 })
13842 .unwrap();
13843
13844 let pane = workspace
13845 .update(cx, |workspace, _, _| workspace.active_pane().clone())
13846 .unwrap();
13847 pane.update_in(cx, |pane, window, cx| {
13848 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
13849 });
13850
13851 let fake_server = fake_servers.next().await.unwrap();
13852
13853 editor.update_in(cx, |editor, window, cx| {
13854 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13855 s.select_ranges([
13856 Point::new(1, 11)..Point::new(1, 11),
13857 Point::new(7, 11)..Point::new(7, 11),
13858 ])
13859 });
13860
13861 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
13862 });
13863
13864 editor.update_in(cx, |editor, window, cx| {
13865 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13866 });
13867
13868 fake_server
13869 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13870 let completion_item = lsp::CompletionItem {
13871 label: "saturating_sub()".into(),
13872 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
13873 lsp::InsertReplaceEdit {
13874 new_text: "saturating_sub()".to_owned(),
13875 insert: lsp::Range::new(
13876 lsp::Position::new(7, 7),
13877 lsp::Position::new(7, 11),
13878 ),
13879 replace: lsp::Range::new(
13880 lsp::Position::new(7, 7),
13881 lsp::Position::new(7, 13),
13882 ),
13883 },
13884 )),
13885 ..lsp::CompletionItem::default()
13886 };
13887
13888 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
13889 })
13890 .next()
13891 .await
13892 .unwrap();
13893
13894 cx.condition(&editor, |editor, _| editor.context_menu_visible())
13895 .await;
13896
13897 editor
13898 .update_in(cx, |editor, window, cx| {
13899 editor
13900 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13901 .unwrap()
13902 })
13903 .await
13904 .unwrap();
13905
13906 editor.update(cx, |editor, cx| {
13907 assert_text_with_selections(editor, expected_multibuffer, cx);
13908 })
13909}
13910
13911#[gpui::test]
13912async fn test_completion(cx: &mut TestAppContext) {
13913 init_test(cx, |_| {});
13914
13915 let mut cx = EditorLspTestContext::new_rust(
13916 lsp::ServerCapabilities {
13917 completion_provider: Some(lsp::CompletionOptions {
13918 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
13919 resolve_provider: Some(true),
13920 ..Default::default()
13921 }),
13922 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
13923 ..Default::default()
13924 },
13925 cx,
13926 )
13927 .await;
13928 let counter = Arc::new(AtomicUsize::new(0));
13929
13930 cx.set_state(indoc! {"
13931 oneˇ
13932 two
13933 three
13934 "});
13935 cx.simulate_keystroke(".");
13936 handle_completion_request(
13937 indoc! {"
13938 one.|<>
13939 two
13940 three
13941 "},
13942 vec!["first_completion", "second_completion"],
13943 true,
13944 counter.clone(),
13945 &mut cx,
13946 )
13947 .await;
13948 cx.condition(|editor, _| editor.context_menu_visible())
13949 .await;
13950 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13951
13952 let _handler = handle_signature_help_request(
13953 &mut cx,
13954 lsp::SignatureHelp {
13955 signatures: vec![lsp::SignatureInformation {
13956 label: "test signature".to_string(),
13957 documentation: None,
13958 parameters: Some(vec![lsp::ParameterInformation {
13959 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
13960 documentation: None,
13961 }]),
13962 active_parameter: None,
13963 }],
13964 active_signature: None,
13965 active_parameter: None,
13966 },
13967 );
13968 cx.update_editor(|editor, window, cx| {
13969 assert!(
13970 !editor.signature_help_state.is_shown(),
13971 "No signature help was called for"
13972 );
13973 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13974 });
13975 cx.run_until_parked();
13976 cx.update_editor(|editor, _, _| {
13977 assert!(
13978 !editor.signature_help_state.is_shown(),
13979 "No signature help should be shown when completions menu is open"
13980 );
13981 });
13982
13983 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13984 editor.context_menu_next(&Default::default(), window, cx);
13985 editor
13986 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13987 .unwrap()
13988 });
13989 cx.assert_editor_state(indoc! {"
13990 one.second_completionˇ
13991 two
13992 three
13993 "});
13994
13995 handle_resolve_completion_request(
13996 &mut cx,
13997 Some(vec![
13998 (
13999 //This overlaps with the primary completion edit which is
14000 //misbehavior from the LSP spec, test that we filter it out
14001 indoc! {"
14002 one.second_ˇcompletion
14003 two
14004 threeˇ
14005 "},
14006 "overlapping additional edit",
14007 ),
14008 (
14009 indoc! {"
14010 one.second_completion
14011 two
14012 threeˇ
14013 "},
14014 "\nadditional edit",
14015 ),
14016 ]),
14017 )
14018 .await;
14019 apply_additional_edits.await.unwrap();
14020 cx.assert_editor_state(indoc! {"
14021 one.second_completionˇ
14022 two
14023 three
14024 additional edit
14025 "});
14026
14027 cx.set_state(indoc! {"
14028 one.second_completion
14029 twoˇ
14030 threeˇ
14031 additional edit
14032 "});
14033 cx.simulate_keystroke(" ");
14034 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14035 cx.simulate_keystroke("s");
14036 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14037
14038 cx.assert_editor_state(indoc! {"
14039 one.second_completion
14040 two sˇ
14041 three sˇ
14042 additional edit
14043 "});
14044 handle_completion_request(
14045 indoc! {"
14046 one.second_completion
14047 two s
14048 three <s|>
14049 additional edit
14050 "},
14051 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14052 true,
14053 counter.clone(),
14054 &mut cx,
14055 )
14056 .await;
14057 cx.condition(|editor, _| editor.context_menu_visible())
14058 .await;
14059 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14060
14061 cx.simulate_keystroke("i");
14062
14063 handle_completion_request(
14064 indoc! {"
14065 one.second_completion
14066 two si
14067 three <si|>
14068 additional edit
14069 "},
14070 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14071 true,
14072 counter.clone(),
14073 &mut cx,
14074 )
14075 .await;
14076 cx.condition(|editor, _| editor.context_menu_visible())
14077 .await;
14078 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14079
14080 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14081 editor
14082 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14083 .unwrap()
14084 });
14085 cx.assert_editor_state(indoc! {"
14086 one.second_completion
14087 two sixth_completionˇ
14088 three sixth_completionˇ
14089 additional edit
14090 "});
14091
14092 apply_additional_edits.await.unwrap();
14093
14094 update_test_language_settings(&mut cx, |settings| {
14095 settings.defaults.show_completions_on_input = Some(false);
14096 });
14097 cx.set_state("editorˇ");
14098 cx.simulate_keystroke(".");
14099 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14100 cx.simulate_keystrokes("c l o");
14101 cx.assert_editor_state("editor.cloˇ");
14102 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14103 cx.update_editor(|editor, window, cx| {
14104 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14105 });
14106 handle_completion_request(
14107 "editor.<clo|>",
14108 vec!["close", "clobber"],
14109 true,
14110 counter.clone(),
14111 &mut cx,
14112 )
14113 .await;
14114 cx.condition(|editor, _| editor.context_menu_visible())
14115 .await;
14116 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
14117
14118 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14119 editor
14120 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14121 .unwrap()
14122 });
14123 cx.assert_editor_state("editor.clobberˇ");
14124 handle_resolve_completion_request(&mut cx, None).await;
14125 apply_additional_edits.await.unwrap();
14126}
14127
14128#[gpui::test]
14129async fn test_completion_reuse(cx: &mut TestAppContext) {
14130 init_test(cx, |_| {});
14131
14132 let mut cx = EditorLspTestContext::new_rust(
14133 lsp::ServerCapabilities {
14134 completion_provider: Some(lsp::CompletionOptions {
14135 trigger_characters: Some(vec![".".to_string()]),
14136 ..Default::default()
14137 }),
14138 ..Default::default()
14139 },
14140 cx,
14141 )
14142 .await;
14143
14144 let counter = Arc::new(AtomicUsize::new(0));
14145 cx.set_state("objˇ");
14146 cx.simulate_keystroke(".");
14147
14148 // Initial completion request returns complete results
14149 let is_incomplete = false;
14150 handle_completion_request(
14151 "obj.|<>",
14152 vec!["a", "ab", "abc"],
14153 is_incomplete,
14154 counter.clone(),
14155 &mut cx,
14156 )
14157 .await;
14158 cx.run_until_parked();
14159 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14160 cx.assert_editor_state("obj.ˇ");
14161 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14162
14163 // Type "a" - filters existing completions
14164 cx.simulate_keystroke("a");
14165 cx.run_until_parked();
14166 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14167 cx.assert_editor_state("obj.aˇ");
14168 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14169
14170 // Type "b" - filters existing completions
14171 cx.simulate_keystroke("b");
14172 cx.run_until_parked();
14173 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14174 cx.assert_editor_state("obj.abˇ");
14175 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14176
14177 // Type "c" - filters existing completions
14178 cx.simulate_keystroke("c");
14179 cx.run_until_parked();
14180 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14181 cx.assert_editor_state("obj.abcˇ");
14182 check_displayed_completions(vec!["abc"], &mut cx);
14183
14184 // Backspace to delete "c" - filters existing completions
14185 cx.update_editor(|editor, window, cx| {
14186 editor.backspace(&Backspace, window, cx);
14187 });
14188 cx.run_until_parked();
14189 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14190 cx.assert_editor_state("obj.abˇ");
14191 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14192
14193 // Moving cursor to the left dismisses menu.
14194 cx.update_editor(|editor, window, cx| {
14195 editor.move_left(&MoveLeft, window, cx);
14196 });
14197 cx.run_until_parked();
14198 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14199 cx.assert_editor_state("obj.aˇb");
14200 cx.update_editor(|editor, _, _| {
14201 assert_eq!(editor.context_menu_visible(), false);
14202 });
14203
14204 // Type "b" - new request
14205 cx.simulate_keystroke("b");
14206 let is_incomplete = false;
14207 handle_completion_request(
14208 "obj.<ab|>a",
14209 vec!["ab", "abc"],
14210 is_incomplete,
14211 counter.clone(),
14212 &mut cx,
14213 )
14214 .await;
14215 cx.run_until_parked();
14216 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14217 cx.assert_editor_state("obj.abˇb");
14218 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14219
14220 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
14221 cx.update_editor(|editor, window, cx| {
14222 editor.backspace(&Backspace, window, cx);
14223 });
14224 let is_incomplete = false;
14225 handle_completion_request(
14226 "obj.<a|>b",
14227 vec!["a", "ab", "abc"],
14228 is_incomplete,
14229 counter.clone(),
14230 &mut cx,
14231 )
14232 .await;
14233 cx.run_until_parked();
14234 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14235 cx.assert_editor_state("obj.aˇb");
14236 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14237
14238 // Backspace to delete "a" - dismisses menu.
14239 cx.update_editor(|editor, window, cx| {
14240 editor.backspace(&Backspace, window, cx);
14241 });
14242 cx.run_until_parked();
14243 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14244 cx.assert_editor_state("obj.ˇb");
14245 cx.update_editor(|editor, _, _| {
14246 assert_eq!(editor.context_menu_visible(), false);
14247 });
14248}
14249
14250#[gpui::test]
14251async fn test_word_completion(cx: &mut TestAppContext) {
14252 let lsp_fetch_timeout_ms = 10;
14253 init_test(cx, |language_settings| {
14254 language_settings.defaults.completions = Some(CompletionSettingsContent {
14255 words_min_length: Some(0),
14256 lsp_fetch_timeout_ms: Some(10),
14257 lsp_insert_mode: Some(LspInsertMode::Insert),
14258 ..Default::default()
14259 });
14260 });
14261
14262 let mut cx = EditorLspTestContext::new_rust(
14263 lsp::ServerCapabilities {
14264 completion_provider: Some(lsp::CompletionOptions {
14265 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14266 ..lsp::CompletionOptions::default()
14267 }),
14268 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14269 ..lsp::ServerCapabilities::default()
14270 },
14271 cx,
14272 )
14273 .await;
14274
14275 let throttle_completions = Arc::new(AtomicBool::new(false));
14276
14277 let lsp_throttle_completions = throttle_completions.clone();
14278 let _completion_requests_handler =
14279 cx.lsp
14280 .server
14281 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
14282 let lsp_throttle_completions = lsp_throttle_completions.clone();
14283 let cx = cx.clone();
14284 async move {
14285 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
14286 cx.background_executor()
14287 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
14288 .await;
14289 }
14290 Ok(Some(lsp::CompletionResponse::Array(vec![
14291 lsp::CompletionItem {
14292 label: "first".into(),
14293 ..lsp::CompletionItem::default()
14294 },
14295 lsp::CompletionItem {
14296 label: "last".into(),
14297 ..lsp::CompletionItem::default()
14298 },
14299 ])))
14300 }
14301 });
14302
14303 cx.set_state(indoc! {"
14304 oneˇ
14305 two
14306 three
14307 "});
14308 cx.simulate_keystroke(".");
14309 cx.executor().run_until_parked();
14310 cx.condition(|editor, _| editor.context_menu_visible())
14311 .await;
14312 cx.update_editor(|editor, window, cx| {
14313 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14314 {
14315 assert_eq!(
14316 completion_menu_entries(menu),
14317 &["first", "last"],
14318 "When LSP server is fast to reply, no fallback word completions are used"
14319 );
14320 } else {
14321 panic!("expected completion menu to be open");
14322 }
14323 editor.cancel(&Cancel, window, cx);
14324 });
14325 cx.executor().run_until_parked();
14326 cx.condition(|editor, _| !editor.context_menu_visible())
14327 .await;
14328
14329 throttle_completions.store(true, atomic::Ordering::Release);
14330 cx.simulate_keystroke(".");
14331 cx.executor()
14332 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
14333 cx.executor().run_until_parked();
14334 cx.condition(|editor, _| editor.context_menu_visible())
14335 .await;
14336 cx.update_editor(|editor, _, _| {
14337 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14338 {
14339 assert_eq!(completion_menu_entries(menu), &["one", "three", "two"],
14340 "When LSP server is slow, document words can be shown instead, if configured accordingly");
14341 } else {
14342 panic!("expected completion menu to be open");
14343 }
14344 });
14345}
14346
14347#[gpui::test]
14348async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
14349 init_test(cx, |language_settings| {
14350 language_settings.defaults.completions = Some(CompletionSettingsContent {
14351 words: Some(WordsCompletionMode::Enabled),
14352 words_min_length: Some(0),
14353 lsp_insert_mode: Some(LspInsertMode::Insert),
14354 ..Default::default()
14355 });
14356 });
14357
14358 let mut cx = EditorLspTestContext::new_rust(
14359 lsp::ServerCapabilities {
14360 completion_provider: Some(lsp::CompletionOptions {
14361 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14362 ..lsp::CompletionOptions::default()
14363 }),
14364 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14365 ..lsp::ServerCapabilities::default()
14366 },
14367 cx,
14368 )
14369 .await;
14370
14371 let _completion_requests_handler =
14372 cx.lsp
14373 .server
14374 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14375 Ok(Some(lsp::CompletionResponse::Array(vec![
14376 lsp::CompletionItem {
14377 label: "first".into(),
14378 ..lsp::CompletionItem::default()
14379 },
14380 lsp::CompletionItem {
14381 label: "last".into(),
14382 ..lsp::CompletionItem::default()
14383 },
14384 ])))
14385 });
14386
14387 cx.set_state(indoc! {"ˇ
14388 first
14389 last
14390 second
14391 "});
14392 cx.simulate_keystroke(".");
14393 cx.executor().run_until_parked();
14394 cx.condition(|editor, _| editor.context_menu_visible())
14395 .await;
14396 cx.update_editor(|editor, _, _| {
14397 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14398 {
14399 assert_eq!(
14400 completion_menu_entries(menu),
14401 &["first", "last", "second"],
14402 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
14403 );
14404 } else {
14405 panic!("expected completion menu to be open");
14406 }
14407 });
14408}
14409
14410#[gpui::test]
14411async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
14412 init_test(cx, |language_settings| {
14413 language_settings.defaults.completions = Some(CompletionSettingsContent {
14414 words: Some(WordsCompletionMode::Disabled),
14415 words_min_length: Some(0),
14416 lsp_insert_mode: Some(LspInsertMode::Insert),
14417 ..Default::default()
14418 });
14419 });
14420
14421 let mut cx = EditorLspTestContext::new_rust(
14422 lsp::ServerCapabilities {
14423 completion_provider: Some(lsp::CompletionOptions {
14424 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14425 ..lsp::CompletionOptions::default()
14426 }),
14427 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14428 ..lsp::ServerCapabilities::default()
14429 },
14430 cx,
14431 )
14432 .await;
14433
14434 let _completion_requests_handler =
14435 cx.lsp
14436 .server
14437 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14438 panic!("LSP completions should not be queried when dealing with word completions")
14439 });
14440
14441 cx.set_state(indoc! {"ˇ
14442 first
14443 last
14444 second
14445 "});
14446 cx.update_editor(|editor, window, cx| {
14447 editor.show_word_completions(&ShowWordCompletions, window, cx);
14448 });
14449 cx.executor().run_until_parked();
14450 cx.condition(|editor, _| editor.context_menu_visible())
14451 .await;
14452 cx.update_editor(|editor, _, _| {
14453 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14454 {
14455 assert_eq!(
14456 completion_menu_entries(menu),
14457 &["first", "last", "second"],
14458 "`ShowWordCompletions` action should show word completions"
14459 );
14460 } else {
14461 panic!("expected completion menu to be open");
14462 }
14463 });
14464
14465 cx.simulate_keystroke("l");
14466 cx.executor().run_until_parked();
14467 cx.condition(|editor, _| editor.context_menu_visible())
14468 .await;
14469 cx.update_editor(|editor, _, _| {
14470 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14471 {
14472 assert_eq!(
14473 completion_menu_entries(menu),
14474 &["last"],
14475 "After showing word completions, further editing should filter them and not query the LSP"
14476 );
14477 } else {
14478 panic!("expected completion menu to be open");
14479 }
14480 });
14481}
14482
14483#[gpui::test]
14484async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
14485 init_test(cx, |language_settings| {
14486 language_settings.defaults.completions = Some(CompletionSettingsContent {
14487 words_min_length: Some(0),
14488 lsp: Some(false),
14489 lsp_insert_mode: Some(LspInsertMode::Insert),
14490 ..Default::default()
14491 });
14492 });
14493
14494 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14495
14496 cx.set_state(indoc! {"ˇ
14497 0_usize
14498 let
14499 33
14500 4.5f32
14501 "});
14502 cx.update_editor(|editor, window, cx| {
14503 editor.show_completions(&ShowCompletions::default(), window, cx);
14504 });
14505 cx.executor().run_until_parked();
14506 cx.condition(|editor, _| editor.context_menu_visible())
14507 .await;
14508 cx.update_editor(|editor, window, cx| {
14509 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14510 {
14511 assert_eq!(
14512 completion_menu_entries(menu),
14513 &["let"],
14514 "With no digits in the completion query, no digits should be in the word completions"
14515 );
14516 } else {
14517 panic!("expected completion menu to be open");
14518 }
14519 editor.cancel(&Cancel, window, cx);
14520 });
14521
14522 cx.set_state(indoc! {"3ˇ
14523 0_usize
14524 let
14525 3
14526 33.35f32
14527 "});
14528 cx.update_editor(|editor, window, cx| {
14529 editor.show_completions(&ShowCompletions::default(), window, cx);
14530 });
14531 cx.executor().run_until_parked();
14532 cx.condition(|editor, _| editor.context_menu_visible())
14533 .await;
14534 cx.update_editor(|editor, _, _| {
14535 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14536 {
14537 assert_eq!(completion_menu_entries(menu), &["33", "35f32"], "The digit is in the completion query, \
14538 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
14539 } else {
14540 panic!("expected completion menu to be open");
14541 }
14542 });
14543}
14544
14545#[gpui::test]
14546async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) {
14547 init_test(cx, |language_settings| {
14548 language_settings.defaults.completions = Some(CompletionSettingsContent {
14549 words: Some(WordsCompletionMode::Enabled),
14550 words_min_length: Some(3),
14551 lsp_insert_mode: Some(LspInsertMode::Insert),
14552 ..Default::default()
14553 });
14554 });
14555
14556 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14557 cx.set_state(indoc! {"ˇ
14558 wow
14559 wowen
14560 wowser
14561 "});
14562 cx.simulate_keystroke("w");
14563 cx.executor().run_until_parked();
14564 cx.update_editor(|editor, _, _| {
14565 if editor.context_menu.borrow_mut().is_some() {
14566 panic!(
14567 "expected completion menu to be hidden, as words completion threshold is not met"
14568 );
14569 }
14570 });
14571
14572 cx.update_editor(|editor, window, cx| {
14573 editor.show_word_completions(&ShowWordCompletions, window, cx);
14574 });
14575 cx.executor().run_until_parked();
14576 cx.update_editor(|editor, window, cx| {
14577 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14578 {
14579 assert_eq!(completion_menu_entries(menu), &["wowser", "wowen", "wow"], "Even though the threshold is not met, invoking word completions with an action should provide the completions");
14580 } else {
14581 panic!("expected completion menu to be open after the word completions are called with an action");
14582 }
14583
14584 editor.cancel(&Cancel, window, cx);
14585 });
14586 cx.update_editor(|editor, _, _| {
14587 if editor.context_menu.borrow_mut().is_some() {
14588 panic!("expected completion menu to be hidden after canceling");
14589 }
14590 });
14591
14592 cx.simulate_keystroke("o");
14593 cx.executor().run_until_parked();
14594 cx.update_editor(|editor, _, _| {
14595 if editor.context_menu.borrow_mut().is_some() {
14596 panic!(
14597 "expected completion menu to be hidden, as words completion threshold is not met still"
14598 );
14599 }
14600 });
14601
14602 cx.simulate_keystroke("w");
14603 cx.executor().run_until_parked();
14604 cx.update_editor(|editor, _, _| {
14605 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14606 {
14607 assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word");
14608 } else {
14609 panic!("expected completion menu to be open after the word completions threshold is met");
14610 }
14611 });
14612}
14613
14614#[gpui::test]
14615async fn test_word_completions_disabled(cx: &mut TestAppContext) {
14616 init_test(cx, |language_settings| {
14617 language_settings.defaults.completions = Some(CompletionSettingsContent {
14618 words: Some(WordsCompletionMode::Enabled),
14619 words_min_length: Some(0),
14620 lsp_insert_mode: Some(LspInsertMode::Insert),
14621 ..Default::default()
14622 });
14623 });
14624
14625 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14626 cx.update_editor(|editor, _, _| {
14627 editor.disable_word_completions();
14628 });
14629 cx.set_state(indoc! {"ˇ
14630 wow
14631 wowen
14632 wowser
14633 "});
14634 cx.simulate_keystroke("w");
14635 cx.executor().run_until_parked();
14636 cx.update_editor(|editor, _, _| {
14637 if editor.context_menu.borrow_mut().is_some() {
14638 panic!(
14639 "expected completion menu to be hidden, as words completion are disabled for this editor"
14640 );
14641 }
14642 });
14643
14644 cx.update_editor(|editor, window, cx| {
14645 editor.show_word_completions(&ShowWordCompletions, window, cx);
14646 });
14647 cx.executor().run_until_parked();
14648 cx.update_editor(|editor, _, _| {
14649 if editor.context_menu.borrow_mut().is_some() {
14650 panic!(
14651 "expected completion menu to be hidden even if called for explicitly, as words completion are disabled for this editor"
14652 );
14653 }
14654 });
14655}
14656
14657fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
14658 let position = || lsp::Position {
14659 line: params.text_document_position.position.line,
14660 character: params.text_document_position.position.character,
14661 };
14662 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14663 range: lsp::Range {
14664 start: position(),
14665 end: position(),
14666 },
14667 new_text: text.to_string(),
14668 }))
14669}
14670
14671#[gpui::test]
14672async fn test_multiline_completion(cx: &mut TestAppContext) {
14673 init_test(cx, |_| {});
14674
14675 let fs = FakeFs::new(cx.executor());
14676 fs.insert_tree(
14677 path!("/a"),
14678 json!({
14679 "main.ts": "a",
14680 }),
14681 )
14682 .await;
14683
14684 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14685 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14686 let typescript_language = Arc::new(Language::new(
14687 LanguageConfig {
14688 name: "TypeScript".into(),
14689 matcher: LanguageMatcher {
14690 path_suffixes: vec!["ts".to_string()],
14691 ..LanguageMatcher::default()
14692 },
14693 line_comments: vec!["// ".into()],
14694 ..LanguageConfig::default()
14695 },
14696 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
14697 ));
14698 language_registry.add(typescript_language.clone());
14699 let mut fake_servers = language_registry.register_fake_lsp(
14700 "TypeScript",
14701 FakeLspAdapter {
14702 capabilities: lsp::ServerCapabilities {
14703 completion_provider: Some(lsp::CompletionOptions {
14704 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14705 ..lsp::CompletionOptions::default()
14706 }),
14707 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14708 ..lsp::ServerCapabilities::default()
14709 },
14710 // Emulate vtsls label generation
14711 label_for_completion: Some(Box::new(|item, _| {
14712 let text = if let Some(description) = item
14713 .label_details
14714 .as_ref()
14715 .and_then(|label_details| label_details.description.as_ref())
14716 {
14717 format!("{} {}", item.label, description)
14718 } else if let Some(detail) = &item.detail {
14719 format!("{} {}", item.label, detail)
14720 } else {
14721 item.label.clone()
14722 };
14723 let len = text.len();
14724 Some(language::CodeLabel {
14725 text,
14726 runs: Vec::new(),
14727 filter_range: 0..len,
14728 })
14729 })),
14730 ..FakeLspAdapter::default()
14731 },
14732 );
14733 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14734 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14735 let worktree_id = workspace
14736 .update(cx, |workspace, _window, cx| {
14737 workspace.project().update(cx, |project, cx| {
14738 project.worktrees(cx).next().unwrap().read(cx).id()
14739 })
14740 })
14741 .unwrap();
14742 let _buffer = project
14743 .update(cx, |project, cx| {
14744 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
14745 })
14746 .await
14747 .unwrap();
14748 let editor = workspace
14749 .update(cx, |workspace, window, cx| {
14750 workspace.open_path((worktree_id, rel_path("main.ts")), None, true, window, cx)
14751 })
14752 .unwrap()
14753 .await
14754 .unwrap()
14755 .downcast::<Editor>()
14756 .unwrap();
14757 let fake_server = fake_servers.next().await.unwrap();
14758
14759 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
14760 let multiline_label_2 = "a\nb\nc\n";
14761 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
14762 let multiline_description = "d\ne\nf\n";
14763 let multiline_detail_2 = "g\nh\ni\n";
14764
14765 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
14766 move |params, _| async move {
14767 Ok(Some(lsp::CompletionResponse::Array(vec![
14768 lsp::CompletionItem {
14769 label: multiline_label.to_string(),
14770 text_edit: gen_text_edit(¶ms, "new_text_1"),
14771 ..lsp::CompletionItem::default()
14772 },
14773 lsp::CompletionItem {
14774 label: "single line label 1".to_string(),
14775 detail: Some(multiline_detail.to_string()),
14776 text_edit: gen_text_edit(¶ms, "new_text_2"),
14777 ..lsp::CompletionItem::default()
14778 },
14779 lsp::CompletionItem {
14780 label: "single line label 2".to_string(),
14781 label_details: Some(lsp::CompletionItemLabelDetails {
14782 description: Some(multiline_description.to_string()),
14783 detail: None,
14784 }),
14785 text_edit: gen_text_edit(¶ms, "new_text_2"),
14786 ..lsp::CompletionItem::default()
14787 },
14788 lsp::CompletionItem {
14789 label: multiline_label_2.to_string(),
14790 detail: Some(multiline_detail_2.to_string()),
14791 text_edit: gen_text_edit(¶ms, "new_text_3"),
14792 ..lsp::CompletionItem::default()
14793 },
14794 lsp::CompletionItem {
14795 label: "Label with many spaces and \t but without newlines".to_string(),
14796 detail: Some(
14797 "Details with many spaces and \t but without newlines".to_string(),
14798 ),
14799 text_edit: gen_text_edit(¶ms, "new_text_4"),
14800 ..lsp::CompletionItem::default()
14801 },
14802 ])))
14803 },
14804 );
14805
14806 editor.update_in(cx, |editor, window, cx| {
14807 cx.focus_self(window);
14808 editor.move_to_end(&MoveToEnd, window, cx);
14809 editor.handle_input(".", window, cx);
14810 });
14811 cx.run_until_parked();
14812 completion_handle.next().await.unwrap();
14813
14814 editor.update(cx, |editor, _| {
14815 assert!(editor.context_menu_visible());
14816 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14817 {
14818 let completion_labels = menu
14819 .completions
14820 .borrow()
14821 .iter()
14822 .map(|c| c.label.text.clone())
14823 .collect::<Vec<_>>();
14824 assert_eq!(
14825 completion_labels,
14826 &[
14827 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
14828 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
14829 "single line label 2 d e f ",
14830 "a b c g h i ",
14831 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
14832 ],
14833 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
14834 );
14835
14836 for completion in menu
14837 .completions
14838 .borrow()
14839 .iter() {
14840 assert_eq!(
14841 completion.label.filter_range,
14842 0..completion.label.text.len(),
14843 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
14844 );
14845 }
14846 } else {
14847 panic!("expected completion menu to be open");
14848 }
14849 });
14850}
14851
14852#[gpui::test]
14853async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
14854 init_test(cx, |_| {});
14855 let mut cx = EditorLspTestContext::new_rust(
14856 lsp::ServerCapabilities {
14857 completion_provider: Some(lsp::CompletionOptions {
14858 trigger_characters: Some(vec![".".to_string()]),
14859 ..Default::default()
14860 }),
14861 ..Default::default()
14862 },
14863 cx,
14864 )
14865 .await;
14866 cx.lsp
14867 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14868 Ok(Some(lsp::CompletionResponse::Array(vec![
14869 lsp::CompletionItem {
14870 label: "first".into(),
14871 ..Default::default()
14872 },
14873 lsp::CompletionItem {
14874 label: "last".into(),
14875 ..Default::default()
14876 },
14877 ])))
14878 });
14879 cx.set_state("variableˇ");
14880 cx.simulate_keystroke(".");
14881 cx.executor().run_until_parked();
14882
14883 cx.update_editor(|editor, _, _| {
14884 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14885 {
14886 assert_eq!(completion_menu_entries(menu), &["first", "last"]);
14887 } else {
14888 panic!("expected completion menu to be open");
14889 }
14890 });
14891
14892 cx.update_editor(|editor, window, cx| {
14893 editor.move_page_down(&MovePageDown::default(), window, cx);
14894 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14895 {
14896 assert!(
14897 menu.selected_item == 1,
14898 "expected PageDown to select the last item from the context menu"
14899 );
14900 } else {
14901 panic!("expected completion menu to stay open after PageDown");
14902 }
14903 });
14904
14905 cx.update_editor(|editor, window, cx| {
14906 editor.move_page_up(&MovePageUp::default(), window, cx);
14907 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14908 {
14909 assert!(
14910 menu.selected_item == 0,
14911 "expected PageUp to select the first item from the context menu"
14912 );
14913 } else {
14914 panic!("expected completion menu to stay open after PageUp");
14915 }
14916 });
14917}
14918
14919#[gpui::test]
14920async fn test_as_is_completions(cx: &mut TestAppContext) {
14921 init_test(cx, |_| {});
14922 let mut cx = EditorLspTestContext::new_rust(
14923 lsp::ServerCapabilities {
14924 completion_provider: Some(lsp::CompletionOptions {
14925 ..Default::default()
14926 }),
14927 ..Default::default()
14928 },
14929 cx,
14930 )
14931 .await;
14932 cx.lsp
14933 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14934 Ok(Some(lsp::CompletionResponse::Array(vec![
14935 lsp::CompletionItem {
14936 label: "unsafe".into(),
14937 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14938 range: lsp::Range {
14939 start: lsp::Position {
14940 line: 1,
14941 character: 2,
14942 },
14943 end: lsp::Position {
14944 line: 1,
14945 character: 3,
14946 },
14947 },
14948 new_text: "unsafe".to_string(),
14949 })),
14950 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
14951 ..Default::default()
14952 },
14953 ])))
14954 });
14955 cx.set_state("fn a() {}\n nˇ");
14956 cx.executor().run_until_parked();
14957 cx.update_editor(|editor, window, cx| {
14958 editor.show_completions(
14959 &ShowCompletions {
14960 trigger: Some("\n".into()),
14961 },
14962 window,
14963 cx,
14964 );
14965 });
14966 cx.executor().run_until_parked();
14967
14968 cx.update_editor(|editor, window, cx| {
14969 editor.confirm_completion(&Default::default(), window, cx)
14970 });
14971 cx.executor().run_until_parked();
14972 cx.assert_editor_state("fn a() {}\n unsafeˇ");
14973}
14974
14975#[gpui::test]
14976async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
14977 init_test(cx, |_| {});
14978 let language =
14979 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
14980 let mut cx = EditorLspTestContext::new(
14981 language,
14982 lsp::ServerCapabilities {
14983 completion_provider: Some(lsp::CompletionOptions {
14984 ..lsp::CompletionOptions::default()
14985 }),
14986 ..lsp::ServerCapabilities::default()
14987 },
14988 cx,
14989 )
14990 .await;
14991
14992 cx.set_state(
14993 "#ifndef BAR_H
14994#define BAR_H
14995
14996#include <stdbool.h>
14997
14998int fn_branch(bool do_branch1, bool do_branch2);
14999
15000#endif // BAR_H
15001ˇ",
15002 );
15003 cx.executor().run_until_parked();
15004 cx.update_editor(|editor, window, cx| {
15005 editor.handle_input("#", window, cx);
15006 });
15007 cx.executor().run_until_parked();
15008 cx.update_editor(|editor, window, cx| {
15009 editor.handle_input("i", window, cx);
15010 });
15011 cx.executor().run_until_parked();
15012 cx.update_editor(|editor, window, cx| {
15013 editor.handle_input("n", window, cx);
15014 });
15015 cx.executor().run_until_parked();
15016 cx.assert_editor_state(
15017 "#ifndef BAR_H
15018#define BAR_H
15019
15020#include <stdbool.h>
15021
15022int fn_branch(bool do_branch1, bool do_branch2);
15023
15024#endif // BAR_H
15025#inˇ",
15026 );
15027
15028 cx.lsp
15029 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15030 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15031 is_incomplete: false,
15032 item_defaults: None,
15033 items: vec![lsp::CompletionItem {
15034 kind: Some(lsp::CompletionItemKind::SNIPPET),
15035 label_details: Some(lsp::CompletionItemLabelDetails {
15036 detail: Some("header".to_string()),
15037 description: None,
15038 }),
15039 label: " include".to_string(),
15040 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15041 range: lsp::Range {
15042 start: lsp::Position {
15043 line: 8,
15044 character: 1,
15045 },
15046 end: lsp::Position {
15047 line: 8,
15048 character: 1,
15049 },
15050 },
15051 new_text: "include \"$0\"".to_string(),
15052 })),
15053 sort_text: Some("40b67681include".to_string()),
15054 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15055 filter_text: Some("include".to_string()),
15056 insert_text: Some("include \"$0\"".to_string()),
15057 ..lsp::CompletionItem::default()
15058 }],
15059 })))
15060 });
15061 cx.update_editor(|editor, window, cx| {
15062 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15063 });
15064 cx.executor().run_until_parked();
15065 cx.update_editor(|editor, window, cx| {
15066 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15067 });
15068 cx.executor().run_until_parked();
15069 cx.assert_editor_state(
15070 "#ifndef BAR_H
15071#define BAR_H
15072
15073#include <stdbool.h>
15074
15075int fn_branch(bool do_branch1, bool do_branch2);
15076
15077#endif // BAR_H
15078#include \"ˇ\"",
15079 );
15080
15081 cx.lsp
15082 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15083 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15084 is_incomplete: true,
15085 item_defaults: None,
15086 items: vec![lsp::CompletionItem {
15087 kind: Some(lsp::CompletionItemKind::FILE),
15088 label: "AGL/".to_string(),
15089 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15090 range: lsp::Range {
15091 start: lsp::Position {
15092 line: 8,
15093 character: 10,
15094 },
15095 end: lsp::Position {
15096 line: 8,
15097 character: 11,
15098 },
15099 },
15100 new_text: "AGL/".to_string(),
15101 })),
15102 sort_text: Some("40b67681AGL/".to_string()),
15103 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
15104 filter_text: Some("AGL/".to_string()),
15105 insert_text: Some("AGL/".to_string()),
15106 ..lsp::CompletionItem::default()
15107 }],
15108 })))
15109 });
15110 cx.update_editor(|editor, window, cx| {
15111 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15112 });
15113 cx.executor().run_until_parked();
15114 cx.update_editor(|editor, window, cx| {
15115 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15116 });
15117 cx.executor().run_until_parked();
15118 cx.assert_editor_state(
15119 r##"#ifndef BAR_H
15120#define BAR_H
15121
15122#include <stdbool.h>
15123
15124int fn_branch(bool do_branch1, bool do_branch2);
15125
15126#endif // BAR_H
15127#include "AGL/ˇ"##,
15128 );
15129
15130 cx.update_editor(|editor, window, cx| {
15131 editor.handle_input("\"", window, cx);
15132 });
15133 cx.executor().run_until_parked();
15134 cx.assert_editor_state(
15135 r##"#ifndef BAR_H
15136#define BAR_H
15137
15138#include <stdbool.h>
15139
15140int fn_branch(bool do_branch1, bool do_branch2);
15141
15142#endif // BAR_H
15143#include "AGL/"ˇ"##,
15144 );
15145}
15146
15147#[gpui::test]
15148async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
15149 init_test(cx, |_| {});
15150
15151 let mut cx = EditorLspTestContext::new_rust(
15152 lsp::ServerCapabilities {
15153 completion_provider: Some(lsp::CompletionOptions {
15154 trigger_characters: Some(vec![".".to_string()]),
15155 resolve_provider: Some(true),
15156 ..Default::default()
15157 }),
15158 ..Default::default()
15159 },
15160 cx,
15161 )
15162 .await;
15163
15164 cx.set_state("fn main() { let a = 2ˇ; }");
15165 cx.simulate_keystroke(".");
15166 let completion_item = lsp::CompletionItem {
15167 label: "Some".into(),
15168 kind: Some(lsp::CompletionItemKind::SNIPPET),
15169 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
15170 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
15171 kind: lsp::MarkupKind::Markdown,
15172 value: "```rust\nSome(2)\n```".to_string(),
15173 })),
15174 deprecated: Some(false),
15175 sort_text: Some("Some".to_string()),
15176 filter_text: Some("Some".to_string()),
15177 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15178 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15179 range: lsp::Range {
15180 start: lsp::Position {
15181 line: 0,
15182 character: 22,
15183 },
15184 end: lsp::Position {
15185 line: 0,
15186 character: 22,
15187 },
15188 },
15189 new_text: "Some(2)".to_string(),
15190 })),
15191 additional_text_edits: Some(vec![lsp::TextEdit {
15192 range: lsp::Range {
15193 start: lsp::Position {
15194 line: 0,
15195 character: 20,
15196 },
15197 end: lsp::Position {
15198 line: 0,
15199 character: 22,
15200 },
15201 },
15202 new_text: "".to_string(),
15203 }]),
15204 ..Default::default()
15205 };
15206
15207 let closure_completion_item = completion_item.clone();
15208 let counter = Arc::new(AtomicUsize::new(0));
15209 let counter_clone = counter.clone();
15210 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15211 let task_completion_item = closure_completion_item.clone();
15212 counter_clone.fetch_add(1, atomic::Ordering::Release);
15213 async move {
15214 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15215 is_incomplete: true,
15216 item_defaults: None,
15217 items: vec![task_completion_item],
15218 })))
15219 }
15220 });
15221
15222 cx.condition(|editor, _| editor.context_menu_visible())
15223 .await;
15224 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
15225 assert!(request.next().await.is_some());
15226 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15227
15228 cx.simulate_keystrokes("S o m");
15229 cx.condition(|editor, _| editor.context_menu_visible())
15230 .await;
15231 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
15232 assert!(request.next().await.is_some());
15233 assert!(request.next().await.is_some());
15234 assert!(request.next().await.is_some());
15235 request.close();
15236 assert!(request.next().await.is_none());
15237 assert_eq!(
15238 counter.load(atomic::Ordering::Acquire),
15239 4,
15240 "With the completions menu open, only one LSP request should happen per input"
15241 );
15242}
15243
15244#[gpui::test]
15245async fn test_toggle_comment(cx: &mut TestAppContext) {
15246 init_test(cx, |_| {});
15247 let mut cx = EditorTestContext::new(cx).await;
15248 let language = Arc::new(Language::new(
15249 LanguageConfig {
15250 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15251 ..Default::default()
15252 },
15253 Some(tree_sitter_rust::LANGUAGE.into()),
15254 ));
15255 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15256
15257 // If multiple selections intersect a line, the line is only toggled once.
15258 cx.set_state(indoc! {"
15259 fn a() {
15260 «//b();
15261 ˇ»// «c();
15262 //ˇ» d();
15263 }
15264 "});
15265
15266 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15267
15268 cx.assert_editor_state(indoc! {"
15269 fn a() {
15270 «b();
15271 c();
15272 ˇ» d();
15273 }
15274 "});
15275
15276 // The comment prefix is inserted at the same column for every line in a
15277 // selection.
15278 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15279
15280 cx.assert_editor_state(indoc! {"
15281 fn a() {
15282 // «b();
15283 // c();
15284 ˇ»// d();
15285 }
15286 "});
15287
15288 // If a selection ends at the beginning of a line, that line is not toggled.
15289 cx.set_selections_state(indoc! {"
15290 fn a() {
15291 // b();
15292 «// c();
15293 ˇ» // d();
15294 }
15295 "});
15296
15297 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15298
15299 cx.assert_editor_state(indoc! {"
15300 fn a() {
15301 // b();
15302 «c();
15303 ˇ» // d();
15304 }
15305 "});
15306
15307 // If a selection span a single line and is empty, the line is toggled.
15308 cx.set_state(indoc! {"
15309 fn a() {
15310 a();
15311 b();
15312 ˇ
15313 }
15314 "});
15315
15316 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15317
15318 cx.assert_editor_state(indoc! {"
15319 fn a() {
15320 a();
15321 b();
15322 //•ˇ
15323 }
15324 "});
15325
15326 // If a selection span multiple lines, empty lines are not toggled.
15327 cx.set_state(indoc! {"
15328 fn a() {
15329 «a();
15330
15331 c();ˇ»
15332 }
15333 "});
15334
15335 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15336
15337 cx.assert_editor_state(indoc! {"
15338 fn a() {
15339 // «a();
15340
15341 // c();ˇ»
15342 }
15343 "});
15344
15345 // If a selection includes multiple comment prefixes, all lines are uncommented.
15346 cx.set_state(indoc! {"
15347 fn a() {
15348 «// a();
15349 /// b();
15350 //! c();ˇ»
15351 }
15352 "});
15353
15354 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15355
15356 cx.assert_editor_state(indoc! {"
15357 fn a() {
15358 «a();
15359 b();
15360 c();ˇ»
15361 }
15362 "});
15363}
15364
15365#[gpui::test]
15366async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
15367 init_test(cx, |_| {});
15368 let mut cx = EditorTestContext::new(cx).await;
15369 let language = Arc::new(Language::new(
15370 LanguageConfig {
15371 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15372 ..Default::default()
15373 },
15374 Some(tree_sitter_rust::LANGUAGE.into()),
15375 ));
15376 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15377
15378 let toggle_comments = &ToggleComments {
15379 advance_downwards: false,
15380 ignore_indent: true,
15381 };
15382
15383 // If multiple selections intersect a line, the line is only toggled once.
15384 cx.set_state(indoc! {"
15385 fn a() {
15386 // «b();
15387 // c();
15388 // ˇ» d();
15389 }
15390 "});
15391
15392 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15393
15394 cx.assert_editor_state(indoc! {"
15395 fn a() {
15396 «b();
15397 c();
15398 ˇ» d();
15399 }
15400 "});
15401
15402 // The comment prefix is inserted at the beginning of each line
15403 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15404
15405 cx.assert_editor_state(indoc! {"
15406 fn a() {
15407 // «b();
15408 // c();
15409 // ˇ» d();
15410 }
15411 "});
15412
15413 // If a selection ends at the beginning of a line, that line is not toggled.
15414 cx.set_selections_state(indoc! {"
15415 fn a() {
15416 // b();
15417 // «c();
15418 ˇ»// d();
15419 }
15420 "});
15421
15422 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15423
15424 cx.assert_editor_state(indoc! {"
15425 fn a() {
15426 // b();
15427 «c();
15428 ˇ»// d();
15429 }
15430 "});
15431
15432 // If a selection span a single line and is empty, the line is toggled.
15433 cx.set_state(indoc! {"
15434 fn a() {
15435 a();
15436 b();
15437 ˇ
15438 }
15439 "});
15440
15441 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15442
15443 cx.assert_editor_state(indoc! {"
15444 fn a() {
15445 a();
15446 b();
15447 //ˇ
15448 }
15449 "});
15450
15451 // If a selection span multiple lines, empty lines are not toggled.
15452 cx.set_state(indoc! {"
15453 fn a() {
15454 «a();
15455
15456 c();ˇ»
15457 }
15458 "});
15459
15460 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15461
15462 cx.assert_editor_state(indoc! {"
15463 fn a() {
15464 // «a();
15465
15466 // c();ˇ»
15467 }
15468 "});
15469
15470 // If a selection includes multiple comment prefixes, all lines are uncommented.
15471 cx.set_state(indoc! {"
15472 fn a() {
15473 // «a();
15474 /// b();
15475 //! c();ˇ»
15476 }
15477 "});
15478
15479 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15480
15481 cx.assert_editor_state(indoc! {"
15482 fn a() {
15483 «a();
15484 b();
15485 c();ˇ»
15486 }
15487 "});
15488}
15489
15490#[gpui::test]
15491async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
15492 init_test(cx, |_| {});
15493
15494 let language = Arc::new(Language::new(
15495 LanguageConfig {
15496 line_comments: vec!["// ".into()],
15497 ..Default::default()
15498 },
15499 Some(tree_sitter_rust::LANGUAGE.into()),
15500 ));
15501
15502 let mut cx = EditorTestContext::new(cx).await;
15503
15504 cx.language_registry().add(language.clone());
15505 cx.update_buffer(|buffer, cx| {
15506 buffer.set_language(Some(language), cx);
15507 });
15508
15509 let toggle_comments = &ToggleComments {
15510 advance_downwards: true,
15511 ignore_indent: false,
15512 };
15513
15514 // Single cursor on one line -> advance
15515 // Cursor moves horizontally 3 characters as well on non-blank line
15516 cx.set_state(indoc!(
15517 "fn a() {
15518 ˇdog();
15519 cat();
15520 }"
15521 ));
15522 cx.update_editor(|editor, window, cx| {
15523 editor.toggle_comments(toggle_comments, window, cx);
15524 });
15525 cx.assert_editor_state(indoc!(
15526 "fn a() {
15527 // dog();
15528 catˇ();
15529 }"
15530 ));
15531
15532 // Single selection on one line -> don't advance
15533 cx.set_state(indoc!(
15534 "fn a() {
15535 «dog()ˇ»;
15536 cat();
15537 }"
15538 ));
15539 cx.update_editor(|editor, window, cx| {
15540 editor.toggle_comments(toggle_comments, window, cx);
15541 });
15542 cx.assert_editor_state(indoc!(
15543 "fn a() {
15544 // «dog()ˇ»;
15545 cat();
15546 }"
15547 ));
15548
15549 // Multiple cursors on one line -> advance
15550 cx.set_state(indoc!(
15551 "fn a() {
15552 ˇdˇog();
15553 cat();
15554 }"
15555 ));
15556 cx.update_editor(|editor, window, cx| {
15557 editor.toggle_comments(toggle_comments, window, cx);
15558 });
15559 cx.assert_editor_state(indoc!(
15560 "fn a() {
15561 // dog();
15562 catˇ(ˇ);
15563 }"
15564 ));
15565
15566 // Multiple cursors on one line, with selection -> don't advance
15567 cx.set_state(indoc!(
15568 "fn a() {
15569 ˇdˇog«()ˇ»;
15570 cat();
15571 }"
15572 ));
15573 cx.update_editor(|editor, window, cx| {
15574 editor.toggle_comments(toggle_comments, window, cx);
15575 });
15576 cx.assert_editor_state(indoc!(
15577 "fn a() {
15578 // ˇdˇog«()ˇ»;
15579 cat();
15580 }"
15581 ));
15582
15583 // Single cursor on one line -> advance
15584 // Cursor moves to column 0 on blank line
15585 cx.set_state(indoc!(
15586 "fn a() {
15587 ˇdog();
15588
15589 cat();
15590 }"
15591 ));
15592 cx.update_editor(|editor, window, cx| {
15593 editor.toggle_comments(toggle_comments, window, cx);
15594 });
15595 cx.assert_editor_state(indoc!(
15596 "fn a() {
15597 // dog();
15598 ˇ
15599 cat();
15600 }"
15601 ));
15602
15603 // Single cursor on one line -> advance
15604 // Cursor starts and ends at column 0
15605 cx.set_state(indoc!(
15606 "fn a() {
15607 ˇ dog();
15608 cat();
15609 }"
15610 ));
15611 cx.update_editor(|editor, window, cx| {
15612 editor.toggle_comments(toggle_comments, window, cx);
15613 });
15614 cx.assert_editor_state(indoc!(
15615 "fn a() {
15616 // dog();
15617 ˇ cat();
15618 }"
15619 ));
15620}
15621
15622#[gpui::test]
15623async fn test_toggle_block_comment(cx: &mut TestAppContext) {
15624 init_test(cx, |_| {});
15625
15626 let mut cx = EditorTestContext::new(cx).await;
15627
15628 let html_language = Arc::new(
15629 Language::new(
15630 LanguageConfig {
15631 name: "HTML".into(),
15632 block_comment: Some(BlockCommentConfig {
15633 start: "<!-- ".into(),
15634 prefix: "".into(),
15635 end: " -->".into(),
15636 tab_size: 0,
15637 }),
15638 ..Default::default()
15639 },
15640 Some(tree_sitter_html::LANGUAGE.into()),
15641 )
15642 .with_injection_query(
15643 r#"
15644 (script_element
15645 (raw_text) @injection.content
15646 (#set! injection.language "javascript"))
15647 "#,
15648 )
15649 .unwrap(),
15650 );
15651
15652 let javascript_language = Arc::new(Language::new(
15653 LanguageConfig {
15654 name: "JavaScript".into(),
15655 line_comments: vec!["// ".into()],
15656 ..Default::default()
15657 },
15658 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
15659 ));
15660
15661 cx.language_registry().add(html_language.clone());
15662 cx.language_registry().add(javascript_language);
15663 cx.update_buffer(|buffer, cx| {
15664 buffer.set_language(Some(html_language), cx);
15665 });
15666
15667 // Toggle comments for empty selections
15668 cx.set_state(
15669 &r#"
15670 <p>A</p>ˇ
15671 <p>B</p>ˇ
15672 <p>C</p>ˇ
15673 "#
15674 .unindent(),
15675 );
15676 cx.update_editor(|editor, window, cx| {
15677 editor.toggle_comments(&ToggleComments::default(), window, cx)
15678 });
15679 cx.assert_editor_state(
15680 &r#"
15681 <!-- <p>A</p>ˇ -->
15682 <!-- <p>B</p>ˇ -->
15683 <!-- <p>C</p>ˇ -->
15684 "#
15685 .unindent(),
15686 );
15687 cx.update_editor(|editor, window, cx| {
15688 editor.toggle_comments(&ToggleComments::default(), window, cx)
15689 });
15690 cx.assert_editor_state(
15691 &r#"
15692 <p>A</p>ˇ
15693 <p>B</p>ˇ
15694 <p>C</p>ˇ
15695 "#
15696 .unindent(),
15697 );
15698
15699 // Toggle comments for mixture of empty and non-empty selections, where
15700 // multiple selections occupy a given line.
15701 cx.set_state(
15702 &r#"
15703 <p>A«</p>
15704 <p>ˇ»B</p>ˇ
15705 <p>C«</p>
15706 <p>ˇ»D</p>ˇ
15707 "#
15708 .unindent(),
15709 );
15710
15711 cx.update_editor(|editor, window, cx| {
15712 editor.toggle_comments(&ToggleComments::default(), window, cx)
15713 });
15714 cx.assert_editor_state(
15715 &r#"
15716 <!-- <p>A«</p>
15717 <p>ˇ»B</p>ˇ -->
15718 <!-- <p>C«</p>
15719 <p>ˇ»D</p>ˇ -->
15720 "#
15721 .unindent(),
15722 );
15723 cx.update_editor(|editor, window, cx| {
15724 editor.toggle_comments(&ToggleComments::default(), window, cx)
15725 });
15726 cx.assert_editor_state(
15727 &r#"
15728 <p>A«</p>
15729 <p>ˇ»B</p>ˇ
15730 <p>C«</p>
15731 <p>ˇ»D</p>ˇ
15732 "#
15733 .unindent(),
15734 );
15735
15736 // Toggle comments when different languages are active for different
15737 // selections.
15738 cx.set_state(
15739 &r#"
15740 ˇ<script>
15741 ˇvar x = new Y();
15742 ˇ</script>
15743 "#
15744 .unindent(),
15745 );
15746 cx.executor().run_until_parked();
15747 cx.update_editor(|editor, window, cx| {
15748 editor.toggle_comments(&ToggleComments::default(), window, cx)
15749 });
15750 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
15751 // Uncommenting and commenting from this position brings in even more wrong artifacts.
15752 cx.assert_editor_state(
15753 &r#"
15754 <!-- ˇ<script> -->
15755 // ˇvar x = new Y();
15756 <!-- ˇ</script> -->
15757 "#
15758 .unindent(),
15759 );
15760}
15761
15762#[gpui::test]
15763fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
15764 init_test(cx, |_| {});
15765
15766 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
15767 let multibuffer = cx.new(|cx| {
15768 let mut multibuffer = MultiBuffer::new(ReadWrite);
15769 multibuffer.push_excerpts(
15770 buffer.clone(),
15771 [
15772 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
15773 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
15774 ],
15775 cx,
15776 );
15777 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
15778 multibuffer
15779 });
15780
15781 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
15782 editor.update_in(cx, |editor, window, cx| {
15783 assert_eq!(editor.text(cx), "aaaa\nbbbb");
15784 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15785 s.select_ranges([
15786 Point::new(0, 0)..Point::new(0, 0),
15787 Point::new(1, 0)..Point::new(1, 0),
15788 ])
15789 });
15790
15791 editor.handle_input("X", window, cx);
15792 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
15793 assert_eq!(
15794 editor.selections.ranges(cx),
15795 [
15796 Point::new(0, 1)..Point::new(0, 1),
15797 Point::new(1, 1)..Point::new(1, 1),
15798 ]
15799 );
15800
15801 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
15802 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15803 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
15804 });
15805 editor.backspace(&Default::default(), window, cx);
15806 assert_eq!(editor.text(cx), "Xa\nbbb");
15807 assert_eq!(
15808 editor.selections.ranges(cx),
15809 [Point::new(1, 0)..Point::new(1, 0)]
15810 );
15811
15812 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15813 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
15814 });
15815 editor.backspace(&Default::default(), window, cx);
15816 assert_eq!(editor.text(cx), "X\nbb");
15817 assert_eq!(
15818 editor.selections.ranges(cx),
15819 [Point::new(0, 1)..Point::new(0, 1)]
15820 );
15821 });
15822}
15823
15824#[gpui::test]
15825fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
15826 init_test(cx, |_| {});
15827
15828 let markers = vec![('[', ']').into(), ('(', ')').into()];
15829 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
15830 indoc! {"
15831 [aaaa
15832 (bbbb]
15833 cccc)",
15834 },
15835 markers.clone(),
15836 );
15837 let excerpt_ranges = markers.into_iter().map(|marker| {
15838 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
15839 ExcerptRange::new(context)
15840 });
15841 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
15842 let multibuffer = cx.new(|cx| {
15843 let mut multibuffer = MultiBuffer::new(ReadWrite);
15844 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
15845 multibuffer
15846 });
15847
15848 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
15849 editor.update_in(cx, |editor, window, cx| {
15850 let (expected_text, selection_ranges) = marked_text_ranges(
15851 indoc! {"
15852 aaaa
15853 bˇbbb
15854 bˇbbˇb
15855 cccc"
15856 },
15857 true,
15858 );
15859 assert_eq!(editor.text(cx), expected_text);
15860 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15861 s.select_ranges(selection_ranges)
15862 });
15863
15864 editor.handle_input("X", window, cx);
15865
15866 let (expected_text, expected_selections) = marked_text_ranges(
15867 indoc! {"
15868 aaaa
15869 bXˇbbXb
15870 bXˇbbXˇb
15871 cccc"
15872 },
15873 false,
15874 );
15875 assert_eq!(editor.text(cx), expected_text);
15876 assert_eq!(editor.selections.ranges(cx), expected_selections);
15877
15878 editor.newline(&Newline, window, cx);
15879 let (expected_text, expected_selections) = marked_text_ranges(
15880 indoc! {"
15881 aaaa
15882 bX
15883 ˇbbX
15884 b
15885 bX
15886 ˇbbX
15887 ˇb
15888 cccc"
15889 },
15890 false,
15891 );
15892 assert_eq!(editor.text(cx), expected_text);
15893 assert_eq!(editor.selections.ranges(cx), expected_selections);
15894 });
15895}
15896
15897#[gpui::test]
15898fn test_refresh_selections(cx: &mut TestAppContext) {
15899 init_test(cx, |_| {});
15900
15901 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
15902 let mut excerpt1_id = None;
15903 let multibuffer = cx.new(|cx| {
15904 let mut multibuffer = MultiBuffer::new(ReadWrite);
15905 excerpt1_id = multibuffer
15906 .push_excerpts(
15907 buffer.clone(),
15908 [
15909 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
15910 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
15911 ],
15912 cx,
15913 )
15914 .into_iter()
15915 .next();
15916 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
15917 multibuffer
15918 });
15919
15920 let editor = cx.add_window(|window, cx| {
15921 let mut editor = build_editor(multibuffer.clone(), window, cx);
15922 let snapshot = editor.snapshot(window, cx);
15923 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15924 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
15925 });
15926 editor.begin_selection(
15927 Point::new(2, 1).to_display_point(&snapshot),
15928 true,
15929 1,
15930 window,
15931 cx,
15932 );
15933 assert_eq!(
15934 editor.selections.ranges(cx),
15935 [
15936 Point::new(1, 3)..Point::new(1, 3),
15937 Point::new(2, 1)..Point::new(2, 1),
15938 ]
15939 );
15940 editor
15941 });
15942
15943 // Refreshing selections is a no-op when excerpts haven't changed.
15944 _ = editor.update(cx, |editor, window, cx| {
15945 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
15946 assert_eq!(
15947 editor.selections.ranges(cx),
15948 [
15949 Point::new(1, 3)..Point::new(1, 3),
15950 Point::new(2, 1)..Point::new(2, 1),
15951 ]
15952 );
15953 });
15954
15955 multibuffer.update(cx, |multibuffer, cx| {
15956 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
15957 });
15958 _ = editor.update(cx, |editor, window, cx| {
15959 // Removing an excerpt causes the first selection to become degenerate.
15960 assert_eq!(
15961 editor.selections.ranges(cx),
15962 [
15963 Point::new(0, 0)..Point::new(0, 0),
15964 Point::new(0, 1)..Point::new(0, 1)
15965 ]
15966 );
15967
15968 // Refreshing selections will relocate the first selection to the original buffer
15969 // location.
15970 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
15971 assert_eq!(
15972 editor.selections.ranges(cx),
15973 [
15974 Point::new(0, 1)..Point::new(0, 1),
15975 Point::new(0, 3)..Point::new(0, 3)
15976 ]
15977 );
15978 assert!(editor.selections.pending_anchor().is_some());
15979 });
15980}
15981
15982#[gpui::test]
15983fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
15984 init_test(cx, |_| {});
15985
15986 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
15987 let mut excerpt1_id = None;
15988 let multibuffer = cx.new(|cx| {
15989 let mut multibuffer = MultiBuffer::new(ReadWrite);
15990 excerpt1_id = multibuffer
15991 .push_excerpts(
15992 buffer.clone(),
15993 [
15994 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
15995 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
15996 ],
15997 cx,
15998 )
15999 .into_iter()
16000 .next();
16001 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
16002 multibuffer
16003 });
16004
16005 let editor = cx.add_window(|window, cx| {
16006 let mut editor = build_editor(multibuffer.clone(), window, cx);
16007 let snapshot = editor.snapshot(window, cx);
16008 editor.begin_selection(
16009 Point::new(1, 3).to_display_point(&snapshot),
16010 false,
16011 1,
16012 window,
16013 cx,
16014 );
16015 assert_eq!(
16016 editor.selections.ranges(cx),
16017 [Point::new(1, 3)..Point::new(1, 3)]
16018 );
16019 editor
16020 });
16021
16022 multibuffer.update(cx, |multibuffer, cx| {
16023 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
16024 });
16025 _ = editor.update(cx, |editor, window, cx| {
16026 assert_eq!(
16027 editor.selections.ranges(cx),
16028 [Point::new(0, 0)..Point::new(0, 0)]
16029 );
16030
16031 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
16032 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16033 assert_eq!(
16034 editor.selections.ranges(cx),
16035 [Point::new(0, 3)..Point::new(0, 3)]
16036 );
16037 assert!(editor.selections.pending_anchor().is_some());
16038 });
16039}
16040
16041#[gpui::test]
16042async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
16043 init_test(cx, |_| {});
16044
16045 let language = Arc::new(
16046 Language::new(
16047 LanguageConfig {
16048 brackets: BracketPairConfig {
16049 pairs: vec![
16050 BracketPair {
16051 start: "{".to_string(),
16052 end: "}".to_string(),
16053 close: true,
16054 surround: true,
16055 newline: true,
16056 },
16057 BracketPair {
16058 start: "/* ".to_string(),
16059 end: " */".to_string(),
16060 close: true,
16061 surround: true,
16062 newline: true,
16063 },
16064 ],
16065 ..Default::default()
16066 },
16067 ..Default::default()
16068 },
16069 Some(tree_sitter_rust::LANGUAGE.into()),
16070 )
16071 .with_indents_query("")
16072 .unwrap(),
16073 );
16074
16075 let text = concat!(
16076 "{ }\n", //
16077 " x\n", //
16078 " /* */\n", //
16079 "x\n", //
16080 "{{} }\n", //
16081 );
16082
16083 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
16084 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
16085 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
16086 editor
16087 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
16088 .await;
16089
16090 editor.update_in(cx, |editor, window, cx| {
16091 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16092 s.select_display_ranges([
16093 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
16094 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
16095 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
16096 ])
16097 });
16098 editor.newline(&Newline, window, cx);
16099
16100 assert_eq!(
16101 editor.buffer().read(cx).read(cx).text(),
16102 concat!(
16103 "{ \n", // Suppress rustfmt
16104 "\n", //
16105 "}\n", //
16106 " x\n", //
16107 " /* \n", //
16108 " \n", //
16109 " */\n", //
16110 "x\n", //
16111 "{{} \n", //
16112 "}\n", //
16113 )
16114 );
16115 });
16116}
16117
16118#[gpui::test]
16119fn test_highlighted_ranges(cx: &mut TestAppContext) {
16120 init_test(cx, |_| {});
16121
16122 let editor = cx.add_window(|window, cx| {
16123 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
16124 build_editor(buffer, window, cx)
16125 });
16126
16127 _ = editor.update(cx, |editor, window, cx| {
16128 struct Type1;
16129 struct Type2;
16130
16131 let buffer = editor.buffer.read(cx).snapshot(cx);
16132
16133 let anchor_range =
16134 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
16135
16136 editor.highlight_background::<Type1>(
16137 &[
16138 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
16139 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
16140 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
16141 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
16142 ],
16143 |_| Hsla::red(),
16144 cx,
16145 );
16146 editor.highlight_background::<Type2>(
16147 &[
16148 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
16149 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
16150 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
16151 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
16152 ],
16153 |_| Hsla::green(),
16154 cx,
16155 );
16156
16157 let snapshot = editor.snapshot(window, cx);
16158 let highlighted_ranges = editor.sorted_background_highlights_in_range(
16159 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
16160 &snapshot,
16161 cx.theme(),
16162 );
16163 assert_eq!(
16164 highlighted_ranges,
16165 &[
16166 (
16167 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
16168 Hsla::green(),
16169 ),
16170 (
16171 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
16172 Hsla::red(),
16173 ),
16174 (
16175 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
16176 Hsla::green(),
16177 ),
16178 (
16179 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16180 Hsla::red(),
16181 ),
16182 ]
16183 );
16184 assert_eq!(
16185 editor.sorted_background_highlights_in_range(
16186 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
16187 &snapshot,
16188 cx.theme(),
16189 ),
16190 &[(
16191 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16192 Hsla::red(),
16193 )]
16194 );
16195 });
16196}
16197
16198#[gpui::test]
16199async fn test_following(cx: &mut TestAppContext) {
16200 init_test(cx, |_| {});
16201
16202 let fs = FakeFs::new(cx.executor());
16203 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16204
16205 let buffer = project.update(cx, |project, cx| {
16206 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, false, cx);
16207 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
16208 });
16209 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
16210 let follower = cx.update(|cx| {
16211 cx.open_window(
16212 WindowOptions {
16213 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
16214 gpui::Point::new(px(0.), px(0.)),
16215 gpui::Point::new(px(10.), px(80.)),
16216 ))),
16217 ..Default::default()
16218 },
16219 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
16220 )
16221 .unwrap()
16222 });
16223
16224 let is_still_following = Rc::new(RefCell::new(true));
16225 let follower_edit_event_count = Rc::new(RefCell::new(0));
16226 let pending_update = Rc::new(RefCell::new(None));
16227 let leader_entity = leader.root(cx).unwrap();
16228 let follower_entity = follower.root(cx).unwrap();
16229 _ = follower.update(cx, {
16230 let update = pending_update.clone();
16231 let is_still_following = is_still_following.clone();
16232 let follower_edit_event_count = follower_edit_event_count.clone();
16233 |_, window, cx| {
16234 cx.subscribe_in(
16235 &leader_entity,
16236 window,
16237 move |_, leader, event, window, cx| {
16238 leader.read(cx).add_event_to_update_proto(
16239 event,
16240 &mut update.borrow_mut(),
16241 window,
16242 cx,
16243 );
16244 },
16245 )
16246 .detach();
16247
16248 cx.subscribe_in(
16249 &follower_entity,
16250 window,
16251 move |_, _, event: &EditorEvent, _window, _cx| {
16252 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
16253 *is_still_following.borrow_mut() = false;
16254 }
16255
16256 if let EditorEvent::BufferEdited = event {
16257 *follower_edit_event_count.borrow_mut() += 1;
16258 }
16259 },
16260 )
16261 .detach();
16262 }
16263 });
16264
16265 // Update the selections only
16266 _ = leader.update(cx, |leader, window, cx| {
16267 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16268 s.select_ranges([1..1])
16269 });
16270 });
16271 follower
16272 .update(cx, |follower, window, cx| {
16273 follower.apply_update_proto(
16274 &project,
16275 pending_update.borrow_mut().take().unwrap(),
16276 window,
16277 cx,
16278 )
16279 })
16280 .unwrap()
16281 .await
16282 .unwrap();
16283 _ = follower.update(cx, |follower, _, cx| {
16284 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
16285 });
16286 assert!(*is_still_following.borrow());
16287 assert_eq!(*follower_edit_event_count.borrow(), 0);
16288
16289 // Update the scroll position only
16290 _ = leader.update(cx, |leader, window, cx| {
16291 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16292 });
16293 follower
16294 .update(cx, |follower, window, cx| {
16295 follower.apply_update_proto(
16296 &project,
16297 pending_update.borrow_mut().take().unwrap(),
16298 window,
16299 cx,
16300 )
16301 })
16302 .unwrap()
16303 .await
16304 .unwrap();
16305 assert_eq!(
16306 follower
16307 .update(cx, |follower, _, cx| follower.scroll_position(cx))
16308 .unwrap(),
16309 gpui::Point::new(1.5, 3.5)
16310 );
16311 assert!(*is_still_following.borrow());
16312 assert_eq!(*follower_edit_event_count.borrow(), 0);
16313
16314 // Update the selections and scroll position. The follower's scroll position is updated
16315 // via autoscroll, not via the leader's exact scroll position.
16316 _ = leader.update(cx, |leader, window, cx| {
16317 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16318 s.select_ranges([0..0])
16319 });
16320 leader.request_autoscroll(Autoscroll::newest(), cx);
16321 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16322 });
16323 follower
16324 .update(cx, |follower, window, cx| {
16325 follower.apply_update_proto(
16326 &project,
16327 pending_update.borrow_mut().take().unwrap(),
16328 window,
16329 cx,
16330 )
16331 })
16332 .unwrap()
16333 .await
16334 .unwrap();
16335 _ = follower.update(cx, |follower, _, cx| {
16336 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
16337 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
16338 });
16339 assert!(*is_still_following.borrow());
16340
16341 // Creating a pending selection that precedes another selection
16342 _ = leader.update(cx, |leader, window, cx| {
16343 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16344 s.select_ranges([1..1])
16345 });
16346 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
16347 });
16348 follower
16349 .update(cx, |follower, window, cx| {
16350 follower.apply_update_proto(
16351 &project,
16352 pending_update.borrow_mut().take().unwrap(),
16353 window,
16354 cx,
16355 )
16356 })
16357 .unwrap()
16358 .await
16359 .unwrap();
16360 _ = follower.update(cx, |follower, _, cx| {
16361 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
16362 });
16363 assert!(*is_still_following.borrow());
16364
16365 // Extend the pending selection so that it surrounds another selection
16366 _ = leader.update(cx, |leader, window, cx| {
16367 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
16368 });
16369 follower
16370 .update(cx, |follower, window, cx| {
16371 follower.apply_update_proto(
16372 &project,
16373 pending_update.borrow_mut().take().unwrap(),
16374 window,
16375 cx,
16376 )
16377 })
16378 .unwrap()
16379 .await
16380 .unwrap();
16381 _ = follower.update(cx, |follower, _, cx| {
16382 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
16383 });
16384
16385 // Scrolling locally breaks the follow
16386 _ = follower.update(cx, |follower, window, cx| {
16387 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
16388 follower.set_scroll_anchor(
16389 ScrollAnchor {
16390 anchor: top_anchor,
16391 offset: gpui::Point::new(0.0, 0.5),
16392 },
16393 window,
16394 cx,
16395 );
16396 });
16397 assert!(!(*is_still_following.borrow()));
16398}
16399
16400#[gpui::test]
16401async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
16402 init_test(cx, |_| {});
16403
16404 let fs = FakeFs::new(cx.executor());
16405 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16406 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16407 let pane = workspace
16408 .update(cx, |workspace, _, _| workspace.active_pane().clone())
16409 .unwrap();
16410
16411 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16412
16413 let leader = pane.update_in(cx, |_, window, cx| {
16414 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
16415 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
16416 });
16417
16418 // Start following the editor when it has no excerpts.
16419 let mut state_message =
16420 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16421 let workspace_entity = workspace.root(cx).unwrap();
16422 let follower_1 = cx
16423 .update_window(*workspace.deref(), |_, window, cx| {
16424 Editor::from_state_proto(
16425 workspace_entity,
16426 ViewId {
16427 creator: CollaboratorId::PeerId(PeerId::default()),
16428 id: 0,
16429 },
16430 &mut state_message,
16431 window,
16432 cx,
16433 )
16434 })
16435 .unwrap()
16436 .unwrap()
16437 .await
16438 .unwrap();
16439
16440 let update_message = Rc::new(RefCell::new(None));
16441 follower_1.update_in(cx, {
16442 let update = update_message.clone();
16443 |_, window, cx| {
16444 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
16445 leader.read(cx).add_event_to_update_proto(
16446 event,
16447 &mut update.borrow_mut(),
16448 window,
16449 cx,
16450 );
16451 })
16452 .detach();
16453 }
16454 });
16455
16456 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
16457 (
16458 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, false, cx),
16459 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, false, cx),
16460 )
16461 });
16462
16463 // Insert some excerpts.
16464 leader.update(cx, |leader, cx| {
16465 leader.buffer.update(cx, |multibuffer, cx| {
16466 multibuffer.set_excerpts_for_path(
16467 PathKey::namespaced(1, rel_path("b.txt").into_arc()),
16468 buffer_1.clone(),
16469 vec![
16470 Point::row_range(0..3),
16471 Point::row_range(1..6),
16472 Point::row_range(12..15),
16473 ],
16474 0,
16475 cx,
16476 );
16477 multibuffer.set_excerpts_for_path(
16478 PathKey::namespaced(1, rel_path("a.txt").into_arc()),
16479 buffer_2.clone(),
16480 vec![Point::row_range(0..6), Point::row_range(8..12)],
16481 0,
16482 cx,
16483 );
16484 });
16485 });
16486
16487 // Apply the update of adding the excerpts.
16488 follower_1
16489 .update_in(cx, |follower, window, cx| {
16490 follower.apply_update_proto(
16491 &project,
16492 update_message.borrow().clone().unwrap(),
16493 window,
16494 cx,
16495 )
16496 })
16497 .await
16498 .unwrap();
16499 assert_eq!(
16500 follower_1.update(cx, |editor, cx| editor.text(cx)),
16501 leader.update(cx, |editor, cx| editor.text(cx))
16502 );
16503 update_message.borrow_mut().take();
16504
16505 // Start following separately after it already has excerpts.
16506 let mut state_message =
16507 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16508 let workspace_entity = workspace.root(cx).unwrap();
16509 let follower_2 = cx
16510 .update_window(*workspace.deref(), |_, window, cx| {
16511 Editor::from_state_proto(
16512 workspace_entity,
16513 ViewId {
16514 creator: CollaboratorId::PeerId(PeerId::default()),
16515 id: 0,
16516 },
16517 &mut state_message,
16518 window,
16519 cx,
16520 )
16521 })
16522 .unwrap()
16523 .unwrap()
16524 .await
16525 .unwrap();
16526 assert_eq!(
16527 follower_2.update(cx, |editor, cx| editor.text(cx)),
16528 leader.update(cx, |editor, cx| editor.text(cx))
16529 );
16530
16531 // Remove some excerpts.
16532 leader.update(cx, |leader, cx| {
16533 leader.buffer.update(cx, |multibuffer, cx| {
16534 let excerpt_ids = multibuffer.excerpt_ids();
16535 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
16536 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
16537 });
16538 });
16539
16540 // Apply the update of removing the excerpts.
16541 follower_1
16542 .update_in(cx, |follower, window, cx| {
16543 follower.apply_update_proto(
16544 &project,
16545 update_message.borrow().clone().unwrap(),
16546 window,
16547 cx,
16548 )
16549 })
16550 .await
16551 .unwrap();
16552 follower_2
16553 .update_in(cx, |follower, window, cx| {
16554 follower.apply_update_proto(
16555 &project,
16556 update_message.borrow().clone().unwrap(),
16557 window,
16558 cx,
16559 )
16560 })
16561 .await
16562 .unwrap();
16563 update_message.borrow_mut().take();
16564 assert_eq!(
16565 follower_1.update(cx, |editor, cx| editor.text(cx)),
16566 leader.update(cx, |editor, cx| editor.text(cx))
16567 );
16568}
16569
16570#[gpui::test]
16571async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16572 init_test(cx, |_| {});
16573
16574 let mut cx = EditorTestContext::new(cx).await;
16575 let lsp_store =
16576 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
16577
16578 cx.set_state(indoc! {"
16579 ˇfn func(abc def: i32) -> u32 {
16580 }
16581 "});
16582
16583 cx.update(|_, cx| {
16584 lsp_store.update(cx, |lsp_store, cx| {
16585 lsp_store
16586 .update_diagnostics(
16587 LanguageServerId(0),
16588 lsp::PublishDiagnosticsParams {
16589 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
16590 version: None,
16591 diagnostics: vec![
16592 lsp::Diagnostic {
16593 range: lsp::Range::new(
16594 lsp::Position::new(0, 11),
16595 lsp::Position::new(0, 12),
16596 ),
16597 severity: Some(lsp::DiagnosticSeverity::ERROR),
16598 ..Default::default()
16599 },
16600 lsp::Diagnostic {
16601 range: lsp::Range::new(
16602 lsp::Position::new(0, 12),
16603 lsp::Position::new(0, 15),
16604 ),
16605 severity: Some(lsp::DiagnosticSeverity::ERROR),
16606 ..Default::default()
16607 },
16608 lsp::Diagnostic {
16609 range: lsp::Range::new(
16610 lsp::Position::new(0, 25),
16611 lsp::Position::new(0, 28),
16612 ),
16613 severity: Some(lsp::DiagnosticSeverity::ERROR),
16614 ..Default::default()
16615 },
16616 ],
16617 },
16618 None,
16619 DiagnosticSourceKind::Pushed,
16620 &[],
16621 cx,
16622 )
16623 .unwrap()
16624 });
16625 });
16626
16627 executor.run_until_parked();
16628
16629 cx.update_editor(|editor, window, cx| {
16630 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16631 });
16632
16633 cx.assert_editor_state(indoc! {"
16634 fn func(abc def: i32) -> ˇu32 {
16635 }
16636 "});
16637
16638 cx.update_editor(|editor, window, cx| {
16639 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16640 });
16641
16642 cx.assert_editor_state(indoc! {"
16643 fn func(abc ˇdef: i32) -> u32 {
16644 }
16645 "});
16646
16647 cx.update_editor(|editor, window, cx| {
16648 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16649 });
16650
16651 cx.assert_editor_state(indoc! {"
16652 fn func(abcˇ def: i32) -> u32 {
16653 }
16654 "});
16655
16656 cx.update_editor(|editor, window, cx| {
16657 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16658 });
16659
16660 cx.assert_editor_state(indoc! {"
16661 fn func(abc def: i32) -> ˇu32 {
16662 }
16663 "});
16664}
16665
16666#[gpui::test]
16667async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16668 init_test(cx, |_| {});
16669
16670 let mut cx = EditorTestContext::new(cx).await;
16671
16672 let diff_base = r#"
16673 use some::mod;
16674
16675 const A: u32 = 42;
16676
16677 fn main() {
16678 println!("hello");
16679
16680 println!("world");
16681 }
16682 "#
16683 .unindent();
16684
16685 // Edits are modified, removed, modified, added
16686 cx.set_state(
16687 &r#"
16688 use some::modified;
16689
16690 ˇ
16691 fn main() {
16692 println!("hello there");
16693
16694 println!("around the");
16695 println!("world");
16696 }
16697 "#
16698 .unindent(),
16699 );
16700
16701 cx.set_head_text(&diff_base);
16702 executor.run_until_parked();
16703
16704 cx.update_editor(|editor, window, cx| {
16705 //Wrap around the bottom of the buffer
16706 for _ in 0..3 {
16707 editor.go_to_next_hunk(&GoToHunk, window, cx);
16708 }
16709 });
16710
16711 cx.assert_editor_state(
16712 &r#"
16713 ˇuse some::modified;
16714
16715
16716 fn main() {
16717 println!("hello there");
16718
16719 println!("around the");
16720 println!("world");
16721 }
16722 "#
16723 .unindent(),
16724 );
16725
16726 cx.update_editor(|editor, window, cx| {
16727 //Wrap around the top of the buffer
16728 for _ in 0..2 {
16729 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16730 }
16731 });
16732
16733 cx.assert_editor_state(
16734 &r#"
16735 use some::modified;
16736
16737
16738 fn main() {
16739 ˇ println!("hello there");
16740
16741 println!("around the");
16742 println!("world");
16743 }
16744 "#
16745 .unindent(),
16746 );
16747
16748 cx.update_editor(|editor, window, cx| {
16749 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16750 });
16751
16752 cx.assert_editor_state(
16753 &r#"
16754 use some::modified;
16755
16756 ˇ
16757 fn main() {
16758 println!("hello there");
16759
16760 println!("around the");
16761 println!("world");
16762 }
16763 "#
16764 .unindent(),
16765 );
16766
16767 cx.update_editor(|editor, window, cx| {
16768 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16769 });
16770
16771 cx.assert_editor_state(
16772 &r#"
16773 ˇuse some::modified;
16774
16775
16776 fn main() {
16777 println!("hello there");
16778
16779 println!("around the");
16780 println!("world");
16781 }
16782 "#
16783 .unindent(),
16784 );
16785
16786 cx.update_editor(|editor, window, cx| {
16787 for _ in 0..2 {
16788 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16789 }
16790 });
16791
16792 cx.assert_editor_state(
16793 &r#"
16794 use some::modified;
16795
16796
16797 fn main() {
16798 ˇ println!("hello there");
16799
16800 println!("around the");
16801 println!("world");
16802 }
16803 "#
16804 .unindent(),
16805 );
16806
16807 cx.update_editor(|editor, window, cx| {
16808 editor.fold(&Fold, window, cx);
16809 });
16810
16811 cx.update_editor(|editor, window, cx| {
16812 editor.go_to_next_hunk(&GoToHunk, window, cx);
16813 });
16814
16815 cx.assert_editor_state(
16816 &r#"
16817 ˇuse some::modified;
16818
16819
16820 fn main() {
16821 println!("hello there");
16822
16823 println!("around the");
16824 println!("world");
16825 }
16826 "#
16827 .unindent(),
16828 );
16829}
16830
16831#[test]
16832fn test_split_words() {
16833 fn split(text: &str) -> Vec<&str> {
16834 split_words(text).collect()
16835 }
16836
16837 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
16838 assert_eq!(split("hello_world"), &["hello_", "world"]);
16839 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
16840 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
16841 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
16842 assert_eq!(split("helloworld"), &["helloworld"]);
16843
16844 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
16845}
16846
16847#[gpui::test]
16848async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
16849 init_test(cx, |_| {});
16850
16851 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
16852 let mut assert = |before, after| {
16853 let _state_context = cx.set_state(before);
16854 cx.run_until_parked();
16855 cx.update_editor(|editor, window, cx| {
16856 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
16857 });
16858 cx.run_until_parked();
16859 cx.assert_editor_state(after);
16860 };
16861
16862 // Outside bracket jumps to outside of matching bracket
16863 assert("console.logˇ(var);", "console.log(var)ˇ;");
16864 assert("console.log(var)ˇ;", "console.logˇ(var);");
16865
16866 // Inside bracket jumps to inside of matching bracket
16867 assert("console.log(ˇvar);", "console.log(varˇ);");
16868 assert("console.log(varˇ);", "console.log(ˇvar);");
16869
16870 // When outside a bracket and inside, favor jumping to the inside bracket
16871 assert(
16872 "console.log('foo', [1, 2, 3]ˇ);",
16873 "console.log(ˇ'foo', [1, 2, 3]);",
16874 );
16875 assert(
16876 "console.log(ˇ'foo', [1, 2, 3]);",
16877 "console.log('foo', [1, 2, 3]ˇ);",
16878 );
16879
16880 // Bias forward if two options are equally likely
16881 assert(
16882 "let result = curried_fun()ˇ();",
16883 "let result = curried_fun()()ˇ;",
16884 );
16885
16886 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
16887 assert(
16888 indoc! {"
16889 function test() {
16890 console.log('test')ˇ
16891 }"},
16892 indoc! {"
16893 function test() {
16894 console.logˇ('test')
16895 }"},
16896 );
16897}
16898
16899#[gpui::test]
16900async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
16901 init_test(cx, |_| {});
16902
16903 let fs = FakeFs::new(cx.executor());
16904 fs.insert_tree(
16905 path!("/a"),
16906 json!({
16907 "main.rs": "fn main() { let a = 5; }",
16908 "other.rs": "// Test file",
16909 }),
16910 )
16911 .await;
16912 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
16913
16914 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
16915 language_registry.add(Arc::new(Language::new(
16916 LanguageConfig {
16917 name: "Rust".into(),
16918 matcher: LanguageMatcher {
16919 path_suffixes: vec!["rs".to_string()],
16920 ..Default::default()
16921 },
16922 brackets: BracketPairConfig {
16923 pairs: vec![BracketPair {
16924 start: "{".to_string(),
16925 end: "}".to_string(),
16926 close: true,
16927 surround: true,
16928 newline: true,
16929 }],
16930 disabled_scopes_by_bracket_ix: Vec::new(),
16931 },
16932 ..Default::default()
16933 },
16934 Some(tree_sitter_rust::LANGUAGE.into()),
16935 )));
16936 let mut fake_servers = language_registry.register_fake_lsp(
16937 "Rust",
16938 FakeLspAdapter {
16939 capabilities: lsp::ServerCapabilities {
16940 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
16941 first_trigger_character: "{".to_string(),
16942 more_trigger_character: None,
16943 }),
16944 ..Default::default()
16945 },
16946 ..Default::default()
16947 },
16948 );
16949
16950 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16951
16952 let cx = &mut VisualTestContext::from_window(*workspace, cx);
16953
16954 let worktree_id = workspace
16955 .update(cx, |workspace, _, cx| {
16956 workspace.project().update(cx, |project, cx| {
16957 project.worktrees(cx).next().unwrap().read(cx).id()
16958 })
16959 })
16960 .unwrap();
16961
16962 let buffer = project
16963 .update(cx, |project, cx| {
16964 project.open_local_buffer(path!("/a/main.rs"), cx)
16965 })
16966 .await
16967 .unwrap();
16968 let editor_handle = workspace
16969 .update(cx, |workspace, window, cx| {
16970 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
16971 })
16972 .unwrap()
16973 .await
16974 .unwrap()
16975 .downcast::<Editor>()
16976 .unwrap();
16977
16978 cx.executor().start_waiting();
16979 let fake_server = fake_servers.next().await.unwrap();
16980
16981 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
16982 |params, _| async move {
16983 assert_eq!(
16984 params.text_document_position.text_document.uri,
16985 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
16986 );
16987 assert_eq!(
16988 params.text_document_position.position,
16989 lsp::Position::new(0, 21),
16990 );
16991
16992 Ok(Some(vec![lsp::TextEdit {
16993 new_text: "]".to_string(),
16994 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
16995 }]))
16996 },
16997 );
16998
16999 editor_handle.update_in(cx, |editor, window, cx| {
17000 window.focus(&editor.focus_handle(cx));
17001 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17002 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
17003 });
17004 editor.handle_input("{", window, cx);
17005 });
17006
17007 cx.executor().run_until_parked();
17008
17009 buffer.update(cx, |buffer, _| {
17010 assert_eq!(
17011 buffer.text(),
17012 "fn main() { let a = {5}; }",
17013 "No extra braces from on type formatting should appear in the buffer"
17014 )
17015 });
17016}
17017
17018#[gpui::test(iterations = 20, seeds(31))]
17019async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
17020 init_test(cx, |_| {});
17021
17022 let mut cx = EditorLspTestContext::new_rust(
17023 lsp::ServerCapabilities {
17024 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
17025 first_trigger_character: ".".to_string(),
17026 more_trigger_character: None,
17027 }),
17028 ..Default::default()
17029 },
17030 cx,
17031 )
17032 .await;
17033
17034 cx.update_buffer(|buffer, _| {
17035 // This causes autoindent to be async.
17036 buffer.set_sync_parse_timeout(Duration::ZERO)
17037 });
17038
17039 cx.set_state("fn c() {\n d()ˇ\n}\n");
17040 cx.simulate_keystroke("\n");
17041 cx.run_until_parked();
17042
17043 let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap());
17044 let mut request =
17045 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
17046 let buffer_cloned = buffer_cloned.clone();
17047 async move {
17048 buffer_cloned.update(&mut cx, |buffer, _| {
17049 assert_eq!(
17050 buffer.text(),
17051 "fn c() {\n d()\n .\n}\n",
17052 "OnTypeFormatting should triggered after autoindent applied"
17053 )
17054 })?;
17055
17056 Ok(Some(vec![]))
17057 }
17058 });
17059
17060 cx.simulate_keystroke(".");
17061 cx.run_until_parked();
17062
17063 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
17064 assert!(request.next().await.is_some());
17065 request.close();
17066 assert!(request.next().await.is_none());
17067}
17068
17069#[gpui::test]
17070async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
17071 init_test(cx, |_| {});
17072
17073 let fs = FakeFs::new(cx.executor());
17074 fs.insert_tree(
17075 path!("/a"),
17076 json!({
17077 "main.rs": "fn main() { let a = 5; }",
17078 "other.rs": "// Test file",
17079 }),
17080 )
17081 .await;
17082
17083 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17084
17085 let server_restarts = Arc::new(AtomicUsize::new(0));
17086 let closure_restarts = Arc::clone(&server_restarts);
17087 let language_server_name = "test language server";
17088 let language_name: LanguageName = "Rust".into();
17089
17090 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17091 language_registry.add(Arc::new(Language::new(
17092 LanguageConfig {
17093 name: language_name.clone(),
17094 matcher: LanguageMatcher {
17095 path_suffixes: vec!["rs".to_string()],
17096 ..Default::default()
17097 },
17098 ..Default::default()
17099 },
17100 Some(tree_sitter_rust::LANGUAGE.into()),
17101 )));
17102 let mut fake_servers = language_registry.register_fake_lsp(
17103 "Rust",
17104 FakeLspAdapter {
17105 name: language_server_name,
17106 initialization_options: Some(json!({
17107 "testOptionValue": true
17108 })),
17109 initializer: Some(Box::new(move |fake_server| {
17110 let task_restarts = Arc::clone(&closure_restarts);
17111 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
17112 task_restarts.fetch_add(1, atomic::Ordering::Release);
17113 futures::future::ready(Ok(()))
17114 });
17115 })),
17116 ..Default::default()
17117 },
17118 );
17119
17120 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17121 let _buffer = project
17122 .update(cx, |project, cx| {
17123 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
17124 })
17125 .await
17126 .unwrap();
17127 let _fake_server = fake_servers.next().await.unwrap();
17128 update_test_language_settings(cx, |language_settings| {
17129 language_settings.languages.0.insert(
17130 language_name.clone().0,
17131 LanguageSettingsContent {
17132 tab_size: NonZeroU32::new(8),
17133 ..Default::default()
17134 },
17135 );
17136 });
17137 cx.executor().run_until_parked();
17138 assert_eq!(
17139 server_restarts.load(atomic::Ordering::Acquire),
17140 0,
17141 "Should not restart LSP server on an unrelated change"
17142 );
17143
17144 update_test_project_settings(cx, |project_settings| {
17145 project_settings.lsp.insert(
17146 "Some other server name".into(),
17147 LspSettings {
17148 binary: None,
17149 settings: None,
17150 initialization_options: Some(json!({
17151 "some other init value": false
17152 })),
17153 enable_lsp_tasks: false,
17154 fetch: None,
17155 },
17156 );
17157 });
17158 cx.executor().run_until_parked();
17159 assert_eq!(
17160 server_restarts.load(atomic::Ordering::Acquire),
17161 0,
17162 "Should not restart LSP server on an unrelated LSP settings change"
17163 );
17164
17165 update_test_project_settings(cx, |project_settings| {
17166 project_settings.lsp.insert(
17167 language_server_name.into(),
17168 LspSettings {
17169 binary: None,
17170 settings: None,
17171 initialization_options: Some(json!({
17172 "anotherInitValue": false
17173 })),
17174 enable_lsp_tasks: false,
17175 fetch: None,
17176 },
17177 );
17178 });
17179 cx.executor().run_until_parked();
17180 assert_eq!(
17181 server_restarts.load(atomic::Ordering::Acquire),
17182 1,
17183 "Should restart LSP server on a related LSP settings change"
17184 );
17185
17186 update_test_project_settings(cx, |project_settings| {
17187 project_settings.lsp.insert(
17188 language_server_name.into(),
17189 LspSettings {
17190 binary: None,
17191 settings: None,
17192 initialization_options: Some(json!({
17193 "anotherInitValue": false
17194 })),
17195 enable_lsp_tasks: false,
17196 fetch: None,
17197 },
17198 );
17199 });
17200 cx.executor().run_until_parked();
17201 assert_eq!(
17202 server_restarts.load(atomic::Ordering::Acquire),
17203 1,
17204 "Should not restart LSP server on a related LSP settings change that is the same"
17205 );
17206
17207 update_test_project_settings(cx, |project_settings| {
17208 project_settings.lsp.insert(
17209 language_server_name.into(),
17210 LspSettings {
17211 binary: None,
17212 settings: None,
17213 initialization_options: None,
17214 enable_lsp_tasks: false,
17215 fetch: None,
17216 },
17217 );
17218 });
17219 cx.executor().run_until_parked();
17220 assert_eq!(
17221 server_restarts.load(atomic::Ordering::Acquire),
17222 2,
17223 "Should restart LSP server on another related LSP settings change"
17224 );
17225}
17226
17227#[gpui::test]
17228async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
17229 init_test(cx, |_| {});
17230
17231 let mut cx = EditorLspTestContext::new_rust(
17232 lsp::ServerCapabilities {
17233 completion_provider: Some(lsp::CompletionOptions {
17234 trigger_characters: Some(vec![".".to_string()]),
17235 resolve_provider: Some(true),
17236 ..Default::default()
17237 }),
17238 ..Default::default()
17239 },
17240 cx,
17241 )
17242 .await;
17243
17244 cx.set_state("fn main() { let a = 2ˇ; }");
17245 cx.simulate_keystroke(".");
17246 let completion_item = lsp::CompletionItem {
17247 label: "some".into(),
17248 kind: Some(lsp::CompletionItemKind::SNIPPET),
17249 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
17250 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
17251 kind: lsp::MarkupKind::Markdown,
17252 value: "```rust\nSome(2)\n```".to_string(),
17253 })),
17254 deprecated: Some(false),
17255 sort_text: Some("fffffff2".to_string()),
17256 filter_text: Some("some".to_string()),
17257 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
17258 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17259 range: lsp::Range {
17260 start: lsp::Position {
17261 line: 0,
17262 character: 22,
17263 },
17264 end: lsp::Position {
17265 line: 0,
17266 character: 22,
17267 },
17268 },
17269 new_text: "Some(2)".to_string(),
17270 })),
17271 additional_text_edits: Some(vec![lsp::TextEdit {
17272 range: lsp::Range {
17273 start: lsp::Position {
17274 line: 0,
17275 character: 20,
17276 },
17277 end: lsp::Position {
17278 line: 0,
17279 character: 22,
17280 },
17281 },
17282 new_text: "".to_string(),
17283 }]),
17284 ..Default::default()
17285 };
17286
17287 let closure_completion_item = completion_item.clone();
17288 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17289 let task_completion_item = closure_completion_item.clone();
17290 async move {
17291 Ok(Some(lsp::CompletionResponse::Array(vec![
17292 task_completion_item,
17293 ])))
17294 }
17295 });
17296
17297 request.next().await;
17298
17299 cx.condition(|editor, _| editor.context_menu_visible())
17300 .await;
17301 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
17302 editor
17303 .confirm_completion(&ConfirmCompletion::default(), window, cx)
17304 .unwrap()
17305 });
17306 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
17307
17308 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
17309 let task_completion_item = completion_item.clone();
17310 async move { Ok(task_completion_item) }
17311 })
17312 .next()
17313 .await
17314 .unwrap();
17315 apply_additional_edits.await.unwrap();
17316 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
17317}
17318
17319#[gpui::test]
17320async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
17321 init_test(cx, |_| {});
17322
17323 let mut cx = EditorLspTestContext::new_rust(
17324 lsp::ServerCapabilities {
17325 completion_provider: Some(lsp::CompletionOptions {
17326 trigger_characters: Some(vec![".".to_string()]),
17327 resolve_provider: Some(true),
17328 ..Default::default()
17329 }),
17330 ..Default::default()
17331 },
17332 cx,
17333 )
17334 .await;
17335
17336 cx.set_state("fn main() { let a = 2ˇ; }");
17337 cx.simulate_keystroke(".");
17338
17339 let item1 = lsp::CompletionItem {
17340 label: "method id()".to_string(),
17341 filter_text: Some("id".to_string()),
17342 detail: None,
17343 documentation: None,
17344 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17345 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17346 new_text: ".id".to_string(),
17347 })),
17348 ..lsp::CompletionItem::default()
17349 };
17350
17351 let item2 = lsp::CompletionItem {
17352 label: "other".to_string(),
17353 filter_text: Some("other".to_string()),
17354 detail: None,
17355 documentation: None,
17356 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17357 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17358 new_text: ".other".to_string(),
17359 })),
17360 ..lsp::CompletionItem::default()
17361 };
17362
17363 let item1 = item1.clone();
17364 cx.set_request_handler::<lsp::request::Completion, _, _>({
17365 let item1 = item1.clone();
17366 move |_, _, _| {
17367 let item1 = item1.clone();
17368 let item2 = item2.clone();
17369 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
17370 }
17371 })
17372 .next()
17373 .await;
17374
17375 cx.condition(|editor, _| editor.context_menu_visible())
17376 .await;
17377 cx.update_editor(|editor, _, _| {
17378 let context_menu = editor.context_menu.borrow_mut();
17379 let context_menu = context_menu
17380 .as_ref()
17381 .expect("Should have the context menu deployed");
17382 match context_menu {
17383 CodeContextMenu::Completions(completions_menu) => {
17384 let completions = completions_menu.completions.borrow_mut();
17385 assert_eq!(
17386 completions
17387 .iter()
17388 .map(|completion| &completion.label.text)
17389 .collect::<Vec<_>>(),
17390 vec!["method id()", "other"]
17391 )
17392 }
17393 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17394 }
17395 });
17396
17397 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
17398 let item1 = item1.clone();
17399 move |_, item_to_resolve, _| {
17400 let item1 = item1.clone();
17401 async move {
17402 if item1 == item_to_resolve {
17403 Ok(lsp::CompletionItem {
17404 label: "method id()".to_string(),
17405 filter_text: Some("id".to_string()),
17406 detail: Some("Now resolved!".to_string()),
17407 documentation: Some(lsp::Documentation::String("Docs".to_string())),
17408 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17409 range: lsp::Range::new(
17410 lsp::Position::new(0, 22),
17411 lsp::Position::new(0, 22),
17412 ),
17413 new_text: ".id".to_string(),
17414 })),
17415 ..lsp::CompletionItem::default()
17416 })
17417 } else {
17418 Ok(item_to_resolve)
17419 }
17420 }
17421 }
17422 })
17423 .next()
17424 .await
17425 .unwrap();
17426 cx.run_until_parked();
17427
17428 cx.update_editor(|editor, window, cx| {
17429 editor.context_menu_next(&Default::default(), window, cx);
17430 });
17431
17432 cx.update_editor(|editor, _, _| {
17433 let context_menu = editor.context_menu.borrow_mut();
17434 let context_menu = context_menu
17435 .as_ref()
17436 .expect("Should have the context menu deployed");
17437 match context_menu {
17438 CodeContextMenu::Completions(completions_menu) => {
17439 let completions = completions_menu.completions.borrow_mut();
17440 assert_eq!(
17441 completions
17442 .iter()
17443 .map(|completion| &completion.label.text)
17444 .collect::<Vec<_>>(),
17445 vec!["method id() Now resolved!", "other"],
17446 "Should update first completion label, but not second as the filter text did not match."
17447 );
17448 }
17449 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17450 }
17451 });
17452}
17453
17454#[gpui::test]
17455async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
17456 init_test(cx, |_| {});
17457 let mut cx = EditorLspTestContext::new_rust(
17458 lsp::ServerCapabilities {
17459 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
17460 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
17461 completion_provider: Some(lsp::CompletionOptions {
17462 resolve_provider: Some(true),
17463 ..Default::default()
17464 }),
17465 ..Default::default()
17466 },
17467 cx,
17468 )
17469 .await;
17470 cx.set_state(indoc! {"
17471 struct TestStruct {
17472 field: i32
17473 }
17474
17475 fn mainˇ() {
17476 let unused_var = 42;
17477 let test_struct = TestStruct { field: 42 };
17478 }
17479 "});
17480 let symbol_range = cx.lsp_range(indoc! {"
17481 struct TestStruct {
17482 field: i32
17483 }
17484
17485 «fn main»() {
17486 let unused_var = 42;
17487 let test_struct = TestStruct { field: 42 };
17488 }
17489 "});
17490 let mut hover_requests =
17491 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
17492 Ok(Some(lsp::Hover {
17493 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
17494 kind: lsp::MarkupKind::Markdown,
17495 value: "Function documentation".to_string(),
17496 }),
17497 range: Some(symbol_range),
17498 }))
17499 });
17500
17501 // Case 1: Test that code action menu hide hover popover
17502 cx.dispatch_action(Hover);
17503 hover_requests.next().await;
17504 cx.condition(|editor, _| editor.hover_state.visible()).await;
17505 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
17506 move |_, _, _| async move {
17507 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
17508 lsp::CodeAction {
17509 title: "Remove unused variable".to_string(),
17510 kind: Some(CodeActionKind::QUICKFIX),
17511 edit: Some(lsp::WorkspaceEdit {
17512 changes: Some(
17513 [(
17514 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
17515 vec![lsp::TextEdit {
17516 range: lsp::Range::new(
17517 lsp::Position::new(5, 4),
17518 lsp::Position::new(5, 27),
17519 ),
17520 new_text: "".to_string(),
17521 }],
17522 )]
17523 .into_iter()
17524 .collect(),
17525 ),
17526 ..Default::default()
17527 }),
17528 ..Default::default()
17529 },
17530 )]))
17531 },
17532 );
17533 cx.update_editor(|editor, window, cx| {
17534 editor.toggle_code_actions(
17535 &ToggleCodeActions {
17536 deployed_from: None,
17537 quick_launch: false,
17538 },
17539 window,
17540 cx,
17541 );
17542 });
17543 code_action_requests.next().await;
17544 cx.run_until_parked();
17545 cx.condition(|editor, _| editor.context_menu_visible())
17546 .await;
17547 cx.update_editor(|editor, _, _| {
17548 assert!(
17549 !editor.hover_state.visible(),
17550 "Hover popover should be hidden when code action menu is shown"
17551 );
17552 // Hide code actions
17553 editor.context_menu.take();
17554 });
17555
17556 // Case 2: Test that code completions hide hover popover
17557 cx.dispatch_action(Hover);
17558 hover_requests.next().await;
17559 cx.condition(|editor, _| editor.hover_state.visible()).await;
17560 let counter = Arc::new(AtomicUsize::new(0));
17561 let mut completion_requests =
17562 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17563 let counter = counter.clone();
17564 async move {
17565 counter.fetch_add(1, atomic::Ordering::Release);
17566 Ok(Some(lsp::CompletionResponse::Array(vec![
17567 lsp::CompletionItem {
17568 label: "main".into(),
17569 kind: Some(lsp::CompletionItemKind::FUNCTION),
17570 detail: Some("() -> ()".to_string()),
17571 ..Default::default()
17572 },
17573 lsp::CompletionItem {
17574 label: "TestStruct".into(),
17575 kind: Some(lsp::CompletionItemKind::STRUCT),
17576 detail: Some("struct TestStruct".to_string()),
17577 ..Default::default()
17578 },
17579 ])))
17580 }
17581 });
17582 cx.update_editor(|editor, window, cx| {
17583 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
17584 });
17585 completion_requests.next().await;
17586 cx.condition(|editor, _| editor.context_menu_visible())
17587 .await;
17588 cx.update_editor(|editor, _, _| {
17589 assert!(
17590 !editor.hover_state.visible(),
17591 "Hover popover should be hidden when completion menu is shown"
17592 );
17593 });
17594}
17595
17596#[gpui::test]
17597async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
17598 init_test(cx, |_| {});
17599
17600 let mut cx = EditorLspTestContext::new_rust(
17601 lsp::ServerCapabilities {
17602 completion_provider: Some(lsp::CompletionOptions {
17603 trigger_characters: Some(vec![".".to_string()]),
17604 resolve_provider: Some(true),
17605 ..Default::default()
17606 }),
17607 ..Default::default()
17608 },
17609 cx,
17610 )
17611 .await;
17612
17613 cx.set_state("fn main() { let a = 2ˇ; }");
17614 cx.simulate_keystroke(".");
17615
17616 let unresolved_item_1 = lsp::CompletionItem {
17617 label: "id".to_string(),
17618 filter_text: Some("id".to_string()),
17619 detail: None,
17620 documentation: None,
17621 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17622 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17623 new_text: ".id".to_string(),
17624 })),
17625 ..lsp::CompletionItem::default()
17626 };
17627 let resolved_item_1 = lsp::CompletionItem {
17628 additional_text_edits: Some(vec![lsp::TextEdit {
17629 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
17630 new_text: "!!".to_string(),
17631 }]),
17632 ..unresolved_item_1.clone()
17633 };
17634 let unresolved_item_2 = lsp::CompletionItem {
17635 label: "other".to_string(),
17636 filter_text: Some("other".to_string()),
17637 detail: None,
17638 documentation: None,
17639 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17640 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17641 new_text: ".other".to_string(),
17642 })),
17643 ..lsp::CompletionItem::default()
17644 };
17645 let resolved_item_2 = lsp::CompletionItem {
17646 additional_text_edits: Some(vec![lsp::TextEdit {
17647 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
17648 new_text: "??".to_string(),
17649 }]),
17650 ..unresolved_item_2.clone()
17651 };
17652
17653 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
17654 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
17655 cx.lsp
17656 .server
17657 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
17658 let unresolved_item_1 = unresolved_item_1.clone();
17659 let resolved_item_1 = resolved_item_1.clone();
17660 let unresolved_item_2 = unresolved_item_2.clone();
17661 let resolved_item_2 = resolved_item_2.clone();
17662 let resolve_requests_1 = resolve_requests_1.clone();
17663 let resolve_requests_2 = resolve_requests_2.clone();
17664 move |unresolved_request, _| {
17665 let unresolved_item_1 = unresolved_item_1.clone();
17666 let resolved_item_1 = resolved_item_1.clone();
17667 let unresolved_item_2 = unresolved_item_2.clone();
17668 let resolved_item_2 = resolved_item_2.clone();
17669 let resolve_requests_1 = resolve_requests_1.clone();
17670 let resolve_requests_2 = resolve_requests_2.clone();
17671 async move {
17672 if unresolved_request == unresolved_item_1 {
17673 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
17674 Ok(resolved_item_1.clone())
17675 } else if unresolved_request == unresolved_item_2 {
17676 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
17677 Ok(resolved_item_2.clone())
17678 } else {
17679 panic!("Unexpected completion item {unresolved_request:?}")
17680 }
17681 }
17682 }
17683 })
17684 .detach();
17685
17686 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17687 let unresolved_item_1 = unresolved_item_1.clone();
17688 let unresolved_item_2 = unresolved_item_2.clone();
17689 async move {
17690 Ok(Some(lsp::CompletionResponse::Array(vec![
17691 unresolved_item_1,
17692 unresolved_item_2,
17693 ])))
17694 }
17695 })
17696 .next()
17697 .await;
17698
17699 cx.condition(|editor, _| editor.context_menu_visible())
17700 .await;
17701 cx.update_editor(|editor, _, _| {
17702 let context_menu = editor.context_menu.borrow_mut();
17703 let context_menu = context_menu
17704 .as_ref()
17705 .expect("Should have the context menu deployed");
17706 match context_menu {
17707 CodeContextMenu::Completions(completions_menu) => {
17708 let completions = completions_menu.completions.borrow_mut();
17709 assert_eq!(
17710 completions
17711 .iter()
17712 .map(|completion| &completion.label.text)
17713 .collect::<Vec<_>>(),
17714 vec!["id", "other"]
17715 )
17716 }
17717 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17718 }
17719 });
17720 cx.run_until_parked();
17721
17722 cx.update_editor(|editor, window, cx| {
17723 editor.context_menu_next(&ContextMenuNext, window, cx);
17724 });
17725 cx.run_until_parked();
17726 cx.update_editor(|editor, window, cx| {
17727 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
17728 });
17729 cx.run_until_parked();
17730 cx.update_editor(|editor, window, cx| {
17731 editor.context_menu_next(&ContextMenuNext, window, cx);
17732 });
17733 cx.run_until_parked();
17734 cx.update_editor(|editor, window, cx| {
17735 editor
17736 .compose_completion(&ComposeCompletion::default(), window, cx)
17737 .expect("No task returned")
17738 })
17739 .await
17740 .expect("Completion failed");
17741 cx.run_until_parked();
17742
17743 cx.update_editor(|editor, _, cx| {
17744 assert_eq!(
17745 resolve_requests_1.load(atomic::Ordering::Acquire),
17746 1,
17747 "Should always resolve once despite multiple selections"
17748 );
17749 assert_eq!(
17750 resolve_requests_2.load(atomic::Ordering::Acquire),
17751 1,
17752 "Should always resolve once after multiple selections and applying the completion"
17753 );
17754 assert_eq!(
17755 editor.text(cx),
17756 "fn main() { let a = ??.other; }",
17757 "Should use resolved data when applying the completion"
17758 );
17759 });
17760}
17761
17762#[gpui::test]
17763async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
17764 init_test(cx, |_| {});
17765
17766 let item_0 = lsp::CompletionItem {
17767 label: "abs".into(),
17768 insert_text: Some("abs".into()),
17769 data: Some(json!({ "very": "special"})),
17770 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
17771 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
17772 lsp::InsertReplaceEdit {
17773 new_text: "abs".to_string(),
17774 insert: lsp::Range::default(),
17775 replace: lsp::Range::default(),
17776 },
17777 )),
17778 ..lsp::CompletionItem::default()
17779 };
17780 let items = iter::once(item_0.clone())
17781 .chain((11..51).map(|i| lsp::CompletionItem {
17782 label: format!("item_{}", i),
17783 insert_text: Some(format!("item_{}", i)),
17784 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
17785 ..lsp::CompletionItem::default()
17786 }))
17787 .collect::<Vec<_>>();
17788
17789 let default_commit_characters = vec!["?".to_string()];
17790 let default_data = json!({ "default": "data"});
17791 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
17792 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
17793 let default_edit_range = lsp::Range {
17794 start: lsp::Position {
17795 line: 0,
17796 character: 5,
17797 },
17798 end: lsp::Position {
17799 line: 0,
17800 character: 5,
17801 },
17802 };
17803
17804 let mut cx = EditorLspTestContext::new_rust(
17805 lsp::ServerCapabilities {
17806 completion_provider: Some(lsp::CompletionOptions {
17807 trigger_characters: Some(vec![".".to_string()]),
17808 resolve_provider: Some(true),
17809 ..Default::default()
17810 }),
17811 ..Default::default()
17812 },
17813 cx,
17814 )
17815 .await;
17816
17817 cx.set_state("fn main() { let a = 2ˇ; }");
17818 cx.simulate_keystroke(".");
17819
17820 let completion_data = default_data.clone();
17821 let completion_characters = default_commit_characters.clone();
17822 let completion_items = items.clone();
17823 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17824 let default_data = completion_data.clone();
17825 let default_commit_characters = completion_characters.clone();
17826 let items = completion_items.clone();
17827 async move {
17828 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
17829 items,
17830 item_defaults: Some(lsp::CompletionListItemDefaults {
17831 data: Some(default_data.clone()),
17832 commit_characters: Some(default_commit_characters.clone()),
17833 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
17834 default_edit_range,
17835 )),
17836 insert_text_format: Some(default_insert_text_format),
17837 insert_text_mode: Some(default_insert_text_mode),
17838 }),
17839 ..lsp::CompletionList::default()
17840 })))
17841 }
17842 })
17843 .next()
17844 .await;
17845
17846 let resolved_items = Arc::new(Mutex::new(Vec::new()));
17847 cx.lsp
17848 .server
17849 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
17850 let closure_resolved_items = resolved_items.clone();
17851 move |item_to_resolve, _| {
17852 let closure_resolved_items = closure_resolved_items.clone();
17853 async move {
17854 closure_resolved_items.lock().push(item_to_resolve.clone());
17855 Ok(item_to_resolve)
17856 }
17857 }
17858 })
17859 .detach();
17860
17861 cx.condition(|editor, _| editor.context_menu_visible())
17862 .await;
17863 cx.run_until_parked();
17864 cx.update_editor(|editor, _, _| {
17865 let menu = editor.context_menu.borrow_mut();
17866 match menu.as_ref().expect("should have the completions menu") {
17867 CodeContextMenu::Completions(completions_menu) => {
17868 assert_eq!(
17869 completions_menu
17870 .entries
17871 .borrow()
17872 .iter()
17873 .map(|mat| mat.string.clone())
17874 .collect::<Vec<String>>(),
17875 items
17876 .iter()
17877 .map(|completion| completion.label.clone())
17878 .collect::<Vec<String>>()
17879 );
17880 }
17881 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
17882 }
17883 });
17884 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
17885 // with 4 from the end.
17886 assert_eq!(
17887 *resolved_items.lock(),
17888 [&items[0..16], &items[items.len() - 4..items.len()]]
17889 .concat()
17890 .iter()
17891 .cloned()
17892 .map(|mut item| {
17893 if item.data.is_none() {
17894 item.data = Some(default_data.clone());
17895 }
17896 item
17897 })
17898 .collect::<Vec<lsp::CompletionItem>>(),
17899 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
17900 );
17901 resolved_items.lock().clear();
17902
17903 cx.update_editor(|editor, window, cx| {
17904 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
17905 });
17906 cx.run_until_parked();
17907 // Completions that have already been resolved are skipped.
17908 assert_eq!(
17909 *resolved_items.lock(),
17910 items[items.len() - 17..items.len() - 4]
17911 .iter()
17912 .cloned()
17913 .map(|mut item| {
17914 if item.data.is_none() {
17915 item.data = Some(default_data.clone());
17916 }
17917 item
17918 })
17919 .collect::<Vec<lsp::CompletionItem>>()
17920 );
17921 resolved_items.lock().clear();
17922}
17923
17924#[gpui::test]
17925async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
17926 init_test(cx, |_| {});
17927
17928 let mut cx = EditorLspTestContext::new(
17929 Language::new(
17930 LanguageConfig {
17931 matcher: LanguageMatcher {
17932 path_suffixes: vec!["jsx".into()],
17933 ..Default::default()
17934 },
17935 overrides: [(
17936 "element".into(),
17937 LanguageConfigOverride {
17938 completion_query_characters: Override::Set(['-'].into_iter().collect()),
17939 ..Default::default()
17940 },
17941 )]
17942 .into_iter()
17943 .collect(),
17944 ..Default::default()
17945 },
17946 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
17947 )
17948 .with_override_query("(jsx_self_closing_element) @element")
17949 .unwrap(),
17950 lsp::ServerCapabilities {
17951 completion_provider: Some(lsp::CompletionOptions {
17952 trigger_characters: Some(vec![":".to_string()]),
17953 ..Default::default()
17954 }),
17955 ..Default::default()
17956 },
17957 cx,
17958 )
17959 .await;
17960
17961 cx.lsp
17962 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
17963 Ok(Some(lsp::CompletionResponse::Array(vec![
17964 lsp::CompletionItem {
17965 label: "bg-blue".into(),
17966 ..Default::default()
17967 },
17968 lsp::CompletionItem {
17969 label: "bg-red".into(),
17970 ..Default::default()
17971 },
17972 lsp::CompletionItem {
17973 label: "bg-yellow".into(),
17974 ..Default::default()
17975 },
17976 ])))
17977 });
17978
17979 cx.set_state(r#"<p class="bgˇ" />"#);
17980
17981 // Trigger completion when typing a dash, because the dash is an extra
17982 // word character in the 'element' scope, which contains the cursor.
17983 cx.simulate_keystroke("-");
17984 cx.executor().run_until_parked();
17985 cx.update_editor(|editor, _, _| {
17986 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17987 {
17988 assert_eq!(
17989 completion_menu_entries(menu),
17990 &["bg-blue", "bg-red", "bg-yellow"]
17991 );
17992 } else {
17993 panic!("expected completion menu to be open");
17994 }
17995 });
17996
17997 cx.simulate_keystroke("l");
17998 cx.executor().run_until_parked();
17999 cx.update_editor(|editor, _, _| {
18000 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18001 {
18002 assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]);
18003 } else {
18004 panic!("expected completion menu to be open");
18005 }
18006 });
18007
18008 // When filtering completions, consider the character after the '-' to
18009 // be the start of a subword.
18010 cx.set_state(r#"<p class="yelˇ" />"#);
18011 cx.simulate_keystroke("l");
18012 cx.executor().run_until_parked();
18013 cx.update_editor(|editor, _, _| {
18014 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18015 {
18016 assert_eq!(completion_menu_entries(menu), &["bg-yellow"]);
18017 } else {
18018 panic!("expected completion menu to be open");
18019 }
18020 });
18021}
18022
18023fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
18024 let entries = menu.entries.borrow();
18025 entries.iter().map(|mat| mat.string.clone()).collect()
18026}
18027
18028#[gpui::test]
18029async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
18030 init_test(cx, |settings| {
18031 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
18032 Formatter::Prettier,
18033 )))
18034 });
18035
18036 let fs = FakeFs::new(cx.executor());
18037 fs.insert_file(path!("/file.ts"), Default::default()).await;
18038
18039 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
18040 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18041
18042 language_registry.add(Arc::new(Language::new(
18043 LanguageConfig {
18044 name: "TypeScript".into(),
18045 matcher: LanguageMatcher {
18046 path_suffixes: vec!["ts".to_string()],
18047 ..Default::default()
18048 },
18049 ..Default::default()
18050 },
18051 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
18052 )));
18053 update_test_language_settings(cx, |settings| {
18054 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
18055 });
18056
18057 let test_plugin = "test_plugin";
18058 let _ = language_registry.register_fake_lsp(
18059 "TypeScript",
18060 FakeLspAdapter {
18061 prettier_plugins: vec![test_plugin],
18062 ..Default::default()
18063 },
18064 );
18065
18066 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
18067 let buffer = project
18068 .update(cx, |project, cx| {
18069 project.open_local_buffer(path!("/file.ts"), cx)
18070 })
18071 .await
18072 .unwrap();
18073
18074 let buffer_text = "one\ntwo\nthree\n";
18075 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
18076 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
18077 editor.update_in(cx, |editor, window, cx| {
18078 editor.set_text(buffer_text, window, cx)
18079 });
18080
18081 editor
18082 .update_in(cx, |editor, window, cx| {
18083 editor.perform_format(
18084 project.clone(),
18085 FormatTrigger::Manual,
18086 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18087 window,
18088 cx,
18089 )
18090 })
18091 .unwrap()
18092 .await;
18093 assert_eq!(
18094 editor.update(cx, |editor, cx| editor.text(cx)),
18095 buffer_text.to_string() + prettier_format_suffix,
18096 "Test prettier formatting was not applied to the original buffer text",
18097 );
18098
18099 update_test_language_settings(cx, |settings| {
18100 settings.defaults.formatter = Some(SelectedFormatter::Auto)
18101 });
18102 let format = editor.update_in(cx, |editor, window, cx| {
18103 editor.perform_format(
18104 project.clone(),
18105 FormatTrigger::Manual,
18106 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18107 window,
18108 cx,
18109 )
18110 });
18111 format.await.unwrap();
18112 assert_eq!(
18113 editor.update(cx, |editor, cx| editor.text(cx)),
18114 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
18115 "Autoformatting (via test prettier) was not applied to the original buffer text",
18116 );
18117}
18118
18119#[gpui::test]
18120async fn test_addition_reverts(cx: &mut TestAppContext) {
18121 init_test(cx, |_| {});
18122 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18123 let base_text = indoc! {r#"
18124 struct Row;
18125 struct Row1;
18126 struct Row2;
18127
18128 struct Row4;
18129 struct Row5;
18130 struct Row6;
18131
18132 struct Row8;
18133 struct Row9;
18134 struct Row10;"#};
18135
18136 // When addition hunks are not adjacent to carets, no hunk revert is performed
18137 assert_hunk_revert(
18138 indoc! {r#"struct Row;
18139 struct Row1;
18140 struct Row1.1;
18141 struct Row1.2;
18142 struct Row2;ˇ
18143
18144 struct Row4;
18145 struct Row5;
18146 struct Row6;
18147
18148 struct Row8;
18149 ˇstruct Row9;
18150 struct Row9.1;
18151 struct Row9.2;
18152 struct Row9.3;
18153 struct Row10;"#},
18154 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18155 indoc! {r#"struct Row;
18156 struct Row1;
18157 struct Row1.1;
18158 struct Row1.2;
18159 struct Row2;ˇ
18160
18161 struct Row4;
18162 struct Row5;
18163 struct Row6;
18164
18165 struct Row8;
18166 ˇstruct Row9;
18167 struct Row9.1;
18168 struct Row9.2;
18169 struct Row9.3;
18170 struct Row10;"#},
18171 base_text,
18172 &mut cx,
18173 );
18174 // Same for selections
18175 assert_hunk_revert(
18176 indoc! {r#"struct Row;
18177 struct Row1;
18178 struct Row2;
18179 struct Row2.1;
18180 struct Row2.2;
18181 «ˇ
18182 struct Row4;
18183 struct» Row5;
18184 «struct Row6;
18185 ˇ»
18186 struct Row9.1;
18187 struct Row9.2;
18188 struct Row9.3;
18189 struct Row8;
18190 struct Row9;
18191 struct Row10;"#},
18192 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18193 indoc! {r#"struct Row;
18194 struct Row1;
18195 struct Row2;
18196 struct Row2.1;
18197 struct Row2.2;
18198 «ˇ
18199 struct Row4;
18200 struct» Row5;
18201 «struct Row6;
18202 ˇ»
18203 struct Row9.1;
18204 struct Row9.2;
18205 struct Row9.3;
18206 struct Row8;
18207 struct Row9;
18208 struct Row10;"#},
18209 base_text,
18210 &mut cx,
18211 );
18212
18213 // When carets and selections intersect the addition hunks, those are reverted.
18214 // Adjacent carets got merged.
18215 assert_hunk_revert(
18216 indoc! {r#"struct Row;
18217 ˇ// something on the top
18218 struct Row1;
18219 struct Row2;
18220 struct Roˇw3.1;
18221 struct Row2.2;
18222 struct Row2.3;ˇ
18223
18224 struct Row4;
18225 struct ˇRow5.1;
18226 struct Row5.2;
18227 struct «Rowˇ»5.3;
18228 struct Row5;
18229 struct Row6;
18230 ˇ
18231 struct Row9.1;
18232 struct «Rowˇ»9.2;
18233 struct «ˇRow»9.3;
18234 struct Row8;
18235 struct Row9;
18236 «ˇ// something on bottom»
18237 struct Row10;"#},
18238 vec![
18239 DiffHunkStatusKind::Added,
18240 DiffHunkStatusKind::Added,
18241 DiffHunkStatusKind::Added,
18242 DiffHunkStatusKind::Added,
18243 DiffHunkStatusKind::Added,
18244 ],
18245 indoc! {r#"struct Row;
18246 ˇstruct Row1;
18247 struct Row2;
18248 ˇ
18249 struct Row4;
18250 ˇstruct Row5;
18251 struct Row6;
18252 ˇ
18253 ˇstruct Row8;
18254 struct Row9;
18255 ˇstruct Row10;"#},
18256 base_text,
18257 &mut cx,
18258 );
18259}
18260
18261#[gpui::test]
18262async fn test_modification_reverts(cx: &mut TestAppContext) {
18263 init_test(cx, |_| {});
18264 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18265 let base_text = indoc! {r#"
18266 struct Row;
18267 struct Row1;
18268 struct Row2;
18269
18270 struct Row4;
18271 struct Row5;
18272 struct Row6;
18273
18274 struct Row8;
18275 struct Row9;
18276 struct Row10;"#};
18277
18278 // Modification hunks behave the same as the addition ones.
18279 assert_hunk_revert(
18280 indoc! {r#"struct Row;
18281 struct Row1;
18282 struct Row33;
18283 ˇ
18284 struct Row4;
18285 struct Row5;
18286 struct Row6;
18287 ˇ
18288 struct Row99;
18289 struct Row9;
18290 struct Row10;"#},
18291 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18292 indoc! {r#"struct Row;
18293 struct Row1;
18294 struct Row33;
18295 ˇ
18296 struct Row4;
18297 struct Row5;
18298 struct Row6;
18299 ˇ
18300 struct Row99;
18301 struct Row9;
18302 struct Row10;"#},
18303 base_text,
18304 &mut cx,
18305 );
18306 assert_hunk_revert(
18307 indoc! {r#"struct Row;
18308 struct Row1;
18309 struct Row33;
18310 «ˇ
18311 struct Row4;
18312 struct» Row5;
18313 «struct Row6;
18314 ˇ»
18315 struct Row99;
18316 struct Row9;
18317 struct Row10;"#},
18318 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18319 indoc! {r#"struct Row;
18320 struct Row1;
18321 struct Row33;
18322 «ˇ
18323 struct Row4;
18324 struct» Row5;
18325 «struct Row6;
18326 ˇ»
18327 struct Row99;
18328 struct Row9;
18329 struct Row10;"#},
18330 base_text,
18331 &mut cx,
18332 );
18333
18334 assert_hunk_revert(
18335 indoc! {r#"ˇstruct Row1.1;
18336 struct Row1;
18337 «ˇstr»uct Row22;
18338
18339 struct ˇRow44;
18340 struct Row5;
18341 struct «Rˇ»ow66;ˇ
18342
18343 «struˇ»ct Row88;
18344 struct Row9;
18345 struct Row1011;ˇ"#},
18346 vec![
18347 DiffHunkStatusKind::Modified,
18348 DiffHunkStatusKind::Modified,
18349 DiffHunkStatusKind::Modified,
18350 DiffHunkStatusKind::Modified,
18351 DiffHunkStatusKind::Modified,
18352 DiffHunkStatusKind::Modified,
18353 ],
18354 indoc! {r#"struct Row;
18355 ˇstruct Row1;
18356 struct Row2;
18357 ˇ
18358 struct Row4;
18359 ˇstruct Row5;
18360 struct Row6;
18361 ˇ
18362 struct Row8;
18363 ˇstruct Row9;
18364 struct Row10;ˇ"#},
18365 base_text,
18366 &mut cx,
18367 );
18368}
18369
18370#[gpui::test]
18371async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
18372 init_test(cx, |_| {});
18373 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18374 let base_text = indoc! {r#"
18375 one
18376
18377 two
18378 three
18379 "#};
18380
18381 cx.set_head_text(base_text);
18382 cx.set_state("\nˇ\n");
18383 cx.executor().run_until_parked();
18384 cx.update_editor(|editor, _window, cx| {
18385 editor.expand_selected_diff_hunks(cx);
18386 });
18387 cx.executor().run_until_parked();
18388 cx.update_editor(|editor, window, cx| {
18389 editor.backspace(&Default::default(), window, cx);
18390 });
18391 cx.run_until_parked();
18392 cx.assert_state_with_diff(
18393 indoc! {r#"
18394
18395 - two
18396 - threeˇ
18397 +
18398 "#}
18399 .to_string(),
18400 );
18401}
18402
18403#[gpui::test]
18404async fn test_deletion_reverts(cx: &mut TestAppContext) {
18405 init_test(cx, |_| {});
18406 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18407 let base_text = indoc! {r#"struct Row;
18408struct Row1;
18409struct Row2;
18410
18411struct Row4;
18412struct Row5;
18413struct Row6;
18414
18415struct Row8;
18416struct Row9;
18417struct Row10;"#};
18418
18419 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
18420 assert_hunk_revert(
18421 indoc! {r#"struct Row;
18422 struct Row2;
18423
18424 ˇstruct Row4;
18425 struct Row5;
18426 struct Row6;
18427 ˇ
18428 struct Row8;
18429 struct Row10;"#},
18430 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18431 indoc! {r#"struct Row;
18432 struct Row2;
18433
18434 ˇstruct Row4;
18435 struct Row5;
18436 struct Row6;
18437 ˇ
18438 struct Row8;
18439 struct Row10;"#},
18440 base_text,
18441 &mut cx,
18442 );
18443 assert_hunk_revert(
18444 indoc! {r#"struct Row;
18445 struct Row2;
18446
18447 «ˇstruct Row4;
18448 struct» Row5;
18449 «struct Row6;
18450 ˇ»
18451 struct Row8;
18452 struct Row10;"#},
18453 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18454 indoc! {r#"struct Row;
18455 struct Row2;
18456
18457 «ˇstruct Row4;
18458 struct» Row5;
18459 «struct Row6;
18460 ˇ»
18461 struct Row8;
18462 struct Row10;"#},
18463 base_text,
18464 &mut cx,
18465 );
18466
18467 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
18468 assert_hunk_revert(
18469 indoc! {r#"struct Row;
18470 ˇstruct Row2;
18471
18472 struct Row4;
18473 struct Row5;
18474 struct Row6;
18475
18476 struct Row8;ˇ
18477 struct Row10;"#},
18478 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18479 indoc! {r#"struct Row;
18480 struct Row1;
18481 ˇstruct Row2;
18482
18483 struct Row4;
18484 struct Row5;
18485 struct Row6;
18486
18487 struct Row8;ˇ
18488 struct Row9;
18489 struct Row10;"#},
18490 base_text,
18491 &mut cx,
18492 );
18493 assert_hunk_revert(
18494 indoc! {r#"struct Row;
18495 struct Row2«ˇ;
18496 struct Row4;
18497 struct» Row5;
18498 «struct Row6;
18499
18500 struct Row8;ˇ»
18501 struct Row10;"#},
18502 vec![
18503 DiffHunkStatusKind::Deleted,
18504 DiffHunkStatusKind::Deleted,
18505 DiffHunkStatusKind::Deleted,
18506 ],
18507 indoc! {r#"struct Row;
18508 struct Row1;
18509 struct Row2«ˇ;
18510
18511 struct Row4;
18512 struct» Row5;
18513 «struct Row6;
18514
18515 struct Row8;ˇ»
18516 struct Row9;
18517 struct Row10;"#},
18518 base_text,
18519 &mut cx,
18520 );
18521}
18522
18523#[gpui::test]
18524async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
18525 init_test(cx, |_| {});
18526
18527 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
18528 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
18529 let base_text_3 =
18530 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
18531
18532 let text_1 = edit_first_char_of_every_line(base_text_1);
18533 let text_2 = edit_first_char_of_every_line(base_text_2);
18534 let text_3 = edit_first_char_of_every_line(base_text_3);
18535
18536 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
18537 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
18538 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
18539
18540 let multibuffer = cx.new(|cx| {
18541 let mut multibuffer = MultiBuffer::new(ReadWrite);
18542 multibuffer.push_excerpts(
18543 buffer_1.clone(),
18544 [
18545 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18546 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18547 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18548 ],
18549 cx,
18550 );
18551 multibuffer.push_excerpts(
18552 buffer_2.clone(),
18553 [
18554 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18555 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18556 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18557 ],
18558 cx,
18559 );
18560 multibuffer.push_excerpts(
18561 buffer_3.clone(),
18562 [
18563 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18564 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18565 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18566 ],
18567 cx,
18568 );
18569 multibuffer
18570 });
18571
18572 let fs = FakeFs::new(cx.executor());
18573 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
18574 let (editor, cx) = cx
18575 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
18576 editor.update_in(cx, |editor, _window, cx| {
18577 for (buffer, diff_base) in [
18578 (buffer_1.clone(), base_text_1),
18579 (buffer_2.clone(), base_text_2),
18580 (buffer_3.clone(), base_text_3),
18581 ] {
18582 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
18583 editor
18584 .buffer
18585 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
18586 }
18587 });
18588 cx.executor().run_until_parked();
18589
18590 editor.update_in(cx, |editor, window, cx| {
18591 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}");
18592 editor.select_all(&SelectAll, window, cx);
18593 editor.git_restore(&Default::default(), window, cx);
18594 });
18595 cx.executor().run_until_parked();
18596
18597 // When all ranges are selected, all buffer hunks are reverted.
18598 editor.update(cx, |editor, cx| {
18599 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");
18600 });
18601 buffer_1.update(cx, |buffer, _| {
18602 assert_eq!(buffer.text(), base_text_1);
18603 });
18604 buffer_2.update(cx, |buffer, _| {
18605 assert_eq!(buffer.text(), base_text_2);
18606 });
18607 buffer_3.update(cx, |buffer, _| {
18608 assert_eq!(buffer.text(), base_text_3);
18609 });
18610
18611 editor.update_in(cx, |editor, window, cx| {
18612 editor.undo(&Default::default(), window, cx);
18613 });
18614
18615 editor.update_in(cx, |editor, window, cx| {
18616 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18617 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
18618 });
18619 editor.git_restore(&Default::default(), window, cx);
18620 });
18621
18622 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
18623 // but not affect buffer_2 and its related excerpts.
18624 editor.update(cx, |editor, cx| {
18625 assert_eq!(
18626 editor.text(cx),
18627 "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}"
18628 );
18629 });
18630 buffer_1.update(cx, |buffer, _| {
18631 assert_eq!(buffer.text(), base_text_1);
18632 });
18633 buffer_2.update(cx, |buffer, _| {
18634 assert_eq!(
18635 buffer.text(),
18636 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
18637 );
18638 });
18639 buffer_3.update(cx, |buffer, _| {
18640 assert_eq!(
18641 buffer.text(),
18642 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
18643 );
18644 });
18645
18646 fn edit_first_char_of_every_line(text: &str) -> String {
18647 text.split('\n')
18648 .map(|line| format!("X{}", &line[1..]))
18649 .collect::<Vec<_>>()
18650 .join("\n")
18651 }
18652}
18653
18654#[gpui::test]
18655async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
18656 init_test(cx, |_| {});
18657
18658 let cols = 4;
18659 let rows = 10;
18660 let sample_text_1 = sample_text(rows, cols, 'a');
18661 assert_eq!(
18662 sample_text_1,
18663 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
18664 );
18665 let sample_text_2 = sample_text(rows, cols, 'l');
18666 assert_eq!(
18667 sample_text_2,
18668 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
18669 );
18670 let sample_text_3 = sample_text(rows, cols, 'v');
18671 assert_eq!(
18672 sample_text_3,
18673 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
18674 );
18675
18676 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
18677 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
18678 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
18679
18680 let multi_buffer = cx.new(|cx| {
18681 let mut multibuffer = MultiBuffer::new(ReadWrite);
18682 multibuffer.push_excerpts(
18683 buffer_1.clone(),
18684 [
18685 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18686 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18687 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18688 ],
18689 cx,
18690 );
18691 multibuffer.push_excerpts(
18692 buffer_2.clone(),
18693 [
18694 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18695 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18696 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18697 ],
18698 cx,
18699 );
18700 multibuffer.push_excerpts(
18701 buffer_3.clone(),
18702 [
18703 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18704 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18705 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18706 ],
18707 cx,
18708 );
18709 multibuffer
18710 });
18711
18712 let fs = FakeFs::new(cx.executor());
18713 fs.insert_tree(
18714 "/a",
18715 json!({
18716 "main.rs": sample_text_1,
18717 "other.rs": sample_text_2,
18718 "lib.rs": sample_text_3,
18719 }),
18720 )
18721 .await;
18722 let project = Project::test(fs, ["/a".as_ref()], cx).await;
18723 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18724 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18725 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18726 Editor::new(
18727 EditorMode::full(),
18728 multi_buffer,
18729 Some(project.clone()),
18730 window,
18731 cx,
18732 )
18733 });
18734 let multibuffer_item_id = workspace
18735 .update(cx, |workspace, window, cx| {
18736 assert!(
18737 workspace.active_item(cx).is_none(),
18738 "active item should be None before the first item is added"
18739 );
18740 workspace.add_item_to_active_pane(
18741 Box::new(multi_buffer_editor.clone()),
18742 None,
18743 true,
18744 window,
18745 cx,
18746 );
18747 let active_item = workspace
18748 .active_item(cx)
18749 .expect("should have an active item after adding the multi buffer");
18750 assert!(
18751 !active_item.is_singleton(cx),
18752 "A multi buffer was expected to active after adding"
18753 );
18754 active_item.item_id()
18755 })
18756 .unwrap();
18757 cx.executor().run_until_parked();
18758
18759 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18760 editor.change_selections(
18761 SelectionEffects::scroll(Autoscroll::Next),
18762 window,
18763 cx,
18764 |s| s.select_ranges(Some(1..2)),
18765 );
18766 editor.open_excerpts(&OpenExcerpts, window, cx);
18767 });
18768 cx.executor().run_until_parked();
18769 let first_item_id = workspace
18770 .update(cx, |workspace, window, cx| {
18771 let active_item = workspace
18772 .active_item(cx)
18773 .expect("should have an active item after navigating into the 1st buffer");
18774 let first_item_id = active_item.item_id();
18775 assert_ne!(
18776 first_item_id, multibuffer_item_id,
18777 "Should navigate into the 1st buffer and activate it"
18778 );
18779 assert!(
18780 active_item.is_singleton(cx),
18781 "New active item should be a singleton buffer"
18782 );
18783 assert_eq!(
18784 active_item
18785 .act_as::<Editor>(cx)
18786 .expect("should have navigated into an editor for the 1st buffer")
18787 .read(cx)
18788 .text(cx),
18789 sample_text_1
18790 );
18791
18792 workspace
18793 .go_back(workspace.active_pane().downgrade(), window, cx)
18794 .detach_and_log_err(cx);
18795
18796 first_item_id
18797 })
18798 .unwrap();
18799 cx.executor().run_until_parked();
18800 workspace
18801 .update(cx, |workspace, _, cx| {
18802 let active_item = workspace
18803 .active_item(cx)
18804 .expect("should have an active item after navigating back");
18805 assert_eq!(
18806 active_item.item_id(),
18807 multibuffer_item_id,
18808 "Should navigate back to the multi buffer"
18809 );
18810 assert!(!active_item.is_singleton(cx));
18811 })
18812 .unwrap();
18813
18814 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18815 editor.change_selections(
18816 SelectionEffects::scroll(Autoscroll::Next),
18817 window,
18818 cx,
18819 |s| s.select_ranges(Some(39..40)),
18820 );
18821 editor.open_excerpts(&OpenExcerpts, window, cx);
18822 });
18823 cx.executor().run_until_parked();
18824 let second_item_id = workspace
18825 .update(cx, |workspace, window, cx| {
18826 let active_item = workspace
18827 .active_item(cx)
18828 .expect("should have an active item after navigating into the 2nd buffer");
18829 let second_item_id = active_item.item_id();
18830 assert_ne!(
18831 second_item_id, multibuffer_item_id,
18832 "Should navigate away from the multibuffer"
18833 );
18834 assert_ne!(
18835 second_item_id, first_item_id,
18836 "Should navigate into the 2nd buffer and activate it"
18837 );
18838 assert!(
18839 active_item.is_singleton(cx),
18840 "New active item should be a singleton buffer"
18841 );
18842 assert_eq!(
18843 active_item
18844 .act_as::<Editor>(cx)
18845 .expect("should have navigated into an editor")
18846 .read(cx)
18847 .text(cx),
18848 sample_text_2
18849 );
18850
18851 workspace
18852 .go_back(workspace.active_pane().downgrade(), window, cx)
18853 .detach_and_log_err(cx);
18854
18855 second_item_id
18856 })
18857 .unwrap();
18858 cx.executor().run_until_parked();
18859 workspace
18860 .update(cx, |workspace, _, cx| {
18861 let active_item = workspace
18862 .active_item(cx)
18863 .expect("should have an active item after navigating back from the 2nd buffer");
18864 assert_eq!(
18865 active_item.item_id(),
18866 multibuffer_item_id,
18867 "Should navigate back from the 2nd buffer to the multi buffer"
18868 );
18869 assert!(!active_item.is_singleton(cx));
18870 })
18871 .unwrap();
18872
18873 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18874 editor.change_selections(
18875 SelectionEffects::scroll(Autoscroll::Next),
18876 window,
18877 cx,
18878 |s| s.select_ranges(Some(70..70)),
18879 );
18880 editor.open_excerpts(&OpenExcerpts, window, cx);
18881 });
18882 cx.executor().run_until_parked();
18883 workspace
18884 .update(cx, |workspace, window, cx| {
18885 let active_item = workspace
18886 .active_item(cx)
18887 .expect("should have an active item after navigating into the 3rd buffer");
18888 let third_item_id = active_item.item_id();
18889 assert_ne!(
18890 third_item_id, multibuffer_item_id,
18891 "Should navigate into the 3rd buffer and activate it"
18892 );
18893 assert_ne!(third_item_id, first_item_id);
18894 assert_ne!(third_item_id, second_item_id);
18895 assert!(
18896 active_item.is_singleton(cx),
18897 "New active item should be a singleton buffer"
18898 );
18899 assert_eq!(
18900 active_item
18901 .act_as::<Editor>(cx)
18902 .expect("should have navigated into an editor")
18903 .read(cx)
18904 .text(cx),
18905 sample_text_3
18906 );
18907
18908 workspace
18909 .go_back(workspace.active_pane().downgrade(), window, cx)
18910 .detach_and_log_err(cx);
18911 })
18912 .unwrap();
18913 cx.executor().run_until_parked();
18914 workspace
18915 .update(cx, |workspace, _, cx| {
18916 let active_item = workspace
18917 .active_item(cx)
18918 .expect("should have an active item after navigating back from the 3rd buffer");
18919 assert_eq!(
18920 active_item.item_id(),
18921 multibuffer_item_id,
18922 "Should navigate back from the 3rd buffer to the multi buffer"
18923 );
18924 assert!(!active_item.is_singleton(cx));
18925 })
18926 .unwrap();
18927}
18928
18929#[gpui::test]
18930async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
18931 init_test(cx, |_| {});
18932
18933 let mut cx = EditorTestContext::new(cx).await;
18934
18935 let diff_base = r#"
18936 use some::mod;
18937
18938 const A: u32 = 42;
18939
18940 fn main() {
18941 println!("hello");
18942
18943 println!("world");
18944 }
18945 "#
18946 .unindent();
18947
18948 cx.set_state(
18949 &r#"
18950 use some::modified;
18951
18952 ˇ
18953 fn main() {
18954 println!("hello there");
18955
18956 println!("around the");
18957 println!("world");
18958 }
18959 "#
18960 .unindent(),
18961 );
18962
18963 cx.set_head_text(&diff_base);
18964 executor.run_until_parked();
18965
18966 cx.update_editor(|editor, window, cx| {
18967 editor.go_to_next_hunk(&GoToHunk, window, cx);
18968 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
18969 });
18970 executor.run_until_parked();
18971 cx.assert_state_with_diff(
18972 r#"
18973 use some::modified;
18974
18975
18976 fn main() {
18977 - println!("hello");
18978 + ˇ println!("hello there");
18979
18980 println!("around the");
18981 println!("world");
18982 }
18983 "#
18984 .unindent(),
18985 );
18986
18987 cx.update_editor(|editor, window, cx| {
18988 for _ in 0..2 {
18989 editor.go_to_next_hunk(&GoToHunk, window, cx);
18990 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
18991 }
18992 });
18993 executor.run_until_parked();
18994 cx.assert_state_with_diff(
18995 r#"
18996 - use some::mod;
18997 + ˇuse some::modified;
18998
18999
19000 fn main() {
19001 - println!("hello");
19002 + println!("hello there");
19003
19004 + println!("around the");
19005 println!("world");
19006 }
19007 "#
19008 .unindent(),
19009 );
19010
19011 cx.update_editor(|editor, window, cx| {
19012 editor.go_to_next_hunk(&GoToHunk, window, cx);
19013 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19014 });
19015 executor.run_until_parked();
19016 cx.assert_state_with_diff(
19017 r#"
19018 - use some::mod;
19019 + use some::modified;
19020
19021 - const A: u32 = 42;
19022 ˇ
19023 fn main() {
19024 - println!("hello");
19025 + println!("hello there");
19026
19027 + println!("around the");
19028 println!("world");
19029 }
19030 "#
19031 .unindent(),
19032 );
19033
19034 cx.update_editor(|editor, window, cx| {
19035 editor.cancel(&Cancel, window, cx);
19036 });
19037
19038 cx.assert_state_with_diff(
19039 r#"
19040 use some::modified;
19041
19042 ˇ
19043 fn main() {
19044 println!("hello there");
19045
19046 println!("around the");
19047 println!("world");
19048 }
19049 "#
19050 .unindent(),
19051 );
19052}
19053
19054#[gpui::test]
19055async fn test_diff_base_change_with_expanded_diff_hunks(
19056 executor: BackgroundExecutor,
19057 cx: &mut TestAppContext,
19058) {
19059 init_test(cx, |_| {});
19060
19061 let mut cx = EditorTestContext::new(cx).await;
19062
19063 let diff_base = r#"
19064 use some::mod1;
19065 use some::mod2;
19066
19067 const A: u32 = 42;
19068 const B: u32 = 42;
19069 const C: u32 = 42;
19070
19071 fn main() {
19072 println!("hello");
19073
19074 println!("world");
19075 }
19076 "#
19077 .unindent();
19078
19079 cx.set_state(
19080 &r#"
19081 use some::mod2;
19082
19083 const A: u32 = 42;
19084 const C: u32 = 42;
19085
19086 fn main(ˇ) {
19087 //println!("hello");
19088
19089 println!("world");
19090 //
19091 //
19092 }
19093 "#
19094 .unindent(),
19095 );
19096
19097 cx.set_head_text(&diff_base);
19098 executor.run_until_parked();
19099
19100 cx.update_editor(|editor, window, cx| {
19101 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19102 });
19103 executor.run_until_parked();
19104 cx.assert_state_with_diff(
19105 r#"
19106 - use some::mod1;
19107 use some::mod2;
19108
19109 const A: u32 = 42;
19110 - const B: u32 = 42;
19111 const C: u32 = 42;
19112
19113 fn main(ˇ) {
19114 - println!("hello");
19115 + //println!("hello");
19116
19117 println!("world");
19118 + //
19119 + //
19120 }
19121 "#
19122 .unindent(),
19123 );
19124
19125 cx.set_head_text("new diff base!");
19126 executor.run_until_parked();
19127 cx.assert_state_with_diff(
19128 r#"
19129 - new diff base!
19130 + use some::mod2;
19131 +
19132 + const A: u32 = 42;
19133 + const C: u32 = 42;
19134 +
19135 + fn main(ˇ) {
19136 + //println!("hello");
19137 +
19138 + println!("world");
19139 + //
19140 + //
19141 + }
19142 "#
19143 .unindent(),
19144 );
19145}
19146
19147#[gpui::test]
19148async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
19149 init_test(cx, |_| {});
19150
19151 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19152 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19153 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19154 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19155 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
19156 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
19157
19158 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
19159 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
19160 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
19161
19162 let multi_buffer = cx.new(|cx| {
19163 let mut multibuffer = MultiBuffer::new(ReadWrite);
19164 multibuffer.push_excerpts(
19165 buffer_1.clone(),
19166 [
19167 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19168 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19169 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19170 ],
19171 cx,
19172 );
19173 multibuffer.push_excerpts(
19174 buffer_2.clone(),
19175 [
19176 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19177 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19178 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19179 ],
19180 cx,
19181 );
19182 multibuffer.push_excerpts(
19183 buffer_3.clone(),
19184 [
19185 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19186 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19187 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19188 ],
19189 cx,
19190 );
19191 multibuffer
19192 });
19193
19194 let editor =
19195 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19196 editor
19197 .update(cx, |editor, _window, cx| {
19198 for (buffer, diff_base) in [
19199 (buffer_1.clone(), file_1_old),
19200 (buffer_2.clone(), file_2_old),
19201 (buffer_3.clone(), file_3_old),
19202 ] {
19203 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
19204 editor
19205 .buffer
19206 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
19207 }
19208 })
19209 .unwrap();
19210
19211 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19212 cx.run_until_parked();
19213
19214 cx.assert_editor_state(
19215 &"
19216 ˇaaa
19217 ccc
19218 ddd
19219
19220 ggg
19221 hhh
19222
19223
19224 lll
19225 mmm
19226 NNN
19227
19228 qqq
19229 rrr
19230
19231 uuu
19232 111
19233 222
19234 333
19235
19236 666
19237 777
19238
19239 000
19240 !!!"
19241 .unindent(),
19242 );
19243
19244 cx.update_editor(|editor, window, cx| {
19245 editor.select_all(&SelectAll, window, cx);
19246 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19247 });
19248 cx.executor().run_until_parked();
19249
19250 cx.assert_state_with_diff(
19251 "
19252 «aaa
19253 - bbb
19254 ccc
19255 ddd
19256
19257 ggg
19258 hhh
19259
19260
19261 lll
19262 mmm
19263 - nnn
19264 + NNN
19265
19266 qqq
19267 rrr
19268
19269 uuu
19270 111
19271 222
19272 333
19273
19274 + 666
19275 777
19276
19277 000
19278 !!!ˇ»"
19279 .unindent(),
19280 );
19281}
19282
19283#[gpui::test]
19284async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
19285 init_test(cx, |_| {});
19286
19287 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
19288 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
19289
19290 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
19291 let multi_buffer = cx.new(|cx| {
19292 let mut multibuffer = MultiBuffer::new(ReadWrite);
19293 multibuffer.push_excerpts(
19294 buffer.clone(),
19295 [
19296 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
19297 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
19298 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
19299 ],
19300 cx,
19301 );
19302 multibuffer
19303 });
19304
19305 let editor =
19306 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19307 editor
19308 .update(cx, |editor, _window, cx| {
19309 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
19310 editor
19311 .buffer
19312 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
19313 })
19314 .unwrap();
19315
19316 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19317 cx.run_until_parked();
19318
19319 cx.update_editor(|editor, window, cx| {
19320 editor.expand_all_diff_hunks(&Default::default(), window, cx)
19321 });
19322 cx.executor().run_until_parked();
19323
19324 // When the start of a hunk coincides with the start of its excerpt,
19325 // the hunk is expanded. When the start of a hunk is earlier than
19326 // the start of its excerpt, the hunk is not expanded.
19327 cx.assert_state_with_diff(
19328 "
19329 ˇaaa
19330 - bbb
19331 + BBB
19332
19333 - ddd
19334 - eee
19335 + DDD
19336 + EEE
19337 fff
19338
19339 iii
19340 "
19341 .unindent(),
19342 );
19343}
19344
19345#[gpui::test]
19346async fn test_edits_around_expanded_insertion_hunks(
19347 executor: BackgroundExecutor,
19348 cx: &mut TestAppContext,
19349) {
19350 init_test(cx, |_| {});
19351
19352 let mut cx = EditorTestContext::new(cx).await;
19353
19354 let diff_base = r#"
19355 use some::mod1;
19356 use some::mod2;
19357
19358 const A: u32 = 42;
19359
19360 fn main() {
19361 println!("hello");
19362
19363 println!("world");
19364 }
19365 "#
19366 .unindent();
19367 executor.run_until_parked();
19368 cx.set_state(
19369 &r#"
19370 use some::mod1;
19371 use some::mod2;
19372
19373 const A: u32 = 42;
19374 const B: u32 = 42;
19375 const C: u32 = 42;
19376 ˇ
19377
19378 fn main() {
19379 println!("hello");
19380
19381 println!("world");
19382 }
19383 "#
19384 .unindent(),
19385 );
19386
19387 cx.set_head_text(&diff_base);
19388 executor.run_until_parked();
19389
19390 cx.update_editor(|editor, window, cx| {
19391 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19392 });
19393 executor.run_until_parked();
19394
19395 cx.assert_state_with_diff(
19396 r#"
19397 use some::mod1;
19398 use some::mod2;
19399
19400 const A: u32 = 42;
19401 + const B: u32 = 42;
19402 + const C: u32 = 42;
19403 + ˇ
19404
19405 fn main() {
19406 println!("hello");
19407
19408 println!("world");
19409 }
19410 "#
19411 .unindent(),
19412 );
19413
19414 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
19415 executor.run_until_parked();
19416
19417 cx.assert_state_with_diff(
19418 r#"
19419 use some::mod1;
19420 use some::mod2;
19421
19422 const A: u32 = 42;
19423 + const B: u32 = 42;
19424 + const C: u32 = 42;
19425 + const D: u32 = 42;
19426 + ˇ
19427
19428 fn main() {
19429 println!("hello");
19430
19431 println!("world");
19432 }
19433 "#
19434 .unindent(),
19435 );
19436
19437 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
19438 executor.run_until_parked();
19439
19440 cx.assert_state_with_diff(
19441 r#"
19442 use some::mod1;
19443 use some::mod2;
19444
19445 const A: u32 = 42;
19446 + const B: u32 = 42;
19447 + const C: u32 = 42;
19448 + const D: u32 = 42;
19449 + const E: u32 = 42;
19450 + ˇ
19451
19452 fn main() {
19453 println!("hello");
19454
19455 println!("world");
19456 }
19457 "#
19458 .unindent(),
19459 );
19460
19461 cx.update_editor(|editor, window, cx| {
19462 editor.delete_line(&DeleteLine, window, cx);
19463 });
19464 executor.run_until_parked();
19465
19466 cx.assert_state_with_diff(
19467 r#"
19468 use some::mod1;
19469 use some::mod2;
19470
19471 const A: u32 = 42;
19472 + const B: u32 = 42;
19473 + const C: u32 = 42;
19474 + const D: u32 = 42;
19475 + const E: u32 = 42;
19476 ˇ
19477 fn main() {
19478 println!("hello");
19479
19480 println!("world");
19481 }
19482 "#
19483 .unindent(),
19484 );
19485
19486 cx.update_editor(|editor, window, cx| {
19487 editor.move_up(&MoveUp, window, cx);
19488 editor.delete_line(&DeleteLine, window, cx);
19489 editor.move_up(&MoveUp, window, cx);
19490 editor.delete_line(&DeleteLine, window, cx);
19491 editor.move_up(&MoveUp, window, cx);
19492 editor.delete_line(&DeleteLine, window, cx);
19493 });
19494 executor.run_until_parked();
19495 cx.assert_state_with_diff(
19496 r#"
19497 use some::mod1;
19498 use some::mod2;
19499
19500 const A: u32 = 42;
19501 + const B: u32 = 42;
19502 ˇ
19503 fn main() {
19504 println!("hello");
19505
19506 println!("world");
19507 }
19508 "#
19509 .unindent(),
19510 );
19511
19512 cx.update_editor(|editor, window, cx| {
19513 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
19514 editor.delete_line(&DeleteLine, window, cx);
19515 });
19516 executor.run_until_parked();
19517 cx.assert_state_with_diff(
19518 r#"
19519 ˇ
19520 fn main() {
19521 println!("hello");
19522
19523 println!("world");
19524 }
19525 "#
19526 .unindent(),
19527 );
19528}
19529
19530#[gpui::test]
19531async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
19532 init_test(cx, |_| {});
19533
19534 let mut cx = EditorTestContext::new(cx).await;
19535 cx.set_head_text(indoc! { "
19536 one
19537 two
19538 three
19539 four
19540 five
19541 "
19542 });
19543 cx.set_state(indoc! { "
19544 one
19545 ˇthree
19546 five
19547 "});
19548 cx.run_until_parked();
19549 cx.update_editor(|editor, window, cx| {
19550 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19551 });
19552 cx.assert_state_with_diff(
19553 indoc! { "
19554 one
19555 - two
19556 ˇthree
19557 - four
19558 five
19559 "}
19560 .to_string(),
19561 );
19562 cx.update_editor(|editor, window, cx| {
19563 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19564 });
19565
19566 cx.assert_state_with_diff(
19567 indoc! { "
19568 one
19569 ˇthree
19570 five
19571 "}
19572 .to_string(),
19573 );
19574
19575 cx.set_state(indoc! { "
19576 one
19577 ˇTWO
19578 three
19579 four
19580 five
19581 "});
19582 cx.run_until_parked();
19583 cx.update_editor(|editor, window, cx| {
19584 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19585 });
19586
19587 cx.assert_state_with_diff(
19588 indoc! { "
19589 one
19590 - two
19591 + ˇTWO
19592 three
19593 four
19594 five
19595 "}
19596 .to_string(),
19597 );
19598 cx.update_editor(|editor, window, cx| {
19599 editor.move_up(&Default::default(), window, cx);
19600 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19601 });
19602 cx.assert_state_with_diff(
19603 indoc! { "
19604 one
19605 ˇTWO
19606 three
19607 four
19608 five
19609 "}
19610 .to_string(),
19611 );
19612}
19613
19614#[gpui::test]
19615async fn test_edits_around_expanded_deletion_hunks(
19616 executor: BackgroundExecutor,
19617 cx: &mut TestAppContext,
19618) {
19619 init_test(cx, |_| {});
19620
19621 let mut cx = EditorTestContext::new(cx).await;
19622
19623 let diff_base = r#"
19624 use some::mod1;
19625 use some::mod2;
19626
19627 const A: u32 = 42;
19628 const B: u32 = 42;
19629 const C: u32 = 42;
19630
19631
19632 fn main() {
19633 println!("hello");
19634
19635 println!("world");
19636 }
19637 "#
19638 .unindent();
19639 executor.run_until_parked();
19640 cx.set_state(
19641 &r#"
19642 use some::mod1;
19643 use some::mod2;
19644
19645 ˇconst B: u32 = 42;
19646 const C: u32 = 42;
19647
19648
19649 fn main() {
19650 println!("hello");
19651
19652 println!("world");
19653 }
19654 "#
19655 .unindent(),
19656 );
19657
19658 cx.set_head_text(&diff_base);
19659 executor.run_until_parked();
19660
19661 cx.update_editor(|editor, window, cx| {
19662 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19663 });
19664 executor.run_until_parked();
19665
19666 cx.assert_state_with_diff(
19667 r#"
19668 use some::mod1;
19669 use some::mod2;
19670
19671 - const A: u32 = 42;
19672 ˇconst B: u32 = 42;
19673 const C: u32 = 42;
19674
19675
19676 fn main() {
19677 println!("hello");
19678
19679 println!("world");
19680 }
19681 "#
19682 .unindent(),
19683 );
19684
19685 cx.update_editor(|editor, window, cx| {
19686 editor.delete_line(&DeleteLine, window, cx);
19687 });
19688 executor.run_until_parked();
19689 cx.assert_state_with_diff(
19690 r#"
19691 use some::mod1;
19692 use some::mod2;
19693
19694 - const A: u32 = 42;
19695 - const B: u32 = 42;
19696 ˇconst C: u32 = 42;
19697
19698
19699 fn main() {
19700 println!("hello");
19701
19702 println!("world");
19703 }
19704 "#
19705 .unindent(),
19706 );
19707
19708 cx.update_editor(|editor, window, cx| {
19709 editor.delete_line(&DeleteLine, window, cx);
19710 });
19711 executor.run_until_parked();
19712 cx.assert_state_with_diff(
19713 r#"
19714 use some::mod1;
19715 use some::mod2;
19716
19717 - const A: u32 = 42;
19718 - const B: u32 = 42;
19719 - const C: u32 = 42;
19720 ˇ
19721
19722 fn main() {
19723 println!("hello");
19724
19725 println!("world");
19726 }
19727 "#
19728 .unindent(),
19729 );
19730
19731 cx.update_editor(|editor, window, cx| {
19732 editor.handle_input("replacement", window, cx);
19733 });
19734 executor.run_until_parked();
19735 cx.assert_state_with_diff(
19736 r#"
19737 use some::mod1;
19738 use some::mod2;
19739
19740 - const A: u32 = 42;
19741 - const B: u32 = 42;
19742 - const C: u32 = 42;
19743 -
19744 + replacementˇ
19745
19746 fn main() {
19747 println!("hello");
19748
19749 println!("world");
19750 }
19751 "#
19752 .unindent(),
19753 );
19754}
19755
19756#[gpui::test]
19757async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19758 init_test(cx, |_| {});
19759
19760 let mut cx = EditorTestContext::new(cx).await;
19761
19762 let base_text = r#"
19763 one
19764 two
19765 three
19766 four
19767 five
19768 "#
19769 .unindent();
19770 executor.run_until_parked();
19771 cx.set_state(
19772 &r#"
19773 one
19774 two
19775 fˇour
19776 five
19777 "#
19778 .unindent(),
19779 );
19780
19781 cx.set_head_text(&base_text);
19782 executor.run_until_parked();
19783
19784 cx.update_editor(|editor, window, cx| {
19785 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19786 });
19787 executor.run_until_parked();
19788
19789 cx.assert_state_with_diff(
19790 r#"
19791 one
19792 two
19793 - three
19794 fˇour
19795 five
19796 "#
19797 .unindent(),
19798 );
19799
19800 cx.update_editor(|editor, window, cx| {
19801 editor.backspace(&Backspace, window, cx);
19802 editor.backspace(&Backspace, window, cx);
19803 });
19804 executor.run_until_parked();
19805 cx.assert_state_with_diff(
19806 r#"
19807 one
19808 two
19809 - threeˇ
19810 - four
19811 + our
19812 five
19813 "#
19814 .unindent(),
19815 );
19816}
19817
19818#[gpui::test]
19819async fn test_edit_after_expanded_modification_hunk(
19820 executor: BackgroundExecutor,
19821 cx: &mut TestAppContext,
19822) {
19823 init_test(cx, |_| {});
19824
19825 let mut cx = EditorTestContext::new(cx).await;
19826
19827 let diff_base = r#"
19828 use some::mod1;
19829 use some::mod2;
19830
19831 const A: u32 = 42;
19832 const B: u32 = 42;
19833 const C: u32 = 42;
19834 const D: u32 = 42;
19835
19836
19837 fn main() {
19838 println!("hello");
19839
19840 println!("world");
19841 }"#
19842 .unindent();
19843
19844 cx.set_state(
19845 &r#"
19846 use some::mod1;
19847 use some::mod2;
19848
19849 const A: u32 = 42;
19850 const B: u32 = 42;
19851 const C: u32 = 43ˇ
19852 const D: u32 = 42;
19853
19854
19855 fn main() {
19856 println!("hello");
19857
19858 println!("world");
19859 }"#
19860 .unindent(),
19861 );
19862
19863 cx.set_head_text(&diff_base);
19864 executor.run_until_parked();
19865 cx.update_editor(|editor, window, cx| {
19866 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19867 });
19868 executor.run_until_parked();
19869
19870 cx.assert_state_with_diff(
19871 r#"
19872 use some::mod1;
19873 use some::mod2;
19874
19875 const A: u32 = 42;
19876 const B: u32 = 42;
19877 - const C: u32 = 42;
19878 + const C: u32 = 43ˇ
19879 const D: u32 = 42;
19880
19881
19882 fn main() {
19883 println!("hello");
19884
19885 println!("world");
19886 }"#
19887 .unindent(),
19888 );
19889
19890 cx.update_editor(|editor, window, cx| {
19891 editor.handle_input("\nnew_line\n", window, cx);
19892 });
19893 executor.run_until_parked();
19894
19895 cx.assert_state_with_diff(
19896 r#"
19897 use some::mod1;
19898 use some::mod2;
19899
19900 const A: u32 = 42;
19901 const B: u32 = 42;
19902 - const C: u32 = 42;
19903 + const C: u32 = 43
19904 + new_line
19905 + ˇ
19906 const D: u32 = 42;
19907
19908
19909 fn main() {
19910 println!("hello");
19911
19912 println!("world");
19913 }"#
19914 .unindent(),
19915 );
19916}
19917
19918#[gpui::test]
19919async fn test_stage_and_unstage_added_file_hunk(
19920 executor: BackgroundExecutor,
19921 cx: &mut TestAppContext,
19922) {
19923 init_test(cx, |_| {});
19924
19925 let mut cx = EditorTestContext::new(cx).await;
19926 cx.update_editor(|editor, _, cx| {
19927 editor.set_expand_all_diff_hunks(cx);
19928 });
19929
19930 let working_copy = r#"
19931 ˇfn main() {
19932 println!("hello, world!");
19933 }
19934 "#
19935 .unindent();
19936
19937 cx.set_state(&working_copy);
19938 executor.run_until_parked();
19939
19940 cx.assert_state_with_diff(
19941 r#"
19942 + ˇfn main() {
19943 + println!("hello, world!");
19944 + }
19945 "#
19946 .unindent(),
19947 );
19948 cx.assert_index_text(None);
19949
19950 cx.update_editor(|editor, window, cx| {
19951 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19952 });
19953 executor.run_until_parked();
19954 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
19955 cx.assert_state_with_diff(
19956 r#"
19957 + ˇfn main() {
19958 + println!("hello, world!");
19959 + }
19960 "#
19961 .unindent(),
19962 );
19963
19964 cx.update_editor(|editor, window, cx| {
19965 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19966 });
19967 executor.run_until_parked();
19968 cx.assert_index_text(None);
19969}
19970
19971async fn setup_indent_guides_editor(
19972 text: &str,
19973 cx: &mut TestAppContext,
19974) -> (BufferId, EditorTestContext) {
19975 init_test(cx, |_| {});
19976
19977 let mut cx = EditorTestContext::new(cx).await;
19978
19979 let buffer_id = cx.update_editor(|editor, window, cx| {
19980 editor.set_text(text, window, cx);
19981 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
19982
19983 buffer_ids[0]
19984 });
19985
19986 (buffer_id, cx)
19987}
19988
19989fn assert_indent_guides(
19990 range: Range<u32>,
19991 expected: Vec<IndentGuide>,
19992 active_indices: Option<Vec<usize>>,
19993 cx: &mut EditorTestContext,
19994) {
19995 let indent_guides = cx.update_editor(|editor, window, cx| {
19996 let snapshot = editor.snapshot(window, cx).display_snapshot;
19997 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
19998 editor,
19999 MultiBufferRow(range.start)..MultiBufferRow(range.end),
20000 true,
20001 &snapshot,
20002 cx,
20003 );
20004
20005 indent_guides.sort_by(|a, b| {
20006 a.depth.cmp(&b.depth).then(
20007 a.start_row
20008 .cmp(&b.start_row)
20009 .then(a.end_row.cmp(&b.end_row)),
20010 )
20011 });
20012 indent_guides
20013 });
20014
20015 if let Some(expected) = active_indices {
20016 let active_indices = cx.update_editor(|editor, window, cx| {
20017 let snapshot = editor.snapshot(window, cx).display_snapshot;
20018 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
20019 });
20020
20021 assert_eq!(
20022 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
20023 expected,
20024 "Active indent guide indices do not match"
20025 );
20026 }
20027
20028 assert_eq!(indent_guides, expected, "Indent guides do not match");
20029}
20030
20031fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
20032 IndentGuide {
20033 buffer_id,
20034 start_row: MultiBufferRow(start_row),
20035 end_row: MultiBufferRow(end_row),
20036 depth,
20037 tab_size: 4,
20038 settings: IndentGuideSettings {
20039 enabled: true,
20040 line_width: 1,
20041 active_line_width: 1,
20042 coloring: IndentGuideColoring::default(),
20043 background_coloring: IndentGuideBackgroundColoring::default(),
20044 },
20045 }
20046}
20047
20048#[gpui::test]
20049async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
20050 let (buffer_id, mut cx) = setup_indent_guides_editor(
20051 &"
20052 fn main() {
20053 let a = 1;
20054 }"
20055 .unindent(),
20056 cx,
20057 )
20058 .await;
20059
20060 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20061}
20062
20063#[gpui::test]
20064async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
20065 let (buffer_id, mut cx) = setup_indent_guides_editor(
20066 &"
20067 fn main() {
20068 let a = 1;
20069 let b = 2;
20070 }"
20071 .unindent(),
20072 cx,
20073 )
20074 .await;
20075
20076 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
20077}
20078
20079#[gpui::test]
20080async fn test_indent_guide_nested(cx: &mut TestAppContext) {
20081 let (buffer_id, mut cx) = setup_indent_guides_editor(
20082 &"
20083 fn main() {
20084 let a = 1;
20085 if a == 3 {
20086 let b = 2;
20087 } else {
20088 let c = 3;
20089 }
20090 }"
20091 .unindent(),
20092 cx,
20093 )
20094 .await;
20095
20096 assert_indent_guides(
20097 0..8,
20098 vec![
20099 indent_guide(buffer_id, 1, 6, 0),
20100 indent_guide(buffer_id, 3, 3, 1),
20101 indent_guide(buffer_id, 5, 5, 1),
20102 ],
20103 None,
20104 &mut cx,
20105 );
20106}
20107
20108#[gpui::test]
20109async fn test_indent_guide_tab(cx: &mut TestAppContext) {
20110 let (buffer_id, mut cx) = setup_indent_guides_editor(
20111 &"
20112 fn main() {
20113 let a = 1;
20114 let b = 2;
20115 let c = 3;
20116 }"
20117 .unindent(),
20118 cx,
20119 )
20120 .await;
20121
20122 assert_indent_guides(
20123 0..5,
20124 vec![
20125 indent_guide(buffer_id, 1, 3, 0),
20126 indent_guide(buffer_id, 2, 2, 1),
20127 ],
20128 None,
20129 &mut cx,
20130 );
20131}
20132
20133#[gpui::test]
20134async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
20135 let (buffer_id, mut cx) = setup_indent_guides_editor(
20136 &"
20137 fn main() {
20138 let a = 1;
20139
20140 let c = 3;
20141 }"
20142 .unindent(),
20143 cx,
20144 )
20145 .await;
20146
20147 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
20148}
20149
20150#[gpui::test]
20151async fn test_indent_guide_complex(cx: &mut TestAppContext) {
20152 let (buffer_id, mut cx) = setup_indent_guides_editor(
20153 &"
20154 fn main() {
20155 let a = 1;
20156
20157 let c = 3;
20158
20159 if a == 3 {
20160 let b = 2;
20161 } else {
20162 let c = 3;
20163 }
20164 }"
20165 .unindent(),
20166 cx,
20167 )
20168 .await;
20169
20170 assert_indent_guides(
20171 0..11,
20172 vec![
20173 indent_guide(buffer_id, 1, 9, 0),
20174 indent_guide(buffer_id, 6, 6, 1),
20175 indent_guide(buffer_id, 8, 8, 1),
20176 ],
20177 None,
20178 &mut cx,
20179 );
20180}
20181
20182#[gpui::test]
20183async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
20184 let (buffer_id, mut cx) = setup_indent_guides_editor(
20185 &"
20186 fn main() {
20187 let a = 1;
20188
20189 let c = 3;
20190
20191 if a == 3 {
20192 let b = 2;
20193 } else {
20194 let c = 3;
20195 }
20196 }"
20197 .unindent(),
20198 cx,
20199 )
20200 .await;
20201
20202 assert_indent_guides(
20203 1..11,
20204 vec![
20205 indent_guide(buffer_id, 1, 9, 0),
20206 indent_guide(buffer_id, 6, 6, 1),
20207 indent_guide(buffer_id, 8, 8, 1),
20208 ],
20209 None,
20210 &mut cx,
20211 );
20212}
20213
20214#[gpui::test]
20215async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
20216 let (buffer_id, mut cx) = setup_indent_guides_editor(
20217 &"
20218 fn main() {
20219 let a = 1;
20220
20221 let c = 3;
20222
20223 if a == 3 {
20224 let b = 2;
20225 } else {
20226 let c = 3;
20227 }
20228 }"
20229 .unindent(),
20230 cx,
20231 )
20232 .await;
20233
20234 assert_indent_guides(
20235 1..10,
20236 vec![
20237 indent_guide(buffer_id, 1, 9, 0),
20238 indent_guide(buffer_id, 6, 6, 1),
20239 indent_guide(buffer_id, 8, 8, 1),
20240 ],
20241 None,
20242 &mut cx,
20243 );
20244}
20245
20246#[gpui::test]
20247async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
20248 let (buffer_id, mut cx) = setup_indent_guides_editor(
20249 &"
20250 fn main() {
20251 if a {
20252 b(
20253 c,
20254 d,
20255 )
20256 } else {
20257 e(
20258 f
20259 )
20260 }
20261 }"
20262 .unindent(),
20263 cx,
20264 )
20265 .await;
20266
20267 assert_indent_guides(
20268 0..11,
20269 vec![
20270 indent_guide(buffer_id, 1, 10, 0),
20271 indent_guide(buffer_id, 2, 5, 1),
20272 indent_guide(buffer_id, 7, 9, 1),
20273 indent_guide(buffer_id, 3, 4, 2),
20274 indent_guide(buffer_id, 8, 8, 2),
20275 ],
20276 None,
20277 &mut cx,
20278 );
20279
20280 cx.update_editor(|editor, window, cx| {
20281 editor.fold_at(MultiBufferRow(2), window, cx);
20282 assert_eq!(
20283 editor.display_text(cx),
20284 "
20285 fn main() {
20286 if a {
20287 b(⋯
20288 )
20289 } else {
20290 e(
20291 f
20292 )
20293 }
20294 }"
20295 .unindent()
20296 );
20297 });
20298
20299 assert_indent_guides(
20300 0..11,
20301 vec![
20302 indent_guide(buffer_id, 1, 10, 0),
20303 indent_guide(buffer_id, 2, 5, 1),
20304 indent_guide(buffer_id, 7, 9, 1),
20305 indent_guide(buffer_id, 8, 8, 2),
20306 ],
20307 None,
20308 &mut cx,
20309 );
20310}
20311
20312#[gpui::test]
20313async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
20314 let (buffer_id, mut cx) = setup_indent_guides_editor(
20315 &"
20316 block1
20317 block2
20318 block3
20319 block4
20320 block2
20321 block1
20322 block1"
20323 .unindent(),
20324 cx,
20325 )
20326 .await;
20327
20328 assert_indent_guides(
20329 1..10,
20330 vec![
20331 indent_guide(buffer_id, 1, 4, 0),
20332 indent_guide(buffer_id, 2, 3, 1),
20333 indent_guide(buffer_id, 3, 3, 2),
20334 ],
20335 None,
20336 &mut cx,
20337 );
20338}
20339
20340#[gpui::test]
20341async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
20342 let (buffer_id, mut cx) = setup_indent_guides_editor(
20343 &"
20344 block1
20345 block2
20346 block3
20347
20348 block1
20349 block1"
20350 .unindent(),
20351 cx,
20352 )
20353 .await;
20354
20355 assert_indent_guides(
20356 0..6,
20357 vec![
20358 indent_guide(buffer_id, 1, 2, 0),
20359 indent_guide(buffer_id, 2, 2, 1),
20360 ],
20361 None,
20362 &mut cx,
20363 );
20364}
20365
20366#[gpui::test]
20367async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
20368 let (buffer_id, mut cx) = setup_indent_guides_editor(
20369 &"
20370 function component() {
20371 \treturn (
20372 \t\t\t
20373 \t\t<div>
20374 \t\t\t<abc></abc>
20375 \t\t</div>
20376 \t)
20377 }"
20378 .unindent(),
20379 cx,
20380 )
20381 .await;
20382
20383 assert_indent_guides(
20384 0..8,
20385 vec![
20386 indent_guide(buffer_id, 1, 6, 0),
20387 indent_guide(buffer_id, 2, 5, 1),
20388 indent_guide(buffer_id, 4, 4, 2),
20389 ],
20390 None,
20391 &mut cx,
20392 );
20393}
20394
20395#[gpui::test]
20396async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
20397 let (buffer_id, mut cx) = setup_indent_guides_editor(
20398 &"
20399 function component() {
20400 \treturn (
20401 \t
20402 \t\t<div>
20403 \t\t\t<abc></abc>
20404 \t\t</div>
20405 \t)
20406 }"
20407 .unindent(),
20408 cx,
20409 )
20410 .await;
20411
20412 assert_indent_guides(
20413 0..8,
20414 vec![
20415 indent_guide(buffer_id, 1, 6, 0),
20416 indent_guide(buffer_id, 2, 5, 1),
20417 indent_guide(buffer_id, 4, 4, 2),
20418 ],
20419 None,
20420 &mut cx,
20421 );
20422}
20423
20424#[gpui::test]
20425async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
20426 let (buffer_id, mut cx) = setup_indent_guides_editor(
20427 &"
20428 block1
20429
20430
20431
20432 block2
20433 "
20434 .unindent(),
20435 cx,
20436 )
20437 .await;
20438
20439 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20440}
20441
20442#[gpui::test]
20443async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
20444 let (buffer_id, mut cx) = setup_indent_guides_editor(
20445 &"
20446 def a:
20447 \tb = 3
20448 \tif True:
20449 \t\tc = 4
20450 \t\td = 5
20451 \tprint(b)
20452 "
20453 .unindent(),
20454 cx,
20455 )
20456 .await;
20457
20458 assert_indent_guides(
20459 0..6,
20460 vec![
20461 indent_guide(buffer_id, 1, 5, 0),
20462 indent_guide(buffer_id, 3, 4, 1),
20463 ],
20464 None,
20465 &mut cx,
20466 );
20467}
20468
20469#[gpui::test]
20470async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
20471 let (buffer_id, mut cx) = setup_indent_guides_editor(
20472 &"
20473 fn main() {
20474 let a = 1;
20475 }"
20476 .unindent(),
20477 cx,
20478 )
20479 .await;
20480
20481 cx.update_editor(|editor, window, cx| {
20482 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20483 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20484 });
20485 });
20486
20487 assert_indent_guides(
20488 0..3,
20489 vec![indent_guide(buffer_id, 1, 1, 0)],
20490 Some(vec![0]),
20491 &mut cx,
20492 );
20493}
20494
20495#[gpui::test]
20496async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
20497 let (buffer_id, mut cx) = setup_indent_guides_editor(
20498 &"
20499 fn main() {
20500 if 1 == 2 {
20501 let a = 1;
20502 }
20503 }"
20504 .unindent(),
20505 cx,
20506 )
20507 .await;
20508
20509 cx.update_editor(|editor, window, cx| {
20510 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20511 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20512 });
20513 });
20514
20515 assert_indent_guides(
20516 0..4,
20517 vec![
20518 indent_guide(buffer_id, 1, 3, 0),
20519 indent_guide(buffer_id, 2, 2, 1),
20520 ],
20521 Some(vec![1]),
20522 &mut cx,
20523 );
20524
20525 cx.update_editor(|editor, window, cx| {
20526 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20527 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20528 });
20529 });
20530
20531 assert_indent_guides(
20532 0..4,
20533 vec![
20534 indent_guide(buffer_id, 1, 3, 0),
20535 indent_guide(buffer_id, 2, 2, 1),
20536 ],
20537 Some(vec![1]),
20538 &mut cx,
20539 );
20540
20541 cx.update_editor(|editor, window, cx| {
20542 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20543 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
20544 });
20545 });
20546
20547 assert_indent_guides(
20548 0..4,
20549 vec![
20550 indent_guide(buffer_id, 1, 3, 0),
20551 indent_guide(buffer_id, 2, 2, 1),
20552 ],
20553 Some(vec![0]),
20554 &mut cx,
20555 );
20556}
20557
20558#[gpui::test]
20559async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
20560 let (buffer_id, mut cx) = setup_indent_guides_editor(
20561 &"
20562 fn main() {
20563 let a = 1;
20564
20565 let b = 2;
20566 }"
20567 .unindent(),
20568 cx,
20569 )
20570 .await;
20571
20572 cx.update_editor(|editor, window, cx| {
20573 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20574 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20575 });
20576 });
20577
20578 assert_indent_guides(
20579 0..5,
20580 vec![indent_guide(buffer_id, 1, 3, 0)],
20581 Some(vec![0]),
20582 &mut cx,
20583 );
20584}
20585
20586#[gpui::test]
20587async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
20588 let (buffer_id, mut cx) = setup_indent_guides_editor(
20589 &"
20590 def m:
20591 a = 1
20592 pass"
20593 .unindent(),
20594 cx,
20595 )
20596 .await;
20597
20598 cx.update_editor(|editor, window, cx| {
20599 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20600 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20601 });
20602 });
20603
20604 assert_indent_guides(
20605 0..3,
20606 vec![indent_guide(buffer_id, 1, 2, 0)],
20607 Some(vec![0]),
20608 &mut cx,
20609 );
20610}
20611
20612#[gpui::test]
20613async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
20614 init_test(cx, |_| {});
20615 let mut cx = EditorTestContext::new(cx).await;
20616 let text = indoc! {
20617 "
20618 impl A {
20619 fn b() {
20620 0;
20621 3;
20622 5;
20623 6;
20624 7;
20625 }
20626 }
20627 "
20628 };
20629 let base_text = indoc! {
20630 "
20631 impl A {
20632 fn b() {
20633 0;
20634 1;
20635 2;
20636 3;
20637 4;
20638 }
20639 fn c() {
20640 5;
20641 6;
20642 7;
20643 }
20644 }
20645 "
20646 };
20647
20648 cx.update_editor(|editor, window, cx| {
20649 editor.set_text(text, window, cx);
20650
20651 editor.buffer().update(cx, |multibuffer, cx| {
20652 let buffer = multibuffer.as_singleton().unwrap();
20653 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
20654
20655 multibuffer.set_all_diff_hunks_expanded(cx);
20656 multibuffer.add_diff(diff, cx);
20657
20658 buffer.read(cx).remote_id()
20659 })
20660 });
20661 cx.run_until_parked();
20662
20663 cx.assert_state_with_diff(
20664 indoc! { "
20665 impl A {
20666 fn b() {
20667 0;
20668 - 1;
20669 - 2;
20670 3;
20671 - 4;
20672 - }
20673 - fn c() {
20674 5;
20675 6;
20676 7;
20677 }
20678 }
20679 ˇ"
20680 }
20681 .to_string(),
20682 );
20683
20684 let mut actual_guides = cx.update_editor(|editor, window, cx| {
20685 editor
20686 .snapshot(window, cx)
20687 .buffer_snapshot
20688 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
20689 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
20690 .collect::<Vec<_>>()
20691 });
20692 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
20693 assert_eq!(
20694 actual_guides,
20695 vec![
20696 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
20697 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
20698 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
20699 ]
20700 );
20701}
20702
20703#[gpui::test]
20704async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
20705 init_test(cx, |_| {});
20706 let mut cx = EditorTestContext::new(cx).await;
20707
20708 let diff_base = r#"
20709 a
20710 b
20711 c
20712 "#
20713 .unindent();
20714
20715 cx.set_state(
20716 &r#"
20717 ˇA
20718 b
20719 C
20720 "#
20721 .unindent(),
20722 );
20723 cx.set_head_text(&diff_base);
20724 cx.update_editor(|editor, window, cx| {
20725 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20726 });
20727 executor.run_until_parked();
20728
20729 let both_hunks_expanded = r#"
20730 - a
20731 + ˇA
20732 b
20733 - c
20734 + C
20735 "#
20736 .unindent();
20737
20738 cx.assert_state_with_diff(both_hunks_expanded.clone());
20739
20740 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20741 let snapshot = editor.snapshot(window, cx);
20742 let hunks = editor
20743 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
20744 .collect::<Vec<_>>();
20745 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20746 let buffer_id = hunks[0].buffer_id;
20747 hunks
20748 .into_iter()
20749 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20750 .collect::<Vec<_>>()
20751 });
20752 assert_eq!(hunk_ranges.len(), 2);
20753
20754 cx.update_editor(|editor, _, cx| {
20755 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20756 });
20757 executor.run_until_parked();
20758
20759 let second_hunk_expanded = r#"
20760 ˇA
20761 b
20762 - c
20763 + C
20764 "#
20765 .unindent();
20766
20767 cx.assert_state_with_diff(second_hunk_expanded);
20768
20769 cx.update_editor(|editor, _, cx| {
20770 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20771 });
20772 executor.run_until_parked();
20773
20774 cx.assert_state_with_diff(both_hunks_expanded.clone());
20775
20776 cx.update_editor(|editor, _, cx| {
20777 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20778 });
20779 executor.run_until_parked();
20780
20781 let first_hunk_expanded = r#"
20782 - a
20783 + ˇA
20784 b
20785 C
20786 "#
20787 .unindent();
20788
20789 cx.assert_state_with_diff(first_hunk_expanded);
20790
20791 cx.update_editor(|editor, _, cx| {
20792 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20793 });
20794 executor.run_until_parked();
20795
20796 cx.assert_state_with_diff(both_hunks_expanded);
20797
20798 cx.set_state(
20799 &r#"
20800 ˇA
20801 b
20802 "#
20803 .unindent(),
20804 );
20805 cx.run_until_parked();
20806
20807 // TODO this cursor position seems bad
20808 cx.assert_state_with_diff(
20809 r#"
20810 - ˇa
20811 + A
20812 b
20813 "#
20814 .unindent(),
20815 );
20816
20817 cx.update_editor(|editor, window, cx| {
20818 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20819 });
20820
20821 cx.assert_state_with_diff(
20822 r#"
20823 - ˇa
20824 + A
20825 b
20826 - c
20827 "#
20828 .unindent(),
20829 );
20830
20831 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20832 let snapshot = editor.snapshot(window, cx);
20833 let hunks = editor
20834 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
20835 .collect::<Vec<_>>();
20836 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20837 let buffer_id = hunks[0].buffer_id;
20838 hunks
20839 .into_iter()
20840 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20841 .collect::<Vec<_>>()
20842 });
20843 assert_eq!(hunk_ranges.len(), 2);
20844
20845 cx.update_editor(|editor, _, cx| {
20846 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20847 });
20848 executor.run_until_parked();
20849
20850 cx.assert_state_with_diff(
20851 r#"
20852 - ˇa
20853 + A
20854 b
20855 "#
20856 .unindent(),
20857 );
20858}
20859
20860#[gpui::test]
20861async fn test_toggle_deletion_hunk_at_start_of_file(
20862 executor: BackgroundExecutor,
20863 cx: &mut TestAppContext,
20864) {
20865 init_test(cx, |_| {});
20866 let mut cx = EditorTestContext::new(cx).await;
20867
20868 let diff_base = r#"
20869 a
20870 b
20871 c
20872 "#
20873 .unindent();
20874
20875 cx.set_state(
20876 &r#"
20877 ˇb
20878 c
20879 "#
20880 .unindent(),
20881 );
20882 cx.set_head_text(&diff_base);
20883 cx.update_editor(|editor, window, cx| {
20884 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20885 });
20886 executor.run_until_parked();
20887
20888 let hunk_expanded = r#"
20889 - a
20890 ˇb
20891 c
20892 "#
20893 .unindent();
20894
20895 cx.assert_state_with_diff(hunk_expanded.clone());
20896
20897 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20898 let snapshot = editor.snapshot(window, cx);
20899 let hunks = editor
20900 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
20901 .collect::<Vec<_>>();
20902 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20903 let buffer_id = hunks[0].buffer_id;
20904 hunks
20905 .into_iter()
20906 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20907 .collect::<Vec<_>>()
20908 });
20909 assert_eq!(hunk_ranges.len(), 1);
20910
20911 cx.update_editor(|editor, _, cx| {
20912 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20913 });
20914 executor.run_until_parked();
20915
20916 let hunk_collapsed = r#"
20917 ˇb
20918 c
20919 "#
20920 .unindent();
20921
20922 cx.assert_state_with_diff(hunk_collapsed);
20923
20924 cx.update_editor(|editor, _, cx| {
20925 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20926 });
20927 executor.run_until_parked();
20928
20929 cx.assert_state_with_diff(hunk_expanded);
20930}
20931
20932#[gpui::test]
20933async fn test_display_diff_hunks(cx: &mut TestAppContext) {
20934 init_test(cx, |_| {});
20935
20936 let fs = FakeFs::new(cx.executor());
20937 fs.insert_tree(
20938 path!("/test"),
20939 json!({
20940 ".git": {},
20941 "file-1": "ONE\n",
20942 "file-2": "TWO\n",
20943 "file-3": "THREE\n",
20944 }),
20945 )
20946 .await;
20947
20948 fs.set_head_for_repo(
20949 path!("/test/.git").as_ref(),
20950 &[
20951 ("file-1", "one\n".into()),
20952 ("file-2", "two\n".into()),
20953 ("file-3", "three\n".into()),
20954 ],
20955 "deadbeef",
20956 );
20957
20958 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
20959 let mut buffers = vec![];
20960 for i in 1..=3 {
20961 let buffer = project
20962 .update(cx, |project, cx| {
20963 let path = format!(path!("/test/file-{}"), i);
20964 project.open_local_buffer(path, cx)
20965 })
20966 .await
20967 .unwrap();
20968 buffers.push(buffer);
20969 }
20970
20971 let multibuffer = cx.new(|cx| {
20972 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
20973 multibuffer.set_all_diff_hunks_expanded(cx);
20974 for buffer in &buffers {
20975 let snapshot = buffer.read(cx).snapshot();
20976 multibuffer.set_excerpts_for_path(
20977 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
20978 buffer.clone(),
20979 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
20980 2,
20981 cx,
20982 );
20983 }
20984 multibuffer
20985 });
20986
20987 let editor = cx.add_window(|window, cx| {
20988 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
20989 });
20990 cx.run_until_parked();
20991
20992 let snapshot = editor
20993 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
20994 .unwrap();
20995 let hunks = snapshot
20996 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
20997 .map(|hunk| match hunk {
20998 DisplayDiffHunk::Unfolded {
20999 display_row_range, ..
21000 } => display_row_range,
21001 DisplayDiffHunk::Folded { .. } => unreachable!(),
21002 })
21003 .collect::<Vec<_>>();
21004 assert_eq!(
21005 hunks,
21006 [
21007 DisplayRow(2)..DisplayRow(4),
21008 DisplayRow(7)..DisplayRow(9),
21009 DisplayRow(12)..DisplayRow(14),
21010 ]
21011 );
21012}
21013
21014#[gpui::test]
21015async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
21016 init_test(cx, |_| {});
21017
21018 let mut cx = EditorTestContext::new(cx).await;
21019 cx.set_head_text(indoc! { "
21020 one
21021 two
21022 three
21023 four
21024 five
21025 "
21026 });
21027 cx.set_index_text(indoc! { "
21028 one
21029 two
21030 three
21031 four
21032 five
21033 "
21034 });
21035 cx.set_state(indoc! {"
21036 one
21037 TWO
21038 ˇTHREE
21039 FOUR
21040 five
21041 "});
21042 cx.run_until_parked();
21043 cx.update_editor(|editor, window, cx| {
21044 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21045 });
21046 cx.run_until_parked();
21047 cx.assert_index_text(Some(indoc! {"
21048 one
21049 TWO
21050 THREE
21051 FOUR
21052 five
21053 "}));
21054 cx.set_state(indoc! { "
21055 one
21056 TWO
21057 ˇTHREE-HUNDRED
21058 FOUR
21059 five
21060 "});
21061 cx.run_until_parked();
21062 cx.update_editor(|editor, window, cx| {
21063 let snapshot = editor.snapshot(window, cx);
21064 let hunks = editor
21065 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
21066 .collect::<Vec<_>>();
21067 assert_eq!(hunks.len(), 1);
21068 assert_eq!(
21069 hunks[0].status(),
21070 DiffHunkStatus {
21071 kind: DiffHunkStatusKind::Modified,
21072 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
21073 }
21074 );
21075
21076 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21077 });
21078 cx.run_until_parked();
21079 cx.assert_index_text(Some(indoc! {"
21080 one
21081 TWO
21082 THREE-HUNDRED
21083 FOUR
21084 five
21085 "}));
21086}
21087
21088#[gpui::test]
21089fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
21090 init_test(cx, |_| {});
21091
21092 let editor = cx.add_window(|window, cx| {
21093 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
21094 build_editor(buffer, window, cx)
21095 });
21096
21097 let render_args = Arc::new(Mutex::new(None));
21098 let snapshot = editor
21099 .update(cx, |editor, window, cx| {
21100 let snapshot = editor.buffer().read(cx).snapshot(cx);
21101 let range =
21102 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
21103
21104 struct RenderArgs {
21105 row: MultiBufferRow,
21106 folded: bool,
21107 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
21108 }
21109
21110 let crease = Crease::inline(
21111 range,
21112 FoldPlaceholder::test(),
21113 {
21114 let toggle_callback = render_args.clone();
21115 move |row, folded, callback, _window, _cx| {
21116 *toggle_callback.lock() = Some(RenderArgs {
21117 row,
21118 folded,
21119 callback,
21120 });
21121 div()
21122 }
21123 },
21124 |_row, _folded, _window, _cx| div(),
21125 );
21126
21127 editor.insert_creases(Some(crease), cx);
21128 let snapshot = editor.snapshot(window, cx);
21129 let _div =
21130 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
21131 snapshot
21132 })
21133 .unwrap();
21134
21135 let render_args = render_args.lock().take().unwrap();
21136 assert_eq!(render_args.row, MultiBufferRow(1));
21137 assert!(!render_args.folded);
21138 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21139
21140 cx.update_window(*editor, |_, window, cx| {
21141 (render_args.callback)(true, window, cx)
21142 })
21143 .unwrap();
21144 let snapshot = editor
21145 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21146 .unwrap();
21147 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
21148
21149 cx.update_window(*editor, |_, window, cx| {
21150 (render_args.callback)(false, window, cx)
21151 })
21152 .unwrap();
21153 let snapshot = editor
21154 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21155 .unwrap();
21156 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21157}
21158
21159#[gpui::test]
21160async fn test_input_text(cx: &mut TestAppContext) {
21161 init_test(cx, |_| {});
21162 let mut cx = EditorTestContext::new(cx).await;
21163
21164 cx.set_state(
21165 &r#"ˇone
21166 two
21167
21168 three
21169 fourˇ
21170 five
21171
21172 siˇx"#
21173 .unindent(),
21174 );
21175
21176 cx.dispatch_action(HandleInput(String::new()));
21177 cx.assert_editor_state(
21178 &r#"ˇone
21179 two
21180
21181 three
21182 fourˇ
21183 five
21184
21185 siˇx"#
21186 .unindent(),
21187 );
21188
21189 cx.dispatch_action(HandleInput("AAAA".to_string()));
21190 cx.assert_editor_state(
21191 &r#"AAAAˇone
21192 two
21193
21194 three
21195 fourAAAAˇ
21196 five
21197
21198 siAAAAˇx"#
21199 .unindent(),
21200 );
21201}
21202
21203#[gpui::test]
21204async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
21205 init_test(cx, |_| {});
21206
21207 let mut cx = EditorTestContext::new(cx).await;
21208 cx.set_state(
21209 r#"let foo = 1;
21210let foo = 2;
21211let foo = 3;
21212let fooˇ = 4;
21213let foo = 5;
21214let foo = 6;
21215let foo = 7;
21216let foo = 8;
21217let foo = 9;
21218let foo = 10;
21219let foo = 11;
21220let foo = 12;
21221let foo = 13;
21222let foo = 14;
21223let foo = 15;"#,
21224 );
21225
21226 cx.update_editor(|e, window, cx| {
21227 assert_eq!(
21228 e.next_scroll_position,
21229 NextScrollCursorCenterTopBottom::Center,
21230 "Default next scroll direction is center",
21231 );
21232
21233 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21234 assert_eq!(
21235 e.next_scroll_position,
21236 NextScrollCursorCenterTopBottom::Top,
21237 "After center, next scroll direction should be top",
21238 );
21239
21240 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21241 assert_eq!(
21242 e.next_scroll_position,
21243 NextScrollCursorCenterTopBottom::Bottom,
21244 "After top, next scroll direction should be bottom",
21245 );
21246
21247 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21248 assert_eq!(
21249 e.next_scroll_position,
21250 NextScrollCursorCenterTopBottom::Center,
21251 "After bottom, scrolling should start over",
21252 );
21253
21254 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21255 assert_eq!(
21256 e.next_scroll_position,
21257 NextScrollCursorCenterTopBottom::Top,
21258 "Scrolling continues if retriggered fast enough"
21259 );
21260 });
21261
21262 cx.executor()
21263 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
21264 cx.executor().run_until_parked();
21265 cx.update_editor(|e, _, _| {
21266 assert_eq!(
21267 e.next_scroll_position,
21268 NextScrollCursorCenterTopBottom::Center,
21269 "If scrolling is not triggered fast enough, it should reset"
21270 );
21271 });
21272}
21273
21274#[gpui::test]
21275async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
21276 init_test(cx, |_| {});
21277 let mut cx = EditorLspTestContext::new_rust(
21278 lsp::ServerCapabilities {
21279 definition_provider: Some(lsp::OneOf::Left(true)),
21280 references_provider: Some(lsp::OneOf::Left(true)),
21281 ..lsp::ServerCapabilities::default()
21282 },
21283 cx,
21284 )
21285 .await;
21286
21287 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
21288 let go_to_definition = cx
21289 .lsp
21290 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21291 move |params, _| async move {
21292 if empty_go_to_definition {
21293 Ok(None)
21294 } else {
21295 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
21296 uri: params.text_document_position_params.text_document.uri,
21297 range: lsp::Range::new(
21298 lsp::Position::new(4, 3),
21299 lsp::Position::new(4, 6),
21300 ),
21301 })))
21302 }
21303 },
21304 );
21305 let references = cx
21306 .lsp
21307 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21308 Ok(Some(vec![lsp::Location {
21309 uri: params.text_document_position.text_document.uri,
21310 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
21311 }]))
21312 });
21313 (go_to_definition, references)
21314 };
21315
21316 cx.set_state(
21317 &r#"fn one() {
21318 let mut a = ˇtwo();
21319 }
21320
21321 fn two() {}"#
21322 .unindent(),
21323 );
21324 set_up_lsp_handlers(false, &mut cx);
21325 let navigated = cx
21326 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21327 .await
21328 .expect("Failed to navigate to definition");
21329 assert_eq!(
21330 navigated,
21331 Navigated::Yes,
21332 "Should have navigated to definition from the GetDefinition response"
21333 );
21334 cx.assert_editor_state(
21335 &r#"fn one() {
21336 let mut a = two();
21337 }
21338
21339 fn «twoˇ»() {}"#
21340 .unindent(),
21341 );
21342
21343 let editors = cx.update_workspace(|workspace, _, cx| {
21344 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21345 });
21346 cx.update_editor(|_, _, test_editor_cx| {
21347 assert_eq!(
21348 editors.len(),
21349 1,
21350 "Initially, only one, test, editor should be open in the workspace"
21351 );
21352 assert_eq!(
21353 test_editor_cx.entity(),
21354 editors.last().expect("Asserted len is 1").clone()
21355 );
21356 });
21357
21358 set_up_lsp_handlers(true, &mut cx);
21359 let navigated = cx
21360 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21361 .await
21362 .expect("Failed to navigate to lookup references");
21363 assert_eq!(
21364 navigated,
21365 Navigated::Yes,
21366 "Should have navigated to references as a fallback after empty GoToDefinition response"
21367 );
21368 // We should not change the selections in the existing file,
21369 // if opening another milti buffer with the references
21370 cx.assert_editor_state(
21371 &r#"fn one() {
21372 let mut a = two();
21373 }
21374
21375 fn «twoˇ»() {}"#
21376 .unindent(),
21377 );
21378 let editors = cx.update_workspace(|workspace, _, cx| {
21379 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21380 });
21381 cx.update_editor(|_, _, test_editor_cx| {
21382 assert_eq!(
21383 editors.len(),
21384 2,
21385 "After falling back to references search, we open a new editor with the results"
21386 );
21387 let references_fallback_text = editors
21388 .into_iter()
21389 .find(|new_editor| *new_editor != test_editor_cx.entity())
21390 .expect("Should have one non-test editor now")
21391 .read(test_editor_cx)
21392 .text(test_editor_cx);
21393 assert_eq!(
21394 references_fallback_text, "fn one() {\n let mut a = two();\n}",
21395 "Should use the range from the references response and not the GoToDefinition one"
21396 );
21397 });
21398}
21399
21400#[gpui::test]
21401async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
21402 init_test(cx, |_| {});
21403 cx.update(|cx| {
21404 let mut editor_settings = EditorSettings::get_global(cx).clone();
21405 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
21406 EditorSettings::override_global(editor_settings, cx);
21407 });
21408 let mut cx = EditorLspTestContext::new_rust(
21409 lsp::ServerCapabilities {
21410 definition_provider: Some(lsp::OneOf::Left(true)),
21411 references_provider: Some(lsp::OneOf::Left(true)),
21412 ..lsp::ServerCapabilities::default()
21413 },
21414 cx,
21415 )
21416 .await;
21417 let original_state = r#"fn one() {
21418 let mut a = ˇtwo();
21419 }
21420
21421 fn two() {}"#
21422 .unindent();
21423 cx.set_state(&original_state);
21424
21425 let mut go_to_definition = cx
21426 .lsp
21427 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21428 move |_, _| async move { Ok(None) },
21429 );
21430 let _references = cx
21431 .lsp
21432 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
21433 panic!("Should not call for references with no go to definition fallback")
21434 });
21435
21436 let navigated = cx
21437 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21438 .await
21439 .expect("Failed to navigate to lookup references");
21440 go_to_definition
21441 .next()
21442 .await
21443 .expect("Should have called the go_to_definition handler");
21444
21445 assert_eq!(
21446 navigated,
21447 Navigated::No,
21448 "Should have navigated to references as a fallback after empty GoToDefinition response"
21449 );
21450 cx.assert_editor_state(&original_state);
21451 let editors = cx.update_workspace(|workspace, _, cx| {
21452 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21453 });
21454 cx.update_editor(|_, _, _| {
21455 assert_eq!(
21456 editors.len(),
21457 1,
21458 "After unsuccessful fallback, no other editor should have been opened"
21459 );
21460 });
21461}
21462
21463#[gpui::test]
21464async fn test_find_all_references_editor_reuse(cx: &mut TestAppContext) {
21465 init_test(cx, |_| {});
21466 let mut cx = EditorLspTestContext::new_rust(
21467 lsp::ServerCapabilities {
21468 references_provider: Some(lsp::OneOf::Left(true)),
21469 ..lsp::ServerCapabilities::default()
21470 },
21471 cx,
21472 )
21473 .await;
21474
21475 cx.set_state(
21476 &r#"
21477 fn one() {
21478 let mut a = two();
21479 }
21480
21481 fn ˇtwo() {}"#
21482 .unindent(),
21483 );
21484 cx.lsp
21485 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21486 Ok(Some(vec![
21487 lsp::Location {
21488 uri: params.text_document_position.text_document.uri.clone(),
21489 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
21490 },
21491 lsp::Location {
21492 uri: params.text_document_position.text_document.uri,
21493 range: lsp::Range::new(lsp::Position::new(4, 4), lsp::Position::new(4, 7)),
21494 },
21495 ]))
21496 });
21497 let navigated = cx
21498 .update_editor(|editor, window, cx| {
21499 editor.find_all_references(&FindAllReferences, window, cx)
21500 })
21501 .unwrap()
21502 .await
21503 .expect("Failed to navigate to references");
21504 assert_eq!(
21505 navigated,
21506 Navigated::Yes,
21507 "Should have navigated to references from the FindAllReferences response"
21508 );
21509 cx.assert_editor_state(
21510 &r#"fn one() {
21511 let mut a = two();
21512 }
21513
21514 fn ˇtwo() {}"#
21515 .unindent(),
21516 );
21517
21518 let editors = cx.update_workspace(|workspace, _, cx| {
21519 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21520 });
21521 cx.update_editor(|_, _, _| {
21522 assert_eq!(editors.len(), 2, "We should have opened a new multibuffer");
21523 });
21524
21525 cx.set_state(
21526 &r#"fn one() {
21527 let mut a = ˇtwo();
21528 }
21529
21530 fn two() {}"#
21531 .unindent(),
21532 );
21533 let navigated = cx
21534 .update_editor(|editor, window, cx| {
21535 editor.find_all_references(&FindAllReferences, window, cx)
21536 })
21537 .unwrap()
21538 .await
21539 .expect("Failed to navigate to references");
21540 assert_eq!(
21541 navigated,
21542 Navigated::Yes,
21543 "Should have navigated to references from the FindAllReferences response"
21544 );
21545 cx.assert_editor_state(
21546 &r#"fn one() {
21547 let mut a = ˇtwo();
21548 }
21549
21550 fn two() {}"#
21551 .unindent(),
21552 );
21553 let editors = cx.update_workspace(|workspace, _, cx| {
21554 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21555 });
21556 cx.update_editor(|_, _, _| {
21557 assert_eq!(
21558 editors.len(),
21559 2,
21560 "should have re-used the previous multibuffer"
21561 );
21562 });
21563
21564 cx.set_state(
21565 &r#"fn one() {
21566 let mut a = ˇtwo();
21567 }
21568 fn three() {}
21569 fn two() {}"#
21570 .unindent(),
21571 );
21572 cx.lsp
21573 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21574 Ok(Some(vec![
21575 lsp::Location {
21576 uri: params.text_document_position.text_document.uri.clone(),
21577 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
21578 },
21579 lsp::Location {
21580 uri: params.text_document_position.text_document.uri,
21581 range: lsp::Range::new(lsp::Position::new(5, 4), lsp::Position::new(5, 7)),
21582 },
21583 ]))
21584 });
21585 let navigated = cx
21586 .update_editor(|editor, window, cx| {
21587 editor.find_all_references(&FindAllReferences, window, cx)
21588 })
21589 .unwrap()
21590 .await
21591 .expect("Failed to navigate to references");
21592 assert_eq!(
21593 navigated,
21594 Navigated::Yes,
21595 "Should have navigated to references from the FindAllReferences response"
21596 );
21597 cx.assert_editor_state(
21598 &r#"fn one() {
21599 let mut a = ˇtwo();
21600 }
21601 fn three() {}
21602 fn two() {}"#
21603 .unindent(),
21604 );
21605 let editors = cx.update_workspace(|workspace, _, cx| {
21606 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21607 });
21608 cx.update_editor(|_, _, _| {
21609 assert_eq!(
21610 editors.len(),
21611 3,
21612 "should have used a new multibuffer as offsets changed"
21613 );
21614 });
21615}
21616#[gpui::test]
21617async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
21618 init_test(cx, |_| {});
21619
21620 let language = Arc::new(Language::new(
21621 LanguageConfig::default(),
21622 Some(tree_sitter_rust::LANGUAGE.into()),
21623 ));
21624
21625 let text = r#"
21626 #[cfg(test)]
21627 mod tests() {
21628 #[test]
21629 fn runnable_1() {
21630 let a = 1;
21631 }
21632
21633 #[test]
21634 fn runnable_2() {
21635 let a = 1;
21636 let b = 2;
21637 }
21638 }
21639 "#
21640 .unindent();
21641
21642 let fs = FakeFs::new(cx.executor());
21643 fs.insert_file("/file.rs", Default::default()).await;
21644
21645 let project = Project::test(fs, ["/a".as_ref()], cx).await;
21646 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21647 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21648 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
21649 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
21650
21651 let editor = cx.new_window_entity(|window, cx| {
21652 Editor::new(
21653 EditorMode::full(),
21654 multi_buffer,
21655 Some(project.clone()),
21656 window,
21657 cx,
21658 )
21659 });
21660
21661 editor.update_in(cx, |editor, window, cx| {
21662 let snapshot = editor.buffer().read(cx).snapshot(cx);
21663 editor.tasks.insert(
21664 (buffer.read(cx).remote_id(), 3),
21665 RunnableTasks {
21666 templates: vec![],
21667 offset: snapshot.anchor_before(43),
21668 column: 0,
21669 extra_variables: HashMap::default(),
21670 context_range: BufferOffset(43)..BufferOffset(85),
21671 },
21672 );
21673 editor.tasks.insert(
21674 (buffer.read(cx).remote_id(), 8),
21675 RunnableTasks {
21676 templates: vec![],
21677 offset: snapshot.anchor_before(86),
21678 column: 0,
21679 extra_variables: HashMap::default(),
21680 context_range: BufferOffset(86)..BufferOffset(191),
21681 },
21682 );
21683
21684 // Test finding task when cursor is inside function body
21685 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21686 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
21687 });
21688 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
21689 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
21690
21691 // Test finding task when cursor is on function name
21692 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21693 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
21694 });
21695 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
21696 assert_eq!(row, 8, "Should find task when cursor is on function name");
21697 });
21698}
21699
21700#[gpui::test]
21701async fn test_folding_buffers(cx: &mut TestAppContext) {
21702 init_test(cx, |_| {});
21703
21704 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
21705 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
21706 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
21707
21708 let fs = FakeFs::new(cx.executor());
21709 fs.insert_tree(
21710 path!("/a"),
21711 json!({
21712 "first.rs": sample_text_1,
21713 "second.rs": sample_text_2,
21714 "third.rs": sample_text_3,
21715 }),
21716 )
21717 .await;
21718 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21719 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21720 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21721 let worktree = project.update(cx, |project, cx| {
21722 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
21723 assert_eq!(worktrees.len(), 1);
21724 worktrees.pop().unwrap()
21725 });
21726 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
21727
21728 let buffer_1 = project
21729 .update(cx, |project, cx| {
21730 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
21731 })
21732 .await
21733 .unwrap();
21734 let buffer_2 = project
21735 .update(cx, |project, cx| {
21736 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
21737 })
21738 .await
21739 .unwrap();
21740 let buffer_3 = project
21741 .update(cx, |project, cx| {
21742 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
21743 })
21744 .await
21745 .unwrap();
21746
21747 let multi_buffer = cx.new(|cx| {
21748 let mut multi_buffer = MultiBuffer::new(ReadWrite);
21749 multi_buffer.push_excerpts(
21750 buffer_1.clone(),
21751 [
21752 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21753 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21754 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21755 ],
21756 cx,
21757 );
21758 multi_buffer.push_excerpts(
21759 buffer_2.clone(),
21760 [
21761 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21762 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21763 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21764 ],
21765 cx,
21766 );
21767 multi_buffer.push_excerpts(
21768 buffer_3.clone(),
21769 [
21770 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21771 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21772 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21773 ],
21774 cx,
21775 );
21776 multi_buffer
21777 });
21778 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
21779 Editor::new(
21780 EditorMode::full(),
21781 multi_buffer.clone(),
21782 Some(project.clone()),
21783 window,
21784 cx,
21785 )
21786 });
21787
21788 assert_eq!(
21789 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21790 "\n\naaaa\nbbbb\ncccc\n\n\nffff\ngggg\n\n\njjjj\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
21791 );
21792
21793 multi_buffer_editor.update(cx, |editor, cx| {
21794 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
21795 });
21796 assert_eq!(
21797 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21798 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
21799 "After folding the first buffer, its text should not be displayed"
21800 );
21801
21802 multi_buffer_editor.update(cx, |editor, cx| {
21803 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
21804 });
21805 assert_eq!(
21806 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21807 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
21808 "After folding the second buffer, its text should not be displayed"
21809 );
21810
21811 multi_buffer_editor.update(cx, |editor, cx| {
21812 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
21813 });
21814 assert_eq!(
21815 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21816 "\n\n\n\n\n",
21817 "After folding the third buffer, its text should not be displayed"
21818 );
21819
21820 // Emulate selection inside the fold logic, that should work
21821 multi_buffer_editor.update_in(cx, |editor, window, cx| {
21822 editor
21823 .snapshot(window, cx)
21824 .next_line_boundary(Point::new(0, 4));
21825 });
21826
21827 multi_buffer_editor.update(cx, |editor, cx| {
21828 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
21829 });
21830 assert_eq!(
21831 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21832 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
21833 "After unfolding the second buffer, its text should be displayed"
21834 );
21835
21836 // Typing inside of buffer 1 causes that buffer to be unfolded.
21837 multi_buffer_editor.update_in(cx, |editor, window, cx| {
21838 assert_eq!(
21839 multi_buffer
21840 .read(cx)
21841 .snapshot(cx)
21842 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
21843 .collect::<String>(),
21844 "bbbb"
21845 );
21846 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
21847 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
21848 });
21849 editor.handle_input("B", window, cx);
21850 });
21851
21852 assert_eq!(
21853 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21854 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
21855 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
21856 );
21857
21858 multi_buffer_editor.update(cx, |editor, cx| {
21859 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
21860 });
21861 assert_eq!(
21862 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21863 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
21864 "After unfolding the all buffers, all original text should be displayed"
21865 );
21866}
21867
21868#[gpui::test]
21869async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
21870 init_test(cx, |_| {});
21871
21872 let sample_text_1 = "1111\n2222\n3333".to_string();
21873 let sample_text_2 = "4444\n5555\n6666".to_string();
21874 let sample_text_3 = "7777\n8888\n9999".to_string();
21875
21876 let fs = FakeFs::new(cx.executor());
21877 fs.insert_tree(
21878 path!("/a"),
21879 json!({
21880 "first.rs": sample_text_1,
21881 "second.rs": sample_text_2,
21882 "third.rs": sample_text_3,
21883 }),
21884 )
21885 .await;
21886 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21887 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21888 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21889 let worktree = project.update(cx, |project, cx| {
21890 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
21891 assert_eq!(worktrees.len(), 1);
21892 worktrees.pop().unwrap()
21893 });
21894 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
21895
21896 let buffer_1 = project
21897 .update(cx, |project, cx| {
21898 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
21899 })
21900 .await
21901 .unwrap();
21902 let buffer_2 = project
21903 .update(cx, |project, cx| {
21904 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
21905 })
21906 .await
21907 .unwrap();
21908 let buffer_3 = project
21909 .update(cx, |project, cx| {
21910 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
21911 })
21912 .await
21913 .unwrap();
21914
21915 let multi_buffer = cx.new(|cx| {
21916 let mut multi_buffer = MultiBuffer::new(ReadWrite);
21917 multi_buffer.push_excerpts(
21918 buffer_1.clone(),
21919 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
21920 cx,
21921 );
21922 multi_buffer.push_excerpts(
21923 buffer_2.clone(),
21924 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
21925 cx,
21926 );
21927 multi_buffer.push_excerpts(
21928 buffer_3.clone(),
21929 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
21930 cx,
21931 );
21932 multi_buffer
21933 });
21934
21935 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
21936 Editor::new(
21937 EditorMode::full(),
21938 multi_buffer,
21939 Some(project.clone()),
21940 window,
21941 cx,
21942 )
21943 });
21944
21945 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
21946 assert_eq!(
21947 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21948 full_text,
21949 );
21950
21951 multi_buffer_editor.update(cx, |editor, cx| {
21952 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
21953 });
21954 assert_eq!(
21955 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21956 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
21957 "After folding the first buffer, its text should not be displayed"
21958 );
21959
21960 multi_buffer_editor.update(cx, |editor, cx| {
21961 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
21962 });
21963
21964 assert_eq!(
21965 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21966 "\n\n\n\n\n\n7777\n8888\n9999",
21967 "After folding the second buffer, its text should not be displayed"
21968 );
21969
21970 multi_buffer_editor.update(cx, |editor, cx| {
21971 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
21972 });
21973 assert_eq!(
21974 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21975 "\n\n\n\n\n",
21976 "After folding the third buffer, its text should not be displayed"
21977 );
21978
21979 multi_buffer_editor.update(cx, |editor, cx| {
21980 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
21981 });
21982 assert_eq!(
21983 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21984 "\n\n\n\n4444\n5555\n6666\n\n",
21985 "After unfolding the second buffer, its text should be displayed"
21986 );
21987
21988 multi_buffer_editor.update(cx, |editor, cx| {
21989 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
21990 });
21991 assert_eq!(
21992 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21993 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
21994 "After unfolding the first buffer, its text should be displayed"
21995 );
21996
21997 multi_buffer_editor.update(cx, |editor, cx| {
21998 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
21999 });
22000 assert_eq!(
22001 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22002 full_text,
22003 "After unfolding all buffers, all original text should be displayed"
22004 );
22005}
22006
22007#[gpui::test]
22008async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
22009 init_test(cx, |_| {});
22010
22011 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
22012
22013 let fs = FakeFs::new(cx.executor());
22014 fs.insert_tree(
22015 path!("/a"),
22016 json!({
22017 "main.rs": sample_text,
22018 }),
22019 )
22020 .await;
22021 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22022 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22023 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22024 let worktree = project.update(cx, |project, cx| {
22025 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22026 assert_eq!(worktrees.len(), 1);
22027 worktrees.pop().unwrap()
22028 });
22029 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22030
22031 let buffer_1 = project
22032 .update(cx, |project, cx| {
22033 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22034 })
22035 .await
22036 .unwrap();
22037
22038 let multi_buffer = cx.new(|cx| {
22039 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22040 multi_buffer.push_excerpts(
22041 buffer_1.clone(),
22042 [ExcerptRange::new(
22043 Point::new(0, 0)
22044 ..Point::new(
22045 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
22046 0,
22047 ),
22048 )],
22049 cx,
22050 );
22051 multi_buffer
22052 });
22053 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22054 Editor::new(
22055 EditorMode::full(),
22056 multi_buffer,
22057 Some(project.clone()),
22058 window,
22059 cx,
22060 )
22061 });
22062
22063 let selection_range = Point::new(1, 0)..Point::new(2, 0);
22064 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22065 enum TestHighlight {}
22066 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
22067 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
22068 editor.highlight_text::<TestHighlight>(
22069 vec![highlight_range.clone()],
22070 HighlightStyle::color(Hsla::green()),
22071 cx,
22072 );
22073 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22074 s.select_ranges(Some(highlight_range))
22075 });
22076 });
22077
22078 let full_text = format!("\n\n{sample_text}");
22079 assert_eq!(
22080 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22081 full_text,
22082 );
22083}
22084
22085#[gpui::test]
22086async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
22087 init_test(cx, |_| {});
22088 cx.update(|cx| {
22089 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
22090 "keymaps/default-linux.json",
22091 cx,
22092 )
22093 .unwrap();
22094 cx.bind_keys(default_key_bindings);
22095 });
22096
22097 let (editor, cx) = cx.add_window_view(|window, cx| {
22098 let multi_buffer = MultiBuffer::build_multi(
22099 [
22100 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
22101 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
22102 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
22103 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
22104 ],
22105 cx,
22106 );
22107 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
22108
22109 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
22110 // fold all but the second buffer, so that we test navigating between two
22111 // adjacent folded buffers, as well as folded buffers at the start and
22112 // end the multibuffer
22113 editor.fold_buffer(buffer_ids[0], cx);
22114 editor.fold_buffer(buffer_ids[2], cx);
22115 editor.fold_buffer(buffer_ids[3], cx);
22116
22117 editor
22118 });
22119 cx.simulate_resize(size(px(1000.), px(1000.)));
22120
22121 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
22122 cx.assert_excerpts_with_selections(indoc! {"
22123 [EXCERPT]
22124 ˇ[FOLDED]
22125 [EXCERPT]
22126 a1
22127 b1
22128 [EXCERPT]
22129 [FOLDED]
22130 [EXCERPT]
22131 [FOLDED]
22132 "
22133 });
22134 cx.simulate_keystroke("down");
22135 cx.assert_excerpts_with_selections(indoc! {"
22136 [EXCERPT]
22137 [FOLDED]
22138 [EXCERPT]
22139 ˇa1
22140 b1
22141 [EXCERPT]
22142 [FOLDED]
22143 [EXCERPT]
22144 [FOLDED]
22145 "
22146 });
22147 cx.simulate_keystroke("down");
22148 cx.assert_excerpts_with_selections(indoc! {"
22149 [EXCERPT]
22150 [FOLDED]
22151 [EXCERPT]
22152 a1
22153 ˇb1
22154 [EXCERPT]
22155 [FOLDED]
22156 [EXCERPT]
22157 [FOLDED]
22158 "
22159 });
22160 cx.simulate_keystroke("down");
22161 cx.assert_excerpts_with_selections(indoc! {"
22162 [EXCERPT]
22163 [FOLDED]
22164 [EXCERPT]
22165 a1
22166 b1
22167 ˇ[EXCERPT]
22168 [FOLDED]
22169 [EXCERPT]
22170 [FOLDED]
22171 "
22172 });
22173 cx.simulate_keystroke("down");
22174 cx.assert_excerpts_with_selections(indoc! {"
22175 [EXCERPT]
22176 [FOLDED]
22177 [EXCERPT]
22178 a1
22179 b1
22180 [EXCERPT]
22181 ˇ[FOLDED]
22182 [EXCERPT]
22183 [FOLDED]
22184 "
22185 });
22186 for _ in 0..5 {
22187 cx.simulate_keystroke("down");
22188 cx.assert_excerpts_with_selections(indoc! {"
22189 [EXCERPT]
22190 [FOLDED]
22191 [EXCERPT]
22192 a1
22193 b1
22194 [EXCERPT]
22195 [FOLDED]
22196 [EXCERPT]
22197 ˇ[FOLDED]
22198 "
22199 });
22200 }
22201
22202 cx.simulate_keystroke("up");
22203 cx.assert_excerpts_with_selections(indoc! {"
22204 [EXCERPT]
22205 [FOLDED]
22206 [EXCERPT]
22207 a1
22208 b1
22209 [EXCERPT]
22210 ˇ[FOLDED]
22211 [EXCERPT]
22212 [FOLDED]
22213 "
22214 });
22215 cx.simulate_keystroke("up");
22216 cx.assert_excerpts_with_selections(indoc! {"
22217 [EXCERPT]
22218 [FOLDED]
22219 [EXCERPT]
22220 a1
22221 b1
22222 ˇ[EXCERPT]
22223 [FOLDED]
22224 [EXCERPT]
22225 [FOLDED]
22226 "
22227 });
22228 cx.simulate_keystroke("up");
22229 cx.assert_excerpts_with_selections(indoc! {"
22230 [EXCERPT]
22231 [FOLDED]
22232 [EXCERPT]
22233 a1
22234 ˇb1
22235 [EXCERPT]
22236 [FOLDED]
22237 [EXCERPT]
22238 [FOLDED]
22239 "
22240 });
22241 cx.simulate_keystroke("up");
22242 cx.assert_excerpts_with_selections(indoc! {"
22243 [EXCERPT]
22244 [FOLDED]
22245 [EXCERPT]
22246 ˇa1
22247 b1
22248 [EXCERPT]
22249 [FOLDED]
22250 [EXCERPT]
22251 [FOLDED]
22252 "
22253 });
22254 for _ in 0..5 {
22255 cx.simulate_keystroke("up");
22256 cx.assert_excerpts_with_selections(indoc! {"
22257 [EXCERPT]
22258 ˇ[FOLDED]
22259 [EXCERPT]
22260 a1
22261 b1
22262 [EXCERPT]
22263 [FOLDED]
22264 [EXCERPT]
22265 [FOLDED]
22266 "
22267 });
22268 }
22269}
22270
22271#[gpui::test]
22272async fn test_edit_prediction_text(cx: &mut TestAppContext) {
22273 init_test(cx, |_| {});
22274
22275 // Simple insertion
22276 assert_highlighted_edits(
22277 "Hello, world!",
22278 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
22279 true,
22280 cx,
22281 |highlighted_edits, cx| {
22282 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
22283 assert_eq!(highlighted_edits.highlights.len(), 1);
22284 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
22285 assert_eq!(
22286 highlighted_edits.highlights[0].1.background_color,
22287 Some(cx.theme().status().created_background)
22288 );
22289 },
22290 )
22291 .await;
22292
22293 // Replacement
22294 assert_highlighted_edits(
22295 "This is a test.",
22296 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
22297 false,
22298 cx,
22299 |highlighted_edits, cx| {
22300 assert_eq!(highlighted_edits.text, "That is a test.");
22301 assert_eq!(highlighted_edits.highlights.len(), 1);
22302 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
22303 assert_eq!(
22304 highlighted_edits.highlights[0].1.background_color,
22305 Some(cx.theme().status().created_background)
22306 );
22307 },
22308 )
22309 .await;
22310
22311 // Multiple edits
22312 assert_highlighted_edits(
22313 "Hello, world!",
22314 vec![
22315 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
22316 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
22317 ],
22318 false,
22319 cx,
22320 |highlighted_edits, cx| {
22321 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
22322 assert_eq!(highlighted_edits.highlights.len(), 2);
22323 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
22324 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
22325 assert_eq!(
22326 highlighted_edits.highlights[0].1.background_color,
22327 Some(cx.theme().status().created_background)
22328 );
22329 assert_eq!(
22330 highlighted_edits.highlights[1].1.background_color,
22331 Some(cx.theme().status().created_background)
22332 );
22333 },
22334 )
22335 .await;
22336
22337 // Multiple lines with edits
22338 assert_highlighted_edits(
22339 "First line\nSecond line\nThird line\nFourth line",
22340 vec![
22341 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
22342 (
22343 Point::new(2, 0)..Point::new(2, 10),
22344 "New third line".to_string(),
22345 ),
22346 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
22347 ],
22348 false,
22349 cx,
22350 |highlighted_edits, cx| {
22351 assert_eq!(
22352 highlighted_edits.text,
22353 "Second modified\nNew third line\nFourth updated line"
22354 );
22355 assert_eq!(highlighted_edits.highlights.len(), 3);
22356 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
22357 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
22358 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
22359 for highlight in &highlighted_edits.highlights {
22360 assert_eq!(
22361 highlight.1.background_color,
22362 Some(cx.theme().status().created_background)
22363 );
22364 }
22365 },
22366 )
22367 .await;
22368}
22369
22370#[gpui::test]
22371async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
22372 init_test(cx, |_| {});
22373
22374 // Deletion
22375 assert_highlighted_edits(
22376 "Hello, world!",
22377 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
22378 true,
22379 cx,
22380 |highlighted_edits, cx| {
22381 assert_eq!(highlighted_edits.text, "Hello, world!");
22382 assert_eq!(highlighted_edits.highlights.len(), 1);
22383 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
22384 assert_eq!(
22385 highlighted_edits.highlights[0].1.background_color,
22386 Some(cx.theme().status().deleted_background)
22387 );
22388 },
22389 )
22390 .await;
22391
22392 // Insertion
22393 assert_highlighted_edits(
22394 "Hello, world!",
22395 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
22396 true,
22397 cx,
22398 |highlighted_edits, cx| {
22399 assert_eq!(highlighted_edits.highlights.len(), 1);
22400 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
22401 assert_eq!(
22402 highlighted_edits.highlights[0].1.background_color,
22403 Some(cx.theme().status().created_background)
22404 );
22405 },
22406 )
22407 .await;
22408}
22409
22410async fn assert_highlighted_edits(
22411 text: &str,
22412 edits: Vec<(Range<Point>, String)>,
22413 include_deletions: bool,
22414 cx: &mut TestAppContext,
22415 assertion_fn: impl Fn(HighlightedText, &App),
22416) {
22417 let window = cx.add_window(|window, cx| {
22418 let buffer = MultiBuffer::build_simple(text, cx);
22419 Editor::new(EditorMode::full(), buffer, None, window, cx)
22420 });
22421 let cx = &mut VisualTestContext::from_window(*window, cx);
22422
22423 let (buffer, snapshot) = window
22424 .update(cx, |editor, _window, cx| {
22425 (
22426 editor.buffer().clone(),
22427 editor.buffer().read(cx).snapshot(cx),
22428 )
22429 })
22430 .unwrap();
22431
22432 let edits = edits
22433 .into_iter()
22434 .map(|(range, edit)| {
22435 (
22436 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
22437 edit,
22438 )
22439 })
22440 .collect::<Vec<_>>();
22441
22442 let text_anchor_edits = edits
22443 .clone()
22444 .into_iter()
22445 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
22446 .collect::<Vec<_>>();
22447
22448 let edit_preview = window
22449 .update(cx, |_, _window, cx| {
22450 buffer
22451 .read(cx)
22452 .as_singleton()
22453 .unwrap()
22454 .read(cx)
22455 .preview_edits(text_anchor_edits.into(), cx)
22456 })
22457 .unwrap()
22458 .await;
22459
22460 cx.update(|_window, cx| {
22461 let highlighted_edits = edit_prediction_edit_text(
22462 snapshot.as_singleton().unwrap().2,
22463 &edits,
22464 &edit_preview,
22465 include_deletions,
22466 cx,
22467 );
22468 assertion_fn(highlighted_edits, cx)
22469 });
22470}
22471
22472#[track_caller]
22473fn assert_breakpoint(
22474 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
22475 path: &Arc<Path>,
22476 expected: Vec<(u32, Breakpoint)>,
22477) {
22478 if expected.is_empty() {
22479 assert!(!breakpoints.contains_key(path), "{}", path.display());
22480 } else {
22481 let mut breakpoint = breakpoints
22482 .get(path)
22483 .unwrap()
22484 .iter()
22485 .map(|breakpoint| {
22486 (
22487 breakpoint.row,
22488 Breakpoint {
22489 message: breakpoint.message.clone(),
22490 state: breakpoint.state,
22491 condition: breakpoint.condition.clone(),
22492 hit_condition: breakpoint.hit_condition.clone(),
22493 },
22494 )
22495 })
22496 .collect::<Vec<_>>();
22497
22498 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
22499
22500 assert_eq!(expected, breakpoint);
22501 }
22502}
22503
22504fn add_log_breakpoint_at_cursor(
22505 editor: &mut Editor,
22506 log_message: &str,
22507 window: &mut Window,
22508 cx: &mut Context<Editor>,
22509) {
22510 let (anchor, bp) = editor
22511 .breakpoints_at_cursors(window, cx)
22512 .first()
22513 .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
22514 .unwrap_or_else(|| {
22515 let cursor_position: Point = editor.selections.newest(cx).head();
22516
22517 let breakpoint_position = editor
22518 .snapshot(window, cx)
22519 .display_snapshot
22520 .buffer_snapshot
22521 .anchor_before(Point::new(cursor_position.row, 0));
22522
22523 (breakpoint_position, Breakpoint::new_log(log_message))
22524 });
22525
22526 editor.edit_breakpoint_at_anchor(
22527 anchor,
22528 bp,
22529 BreakpointEditAction::EditLogMessage(log_message.into()),
22530 cx,
22531 );
22532}
22533
22534#[gpui::test]
22535async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
22536 init_test(cx, |_| {});
22537
22538 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22539 let fs = FakeFs::new(cx.executor());
22540 fs.insert_tree(
22541 path!("/a"),
22542 json!({
22543 "main.rs": sample_text,
22544 }),
22545 )
22546 .await;
22547 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22548 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22549 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22550
22551 let fs = FakeFs::new(cx.executor());
22552 fs.insert_tree(
22553 path!("/a"),
22554 json!({
22555 "main.rs": sample_text,
22556 }),
22557 )
22558 .await;
22559 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22560 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22561 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22562 let worktree_id = workspace
22563 .update(cx, |workspace, _window, cx| {
22564 workspace.project().update(cx, |project, cx| {
22565 project.worktrees(cx).next().unwrap().read(cx).id()
22566 })
22567 })
22568 .unwrap();
22569
22570 let buffer = project
22571 .update(cx, |project, cx| {
22572 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22573 })
22574 .await
22575 .unwrap();
22576
22577 let (editor, cx) = cx.add_window_view(|window, cx| {
22578 Editor::new(
22579 EditorMode::full(),
22580 MultiBuffer::build_from_buffer(buffer, cx),
22581 Some(project.clone()),
22582 window,
22583 cx,
22584 )
22585 });
22586
22587 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22588 let abs_path = project.read_with(cx, |project, cx| {
22589 project
22590 .absolute_path(&project_path, cx)
22591 .map(Arc::from)
22592 .unwrap()
22593 });
22594
22595 // assert we can add breakpoint on the first line
22596 editor.update_in(cx, |editor, window, cx| {
22597 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22598 editor.move_to_end(&MoveToEnd, window, cx);
22599 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22600 });
22601
22602 let breakpoints = editor.update(cx, |editor, cx| {
22603 editor
22604 .breakpoint_store()
22605 .as_ref()
22606 .unwrap()
22607 .read(cx)
22608 .all_source_breakpoints(cx)
22609 });
22610
22611 assert_eq!(1, breakpoints.len());
22612 assert_breakpoint(
22613 &breakpoints,
22614 &abs_path,
22615 vec![
22616 (0, Breakpoint::new_standard()),
22617 (3, Breakpoint::new_standard()),
22618 ],
22619 );
22620
22621 editor.update_in(cx, |editor, window, cx| {
22622 editor.move_to_beginning(&MoveToBeginning, window, cx);
22623 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22624 });
22625
22626 let breakpoints = editor.update(cx, |editor, cx| {
22627 editor
22628 .breakpoint_store()
22629 .as_ref()
22630 .unwrap()
22631 .read(cx)
22632 .all_source_breakpoints(cx)
22633 });
22634
22635 assert_eq!(1, breakpoints.len());
22636 assert_breakpoint(
22637 &breakpoints,
22638 &abs_path,
22639 vec![(3, Breakpoint::new_standard())],
22640 );
22641
22642 editor.update_in(cx, |editor, window, cx| {
22643 editor.move_to_end(&MoveToEnd, window, cx);
22644 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22645 });
22646
22647 let breakpoints = editor.update(cx, |editor, cx| {
22648 editor
22649 .breakpoint_store()
22650 .as_ref()
22651 .unwrap()
22652 .read(cx)
22653 .all_source_breakpoints(cx)
22654 });
22655
22656 assert_eq!(0, breakpoints.len());
22657 assert_breakpoint(&breakpoints, &abs_path, vec![]);
22658}
22659
22660#[gpui::test]
22661async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
22662 init_test(cx, |_| {});
22663
22664 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22665
22666 let fs = FakeFs::new(cx.executor());
22667 fs.insert_tree(
22668 path!("/a"),
22669 json!({
22670 "main.rs": sample_text,
22671 }),
22672 )
22673 .await;
22674 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22675 let (workspace, cx) =
22676 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
22677
22678 let worktree_id = workspace.update(cx, |workspace, cx| {
22679 workspace.project().update(cx, |project, cx| {
22680 project.worktrees(cx).next().unwrap().read(cx).id()
22681 })
22682 });
22683
22684 let buffer = project
22685 .update(cx, |project, cx| {
22686 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22687 })
22688 .await
22689 .unwrap();
22690
22691 let (editor, cx) = cx.add_window_view(|window, cx| {
22692 Editor::new(
22693 EditorMode::full(),
22694 MultiBuffer::build_from_buffer(buffer, cx),
22695 Some(project.clone()),
22696 window,
22697 cx,
22698 )
22699 });
22700
22701 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22702 let abs_path = project.read_with(cx, |project, cx| {
22703 project
22704 .absolute_path(&project_path, cx)
22705 .map(Arc::from)
22706 .unwrap()
22707 });
22708
22709 editor.update_in(cx, |editor, window, cx| {
22710 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
22711 });
22712
22713 let breakpoints = editor.update(cx, |editor, cx| {
22714 editor
22715 .breakpoint_store()
22716 .as_ref()
22717 .unwrap()
22718 .read(cx)
22719 .all_source_breakpoints(cx)
22720 });
22721
22722 assert_breakpoint(
22723 &breakpoints,
22724 &abs_path,
22725 vec![(0, Breakpoint::new_log("hello world"))],
22726 );
22727
22728 // Removing a log message from a log breakpoint should remove it
22729 editor.update_in(cx, |editor, window, cx| {
22730 add_log_breakpoint_at_cursor(editor, "", window, cx);
22731 });
22732
22733 let breakpoints = editor.update(cx, |editor, cx| {
22734 editor
22735 .breakpoint_store()
22736 .as_ref()
22737 .unwrap()
22738 .read(cx)
22739 .all_source_breakpoints(cx)
22740 });
22741
22742 assert_breakpoint(&breakpoints, &abs_path, vec![]);
22743
22744 editor.update_in(cx, |editor, window, cx| {
22745 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22746 editor.move_to_end(&MoveToEnd, window, cx);
22747 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22748 // Not adding a log message to a standard breakpoint shouldn't remove it
22749 add_log_breakpoint_at_cursor(editor, "", window, cx);
22750 });
22751
22752 let breakpoints = editor.update(cx, |editor, cx| {
22753 editor
22754 .breakpoint_store()
22755 .as_ref()
22756 .unwrap()
22757 .read(cx)
22758 .all_source_breakpoints(cx)
22759 });
22760
22761 assert_breakpoint(
22762 &breakpoints,
22763 &abs_path,
22764 vec![
22765 (0, Breakpoint::new_standard()),
22766 (3, Breakpoint::new_standard()),
22767 ],
22768 );
22769
22770 editor.update_in(cx, |editor, window, cx| {
22771 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
22772 });
22773
22774 let breakpoints = editor.update(cx, |editor, cx| {
22775 editor
22776 .breakpoint_store()
22777 .as_ref()
22778 .unwrap()
22779 .read(cx)
22780 .all_source_breakpoints(cx)
22781 });
22782
22783 assert_breakpoint(
22784 &breakpoints,
22785 &abs_path,
22786 vec![
22787 (0, Breakpoint::new_standard()),
22788 (3, Breakpoint::new_log("hello world")),
22789 ],
22790 );
22791
22792 editor.update_in(cx, |editor, window, cx| {
22793 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
22794 });
22795
22796 let breakpoints = editor.update(cx, |editor, cx| {
22797 editor
22798 .breakpoint_store()
22799 .as_ref()
22800 .unwrap()
22801 .read(cx)
22802 .all_source_breakpoints(cx)
22803 });
22804
22805 assert_breakpoint(
22806 &breakpoints,
22807 &abs_path,
22808 vec![
22809 (0, Breakpoint::new_standard()),
22810 (3, Breakpoint::new_log("hello Earth!!")),
22811 ],
22812 );
22813}
22814
22815/// This also tests that Editor::breakpoint_at_cursor_head is working properly
22816/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
22817/// or when breakpoints were placed out of order. This tests for a regression too
22818#[gpui::test]
22819async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
22820 init_test(cx, |_| {});
22821
22822 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22823 let fs = FakeFs::new(cx.executor());
22824 fs.insert_tree(
22825 path!("/a"),
22826 json!({
22827 "main.rs": sample_text,
22828 }),
22829 )
22830 .await;
22831 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22832 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22833 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22834
22835 let fs = FakeFs::new(cx.executor());
22836 fs.insert_tree(
22837 path!("/a"),
22838 json!({
22839 "main.rs": sample_text,
22840 }),
22841 )
22842 .await;
22843 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22844 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22845 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22846 let worktree_id = workspace
22847 .update(cx, |workspace, _window, cx| {
22848 workspace.project().update(cx, |project, cx| {
22849 project.worktrees(cx).next().unwrap().read(cx).id()
22850 })
22851 })
22852 .unwrap();
22853
22854 let buffer = project
22855 .update(cx, |project, cx| {
22856 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22857 })
22858 .await
22859 .unwrap();
22860
22861 let (editor, cx) = cx.add_window_view(|window, cx| {
22862 Editor::new(
22863 EditorMode::full(),
22864 MultiBuffer::build_from_buffer(buffer, cx),
22865 Some(project.clone()),
22866 window,
22867 cx,
22868 )
22869 });
22870
22871 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22872 let abs_path = project.read_with(cx, |project, cx| {
22873 project
22874 .absolute_path(&project_path, cx)
22875 .map(Arc::from)
22876 .unwrap()
22877 });
22878
22879 // assert we can add breakpoint on the first line
22880 editor.update_in(cx, |editor, window, cx| {
22881 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22882 editor.move_to_end(&MoveToEnd, window, cx);
22883 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22884 editor.move_up(&MoveUp, window, cx);
22885 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22886 });
22887
22888 let breakpoints = editor.update(cx, |editor, cx| {
22889 editor
22890 .breakpoint_store()
22891 .as_ref()
22892 .unwrap()
22893 .read(cx)
22894 .all_source_breakpoints(cx)
22895 });
22896
22897 assert_eq!(1, breakpoints.len());
22898 assert_breakpoint(
22899 &breakpoints,
22900 &abs_path,
22901 vec![
22902 (0, Breakpoint::new_standard()),
22903 (2, Breakpoint::new_standard()),
22904 (3, Breakpoint::new_standard()),
22905 ],
22906 );
22907
22908 editor.update_in(cx, |editor, window, cx| {
22909 editor.move_to_beginning(&MoveToBeginning, window, cx);
22910 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22911 editor.move_to_end(&MoveToEnd, window, cx);
22912 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22913 // Disabling a breakpoint that doesn't exist should do nothing
22914 editor.move_up(&MoveUp, window, cx);
22915 editor.move_up(&MoveUp, window, cx);
22916 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22917 });
22918
22919 let breakpoints = editor.update(cx, |editor, cx| {
22920 editor
22921 .breakpoint_store()
22922 .as_ref()
22923 .unwrap()
22924 .read(cx)
22925 .all_source_breakpoints(cx)
22926 });
22927
22928 let disable_breakpoint = {
22929 let mut bp = Breakpoint::new_standard();
22930 bp.state = BreakpointState::Disabled;
22931 bp
22932 };
22933
22934 assert_eq!(1, breakpoints.len());
22935 assert_breakpoint(
22936 &breakpoints,
22937 &abs_path,
22938 vec![
22939 (0, disable_breakpoint.clone()),
22940 (2, Breakpoint::new_standard()),
22941 (3, disable_breakpoint.clone()),
22942 ],
22943 );
22944
22945 editor.update_in(cx, |editor, window, cx| {
22946 editor.move_to_beginning(&MoveToBeginning, window, cx);
22947 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
22948 editor.move_to_end(&MoveToEnd, window, cx);
22949 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
22950 editor.move_up(&MoveUp, window, cx);
22951 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22952 });
22953
22954 let breakpoints = editor.update(cx, |editor, cx| {
22955 editor
22956 .breakpoint_store()
22957 .as_ref()
22958 .unwrap()
22959 .read(cx)
22960 .all_source_breakpoints(cx)
22961 });
22962
22963 assert_eq!(1, breakpoints.len());
22964 assert_breakpoint(
22965 &breakpoints,
22966 &abs_path,
22967 vec![
22968 (0, Breakpoint::new_standard()),
22969 (2, disable_breakpoint),
22970 (3, Breakpoint::new_standard()),
22971 ],
22972 );
22973}
22974
22975#[gpui::test]
22976async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
22977 init_test(cx, |_| {});
22978 let capabilities = lsp::ServerCapabilities {
22979 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
22980 prepare_provider: Some(true),
22981 work_done_progress_options: Default::default(),
22982 })),
22983 ..Default::default()
22984 };
22985 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
22986
22987 cx.set_state(indoc! {"
22988 struct Fˇoo {}
22989 "});
22990
22991 cx.update_editor(|editor, _, cx| {
22992 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
22993 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
22994 editor.highlight_background::<DocumentHighlightRead>(
22995 &[highlight_range],
22996 |theme| theme.colors().editor_document_highlight_read_background,
22997 cx,
22998 );
22999 });
23000
23001 let mut prepare_rename_handler = cx
23002 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
23003 move |_, _, _| async move {
23004 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
23005 start: lsp::Position {
23006 line: 0,
23007 character: 7,
23008 },
23009 end: lsp::Position {
23010 line: 0,
23011 character: 10,
23012 },
23013 })))
23014 },
23015 );
23016 let prepare_rename_task = cx
23017 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23018 .expect("Prepare rename was not started");
23019 prepare_rename_handler.next().await.unwrap();
23020 prepare_rename_task.await.expect("Prepare rename failed");
23021
23022 let mut rename_handler =
23023 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23024 let edit = lsp::TextEdit {
23025 range: lsp::Range {
23026 start: lsp::Position {
23027 line: 0,
23028 character: 7,
23029 },
23030 end: lsp::Position {
23031 line: 0,
23032 character: 10,
23033 },
23034 },
23035 new_text: "FooRenamed".to_string(),
23036 };
23037 Ok(Some(lsp::WorkspaceEdit::new(
23038 // Specify the same edit twice
23039 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
23040 )))
23041 });
23042 let rename_task = cx
23043 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23044 .expect("Confirm rename was not started");
23045 rename_handler.next().await.unwrap();
23046 rename_task.await.expect("Confirm rename failed");
23047 cx.run_until_parked();
23048
23049 // Despite two edits, only one is actually applied as those are identical
23050 cx.assert_editor_state(indoc! {"
23051 struct FooRenamedˇ {}
23052 "});
23053}
23054
23055#[gpui::test]
23056async fn test_rename_without_prepare(cx: &mut TestAppContext) {
23057 init_test(cx, |_| {});
23058 // These capabilities indicate that the server does not support prepare rename.
23059 let capabilities = lsp::ServerCapabilities {
23060 rename_provider: Some(lsp::OneOf::Left(true)),
23061 ..Default::default()
23062 };
23063 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
23064
23065 cx.set_state(indoc! {"
23066 struct Fˇoo {}
23067 "});
23068
23069 cx.update_editor(|editor, _window, cx| {
23070 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
23071 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
23072 editor.highlight_background::<DocumentHighlightRead>(
23073 &[highlight_range],
23074 |theme| theme.colors().editor_document_highlight_read_background,
23075 cx,
23076 );
23077 });
23078
23079 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23080 .expect("Prepare rename was not started")
23081 .await
23082 .expect("Prepare rename failed");
23083
23084 let mut rename_handler =
23085 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23086 let edit = lsp::TextEdit {
23087 range: lsp::Range {
23088 start: lsp::Position {
23089 line: 0,
23090 character: 7,
23091 },
23092 end: lsp::Position {
23093 line: 0,
23094 character: 10,
23095 },
23096 },
23097 new_text: "FooRenamed".to_string(),
23098 };
23099 Ok(Some(lsp::WorkspaceEdit::new(
23100 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
23101 )))
23102 });
23103 let rename_task = cx
23104 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23105 .expect("Confirm rename was not started");
23106 rename_handler.next().await.unwrap();
23107 rename_task.await.expect("Confirm rename failed");
23108 cx.run_until_parked();
23109
23110 // Correct range is renamed, as `surrounding_word` is used to find it.
23111 cx.assert_editor_state(indoc! {"
23112 struct FooRenamedˇ {}
23113 "});
23114}
23115
23116#[gpui::test]
23117async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
23118 init_test(cx, |_| {});
23119 let mut cx = EditorTestContext::new(cx).await;
23120
23121 let language = Arc::new(
23122 Language::new(
23123 LanguageConfig::default(),
23124 Some(tree_sitter_html::LANGUAGE.into()),
23125 )
23126 .with_brackets_query(
23127 r#"
23128 ("<" @open "/>" @close)
23129 ("</" @open ">" @close)
23130 ("<" @open ">" @close)
23131 ("\"" @open "\"" @close)
23132 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
23133 "#,
23134 )
23135 .unwrap(),
23136 );
23137 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23138
23139 cx.set_state(indoc! {"
23140 <span>ˇ</span>
23141 "});
23142 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23143 cx.assert_editor_state(indoc! {"
23144 <span>
23145 ˇ
23146 </span>
23147 "});
23148
23149 cx.set_state(indoc! {"
23150 <span><span></span>ˇ</span>
23151 "});
23152 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23153 cx.assert_editor_state(indoc! {"
23154 <span><span></span>
23155 ˇ</span>
23156 "});
23157
23158 cx.set_state(indoc! {"
23159 <span>ˇ
23160 </span>
23161 "});
23162 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23163 cx.assert_editor_state(indoc! {"
23164 <span>
23165 ˇ
23166 </span>
23167 "});
23168}
23169
23170#[gpui::test(iterations = 10)]
23171async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
23172 init_test(cx, |_| {});
23173
23174 let fs = FakeFs::new(cx.executor());
23175 fs.insert_tree(
23176 path!("/dir"),
23177 json!({
23178 "a.ts": "a",
23179 }),
23180 )
23181 .await;
23182
23183 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
23184 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23185 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23186
23187 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23188 language_registry.add(Arc::new(Language::new(
23189 LanguageConfig {
23190 name: "TypeScript".into(),
23191 matcher: LanguageMatcher {
23192 path_suffixes: vec!["ts".to_string()],
23193 ..Default::default()
23194 },
23195 ..Default::default()
23196 },
23197 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
23198 )));
23199 let mut fake_language_servers = language_registry.register_fake_lsp(
23200 "TypeScript",
23201 FakeLspAdapter {
23202 capabilities: lsp::ServerCapabilities {
23203 code_lens_provider: Some(lsp::CodeLensOptions {
23204 resolve_provider: Some(true),
23205 }),
23206 execute_command_provider: Some(lsp::ExecuteCommandOptions {
23207 commands: vec!["_the/command".to_string()],
23208 ..lsp::ExecuteCommandOptions::default()
23209 }),
23210 ..lsp::ServerCapabilities::default()
23211 },
23212 ..FakeLspAdapter::default()
23213 },
23214 );
23215
23216 let editor = workspace
23217 .update(cx, |workspace, window, cx| {
23218 workspace.open_abs_path(
23219 PathBuf::from(path!("/dir/a.ts")),
23220 OpenOptions::default(),
23221 window,
23222 cx,
23223 )
23224 })
23225 .unwrap()
23226 .await
23227 .unwrap()
23228 .downcast::<Editor>()
23229 .unwrap();
23230 cx.executor().run_until_parked();
23231
23232 let fake_server = fake_language_servers.next().await.unwrap();
23233
23234 let buffer = editor.update(cx, |editor, cx| {
23235 editor
23236 .buffer()
23237 .read(cx)
23238 .as_singleton()
23239 .expect("have opened a single file by path")
23240 });
23241
23242 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
23243 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
23244 drop(buffer_snapshot);
23245 let actions = cx
23246 .update_window(*workspace, |_, window, cx| {
23247 project.code_actions(&buffer, anchor..anchor, window, cx)
23248 })
23249 .unwrap();
23250
23251 fake_server
23252 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23253 Ok(Some(vec![
23254 lsp::CodeLens {
23255 range: lsp::Range::default(),
23256 command: Some(lsp::Command {
23257 title: "Code lens command".to_owned(),
23258 command: "_the/command".to_owned(),
23259 arguments: None,
23260 }),
23261 data: None,
23262 },
23263 lsp::CodeLens {
23264 range: lsp::Range::default(),
23265 command: Some(lsp::Command {
23266 title: "Command not in capabilities".to_owned(),
23267 command: "not in capabilities".to_owned(),
23268 arguments: None,
23269 }),
23270 data: None,
23271 },
23272 lsp::CodeLens {
23273 range: lsp::Range {
23274 start: lsp::Position {
23275 line: 1,
23276 character: 1,
23277 },
23278 end: lsp::Position {
23279 line: 1,
23280 character: 1,
23281 },
23282 },
23283 command: Some(lsp::Command {
23284 title: "Command not in range".to_owned(),
23285 command: "_the/command".to_owned(),
23286 arguments: None,
23287 }),
23288 data: None,
23289 },
23290 ]))
23291 })
23292 .next()
23293 .await;
23294
23295 let actions = actions.await.unwrap();
23296 assert_eq!(
23297 actions.len(),
23298 1,
23299 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
23300 );
23301 let action = actions[0].clone();
23302 let apply = project.update(cx, |project, cx| {
23303 project.apply_code_action(buffer.clone(), action, true, cx)
23304 });
23305
23306 // Resolving the code action does not populate its edits. In absence of
23307 // edits, we must execute the given command.
23308 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
23309 |mut lens, _| async move {
23310 let lens_command = lens.command.as_mut().expect("should have a command");
23311 assert_eq!(lens_command.title, "Code lens command");
23312 lens_command.arguments = Some(vec![json!("the-argument")]);
23313 Ok(lens)
23314 },
23315 );
23316
23317 // While executing the command, the language server sends the editor
23318 // a `workspaceEdit` request.
23319 fake_server
23320 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
23321 let fake = fake_server.clone();
23322 move |params, _| {
23323 assert_eq!(params.command, "_the/command");
23324 let fake = fake.clone();
23325 async move {
23326 fake.server
23327 .request::<lsp::request::ApplyWorkspaceEdit>(
23328 lsp::ApplyWorkspaceEditParams {
23329 label: None,
23330 edit: lsp::WorkspaceEdit {
23331 changes: Some(
23332 [(
23333 lsp::Uri::from_file_path(path!("/dir/a.ts")).unwrap(),
23334 vec![lsp::TextEdit {
23335 range: lsp::Range::new(
23336 lsp::Position::new(0, 0),
23337 lsp::Position::new(0, 0),
23338 ),
23339 new_text: "X".into(),
23340 }],
23341 )]
23342 .into_iter()
23343 .collect(),
23344 ),
23345 ..lsp::WorkspaceEdit::default()
23346 },
23347 },
23348 )
23349 .await
23350 .into_response()
23351 .unwrap();
23352 Ok(Some(json!(null)))
23353 }
23354 }
23355 })
23356 .next()
23357 .await;
23358
23359 // Applying the code lens command returns a project transaction containing the edits
23360 // sent by the language server in its `workspaceEdit` request.
23361 let transaction = apply.await.unwrap();
23362 assert!(transaction.0.contains_key(&buffer));
23363 buffer.update(cx, |buffer, cx| {
23364 assert_eq!(buffer.text(), "Xa");
23365 buffer.undo(cx);
23366 assert_eq!(buffer.text(), "a");
23367 });
23368
23369 let actions_after_edits = cx
23370 .update_window(*workspace, |_, window, cx| {
23371 project.code_actions(&buffer, anchor..anchor, window, cx)
23372 })
23373 .unwrap()
23374 .await
23375 .unwrap();
23376 assert_eq!(
23377 actions, actions_after_edits,
23378 "For the same selection, same code lens actions should be returned"
23379 );
23380
23381 let _responses =
23382 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23383 panic!("No more code lens requests are expected");
23384 });
23385 editor.update_in(cx, |editor, window, cx| {
23386 editor.select_all(&SelectAll, window, cx);
23387 });
23388 cx.executor().run_until_parked();
23389 let new_actions = cx
23390 .update_window(*workspace, |_, window, cx| {
23391 project.code_actions(&buffer, anchor..anchor, window, cx)
23392 })
23393 .unwrap()
23394 .await
23395 .unwrap();
23396 assert_eq!(
23397 actions, new_actions,
23398 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
23399 );
23400}
23401
23402#[gpui::test]
23403async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
23404 init_test(cx, |_| {});
23405
23406 let fs = FakeFs::new(cx.executor());
23407 let main_text = r#"fn main() {
23408println!("1");
23409println!("2");
23410println!("3");
23411println!("4");
23412println!("5");
23413}"#;
23414 let lib_text = "mod foo {}";
23415 fs.insert_tree(
23416 path!("/a"),
23417 json!({
23418 "lib.rs": lib_text,
23419 "main.rs": main_text,
23420 }),
23421 )
23422 .await;
23423
23424 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23425 let (workspace, cx) =
23426 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23427 let worktree_id = workspace.update(cx, |workspace, cx| {
23428 workspace.project().update(cx, |project, cx| {
23429 project.worktrees(cx).next().unwrap().read(cx).id()
23430 })
23431 });
23432
23433 let expected_ranges = vec![
23434 Point::new(0, 0)..Point::new(0, 0),
23435 Point::new(1, 0)..Point::new(1, 1),
23436 Point::new(2, 0)..Point::new(2, 2),
23437 Point::new(3, 0)..Point::new(3, 3),
23438 ];
23439
23440 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23441 let editor_1 = workspace
23442 .update_in(cx, |workspace, window, cx| {
23443 workspace.open_path(
23444 (worktree_id, rel_path("main.rs")),
23445 Some(pane_1.downgrade()),
23446 true,
23447 window,
23448 cx,
23449 )
23450 })
23451 .unwrap()
23452 .await
23453 .downcast::<Editor>()
23454 .unwrap();
23455 pane_1.update(cx, |pane, cx| {
23456 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23457 open_editor.update(cx, |editor, cx| {
23458 assert_eq!(
23459 editor.display_text(cx),
23460 main_text,
23461 "Original main.rs text on initial open",
23462 );
23463 assert_eq!(
23464 editor
23465 .selections
23466 .all::<Point>(cx)
23467 .into_iter()
23468 .map(|s| s.range())
23469 .collect::<Vec<_>>(),
23470 vec![Point::zero()..Point::zero()],
23471 "Default selections on initial open",
23472 );
23473 })
23474 });
23475 editor_1.update_in(cx, |editor, window, cx| {
23476 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23477 s.select_ranges(expected_ranges.clone());
23478 });
23479 });
23480
23481 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
23482 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
23483 });
23484 let editor_2 = workspace
23485 .update_in(cx, |workspace, window, cx| {
23486 workspace.open_path(
23487 (worktree_id, rel_path("main.rs")),
23488 Some(pane_2.downgrade()),
23489 true,
23490 window,
23491 cx,
23492 )
23493 })
23494 .unwrap()
23495 .await
23496 .downcast::<Editor>()
23497 .unwrap();
23498 pane_2.update(cx, |pane, cx| {
23499 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23500 open_editor.update(cx, |editor, cx| {
23501 assert_eq!(
23502 editor.display_text(cx),
23503 main_text,
23504 "Original main.rs text on initial open in another panel",
23505 );
23506 assert_eq!(
23507 editor
23508 .selections
23509 .all::<Point>(cx)
23510 .into_iter()
23511 .map(|s| s.range())
23512 .collect::<Vec<_>>(),
23513 vec![Point::zero()..Point::zero()],
23514 "Default selections on initial open in another panel",
23515 );
23516 })
23517 });
23518
23519 editor_2.update_in(cx, |editor, window, cx| {
23520 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
23521 });
23522
23523 let _other_editor_1 = workspace
23524 .update_in(cx, |workspace, window, cx| {
23525 workspace.open_path(
23526 (worktree_id, rel_path("lib.rs")),
23527 Some(pane_1.downgrade()),
23528 true,
23529 window,
23530 cx,
23531 )
23532 })
23533 .unwrap()
23534 .await
23535 .downcast::<Editor>()
23536 .unwrap();
23537 pane_1
23538 .update_in(cx, |pane, window, cx| {
23539 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23540 })
23541 .await
23542 .unwrap();
23543 drop(editor_1);
23544 pane_1.update(cx, |pane, cx| {
23545 pane.active_item()
23546 .unwrap()
23547 .downcast::<Editor>()
23548 .unwrap()
23549 .update(cx, |editor, cx| {
23550 assert_eq!(
23551 editor.display_text(cx),
23552 lib_text,
23553 "Other file should be open and active",
23554 );
23555 });
23556 assert_eq!(pane.items().count(), 1, "No other editors should be open");
23557 });
23558
23559 let _other_editor_2 = workspace
23560 .update_in(cx, |workspace, window, cx| {
23561 workspace.open_path(
23562 (worktree_id, rel_path("lib.rs")),
23563 Some(pane_2.downgrade()),
23564 true,
23565 window,
23566 cx,
23567 )
23568 })
23569 .unwrap()
23570 .await
23571 .downcast::<Editor>()
23572 .unwrap();
23573 pane_2
23574 .update_in(cx, |pane, window, cx| {
23575 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23576 })
23577 .await
23578 .unwrap();
23579 drop(editor_2);
23580 pane_2.update(cx, |pane, cx| {
23581 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23582 open_editor.update(cx, |editor, cx| {
23583 assert_eq!(
23584 editor.display_text(cx),
23585 lib_text,
23586 "Other file should be open and active in another panel too",
23587 );
23588 });
23589 assert_eq!(
23590 pane.items().count(),
23591 1,
23592 "No other editors should be open in another pane",
23593 );
23594 });
23595
23596 let _editor_1_reopened = workspace
23597 .update_in(cx, |workspace, window, cx| {
23598 workspace.open_path(
23599 (worktree_id, rel_path("main.rs")),
23600 Some(pane_1.downgrade()),
23601 true,
23602 window,
23603 cx,
23604 )
23605 })
23606 .unwrap()
23607 .await
23608 .downcast::<Editor>()
23609 .unwrap();
23610 let _editor_2_reopened = workspace
23611 .update_in(cx, |workspace, window, cx| {
23612 workspace.open_path(
23613 (worktree_id, rel_path("main.rs")),
23614 Some(pane_2.downgrade()),
23615 true,
23616 window,
23617 cx,
23618 )
23619 })
23620 .unwrap()
23621 .await
23622 .downcast::<Editor>()
23623 .unwrap();
23624 pane_1.update(cx, |pane, cx| {
23625 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23626 open_editor.update(cx, |editor, cx| {
23627 assert_eq!(
23628 editor.display_text(cx),
23629 main_text,
23630 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
23631 );
23632 assert_eq!(
23633 editor
23634 .selections
23635 .all::<Point>(cx)
23636 .into_iter()
23637 .map(|s| s.range())
23638 .collect::<Vec<_>>(),
23639 expected_ranges,
23640 "Previous editor in the 1st panel had selections and should get them restored on reopen",
23641 );
23642 })
23643 });
23644 pane_2.update(cx, |pane, cx| {
23645 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23646 open_editor.update(cx, |editor, cx| {
23647 assert_eq!(
23648 editor.display_text(cx),
23649 r#"fn main() {
23650⋯rintln!("1");
23651⋯intln!("2");
23652⋯ntln!("3");
23653println!("4");
23654println!("5");
23655}"#,
23656 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
23657 );
23658 assert_eq!(
23659 editor
23660 .selections
23661 .all::<Point>(cx)
23662 .into_iter()
23663 .map(|s| s.range())
23664 .collect::<Vec<_>>(),
23665 vec![Point::zero()..Point::zero()],
23666 "Previous editor in the 2nd pane had no selections changed hence should restore none",
23667 );
23668 })
23669 });
23670}
23671
23672#[gpui::test]
23673async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
23674 init_test(cx, |_| {});
23675
23676 let fs = FakeFs::new(cx.executor());
23677 let main_text = r#"fn main() {
23678println!("1");
23679println!("2");
23680println!("3");
23681println!("4");
23682println!("5");
23683}"#;
23684 let lib_text = "mod foo {}";
23685 fs.insert_tree(
23686 path!("/a"),
23687 json!({
23688 "lib.rs": lib_text,
23689 "main.rs": main_text,
23690 }),
23691 )
23692 .await;
23693
23694 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23695 let (workspace, cx) =
23696 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23697 let worktree_id = workspace.update(cx, |workspace, cx| {
23698 workspace.project().update(cx, |project, cx| {
23699 project.worktrees(cx).next().unwrap().read(cx).id()
23700 })
23701 });
23702
23703 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23704 let editor = workspace
23705 .update_in(cx, |workspace, window, cx| {
23706 workspace.open_path(
23707 (worktree_id, rel_path("main.rs")),
23708 Some(pane.downgrade()),
23709 true,
23710 window,
23711 cx,
23712 )
23713 })
23714 .unwrap()
23715 .await
23716 .downcast::<Editor>()
23717 .unwrap();
23718 pane.update(cx, |pane, cx| {
23719 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23720 open_editor.update(cx, |editor, cx| {
23721 assert_eq!(
23722 editor.display_text(cx),
23723 main_text,
23724 "Original main.rs text on initial open",
23725 );
23726 })
23727 });
23728 editor.update_in(cx, |editor, window, cx| {
23729 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
23730 });
23731
23732 cx.update_global(|store: &mut SettingsStore, cx| {
23733 store.update_user_settings(cx, |s| {
23734 s.workspace.restore_on_file_reopen = Some(false);
23735 });
23736 });
23737 editor.update_in(cx, |editor, window, cx| {
23738 editor.fold_ranges(
23739 vec![
23740 Point::new(1, 0)..Point::new(1, 1),
23741 Point::new(2, 0)..Point::new(2, 2),
23742 Point::new(3, 0)..Point::new(3, 3),
23743 ],
23744 false,
23745 window,
23746 cx,
23747 );
23748 });
23749 pane.update_in(cx, |pane, window, cx| {
23750 pane.close_all_items(&CloseAllItems::default(), window, cx)
23751 })
23752 .await
23753 .unwrap();
23754 pane.update(cx, |pane, _| {
23755 assert!(pane.active_item().is_none());
23756 });
23757 cx.update_global(|store: &mut SettingsStore, cx| {
23758 store.update_user_settings(cx, |s| {
23759 s.workspace.restore_on_file_reopen = Some(true);
23760 });
23761 });
23762
23763 let _editor_reopened = workspace
23764 .update_in(cx, |workspace, window, cx| {
23765 workspace.open_path(
23766 (worktree_id, rel_path("main.rs")),
23767 Some(pane.downgrade()),
23768 true,
23769 window,
23770 cx,
23771 )
23772 })
23773 .unwrap()
23774 .await
23775 .downcast::<Editor>()
23776 .unwrap();
23777 pane.update(cx, |pane, cx| {
23778 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23779 open_editor.update(cx, |editor, cx| {
23780 assert_eq!(
23781 editor.display_text(cx),
23782 main_text,
23783 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
23784 );
23785 })
23786 });
23787}
23788
23789#[gpui::test]
23790async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
23791 struct EmptyModalView {
23792 focus_handle: gpui::FocusHandle,
23793 }
23794 impl EventEmitter<DismissEvent> for EmptyModalView {}
23795 impl Render for EmptyModalView {
23796 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
23797 div()
23798 }
23799 }
23800 impl Focusable for EmptyModalView {
23801 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
23802 self.focus_handle.clone()
23803 }
23804 }
23805 impl workspace::ModalView for EmptyModalView {}
23806 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
23807 EmptyModalView {
23808 focus_handle: cx.focus_handle(),
23809 }
23810 }
23811
23812 init_test(cx, |_| {});
23813
23814 let fs = FakeFs::new(cx.executor());
23815 let project = Project::test(fs, [], cx).await;
23816 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23817 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
23818 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23819 let editor = cx.new_window_entity(|window, cx| {
23820 Editor::new(
23821 EditorMode::full(),
23822 buffer,
23823 Some(project.clone()),
23824 window,
23825 cx,
23826 )
23827 });
23828 workspace
23829 .update(cx, |workspace, window, cx| {
23830 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
23831 })
23832 .unwrap();
23833 editor.update_in(cx, |editor, window, cx| {
23834 editor.open_context_menu(&OpenContextMenu, window, cx);
23835 assert!(editor.mouse_context_menu.is_some());
23836 });
23837 workspace
23838 .update(cx, |workspace, window, cx| {
23839 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
23840 })
23841 .unwrap();
23842 cx.read(|cx| {
23843 assert!(editor.read(cx).mouse_context_menu.is_none());
23844 });
23845}
23846
23847fn set_linked_edit_ranges(
23848 opening: (Point, Point),
23849 closing: (Point, Point),
23850 editor: &mut Editor,
23851 cx: &mut Context<Editor>,
23852) {
23853 let Some((buffer, _)) = editor
23854 .buffer
23855 .read(cx)
23856 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
23857 else {
23858 panic!("Failed to get buffer for selection position");
23859 };
23860 let buffer = buffer.read(cx);
23861 let buffer_id = buffer.remote_id();
23862 let opening_range = buffer.anchor_before(opening.0)..buffer.anchor_after(opening.1);
23863 let closing_range = buffer.anchor_before(closing.0)..buffer.anchor_after(closing.1);
23864 let mut linked_ranges = HashMap::default();
23865 linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
23866 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
23867}
23868
23869#[gpui::test]
23870async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
23871 init_test(cx, |_| {});
23872
23873 let fs = FakeFs::new(cx.executor());
23874 fs.insert_file(path!("/file.html"), Default::default())
23875 .await;
23876
23877 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
23878
23879 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23880 let html_language = Arc::new(Language::new(
23881 LanguageConfig {
23882 name: "HTML".into(),
23883 matcher: LanguageMatcher {
23884 path_suffixes: vec!["html".to_string()],
23885 ..LanguageMatcher::default()
23886 },
23887 brackets: BracketPairConfig {
23888 pairs: vec![BracketPair {
23889 start: "<".into(),
23890 end: ">".into(),
23891 close: true,
23892 ..Default::default()
23893 }],
23894 ..Default::default()
23895 },
23896 ..Default::default()
23897 },
23898 Some(tree_sitter_html::LANGUAGE.into()),
23899 ));
23900 language_registry.add(html_language);
23901 let mut fake_servers = language_registry.register_fake_lsp(
23902 "HTML",
23903 FakeLspAdapter {
23904 capabilities: lsp::ServerCapabilities {
23905 completion_provider: Some(lsp::CompletionOptions {
23906 resolve_provider: Some(true),
23907 ..Default::default()
23908 }),
23909 ..Default::default()
23910 },
23911 ..Default::default()
23912 },
23913 );
23914
23915 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23916 let cx = &mut VisualTestContext::from_window(*workspace, cx);
23917
23918 let worktree_id = workspace
23919 .update(cx, |workspace, _window, cx| {
23920 workspace.project().update(cx, |project, cx| {
23921 project.worktrees(cx).next().unwrap().read(cx).id()
23922 })
23923 })
23924 .unwrap();
23925 project
23926 .update(cx, |project, cx| {
23927 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
23928 })
23929 .await
23930 .unwrap();
23931 let editor = workspace
23932 .update(cx, |workspace, window, cx| {
23933 workspace.open_path((worktree_id, rel_path("file.html")), None, true, window, cx)
23934 })
23935 .unwrap()
23936 .await
23937 .unwrap()
23938 .downcast::<Editor>()
23939 .unwrap();
23940
23941 let fake_server = fake_servers.next().await.unwrap();
23942 editor.update_in(cx, |editor, window, cx| {
23943 editor.set_text("<ad></ad>", window, cx);
23944 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
23945 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
23946 });
23947 set_linked_edit_ranges(
23948 (Point::new(0, 1), Point::new(0, 3)),
23949 (Point::new(0, 6), Point::new(0, 8)),
23950 editor,
23951 cx,
23952 );
23953 });
23954 let mut completion_handle =
23955 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
23956 Ok(Some(lsp::CompletionResponse::Array(vec![
23957 lsp::CompletionItem {
23958 label: "head".to_string(),
23959 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
23960 lsp::InsertReplaceEdit {
23961 new_text: "head".to_string(),
23962 insert: lsp::Range::new(
23963 lsp::Position::new(0, 1),
23964 lsp::Position::new(0, 3),
23965 ),
23966 replace: lsp::Range::new(
23967 lsp::Position::new(0, 1),
23968 lsp::Position::new(0, 3),
23969 ),
23970 },
23971 )),
23972 ..Default::default()
23973 },
23974 ])))
23975 });
23976 editor.update_in(cx, |editor, window, cx| {
23977 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
23978 });
23979 cx.run_until_parked();
23980 completion_handle.next().await.unwrap();
23981 editor.update(cx, |editor, _| {
23982 assert!(
23983 editor.context_menu_visible(),
23984 "Completion menu should be visible"
23985 );
23986 });
23987 editor.update_in(cx, |editor, window, cx| {
23988 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
23989 });
23990 cx.executor().run_until_parked();
23991 editor.update(cx, |editor, cx| {
23992 assert_eq!(editor.text(cx), "<head></head>");
23993 });
23994}
23995
23996#[gpui::test]
23997async fn test_linked_edits_on_typing_punctuation(cx: &mut TestAppContext) {
23998 init_test(cx, |_| {});
23999
24000 let mut cx = EditorTestContext::new(cx).await;
24001 let language = Arc::new(Language::new(
24002 LanguageConfig {
24003 name: "TSX".into(),
24004 matcher: LanguageMatcher {
24005 path_suffixes: vec!["tsx".to_string()],
24006 ..LanguageMatcher::default()
24007 },
24008 brackets: BracketPairConfig {
24009 pairs: vec![BracketPair {
24010 start: "<".into(),
24011 end: ">".into(),
24012 close: true,
24013 ..Default::default()
24014 }],
24015 ..Default::default()
24016 },
24017 linked_edit_characters: HashSet::from_iter(['.']),
24018 ..Default::default()
24019 },
24020 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
24021 ));
24022 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24023
24024 // Test typing > does not extend linked pair
24025 cx.set_state("<divˇ<div></div>");
24026 cx.update_editor(|editor, _, cx| {
24027 set_linked_edit_ranges(
24028 (Point::new(0, 1), Point::new(0, 4)),
24029 (Point::new(0, 11), Point::new(0, 14)),
24030 editor,
24031 cx,
24032 );
24033 });
24034 cx.update_editor(|editor, window, cx| {
24035 editor.handle_input(">", window, cx);
24036 });
24037 cx.assert_editor_state("<div>ˇ<div></div>");
24038
24039 // Test typing . do extend linked pair
24040 cx.set_state("<Animatedˇ></Animated>");
24041 cx.update_editor(|editor, _, cx| {
24042 set_linked_edit_ranges(
24043 (Point::new(0, 1), Point::new(0, 9)),
24044 (Point::new(0, 12), Point::new(0, 20)),
24045 editor,
24046 cx,
24047 );
24048 });
24049 cx.update_editor(|editor, window, cx| {
24050 editor.handle_input(".", window, cx);
24051 });
24052 cx.assert_editor_state("<Animated.ˇ></Animated.>");
24053 cx.update_editor(|editor, _, cx| {
24054 set_linked_edit_ranges(
24055 (Point::new(0, 1), Point::new(0, 10)),
24056 (Point::new(0, 13), Point::new(0, 21)),
24057 editor,
24058 cx,
24059 );
24060 });
24061 cx.update_editor(|editor, window, cx| {
24062 editor.handle_input("V", window, cx);
24063 });
24064 cx.assert_editor_state("<Animated.Vˇ></Animated.V>");
24065}
24066
24067#[gpui::test]
24068async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
24069 init_test(cx, |_| {});
24070
24071 let fs = FakeFs::new(cx.executor());
24072 fs.insert_tree(
24073 path!("/root"),
24074 json!({
24075 "a": {
24076 "main.rs": "fn main() {}",
24077 },
24078 "foo": {
24079 "bar": {
24080 "external_file.rs": "pub mod external {}",
24081 }
24082 }
24083 }),
24084 )
24085 .await;
24086
24087 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
24088 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24089 language_registry.add(rust_lang());
24090 let _fake_servers = language_registry.register_fake_lsp(
24091 "Rust",
24092 FakeLspAdapter {
24093 ..FakeLspAdapter::default()
24094 },
24095 );
24096 let (workspace, cx) =
24097 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24098 let worktree_id = workspace.update(cx, |workspace, cx| {
24099 workspace.project().update(cx, |project, cx| {
24100 project.worktrees(cx).next().unwrap().read(cx).id()
24101 })
24102 });
24103
24104 let assert_language_servers_count =
24105 |expected: usize, context: &str, cx: &mut VisualTestContext| {
24106 project.update(cx, |project, cx| {
24107 let current = project
24108 .lsp_store()
24109 .read(cx)
24110 .as_local()
24111 .unwrap()
24112 .language_servers
24113 .len();
24114 assert_eq!(expected, current, "{context}");
24115 });
24116 };
24117
24118 assert_language_servers_count(
24119 0,
24120 "No servers should be running before any file is open",
24121 cx,
24122 );
24123 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
24124 let main_editor = workspace
24125 .update_in(cx, |workspace, window, cx| {
24126 workspace.open_path(
24127 (worktree_id, rel_path("main.rs")),
24128 Some(pane.downgrade()),
24129 true,
24130 window,
24131 cx,
24132 )
24133 })
24134 .unwrap()
24135 .await
24136 .downcast::<Editor>()
24137 .unwrap();
24138 pane.update(cx, |pane, cx| {
24139 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24140 open_editor.update(cx, |editor, cx| {
24141 assert_eq!(
24142 editor.display_text(cx),
24143 "fn main() {}",
24144 "Original main.rs text on initial open",
24145 );
24146 });
24147 assert_eq!(open_editor, main_editor);
24148 });
24149 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
24150
24151 let external_editor = workspace
24152 .update_in(cx, |workspace, window, cx| {
24153 workspace.open_abs_path(
24154 PathBuf::from("/root/foo/bar/external_file.rs"),
24155 OpenOptions::default(),
24156 window,
24157 cx,
24158 )
24159 })
24160 .await
24161 .expect("opening external file")
24162 .downcast::<Editor>()
24163 .expect("downcasted external file's open element to editor");
24164 pane.update(cx, |pane, cx| {
24165 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24166 open_editor.update(cx, |editor, cx| {
24167 assert_eq!(
24168 editor.display_text(cx),
24169 "pub mod external {}",
24170 "External file is open now",
24171 );
24172 });
24173 assert_eq!(open_editor, external_editor);
24174 });
24175 assert_language_servers_count(
24176 1,
24177 "Second, external, *.rs file should join the existing server",
24178 cx,
24179 );
24180
24181 pane.update_in(cx, |pane, window, cx| {
24182 pane.close_active_item(&CloseActiveItem::default(), window, cx)
24183 })
24184 .await
24185 .unwrap();
24186 pane.update_in(cx, |pane, window, cx| {
24187 pane.navigate_backward(&Default::default(), window, cx);
24188 });
24189 cx.run_until_parked();
24190 pane.update(cx, |pane, cx| {
24191 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24192 open_editor.update(cx, |editor, cx| {
24193 assert_eq!(
24194 editor.display_text(cx),
24195 "pub mod external {}",
24196 "External file is open now",
24197 );
24198 });
24199 });
24200 assert_language_servers_count(
24201 1,
24202 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
24203 cx,
24204 );
24205
24206 cx.update(|_, cx| {
24207 workspace::reload(cx);
24208 });
24209 assert_language_servers_count(
24210 1,
24211 "After reloading the worktree with local and external files opened, only one project should be started",
24212 cx,
24213 );
24214}
24215
24216#[gpui::test]
24217async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
24218 init_test(cx, |_| {});
24219
24220 let mut cx = EditorTestContext::new(cx).await;
24221 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24222 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24223
24224 // test cursor move to start of each line on tab
24225 // for `if`, `elif`, `else`, `while`, `with` and `for`
24226 cx.set_state(indoc! {"
24227 def main():
24228 ˇ for item in items:
24229 ˇ while item.active:
24230 ˇ if item.value > 10:
24231 ˇ continue
24232 ˇ elif item.value < 0:
24233 ˇ break
24234 ˇ else:
24235 ˇ with item.context() as ctx:
24236 ˇ yield count
24237 ˇ else:
24238 ˇ log('while else')
24239 ˇ else:
24240 ˇ log('for else')
24241 "});
24242 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24243 cx.assert_editor_state(indoc! {"
24244 def main():
24245 ˇfor item in items:
24246 ˇwhile item.active:
24247 ˇif item.value > 10:
24248 ˇcontinue
24249 ˇelif item.value < 0:
24250 ˇbreak
24251 ˇelse:
24252 ˇwith item.context() as ctx:
24253 ˇyield count
24254 ˇelse:
24255 ˇlog('while else')
24256 ˇelse:
24257 ˇlog('for else')
24258 "});
24259 // test relative indent is preserved when tab
24260 // for `if`, `elif`, `else`, `while`, `with` and `for`
24261 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24262 cx.assert_editor_state(indoc! {"
24263 def main():
24264 ˇfor item in items:
24265 ˇwhile item.active:
24266 ˇif item.value > 10:
24267 ˇcontinue
24268 ˇelif item.value < 0:
24269 ˇbreak
24270 ˇelse:
24271 ˇwith item.context() as ctx:
24272 ˇyield count
24273 ˇelse:
24274 ˇlog('while else')
24275 ˇelse:
24276 ˇlog('for else')
24277 "});
24278
24279 // test cursor move to start of each line on tab
24280 // for `try`, `except`, `else`, `finally`, `match` and `def`
24281 cx.set_state(indoc! {"
24282 def main():
24283 ˇ try:
24284 ˇ fetch()
24285 ˇ except ValueError:
24286 ˇ handle_error()
24287 ˇ else:
24288 ˇ match value:
24289 ˇ case _:
24290 ˇ finally:
24291 ˇ def status():
24292 ˇ return 0
24293 "});
24294 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24295 cx.assert_editor_state(indoc! {"
24296 def main():
24297 ˇtry:
24298 ˇfetch()
24299 ˇexcept ValueError:
24300 ˇhandle_error()
24301 ˇelse:
24302 ˇmatch value:
24303 ˇcase _:
24304 ˇfinally:
24305 ˇdef status():
24306 ˇreturn 0
24307 "});
24308 // test relative indent is preserved when tab
24309 // for `try`, `except`, `else`, `finally`, `match` and `def`
24310 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24311 cx.assert_editor_state(indoc! {"
24312 def main():
24313 ˇtry:
24314 ˇfetch()
24315 ˇexcept ValueError:
24316 ˇhandle_error()
24317 ˇelse:
24318 ˇmatch value:
24319 ˇcase _:
24320 ˇfinally:
24321 ˇdef status():
24322 ˇreturn 0
24323 "});
24324}
24325
24326#[gpui::test]
24327async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
24328 init_test(cx, |_| {});
24329
24330 let mut cx = EditorTestContext::new(cx).await;
24331 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24332 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24333
24334 // test `else` auto outdents when typed inside `if` block
24335 cx.set_state(indoc! {"
24336 def main():
24337 if i == 2:
24338 return
24339 ˇ
24340 "});
24341 cx.update_editor(|editor, window, cx| {
24342 editor.handle_input("else:", window, cx);
24343 });
24344 cx.assert_editor_state(indoc! {"
24345 def main():
24346 if i == 2:
24347 return
24348 else:ˇ
24349 "});
24350
24351 // test `except` auto outdents when typed inside `try` block
24352 cx.set_state(indoc! {"
24353 def main():
24354 try:
24355 i = 2
24356 ˇ
24357 "});
24358 cx.update_editor(|editor, window, cx| {
24359 editor.handle_input("except:", window, cx);
24360 });
24361 cx.assert_editor_state(indoc! {"
24362 def main():
24363 try:
24364 i = 2
24365 except:ˇ
24366 "});
24367
24368 // test `else` auto outdents when typed inside `except` block
24369 cx.set_state(indoc! {"
24370 def main():
24371 try:
24372 i = 2
24373 except:
24374 j = 2
24375 ˇ
24376 "});
24377 cx.update_editor(|editor, window, cx| {
24378 editor.handle_input("else:", window, cx);
24379 });
24380 cx.assert_editor_state(indoc! {"
24381 def main():
24382 try:
24383 i = 2
24384 except:
24385 j = 2
24386 else:ˇ
24387 "});
24388
24389 // test `finally` auto outdents when typed inside `else` block
24390 cx.set_state(indoc! {"
24391 def main():
24392 try:
24393 i = 2
24394 except:
24395 j = 2
24396 else:
24397 k = 2
24398 ˇ
24399 "});
24400 cx.update_editor(|editor, window, cx| {
24401 editor.handle_input("finally:", window, cx);
24402 });
24403 cx.assert_editor_state(indoc! {"
24404 def main():
24405 try:
24406 i = 2
24407 except:
24408 j = 2
24409 else:
24410 k = 2
24411 finally:ˇ
24412 "});
24413
24414 // test `else` does not outdents when typed inside `except` block right after for block
24415 cx.set_state(indoc! {"
24416 def main():
24417 try:
24418 i = 2
24419 except:
24420 for i in range(n):
24421 pass
24422 ˇ
24423 "});
24424 cx.update_editor(|editor, window, cx| {
24425 editor.handle_input("else:", window, cx);
24426 });
24427 cx.assert_editor_state(indoc! {"
24428 def main():
24429 try:
24430 i = 2
24431 except:
24432 for i in range(n):
24433 pass
24434 else:ˇ
24435 "});
24436
24437 // test `finally` auto outdents when typed inside `else` block right after for block
24438 cx.set_state(indoc! {"
24439 def main():
24440 try:
24441 i = 2
24442 except:
24443 j = 2
24444 else:
24445 for i in range(n):
24446 pass
24447 ˇ
24448 "});
24449 cx.update_editor(|editor, window, cx| {
24450 editor.handle_input("finally:", window, cx);
24451 });
24452 cx.assert_editor_state(indoc! {"
24453 def main():
24454 try:
24455 i = 2
24456 except:
24457 j = 2
24458 else:
24459 for i in range(n):
24460 pass
24461 finally:ˇ
24462 "});
24463
24464 // test `except` outdents to inner "try" block
24465 cx.set_state(indoc! {"
24466 def main():
24467 try:
24468 i = 2
24469 if i == 2:
24470 try:
24471 i = 3
24472 ˇ
24473 "});
24474 cx.update_editor(|editor, window, cx| {
24475 editor.handle_input("except:", window, cx);
24476 });
24477 cx.assert_editor_state(indoc! {"
24478 def main():
24479 try:
24480 i = 2
24481 if i == 2:
24482 try:
24483 i = 3
24484 except:ˇ
24485 "});
24486
24487 // test `except` outdents to outer "try" block
24488 cx.set_state(indoc! {"
24489 def main():
24490 try:
24491 i = 2
24492 if i == 2:
24493 try:
24494 i = 3
24495 ˇ
24496 "});
24497 cx.update_editor(|editor, window, cx| {
24498 editor.handle_input("except:", window, cx);
24499 });
24500 cx.assert_editor_state(indoc! {"
24501 def main():
24502 try:
24503 i = 2
24504 if i == 2:
24505 try:
24506 i = 3
24507 except:ˇ
24508 "});
24509
24510 // test `else` stays at correct indent when typed after `for` block
24511 cx.set_state(indoc! {"
24512 def main():
24513 for i in range(10):
24514 if i == 3:
24515 break
24516 ˇ
24517 "});
24518 cx.update_editor(|editor, window, cx| {
24519 editor.handle_input("else:", window, cx);
24520 });
24521 cx.assert_editor_state(indoc! {"
24522 def main():
24523 for i in range(10):
24524 if i == 3:
24525 break
24526 else:ˇ
24527 "});
24528
24529 // test does not outdent on typing after line with square brackets
24530 cx.set_state(indoc! {"
24531 def f() -> list[str]:
24532 ˇ
24533 "});
24534 cx.update_editor(|editor, window, cx| {
24535 editor.handle_input("a", window, cx);
24536 });
24537 cx.assert_editor_state(indoc! {"
24538 def f() -> list[str]:
24539 aˇ
24540 "});
24541
24542 // test does not outdent on typing : after case keyword
24543 cx.set_state(indoc! {"
24544 match 1:
24545 caseˇ
24546 "});
24547 cx.update_editor(|editor, window, cx| {
24548 editor.handle_input(":", window, cx);
24549 });
24550 cx.assert_editor_state(indoc! {"
24551 match 1:
24552 case:ˇ
24553 "});
24554}
24555
24556#[gpui::test]
24557async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
24558 init_test(cx, |_| {});
24559 update_test_language_settings(cx, |settings| {
24560 settings.defaults.extend_comment_on_newline = Some(false);
24561 });
24562 let mut cx = EditorTestContext::new(cx).await;
24563 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24564 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24565
24566 // test correct indent after newline on comment
24567 cx.set_state(indoc! {"
24568 # COMMENT:ˇ
24569 "});
24570 cx.update_editor(|editor, window, cx| {
24571 editor.newline(&Newline, window, cx);
24572 });
24573 cx.assert_editor_state(indoc! {"
24574 # COMMENT:
24575 ˇ
24576 "});
24577
24578 // test correct indent after newline in brackets
24579 cx.set_state(indoc! {"
24580 {ˇ}
24581 "});
24582 cx.update_editor(|editor, window, cx| {
24583 editor.newline(&Newline, window, cx);
24584 });
24585 cx.run_until_parked();
24586 cx.assert_editor_state(indoc! {"
24587 {
24588 ˇ
24589 }
24590 "});
24591
24592 cx.set_state(indoc! {"
24593 (ˇ)
24594 "});
24595 cx.update_editor(|editor, window, cx| {
24596 editor.newline(&Newline, window, cx);
24597 });
24598 cx.run_until_parked();
24599 cx.assert_editor_state(indoc! {"
24600 (
24601 ˇ
24602 )
24603 "});
24604
24605 // do not indent after empty lists or dictionaries
24606 cx.set_state(indoc! {"
24607 a = []ˇ
24608 "});
24609 cx.update_editor(|editor, window, cx| {
24610 editor.newline(&Newline, window, cx);
24611 });
24612 cx.run_until_parked();
24613 cx.assert_editor_state(indoc! {"
24614 a = []
24615 ˇ
24616 "});
24617}
24618
24619#[gpui::test]
24620async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
24621 init_test(cx, |_| {});
24622
24623 let mut cx = EditorTestContext::new(cx).await;
24624 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24625 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24626
24627 // test cursor move to start of each line on tab
24628 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
24629 cx.set_state(indoc! {"
24630 function main() {
24631 ˇ for item in $items; do
24632 ˇ while [ -n \"$item\" ]; do
24633 ˇ if [ \"$value\" -gt 10 ]; then
24634 ˇ continue
24635 ˇ elif [ \"$value\" -lt 0 ]; then
24636 ˇ break
24637 ˇ else
24638 ˇ echo \"$item\"
24639 ˇ fi
24640 ˇ done
24641 ˇ done
24642 ˇ}
24643 "});
24644 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24645 cx.assert_editor_state(indoc! {"
24646 function main() {
24647 ˇfor item in $items; do
24648 ˇwhile [ -n \"$item\" ]; do
24649 ˇif [ \"$value\" -gt 10 ]; then
24650 ˇcontinue
24651 ˇelif [ \"$value\" -lt 0 ]; then
24652 ˇbreak
24653 ˇelse
24654 ˇecho \"$item\"
24655 ˇfi
24656 ˇdone
24657 ˇdone
24658 ˇ}
24659 "});
24660 // test relative indent is preserved when tab
24661 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24662 cx.assert_editor_state(indoc! {"
24663 function main() {
24664 ˇfor item in $items; do
24665 ˇwhile [ -n \"$item\" ]; do
24666 ˇif [ \"$value\" -gt 10 ]; then
24667 ˇcontinue
24668 ˇelif [ \"$value\" -lt 0 ]; then
24669 ˇbreak
24670 ˇelse
24671 ˇecho \"$item\"
24672 ˇfi
24673 ˇdone
24674 ˇdone
24675 ˇ}
24676 "});
24677
24678 // test cursor move to start of each line on tab
24679 // for `case` statement with patterns
24680 cx.set_state(indoc! {"
24681 function handle() {
24682 ˇ case \"$1\" in
24683 ˇ start)
24684 ˇ echo \"a\"
24685 ˇ ;;
24686 ˇ stop)
24687 ˇ echo \"b\"
24688 ˇ ;;
24689 ˇ *)
24690 ˇ echo \"c\"
24691 ˇ ;;
24692 ˇ esac
24693 ˇ}
24694 "});
24695 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24696 cx.assert_editor_state(indoc! {"
24697 function handle() {
24698 ˇcase \"$1\" in
24699 ˇstart)
24700 ˇecho \"a\"
24701 ˇ;;
24702 ˇstop)
24703 ˇecho \"b\"
24704 ˇ;;
24705 ˇ*)
24706 ˇecho \"c\"
24707 ˇ;;
24708 ˇesac
24709 ˇ}
24710 "});
24711}
24712
24713#[gpui::test]
24714async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
24715 init_test(cx, |_| {});
24716
24717 let mut cx = EditorTestContext::new(cx).await;
24718 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24719 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24720
24721 // test indents on comment insert
24722 cx.set_state(indoc! {"
24723 function main() {
24724 ˇ for item in $items; do
24725 ˇ while [ -n \"$item\" ]; do
24726 ˇ if [ \"$value\" -gt 10 ]; then
24727 ˇ continue
24728 ˇ elif [ \"$value\" -lt 0 ]; then
24729 ˇ break
24730 ˇ else
24731 ˇ echo \"$item\"
24732 ˇ fi
24733 ˇ done
24734 ˇ done
24735 ˇ}
24736 "});
24737 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
24738 cx.assert_editor_state(indoc! {"
24739 function main() {
24740 #ˇ for item in $items; do
24741 #ˇ while [ -n \"$item\" ]; do
24742 #ˇ if [ \"$value\" -gt 10 ]; then
24743 #ˇ continue
24744 #ˇ elif [ \"$value\" -lt 0 ]; then
24745 #ˇ break
24746 #ˇ else
24747 #ˇ echo \"$item\"
24748 #ˇ fi
24749 #ˇ done
24750 #ˇ done
24751 #ˇ}
24752 "});
24753}
24754
24755#[gpui::test]
24756async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
24757 init_test(cx, |_| {});
24758
24759 let mut cx = EditorTestContext::new(cx).await;
24760 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24761 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24762
24763 // test `else` auto outdents when typed inside `if` block
24764 cx.set_state(indoc! {"
24765 if [ \"$1\" = \"test\" ]; then
24766 echo \"foo bar\"
24767 ˇ
24768 "});
24769 cx.update_editor(|editor, window, cx| {
24770 editor.handle_input("else", window, cx);
24771 });
24772 cx.assert_editor_state(indoc! {"
24773 if [ \"$1\" = \"test\" ]; then
24774 echo \"foo bar\"
24775 elseˇ
24776 "});
24777
24778 // test `elif` auto outdents when typed inside `if` block
24779 cx.set_state(indoc! {"
24780 if [ \"$1\" = \"test\" ]; then
24781 echo \"foo bar\"
24782 ˇ
24783 "});
24784 cx.update_editor(|editor, window, cx| {
24785 editor.handle_input("elif", window, cx);
24786 });
24787 cx.assert_editor_state(indoc! {"
24788 if [ \"$1\" = \"test\" ]; then
24789 echo \"foo bar\"
24790 elifˇ
24791 "});
24792
24793 // test `fi` auto outdents when typed inside `else` block
24794 cx.set_state(indoc! {"
24795 if [ \"$1\" = \"test\" ]; then
24796 echo \"foo bar\"
24797 else
24798 echo \"bar baz\"
24799 ˇ
24800 "});
24801 cx.update_editor(|editor, window, cx| {
24802 editor.handle_input("fi", window, cx);
24803 });
24804 cx.assert_editor_state(indoc! {"
24805 if [ \"$1\" = \"test\" ]; then
24806 echo \"foo bar\"
24807 else
24808 echo \"bar baz\"
24809 fiˇ
24810 "});
24811
24812 // test `done` auto outdents when typed inside `while` block
24813 cx.set_state(indoc! {"
24814 while read line; do
24815 echo \"$line\"
24816 ˇ
24817 "});
24818 cx.update_editor(|editor, window, cx| {
24819 editor.handle_input("done", window, cx);
24820 });
24821 cx.assert_editor_state(indoc! {"
24822 while read line; do
24823 echo \"$line\"
24824 doneˇ
24825 "});
24826
24827 // test `done` auto outdents when typed inside `for` block
24828 cx.set_state(indoc! {"
24829 for file in *.txt; do
24830 cat \"$file\"
24831 ˇ
24832 "});
24833 cx.update_editor(|editor, window, cx| {
24834 editor.handle_input("done", window, cx);
24835 });
24836 cx.assert_editor_state(indoc! {"
24837 for file in *.txt; do
24838 cat \"$file\"
24839 doneˇ
24840 "});
24841
24842 // test `esac` auto outdents when typed inside `case` block
24843 cx.set_state(indoc! {"
24844 case \"$1\" in
24845 start)
24846 echo \"foo bar\"
24847 ;;
24848 stop)
24849 echo \"bar baz\"
24850 ;;
24851 ˇ
24852 "});
24853 cx.update_editor(|editor, window, cx| {
24854 editor.handle_input("esac", window, cx);
24855 });
24856 cx.assert_editor_state(indoc! {"
24857 case \"$1\" in
24858 start)
24859 echo \"foo bar\"
24860 ;;
24861 stop)
24862 echo \"bar baz\"
24863 ;;
24864 esacˇ
24865 "});
24866
24867 // test `*)` auto outdents when typed inside `case` block
24868 cx.set_state(indoc! {"
24869 case \"$1\" in
24870 start)
24871 echo \"foo bar\"
24872 ;;
24873 ˇ
24874 "});
24875 cx.update_editor(|editor, window, cx| {
24876 editor.handle_input("*)", window, cx);
24877 });
24878 cx.assert_editor_state(indoc! {"
24879 case \"$1\" in
24880 start)
24881 echo \"foo bar\"
24882 ;;
24883 *)ˇ
24884 "});
24885
24886 // test `fi` outdents to correct level with nested if blocks
24887 cx.set_state(indoc! {"
24888 if [ \"$1\" = \"test\" ]; then
24889 echo \"outer if\"
24890 if [ \"$2\" = \"debug\" ]; then
24891 echo \"inner if\"
24892 ˇ
24893 "});
24894 cx.update_editor(|editor, window, cx| {
24895 editor.handle_input("fi", window, cx);
24896 });
24897 cx.assert_editor_state(indoc! {"
24898 if [ \"$1\" = \"test\" ]; then
24899 echo \"outer if\"
24900 if [ \"$2\" = \"debug\" ]; then
24901 echo \"inner if\"
24902 fiˇ
24903 "});
24904}
24905
24906#[gpui::test]
24907async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
24908 init_test(cx, |_| {});
24909 update_test_language_settings(cx, |settings| {
24910 settings.defaults.extend_comment_on_newline = Some(false);
24911 });
24912 let mut cx = EditorTestContext::new(cx).await;
24913 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24914 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24915
24916 // test correct indent after newline on comment
24917 cx.set_state(indoc! {"
24918 # COMMENT:ˇ
24919 "});
24920 cx.update_editor(|editor, window, cx| {
24921 editor.newline(&Newline, window, cx);
24922 });
24923 cx.assert_editor_state(indoc! {"
24924 # COMMENT:
24925 ˇ
24926 "});
24927
24928 // test correct indent after newline after `then`
24929 cx.set_state(indoc! {"
24930
24931 if [ \"$1\" = \"test\" ]; thenˇ
24932 "});
24933 cx.update_editor(|editor, window, cx| {
24934 editor.newline(&Newline, window, cx);
24935 });
24936 cx.run_until_parked();
24937 cx.assert_editor_state(indoc! {"
24938
24939 if [ \"$1\" = \"test\" ]; then
24940 ˇ
24941 "});
24942
24943 // test correct indent after newline after `else`
24944 cx.set_state(indoc! {"
24945 if [ \"$1\" = \"test\" ]; then
24946 elseˇ
24947 "});
24948 cx.update_editor(|editor, window, cx| {
24949 editor.newline(&Newline, window, cx);
24950 });
24951 cx.run_until_parked();
24952 cx.assert_editor_state(indoc! {"
24953 if [ \"$1\" = \"test\" ]; then
24954 else
24955 ˇ
24956 "});
24957
24958 // test correct indent after newline after `elif`
24959 cx.set_state(indoc! {"
24960 if [ \"$1\" = \"test\" ]; then
24961 elifˇ
24962 "});
24963 cx.update_editor(|editor, window, cx| {
24964 editor.newline(&Newline, window, cx);
24965 });
24966 cx.run_until_parked();
24967 cx.assert_editor_state(indoc! {"
24968 if [ \"$1\" = \"test\" ]; then
24969 elif
24970 ˇ
24971 "});
24972
24973 // test correct indent after newline after `do`
24974 cx.set_state(indoc! {"
24975 for file in *.txt; doˇ
24976 "});
24977 cx.update_editor(|editor, window, cx| {
24978 editor.newline(&Newline, window, cx);
24979 });
24980 cx.run_until_parked();
24981 cx.assert_editor_state(indoc! {"
24982 for file in *.txt; do
24983 ˇ
24984 "});
24985
24986 // test correct indent after newline after case pattern
24987 cx.set_state(indoc! {"
24988 case \"$1\" in
24989 start)ˇ
24990 "});
24991 cx.update_editor(|editor, window, cx| {
24992 editor.newline(&Newline, window, cx);
24993 });
24994 cx.run_until_parked();
24995 cx.assert_editor_state(indoc! {"
24996 case \"$1\" in
24997 start)
24998 ˇ
24999 "});
25000
25001 // test correct indent after newline after case pattern
25002 cx.set_state(indoc! {"
25003 case \"$1\" in
25004 start)
25005 ;;
25006 *)ˇ
25007 "});
25008 cx.update_editor(|editor, window, cx| {
25009 editor.newline(&Newline, window, cx);
25010 });
25011 cx.run_until_parked();
25012 cx.assert_editor_state(indoc! {"
25013 case \"$1\" in
25014 start)
25015 ;;
25016 *)
25017 ˇ
25018 "});
25019
25020 // test correct indent after newline after function opening brace
25021 cx.set_state(indoc! {"
25022 function test() {ˇ}
25023 "});
25024 cx.update_editor(|editor, window, cx| {
25025 editor.newline(&Newline, window, cx);
25026 });
25027 cx.run_until_parked();
25028 cx.assert_editor_state(indoc! {"
25029 function test() {
25030 ˇ
25031 }
25032 "});
25033
25034 // test no extra indent after semicolon on same line
25035 cx.set_state(indoc! {"
25036 echo \"test\";ˇ
25037 "});
25038 cx.update_editor(|editor, window, cx| {
25039 editor.newline(&Newline, window, cx);
25040 });
25041 cx.run_until_parked();
25042 cx.assert_editor_state(indoc! {"
25043 echo \"test\";
25044 ˇ
25045 "});
25046}
25047
25048fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
25049 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
25050 point..point
25051}
25052
25053#[track_caller]
25054fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
25055 let (text, ranges) = marked_text_ranges(marked_text, true);
25056 assert_eq!(editor.text(cx), text);
25057 assert_eq!(
25058 editor.selections.ranges(cx),
25059 ranges,
25060 "Assert selections are {}",
25061 marked_text
25062 );
25063}
25064
25065pub fn handle_signature_help_request(
25066 cx: &mut EditorLspTestContext,
25067 mocked_response: lsp::SignatureHelp,
25068) -> impl Future<Output = ()> + use<> {
25069 let mut request =
25070 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
25071 let mocked_response = mocked_response.clone();
25072 async move { Ok(Some(mocked_response)) }
25073 });
25074
25075 async move {
25076 request.next().await;
25077 }
25078}
25079
25080#[track_caller]
25081pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
25082 cx.update_editor(|editor, _, _| {
25083 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
25084 let entries = menu.entries.borrow();
25085 let entries = entries
25086 .iter()
25087 .map(|entry| entry.string.as_str())
25088 .collect::<Vec<_>>();
25089 assert_eq!(entries, expected);
25090 } else {
25091 panic!("Expected completions menu");
25092 }
25093 });
25094}
25095
25096/// Handle completion request passing a marked string specifying where the completion
25097/// should be triggered from using '|' character, what range should be replaced, and what completions
25098/// should be returned using '<' and '>' to delimit the range.
25099///
25100/// Also see `handle_completion_request_with_insert_and_replace`.
25101#[track_caller]
25102pub fn handle_completion_request(
25103 marked_string: &str,
25104 completions: Vec<&'static str>,
25105 is_incomplete: bool,
25106 counter: Arc<AtomicUsize>,
25107 cx: &mut EditorLspTestContext,
25108) -> impl Future<Output = ()> {
25109 let complete_from_marker: TextRangeMarker = '|'.into();
25110 let replace_range_marker: TextRangeMarker = ('<', '>').into();
25111 let (_, mut marked_ranges) = marked_text_ranges_by(
25112 marked_string,
25113 vec![complete_from_marker.clone(), replace_range_marker.clone()],
25114 );
25115
25116 let complete_from_position =
25117 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
25118 let replace_range =
25119 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
25120
25121 let mut request =
25122 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
25123 let completions = completions.clone();
25124 counter.fetch_add(1, atomic::Ordering::Release);
25125 async move {
25126 assert_eq!(params.text_document_position.text_document.uri, url.clone());
25127 assert_eq!(
25128 params.text_document_position.position,
25129 complete_from_position
25130 );
25131 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
25132 is_incomplete,
25133 item_defaults: None,
25134 items: completions
25135 .iter()
25136 .map(|completion_text| lsp::CompletionItem {
25137 label: completion_text.to_string(),
25138 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
25139 range: replace_range,
25140 new_text: completion_text.to_string(),
25141 })),
25142 ..Default::default()
25143 })
25144 .collect(),
25145 })))
25146 }
25147 });
25148
25149 async move {
25150 request.next().await;
25151 }
25152}
25153
25154/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
25155/// given instead, which also contains an `insert` range.
25156///
25157/// This function uses markers to define ranges:
25158/// - `|` marks the cursor position
25159/// - `<>` marks the replace range
25160/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
25161pub fn handle_completion_request_with_insert_and_replace(
25162 cx: &mut EditorLspTestContext,
25163 marked_string: &str,
25164 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
25165 counter: Arc<AtomicUsize>,
25166) -> impl Future<Output = ()> {
25167 let complete_from_marker: TextRangeMarker = '|'.into();
25168 let replace_range_marker: TextRangeMarker = ('<', '>').into();
25169 let insert_range_marker: TextRangeMarker = ('{', '}').into();
25170
25171 let (_, mut marked_ranges) = marked_text_ranges_by(
25172 marked_string,
25173 vec![
25174 complete_from_marker.clone(),
25175 replace_range_marker.clone(),
25176 insert_range_marker.clone(),
25177 ],
25178 );
25179
25180 let complete_from_position =
25181 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
25182 let replace_range =
25183 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
25184
25185 let insert_range = match marked_ranges.remove(&insert_range_marker) {
25186 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
25187 _ => lsp::Range {
25188 start: replace_range.start,
25189 end: complete_from_position,
25190 },
25191 };
25192
25193 let mut request =
25194 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
25195 let completions = completions.clone();
25196 counter.fetch_add(1, atomic::Ordering::Release);
25197 async move {
25198 assert_eq!(params.text_document_position.text_document.uri, url.clone());
25199 assert_eq!(
25200 params.text_document_position.position, complete_from_position,
25201 "marker `|` position doesn't match",
25202 );
25203 Ok(Some(lsp::CompletionResponse::Array(
25204 completions
25205 .iter()
25206 .map(|(label, new_text)| lsp::CompletionItem {
25207 label: label.to_string(),
25208 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
25209 lsp::InsertReplaceEdit {
25210 insert: insert_range,
25211 replace: replace_range,
25212 new_text: new_text.to_string(),
25213 },
25214 )),
25215 ..Default::default()
25216 })
25217 .collect(),
25218 )))
25219 }
25220 });
25221
25222 async move {
25223 request.next().await;
25224 }
25225}
25226
25227fn handle_resolve_completion_request(
25228 cx: &mut EditorLspTestContext,
25229 edits: Option<Vec<(&'static str, &'static str)>>,
25230) -> impl Future<Output = ()> {
25231 let edits = edits.map(|edits| {
25232 edits
25233 .iter()
25234 .map(|(marked_string, new_text)| {
25235 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
25236 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
25237 lsp::TextEdit::new(replace_range, new_text.to_string())
25238 })
25239 .collect::<Vec<_>>()
25240 });
25241
25242 let mut request =
25243 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
25244 let edits = edits.clone();
25245 async move {
25246 Ok(lsp::CompletionItem {
25247 additional_text_edits: edits,
25248 ..Default::default()
25249 })
25250 }
25251 });
25252
25253 async move {
25254 request.next().await;
25255 }
25256}
25257
25258pub(crate) fn update_test_language_settings(
25259 cx: &mut TestAppContext,
25260 f: impl Fn(&mut AllLanguageSettingsContent),
25261) {
25262 cx.update(|cx| {
25263 SettingsStore::update_global(cx, |store, cx| {
25264 store.update_user_settings(cx, |settings| f(&mut settings.project.all_languages));
25265 });
25266 });
25267}
25268
25269pub(crate) fn update_test_project_settings(
25270 cx: &mut TestAppContext,
25271 f: impl Fn(&mut ProjectSettingsContent),
25272) {
25273 cx.update(|cx| {
25274 SettingsStore::update_global(cx, |store, cx| {
25275 store.update_user_settings(cx, |settings| f(&mut settings.project));
25276 });
25277 });
25278}
25279
25280pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
25281 cx.update(|cx| {
25282 assets::Assets.load_test_fonts(cx);
25283 let store = SettingsStore::test(cx);
25284 cx.set_global(store);
25285 theme::init(theme::LoadThemes::JustBase, cx);
25286 release_channel::init(SemanticVersion::default(), cx);
25287 client::init_settings(cx);
25288 language::init(cx);
25289 Project::init_settings(cx);
25290 workspace::init_settings(cx);
25291 crate::init(cx);
25292 });
25293 zlog::init_test();
25294 update_test_language_settings(cx, f);
25295}
25296
25297#[track_caller]
25298fn assert_hunk_revert(
25299 not_reverted_text_with_selections: &str,
25300 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
25301 expected_reverted_text_with_selections: &str,
25302 base_text: &str,
25303 cx: &mut EditorLspTestContext,
25304) {
25305 cx.set_state(not_reverted_text_with_selections);
25306 cx.set_head_text(base_text);
25307 cx.executor().run_until_parked();
25308
25309 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
25310 let snapshot = editor.snapshot(window, cx);
25311 let reverted_hunk_statuses = snapshot
25312 .buffer_snapshot
25313 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
25314 .map(|hunk| hunk.status().kind)
25315 .collect::<Vec<_>>();
25316
25317 editor.git_restore(&Default::default(), window, cx);
25318 reverted_hunk_statuses
25319 });
25320 cx.executor().run_until_parked();
25321 cx.assert_editor_state(expected_reverted_text_with_selections);
25322 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
25323}
25324
25325#[gpui::test(iterations = 10)]
25326async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
25327 init_test(cx, |_| {});
25328
25329 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
25330 let counter = diagnostic_requests.clone();
25331
25332 let fs = FakeFs::new(cx.executor());
25333 fs.insert_tree(
25334 path!("/a"),
25335 json!({
25336 "first.rs": "fn main() { let a = 5; }",
25337 "second.rs": "// Test file",
25338 }),
25339 )
25340 .await;
25341
25342 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25343 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25344 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25345
25346 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25347 language_registry.add(rust_lang());
25348 let mut fake_servers = language_registry.register_fake_lsp(
25349 "Rust",
25350 FakeLspAdapter {
25351 capabilities: lsp::ServerCapabilities {
25352 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
25353 lsp::DiagnosticOptions {
25354 identifier: None,
25355 inter_file_dependencies: true,
25356 workspace_diagnostics: true,
25357 work_done_progress_options: Default::default(),
25358 },
25359 )),
25360 ..Default::default()
25361 },
25362 ..Default::default()
25363 },
25364 );
25365
25366 let editor = workspace
25367 .update(cx, |workspace, window, cx| {
25368 workspace.open_abs_path(
25369 PathBuf::from(path!("/a/first.rs")),
25370 OpenOptions::default(),
25371 window,
25372 cx,
25373 )
25374 })
25375 .unwrap()
25376 .await
25377 .unwrap()
25378 .downcast::<Editor>()
25379 .unwrap();
25380 let fake_server = fake_servers.next().await.unwrap();
25381 let server_id = fake_server.server.server_id();
25382 let mut first_request = fake_server
25383 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
25384 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
25385 let result_id = Some(new_result_id.to_string());
25386 assert_eq!(
25387 params.text_document.uri,
25388 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25389 );
25390 async move {
25391 Ok(lsp::DocumentDiagnosticReportResult::Report(
25392 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
25393 related_documents: None,
25394 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
25395 items: Vec::new(),
25396 result_id,
25397 },
25398 }),
25399 ))
25400 }
25401 });
25402
25403 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
25404 project.update(cx, |project, cx| {
25405 let buffer_id = editor
25406 .read(cx)
25407 .buffer()
25408 .read(cx)
25409 .as_singleton()
25410 .expect("created a singleton buffer")
25411 .read(cx)
25412 .remote_id();
25413 let buffer_result_id = project
25414 .lsp_store()
25415 .read(cx)
25416 .result_id(server_id, buffer_id, cx);
25417 assert_eq!(expected, buffer_result_id);
25418 });
25419 };
25420
25421 ensure_result_id(None, cx);
25422 cx.executor().advance_clock(Duration::from_millis(60));
25423 cx.executor().run_until_parked();
25424 assert_eq!(
25425 diagnostic_requests.load(atomic::Ordering::Acquire),
25426 1,
25427 "Opening file should trigger diagnostic request"
25428 );
25429 first_request
25430 .next()
25431 .await
25432 .expect("should have sent the first diagnostics pull request");
25433 ensure_result_id(Some("1".to_string()), cx);
25434
25435 // Editing should trigger diagnostics
25436 editor.update_in(cx, |editor, window, cx| {
25437 editor.handle_input("2", window, cx)
25438 });
25439 cx.executor().advance_clock(Duration::from_millis(60));
25440 cx.executor().run_until_parked();
25441 assert_eq!(
25442 diagnostic_requests.load(atomic::Ordering::Acquire),
25443 2,
25444 "Editing should trigger diagnostic request"
25445 );
25446 ensure_result_id(Some("2".to_string()), cx);
25447
25448 // Moving cursor should not trigger diagnostic request
25449 editor.update_in(cx, |editor, window, cx| {
25450 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25451 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
25452 });
25453 });
25454 cx.executor().advance_clock(Duration::from_millis(60));
25455 cx.executor().run_until_parked();
25456 assert_eq!(
25457 diagnostic_requests.load(atomic::Ordering::Acquire),
25458 2,
25459 "Cursor movement should not trigger diagnostic request"
25460 );
25461 ensure_result_id(Some("2".to_string()), cx);
25462 // Multiple rapid edits should be debounced
25463 for _ in 0..5 {
25464 editor.update_in(cx, |editor, window, cx| {
25465 editor.handle_input("x", window, cx)
25466 });
25467 }
25468 cx.executor().advance_clock(Duration::from_millis(60));
25469 cx.executor().run_until_parked();
25470
25471 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
25472 assert!(
25473 final_requests <= 4,
25474 "Multiple rapid edits should be debounced (got {final_requests} requests)",
25475 );
25476 ensure_result_id(Some(final_requests.to_string()), cx);
25477}
25478
25479#[gpui::test]
25480async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
25481 // Regression test for issue #11671
25482 // Previously, adding a cursor after moving multiple cursors would reset
25483 // the cursor count instead of adding to the existing cursors.
25484 init_test(cx, |_| {});
25485 let mut cx = EditorTestContext::new(cx).await;
25486
25487 // Create a simple buffer with cursor at start
25488 cx.set_state(indoc! {"
25489 ˇaaaa
25490 bbbb
25491 cccc
25492 dddd
25493 eeee
25494 ffff
25495 gggg
25496 hhhh"});
25497
25498 // Add 2 cursors below (so we have 3 total)
25499 cx.update_editor(|editor, window, cx| {
25500 editor.add_selection_below(&Default::default(), window, cx);
25501 editor.add_selection_below(&Default::default(), window, cx);
25502 });
25503
25504 // Verify we have 3 cursors
25505 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
25506 assert_eq!(
25507 initial_count, 3,
25508 "Should have 3 cursors after adding 2 below"
25509 );
25510
25511 // Move down one line
25512 cx.update_editor(|editor, window, cx| {
25513 editor.move_down(&MoveDown, window, cx);
25514 });
25515
25516 // Add another cursor below
25517 cx.update_editor(|editor, window, cx| {
25518 editor.add_selection_below(&Default::default(), window, cx);
25519 });
25520
25521 // Should now have 4 cursors (3 original + 1 new)
25522 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
25523 assert_eq!(
25524 final_count, 4,
25525 "Should have 4 cursors after moving and adding another"
25526 );
25527}
25528
25529#[gpui::test(iterations = 10)]
25530async fn test_document_colors(cx: &mut TestAppContext) {
25531 let expected_color = Rgba {
25532 r: 0.33,
25533 g: 0.33,
25534 b: 0.33,
25535 a: 0.33,
25536 };
25537
25538 init_test(cx, |_| {});
25539
25540 let fs = FakeFs::new(cx.executor());
25541 fs.insert_tree(
25542 path!("/a"),
25543 json!({
25544 "first.rs": "fn main() { let a = 5; }",
25545 }),
25546 )
25547 .await;
25548
25549 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25550 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25551 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25552
25553 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25554 language_registry.add(rust_lang());
25555 let mut fake_servers = language_registry.register_fake_lsp(
25556 "Rust",
25557 FakeLspAdapter {
25558 capabilities: lsp::ServerCapabilities {
25559 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
25560 ..lsp::ServerCapabilities::default()
25561 },
25562 name: "rust-analyzer",
25563 ..FakeLspAdapter::default()
25564 },
25565 );
25566 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
25567 "Rust",
25568 FakeLspAdapter {
25569 capabilities: lsp::ServerCapabilities {
25570 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
25571 ..lsp::ServerCapabilities::default()
25572 },
25573 name: "not-rust-analyzer",
25574 ..FakeLspAdapter::default()
25575 },
25576 );
25577
25578 let editor = workspace
25579 .update(cx, |workspace, window, cx| {
25580 workspace.open_abs_path(
25581 PathBuf::from(path!("/a/first.rs")),
25582 OpenOptions::default(),
25583 window,
25584 cx,
25585 )
25586 })
25587 .unwrap()
25588 .await
25589 .unwrap()
25590 .downcast::<Editor>()
25591 .unwrap();
25592 let fake_language_server = fake_servers.next().await.unwrap();
25593 let fake_language_server_without_capabilities =
25594 fake_servers_without_capabilities.next().await.unwrap();
25595 let requests_made = Arc::new(AtomicUsize::new(0));
25596 let closure_requests_made = Arc::clone(&requests_made);
25597 let mut color_request_handle = fake_language_server
25598 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
25599 let requests_made = Arc::clone(&closure_requests_made);
25600 async move {
25601 assert_eq!(
25602 params.text_document.uri,
25603 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25604 );
25605 requests_made.fetch_add(1, atomic::Ordering::Release);
25606 Ok(vec![
25607 lsp::ColorInformation {
25608 range: lsp::Range {
25609 start: lsp::Position {
25610 line: 0,
25611 character: 0,
25612 },
25613 end: lsp::Position {
25614 line: 0,
25615 character: 1,
25616 },
25617 },
25618 color: lsp::Color {
25619 red: 0.33,
25620 green: 0.33,
25621 blue: 0.33,
25622 alpha: 0.33,
25623 },
25624 },
25625 lsp::ColorInformation {
25626 range: lsp::Range {
25627 start: lsp::Position {
25628 line: 0,
25629 character: 0,
25630 },
25631 end: lsp::Position {
25632 line: 0,
25633 character: 1,
25634 },
25635 },
25636 color: lsp::Color {
25637 red: 0.33,
25638 green: 0.33,
25639 blue: 0.33,
25640 alpha: 0.33,
25641 },
25642 },
25643 ])
25644 }
25645 });
25646
25647 let _handle = fake_language_server_without_capabilities
25648 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
25649 panic!("Should not be called");
25650 });
25651 cx.executor().advance_clock(Duration::from_millis(100));
25652 color_request_handle.next().await.unwrap();
25653 cx.run_until_parked();
25654 assert_eq!(
25655 1,
25656 requests_made.load(atomic::Ordering::Acquire),
25657 "Should query for colors once per editor open"
25658 );
25659 editor.update_in(cx, |editor, _, cx| {
25660 assert_eq!(
25661 vec![expected_color],
25662 extract_color_inlays(editor, cx),
25663 "Should have an initial inlay"
25664 );
25665 });
25666
25667 // opening another file in a split should not influence the LSP query counter
25668 workspace
25669 .update(cx, |workspace, window, cx| {
25670 assert_eq!(
25671 workspace.panes().len(),
25672 1,
25673 "Should have one pane with one editor"
25674 );
25675 workspace.move_item_to_pane_in_direction(
25676 &MoveItemToPaneInDirection {
25677 direction: SplitDirection::Right,
25678 focus: false,
25679 clone: true,
25680 },
25681 window,
25682 cx,
25683 );
25684 })
25685 .unwrap();
25686 cx.run_until_parked();
25687 workspace
25688 .update(cx, |workspace, _, cx| {
25689 let panes = workspace.panes();
25690 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
25691 for pane in panes {
25692 let editor = pane
25693 .read(cx)
25694 .active_item()
25695 .and_then(|item| item.downcast::<Editor>())
25696 .expect("Should have opened an editor in each split");
25697 let editor_file = editor
25698 .read(cx)
25699 .buffer()
25700 .read(cx)
25701 .as_singleton()
25702 .expect("test deals with singleton buffers")
25703 .read(cx)
25704 .file()
25705 .expect("test buffese should have a file")
25706 .path();
25707 assert_eq!(
25708 editor_file.as_ref(),
25709 rel_path("first.rs"),
25710 "Both editors should be opened for the same file"
25711 )
25712 }
25713 })
25714 .unwrap();
25715
25716 cx.executor().advance_clock(Duration::from_millis(500));
25717 let save = editor.update_in(cx, |editor, window, cx| {
25718 editor.move_to_end(&MoveToEnd, window, cx);
25719 editor.handle_input("dirty", window, cx);
25720 editor.save(
25721 SaveOptions {
25722 format: true,
25723 autosave: true,
25724 },
25725 project.clone(),
25726 window,
25727 cx,
25728 )
25729 });
25730 save.await.unwrap();
25731
25732 color_request_handle.next().await.unwrap();
25733 cx.run_until_parked();
25734 assert_eq!(
25735 3,
25736 requests_made.load(atomic::Ordering::Acquire),
25737 "Should query for colors once per save and once per formatting after save"
25738 );
25739
25740 drop(editor);
25741 let close = workspace
25742 .update(cx, |workspace, window, cx| {
25743 workspace.active_pane().update(cx, |pane, cx| {
25744 pane.close_active_item(&CloseActiveItem::default(), window, cx)
25745 })
25746 })
25747 .unwrap();
25748 close.await.unwrap();
25749 let close = workspace
25750 .update(cx, |workspace, window, cx| {
25751 workspace.active_pane().update(cx, |pane, cx| {
25752 pane.close_active_item(&CloseActiveItem::default(), window, cx)
25753 })
25754 })
25755 .unwrap();
25756 close.await.unwrap();
25757 assert_eq!(
25758 3,
25759 requests_made.load(atomic::Ordering::Acquire),
25760 "After saving and closing all editors, no extra requests should be made"
25761 );
25762 workspace
25763 .update(cx, |workspace, _, cx| {
25764 assert!(
25765 workspace.active_item(cx).is_none(),
25766 "Should close all editors"
25767 )
25768 })
25769 .unwrap();
25770
25771 workspace
25772 .update(cx, |workspace, window, cx| {
25773 workspace.active_pane().update(cx, |pane, cx| {
25774 pane.navigate_backward(&workspace::GoBack, window, cx);
25775 })
25776 })
25777 .unwrap();
25778 cx.executor().advance_clock(Duration::from_millis(100));
25779 cx.run_until_parked();
25780 let editor = workspace
25781 .update(cx, |workspace, _, cx| {
25782 workspace
25783 .active_item(cx)
25784 .expect("Should have reopened the editor again after navigating back")
25785 .downcast::<Editor>()
25786 .expect("Should be an editor")
25787 })
25788 .unwrap();
25789 color_request_handle.next().await.unwrap();
25790 assert_eq!(
25791 3,
25792 requests_made.load(atomic::Ordering::Acquire),
25793 "Cache should be reused on buffer close and reopen"
25794 );
25795 editor.update(cx, |editor, cx| {
25796 assert_eq!(
25797 vec![expected_color],
25798 extract_color_inlays(editor, cx),
25799 "Should have an initial inlay"
25800 );
25801 });
25802
25803 drop(color_request_handle);
25804 let closure_requests_made = Arc::clone(&requests_made);
25805 let mut empty_color_request_handle = fake_language_server
25806 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
25807 let requests_made = Arc::clone(&closure_requests_made);
25808 async move {
25809 assert_eq!(
25810 params.text_document.uri,
25811 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25812 );
25813 requests_made.fetch_add(1, atomic::Ordering::Release);
25814 Ok(Vec::new())
25815 }
25816 });
25817 let save = editor.update_in(cx, |editor, window, cx| {
25818 editor.move_to_end(&MoveToEnd, window, cx);
25819 editor.handle_input("dirty_again", window, cx);
25820 editor.save(
25821 SaveOptions {
25822 format: false,
25823 autosave: true,
25824 },
25825 project.clone(),
25826 window,
25827 cx,
25828 )
25829 });
25830 save.await.unwrap();
25831
25832 empty_color_request_handle.next().await.unwrap();
25833 cx.run_until_parked();
25834 assert_eq!(
25835 4,
25836 requests_made.load(atomic::Ordering::Acquire),
25837 "Should query for colors once per save only, as formatting was not requested"
25838 );
25839 editor.update(cx, |editor, cx| {
25840 assert_eq!(
25841 Vec::<Rgba>::new(),
25842 extract_color_inlays(editor, cx),
25843 "Should clear all colors when the server returns an empty response"
25844 );
25845 });
25846}
25847
25848#[gpui::test]
25849async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
25850 init_test(cx, |_| {});
25851 let (editor, cx) = cx.add_window_view(Editor::single_line);
25852 editor.update_in(cx, |editor, window, cx| {
25853 editor.set_text("oops\n\nwow\n", window, cx)
25854 });
25855 cx.run_until_parked();
25856 editor.update(cx, |editor, cx| {
25857 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
25858 });
25859 editor.update(cx, |editor, cx| editor.edit([(3..5, "")], cx));
25860 cx.run_until_parked();
25861 editor.update(cx, |editor, cx| {
25862 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
25863 });
25864}
25865
25866#[gpui::test]
25867async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
25868 init_test(cx, |_| {});
25869
25870 cx.update(|cx| {
25871 register_project_item::<Editor>(cx);
25872 });
25873
25874 let fs = FakeFs::new(cx.executor());
25875 fs.insert_tree("/root1", json!({})).await;
25876 fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
25877 .await;
25878
25879 let project = Project::test(fs, ["/root1".as_ref()], cx).await;
25880 let (workspace, cx) =
25881 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
25882
25883 let worktree_id = project.update(cx, |project, cx| {
25884 project.worktrees(cx).next().unwrap().read(cx).id()
25885 });
25886
25887 let handle = workspace
25888 .update_in(cx, |workspace, window, cx| {
25889 let project_path = (worktree_id, rel_path("one.pdf"));
25890 workspace.open_path(project_path, None, true, window, cx)
25891 })
25892 .await
25893 .unwrap();
25894
25895 assert_eq!(
25896 handle.to_any().entity_type(),
25897 TypeId::of::<InvalidBufferView>()
25898 );
25899}
25900
25901#[gpui::test]
25902async fn test_select_next_prev_syntax_node(cx: &mut TestAppContext) {
25903 init_test(cx, |_| {});
25904
25905 let language = Arc::new(Language::new(
25906 LanguageConfig::default(),
25907 Some(tree_sitter_rust::LANGUAGE.into()),
25908 ));
25909
25910 // Test hierarchical sibling navigation
25911 let text = r#"
25912 fn outer() {
25913 if condition {
25914 let a = 1;
25915 }
25916 let b = 2;
25917 }
25918
25919 fn another() {
25920 let c = 3;
25921 }
25922 "#;
25923
25924 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
25925 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
25926 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
25927
25928 // Wait for parsing to complete
25929 editor
25930 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
25931 .await;
25932
25933 editor.update_in(cx, |editor, window, cx| {
25934 // Start by selecting "let a = 1;" inside the if block
25935 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25936 s.select_display_ranges([
25937 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 26)
25938 ]);
25939 });
25940
25941 let initial_selection = editor.selections.display_ranges(cx);
25942 assert_eq!(initial_selection.len(), 1, "Should have one selection");
25943
25944 // Test select next sibling - should move up levels to find the next sibling
25945 // Since "let a = 1;" has no siblings in the if block, it should move up
25946 // to find "let b = 2;" which is a sibling of the if block
25947 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
25948 let next_selection = editor.selections.display_ranges(cx);
25949
25950 // Should have a selection and it should be different from the initial
25951 assert_eq!(
25952 next_selection.len(),
25953 1,
25954 "Should have one selection after next"
25955 );
25956 assert_ne!(
25957 next_selection[0], initial_selection[0],
25958 "Next sibling selection should be different"
25959 );
25960
25961 // Test hierarchical navigation by going to the end of the current function
25962 // and trying to navigate to the next function
25963 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25964 s.select_display_ranges([
25965 DisplayPoint::new(DisplayRow(5), 12)..DisplayPoint::new(DisplayRow(5), 22)
25966 ]);
25967 });
25968
25969 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
25970 let function_next_selection = editor.selections.display_ranges(cx);
25971
25972 // Should move to the next function
25973 assert_eq!(
25974 function_next_selection.len(),
25975 1,
25976 "Should have one selection after function next"
25977 );
25978
25979 // Test select previous sibling navigation
25980 editor.select_prev_syntax_node(&SelectPreviousSyntaxNode, window, cx);
25981 let prev_selection = editor.selections.display_ranges(cx);
25982
25983 // Should have a selection and it should be different
25984 assert_eq!(
25985 prev_selection.len(),
25986 1,
25987 "Should have one selection after prev"
25988 );
25989 assert_ne!(
25990 prev_selection[0], function_next_selection[0],
25991 "Previous sibling selection should be different from next"
25992 );
25993 });
25994}
25995
25996#[gpui::test]
25997async fn test_next_prev_document_highlight(cx: &mut TestAppContext) {
25998 init_test(cx, |_| {});
25999
26000 let mut cx = EditorTestContext::new(cx).await;
26001 cx.set_state(
26002 "let ˇvariable = 42;
26003let another = variable + 1;
26004let result = variable * 2;",
26005 );
26006
26007 // Set up document highlights manually (simulating LSP response)
26008 cx.update_editor(|editor, _window, cx| {
26009 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
26010
26011 // Create highlights for "variable" occurrences
26012 let highlight_ranges = [
26013 Point::new(0, 4)..Point::new(0, 12), // First "variable"
26014 Point::new(1, 14)..Point::new(1, 22), // Second "variable"
26015 Point::new(2, 13)..Point::new(2, 21), // Third "variable"
26016 ];
26017
26018 let anchor_ranges: Vec<_> = highlight_ranges
26019 .iter()
26020 .map(|range| range.clone().to_anchors(&buffer_snapshot))
26021 .collect();
26022
26023 editor.highlight_background::<DocumentHighlightRead>(
26024 &anchor_ranges,
26025 |theme| theme.colors().editor_document_highlight_read_background,
26026 cx,
26027 );
26028 });
26029
26030 // Go to next highlight - should move to second "variable"
26031 cx.update_editor(|editor, window, cx| {
26032 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26033 });
26034 cx.assert_editor_state(
26035 "let variable = 42;
26036let another = ˇvariable + 1;
26037let result = variable * 2;",
26038 );
26039
26040 // Go to next highlight - should move to third "variable"
26041 cx.update_editor(|editor, window, cx| {
26042 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26043 });
26044 cx.assert_editor_state(
26045 "let variable = 42;
26046let another = variable + 1;
26047let result = ˇvariable * 2;",
26048 );
26049
26050 // Go to next highlight - should stay at third "variable" (no wrap-around)
26051 cx.update_editor(|editor, window, cx| {
26052 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26053 });
26054 cx.assert_editor_state(
26055 "let variable = 42;
26056let another = variable + 1;
26057let result = ˇvariable * 2;",
26058 );
26059
26060 // Now test going backwards from third position
26061 cx.update_editor(|editor, window, cx| {
26062 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26063 });
26064 cx.assert_editor_state(
26065 "let variable = 42;
26066let another = ˇvariable + 1;
26067let result = variable * 2;",
26068 );
26069
26070 // Go to previous highlight - should move to first "variable"
26071 cx.update_editor(|editor, window, cx| {
26072 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26073 });
26074 cx.assert_editor_state(
26075 "let ˇvariable = 42;
26076let another = variable + 1;
26077let result = variable * 2;",
26078 );
26079
26080 // Go to previous highlight - should stay on first "variable"
26081 cx.update_editor(|editor, window, cx| {
26082 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26083 });
26084 cx.assert_editor_state(
26085 "let ˇvariable = 42;
26086let another = variable + 1;
26087let result = variable * 2;",
26088 );
26089}
26090
26091#[gpui::test]
26092async fn test_paste_url_from_other_app_creates_markdown_link_over_selected_text(
26093 cx: &mut gpui::TestAppContext,
26094) {
26095 init_test(cx, |_| {});
26096
26097 let url = "https://zed.dev";
26098
26099 let markdown_language = Arc::new(Language::new(
26100 LanguageConfig {
26101 name: "Markdown".into(),
26102 ..LanguageConfig::default()
26103 },
26104 None,
26105 ));
26106
26107 let mut cx = EditorTestContext::new(cx).await;
26108 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26109 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)");
26110
26111 cx.update_editor(|editor, window, cx| {
26112 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26113 editor.paste(&Paste, window, cx);
26114 });
26115
26116 cx.assert_editor_state(&format!(
26117 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)"
26118 ));
26119}
26120
26121#[gpui::test]
26122async fn test_paste_url_from_zed_copy_creates_markdown_link_over_selected_text(
26123 cx: &mut gpui::TestAppContext,
26124) {
26125 init_test(cx, |_| {});
26126
26127 let url = "https://zed.dev";
26128
26129 let markdown_language = Arc::new(Language::new(
26130 LanguageConfig {
26131 name: "Markdown".into(),
26132 ..LanguageConfig::default()
26133 },
26134 None,
26135 ));
26136
26137 let mut cx = EditorTestContext::new(cx).await;
26138 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26139 cx.set_state(&format!(
26140 "Hello, editor.\nZed is great (see this link: )\n«{url}ˇ»"
26141 ));
26142
26143 cx.update_editor(|editor, window, cx| {
26144 editor.copy(&Copy, window, cx);
26145 });
26146
26147 cx.set_state(&format!(
26148 "Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)\n{url}"
26149 ));
26150
26151 cx.update_editor(|editor, window, cx| {
26152 editor.paste(&Paste, window, cx);
26153 });
26154
26155 cx.assert_editor_state(&format!(
26156 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)\n{url}"
26157 ));
26158}
26159
26160#[gpui::test]
26161async fn test_paste_url_from_other_app_replaces_existing_url_without_creating_markdown_link(
26162 cx: &mut gpui::TestAppContext,
26163) {
26164 init_test(cx, |_| {});
26165
26166 let url = "https://zed.dev";
26167
26168 let markdown_language = Arc::new(Language::new(
26169 LanguageConfig {
26170 name: "Markdown".into(),
26171 ..LanguageConfig::default()
26172 },
26173 None,
26174 ));
26175
26176 let mut cx = EditorTestContext::new(cx).await;
26177 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26178 cx.set_state("Please visit zed's homepage: «https://www.apple.comˇ»");
26179
26180 cx.update_editor(|editor, window, cx| {
26181 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26182 editor.paste(&Paste, window, cx);
26183 });
26184
26185 cx.assert_editor_state(&format!("Please visit zed's homepage: {url}ˇ"));
26186}
26187
26188#[gpui::test]
26189async fn test_paste_plain_text_from_other_app_replaces_selection_without_creating_markdown_link(
26190 cx: &mut gpui::TestAppContext,
26191) {
26192 init_test(cx, |_| {});
26193
26194 let text = "Awesome";
26195
26196 let markdown_language = Arc::new(Language::new(
26197 LanguageConfig {
26198 name: "Markdown".into(),
26199 ..LanguageConfig::default()
26200 },
26201 None,
26202 ));
26203
26204 let mut cx = EditorTestContext::new(cx).await;
26205 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26206 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat»");
26207
26208 cx.update_editor(|editor, window, cx| {
26209 cx.write_to_clipboard(ClipboardItem::new_string(text.to_string()));
26210 editor.paste(&Paste, window, cx);
26211 });
26212
26213 cx.assert_editor_state(&format!("Hello, {text}ˇ.\nZed is {text}ˇ"));
26214}
26215
26216#[gpui::test]
26217async fn test_paste_url_from_other_app_without_creating_markdown_link_in_non_markdown_language(
26218 cx: &mut gpui::TestAppContext,
26219) {
26220 init_test(cx, |_| {});
26221
26222 let url = "https://zed.dev";
26223
26224 let markdown_language = Arc::new(Language::new(
26225 LanguageConfig {
26226 name: "Rust".into(),
26227 ..LanguageConfig::default()
26228 },
26229 None,
26230 ));
26231
26232 let mut cx = EditorTestContext::new(cx).await;
26233 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26234 cx.set_state("// Hello, «editorˇ».\n// Zed is «ˇgreat» (see this link: ˇ)");
26235
26236 cx.update_editor(|editor, window, cx| {
26237 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26238 editor.paste(&Paste, window, cx);
26239 });
26240
26241 cx.assert_editor_state(&format!(
26242 "// Hello, {url}ˇ.\n// Zed is {url}ˇ (see this link: {url}ˇ)"
26243 ));
26244}
26245
26246#[gpui::test]
26247async fn test_paste_url_from_other_app_creates_markdown_link_selectively_in_multi_buffer(
26248 cx: &mut TestAppContext,
26249) {
26250 init_test(cx, |_| {});
26251
26252 let url = "https://zed.dev";
26253
26254 let markdown_language = Arc::new(Language::new(
26255 LanguageConfig {
26256 name: "Markdown".into(),
26257 ..LanguageConfig::default()
26258 },
26259 None,
26260 ));
26261
26262 let (editor, cx) = cx.add_window_view(|window, cx| {
26263 let multi_buffer = MultiBuffer::build_multi(
26264 [
26265 ("this will embed -> link", vec![Point::row_range(0..1)]),
26266 ("this will replace -> link", vec![Point::row_range(0..1)]),
26267 ],
26268 cx,
26269 );
26270 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
26271 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26272 s.select_ranges(vec![
26273 Point::new(0, 19)..Point::new(0, 23),
26274 Point::new(1, 21)..Point::new(1, 25),
26275 ])
26276 });
26277 let first_buffer_id = multi_buffer
26278 .read(cx)
26279 .excerpt_buffer_ids()
26280 .into_iter()
26281 .next()
26282 .unwrap();
26283 let first_buffer = multi_buffer.read(cx).buffer(first_buffer_id).unwrap();
26284 first_buffer.update(cx, |buffer, cx| {
26285 buffer.set_language(Some(markdown_language.clone()), cx);
26286 });
26287
26288 editor
26289 });
26290 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
26291
26292 cx.update_editor(|editor, window, cx| {
26293 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26294 editor.paste(&Paste, window, cx);
26295 });
26296
26297 cx.assert_editor_state(&format!(
26298 "this will embed -> [link]({url})ˇ\nthis will replace -> {url}ˇ"
26299 ));
26300}
26301
26302#[track_caller]
26303fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
26304 editor
26305 .all_inlays(cx)
26306 .into_iter()
26307 .filter_map(|inlay| inlay.get_color())
26308 .map(Rgba::from)
26309 .collect()
26310}