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), 0)..DisplayPoint::new(DisplayRow(0), 0),
4247 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
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 assert_eq!(
11938 params.context.only,
11939 Some(vec!["code-action-1".into(), "code-action-2".into()])
11940 );
11941 let uri = lsp::Uri::from_file_path(path!("/file.rs")).unwrap();
11942 Ok(Some(vec![
11943 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
11944 kind: Some("code-action-1".into()),
11945 edit: Some(lsp::WorkspaceEdit::new(
11946 [(
11947 uri.clone(),
11948 vec![lsp::TextEdit::new(
11949 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
11950 "applied-code-action-1-edit\n".to_string(),
11951 )],
11952 )]
11953 .into_iter()
11954 .collect(),
11955 )),
11956 command: Some(lsp::Command {
11957 command: "the-command-for-code-action-1".into(),
11958 ..Default::default()
11959 }),
11960 ..Default::default()
11961 }),
11962 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
11963 kind: Some("code-action-2".into()),
11964 edit: Some(lsp::WorkspaceEdit::new(
11965 [(
11966 uri,
11967 vec![lsp::TextEdit::new(
11968 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
11969 "applied-code-action-2-edit\n".to_string(),
11970 )],
11971 )]
11972 .into_iter()
11973 .collect(),
11974 )),
11975 ..Default::default()
11976 }),
11977 ]))
11978 },
11979 );
11980
11981 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
11982 move |params, _| async move { Ok(params) }
11983 });
11984
11985 let command_lock = Arc::new(futures::lock::Mutex::new(()));
11986 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
11987 let fake = fake_server.clone();
11988 let lock = command_lock.clone();
11989 move |params, _| {
11990 assert_eq!(params.command, "the-command-for-code-action-1");
11991 let fake = fake.clone();
11992 let lock = lock.clone();
11993 async move {
11994 lock.lock().await;
11995 fake.server
11996 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
11997 label: None,
11998 edit: lsp::WorkspaceEdit {
11999 changes: Some(
12000 [(
12001 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
12002 vec![lsp::TextEdit {
12003 range: lsp::Range::new(
12004 lsp::Position::new(0, 0),
12005 lsp::Position::new(0, 0),
12006 ),
12007 new_text: "applied-code-action-1-command\n".into(),
12008 }],
12009 )]
12010 .into_iter()
12011 .collect(),
12012 ),
12013 ..Default::default()
12014 },
12015 })
12016 .await
12017 .into_response()
12018 .unwrap();
12019 Ok(Some(json!(null)))
12020 }
12021 }
12022 });
12023
12024 cx.executor().start_waiting();
12025 editor
12026 .update_in(cx, |editor, window, cx| {
12027 editor.perform_format(
12028 project.clone(),
12029 FormatTrigger::Manual,
12030 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12031 window,
12032 cx,
12033 )
12034 })
12035 .unwrap()
12036 .await;
12037 editor.update(cx, |editor, cx| {
12038 assert_eq!(
12039 editor.text(cx),
12040 r#"
12041 applied-code-action-2-edit
12042 applied-code-action-1-command
12043 applied-code-action-1-edit
12044 applied-formatting
12045 one
12046 two
12047 three
12048 "#
12049 .unindent()
12050 );
12051 });
12052
12053 editor.update_in(cx, |editor, window, cx| {
12054 editor.undo(&Default::default(), window, cx);
12055 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12056 });
12057
12058 // Perform a manual edit while waiting for an LSP command
12059 // that's being run as part of a formatting code action.
12060 let lock_guard = command_lock.lock().await;
12061 let format = editor
12062 .update_in(cx, |editor, window, cx| {
12063 editor.perform_format(
12064 project.clone(),
12065 FormatTrigger::Manual,
12066 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12067 window,
12068 cx,
12069 )
12070 })
12071 .unwrap();
12072 cx.run_until_parked();
12073 editor.update(cx, |editor, cx| {
12074 assert_eq!(
12075 editor.text(cx),
12076 r#"
12077 applied-code-action-1-edit
12078 applied-formatting
12079 one
12080 two
12081 three
12082 "#
12083 .unindent()
12084 );
12085
12086 editor.buffer.update(cx, |buffer, cx| {
12087 let ix = buffer.len(cx);
12088 buffer.edit([(ix..ix, "edited\n")], None, cx);
12089 });
12090 });
12091
12092 // Allow the LSP command to proceed. Because the buffer was edited,
12093 // the second code action will not be run.
12094 drop(lock_guard);
12095 format.await;
12096 editor.update_in(cx, |editor, window, cx| {
12097 assert_eq!(
12098 editor.text(cx),
12099 r#"
12100 applied-code-action-1-command
12101 applied-code-action-1-edit
12102 applied-formatting
12103 one
12104 two
12105 three
12106 edited
12107 "#
12108 .unindent()
12109 );
12110
12111 // The manual edit is undone first, because it is the last thing the user did
12112 // (even though the command completed afterwards).
12113 editor.undo(&Default::default(), window, cx);
12114 assert_eq!(
12115 editor.text(cx),
12116 r#"
12117 applied-code-action-1-command
12118 applied-code-action-1-edit
12119 applied-formatting
12120 one
12121 two
12122 three
12123 "#
12124 .unindent()
12125 );
12126
12127 // All the formatting (including the command, which completed after the manual edit)
12128 // is undone together.
12129 editor.undo(&Default::default(), window, cx);
12130 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12131 });
12132}
12133
12134#[gpui::test]
12135async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
12136 init_test(cx, |settings| {
12137 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
12138 Formatter::LanguageServer { name: None },
12139 ])))
12140 });
12141
12142 let fs = FakeFs::new(cx.executor());
12143 fs.insert_file(path!("/file.ts"), Default::default()).await;
12144
12145 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12146
12147 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12148 language_registry.add(Arc::new(Language::new(
12149 LanguageConfig {
12150 name: "TypeScript".into(),
12151 matcher: LanguageMatcher {
12152 path_suffixes: vec!["ts".to_string()],
12153 ..Default::default()
12154 },
12155 ..LanguageConfig::default()
12156 },
12157 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
12158 )));
12159 update_test_language_settings(cx, |settings| {
12160 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
12161 });
12162 let mut fake_servers = language_registry.register_fake_lsp(
12163 "TypeScript",
12164 FakeLspAdapter {
12165 capabilities: lsp::ServerCapabilities {
12166 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
12167 ..Default::default()
12168 },
12169 ..Default::default()
12170 },
12171 );
12172
12173 let buffer = project
12174 .update(cx, |project, cx| {
12175 project.open_local_buffer(path!("/file.ts"), cx)
12176 })
12177 .await
12178 .unwrap();
12179
12180 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12181 let (editor, cx) = cx.add_window_view(|window, cx| {
12182 build_editor_with_project(project.clone(), buffer, window, cx)
12183 });
12184 editor.update_in(cx, |editor, window, cx| {
12185 editor.set_text(
12186 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12187 window,
12188 cx,
12189 )
12190 });
12191
12192 cx.executor().start_waiting();
12193 let fake_server = fake_servers.next().await.unwrap();
12194
12195 let format = editor
12196 .update_in(cx, |editor, window, cx| {
12197 editor.perform_code_action_kind(
12198 project.clone(),
12199 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12200 window,
12201 cx,
12202 )
12203 })
12204 .unwrap();
12205 fake_server
12206 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
12207 assert_eq!(
12208 params.text_document.uri,
12209 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12210 );
12211 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
12212 lsp::CodeAction {
12213 title: "Organize Imports".to_string(),
12214 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
12215 edit: Some(lsp::WorkspaceEdit {
12216 changes: Some(
12217 [(
12218 params.text_document.uri.clone(),
12219 vec![lsp::TextEdit::new(
12220 lsp::Range::new(
12221 lsp::Position::new(1, 0),
12222 lsp::Position::new(2, 0),
12223 ),
12224 "".to_string(),
12225 )],
12226 )]
12227 .into_iter()
12228 .collect(),
12229 ),
12230 ..Default::default()
12231 }),
12232 ..Default::default()
12233 },
12234 )]))
12235 })
12236 .next()
12237 .await;
12238 cx.executor().start_waiting();
12239 format.await;
12240 assert_eq!(
12241 editor.update(cx, |editor, cx| editor.text(cx)),
12242 "import { a } from 'module';\n\nconst x = a;\n"
12243 );
12244
12245 editor.update_in(cx, |editor, window, cx| {
12246 editor.set_text(
12247 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12248 window,
12249 cx,
12250 )
12251 });
12252 // Ensure we don't lock if code action hangs.
12253 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
12254 move |params, _| async move {
12255 assert_eq!(
12256 params.text_document.uri,
12257 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12258 );
12259 futures::future::pending::<()>().await;
12260 unreachable!()
12261 },
12262 );
12263 let format = editor
12264 .update_in(cx, |editor, window, cx| {
12265 editor.perform_code_action_kind(
12266 project,
12267 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12268 window,
12269 cx,
12270 )
12271 })
12272 .unwrap();
12273 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
12274 cx.executor().start_waiting();
12275 format.await;
12276 assert_eq!(
12277 editor.update(cx, |editor, cx| editor.text(cx)),
12278 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
12279 );
12280}
12281
12282#[gpui::test]
12283async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
12284 init_test(cx, |_| {});
12285
12286 let mut cx = EditorLspTestContext::new_rust(
12287 lsp::ServerCapabilities {
12288 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12289 ..Default::default()
12290 },
12291 cx,
12292 )
12293 .await;
12294
12295 cx.set_state(indoc! {"
12296 one.twoˇ
12297 "});
12298
12299 // The format request takes a long time. When it completes, it inserts
12300 // a newline and an indent before the `.`
12301 cx.lsp
12302 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
12303 let executor = cx.background_executor().clone();
12304 async move {
12305 executor.timer(Duration::from_millis(100)).await;
12306 Ok(Some(vec![lsp::TextEdit {
12307 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
12308 new_text: "\n ".into(),
12309 }]))
12310 }
12311 });
12312
12313 // Submit a format request.
12314 let format_1 = cx
12315 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12316 .unwrap();
12317 cx.executor().run_until_parked();
12318
12319 // Submit a second format request.
12320 let format_2 = cx
12321 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12322 .unwrap();
12323 cx.executor().run_until_parked();
12324
12325 // Wait for both format requests to complete
12326 cx.executor().advance_clock(Duration::from_millis(200));
12327 cx.executor().start_waiting();
12328 format_1.await.unwrap();
12329 cx.executor().start_waiting();
12330 format_2.await.unwrap();
12331
12332 // The formatting edits only happens once.
12333 cx.assert_editor_state(indoc! {"
12334 one
12335 .twoˇ
12336 "});
12337}
12338
12339#[gpui::test]
12340async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
12341 init_test(cx, |settings| {
12342 settings.defaults.formatter = Some(SelectedFormatter::Auto)
12343 });
12344
12345 let mut cx = EditorLspTestContext::new_rust(
12346 lsp::ServerCapabilities {
12347 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12348 ..Default::default()
12349 },
12350 cx,
12351 )
12352 .await;
12353
12354 // Set up a buffer white some trailing whitespace and no trailing newline.
12355 cx.set_state(
12356 &[
12357 "one ", //
12358 "twoˇ", //
12359 "three ", //
12360 "four", //
12361 ]
12362 .join("\n"),
12363 );
12364
12365 // Submit a format request.
12366 let format = cx
12367 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12368 .unwrap();
12369
12370 // Record which buffer changes have been sent to the language server
12371 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
12372 cx.lsp
12373 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
12374 let buffer_changes = buffer_changes.clone();
12375 move |params, _| {
12376 buffer_changes.lock().extend(
12377 params
12378 .content_changes
12379 .into_iter()
12380 .map(|e| (e.range.unwrap(), e.text)),
12381 );
12382 }
12383 });
12384
12385 // Handle formatting requests to the language server.
12386 cx.lsp
12387 .set_request_handler::<lsp::request::Formatting, _, _>({
12388 let buffer_changes = buffer_changes.clone();
12389 move |_, _| {
12390 // When formatting is requested, trailing whitespace has already been stripped,
12391 // and the trailing newline has already been added.
12392 assert_eq!(
12393 &buffer_changes.lock()[1..],
12394 &[
12395 (
12396 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
12397 "".into()
12398 ),
12399 (
12400 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
12401 "".into()
12402 ),
12403 (
12404 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
12405 "\n".into()
12406 ),
12407 ]
12408 );
12409
12410 // Insert blank lines between each line of the buffer.
12411 async move {
12412 Ok(Some(vec![
12413 lsp::TextEdit {
12414 range: lsp::Range::new(
12415 lsp::Position::new(1, 0),
12416 lsp::Position::new(1, 0),
12417 ),
12418 new_text: "\n".into(),
12419 },
12420 lsp::TextEdit {
12421 range: lsp::Range::new(
12422 lsp::Position::new(2, 0),
12423 lsp::Position::new(2, 0),
12424 ),
12425 new_text: "\n".into(),
12426 },
12427 ]))
12428 }
12429 }
12430 });
12431
12432 // After formatting the buffer, the trailing whitespace is stripped,
12433 // a newline is appended, and the edits provided by the language server
12434 // have been applied.
12435 format.await.unwrap();
12436 cx.assert_editor_state(
12437 &[
12438 "one", //
12439 "", //
12440 "twoˇ", //
12441 "", //
12442 "three", //
12443 "four", //
12444 "", //
12445 ]
12446 .join("\n"),
12447 );
12448
12449 // Undoing the formatting undoes the trailing whitespace removal, the
12450 // trailing newline, and the LSP edits.
12451 cx.update_buffer(|buffer, cx| buffer.undo(cx));
12452 cx.assert_editor_state(
12453 &[
12454 "one ", //
12455 "twoˇ", //
12456 "three ", //
12457 "four", //
12458 ]
12459 .join("\n"),
12460 );
12461}
12462
12463#[gpui::test]
12464async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
12465 cx: &mut TestAppContext,
12466) {
12467 init_test(cx, |_| {});
12468
12469 cx.update(|cx| {
12470 cx.update_global::<SettingsStore, _>(|settings, cx| {
12471 settings.update_user_settings(cx, |settings| {
12472 settings.editor.auto_signature_help = Some(true);
12473 });
12474 });
12475 });
12476
12477 let mut cx = EditorLspTestContext::new_rust(
12478 lsp::ServerCapabilities {
12479 signature_help_provider: Some(lsp::SignatureHelpOptions {
12480 ..Default::default()
12481 }),
12482 ..Default::default()
12483 },
12484 cx,
12485 )
12486 .await;
12487
12488 let language = Language::new(
12489 LanguageConfig {
12490 name: "Rust".into(),
12491 brackets: BracketPairConfig {
12492 pairs: vec![
12493 BracketPair {
12494 start: "{".to_string(),
12495 end: "}".to_string(),
12496 close: true,
12497 surround: true,
12498 newline: true,
12499 },
12500 BracketPair {
12501 start: "(".to_string(),
12502 end: ")".to_string(),
12503 close: true,
12504 surround: true,
12505 newline: true,
12506 },
12507 BracketPair {
12508 start: "/*".to_string(),
12509 end: " */".to_string(),
12510 close: true,
12511 surround: true,
12512 newline: true,
12513 },
12514 BracketPair {
12515 start: "[".to_string(),
12516 end: "]".to_string(),
12517 close: false,
12518 surround: false,
12519 newline: true,
12520 },
12521 BracketPair {
12522 start: "\"".to_string(),
12523 end: "\"".to_string(),
12524 close: true,
12525 surround: true,
12526 newline: false,
12527 },
12528 BracketPair {
12529 start: "<".to_string(),
12530 end: ">".to_string(),
12531 close: false,
12532 surround: true,
12533 newline: true,
12534 },
12535 ],
12536 ..Default::default()
12537 },
12538 autoclose_before: "})]".to_string(),
12539 ..Default::default()
12540 },
12541 Some(tree_sitter_rust::LANGUAGE.into()),
12542 );
12543 let language = Arc::new(language);
12544
12545 cx.language_registry().add(language.clone());
12546 cx.update_buffer(|buffer, cx| {
12547 buffer.set_language(Some(language), cx);
12548 });
12549
12550 cx.set_state(
12551 &r#"
12552 fn main() {
12553 sampleˇ
12554 }
12555 "#
12556 .unindent(),
12557 );
12558
12559 cx.update_editor(|editor, window, cx| {
12560 editor.handle_input("(", window, cx);
12561 });
12562 cx.assert_editor_state(
12563 &"
12564 fn main() {
12565 sample(ˇ)
12566 }
12567 "
12568 .unindent(),
12569 );
12570
12571 let mocked_response = lsp::SignatureHelp {
12572 signatures: vec![lsp::SignatureInformation {
12573 label: "fn sample(param1: u8, param2: u8)".to_string(),
12574 documentation: None,
12575 parameters: Some(vec![
12576 lsp::ParameterInformation {
12577 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12578 documentation: None,
12579 },
12580 lsp::ParameterInformation {
12581 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12582 documentation: None,
12583 },
12584 ]),
12585 active_parameter: None,
12586 }],
12587 active_signature: Some(0),
12588 active_parameter: Some(0),
12589 };
12590 handle_signature_help_request(&mut cx, mocked_response).await;
12591
12592 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12593 .await;
12594
12595 cx.editor(|editor, _, _| {
12596 let signature_help_state = editor.signature_help_state.popover().cloned();
12597 let signature = signature_help_state.unwrap();
12598 assert_eq!(
12599 signature.signatures[signature.current_signature].label,
12600 "fn sample(param1: u8, param2: u8)"
12601 );
12602 });
12603}
12604
12605#[gpui::test]
12606async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
12607 init_test(cx, |_| {});
12608
12609 cx.update(|cx| {
12610 cx.update_global::<SettingsStore, _>(|settings, cx| {
12611 settings.update_user_settings(cx, |settings| {
12612 settings.editor.auto_signature_help = Some(false);
12613 settings.editor.show_signature_help_after_edits = Some(false);
12614 });
12615 });
12616 });
12617
12618 let mut cx = EditorLspTestContext::new_rust(
12619 lsp::ServerCapabilities {
12620 signature_help_provider: Some(lsp::SignatureHelpOptions {
12621 ..Default::default()
12622 }),
12623 ..Default::default()
12624 },
12625 cx,
12626 )
12627 .await;
12628
12629 let language = Language::new(
12630 LanguageConfig {
12631 name: "Rust".into(),
12632 brackets: BracketPairConfig {
12633 pairs: vec![
12634 BracketPair {
12635 start: "{".to_string(),
12636 end: "}".to_string(),
12637 close: true,
12638 surround: true,
12639 newline: true,
12640 },
12641 BracketPair {
12642 start: "(".to_string(),
12643 end: ")".to_string(),
12644 close: true,
12645 surround: true,
12646 newline: true,
12647 },
12648 BracketPair {
12649 start: "/*".to_string(),
12650 end: " */".to_string(),
12651 close: true,
12652 surround: true,
12653 newline: true,
12654 },
12655 BracketPair {
12656 start: "[".to_string(),
12657 end: "]".to_string(),
12658 close: false,
12659 surround: false,
12660 newline: true,
12661 },
12662 BracketPair {
12663 start: "\"".to_string(),
12664 end: "\"".to_string(),
12665 close: true,
12666 surround: true,
12667 newline: false,
12668 },
12669 BracketPair {
12670 start: "<".to_string(),
12671 end: ">".to_string(),
12672 close: false,
12673 surround: true,
12674 newline: true,
12675 },
12676 ],
12677 ..Default::default()
12678 },
12679 autoclose_before: "})]".to_string(),
12680 ..Default::default()
12681 },
12682 Some(tree_sitter_rust::LANGUAGE.into()),
12683 );
12684 let language = Arc::new(language);
12685
12686 cx.language_registry().add(language.clone());
12687 cx.update_buffer(|buffer, cx| {
12688 buffer.set_language(Some(language), cx);
12689 });
12690
12691 // Ensure that signature_help is not called when no signature help is enabled.
12692 cx.set_state(
12693 &r#"
12694 fn main() {
12695 sampleˇ
12696 }
12697 "#
12698 .unindent(),
12699 );
12700 cx.update_editor(|editor, window, cx| {
12701 editor.handle_input("(", window, cx);
12702 });
12703 cx.assert_editor_state(
12704 &"
12705 fn main() {
12706 sample(ˇ)
12707 }
12708 "
12709 .unindent(),
12710 );
12711 cx.editor(|editor, _, _| {
12712 assert!(editor.signature_help_state.task().is_none());
12713 });
12714
12715 let mocked_response = lsp::SignatureHelp {
12716 signatures: vec![lsp::SignatureInformation {
12717 label: "fn sample(param1: u8, param2: u8)".to_string(),
12718 documentation: None,
12719 parameters: Some(vec![
12720 lsp::ParameterInformation {
12721 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12722 documentation: None,
12723 },
12724 lsp::ParameterInformation {
12725 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12726 documentation: None,
12727 },
12728 ]),
12729 active_parameter: None,
12730 }],
12731 active_signature: Some(0),
12732 active_parameter: Some(0),
12733 };
12734
12735 // Ensure that signature_help is called when enabled afte edits
12736 cx.update(|_, cx| {
12737 cx.update_global::<SettingsStore, _>(|settings, cx| {
12738 settings.update_user_settings(cx, |settings| {
12739 settings.editor.auto_signature_help = Some(false);
12740 settings.editor.show_signature_help_after_edits = Some(true);
12741 });
12742 });
12743 });
12744 cx.set_state(
12745 &r#"
12746 fn main() {
12747 sampleˇ
12748 }
12749 "#
12750 .unindent(),
12751 );
12752 cx.update_editor(|editor, window, cx| {
12753 editor.handle_input("(", window, cx);
12754 });
12755 cx.assert_editor_state(
12756 &"
12757 fn main() {
12758 sample(ˇ)
12759 }
12760 "
12761 .unindent(),
12762 );
12763 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12764 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12765 .await;
12766 cx.update_editor(|editor, _, _| {
12767 let signature_help_state = editor.signature_help_state.popover().cloned();
12768 assert!(signature_help_state.is_some());
12769 let signature = signature_help_state.unwrap();
12770 assert_eq!(
12771 signature.signatures[signature.current_signature].label,
12772 "fn sample(param1: u8, param2: u8)"
12773 );
12774 editor.signature_help_state = SignatureHelpState::default();
12775 });
12776
12777 // Ensure that signature_help is called when auto signature help override is enabled
12778 cx.update(|_, cx| {
12779 cx.update_global::<SettingsStore, _>(|settings, cx| {
12780 settings.update_user_settings(cx, |settings| {
12781 settings.editor.auto_signature_help = Some(true);
12782 settings.editor.show_signature_help_after_edits = Some(false);
12783 });
12784 });
12785 });
12786 cx.set_state(
12787 &r#"
12788 fn main() {
12789 sampleˇ
12790 }
12791 "#
12792 .unindent(),
12793 );
12794 cx.update_editor(|editor, window, cx| {
12795 editor.handle_input("(", window, cx);
12796 });
12797 cx.assert_editor_state(
12798 &"
12799 fn main() {
12800 sample(ˇ)
12801 }
12802 "
12803 .unindent(),
12804 );
12805 handle_signature_help_request(&mut cx, mocked_response).await;
12806 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12807 .await;
12808 cx.editor(|editor, _, _| {
12809 let signature_help_state = editor.signature_help_state.popover().cloned();
12810 assert!(signature_help_state.is_some());
12811 let signature = signature_help_state.unwrap();
12812 assert_eq!(
12813 signature.signatures[signature.current_signature].label,
12814 "fn sample(param1: u8, param2: u8)"
12815 );
12816 });
12817}
12818
12819#[gpui::test]
12820async fn test_signature_help(cx: &mut TestAppContext) {
12821 init_test(cx, |_| {});
12822 cx.update(|cx| {
12823 cx.update_global::<SettingsStore, _>(|settings, cx| {
12824 settings.update_user_settings(cx, |settings| {
12825 settings.editor.auto_signature_help = Some(true);
12826 });
12827 });
12828 });
12829
12830 let mut cx = EditorLspTestContext::new_rust(
12831 lsp::ServerCapabilities {
12832 signature_help_provider: Some(lsp::SignatureHelpOptions {
12833 ..Default::default()
12834 }),
12835 ..Default::default()
12836 },
12837 cx,
12838 )
12839 .await;
12840
12841 // A test that directly calls `show_signature_help`
12842 cx.update_editor(|editor, window, cx| {
12843 editor.show_signature_help(&ShowSignatureHelp, window, cx);
12844 });
12845
12846 let mocked_response = lsp::SignatureHelp {
12847 signatures: vec![lsp::SignatureInformation {
12848 label: "fn sample(param1: u8, param2: u8)".to_string(),
12849 documentation: None,
12850 parameters: Some(vec![
12851 lsp::ParameterInformation {
12852 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12853 documentation: None,
12854 },
12855 lsp::ParameterInformation {
12856 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12857 documentation: None,
12858 },
12859 ]),
12860 active_parameter: None,
12861 }],
12862 active_signature: Some(0),
12863 active_parameter: Some(0),
12864 };
12865 handle_signature_help_request(&mut cx, mocked_response).await;
12866
12867 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12868 .await;
12869
12870 cx.editor(|editor, _, _| {
12871 let signature_help_state = editor.signature_help_state.popover().cloned();
12872 assert!(signature_help_state.is_some());
12873 let signature = signature_help_state.unwrap();
12874 assert_eq!(
12875 signature.signatures[signature.current_signature].label,
12876 "fn sample(param1: u8, param2: u8)"
12877 );
12878 });
12879
12880 // When exiting outside from inside the brackets, `signature_help` is closed.
12881 cx.set_state(indoc! {"
12882 fn main() {
12883 sample(ˇ);
12884 }
12885
12886 fn sample(param1: u8, param2: u8) {}
12887 "});
12888
12889 cx.update_editor(|editor, window, cx| {
12890 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12891 s.select_ranges([0..0])
12892 });
12893 });
12894
12895 let mocked_response = lsp::SignatureHelp {
12896 signatures: Vec::new(),
12897 active_signature: None,
12898 active_parameter: None,
12899 };
12900 handle_signature_help_request(&mut cx, mocked_response).await;
12901
12902 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
12903 .await;
12904
12905 cx.editor(|editor, _, _| {
12906 assert!(!editor.signature_help_state.is_shown());
12907 });
12908
12909 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
12910 cx.set_state(indoc! {"
12911 fn main() {
12912 sample(ˇ);
12913 }
12914
12915 fn sample(param1: u8, param2: u8) {}
12916 "});
12917
12918 let mocked_response = lsp::SignatureHelp {
12919 signatures: vec![lsp::SignatureInformation {
12920 label: "fn sample(param1: u8, param2: u8)".to_string(),
12921 documentation: None,
12922 parameters: Some(vec![
12923 lsp::ParameterInformation {
12924 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12925 documentation: None,
12926 },
12927 lsp::ParameterInformation {
12928 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12929 documentation: None,
12930 },
12931 ]),
12932 active_parameter: None,
12933 }],
12934 active_signature: Some(0),
12935 active_parameter: Some(0),
12936 };
12937 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12938 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12939 .await;
12940 cx.editor(|editor, _, _| {
12941 assert!(editor.signature_help_state.is_shown());
12942 });
12943
12944 // Restore the popover with more parameter input
12945 cx.set_state(indoc! {"
12946 fn main() {
12947 sample(param1, param2ˇ);
12948 }
12949
12950 fn sample(param1: u8, param2: u8) {}
12951 "});
12952
12953 let mocked_response = lsp::SignatureHelp {
12954 signatures: vec![lsp::SignatureInformation {
12955 label: "fn sample(param1: u8, param2: u8)".to_string(),
12956 documentation: None,
12957 parameters: Some(vec![
12958 lsp::ParameterInformation {
12959 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12960 documentation: None,
12961 },
12962 lsp::ParameterInformation {
12963 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12964 documentation: None,
12965 },
12966 ]),
12967 active_parameter: None,
12968 }],
12969 active_signature: Some(0),
12970 active_parameter: Some(1),
12971 };
12972 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12973 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12974 .await;
12975
12976 // When selecting a range, the popover is gone.
12977 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
12978 cx.update_editor(|editor, window, cx| {
12979 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12980 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
12981 })
12982 });
12983 cx.assert_editor_state(indoc! {"
12984 fn main() {
12985 sample(param1, «ˇparam2»);
12986 }
12987
12988 fn sample(param1: u8, param2: u8) {}
12989 "});
12990 cx.editor(|editor, _, _| {
12991 assert!(!editor.signature_help_state.is_shown());
12992 });
12993
12994 // When unselecting again, the popover is back if within the brackets.
12995 cx.update_editor(|editor, window, cx| {
12996 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12997 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
12998 })
12999 });
13000 cx.assert_editor_state(indoc! {"
13001 fn main() {
13002 sample(param1, ˇparam2);
13003 }
13004
13005 fn sample(param1: u8, param2: u8) {}
13006 "});
13007 handle_signature_help_request(&mut cx, mocked_response).await;
13008 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13009 .await;
13010 cx.editor(|editor, _, _| {
13011 assert!(editor.signature_help_state.is_shown());
13012 });
13013
13014 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
13015 cx.update_editor(|editor, window, cx| {
13016 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13017 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
13018 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13019 })
13020 });
13021 cx.assert_editor_state(indoc! {"
13022 fn main() {
13023 sample(param1, ˇparam2);
13024 }
13025
13026 fn sample(param1: u8, param2: u8) {}
13027 "});
13028
13029 let mocked_response = lsp::SignatureHelp {
13030 signatures: vec![lsp::SignatureInformation {
13031 label: "fn sample(param1: u8, param2: u8)".to_string(),
13032 documentation: None,
13033 parameters: Some(vec![
13034 lsp::ParameterInformation {
13035 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13036 documentation: None,
13037 },
13038 lsp::ParameterInformation {
13039 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13040 documentation: None,
13041 },
13042 ]),
13043 active_parameter: None,
13044 }],
13045 active_signature: Some(0),
13046 active_parameter: Some(1),
13047 };
13048 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13049 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13050 .await;
13051 cx.update_editor(|editor, _, cx| {
13052 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
13053 });
13054 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13055 .await;
13056 cx.update_editor(|editor, window, cx| {
13057 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13058 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13059 })
13060 });
13061 cx.assert_editor_state(indoc! {"
13062 fn main() {
13063 sample(param1, «ˇparam2»);
13064 }
13065
13066 fn sample(param1: u8, param2: u8) {}
13067 "});
13068 cx.update_editor(|editor, window, cx| {
13069 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13070 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13071 })
13072 });
13073 cx.assert_editor_state(indoc! {"
13074 fn main() {
13075 sample(param1, ˇparam2);
13076 }
13077
13078 fn sample(param1: u8, param2: u8) {}
13079 "});
13080 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
13081 .await;
13082}
13083
13084#[gpui::test]
13085async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
13086 init_test(cx, |_| {});
13087
13088 let mut cx = EditorLspTestContext::new_rust(
13089 lsp::ServerCapabilities {
13090 signature_help_provider: Some(lsp::SignatureHelpOptions {
13091 ..Default::default()
13092 }),
13093 ..Default::default()
13094 },
13095 cx,
13096 )
13097 .await;
13098
13099 cx.set_state(indoc! {"
13100 fn main() {
13101 overloadedˇ
13102 }
13103 "});
13104
13105 cx.update_editor(|editor, window, cx| {
13106 editor.handle_input("(", window, cx);
13107 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13108 });
13109
13110 // Mock response with 3 signatures
13111 let mocked_response = lsp::SignatureHelp {
13112 signatures: vec![
13113 lsp::SignatureInformation {
13114 label: "fn overloaded(x: i32)".to_string(),
13115 documentation: None,
13116 parameters: Some(vec![lsp::ParameterInformation {
13117 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13118 documentation: None,
13119 }]),
13120 active_parameter: None,
13121 },
13122 lsp::SignatureInformation {
13123 label: "fn overloaded(x: i32, y: i32)".to_string(),
13124 documentation: None,
13125 parameters: Some(vec![
13126 lsp::ParameterInformation {
13127 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13128 documentation: None,
13129 },
13130 lsp::ParameterInformation {
13131 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13132 documentation: None,
13133 },
13134 ]),
13135 active_parameter: None,
13136 },
13137 lsp::SignatureInformation {
13138 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
13139 documentation: None,
13140 parameters: Some(vec![
13141 lsp::ParameterInformation {
13142 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13143 documentation: None,
13144 },
13145 lsp::ParameterInformation {
13146 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13147 documentation: None,
13148 },
13149 lsp::ParameterInformation {
13150 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
13151 documentation: None,
13152 },
13153 ]),
13154 active_parameter: None,
13155 },
13156 ],
13157 active_signature: Some(1),
13158 active_parameter: Some(0),
13159 };
13160 handle_signature_help_request(&mut cx, mocked_response).await;
13161
13162 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13163 .await;
13164
13165 // Verify we have multiple signatures and the right one is selected
13166 cx.editor(|editor, _, _| {
13167 let popover = editor.signature_help_state.popover().cloned().unwrap();
13168 assert_eq!(popover.signatures.len(), 3);
13169 // active_signature was 1, so that should be the current
13170 assert_eq!(popover.current_signature, 1);
13171 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
13172 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
13173 assert_eq!(
13174 popover.signatures[2].label,
13175 "fn overloaded(x: i32, y: i32, z: i32)"
13176 );
13177 });
13178
13179 // Test navigation functionality
13180 cx.update_editor(|editor, window, cx| {
13181 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13182 });
13183
13184 cx.editor(|editor, _, _| {
13185 let popover = editor.signature_help_state.popover().cloned().unwrap();
13186 assert_eq!(popover.current_signature, 2);
13187 });
13188
13189 // Test wrap around
13190 cx.update_editor(|editor, window, cx| {
13191 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13192 });
13193
13194 cx.editor(|editor, _, _| {
13195 let popover = editor.signature_help_state.popover().cloned().unwrap();
13196 assert_eq!(popover.current_signature, 0);
13197 });
13198
13199 // Test previous navigation
13200 cx.update_editor(|editor, window, cx| {
13201 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
13202 });
13203
13204 cx.editor(|editor, _, _| {
13205 let popover = editor.signature_help_state.popover().cloned().unwrap();
13206 assert_eq!(popover.current_signature, 2);
13207 });
13208}
13209
13210#[gpui::test]
13211async fn test_completion_mode(cx: &mut TestAppContext) {
13212 init_test(cx, |_| {});
13213 let mut cx = EditorLspTestContext::new_rust(
13214 lsp::ServerCapabilities {
13215 completion_provider: Some(lsp::CompletionOptions {
13216 resolve_provider: Some(true),
13217 ..Default::default()
13218 }),
13219 ..Default::default()
13220 },
13221 cx,
13222 )
13223 .await;
13224
13225 struct Run {
13226 run_description: &'static str,
13227 initial_state: String,
13228 buffer_marked_text: String,
13229 completion_label: &'static str,
13230 completion_text: &'static str,
13231 expected_with_insert_mode: String,
13232 expected_with_replace_mode: String,
13233 expected_with_replace_subsequence_mode: String,
13234 expected_with_replace_suffix_mode: String,
13235 }
13236
13237 let runs = [
13238 Run {
13239 run_description: "Start of word matches completion text",
13240 initial_state: "before ediˇ after".into(),
13241 buffer_marked_text: "before <edi|> after".into(),
13242 completion_label: "editor",
13243 completion_text: "editor",
13244 expected_with_insert_mode: "before editorˇ after".into(),
13245 expected_with_replace_mode: "before editorˇ after".into(),
13246 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13247 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13248 },
13249 Run {
13250 run_description: "Accept same text at the middle of the word",
13251 initial_state: "before ediˇtor after".into(),
13252 buffer_marked_text: "before <edi|tor> after".into(),
13253 completion_label: "editor",
13254 completion_text: "editor",
13255 expected_with_insert_mode: "before editorˇtor after".into(),
13256 expected_with_replace_mode: "before editorˇ after".into(),
13257 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13258 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13259 },
13260 Run {
13261 run_description: "End of word matches completion text -- cursor at end",
13262 initial_state: "before torˇ after".into(),
13263 buffer_marked_text: "before <tor|> after".into(),
13264 completion_label: "editor",
13265 completion_text: "editor",
13266 expected_with_insert_mode: "before editorˇ after".into(),
13267 expected_with_replace_mode: "before editorˇ after".into(),
13268 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13269 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13270 },
13271 Run {
13272 run_description: "End of word matches completion text -- cursor at start",
13273 initial_state: "before ˇtor after".into(),
13274 buffer_marked_text: "before <|tor> after".into(),
13275 completion_label: "editor",
13276 completion_text: "editor",
13277 expected_with_insert_mode: "before editorˇtor after".into(),
13278 expected_with_replace_mode: "before editorˇ after".into(),
13279 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13280 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13281 },
13282 Run {
13283 run_description: "Prepend text containing whitespace",
13284 initial_state: "pˇfield: bool".into(),
13285 buffer_marked_text: "<p|field>: bool".into(),
13286 completion_label: "pub ",
13287 completion_text: "pub ",
13288 expected_with_insert_mode: "pub ˇfield: bool".into(),
13289 expected_with_replace_mode: "pub ˇ: bool".into(),
13290 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
13291 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
13292 },
13293 Run {
13294 run_description: "Add element to start of list",
13295 initial_state: "[element_ˇelement_2]".into(),
13296 buffer_marked_text: "[<element_|element_2>]".into(),
13297 completion_label: "element_1",
13298 completion_text: "element_1",
13299 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
13300 expected_with_replace_mode: "[element_1ˇ]".into(),
13301 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
13302 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
13303 },
13304 Run {
13305 run_description: "Add element to start of list -- first and second elements are equal",
13306 initial_state: "[elˇelement]".into(),
13307 buffer_marked_text: "[<el|element>]".into(),
13308 completion_label: "element",
13309 completion_text: "element",
13310 expected_with_insert_mode: "[elementˇelement]".into(),
13311 expected_with_replace_mode: "[elementˇ]".into(),
13312 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
13313 expected_with_replace_suffix_mode: "[elementˇ]".into(),
13314 },
13315 Run {
13316 run_description: "Ends with matching suffix",
13317 initial_state: "SubˇError".into(),
13318 buffer_marked_text: "<Sub|Error>".into(),
13319 completion_label: "SubscriptionError",
13320 completion_text: "SubscriptionError",
13321 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
13322 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13323 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13324 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
13325 },
13326 Run {
13327 run_description: "Suffix is a subsequence -- contiguous",
13328 initial_state: "SubˇErr".into(),
13329 buffer_marked_text: "<Sub|Err>".into(),
13330 completion_label: "SubscriptionError",
13331 completion_text: "SubscriptionError",
13332 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
13333 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13334 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13335 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
13336 },
13337 Run {
13338 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
13339 initial_state: "Suˇscrirr".into(),
13340 buffer_marked_text: "<Su|scrirr>".into(),
13341 completion_label: "SubscriptionError",
13342 completion_text: "SubscriptionError",
13343 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
13344 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13345 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13346 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
13347 },
13348 Run {
13349 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
13350 initial_state: "foo(indˇix)".into(),
13351 buffer_marked_text: "foo(<ind|ix>)".into(),
13352 completion_label: "node_index",
13353 completion_text: "node_index",
13354 expected_with_insert_mode: "foo(node_indexˇix)".into(),
13355 expected_with_replace_mode: "foo(node_indexˇ)".into(),
13356 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
13357 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
13358 },
13359 Run {
13360 run_description: "Replace range ends before cursor - should extend to cursor",
13361 initial_state: "before editˇo after".into(),
13362 buffer_marked_text: "before <{ed}>it|o after".into(),
13363 completion_label: "editor",
13364 completion_text: "editor",
13365 expected_with_insert_mode: "before editorˇo after".into(),
13366 expected_with_replace_mode: "before editorˇo after".into(),
13367 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
13368 expected_with_replace_suffix_mode: "before editorˇo after".into(),
13369 },
13370 Run {
13371 run_description: "Uses label for suffix matching",
13372 initial_state: "before ediˇtor after".into(),
13373 buffer_marked_text: "before <edi|tor> after".into(),
13374 completion_label: "editor",
13375 completion_text: "editor()",
13376 expected_with_insert_mode: "before editor()ˇtor after".into(),
13377 expected_with_replace_mode: "before editor()ˇ after".into(),
13378 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
13379 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
13380 },
13381 Run {
13382 run_description: "Case insensitive subsequence and suffix matching",
13383 initial_state: "before EDiˇtoR after".into(),
13384 buffer_marked_text: "before <EDi|toR> after".into(),
13385 completion_label: "editor",
13386 completion_text: "editor",
13387 expected_with_insert_mode: "before editorˇtoR after".into(),
13388 expected_with_replace_mode: "before editorˇ after".into(),
13389 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13390 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13391 },
13392 ];
13393
13394 for run in runs {
13395 let run_variations = [
13396 (LspInsertMode::Insert, run.expected_with_insert_mode),
13397 (LspInsertMode::Replace, run.expected_with_replace_mode),
13398 (
13399 LspInsertMode::ReplaceSubsequence,
13400 run.expected_with_replace_subsequence_mode,
13401 ),
13402 (
13403 LspInsertMode::ReplaceSuffix,
13404 run.expected_with_replace_suffix_mode,
13405 ),
13406 ];
13407
13408 for (lsp_insert_mode, expected_text) in run_variations {
13409 eprintln!(
13410 "run = {:?}, mode = {lsp_insert_mode:.?}",
13411 run.run_description,
13412 );
13413
13414 update_test_language_settings(&mut cx, |settings| {
13415 settings.defaults.completions = Some(CompletionSettingsContent {
13416 lsp_insert_mode: Some(lsp_insert_mode),
13417 words: Some(WordsCompletionMode::Disabled),
13418 words_min_length: Some(0),
13419 ..Default::default()
13420 });
13421 });
13422
13423 cx.set_state(&run.initial_state);
13424 cx.update_editor(|editor, window, cx| {
13425 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13426 });
13427
13428 let counter = Arc::new(AtomicUsize::new(0));
13429 handle_completion_request_with_insert_and_replace(
13430 &mut cx,
13431 &run.buffer_marked_text,
13432 vec![(run.completion_label, run.completion_text)],
13433 counter.clone(),
13434 )
13435 .await;
13436 cx.condition(|editor, _| editor.context_menu_visible())
13437 .await;
13438 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13439
13440 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13441 editor
13442 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13443 .unwrap()
13444 });
13445 cx.assert_editor_state(&expected_text);
13446 handle_resolve_completion_request(&mut cx, None).await;
13447 apply_additional_edits.await.unwrap();
13448 }
13449 }
13450}
13451
13452#[gpui::test]
13453async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
13454 init_test(cx, |_| {});
13455 let mut cx = EditorLspTestContext::new_rust(
13456 lsp::ServerCapabilities {
13457 completion_provider: Some(lsp::CompletionOptions {
13458 resolve_provider: Some(true),
13459 ..Default::default()
13460 }),
13461 ..Default::default()
13462 },
13463 cx,
13464 )
13465 .await;
13466
13467 let initial_state = "SubˇError";
13468 let buffer_marked_text = "<Sub|Error>";
13469 let completion_text = "SubscriptionError";
13470 let expected_with_insert_mode = "SubscriptionErrorˇError";
13471 let expected_with_replace_mode = "SubscriptionErrorˇ";
13472
13473 update_test_language_settings(&mut cx, |settings| {
13474 settings.defaults.completions = Some(CompletionSettingsContent {
13475 words: Some(WordsCompletionMode::Disabled),
13476 words_min_length: Some(0),
13477 // set the opposite here to ensure that the action is overriding the default behavior
13478 lsp_insert_mode: Some(LspInsertMode::Insert),
13479 ..Default::default()
13480 });
13481 });
13482
13483 cx.set_state(initial_state);
13484 cx.update_editor(|editor, window, cx| {
13485 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13486 });
13487
13488 let counter = Arc::new(AtomicUsize::new(0));
13489 handle_completion_request_with_insert_and_replace(
13490 &mut cx,
13491 buffer_marked_text,
13492 vec![(completion_text, completion_text)],
13493 counter.clone(),
13494 )
13495 .await;
13496 cx.condition(|editor, _| editor.context_menu_visible())
13497 .await;
13498 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13499
13500 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13501 editor
13502 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13503 .unwrap()
13504 });
13505 cx.assert_editor_state(expected_with_replace_mode);
13506 handle_resolve_completion_request(&mut cx, None).await;
13507 apply_additional_edits.await.unwrap();
13508
13509 update_test_language_settings(&mut cx, |settings| {
13510 settings.defaults.completions = Some(CompletionSettingsContent {
13511 words: Some(WordsCompletionMode::Disabled),
13512 words_min_length: Some(0),
13513 // set the opposite here to ensure that the action is overriding the default behavior
13514 lsp_insert_mode: Some(LspInsertMode::Replace),
13515 ..Default::default()
13516 });
13517 });
13518
13519 cx.set_state(initial_state);
13520 cx.update_editor(|editor, window, cx| {
13521 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13522 });
13523 handle_completion_request_with_insert_and_replace(
13524 &mut cx,
13525 buffer_marked_text,
13526 vec![(completion_text, completion_text)],
13527 counter.clone(),
13528 )
13529 .await;
13530 cx.condition(|editor, _| editor.context_menu_visible())
13531 .await;
13532 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
13533
13534 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13535 editor
13536 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
13537 .unwrap()
13538 });
13539 cx.assert_editor_state(expected_with_insert_mode);
13540 handle_resolve_completion_request(&mut cx, None).await;
13541 apply_additional_edits.await.unwrap();
13542}
13543
13544#[gpui::test]
13545async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
13546 init_test(cx, |_| {});
13547 let mut cx = EditorLspTestContext::new_rust(
13548 lsp::ServerCapabilities {
13549 completion_provider: Some(lsp::CompletionOptions {
13550 resolve_provider: Some(true),
13551 ..Default::default()
13552 }),
13553 ..Default::default()
13554 },
13555 cx,
13556 )
13557 .await;
13558
13559 // scenario: surrounding text matches completion text
13560 let completion_text = "to_offset";
13561 let initial_state = indoc! {"
13562 1. buf.to_offˇsuffix
13563 2. buf.to_offˇsuf
13564 3. buf.to_offˇfix
13565 4. buf.to_offˇ
13566 5. into_offˇensive
13567 6. ˇsuffix
13568 7. let ˇ //
13569 8. aaˇzz
13570 9. buf.to_off«zzzzzˇ»suffix
13571 10. buf.«ˇzzzzz»suffix
13572 11. to_off«ˇzzzzz»
13573
13574 buf.to_offˇsuffix // newest cursor
13575 "};
13576 let completion_marked_buffer = indoc! {"
13577 1. buf.to_offsuffix
13578 2. buf.to_offsuf
13579 3. buf.to_offfix
13580 4. buf.to_off
13581 5. into_offensive
13582 6. suffix
13583 7. let //
13584 8. aazz
13585 9. buf.to_offzzzzzsuffix
13586 10. buf.zzzzzsuffix
13587 11. to_offzzzzz
13588
13589 buf.<to_off|suffix> // newest cursor
13590 "};
13591 let expected = indoc! {"
13592 1. buf.to_offsetˇ
13593 2. buf.to_offsetˇsuf
13594 3. buf.to_offsetˇfix
13595 4. buf.to_offsetˇ
13596 5. into_offsetˇensive
13597 6. to_offsetˇsuffix
13598 7. let to_offsetˇ //
13599 8. aato_offsetˇzz
13600 9. buf.to_offsetˇ
13601 10. buf.to_offsetˇsuffix
13602 11. to_offsetˇ
13603
13604 buf.to_offsetˇ // newest cursor
13605 "};
13606 cx.set_state(initial_state);
13607 cx.update_editor(|editor, window, cx| {
13608 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13609 });
13610 handle_completion_request_with_insert_and_replace(
13611 &mut cx,
13612 completion_marked_buffer,
13613 vec![(completion_text, completion_text)],
13614 Arc::new(AtomicUsize::new(0)),
13615 )
13616 .await;
13617 cx.condition(|editor, _| editor.context_menu_visible())
13618 .await;
13619 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13620 editor
13621 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13622 .unwrap()
13623 });
13624 cx.assert_editor_state(expected);
13625 handle_resolve_completion_request(&mut cx, None).await;
13626 apply_additional_edits.await.unwrap();
13627
13628 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
13629 let completion_text = "foo_and_bar";
13630 let initial_state = indoc! {"
13631 1. ooanbˇ
13632 2. zooanbˇ
13633 3. ooanbˇz
13634 4. zooanbˇz
13635 5. ooanˇ
13636 6. oanbˇ
13637
13638 ooanbˇ
13639 "};
13640 let completion_marked_buffer = indoc! {"
13641 1. ooanb
13642 2. zooanb
13643 3. ooanbz
13644 4. zooanbz
13645 5. ooan
13646 6. oanb
13647
13648 <ooanb|>
13649 "};
13650 let expected = indoc! {"
13651 1. foo_and_barˇ
13652 2. zfoo_and_barˇ
13653 3. foo_and_barˇz
13654 4. zfoo_and_barˇz
13655 5. ooanfoo_and_barˇ
13656 6. oanbfoo_and_barˇ
13657
13658 foo_and_barˇ
13659 "};
13660 cx.set_state(initial_state);
13661 cx.update_editor(|editor, window, cx| {
13662 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13663 });
13664 handle_completion_request_with_insert_and_replace(
13665 &mut cx,
13666 completion_marked_buffer,
13667 vec![(completion_text, completion_text)],
13668 Arc::new(AtomicUsize::new(0)),
13669 )
13670 .await;
13671 cx.condition(|editor, _| editor.context_menu_visible())
13672 .await;
13673 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13674 editor
13675 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13676 .unwrap()
13677 });
13678 cx.assert_editor_state(expected);
13679 handle_resolve_completion_request(&mut cx, None).await;
13680 apply_additional_edits.await.unwrap();
13681
13682 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
13683 // (expects the same as if it was inserted at the end)
13684 let completion_text = "foo_and_bar";
13685 let initial_state = indoc! {"
13686 1. ooˇanb
13687 2. zooˇanb
13688 3. ooˇanbz
13689 4. zooˇanbz
13690
13691 ooˇanb
13692 "};
13693 let completion_marked_buffer = indoc! {"
13694 1. ooanb
13695 2. zooanb
13696 3. ooanbz
13697 4. zooanbz
13698
13699 <oo|anb>
13700 "};
13701 let expected = indoc! {"
13702 1. foo_and_barˇ
13703 2. zfoo_and_barˇ
13704 3. foo_and_barˇz
13705 4. zfoo_and_barˇz
13706
13707 foo_and_barˇ
13708 "};
13709 cx.set_state(initial_state);
13710 cx.update_editor(|editor, window, cx| {
13711 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13712 });
13713 handle_completion_request_with_insert_and_replace(
13714 &mut cx,
13715 completion_marked_buffer,
13716 vec![(completion_text, completion_text)],
13717 Arc::new(AtomicUsize::new(0)),
13718 )
13719 .await;
13720 cx.condition(|editor, _| editor.context_menu_visible())
13721 .await;
13722 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13723 editor
13724 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13725 .unwrap()
13726 });
13727 cx.assert_editor_state(expected);
13728 handle_resolve_completion_request(&mut cx, None).await;
13729 apply_additional_edits.await.unwrap();
13730}
13731
13732// This used to crash
13733#[gpui::test]
13734async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
13735 init_test(cx, |_| {});
13736
13737 let buffer_text = indoc! {"
13738 fn main() {
13739 10.satu;
13740
13741 //
13742 // separate cursors so they open in different excerpts (manually reproducible)
13743 //
13744
13745 10.satu20;
13746 }
13747 "};
13748 let multibuffer_text_with_selections = indoc! {"
13749 fn main() {
13750 10.satuˇ;
13751
13752 //
13753
13754 //
13755
13756 10.satuˇ20;
13757 }
13758 "};
13759 let expected_multibuffer = indoc! {"
13760 fn main() {
13761 10.saturating_sub()ˇ;
13762
13763 //
13764
13765 //
13766
13767 10.saturating_sub()ˇ;
13768 }
13769 "};
13770
13771 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
13772 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
13773
13774 let fs = FakeFs::new(cx.executor());
13775 fs.insert_tree(
13776 path!("/a"),
13777 json!({
13778 "main.rs": buffer_text,
13779 }),
13780 )
13781 .await;
13782
13783 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13784 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13785 language_registry.add(rust_lang());
13786 let mut fake_servers = language_registry.register_fake_lsp(
13787 "Rust",
13788 FakeLspAdapter {
13789 capabilities: lsp::ServerCapabilities {
13790 completion_provider: Some(lsp::CompletionOptions {
13791 resolve_provider: None,
13792 ..lsp::CompletionOptions::default()
13793 }),
13794 ..lsp::ServerCapabilities::default()
13795 },
13796 ..FakeLspAdapter::default()
13797 },
13798 );
13799 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13800 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13801 let buffer = project
13802 .update(cx, |project, cx| {
13803 project.open_local_buffer(path!("/a/main.rs"), cx)
13804 })
13805 .await
13806 .unwrap();
13807
13808 let multi_buffer = cx.new(|cx| {
13809 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
13810 multi_buffer.push_excerpts(
13811 buffer.clone(),
13812 [ExcerptRange::new(0..first_excerpt_end)],
13813 cx,
13814 );
13815 multi_buffer.push_excerpts(
13816 buffer.clone(),
13817 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
13818 cx,
13819 );
13820 multi_buffer
13821 });
13822
13823 let editor = workspace
13824 .update(cx, |_, window, cx| {
13825 cx.new(|cx| {
13826 Editor::new(
13827 EditorMode::Full {
13828 scale_ui_elements_with_buffer_font_size: false,
13829 show_active_line_background: false,
13830 sized_by_content: false,
13831 },
13832 multi_buffer.clone(),
13833 Some(project.clone()),
13834 window,
13835 cx,
13836 )
13837 })
13838 })
13839 .unwrap();
13840
13841 let pane = workspace
13842 .update(cx, |workspace, _, _| workspace.active_pane().clone())
13843 .unwrap();
13844 pane.update_in(cx, |pane, window, cx| {
13845 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
13846 });
13847
13848 let fake_server = fake_servers.next().await.unwrap();
13849
13850 editor.update_in(cx, |editor, window, cx| {
13851 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13852 s.select_ranges([
13853 Point::new(1, 11)..Point::new(1, 11),
13854 Point::new(7, 11)..Point::new(7, 11),
13855 ])
13856 });
13857
13858 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
13859 });
13860
13861 editor.update_in(cx, |editor, window, cx| {
13862 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13863 });
13864
13865 fake_server
13866 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13867 let completion_item = lsp::CompletionItem {
13868 label: "saturating_sub()".into(),
13869 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
13870 lsp::InsertReplaceEdit {
13871 new_text: "saturating_sub()".to_owned(),
13872 insert: lsp::Range::new(
13873 lsp::Position::new(7, 7),
13874 lsp::Position::new(7, 11),
13875 ),
13876 replace: lsp::Range::new(
13877 lsp::Position::new(7, 7),
13878 lsp::Position::new(7, 13),
13879 ),
13880 },
13881 )),
13882 ..lsp::CompletionItem::default()
13883 };
13884
13885 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
13886 })
13887 .next()
13888 .await
13889 .unwrap();
13890
13891 cx.condition(&editor, |editor, _| editor.context_menu_visible())
13892 .await;
13893
13894 editor
13895 .update_in(cx, |editor, window, cx| {
13896 editor
13897 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13898 .unwrap()
13899 })
13900 .await
13901 .unwrap();
13902
13903 editor.update(cx, |editor, cx| {
13904 assert_text_with_selections(editor, expected_multibuffer, cx);
13905 })
13906}
13907
13908#[gpui::test]
13909async fn test_completion(cx: &mut TestAppContext) {
13910 init_test(cx, |_| {});
13911
13912 let mut cx = EditorLspTestContext::new_rust(
13913 lsp::ServerCapabilities {
13914 completion_provider: Some(lsp::CompletionOptions {
13915 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
13916 resolve_provider: Some(true),
13917 ..Default::default()
13918 }),
13919 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
13920 ..Default::default()
13921 },
13922 cx,
13923 )
13924 .await;
13925 let counter = Arc::new(AtomicUsize::new(0));
13926
13927 cx.set_state(indoc! {"
13928 oneˇ
13929 two
13930 three
13931 "});
13932 cx.simulate_keystroke(".");
13933 handle_completion_request(
13934 indoc! {"
13935 one.|<>
13936 two
13937 three
13938 "},
13939 vec!["first_completion", "second_completion"],
13940 true,
13941 counter.clone(),
13942 &mut cx,
13943 )
13944 .await;
13945 cx.condition(|editor, _| editor.context_menu_visible())
13946 .await;
13947 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13948
13949 let _handler = handle_signature_help_request(
13950 &mut cx,
13951 lsp::SignatureHelp {
13952 signatures: vec![lsp::SignatureInformation {
13953 label: "test signature".to_string(),
13954 documentation: None,
13955 parameters: Some(vec![lsp::ParameterInformation {
13956 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
13957 documentation: None,
13958 }]),
13959 active_parameter: None,
13960 }],
13961 active_signature: None,
13962 active_parameter: None,
13963 },
13964 );
13965 cx.update_editor(|editor, window, cx| {
13966 assert!(
13967 !editor.signature_help_state.is_shown(),
13968 "No signature help was called for"
13969 );
13970 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13971 });
13972 cx.run_until_parked();
13973 cx.update_editor(|editor, _, _| {
13974 assert!(
13975 !editor.signature_help_state.is_shown(),
13976 "No signature help should be shown when completions menu is open"
13977 );
13978 });
13979
13980 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13981 editor.context_menu_next(&Default::default(), window, cx);
13982 editor
13983 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13984 .unwrap()
13985 });
13986 cx.assert_editor_state(indoc! {"
13987 one.second_completionˇ
13988 two
13989 three
13990 "});
13991
13992 handle_resolve_completion_request(
13993 &mut cx,
13994 Some(vec![
13995 (
13996 //This overlaps with the primary completion edit which is
13997 //misbehavior from the LSP spec, test that we filter it out
13998 indoc! {"
13999 one.second_ˇcompletion
14000 two
14001 threeˇ
14002 "},
14003 "overlapping additional edit",
14004 ),
14005 (
14006 indoc! {"
14007 one.second_completion
14008 two
14009 threeˇ
14010 "},
14011 "\nadditional edit",
14012 ),
14013 ]),
14014 )
14015 .await;
14016 apply_additional_edits.await.unwrap();
14017 cx.assert_editor_state(indoc! {"
14018 one.second_completionˇ
14019 two
14020 three
14021 additional edit
14022 "});
14023
14024 cx.set_state(indoc! {"
14025 one.second_completion
14026 twoˇ
14027 threeˇ
14028 additional edit
14029 "});
14030 cx.simulate_keystroke(" ");
14031 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14032 cx.simulate_keystroke("s");
14033 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14034
14035 cx.assert_editor_state(indoc! {"
14036 one.second_completion
14037 two sˇ
14038 three sˇ
14039 additional edit
14040 "});
14041 handle_completion_request(
14042 indoc! {"
14043 one.second_completion
14044 two s
14045 three <s|>
14046 additional edit
14047 "},
14048 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14049 true,
14050 counter.clone(),
14051 &mut cx,
14052 )
14053 .await;
14054 cx.condition(|editor, _| editor.context_menu_visible())
14055 .await;
14056 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14057
14058 cx.simulate_keystroke("i");
14059
14060 handle_completion_request(
14061 indoc! {"
14062 one.second_completion
14063 two si
14064 three <si|>
14065 additional edit
14066 "},
14067 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14068 true,
14069 counter.clone(),
14070 &mut cx,
14071 )
14072 .await;
14073 cx.condition(|editor, _| editor.context_menu_visible())
14074 .await;
14075 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14076
14077 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14078 editor
14079 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14080 .unwrap()
14081 });
14082 cx.assert_editor_state(indoc! {"
14083 one.second_completion
14084 two sixth_completionˇ
14085 three sixth_completionˇ
14086 additional edit
14087 "});
14088
14089 apply_additional_edits.await.unwrap();
14090
14091 update_test_language_settings(&mut cx, |settings| {
14092 settings.defaults.show_completions_on_input = Some(false);
14093 });
14094 cx.set_state("editorˇ");
14095 cx.simulate_keystroke(".");
14096 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14097 cx.simulate_keystrokes("c l o");
14098 cx.assert_editor_state("editor.cloˇ");
14099 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14100 cx.update_editor(|editor, window, cx| {
14101 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14102 });
14103 handle_completion_request(
14104 "editor.<clo|>",
14105 vec!["close", "clobber"],
14106 true,
14107 counter.clone(),
14108 &mut cx,
14109 )
14110 .await;
14111 cx.condition(|editor, _| editor.context_menu_visible())
14112 .await;
14113 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
14114
14115 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14116 editor
14117 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14118 .unwrap()
14119 });
14120 cx.assert_editor_state("editor.clobberˇ");
14121 handle_resolve_completion_request(&mut cx, None).await;
14122 apply_additional_edits.await.unwrap();
14123}
14124
14125#[gpui::test]
14126async fn test_completion_reuse(cx: &mut TestAppContext) {
14127 init_test(cx, |_| {});
14128
14129 let mut cx = EditorLspTestContext::new_rust(
14130 lsp::ServerCapabilities {
14131 completion_provider: Some(lsp::CompletionOptions {
14132 trigger_characters: Some(vec![".".to_string()]),
14133 ..Default::default()
14134 }),
14135 ..Default::default()
14136 },
14137 cx,
14138 )
14139 .await;
14140
14141 let counter = Arc::new(AtomicUsize::new(0));
14142 cx.set_state("objˇ");
14143 cx.simulate_keystroke(".");
14144
14145 // Initial completion request returns complete results
14146 let is_incomplete = false;
14147 handle_completion_request(
14148 "obj.|<>",
14149 vec!["a", "ab", "abc"],
14150 is_incomplete,
14151 counter.clone(),
14152 &mut cx,
14153 )
14154 .await;
14155 cx.run_until_parked();
14156 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14157 cx.assert_editor_state("obj.ˇ");
14158 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14159
14160 // Type "a" - filters existing completions
14161 cx.simulate_keystroke("a");
14162 cx.run_until_parked();
14163 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14164 cx.assert_editor_state("obj.aˇ");
14165 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14166
14167 // Type "b" - filters existing completions
14168 cx.simulate_keystroke("b");
14169 cx.run_until_parked();
14170 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14171 cx.assert_editor_state("obj.abˇ");
14172 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14173
14174 // Type "c" - filters existing completions
14175 cx.simulate_keystroke("c");
14176 cx.run_until_parked();
14177 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14178 cx.assert_editor_state("obj.abcˇ");
14179 check_displayed_completions(vec!["abc"], &mut cx);
14180
14181 // Backspace to delete "c" - filters existing completions
14182 cx.update_editor(|editor, window, cx| {
14183 editor.backspace(&Backspace, window, cx);
14184 });
14185 cx.run_until_parked();
14186 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14187 cx.assert_editor_state("obj.abˇ");
14188 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14189
14190 // Moving cursor to the left dismisses menu.
14191 cx.update_editor(|editor, window, cx| {
14192 editor.move_left(&MoveLeft, window, cx);
14193 });
14194 cx.run_until_parked();
14195 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14196 cx.assert_editor_state("obj.aˇb");
14197 cx.update_editor(|editor, _, _| {
14198 assert_eq!(editor.context_menu_visible(), false);
14199 });
14200
14201 // Type "b" - new request
14202 cx.simulate_keystroke("b");
14203 let is_incomplete = false;
14204 handle_completion_request(
14205 "obj.<ab|>a",
14206 vec!["ab", "abc"],
14207 is_incomplete,
14208 counter.clone(),
14209 &mut cx,
14210 )
14211 .await;
14212 cx.run_until_parked();
14213 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14214 cx.assert_editor_state("obj.abˇb");
14215 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14216
14217 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
14218 cx.update_editor(|editor, window, cx| {
14219 editor.backspace(&Backspace, window, cx);
14220 });
14221 let is_incomplete = false;
14222 handle_completion_request(
14223 "obj.<a|>b",
14224 vec!["a", "ab", "abc"],
14225 is_incomplete,
14226 counter.clone(),
14227 &mut cx,
14228 )
14229 .await;
14230 cx.run_until_parked();
14231 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14232 cx.assert_editor_state("obj.aˇb");
14233 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14234
14235 // Backspace to delete "a" - dismisses menu.
14236 cx.update_editor(|editor, window, cx| {
14237 editor.backspace(&Backspace, window, cx);
14238 });
14239 cx.run_until_parked();
14240 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14241 cx.assert_editor_state("obj.ˇb");
14242 cx.update_editor(|editor, _, _| {
14243 assert_eq!(editor.context_menu_visible(), false);
14244 });
14245}
14246
14247#[gpui::test]
14248async fn test_word_completion(cx: &mut TestAppContext) {
14249 let lsp_fetch_timeout_ms = 10;
14250 init_test(cx, |language_settings| {
14251 language_settings.defaults.completions = Some(CompletionSettingsContent {
14252 words_min_length: Some(0),
14253 lsp_fetch_timeout_ms: Some(10),
14254 lsp_insert_mode: Some(LspInsertMode::Insert),
14255 ..Default::default()
14256 });
14257 });
14258
14259 let mut cx = EditorLspTestContext::new_rust(
14260 lsp::ServerCapabilities {
14261 completion_provider: Some(lsp::CompletionOptions {
14262 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14263 ..lsp::CompletionOptions::default()
14264 }),
14265 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14266 ..lsp::ServerCapabilities::default()
14267 },
14268 cx,
14269 )
14270 .await;
14271
14272 let throttle_completions = Arc::new(AtomicBool::new(false));
14273
14274 let lsp_throttle_completions = throttle_completions.clone();
14275 let _completion_requests_handler =
14276 cx.lsp
14277 .server
14278 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
14279 let lsp_throttle_completions = lsp_throttle_completions.clone();
14280 let cx = cx.clone();
14281 async move {
14282 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
14283 cx.background_executor()
14284 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
14285 .await;
14286 }
14287 Ok(Some(lsp::CompletionResponse::Array(vec![
14288 lsp::CompletionItem {
14289 label: "first".into(),
14290 ..lsp::CompletionItem::default()
14291 },
14292 lsp::CompletionItem {
14293 label: "last".into(),
14294 ..lsp::CompletionItem::default()
14295 },
14296 ])))
14297 }
14298 });
14299
14300 cx.set_state(indoc! {"
14301 oneˇ
14302 two
14303 three
14304 "});
14305 cx.simulate_keystroke(".");
14306 cx.executor().run_until_parked();
14307 cx.condition(|editor, _| editor.context_menu_visible())
14308 .await;
14309 cx.update_editor(|editor, window, cx| {
14310 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14311 {
14312 assert_eq!(
14313 completion_menu_entries(menu),
14314 &["first", "last"],
14315 "When LSP server is fast to reply, no fallback word completions are used"
14316 );
14317 } else {
14318 panic!("expected completion menu to be open");
14319 }
14320 editor.cancel(&Cancel, window, cx);
14321 });
14322 cx.executor().run_until_parked();
14323 cx.condition(|editor, _| !editor.context_menu_visible())
14324 .await;
14325
14326 throttle_completions.store(true, atomic::Ordering::Release);
14327 cx.simulate_keystroke(".");
14328 cx.executor()
14329 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
14330 cx.executor().run_until_parked();
14331 cx.condition(|editor, _| editor.context_menu_visible())
14332 .await;
14333 cx.update_editor(|editor, _, _| {
14334 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14335 {
14336 assert_eq!(completion_menu_entries(menu), &["one", "three", "two"],
14337 "When LSP server is slow, document words can be shown instead, if configured accordingly");
14338 } else {
14339 panic!("expected completion menu to be open");
14340 }
14341 });
14342}
14343
14344#[gpui::test]
14345async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
14346 init_test(cx, |language_settings| {
14347 language_settings.defaults.completions = Some(CompletionSettingsContent {
14348 words: Some(WordsCompletionMode::Enabled),
14349 words_min_length: Some(0),
14350 lsp_insert_mode: Some(LspInsertMode::Insert),
14351 ..Default::default()
14352 });
14353 });
14354
14355 let mut cx = EditorLspTestContext::new_rust(
14356 lsp::ServerCapabilities {
14357 completion_provider: Some(lsp::CompletionOptions {
14358 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14359 ..lsp::CompletionOptions::default()
14360 }),
14361 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14362 ..lsp::ServerCapabilities::default()
14363 },
14364 cx,
14365 )
14366 .await;
14367
14368 let _completion_requests_handler =
14369 cx.lsp
14370 .server
14371 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14372 Ok(Some(lsp::CompletionResponse::Array(vec![
14373 lsp::CompletionItem {
14374 label: "first".into(),
14375 ..lsp::CompletionItem::default()
14376 },
14377 lsp::CompletionItem {
14378 label: "last".into(),
14379 ..lsp::CompletionItem::default()
14380 },
14381 ])))
14382 });
14383
14384 cx.set_state(indoc! {"ˇ
14385 first
14386 last
14387 second
14388 "});
14389 cx.simulate_keystroke(".");
14390 cx.executor().run_until_parked();
14391 cx.condition(|editor, _| editor.context_menu_visible())
14392 .await;
14393 cx.update_editor(|editor, _, _| {
14394 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14395 {
14396 assert_eq!(
14397 completion_menu_entries(menu),
14398 &["first", "last", "second"],
14399 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
14400 );
14401 } else {
14402 panic!("expected completion menu to be open");
14403 }
14404 });
14405}
14406
14407#[gpui::test]
14408async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
14409 init_test(cx, |language_settings| {
14410 language_settings.defaults.completions = Some(CompletionSettingsContent {
14411 words: Some(WordsCompletionMode::Disabled),
14412 words_min_length: Some(0),
14413 lsp_insert_mode: Some(LspInsertMode::Insert),
14414 ..Default::default()
14415 });
14416 });
14417
14418 let mut cx = EditorLspTestContext::new_rust(
14419 lsp::ServerCapabilities {
14420 completion_provider: Some(lsp::CompletionOptions {
14421 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14422 ..lsp::CompletionOptions::default()
14423 }),
14424 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14425 ..lsp::ServerCapabilities::default()
14426 },
14427 cx,
14428 )
14429 .await;
14430
14431 let _completion_requests_handler =
14432 cx.lsp
14433 .server
14434 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14435 panic!("LSP completions should not be queried when dealing with word completions")
14436 });
14437
14438 cx.set_state(indoc! {"ˇ
14439 first
14440 last
14441 second
14442 "});
14443 cx.update_editor(|editor, window, cx| {
14444 editor.show_word_completions(&ShowWordCompletions, window, cx);
14445 });
14446 cx.executor().run_until_parked();
14447 cx.condition(|editor, _| editor.context_menu_visible())
14448 .await;
14449 cx.update_editor(|editor, _, _| {
14450 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14451 {
14452 assert_eq!(
14453 completion_menu_entries(menu),
14454 &["first", "last", "second"],
14455 "`ShowWordCompletions` action should show word completions"
14456 );
14457 } else {
14458 panic!("expected completion menu to be open");
14459 }
14460 });
14461
14462 cx.simulate_keystroke("l");
14463 cx.executor().run_until_parked();
14464 cx.condition(|editor, _| editor.context_menu_visible())
14465 .await;
14466 cx.update_editor(|editor, _, _| {
14467 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14468 {
14469 assert_eq!(
14470 completion_menu_entries(menu),
14471 &["last"],
14472 "After showing word completions, further editing should filter them and not query the LSP"
14473 );
14474 } else {
14475 panic!("expected completion menu to be open");
14476 }
14477 });
14478}
14479
14480#[gpui::test]
14481async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
14482 init_test(cx, |language_settings| {
14483 language_settings.defaults.completions = Some(CompletionSettingsContent {
14484 words_min_length: Some(0),
14485 lsp: Some(false),
14486 lsp_insert_mode: Some(LspInsertMode::Insert),
14487 ..Default::default()
14488 });
14489 });
14490
14491 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14492
14493 cx.set_state(indoc! {"ˇ
14494 0_usize
14495 let
14496 33
14497 4.5f32
14498 "});
14499 cx.update_editor(|editor, window, cx| {
14500 editor.show_completions(&ShowCompletions::default(), window, cx);
14501 });
14502 cx.executor().run_until_parked();
14503 cx.condition(|editor, _| editor.context_menu_visible())
14504 .await;
14505 cx.update_editor(|editor, window, cx| {
14506 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14507 {
14508 assert_eq!(
14509 completion_menu_entries(menu),
14510 &["let"],
14511 "With no digits in the completion query, no digits should be in the word completions"
14512 );
14513 } else {
14514 panic!("expected completion menu to be open");
14515 }
14516 editor.cancel(&Cancel, window, cx);
14517 });
14518
14519 cx.set_state(indoc! {"3ˇ
14520 0_usize
14521 let
14522 3
14523 33.35f32
14524 "});
14525 cx.update_editor(|editor, window, cx| {
14526 editor.show_completions(&ShowCompletions::default(), window, cx);
14527 });
14528 cx.executor().run_until_parked();
14529 cx.condition(|editor, _| editor.context_menu_visible())
14530 .await;
14531 cx.update_editor(|editor, _, _| {
14532 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14533 {
14534 assert_eq!(completion_menu_entries(menu), &["33", "35f32"], "The digit is in the completion query, \
14535 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
14536 } else {
14537 panic!("expected completion menu to be open");
14538 }
14539 });
14540}
14541
14542#[gpui::test]
14543async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) {
14544 init_test(cx, |language_settings| {
14545 language_settings.defaults.completions = Some(CompletionSettingsContent {
14546 words: Some(WordsCompletionMode::Enabled),
14547 words_min_length: Some(3),
14548 lsp_insert_mode: Some(LspInsertMode::Insert),
14549 ..Default::default()
14550 });
14551 });
14552
14553 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14554 cx.set_state(indoc! {"ˇ
14555 wow
14556 wowen
14557 wowser
14558 "});
14559 cx.simulate_keystroke("w");
14560 cx.executor().run_until_parked();
14561 cx.update_editor(|editor, _, _| {
14562 if editor.context_menu.borrow_mut().is_some() {
14563 panic!(
14564 "expected completion menu to be hidden, as words completion threshold is not met"
14565 );
14566 }
14567 });
14568
14569 cx.update_editor(|editor, window, cx| {
14570 editor.show_word_completions(&ShowWordCompletions, window, cx);
14571 });
14572 cx.executor().run_until_parked();
14573 cx.update_editor(|editor, window, cx| {
14574 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14575 {
14576 assert_eq!(completion_menu_entries(menu), &["wowser", "wowen", "wow"], "Even though the threshold is not met, invoking word completions with an action should provide the completions");
14577 } else {
14578 panic!("expected completion menu to be open after the word completions are called with an action");
14579 }
14580
14581 editor.cancel(&Cancel, window, cx);
14582 });
14583 cx.update_editor(|editor, _, _| {
14584 if editor.context_menu.borrow_mut().is_some() {
14585 panic!("expected completion menu to be hidden after canceling");
14586 }
14587 });
14588
14589 cx.simulate_keystroke("o");
14590 cx.executor().run_until_parked();
14591 cx.update_editor(|editor, _, _| {
14592 if editor.context_menu.borrow_mut().is_some() {
14593 panic!(
14594 "expected completion menu to be hidden, as words completion threshold is not met still"
14595 );
14596 }
14597 });
14598
14599 cx.simulate_keystroke("w");
14600 cx.executor().run_until_parked();
14601 cx.update_editor(|editor, _, _| {
14602 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14603 {
14604 assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word");
14605 } else {
14606 panic!("expected completion menu to be open after the word completions threshold is met");
14607 }
14608 });
14609}
14610
14611#[gpui::test]
14612async fn test_word_completions_disabled(cx: &mut TestAppContext) {
14613 init_test(cx, |language_settings| {
14614 language_settings.defaults.completions = Some(CompletionSettingsContent {
14615 words: Some(WordsCompletionMode::Enabled),
14616 words_min_length: Some(0),
14617 lsp_insert_mode: Some(LspInsertMode::Insert),
14618 ..Default::default()
14619 });
14620 });
14621
14622 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14623 cx.update_editor(|editor, _, _| {
14624 editor.disable_word_completions();
14625 });
14626 cx.set_state(indoc! {"ˇ
14627 wow
14628 wowen
14629 wowser
14630 "});
14631 cx.simulate_keystroke("w");
14632 cx.executor().run_until_parked();
14633 cx.update_editor(|editor, _, _| {
14634 if editor.context_menu.borrow_mut().is_some() {
14635 panic!(
14636 "expected completion menu to be hidden, as words completion are disabled for this editor"
14637 );
14638 }
14639 });
14640
14641 cx.update_editor(|editor, window, cx| {
14642 editor.show_word_completions(&ShowWordCompletions, window, cx);
14643 });
14644 cx.executor().run_until_parked();
14645 cx.update_editor(|editor, _, _| {
14646 if editor.context_menu.borrow_mut().is_some() {
14647 panic!(
14648 "expected completion menu to be hidden even if called for explicitly, as words completion are disabled for this editor"
14649 );
14650 }
14651 });
14652}
14653
14654fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
14655 let position = || lsp::Position {
14656 line: params.text_document_position.position.line,
14657 character: params.text_document_position.position.character,
14658 };
14659 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14660 range: lsp::Range {
14661 start: position(),
14662 end: position(),
14663 },
14664 new_text: text.to_string(),
14665 }))
14666}
14667
14668#[gpui::test]
14669async fn test_multiline_completion(cx: &mut TestAppContext) {
14670 init_test(cx, |_| {});
14671
14672 let fs = FakeFs::new(cx.executor());
14673 fs.insert_tree(
14674 path!("/a"),
14675 json!({
14676 "main.ts": "a",
14677 }),
14678 )
14679 .await;
14680
14681 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14682 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14683 let typescript_language = Arc::new(Language::new(
14684 LanguageConfig {
14685 name: "TypeScript".into(),
14686 matcher: LanguageMatcher {
14687 path_suffixes: vec!["ts".to_string()],
14688 ..LanguageMatcher::default()
14689 },
14690 line_comments: vec!["// ".into()],
14691 ..LanguageConfig::default()
14692 },
14693 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
14694 ));
14695 language_registry.add(typescript_language.clone());
14696 let mut fake_servers = language_registry.register_fake_lsp(
14697 "TypeScript",
14698 FakeLspAdapter {
14699 capabilities: lsp::ServerCapabilities {
14700 completion_provider: Some(lsp::CompletionOptions {
14701 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14702 ..lsp::CompletionOptions::default()
14703 }),
14704 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14705 ..lsp::ServerCapabilities::default()
14706 },
14707 // Emulate vtsls label generation
14708 label_for_completion: Some(Box::new(|item, _| {
14709 let text = if let Some(description) = item
14710 .label_details
14711 .as_ref()
14712 .and_then(|label_details| label_details.description.as_ref())
14713 {
14714 format!("{} {}", item.label, description)
14715 } else if let Some(detail) = &item.detail {
14716 format!("{} {}", item.label, detail)
14717 } else {
14718 item.label.clone()
14719 };
14720 let len = text.len();
14721 Some(language::CodeLabel {
14722 text,
14723 runs: Vec::new(),
14724 filter_range: 0..len,
14725 })
14726 })),
14727 ..FakeLspAdapter::default()
14728 },
14729 );
14730 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14731 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14732 let worktree_id = workspace
14733 .update(cx, |workspace, _window, cx| {
14734 workspace.project().update(cx, |project, cx| {
14735 project.worktrees(cx).next().unwrap().read(cx).id()
14736 })
14737 })
14738 .unwrap();
14739 let _buffer = project
14740 .update(cx, |project, cx| {
14741 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
14742 })
14743 .await
14744 .unwrap();
14745 let editor = workspace
14746 .update(cx, |workspace, window, cx| {
14747 workspace.open_path((worktree_id, rel_path("main.ts")), None, true, window, cx)
14748 })
14749 .unwrap()
14750 .await
14751 .unwrap()
14752 .downcast::<Editor>()
14753 .unwrap();
14754 let fake_server = fake_servers.next().await.unwrap();
14755
14756 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
14757 let multiline_label_2 = "a\nb\nc\n";
14758 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
14759 let multiline_description = "d\ne\nf\n";
14760 let multiline_detail_2 = "g\nh\ni\n";
14761
14762 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
14763 move |params, _| async move {
14764 Ok(Some(lsp::CompletionResponse::Array(vec![
14765 lsp::CompletionItem {
14766 label: multiline_label.to_string(),
14767 text_edit: gen_text_edit(¶ms, "new_text_1"),
14768 ..lsp::CompletionItem::default()
14769 },
14770 lsp::CompletionItem {
14771 label: "single line label 1".to_string(),
14772 detail: Some(multiline_detail.to_string()),
14773 text_edit: gen_text_edit(¶ms, "new_text_2"),
14774 ..lsp::CompletionItem::default()
14775 },
14776 lsp::CompletionItem {
14777 label: "single line label 2".to_string(),
14778 label_details: Some(lsp::CompletionItemLabelDetails {
14779 description: Some(multiline_description.to_string()),
14780 detail: None,
14781 }),
14782 text_edit: gen_text_edit(¶ms, "new_text_2"),
14783 ..lsp::CompletionItem::default()
14784 },
14785 lsp::CompletionItem {
14786 label: multiline_label_2.to_string(),
14787 detail: Some(multiline_detail_2.to_string()),
14788 text_edit: gen_text_edit(¶ms, "new_text_3"),
14789 ..lsp::CompletionItem::default()
14790 },
14791 lsp::CompletionItem {
14792 label: "Label with many spaces and \t but without newlines".to_string(),
14793 detail: Some(
14794 "Details with many spaces and \t but without newlines".to_string(),
14795 ),
14796 text_edit: gen_text_edit(¶ms, "new_text_4"),
14797 ..lsp::CompletionItem::default()
14798 },
14799 ])))
14800 },
14801 );
14802
14803 editor.update_in(cx, |editor, window, cx| {
14804 cx.focus_self(window);
14805 editor.move_to_end(&MoveToEnd, window, cx);
14806 editor.handle_input(".", window, cx);
14807 });
14808 cx.run_until_parked();
14809 completion_handle.next().await.unwrap();
14810
14811 editor.update(cx, |editor, _| {
14812 assert!(editor.context_menu_visible());
14813 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14814 {
14815 let completion_labels = menu
14816 .completions
14817 .borrow()
14818 .iter()
14819 .map(|c| c.label.text.clone())
14820 .collect::<Vec<_>>();
14821 assert_eq!(
14822 completion_labels,
14823 &[
14824 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
14825 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
14826 "single line label 2 d e f ",
14827 "a b c g h i ",
14828 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
14829 ],
14830 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
14831 );
14832
14833 for completion in menu
14834 .completions
14835 .borrow()
14836 .iter() {
14837 assert_eq!(
14838 completion.label.filter_range,
14839 0..completion.label.text.len(),
14840 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
14841 );
14842 }
14843 } else {
14844 panic!("expected completion menu to be open");
14845 }
14846 });
14847}
14848
14849#[gpui::test]
14850async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
14851 init_test(cx, |_| {});
14852 let mut cx = EditorLspTestContext::new_rust(
14853 lsp::ServerCapabilities {
14854 completion_provider: Some(lsp::CompletionOptions {
14855 trigger_characters: Some(vec![".".to_string()]),
14856 ..Default::default()
14857 }),
14858 ..Default::default()
14859 },
14860 cx,
14861 )
14862 .await;
14863 cx.lsp
14864 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14865 Ok(Some(lsp::CompletionResponse::Array(vec![
14866 lsp::CompletionItem {
14867 label: "first".into(),
14868 ..Default::default()
14869 },
14870 lsp::CompletionItem {
14871 label: "last".into(),
14872 ..Default::default()
14873 },
14874 ])))
14875 });
14876 cx.set_state("variableˇ");
14877 cx.simulate_keystroke(".");
14878 cx.executor().run_until_parked();
14879
14880 cx.update_editor(|editor, _, _| {
14881 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14882 {
14883 assert_eq!(completion_menu_entries(menu), &["first", "last"]);
14884 } else {
14885 panic!("expected completion menu to be open");
14886 }
14887 });
14888
14889 cx.update_editor(|editor, window, cx| {
14890 editor.move_page_down(&MovePageDown::default(), window, cx);
14891 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14892 {
14893 assert!(
14894 menu.selected_item == 1,
14895 "expected PageDown to select the last item from the context menu"
14896 );
14897 } else {
14898 panic!("expected completion menu to stay open after PageDown");
14899 }
14900 });
14901
14902 cx.update_editor(|editor, window, cx| {
14903 editor.move_page_up(&MovePageUp::default(), window, cx);
14904 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14905 {
14906 assert!(
14907 menu.selected_item == 0,
14908 "expected PageUp to select the first item from the context menu"
14909 );
14910 } else {
14911 panic!("expected completion menu to stay open after PageUp");
14912 }
14913 });
14914}
14915
14916#[gpui::test]
14917async fn test_as_is_completions(cx: &mut TestAppContext) {
14918 init_test(cx, |_| {});
14919 let mut cx = EditorLspTestContext::new_rust(
14920 lsp::ServerCapabilities {
14921 completion_provider: Some(lsp::CompletionOptions {
14922 ..Default::default()
14923 }),
14924 ..Default::default()
14925 },
14926 cx,
14927 )
14928 .await;
14929 cx.lsp
14930 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14931 Ok(Some(lsp::CompletionResponse::Array(vec![
14932 lsp::CompletionItem {
14933 label: "unsafe".into(),
14934 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14935 range: lsp::Range {
14936 start: lsp::Position {
14937 line: 1,
14938 character: 2,
14939 },
14940 end: lsp::Position {
14941 line: 1,
14942 character: 3,
14943 },
14944 },
14945 new_text: "unsafe".to_string(),
14946 })),
14947 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
14948 ..Default::default()
14949 },
14950 ])))
14951 });
14952 cx.set_state("fn a() {}\n nˇ");
14953 cx.executor().run_until_parked();
14954 cx.update_editor(|editor, window, cx| {
14955 editor.show_completions(
14956 &ShowCompletions {
14957 trigger: Some("\n".into()),
14958 },
14959 window,
14960 cx,
14961 );
14962 });
14963 cx.executor().run_until_parked();
14964
14965 cx.update_editor(|editor, window, cx| {
14966 editor.confirm_completion(&Default::default(), window, cx)
14967 });
14968 cx.executor().run_until_parked();
14969 cx.assert_editor_state("fn a() {}\n unsafeˇ");
14970}
14971
14972#[gpui::test]
14973async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
14974 init_test(cx, |_| {});
14975 let language =
14976 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
14977 let mut cx = EditorLspTestContext::new(
14978 language,
14979 lsp::ServerCapabilities {
14980 completion_provider: Some(lsp::CompletionOptions {
14981 ..lsp::CompletionOptions::default()
14982 }),
14983 ..lsp::ServerCapabilities::default()
14984 },
14985 cx,
14986 )
14987 .await;
14988
14989 cx.set_state(
14990 "#ifndef BAR_H
14991#define BAR_H
14992
14993#include <stdbool.h>
14994
14995int fn_branch(bool do_branch1, bool do_branch2);
14996
14997#endif // BAR_H
14998ˇ",
14999 );
15000 cx.executor().run_until_parked();
15001 cx.update_editor(|editor, window, cx| {
15002 editor.handle_input("#", window, cx);
15003 });
15004 cx.executor().run_until_parked();
15005 cx.update_editor(|editor, window, cx| {
15006 editor.handle_input("i", window, cx);
15007 });
15008 cx.executor().run_until_parked();
15009 cx.update_editor(|editor, window, cx| {
15010 editor.handle_input("n", window, cx);
15011 });
15012 cx.executor().run_until_parked();
15013 cx.assert_editor_state(
15014 "#ifndef BAR_H
15015#define BAR_H
15016
15017#include <stdbool.h>
15018
15019int fn_branch(bool do_branch1, bool do_branch2);
15020
15021#endif // BAR_H
15022#inˇ",
15023 );
15024
15025 cx.lsp
15026 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15027 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15028 is_incomplete: false,
15029 item_defaults: None,
15030 items: vec![lsp::CompletionItem {
15031 kind: Some(lsp::CompletionItemKind::SNIPPET),
15032 label_details: Some(lsp::CompletionItemLabelDetails {
15033 detail: Some("header".to_string()),
15034 description: None,
15035 }),
15036 label: " include".to_string(),
15037 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15038 range: lsp::Range {
15039 start: lsp::Position {
15040 line: 8,
15041 character: 1,
15042 },
15043 end: lsp::Position {
15044 line: 8,
15045 character: 1,
15046 },
15047 },
15048 new_text: "include \"$0\"".to_string(),
15049 })),
15050 sort_text: Some("40b67681include".to_string()),
15051 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15052 filter_text: Some("include".to_string()),
15053 insert_text: Some("include \"$0\"".to_string()),
15054 ..lsp::CompletionItem::default()
15055 }],
15056 })))
15057 });
15058 cx.update_editor(|editor, window, cx| {
15059 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15060 });
15061 cx.executor().run_until_parked();
15062 cx.update_editor(|editor, window, cx| {
15063 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15064 });
15065 cx.executor().run_until_parked();
15066 cx.assert_editor_state(
15067 "#ifndef BAR_H
15068#define BAR_H
15069
15070#include <stdbool.h>
15071
15072int fn_branch(bool do_branch1, bool do_branch2);
15073
15074#endif // BAR_H
15075#include \"ˇ\"",
15076 );
15077
15078 cx.lsp
15079 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15080 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15081 is_incomplete: true,
15082 item_defaults: None,
15083 items: vec![lsp::CompletionItem {
15084 kind: Some(lsp::CompletionItemKind::FILE),
15085 label: "AGL/".to_string(),
15086 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15087 range: lsp::Range {
15088 start: lsp::Position {
15089 line: 8,
15090 character: 10,
15091 },
15092 end: lsp::Position {
15093 line: 8,
15094 character: 11,
15095 },
15096 },
15097 new_text: "AGL/".to_string(),
15098 })),
15099 sort_text: Some("40b67681AGL/".to_string()),
15100 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
15101 filter_text: Some("AGL/".to_string()),
15102 insert_text: Some("AGL/".to_string()),
15103 ..lsp::CompletionItem::default()
15104 }],
15105 })))
15106 });
15107 cx.update_editor(|editor, window, cx| {
15108 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15109 });
15110 cx.executor().run_until_parked();
15111 cx.update_editor(|editor, window, cx| {
15112 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15113 });
15114 cx.executor().run_until_parked();
15115 cx.assert_editor_state(
15116 r##"#ifndef BAR_H
15117#define BAR_H
15118
15119#include <stdbool.h>
15120
15121int fn_branch(bool do_branch1, bool do_branch2);
15122
15123#endif // BAR_H
15124#include "AGL/ˇ"##,
15125 );
15126
15127 cx.update_editor(|editor, window, cx| {
15128 editor.handle_input("\"", window, cx);
15129 });
15130 cx.executor().run_until_parked();
15131 cx.assert_editor_state(
15132 r##"#ifndef BAR_H
15133#define BAR_H
15134
15135#include <stdbool.h>
15136
15137int fn_branch(bool do_branch1, bool do_branch2);
15138
15139#endif // BAR_H
15140#include "AGL/"ˇ"##,
15141 );
15142}
15143
15144#[gpui::test]
15145async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
15146 init_test(cx, |_| {});
15147
15148 let mut cx = EditorLspTestContext::new_rust(
15149 lsp::ServerCapabilities {
15150 completion_provider: Some(lsp::CompletionOptions {
15151 trigger_characters: Some(vec![".".to_string()]),
15152 resolve_provider: Some(true),
15153 ..Default::default()
15154 }),
15155 ..Default::default()
15156 },
15157 cx,
15158 )
15159 .await;
15160
15161 cx.set_state("fn main() { let a = 2ˇ; }");
15162 cx.simulate_keystroke(".");
15163 let completion_item = lsp::CompletionItem {
15164 label: "Some".into(),
15165 kind: Some(lsp::CompletionItemKind::SNIPPET),
15166 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
15167 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
15168 kind: lsp::MarkupKind::Markdown,
15169 value: "```rust\nSome(2)\n```".to_string(),
15170 })),
15171 deprecated: Some(false),
15172 sort_text: Some("Some".to_string()),
15173 filter_text: Some("Some".to_string()),
15174 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15175 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15176 range: lsp::Range {
15177 start: lsp::Position {
15178 line: 0,
15179 character: 22,
15180 },
15181 end: lsp::Position {
15182 line: 0,
15183 character: 22,
15184 },
15185 },
15186 new_text: "Some(2)".to_string(),
15187 })),
15188 additional_text_edits: Some(vec![lsp::TextEdit {
15189 range: lsp::Range {
15190 start: lsp::Position {
15191 line: 0,
15192 character: 20,
15193 },
15194 end: lsp::Position {
15195 line: 0,
15196 character: 22,
15197 },
15198 },
15199 new_text: "".to_string(),
15200 }]),
15201 ..Default::default()
15202 };
15203
15204 let closure_completion_item = completion_item.clone();
15205 let counter = Arc::new(AtomicUsize::new(0));
15206 let counter_clone = counter.clone();
15207 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15208 let task_completion_item = closure_completion_item.clone();
15209 counter_clone.fetch_add(1, atomic::Ordering::Release);
15210 async move {
15211 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15212 is_incomplete: true,
15213 item_defaults: None,
15214 items: vec![task_completion_item],
15215 })))
15216 }
15217 });
15218
15219 cx.condition(|editor, _| editor.context_menu_visible())
15220 .await;
15221 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
15222 assert!(request.next().await.is_some());
15223 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15224
15225 cx.simulate_keystrokes("S o m");
15226 cx.condition(|editor, _| editor.context_menu_visible())
15227 .await;
15228 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
15229 assert!(request.next().await.is_some());
15230 assert!(request.next().await.is_some());
15231 assert!(request.next().await.is_some());
15232 request.close();
15233 assert!(request.next().await.is_none());
15234 assert_eq!(
15235 counter.load(atomic::Ordering::Acquire),
15236 4,
15237 "With the completions menu open, only one LSP request should happen per input"
15238 );
15239}
15240
15241#[gpui::test]
15242async fn test_toggle_comment(cx: &mut TestAppContext) {
15243 init_test(cx, |_| {});
15244 let mut cx = EditorTestContext::new(cx).await;
15245 let language = Arc::new(Language::new(
15246 LanguageConfig {
15247 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15248 ..Default::default()
15249 },
15250 Some(tree_sitter_rust::LANGUAGE.into()),
15251 ));
15252 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15253
15254 // If multiple selections intersect a line, the line is only toggled once.
15255 cx.set_state(indoc! {"
15256 fn a() {
15257 «//b();
15258 ˇ»// «c();
15259 //ˇ» d();
15260 }
15261 "});
15262
15263 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15264
15265 cx.assert_editor_state(indoc! {"
15266 fn a() {
15267 «b();
15268 c();
15269 ˇ» d();
15270 }
15271 "});
15272
15273 // The comment prefix is inserted at the same column for every line in a
15274 // selection.
15275 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15276
15277 cx.assert_editor_state(indoc! {"
15278 fn a() {
15279 // «b();
15280 // c();
15281 ˇ»// d();
15282 }
15283 "});
15284
15285 // If a selection ends at the beginning of a line, that line is not toggled.
15286 cx.set_selections_state(indoc! {"
15287 fn a() {
15288 // b();
15289 «// c();
15290 ˇ» // d();
15291 }
15292 "});
15293
15294 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15295
15296 cx.assert_editor_state(indoc! {"
15297 fn a() {
15298 // b();
15299 «c();
15300 ˇ» // d();
15301 }
15302 "});
15303
15304 // If a selection span a single line and is empty, the line is toggled.
15305 cx.set_state(indoc! {"
15306 fn a() {
15307 a();
15308 b();
15309 ˇ
15310 }
15311 "});
15312
15313 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15314
15315 cx.assert_editor_state(indoc! {"
15316 fn a() {
15317 a();
15318 b();
15319 //•ˇ
15320 }
15321 "});
15322
15323 // If a selection span multiple lines, empty lines are not toggled.
15324 cx.set_state(indoc! {"
15325 fn a() {
15326 «a();
15327
15328 c();ˇ»
15329 }
15330 "});
15331
15332 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15333
15334 cx.assert_editor_state(indoc! {"
15335 fn a() {
15336 // «a();
15337
15338 // c();ˇ»
15339 }
15340 "});
15341
15342 // If a selection includes multiple comment prefixes, all lines are uncommented.
15343 cx.set_state(indoc! {"
15344 fn a() {
15345 «// a();
15346 /// b();
15347 //! c();ˇ»
15348 }
15349 "});
15350
15351 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15352
15353 cx.assert_editor_state(indoc! {"
15354 fn a() {
15355 «a();
15356 b();
15357 c();ˇ»
15358 }
15359 "});
15360}
15361
15362#[gpui::test]
15363async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
15364 init_test(cx, |_| {});
15365 let mut cx = EditorTestContext::new(cx).await;
15366 let language = Arc::new(Language::new(
15367 LanguageConfig {
15368 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15369 ..Default::default()
15370 },
15371 Some(tree_sitter_rust::LANGUAGE.into()),
15372 ));
15373 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15374
15375 let toggle_comments = &ToggleComments {
15376 advance_downwards: false,
15377 ignore_indent: true,
15378 };
15379
15380 // If multiple selections intersect a line, the line is only toggled once.
15381 cx.set_state(indoc! {"
15382 fn a() {
15383 // «b();
15384 // c();
15385 // ˇ» d();
15386 }
15387 "});
15388
15389 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15390
15391 cx.assert_editor_state(indoc! {"
15392 fn a() {
15393 «b();
15394 c();
15395 ˇ» d();
15396 }
15397 "});
15398
15399 // The comment prefix is inserted at the beginning of each line
15400 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15401
15402 cx.assert_editor_state(indoc! {"
15403 fn a() {
15404 // «b();
15405 // c();
15406 // ˇ» d();
15407 }
15408 "});
15409
15410 // If a selection ends at the beginning of a line, that line is not toggled.
15411 cx.set_selections_state(indoc! {"
15412 fn a() {
15413 // b();
15414 // «c();
15415 ˇ»// d();
15416 }
15417 "});
15418
15419 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15420
15421 cx.assert_editor_state(indoc! {"
15422 fn a() {
15423 // b();
15424 «c();
15425 ˇ»// d();
15426 }
15427 "});
15428
15429 // If a selection span a single line and is empty, the line is toggled.
15430 cx.set_state(indoc! {"
15431 fn a() {
15432 a();
15433 b();
15434 ˇ
15435 }
15436 "});
15437
15438 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15439
15440 cx.assert_editor_state(indoc! {"
15441 fn a() {
15442 a();
15443 b();
15444 //ˇ
15445 }
15446 "});
15447
15448 // If a selection span multiple lines, empty lines are not toggled.
15449 cx.set_state(indoc! {"
15450 fn a() {
15451 «a();
15452
15453 c();ˇ»
15454 }
15455 "});
15456
15457 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15458
15459 cx.assert_editor_state(indoc! {"
15460 fn a() {
15461 // «a();
15462
15463 // c();ˇ»
15464 }
15465 "});
15466
15467 // If a selection includes multiple comment prefixes, all lines are uncommented.
15468 cx.set_state(indoc! {"
15469 fn a() {
15470 // «a();
15471 /// b();
15472 //! c();ˇ»
15473 }
15474 "});
15475
15476 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15477
15478 cx.assert_editor_state(indoc! {"
15479 fn a() {
15480 «a();
15481 b();
15482 c();ˇ»
15483 }
15484 "});
15485}
15486
15487#[gpui::test]
15488async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
15489 init_test(cx, |_| {});
15490
15491 let language = Arc::new(Language::new(
15492 LanguageConfig {
15493 line_comments: vec!["// ".into()],
15494 ..Default::default()
15495 },
15496 Some(tree_sitter_rust::LANGUAGE.into()),
15497 ));
15498
15499 let mut cx = EditorTestContext::new(cx).await;
15500
15501 cx.language_registry().add(language.clone());
15502 cx.update_buffer(|buffer, cx| {
15503 buffer.set_language(Some(language), cx);
15504 });
15505
15506 let toggle_comments = &ToggleComments {
15507 advance_downwards: true,
15508 ignore_indent: false,
15509 };
15510
15511 // Single cursor on one line -> advance
15512 // Cursor moves horizontally 3 characters as well on non-blank line
15513 cx.set_state(indoc!(
15514 "fn a() {
15515 ˇdog();
15516 cat();
15517 }"
15518 ));
15519 cx.update_editor(|editor, window, cx| {
15520 editor.toggle_comments(toggle_comments, window, cx);
15521 });
15522 cx.assert_editor_state(indoc!(
15523 "fn a() {
15524 // dog();
15525 catˇ();
15526 }"
15527 ));
15528
15529 // Single selection on one line -> don't advance
15530 cx.set_state(indoc!(
15531 "fn a() {
15532 «dog()ˇ»;
15533 cat();
15534 }"
15535 ));
15536 cx.update_editor(|editor, window, cx| {
15537 editor.toggle_comments(toggle_comments, window, cx);
15538 });
15539 cx.assert_editor_state(indoc!(
15540 "fn a() {
15541 // «dog()ˇ»;
15542 cat();
15543 }"
15544 ));
15545
15546 // Multiple cursors on one line -> advance
15547 cx.set_state(indoc!(
15548 "fn a() {
15549 ˇdˇog();
15550 cat();
15551 }"
15552 ));
15553 cx.update_editor(|editor, window, cx| {
15554 editor.toggle_comments(toggle_comments, window, cx);
15555 });
15556 cx.assert_editor_state(indoc!(
15557 "fn a() {
15558 // dog();
15559 catˇ(ˇ);
15560 }"
15561 ));
15562
15563 // Multiple cursors on one line, with selection -> don't advance
15564 cx.set_state(indoc!(
15565 "fn a() {
15566 ˇdˇog«()ˇ»;
15567 cat();
15568 }"
15569 ));
15570 cx.update_editor(|editor, window, cx| {
15571 editor.toggle_comments(toggle_comments, window, cx);
15572 });
15573 cx.assert_editor_state(indoc!(
15574 "fn a() {
15575 // ˇdˇog«()ˇ»;
15576 cat();
15577 }"
15578 ));
15579
15580 // Single cursor on one line -> advance
15581 // Cursor moves to column 0 on blank line
15582 cx.set_state(indoc!(
15583 "fn a() {
15584 ˇdog();
15585
15586 cat();
15587 }"
15588 ));
15589 cx.update_editor(|editor, window, cx| {
15590 editor.toggle_comments(toggle_comments, window, cx);
15591 });
15592 cx.assert_editor_state(indoc!(
15593 "fn a() {
15594 // dog();
15595 ˇ
15596 cat();
15597 }"
15598 ));
15599
15600 // Single cursor on one line -> advance
15601 // Cursor starts and ends at column 0
15602 cx.set_state(indoc!(
15603 "fn a() {
15604 ˇ dog();
15605 cat();
15606 }"
15607 ));
15608 cx.update_editor(|editor, window, cx| {
15609 editor.toggle_comments(toggle_comments, window, cx);
15610 });
15611 cx.assert_editor_state(indoc!(
15612 "fn a() {
15613 // dog();
15614 ˇ cat();
15615 }"
15616 ));
15617}
15618
15619#[gpui::test]
15620async fn test_toggle_block_comment(cx: &mut TestAppContext) {
15621 init_test(cx, |_| {});
15622
15623 let mut cx = EditorTestContext::new(cx).await;
15624
15625 let html_language = Arc::new(
15626 Language::new(
15627 LanguageConfig {
15628 name: "HTML".into(),
15629 block_comment: Some(BlockCommentConfig {
15630 start: "<!-- ".into(),
15631 prefix: "".into(),
15632 end: " -->".into(),
15633 tab_size: 0,
15634 }),
15635 ..Default::default()
15636 },
15637 Some(tree_sitter_html::LANGUAGE.into()),
15638 )
15639 .with_injection_query(
15640 r#"
15641 (script_element
15642 (raw_text) @injection.content
15643 (#set! injection.language "javascript"))
15644 "#,
15645 )
15646 .unwrap(),
15647 );
15648
15649 let javascript_language = Arc::new(Language::new(
15650 LanguageConfig {
15651 name: "JavaScript".into(),
15652 line_comments: vec!["// ".into()],
15653 ..Default::default()
15654 },
15655 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
15656 ));
15657
15658 cx.language_registry().add(html_language.clone());
15659 cx.language_registry().add(javascript_language);
15660 cx.update_buffer(|buffer, cx| {
15661 buffer.set_language(Some(html_language), cx);
15662 });
15663
15664 // Toggle comments for empty selections
15665 cx.set_state(
15666 &r#"
15667 <p>A</p>ˇ
15668 <p>B</p>ˇ
15669 <p>C</p>ˇ
15670 "#
15671 .unindent(),
15672 );
15673 cx.update_editor(|editor, window, cx| {
15674 editor.toggle_comments(&ToggleComments::default(), window, cx)
15675 });
15676 cx.assert_editor_state(
15677 &r#"
15678 <!-- <p>A</p>ˇ -->
15679 <!-- <p>B</p>ˇ -->
15680 <!-- <p>C</p>ˇ -->
15681 "#
15682 .unindent(),
15683 );
15684 cx.update_editor(|editor, window, cx| {
15685 editor.toggle_comments(&ToggleComments::default(), window, cx)
15686 });
15687 cx.assert_editor_state(
15688 &r#"
15689 <p>A</p>ˇ
15690 <p>B</p>ˇ
15691 <p>C</p>ˇ
15692 "#
15693 .unindent(),
15694 );
15695
15696 // Toggle comments for mixture of empty and non-empty selections, where
15697 // multiple selections occupy a given line.
15698 cx.set_state(
15699 &r#"
15700 <p>A«</p>
15701 <p>ˇ»B</p>ˇ
15702 <p>C«</p>
15703 <p>ˇ»D</p>ˇ
15704 "#
15705 .unindent(),
15706 );
15707
15708 cx.update_editor(|editor, window, cx| {
15709 editor.toggle_comments(&ToggleComments::default(), window, cx)
15710 });
15711 cx.assert_editor_state(
15712 &r#"
15713 <!-- <p>A«</p>
15714 <p>ˇ»B</p>ˇ -->
15715 <!-- <p>C«</p>
15716 <p>ˇ»D</p>ˇ -->
15717 "#
15718 .unindent(),
15719 );
15720 cx.update_editor(|editor, window, cx| {
15721 editor.toggle_comments(&ToggleComments::default(), window, cx)
15722 });
15723 cx.assert_editor_state(
15724 &r#"
15725 <p>A«</p>
15726 <p>ˇ»B</p>ˇ
15727 <p>C«</p>
15728 <p>ˇ»D</p>ˇ
15729 "#
15730 .unindent(),
15731 );
15732
15733 // Toggle comments when different languages are active for different
15734 // selections.
15735 cx.set_state(
15736 &r#"
15737 ˇ<script>
15738 ˇvar x = new Y();
15739 ˇ</script>
15740 "#
15741 .unindent(),
15742 );
15743 cx.executor().run_until_parked();
15744 cx.update_editor(|editor, window, cx| {
15745 editor.toggle_comments(&ToggleComments::default(), window, cx)
15746 });
15747 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
15748 // Uncommenting and commenting from this position brings in even more wrong artifacts.
15749 cx.assert_editor_state(
15750 &r#"
15751 <!-- ˇ<script> -->
15752 // ˇvar x = new Y();
15753 <!-- ˇ</script> -->
15754 "#
15755 .unindent(),
15756 );
15757}
15758
15759#[gpui::test]
15760fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
15761 init_test(cx, |_| {});
15762
15763 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
15764 let multibuffer = cx.new(|cx| {
15765 let mut multibuffer = MultiBuffer::new(ReadWrite);
15766 multibuffer.push_excerpts(
15767 buffer.clone(),
15768 [
15769 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
15770 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
15771 ],
15772 cx,
15773 );
15774 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
15775 multibuffer
15776 });
15777
15778 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
15779 editor.update_in(cx, |editor, window, cx| {
15780 assert_eq!(editor.text(cx), "aaaa\nbbbb");
15781 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15782 s.select_ranges([
15783 Point::new(0, 0)..Point::new(0, 0),
15784 Point::new(1, 0)..Point::new(1, 0),
15785 ])
15786 });
15787
15788 editor.handle_input("X", window, cx);
15789 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
15790 assert_eq!(
15791 editor.selections.ranges(cx),
15792 [
15793 Point::new(0, 1)..Point::new(0, 1),
15794 Point::new(1, 1)..Point::new(1, 1),
15795 ]
15796 );
15797
15798 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
15799 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15800 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
15801 });
15802 editor.backspace(&Default::default(), window, cx);
15803 assert_eq!(editor.text(cx), "Xa\nbbb");
15804 assert_eq!(
15805 editor.selections.ranges(cx),
15806 [Point::new(1, 0)..Point::new(1, 0)]
15807 );
15808
15809 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15810 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
15811 });
15812 editor.backspace(&Default::default(), window, cx);
15813 assert_eq!(editor.text(cx), "X\nbb");
15814 assert_eq!(
15815 editor.selections.ranges(cx),
15816 [Point::new(0, 1)..Point::new(0, 1)]
15817 );
15818 });
15819}
15820
15821#[gpui::test]
15822fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
15823 init_test(cx, |_| {});
15824
15825 let markers = vec![('[', ']').into(), ('(', ')').into()];
15826 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
15827 indoc! {"
15828 [aaaa
15829 (bbbb]
15830 cccc)",
15831 },
15832 markers.clone(),
15833 );
15834 let excerpt_ranges = markers.into_iter().map(|marker| {
15835 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
15836 ExcerptRange::new(context)
15837 });
15838 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
15839 let multibuffer = cx.new(|cx| {
15840 let mut multibuffer = MultiBuffer::new(ReadWrite);
15841 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
15842 multibuffer
15843 });
15844
15845 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
15846 editor.update_in(cx, |editor, window, cx| {
15847 let (expected_text, selection_ranges) = marked_text_ranges(
15848 indoc! {"
15849 aaaa
15850 bˇbbb
15851 bˇbbˇb
15852 cccc"
15853 },
15854 true,
15855 );
15856 assert_eq!(editor.text(cx), expected_text);
15857 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15858 s.select_ranges(selection_ranges)
15859 });
15860
15861 editor.handle_input("X", window, cx);
15862
15863 let (expected_text, expected_selections) = marked_text_ranges(
15864 indoc! {"
15865 aaaa
15866 bXˇbbXb
15867 bXˇbbXˇb
15868 cccc"
15869 },
15870 false,
15871 );
15872 assert_eq!(editor.text(cx), expected_text);
15873 assert_eq!(editor.selections.ranges(cx), expected_selections);
15874
15875 editor.newline(&Newline, window, cx);
15876 let (expected_text, expected_selections) = marked_text_ranges(
15877 indoc! {"
15878 aaaa
15879 bX
15880 ˇbbX
15881 b
15882 bX
15883 ˇbbX
15884 ˇb
15885 cccc"
15886 },
15887 false,
15888 );
15889 assert_eq!(editor.text(cx), expected_text);
15890 assert_eq!(editor.selections.ranges(cx), expected_selections);
15891 });
15892}
15893
15894#[gpui::test]
15895fn test_refresh_selections(cx: &mut TestAppContext) {
15896 init_test(cx, |_| {});
15897
15898 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
15899 let mut excerpt1_id = None;
15900 let multibuffer = cx.new(|cx| {
15901 let mut multibuffer = MultiBuffer::new(ReadWrite);
15902 excerpt1_id = multibuffer
15903 .push_excerpts(
15904 buffer.clone(),
15905 [
15906 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
15907 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
15908 ],
15909 cx,
15910 )
15911 .into_iter()
15912 .next();
15913 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
15914 multibuffer
15915 });
15916
15917 let editor = cx.add_window(|window, cx| {
15918 let mut editor = build_editor(multibuffer.clone(), window, cx);
15919 let snapshot = editor.snapshot(window, cx);
15920 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15921 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
15922 });
15923 editor.begin_selection(
15924 Point::new(2, 1).to_display_point(&snapshot),
15925 true,
15926 1,
15927 window,
15928 cx,
15929 );
15930 assert_eq!(
15931 editor.selections.ranges(cx),
15932 [
15933 Point::new(1, 3)..Point::new(1, 3),
15934 Point::new(2, 1)..Point::new(2, 1),
15935 ]
15936 );
15937 editor
15938 });
15939
15940 // Refreshing selections is a no-op when excerpts haven't changed.
15941 _ = editor.update(cx, |editor, window, cx| {
15942 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
15943 assert_eq!(
15944 editor.selections.ranges(cx),
15945 [
15946 Point::new(1, 3)..Point::new(1, 3),
15947 Point::new(2, 1)..Point::new(2, 1),
15948 ]
15949 );
15950 });
15951
15952 multibuffer.update(cx, |multibuffer, cx| {
15953 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
15954 });
15955 _ = editor.update(cx, |editor, window, cx| {
15956 // Removing an excerpt causes the first selection to become degenerate.
15957 assert_eq!(
15958 editor.selections.ranges(cx),
15959 [
15960 Point::new(0, 0)..Point::new(0, 0),
15961 Point::new(0, 1)..Point::new(0, 1)
15962 ]
15963 );
15964
15965 // Refreshing selections will relocate the first selection to the original buffer
15966 // location.
15967 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
15968 assert_eq!(
15969 editor.selections.ranges(cx),
15970 [
15971 Point::new(0, 1)..Point::new(0, 1),
15972 Point::new(0, 3)..Point::new(0, 3)
15973 ]
15974 );
15975 assert!(editor.selections.pending_anchor().is_some());
15976 });
15977}
15978
15979#[gpui::test]
15980fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
15981 init_test(cx, |_| {});
15982
15983 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
15984 let mut excerpt1_id = None;
15985 let multibuffer = cx.new(|cx| {
15986 let mut multibuffer = MultiBuffer::new(ReadWrite);
15987 excerpt1_id = multibuffer
15988 .push_excerpts(
15989 buffer.clone(),
15990 [
15991 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
15992 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
15993 ],
15994 cx,
15995 )
15996 .into_iter()
15997 .next();
15998 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
15999 multibuffer
16000 });
16001
16002 let editor = cx.add_window(|window, cx| {
16003 let mut editor = build_editor(multibuffer.clone(), window, cx);
16004 let snapshot = editor.snapshot(window, cx);
16005 editor.begin_selection(
16006 Point::new(1, 3).to_display_point(&snapshot),
16007 false,
16008 1,
16009 window,
16010 cx,
16011 );
16012 assert_eq!(
16013 editor.selections.ranges(cx),
16014 [Point::new(1, 3)..Point::new(1, 3)]
16015 );
16016 editor
16017 });
16018
16019 multibuffer.update(cx, |multibuffer, cx| {
16020 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
16021 });
16022 _ = editor.update(cx, |editor, window, cx| {
16023 assert_eq!(
16024 editor.selections.ranges(cx),
16025 [Point::new(0, 0)..Point::new(0, 0)]
16026 );
16027
16028 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
16029 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16030 assert_eq!(
16031 editor.selections.ranges(cx),
16032 [Point::new(0, 3)..Point::new(0, 3)]
16033 );
16034 assert!(editor.selections.pending_anchor().is_some());
16035 });
16036}
16037
16038#[gpui::test]
16039async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
16040 init_test(cx, |_| {});
16041
16042 let language = Arc::new(
16043 Language::new(
16044 LanguageConfig {
16045 brackets: BracketPairConfig {
16046 pairs: vec![
16047 BracketPair {
16048 start: "{".to_string(),
16049 end: "}".to_string(),
16050 close: true,
16051 surround: true,
16052 newline: true,
16053 },
16054 BracketPair {
16055 start: "/* ".to_string(),
16056 end: " */".to_string(),
16057 close: true,
16058 surround: true,
16059 newline: true,
16060 },
16061 ],
16062 ..Default::default()
16063 },
16064 ..Default::default()
16065 },
16066 Some(tree_sitter_rust::LANGUAGE.into()),
16067 )
16068 .with_indents_query("")
16069 .unwrap(),
16070 );
16071
16072 let text = concat!(
16073 "{ }\n", //
16074 " x\n", //
16075 " /* */\n", //
16076 "x\n", //
16077 "{{} }\n", //
16078 );
16079
16080 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
16081 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
16082 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
16083 editor
16084 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
16085 .await;
16086
16087 editor.update_in(cx, |editor, window, cx| {
16088 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16089 s.select_display_ranges([
16090 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
16091 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
16092 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
16093 ])
16094 });
16095 editor.newline(&Newline, window, cx);
16096
16097 assert_eq!(
16098 editor.buffer().read(cx).read(cx).text(),
16099 concat!(
16100 "{ \n", // Suppress rustfmt
16101 "\n", //
16102 "}\n", //
16103 " x\n", //
16104 " /* \n", //
16105 " \n", //
16106 " */\n", //
16107 "x\n", //
16108 "{{} \n", //
16109 "}\n", //
16110 )
16111 );
16112 });
16113}
16114
16115#[gpui::test]
16116fn test_highlighted_ranges(cx: &mut TestAppContext) {
16117 init_test(cx, |_| {});
16118
16119 let editor = cx.add_window(|window, cx| {
16120 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
16121 build_editor(buffer, window, cx)
16122 });
16123
16124 _ = editor.update(cx, |editor, window, cx| {
16125 struct Type1;
16126 struct Type2;
16127
16128 let buffer = editor.buffer.read(cx).snapshot(cx);
16129
16130 let anchor_range =
16131 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
16132
16133 editor.highlight_background::<Type1>(
16134 &[
16135 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
16136 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
16137 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
16138 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
16139 ],
16140 |_| Hsla::red(),
16141 cx,
16142 );
16143 editor.highlight_background::<Type2>(
16144 &[
16145 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
16146 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
16147 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
16148 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
16149 ],
16150 |_| Hsla::green(),
16151 cx,
16152 );
16153
16154 let snapshot = editor.snapshot(window, cx);
16155 let highlighted_ranges = editor.sorted_background_highlights_in_range(
16156 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
16157 &snapshot,
16158 cx.theme(),
16159 );
16160 assert_eq!(
16161 highlighted_ranges,
16162 &[
16163 (
16164 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
16165 Hsla::green(),
16166 ),
16167 (
16168 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
16169 Hsla::red(),
16170 ),
16171 (
16172 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
16173 Hsla::green(),
16174 ),
16175 (
16176 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16177 Hsla::red(),
16178 ),
16179 ]
16180 );
16181 assert_eq!(
16182 editor.sorted_background_highlights_in_range(
16183 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
16184 &snapshot,
16185 cx.theme(),
16186 ),
16187 &[(
16188 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16189 Hsla::red(),
16190 )]
16191 );
16192 });
16193}
16194
16195#[gpui::test]
16196async fn test_following(cx: &mut TestAppContext) {
16197 init_test(cx, |_| {});
16198
16199 let fs = FakeFs::new(cx.executor());
16200 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16201
16202 let buffer = project.update(cx, |project, cx| {
16203 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, false, cx);
16204 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
16205 });
16206 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
16207 let follower = cx.update(|cx| {
16208 cx.open_window(
16209 WindowOptions {
16210 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
16211 gpui::Point::new(px(0.), px(0.)),
16212 gpui::Point::new(px(10.), px(80.)),
16213 ))),
16214 ..Default::default()
16215 },
16216 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
16217 )
16218 .unwrap()
16219 });
16220
16221 let is_still_following = Rc::new(RefCell::new(true));
16222 let follower_edit_event_count = Rc::new(RefCell::new(0));
16223 let pending_update = Rc::new(RefCell::new(None));
16224 let leader_entity = leader.root(cx).unwrap();
16225 let follower_entity = follower.root(cx).unwrap();
16226 _ = follower.update(cx, {
16227 let update = pending_update.clone();
16228 let is_still_following = is_still_following.clone();
16229 let follower_edit_event_count = follower_edit_event_count.clone();
16230 |_, window, cx| {
16231 cx.subscribe_in(
16232 &leader_entity,
16233 window,
16234 move |_, leader, event, window, cx| {
16235 leader.read(cx).add_event_to_update_proto(
16236 event,
16237 &mut update.borrow_mut(),
16238 window,
16239 cx,
16240 );
16241 },
16242 )
16243 .detach();
16244
16245 cx.subscribe_in(
16246 &follower_entity,
16247 window,
16248 move |_, _, event: &EditorEvent, _window, _cx| {
16249 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
16250 *is_still_following.borrow_mut() = false;
16251 }
16252
16253 if let EditorEvent::BufferEdited = event {
16254 *follower_edit_event_count.borrow_mut() += 1;
16255 }
16256 },
16257 )
16258 .detach();
16259 }
16260 });
16261
16262 // Update the selections only
16263 _ = leader.update(cx, |leader, window, cx| {
16264 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16265 s.select_ranges([1..1])
16266 });
16267 });
16268 follower
16269 .update(cx, |follower, window, cx| {
16270 follower.apply_update_proto(
16271 &project,
16272 pending_update.borrow_mut().take().unwrap(),
16273 window,
16274 cx,
16275 )
16276 })
16277 .unwrap()
16278 .await
16279 .unwrap();
16280 _ = follower.update(cx, |follower, _, cx| {
16281 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
16282 });
16283 assert!(*is_still_following.borrow());
16284 assert_eq!(*follower_edit_event_count.borrow(), 0);
16285
16286 // Update the scroll position only
16287 _ = leader.update(cx, |leader, window, cx| {
16288 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16289 });
16290 follower
16291 .update(cx, |follower, window, cx| {
16292 follower.apply_update_proto(
16293 &project,
16294 pending_update.borrow_mut().take().unwrap(),
16295 window,
16296 cx,
16297 )
16298 })
16299 .unwrap()
16300 .await
16301 .unwrap();
16302 assert_eq!(
16303 follower
16304 .update(cx, |follower, _, cx| follower.scroll_position(cx))
16305 .unwrap(),
16306 gpui::Point::new(1.5, 3.5)
16307 );
16308 assert!(*is_still_following.borrow());
16309 assert_eq!(*follower_edit_event_count.borrow(), 0);
16310
16311 // Update the selections and scroll position. The follower's scroll position is updated
16312 // via autoscroll, not via the leader's exact scroll position.
16313 _ = leader.update(cx, |leader, window, cx| {
16314 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16315 s.select_ranges([0..0])
16316 });
16317 leader.request_autoscroll(Autoscroll::newest(), cx);
16318 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16319 });
16320 follower
16321 .update(cx, |follower, window, cx| {
16322 follower.apply_update_proto(
16323 &project,
16324 pending_update.borrow_mut().take().unwrap(),
16325 window,
16326 cx,
16327 )
16328 })
16329 .unwrap()
16330 .await
16331 .unwrap();
16332 _ = follower.update(cx, |follower, _, cx| {
16333 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
16334 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
16335 });
16336 assert!(*is_still_following.borrow());
16337
16338 // Creating a pending selection that precedes another selection
16339 _ = leader.update(cx, |leader, window, cx| {
16340 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16341 s.select_ranges([1..1])
16342 });
16343 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
16344 });
16345 follower
16346 .update(cx, |follower, window, cx| {
16347 follower.apply_update_proto(
16348 &project,
16349 pending_update.borrow_mut().take().unwrap(),
16350 window,
16351 cx,
16352 )
16353 })
16354 .unwrap()
16355 .await
16356 .unwrap();
16357 _ = follower.update(cx, |follower, _, cx| {
16358 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
16359 });
16360 assert!(*is_still_following.borrow());
16361
16362 // Extend the pending selection so that it surrounds another selection
16363 _ = leader.update(cx, |leader, window, cx| {
16364 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
16365 });
16366 follower
16367 .update(cx, |follower, window, cx| {
16368 follower.apply_update_proto(
16369 &project,
16370 pending_update.borrow_mut().take().unwrap(),
16371 window,
16372 cx,
16373 )
16374 })
16375 .unwrap()
16376 .await
16377 .unwrap();
16378 _ = follower.update(cx, |follower, _, cx| {
16379 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
16380 });
16381
16382 // Scrolling locally breaks the follow
16383 _ = follower.update(cx, |follower, window, cx| {
16384 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
16385 follower.set_scroll_anchor(
16386 ScrollAnchor {
16387 anchor: top_anchor,
16388 offset: gpui::Point::new(0.0, 0.5),
16389 },
16390 window,
16391 cx,
16392 );
16393 });
16394 assert!(!(*is_still_following.borrow()));
16395}
16396
16397#[gpui::test]
16398async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
16399 init_test(cx, |_| {});
16400
16401 let fs = FakeFs::new(cx.executor());
16402 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16403 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16404 let pane = workspace
16405 .update(cx, |workspace, _, _| workspace.active_pane().clone())
16406 .unwrap();
16407
16408 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16409
16410 let leader = pane.update_in(cx, |_, window, cx| {
16411 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
16412 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
16413 });
16414
16415 // Start following the editor when it has no excerpts.
16416 let mut state_message =
16417 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16418 let workspace_entity = workspace.root(cx).unwrap();
16419 let follower_1 = cx
16420 .update_window(*workspace.deref(), |_, window, cx| {
16421 Editor::from_state_proto(
16422 workspace_entity,
16423 ViewId {
16424 creator: CollaboratorId::PeerId(PeerId::default()),
16425 id: 0,
16426 },
16427 &mut state_message,
16428 window,
16429 cx,
16430 )
16431 })
16432 .unwrap()
16433 .unwrap()
16434 .await
16435 .unwrap();
16436
16437 let update_message = Rc::new(RefCell::new(None));
16438 follower_1.update_in(cx, {
16439 let update = update_message.clone();
16440 |_, window, cx| {
16441 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
16442 leader.read(cx).add_event_to_update_proto(
16443 event,
16444 &mut update.borrow_mut(),
16445 window,
16446 cx,
16447 );
16448 })
16449 .detach();
16450 }
16451 });
16452
16453 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
16454 (
16455 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, false, cx),
16456 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, false, cx),
16457 )
16458 });
16459
16460 // Insert some excerpts.
16461 leader.update(cx, |leader, cx| {
16462 leader.buffer.update(cx, |multibuffer, cx| {
16463 multibuffer.set_excerpts_for_path(
16464 PathKey::namespaced(1, "b.txt".into()),
16465 buffer_1.clone(),
16466 vec![
16467 Point::row_range(0..3),
16468 Point::row_range(1..6),
16469 Point::row_range(12..15),
16470 ],
16471 0,
16472 cx,
16473 );
16474 multibuffer.set_excerpts_for_path(
16475 PathKey::namespaced(1, "a.txt".into()),
16476 buffer_2.clone(),
16477 vec![Point::row_range(0..6), Point::row_range(8..12)],
16478 0,
16479 cx,
16480 );
16481 });
16482 });
16483
16484 // Apply the update of adding the excerpts.
16485 follower_1
16486 .update_in(cx, |follower, window, cx| {
16487 follower.apply_update_proto(
16488 &project,
16489 update_message.borrow().clone().unwrap(),
16490 window,
16491 cx,
16492 )
16493 })
16494 .await
16495 .unwrap();
16496 assert_eq!(
16497 follower_1.update(cx, |editor, cx| editor.text(cx)),
16498 leader.update(cx, |editor, cx| editor.text(cx))
16499 );
16500 update_message.borrow_mut().take();
16501
16502 // Start following separately after it already has excerpts.
16503 let mut state_message =
16504 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16505 let workspace_entity = workspace.root(cx).unwrap();
16506 let follower_2 = cx
16507 .update_window(*workspace.deref(), |_, window, cx| {
16508 Editor::from_state_proto(
16509 workspace_entity,
16510 ViewId {
16511 creator: CollaboratorId::PeerId(PeerId::default()),
16512 id: 0,
16513 },
16514 &mut state_message,
16515 window,
16516 cx,
16517 )
16518 })
16519 .unwrap()
16520 .unwrap()
16521 .await
16522 .unwrap();
16523 assert_eq!(
16524 follower_2.update(cx, |editor, cx| editor.text(cx)),
16525 leader.update(cx, |editor, cx| editor.text(cx))
16526 );
16527
16528 // Remove some excerpts.
16529 leader.update(cx, |leader, cx| {
16530 leader.buffer.update(cx, |multibuffer, cx| {
16531 let excerpt_ids = multibuffer.excerpt_ids();
16532 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
16533 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
16534 });
16535 });
16536
16537 // Apply the update of removing the excerpts.
16538 follower_1
16539 .update_in(cx, |follower, window, cx| {
16540 follower.apply_update_proto(
16541 &project,
16542 update_message.borrow().clone().unwrap(),
16543 window,
16544 cx,
16545 )
16546 })
16547 .await
16548 .unwrap();
16549 follower_2
16550 .update_in(cx, |follower, window, cx| {
16551 follower.apply_update_proto(
16552 &project,
16553 update_message.borrow().clone().unwrap(),
16554 window,
16555 cx,
16556 )
16557 })
16558 .await
16559 .unwrap();
16560 update_message.borrow_mut().take();
16561 assert_eq!(
16562 follower_1.update(cx, |editor, cx| editor.text(cx)),
16563 leader.update(cx, |editor, cx| editor.text(cx))
16564 );
16565}
16566
16567#[gpui::test]
16568async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16569 init_test(cx, |_| {});
16570
16571 let mut cx = EditorTestContext::new(cx).await;
16572 let lsp_store =
16573 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
16574
16575 cx.set_state(indoc! {"
16576 ˇfn func(abc def: i32) -> u32 {
16577 }
16578 "});
16579
16580 cx.update(|_, cx| {
16581 lsp_store.update(cx, |lsp_store, cx| {
16582 lsp_store
16583 .update_diagnostics(
16584 LanguageServerId(0),
16585 lsp::PublishDiagnosticsParams {
16586 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
16587 version: None,
16588 diagnostics: vec![
16589 lsp::Diagnostic {
16590 range: lsp::Range::new(
16591 lsp::Position::new(0, 11),
16592 lsp::Position::new(0, 12),
16593 ),
16594 severity: Some(lsp::DiagnosticSeverity::ERROR),
16595 ..Default::default()
16596 },
16597 lsp::Diagnostic {
16598 range: lsp::Range::new(
16599 lsp::Position::new(0, 12),
16600 lsp::Position::new(0, 15),
16601 ),
16602 severity: Some(lsp::DiagnosticSeverity::ERROR),
16603 ..Default::default()
16604 },
16605 lsp::Diagnostic {
16606 range: lsp::Range::new(
16607 lsp::Position::new(0, 25),
16608 lsp::Position::new(0, 28),
16609 ),
16610 severity: Some(lsp::DiagnosticSeverity::ERROR),
16611 ..Default::default()
16612 },
16613 ],
16614 },
16615 None,
16616 DiagnosticSourceKind::Pushed,
16617 &[],
16618 cx,
16619 )
16620 .unwrap()
16621 });
16622 });
16623
16624 executor.run_until_parked();
16625
16626 cx.update_editor(|editor, window, cx| {
16627 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16628 });
16629
16630 cx.assert_editor_state(indoc! {"
16631 fn func(abc def: i32) -> ˇu32 {
16632 }
16633 "});
16634
16635 cx.update_editor(|editor, window, cx| {
16636 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16637 });
16638
16639 cx.assert_editor_state(indoc! {"
16640 fn func(abc ˇdef: i32) -> u32 {
16641 }
16642 "});
16643
16644 cx.update_editor(|editor, window, cx| {
16645 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16646 });
16647
16648 cx.assert_editor_state(indoc! {"
16649 fn func(abcˇ def: i32) -> u32 {
16650 }
16651 "});
16652
16653 cx.update_editor(|editor, window, cx| {
16654 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16655 });
16656
16657 cx.assert_editor_state(indoc! {"
16658 fn func(abc def: i32) -> ˇu32 {
16659 }
16660 "});
16661}
16662
16663#[gpui::test]
16664async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16665 init_test(cx, |_| {});
16666
16667 let mut cx = EditorTestContext::new(cx).await;
16668
16669 let diff_base = r#"
16670 use some::mod;
16671
16672 const A: u32 = 42;
16673
16674 fn main() {
16675 println!("hello");
16676
16677 println!("world");
16678 }
16679 "#
16680 .unindent();
16681
16682 // Edits are modified, removed, modified, added
16683 cx.set_state(
16684 &r#"
16685 use some::modified;
16686
16687 ˇ
16688 fn main() {
16689 println!("hello there");
16690
16691 println!("around the");
16692 println!("world");
16693 }
16694 "#
16695 .unindent(),
16696 );
16697
16698 cx.set_head_text(&diff_base);
16699 executor.run_until_parked();
16700
16701 cx.update_editor(|editor, window, cx| {
16702 //Wrap around the bottom of the buffer
16703 for _ in 0..3 {
16704 editor.go_to_next_hunk(&GoToHunk, window, cx);
16705 }
16706 });
16707
16708 cx.assert_editor_state(
16709 &r#"
16710 ˇuse some::modified;
16711
16712
16713 fn main() {
16714 println!("hello there");
16715
16716 println!("around the");
16717 println!("world");
16718 }
16719 "#
16720 .unindent(),
16721 );
16722
16723 cx.update_editor(|editor, window, cx| {
16724 //Wrap around the top of the buffer
16725 for _ in 0..2 {
16726 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16727 }
16728 });
16729
16730 cx.assert_editor_state(
16731 &r#"
16732 use some::modified;
16733
16734
16735 fn main() {
16736 ˇ println!("hello there");
16737
16738 println!("around the");
16739 println!("world");
16740 }
16741 "#
16742 .unindent(),
16743 );
16744
16745 cx.update_editor(|editor, window, cx| {
16746 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16747 });
16748
16749 cx.assert_editor_state(
16750 &r#"
16751 use some::modified;
16752
16753 ˇ
16754 fn main() {
16755 println!("hello there");
16756
16757 println!("around the");
16758 println!("world");
16759 }
16760 "#
16761 .unindent(),
16762 );
16763
16764 cx.update_editor(|editor, window, cx| {
16765 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16766 });
16767
16768 cx.assert_editor_state(
16769 &r#"
16770 ˇuse some::modified;
16771
16772
16773 fn main() {
16774 println!("hello there");
16775
16776 println!("around the");
16777 println!("world");
16778 }
16779 "#
16780 .unindent(),
16781 );
16782
16783 cx.update_editor(|editor, window, cx| {
16784 for _ in 0..2 {
16785 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16786 }
16787 });
16788
16789 cx.assert_editor_state(
16790 &r#"
16791 use some::modified;
16792
16793
16794 fn main() {
16795 ˇ println!("hello there");
16796
16797 println!("around the");
16798 println!("world");
16799 }
16800 "#
16801 .unindent(),
16802 );
16803
16804 cx.update_editor(|editor, window, cx| {
16805 editor.fold(&Fold, window, cx);
16806 });
16807
16808 cx.update_editor(|editor, window, cx| {
16809 editor.go_to_next_hunk(&GoToHunk, window, cx);
16810 });
16811
16812 cx.assert_editor_state(
16813 &r#"
16814 ˇuse some::modified;
16815
16816
16817 fn main() {
16818 println!("hello there");
16819
16820 println!("around the");
16821 println!("world");
16822 }
16823 "#
16824 .unindent(),
16825 );
16826}
16827
16828#[test]
16829fn test_split_words() {
16830 fn split(text: &str) -> Vec<&str> {
16831 split_words(text).collect()
16832 }
16833
16834 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
16835 assert_eq!(split("hello_world"), &["hello_", "world"]);
16836 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
16837 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
16838 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
16839 assert_eq!(split("helloworld"), &["helloworld"]);
16840
16841 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
16842}
16843
16844#[gpui::test]
16845async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
16846 init_test(cx, |_| {});
16847
16848 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
16849 let mut assert = |before, after| {
16850 let _state_context = cx.set_state(before);
16851 cx.run_until_parked();
16852 cx.update_editor(|editor, window, cx| {
16853 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
16854 });
16855 cx.run_until_parked();
16856 cx.assert_editor_state(after);
16857 };
16858
16859 // Outside bracket jumps to outside of matching bracket
16860 assert("console.logˇ(var);", "console.log(var)ˇ;");
16861 assert("console.log(var)ˇ;", "console.logˇ(var);");
16862
16863 // Inside bracket jumps to inside of matching bracket
16864 assert("console.log(ˇvar);", "console.log(varˇ);");
16865 assert("console.log(varˇ);", "console.log(ˇvar);");
16866
16867 // When outside a bracket and inside, favor jumping to the inside bracket
16868 assert(
16869 "console.log('foo', [1, 2, 3]ˇ);",
16870 "console.log(ˇ'foo', [1, 2, 3]);",
16871 );
16872 assert(
16873 "console.log(ˇ'foo', [1, 2, 3]);",
16874 "console.log('foo', [1, 2, 3]ˇ);",
16875 );
16876
16877 // Bias forward if two options are equally likely
16878 assert(
16879 "let result = curried_fun()ˇ();",
16880 "let result = curried_fun()()ˇ;",
16881 );
16882
16883 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
16884 assert(
16885 indoc! {"
16886 function test() {
16887 console.log('test')ˇ
16888 }"},
16889 indoc! {"
16890 function test() {
16891 console.logˇ('test')
16892 }"},
16893 );
16894}
16895
16896#[gpui::test]
16897async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
16898 init_test(cx, |_| {});
16899
16900 let fs = FakeFs::new(cx.executor());
16901 fs.insert_tree(
16902 path!("/a"),
16903 json!({
16904 "main.rs": "fn main() { let a = 5; }",
16905 "other.rs": "// Test file",
16906 }),
16907 )
16908 .await;
16909 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
16910
16911 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
16912 language_registry.add(Arc::new(Language::new(
16913 LanguageConfig {
16914 name: "Rust".into(),
16915 matcher: LanguageMatcher {
16916 path_suffixes: vec!["rs".to_string()],
16917 ..Default::default()
16918 },
16919 brackets: BracketPairConfig {
16920 pairs: vec![BracketPair {
16921 start: "{".to_string(),
16922 end: "}".to_string(),
16923 close: true,
16924 surround: true,
16925 newline: true,
16926 }],
16927 disabled_scopes_by_bracket_ix: Vec::new(),
16928 },
16929 ..Default::default()
16930 },
16931 Some(tree_sitter_rust::LANGUAGE.into()),
16932 )));
16933 let mut fake_servers = language_registry.register_fake_lsp(
16934 "Rust",
16935 FakeLspAdapter {
16936 capabilities: lsp::ServerCapabilities {
16937 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
16938 first_trigger_character: "{".to_string(),
16939 more_trigger_character: None,
16940 }),
16941 ..Default::default()
16942 },
16943 ..Default::default()
16944 },
16945 );
16946
16947 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16948
16949 let cx = &mut VisualTestContext::from_window(*workspace, cx);
16950
16951 let worktree_id = workspace
16952 .update(cx, |workspace, _, cx| {
16953 workspace.project().update(cx, |project, cx| {
16954 project.worktrees(cx).next().unwrap().read(cx).id()
16955 })
16956 })
16957 .unwrap();
16958
16959 let buffer = project
16960 .update(cx, |project, cx| {
16961 project.open_local_buffer(path!("/a/main.rs"), cx)
16962 })
16963 .await
16964 .unwrap();
16965 let editor_handle = workspace
16966 .update(cx, |workspace, window, cx| {
16967 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
16968 })
16969 .unwrap()
16970 .await
16971 .unwrap()
16972 .downcast::<Editor>()
16973 .unwrap();
16974
16975 cx.executor().start_waiting();
16976 let fake_server = fake_servers.next().await.unwrap();
16977
16978 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
16979 |params, _| async move {
16980 assert_eq!(
16981 params.text_document_position.text_document.uri,
16982 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
16983 );
16984 assert_eq!(
16985 params.text_document_position.position,
16986 lsp::Position::new(0, 21),
16987 );
16988
16989 Ok(Some(vec![lsp::TextEdit {
16990 new_text: "]".to_string(),
16991 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
16992 }]))
16993 },
16994 );
16995
16996 editor_handle.update_in(cx, |editor, window, cx| {
16997 window.focus(&editor.focus_handle(cx));
16998 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16999 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
17000 });
17001 editor.handle_input("{", window, cx);
17002 });
17003
17004 cx.executor().run_until_parked();
17005
17006 buffer.update(cx, |buffer, _| {
17007 assert_eq!(
17008 buffer.text(),
17009 "fn main() { let a = {5}; }",
17010 "No extra braces from on type formatting should appear in the buffer"
17011 )
17012 });
17013}
17014
17015#[gpui::test(iterations = 20, seeds(31))]
17016async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
17017 init_test(cx, |_| {});
17018
17019 let mut cx = EditorLspTestContext::new_rust(
17020 lsp::ServerCapabilities {
17021 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
17022 first_trigger_character: ".".to_string(),
17023 more_trigger_character: None,
17024 }),
17025 ..Default::default()
17026 },
17027 cx,
17028 )
17029 .await;
17030
17031 cx.update_buffer(|buffer, _| {
17032 // This causes autoindent to be async.
17033 buffer.set_sync_parse_timeout(Duration::ZERO)
17034 });
17035
17036 cx.set_state("fn c() {\n d()ˇ\n}\n");
17037 cx.simulate_keystroke("\n");
17038 cx.run_until_parked();
17039
17040 let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap());
17041 let mut request =
17042 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
17043 let buffer_cloned = buffer_cloned.clone();
17044 async move {
17045 buffer_cloned.update(&mut cx, |buffer, _| {
17046 assert_eq!(
17047 buffer.text(),
17048 "fn c() {\n d()\n .\n}\n",
17049 "OnTypeFormatting should triggered after autoindent applied"
17050 )
17051 })?;
17052
17053 Ok(Some(vec![]))
17054 }
17055 });
17056
17057 cx.simulate_keystroke(".");
17058 cx.run_until_parked();
17059
17060 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
17061 assert!(request.next().await.is_some());
17062 request.close();
17063 assert!(request.next().await.is_none());
17064}
17065
17066#[gpui::test]
17067async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
17068 init_test(cx, |_| {});
17069
17070 let fs = FakeFs::new(cx.executor());
17071 fs.insert_tree(
17072 path!("/a"),
17073 json!({
17074 "main.rs": "fn main() { let a = 5; }",
17075 "other.rs": "// Test file",
17076 }),
17077 )
17078 .await;
17079
17080 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17081
17082 let server_restarts = Arc::new(AtomicUsize::new(0));
17083 let closure_restarts = Arc::clone(&server_restarts);
17084 let language_server_name = "test language server";
17085 let language_name: LanguageName = "Rust".into();
17086
17087 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17088 language_registry.add(Arc::new(Language::new(
17089 LanguageConfig {
17090 name: language_name.clone(),
17091 matcher: LanguageMatcher {
17092 path_suffixes: vec!["rs".to_string()],
17093 ..Default::default()
17094 },
17095 ..Default::default()
17096 },
17097 Some(tree_sitter_rust::LANGUAGE.into()),
17098 )));
17099 let mut fake_servers = language_registry.register_fake_lsp(
17100 "Rust",
17101 FakeLspAdapter {
17102 name: language_server_name,
17103 initialization_options: Some(json!({
17104 "testOptionValue": true
17105 })),
17106 initializer: Some(Box::new(move |fake_server| {
17107 let task_restarts = Arc::clone(&closure_restarts);
17108 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
17109 task_restarts.fetch_add(1, atomic::Ordering::Release);
17110 futures::future::ready(Ok(()))
17111 });
17112 })),
17113 ..Default::default()
17114 },
17115 );
17116
17117 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17118 let _buffer = project
17119 .update(cx, |project, cx| {
17120 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
17121 })
17122 .await
17123 .unwrap();
17124 let _fake_server = fake_servers.next().await.unwrap();
17125 update_test_language_settings(cx, |language_settings| {
17126 language_settings.languages.0.insert(
17127 language_name.clone().0,
17128 LanguageSettingsContent {
17129 tab_size: NonZeroU32::new(8),
17130 ..Default::default()
17131 },
17132 );
17133 });
17134 cx.executor().run_until_parked();
17135 assert_eq!(
17136 server_restarts.load(atomic::Ordering::Acquire),
17137 0,
17138 "Should not restart LSP server on an unrelated change"
17139 );
17140
17141 update_test_project_settings(cx, |project_settings| {
17142 project_settings.lsp.insert(
17143 "Some other server name".into(),
17144 LspSettings {
17145 binary: None,
17146 settings: None,
17147 initialization_options: Some(json!({
17148 "some other init value": false
17149 })),
17150 enable_lsp_tasks: false,
17151 fetch: None,
17152 },
17153 );
17154 });
17155 cx.executor().run_until_parked();
17156 assert_eq!(
17157 server_restarts.load(atomic::Ordering::Acquire),
17158 0,
17159 "Should not restart LSP server on an unrelated LSP settings change"
17160 );
17161
17162 update_test_project_settings(cx, |project_settings| {
17163 project_settings.lsp.insert(
17164 language_server_name.into(),
17165 LspSettings {
17166 binary: None,
17167 settings: None,
17168 initialization_options: Some(json!({
17169 "anotherInitValue": false
17170 })),
17171 enable_lsp_tasks: false,
17172 fetch: None,
17173 },
17174 );
17175 });
17176 cx.executor().run_until_parked();
17177 assert_eq!(
17178 server_restarts.load(atomic::Ordering::Acquire),
17179 1,
17180 "Should restart LSP server on a related LSP settings change"
17181 );
17182
17183 update_test_project_settings(cx, |project_settings| {
17184 project_settings.lsp.insert(
17185 language_server_name.into(),
17186 LspSettings {
17187 binary: None,
17188 settings: None,
17189 initialization_options: Some(json!({
17190 "anotherInitValue": false
17191 })),
17192 enable_lsp_tasks: false,
17193 fetch: None,
17194 },
17195 );
17196 });
17197 cx.executor().run_until_parked();
17198 assert_eq!(
17199 server_restarts.load(atomic::Ordering::Acquire),
17200 1,
17201 "Should not restart LSP server on a related LSP settings change that is the same"
17202 );
17203
17204 update_test_project_settings(cx, |project_settings| {
17205 project_settings.lsp.insert(
17206 language_server_name.into(),
17207 LspSettings {
17208 binary: None,
17209 settings: None,
17210 initialization_options: None,
17211 enable_lsp_tasks: false,
17212 fetch: None,
17213 },
17214 );
17215 });
17216 cx.executor().run_until_parked();
17217 assert_eq!(
17218 server_restarts.load(atomic::Ordering::Acquire),
17219 2,
17220 "Should restart LSP server on another related LSP settings change"
17221 );
17222}
17223
17224#[gpui::test]
17225async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
17226 init_test(cx, |_| {});
17227
17228 let mut cx = EditorLspTestContext::new_rust(
17229 lsp::ServerCapabilities {
17230 completion_provider: Some(lsp::CompletionOptions {
17231 trigger_characters: Some(vec![".".to_string()]),
17232 resolve_provider: Some(true),
17233 ..Default::default()
17234 }),
17235 ..Default::default()
17236 },
17237 cx,
17238 )
17239 .await;
17240
17241 cx.set_state("fn main() { let a = 2ˇ; }");
17242 cx.simulate_keystroke(".");
17243 let completion_item = lsp::CompletionItem {
17244 label: "some".into(),
17245 kind: Some(lsp::CompletionItemKind::SNIPPET),
17246 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
17247 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
17248 kind: lsp::MarkupKind::Markdown,
17249 value: "```rust\nSome(2)\n```".to_string(),
17250 })),
17251 deprecated: Some(false),
17252 sort_text: Some("fffffff2".to_string()),
17253 filter_text: Some("some".to_string()),
17254 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
17255 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17256 range: lsp::Range {
17257 start: lsp::Position {
17258 line: 0,
17259 character: 22,
17260 },
17261 end: lsp::Position {
17262 line: 0,
17263 character: 22,
17264 },
17265 },
17266 new_text: "Some(2)".to_string(),
17267 })),
17268 additional_text_edits: Some(vec![lsp::TextEdit {
17269 range: lsp::Range {
17270 start: lsp::Position {
17271 line: 0,
17272 character: 20,
17273 },
17274 end: lsp::Position {
17275 line: 0,
17276 character: 22,
17277 },
17278 },
17279 new_text: "".to_string(),
17280 }]),
17281 ..Default::default()
17282 };
17283
17284 let closure_completion_item = completion_item.clone();
17285 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17286 let task_completion_item = closure_completion_item.clone();
17287 async move {
17288 Ok(Some(lsp::CompletionResponse::Array(vec![
17289 task_completion_item,
17290 ])))
17291 }
17292 });
17293
17294 request.next().await;
17295
17296 cx.condition(|editor, _| editor.context_menu_visible())
17297 .await;
17298 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
17299 editor
17300 .confirm_completion(&ConfirmCompletion::default(), window, cx)
17301 .unwrap()
17302 });
17303 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
17304
17305 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
17306 let task_completion_item = completion_item.clone();
17307 async move { Ok(task_completion_item) }
17308 })
17309 .next()
17310 .await
17311 .unwrap();
17312 apply_additional_edits.await.unwrap();
17313 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
17314}
17315
17316#[gpui::test]
17317async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
17318 init_test(cx, |_| {});
17319
17320 let mut cx = EditorLspTestContext::new_rust(
17321 lsp::ServerCapabilities {
17322 completion_provider: Some(lsp::CompletionOptions {
17323 trigger_characters: Some(vec![".".to_string()]),
17324 resolve_provider: Some(true),
17325 ..Default::default()
17326 }),
17327 ..Default::default()
17328 },
17329 cx,
17330 )
17331 .await;
17332
17333 cx.set_state("fn main() { let a = 2ˇ; }");
17334 cx.simulate_keystroke(".");
17335
17336 let item1 = lsp::CompletionItem {
17337 label: "method id()".to_string(),
17338 filter_text: Some("id".to_string()),
17339 detail: None,
17340 documentation: None,
17341 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17342 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17343 new_text: ".id".to_string(),
17344 })),
17345 ..lsp::CompletionItem::default()
17346 };
17347
17348 let item2 = lsp::CompletionItem {
17349 label: "other".to_string(),
17350 filter_text: Some("other".to_string()),
17351 detail: None,
17352 documentation: None,
17353 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17354 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17355 new_text: ".other".to_string(),
17356 })),
17357 ..lsp::CompletionItem::default()
17358 };
17359
17360 let item1 = item1.clone();
17361 cx.set_request_handler::<lsp::request::Completion, _, _>({
17362 let item1 = item1.clone();
17363 move |_, _, _| {
17364 let item1 = item1.clone();
17365 let item2 = item2.clone();
17366 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
17367 }
17368 })
17369 .next()
17370 .await;
17371
17372 cx.condition(|editor, _| editor.context_menu_visible())
17373 .await;
17374 cx.update_editor(|editor, _, _| {
17375 let context_menu = editor.context_menu.borrow_mut();
17376 let context_menu = context_menu
17377 .as_ref()
17378 .expect("Should have the context menu deployed");
17379 match context_menu {
17380 CodeContextMenu::Completions(completions_menu) => {
17381 let completions = completions_menu.completions.borrow_mut();
17382 assert_eq!(
17383 completions
17384 .iter()
17385 .map(|completion| &completion.label.text)
17386 .collect::<Vec<_>>(),
17387 vec!["method id()", "other"]
17388 )
17389 }
17390 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17391 }
17392 });
17393
17394 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
17395 let item1 = item1.clone();
17396 move |_, item_to_resolve, _| {
17397 let item1 = item1.clone();
17398 async move {
17399 if item1 == item_to_resolve {
17400 Ok(lsp::CompletionItem {
17401 label: "method id()".to_string(),
17402 filter_text: Some("id".to_string()),
17403 detail: Some("Now resolved!".to_string()),
17404 documentation: Some(lsp::Documentation::String("Docs".to_string())),
17405 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17406 range: lsp::Range::new(
17407 lsp::Position::new(0, 22),
17408 lsp::Position::new(0, 22),
17409 ),
17410 new_text: ".id".to_string(),
17411 })),
17412 ..lsp::CompletionItem::default()
17413 })
17414 } else {
17415 Ok(item_to_resolve)
17416 }
17417 }
17418 }
17419 })
17420 .next()
17421 .await
17422 .unwrap();
17423 cx.run_until_parked();
17424
17425 cx.update_editor(|editor, window, cx| {
17426 editor.context_menu_next(&Default::default(), window, cx);
17427 });
17428
17429 cx.update_editor(|editor, _, _| {
17430 let context_menu = editor.context_menu.borrow_mut();
17431 let context_menu = context_menu
17432 .as_ref()
17433 .expect("Should have the context menu deployed");
17434 match context_menu {
17435 CodeContextMenu::Completions(completions_menu) => {
17436 let completions = completions_menu.completions.borrow_mut();
17437 assert_eq!(
17438 completions
17439 .iter()
17440 .map(|completion| &completion.label.text)
17441 .collect::<Vec<_>>(),
17442 vec!["method id() Now resolved!", "other"],
17443 "Should update first completion label, but not second as the filter text did not match."
17444 );
17445 }
17446 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17447 }
17448 });
17449}
17450
17451#[gpui::test]
17452async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
17453 init_test(cx, |_| {});
17454 let mut cx = EditorLspTestContext::new_rust(
17455 lsp::ServerCapabilities {
17456 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
17457 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
17458 completion_provider: Some(lsp::CompletionOptions {
17459 resolve_provider: Some(true),
17460 ..Default::default()
17461 }),
17462 ..Default::default()
17463 },
17464 cx,
17465 )
17466 .await;
17467 cx.set_state(indoc! {"
17468 struct TestStruct {
17469 field: i32
17470 }
17471
17472 fn mainˇ() {
17473 let unused_var = 42;
17474 let test_struct = TestStruct { field: 42 };
17475 }
17476 "});
17477 let symbol_range = cx.lsp_range(indoc! {"
17478 struct TestStruct {
17479 field: i32
17480 }
17481
17482 «fn main»() {
17483 let unused_var = 42;
17484 let test_struct = TestStruct { field: 42 };
17485 }
17486 "});
17487 let mut hover_requests =
17488 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
17489 Ok(Some(lsp::Hover {
17490 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
17491 kind: lsp::MarkupKind::Markdown,
17492 value: "Function documentation".to_string(),
17493 }),
17494 range: Some(symbol_range),
17495 }))
17496 });
17497
17498 // Case 1: Test that code action menu hide hover popover
17499 cx.dispatch_action(Hover);
17500 hover_requests.next().await;
17501 cx.condition(|editor, _| editor.hover_state.visible()).await;
17502 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
17503 move |_, _, _| async move {
17504 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
17505 lsp::CodeAction {
17506 title: "Remove unused variable".to_string(),
17507 kind: Some(CodeActionKind::QUICKFIX),
17508 edit: Some(lsp::WorkspaceEdit {
17509 changes: Some(
17510 [(
17511 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
17512 vec![lsp::TextEdit {
17513 range: lsp::Range::new(
17514 lsp::Position::new(5, 4),
17515 lsp::Position::new(5, 27),
17516 ),
17517 new_text: "".to_string(),
17518 }],
17519 )]
17520 .into_iter()
17521 .collect(),
17522 ),
17523 ..Default::default()
17524 }),
17525 ..Default::default()
17526 },
17527 )]))
17528 },
17529 );
17530 cx.update_editor(|editor, window, cx| {
17531 editor.toggle_code_actions(
17532 &ToggleCodeActions {
17533 deployed_from: None,
17534 quick_launch: false,
17535 },
17536 window,
17537 cx,
17538 );
17539 });
17540 code_action_requests.next().await;
17541 cx.run_until_parked();
17542 cx.condition(|editor, _| editor.context_menu_visible())
17543 .await;
17544 cx.update_editor(|editor, _, _| {
17545 assert!(
17546 !editor.hover_state.visible(),
17547 "Hover popover should be hidden when code action menu is shown"
17548 );
17549 // Hide code actions
17550 editor.context_menu.take();
17551 });
17552
17553 // Case 2: Test that code completions hide hover popover
17554 cx.dispatch_action(Hover);
17555 hover_requests.next().await;
17556 cx.condition(|editor, _| editor.hover_state.visible()).await;
17557 let counter = Arc::new(AtomicUsize::new(0));
17558 let mut completion_requests =
17559 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17560 let counter = counter.clone();
17561 async move {
17562 counter.fetch_add(1, atomic::Ordering::Release);
17563 Ok(Some(lsp::CompletionResponse::Array(vec![
17564 lsp::CompletionItem {
17565 label: "main".into(),
17566 kind: Some(lsp::CompletionItemKind::FUNCTION),
17567 detail: Some("() -> ()".to_string()),
17568 ..Default::default()
17569 },
17570 lsp::CompletionItem {
17571 label: "TestStruct".into(),
17572 kind: Some(lsp::CompletionItemKind::STRUCT),
17573 detail: Some("struct TestStruct".to_string()),
17574 ..Default::default()
17575 },
17576 ])))
17577 }
17578 });
17579 cx.update_editor(|editor, window, cx| {
17580 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
17581 });
17582 completion_requests.next().await;
17583 cx.condition(|editor, _| editor.context_menu_visible())
17584 .await;
17585 cx.update_editor(|editor, _, _| {
17586 assert!(
17587 !editor.hover_state.visible(),
17588 "Hover popover should be hidden when completion menu is shown"
17589 );
17590 });
17591}
17592
17593#[gpui::test]
17594async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
17595 init_test(cx, |_| {});
17596
17597 let mut cx = EditorLspTestContext::new_rust(
17598 lsp::ServerCapabilities {
17599 completion_provider: Some(lsp::CompletionOptions {
17600 trigger_characters: Some(vec![".".to_string()]),
17601 resolve_provider: Some(true),
17602 ..Default::default()
17603 }),
17604 ..Default::default()
17605 },
17606 cx,
17607 )
17608 .await;
17609
17610 cx.set_state("fn main() { let a = 2ˇ; }");
17611 cx.simulate_keystroke(".");
17612
17613 let unresolved_item_1 = lsp::CompletionItem {
17614 label: "id".to_string(),
17615 filter_text: Some("id".to_string()),
17616 detail: None,
17617 documentation: None,
17618 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17619 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17620 new_text: ".id".to_string(),
17621 })),
17622 ..lsp::CompletionItem::default()
17623 };
17624 let resolved_item_1 = lsp::CompletionItem {
17625 additional_text_edits: Some(vec![lsp::TextEdit {
17626 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
17627 new_text: "!!".to_string(),
17628 }]),
17629 ..unresolved_item_1.clone()
17630 };
17631 let unresolved_item_2 = lsp::CompletionItem {
17632 label: "other".to_string(),
17633 filter_text: Some("other".to_string()),
17634 detail: None,
17635 documentation: None,
17636 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17637 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17638 new_text: ".other".to_string(),
17639 })),
17640 ..lsp::CompletionItem::default()
17641 };
17642 let resolved_item_2 = lsp::CompletionItem {
17643 additional_text_edits: Some(vec![lsp::TextEdit {
17644 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
17645 new_text: "??".to_string(),
17646 }]),
17647 ..unresolved_item_2.clone()
17648 };
17649
17650 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
17651 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
17652 cx.lsp
17653 .server
17654 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
17655 let unresolved_item_1 = unresolved_item_1.clone();
17656 let resolved_item_1 = resolved_item_1.clone();
17657 let unresolved_item_2 = unresolved_item_2.clone();
17658 let resolved_item_2 = resolved_item_2.clone();
17659 let resolve_requests_1 = resolve_requests_1.clone();
17660 let resolve_requests_2 = resolve_requests_2.clone();
17661 move |unresolved_request, _| {
17662 let unresolved_item_1 = unresolved_item_1.clone();
17663 let resolved_item_1 = resolved_item_1.clone();
17664 let unresolved_item_2 = unresolved_item_2.clone();
17665 let resolved_item_2 = resolved_item_2.clone();
17666 let resolve_requests_1 = resolve_requests_1.clone();
17667 let resolve_requests_2 = resolve_requests_2.clone();
17668 async move {
17669 if unresolved_request == unresolved_item_1 {
17670 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
17671 Ok(resolved_item_1.clone())
17672 } else if unresolved_request == unresolved_item_2 {
17673 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
17674 Ok(resolved_item_2.clone())
17675 } else {
17676 panic!("Unexpected completion item {unresolved_request:?}")
17677 }
17678 }
17679 }
17680 })
17681 .detach();
17682
17683 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17684 let unresolved_item_1 = unresolved_item_1.clone();
17685 let unresolved_item_2 = unresolved_item_2.clone();
17686 async move {
17687 Ok(Some(lsp::CompletionResponse::Array(vec![
17688 unresolved_item_1,
17689 unresolved_item_2,
17690 ])))
17691 }
17692 })
17693 .next()
17694 .await;
17695
17696 cx.condition(|editor, _| editor.context_menu_visible())
17697 .await;
17698 cx.update_editor(|editor, _, _| {
17699 let context_menu = editor.context_menu.borrow_mut();
17700 let context_menu = context_menu
17701 .as_ref()
17702 .expect("Should have the context menu deployed");
17703 match context_menu {
17704 CodeContextMenu::Completions(completions_menu) => {
17705 let completions = completions_menu.completions.borrow_mut();
17706 assert_eq!(
17707 completions
17708 .iter()
17709 .map(|completion| &completion.label.text)
17710 .collect::<Vec<_>>(),
17711 vec!["id", "other"]
17712 )
17713 }
17714 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17715 }
17716 });
17717 cx.run_until_parked();
17718
17719 cx.update_editor(|editor, window, cx| {
17720 editor.context_menu_next(&ContextMenuNext, window, cx);
17721 });
17722 cx.run_until_parked();
17723 cx.update_editor(|editor, window, cx| {
17724 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
17725 });
17726 cx.run_until_parked();
17727 cx.update_editor(|editor, window, cx| {
17728 editor.context_menu_next(&ContextMenuNext, window, cx);
17729 });
17730 cx.run_until_parked();
17731 cx.update_editor(|editor, window, cx| {
17732 editor
17733 .compose_completion(&ComposeCompletion::default(), window, cx)
17734 .expect("No task returned")
17735 })
17736 .await
17737 .expect("Completion failed");
17738 cx.run_until_parked();
17739
17740 cx.update_editor(|editor, _, cx| {
17741 assert_eq!(
17742 resolve_requests_1.load(atomic::Ordering::Acquire),
17743 1,
17744 "Should always resolve once despite multiple selections"
17745 );
17746 assert_eq!(
17747 resolve_requests_2.load(atomic::Ordering::Acquire),
17748 1,
17749 "Should always resolve once after multiple selections and applying the completion"
17750 );
17751 assert_eq!(
17752 editor.text(cx),
17753 "fn main() { let a = ??.other; }",
17754 "Should use resolved data when applying the completion"
17755 );
17756 });
17757}
17758
17759#[gpui::test]
17760async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
17761 init_test(cx, |_| {});
17762
17763 let item_0 = lsp::CompletionItem {
17764 label: "abs".into(),
17765 insert_text: Some("abs".into()),
17766 data: Some(json!({ "very": "special"})),
17767 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
17768 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
17769 lsp::InsertReplaceEdit {
17770 new_text: "abs".to_string(),
17771 insert: lsp::Range::default(),
17772 replace: lsp::Range::default(),
17773 },
17774 )),
17775 ..lsp::CompletionItem::default()
17776 };
17777 let items = iter::once(item_0.clone())
17778 .chain((11..51).map(|i| lsp::CompletionItem {
17779 label: format!("item_{}", i),
17780 insert_text: Some(format!("item_{}", i)),
17781 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
17782 ..lsp::CompletionItem::default()
17783 }))
17784 .collect::<Vec<_>>();
17785
17786 let default_commit_characters = vec!["?".to_string()];
17787 let default_data = json!({ "default": "data"});
17788 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
17789 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
17790 let default_edit_range = lsp::Range {
17791 start: lsp::Position {
17792 line: 0,
17793 character: 5,
17794 },
17795 end: lsp::Position {
17796 line: 0,
17797 character: 5,
17798 },
17799 };
17800
17801 let mut cx = EditorLspTestContext::new_rust(
17802 lsp::ServerCapabilities {
17803 completion_provider: Some(lsp::CompletionOptions {
17804 trigger_characters: Some(vec![".".to_string()]),
17805 resolve_provider: Some(true),
17806 ..Default::default()
17807 }),
17808 ..Default::default()
17809 },
17810 cx,
17811 )
17812 .await;
17813
17814 cx.set_state("fn main() { let a = 2ˇ; }");
17815 cx.simulate_keystroke(".");
17816
17817 let completion_data = default_data.clone();
17818 let completion_characters = default_commit_characters.clone();
17819 let completion_items = items.clone();
17820 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17821 let default_data = completion_data.clone();
17822 let default_commit_characters = completion_characters.clone();
17823 let items = completion_items.clone();
17824 async move {
17825 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
17826 items,
17827 item_defaults: Some(lsp::CompletionListItemDefaults {
17828 data: Some(default_data.clone()),
17829 commit_characters: Some(default_commit_characters.clone()),
17830 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
17831 default_edit_range,
17832 )),
17833 insert_text_format: Some(default_insert_text_format),
17834 insert_text_mode: Some(default_insert_text_mode),
17835 }),
17836 ..lsp::CompletionList::default()
17837 })))
17838 }
17839 })
17840 .next()
17841 .await;
17842
17843 let resolved_items = Arc::new(Mutex::new(Vec::new()));
17844 cx.lsp
17845 .server
17846 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
17847 let closure_resolved_items = resolved_items.clone();
17848 move |item_to_resolve, _| {
17849 let closure_resolved_items = closure_resolved_items.clone();
17850 async move {
17851 closure_resolved_items.lock().push(item_to_resolve.clone());
17852 Ok(item_to_resolve)
17853 }
17854 }
17855 })
17856 .detach();
17857
17858 cx.condition(|editor, _| editor.context_menu_visible())
17859 .await;
17860 cx.run_until_parked();
17861 cx.update_editor(|editor, _, _| {
17862 let menu = editor.context_menu.borrow_mut();
17863 match menu.as_ref().expect("should have the completions menu") {
17864 CodeContextMenu::Completions(completions_menu) => {
17865 assert_eq!(
17866 completions_menu
17867 .entries
17868 .borrow()
17869 .iter()
17870 .map(|mat| mat.string.clone())
17871 .collect::<Vec<String>>(),
17872 items
17873 .iter()
17874 .map(|completion| completion.label.clone())
17875 .collect::<Vec<String>>()
17876 );
17877 }
17878 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
17879 }
17880 });
17881 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
17882 // with 4 from the end.
17883 assert_eq!(
17884 *resolved_items.lock(),
17885 [&items[0..16], &items[items.len() - 4..items.len()]]
17886 .concat()
17887 .iter()
17888 .cloned()
17889 .map(|mut item| {
17890 if item.data.is_none() {
17891 item.data = Some(default_data.clone());
17892 }
17893 item
17894 })
17895 .collect::<Vec<lsp::CompletionItem>>(),
17896 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
17897 );
17898 resolved_items.lock().clear();
17899
17900 cx.update_editor(|editor, window, cx| {
17901 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
17902 });
17903 cx.run_until_parked();
17904 // Completions that have already been resolved are skipped.
17905 assert_eq!(
17906 *resolved_items.lock(),
17907 items[items.len() - 17..items.len() - 4]
17908 .iter()
17909 .cloned()
17910 .map(|mut item| {
17911 if item.data.is_none() {
17912 item.data = Some(default_data.clone());
17913 }
17914 item
17915 })
17916 .collect::<Vec<lsp::CompletionItem>>()
17917 );
17918 resolved_items.lock().clear();
17919}
17920
17921#[gpui::test]
17922async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
17923 init_test(cx, |_| {});
17924
17925 let mut cx = EditorLspTestContext::new(
17926 Language::new(
17927 LanguageConfig {
17928 matcher: LanguageMatcher {
17929 path_suffixes: vec!["jsx".into()],
17930 ..Default::default()
17931 },
17932 overrides: [(
17933 "element".into(),
17934 LanguageConfigOverride {
17935 completion_query_characters: Override::Set(['-'].into_iter().collect()),
17936 ..Default::default()
17937 },
17938 )]
17939 .into_iter()
17940 .collect(),
17941 ..Default::default()
17942 },
17943 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
17944 )
17945 .with_override_query("(jsx_self_closing_element) @element")
17946 .unwrap(),
17947 lsp::ServerCapabilities {
17948 completion_provider: Some(lsp::CompletionOptions {
17949 trigger_characters: Some(vec![":".to_string()]),
17950 ..Default::default()
17951 }),
17952 ..Default::default()
17953 },
17954 cx,
17955 )
17956 .await;
17957
17958 cx.lsp
17959 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
17960 Ok(Some(lsp::CompletionResponse::Array(vec![
17961 lsp::CompletionItem {
17962 label: "bg-blue".into(),
17963 ..Default::default()
17964 },
17965 lsp::CompletionItem {
17966 label: "bg-red".into(),
17967 ..Default::default()
17968 },
17969 lsp::CompletionItem {
17970 label: "bg-yellow".into(),
17971 ..Default::default()
17972 },
17973 ])))
17974 });
17975
17976 cx.set_state(r#"<p class="bgˇ" />"#);
17977
17978 // Trigger completion when typing a dash, because the dash is an extra
17979 // word character in the 'element' scope, which contains the cursor.
17980 cx.simulate_keystroke("-");
17981 cx.executor().run_until_parked();
17982 cx.update_editor(|editor, _, _| {
17983 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17984 {
17985 assert_eq!(
17986 completion_menu_entries(menu),
17987 &["bg-blue", "bg-red", "bg-yellow"]
17988 );
17989 } else {
17990 panic!("expected completion menu to be open");
17991 }
17992 });
17993
17994 cx.simulate_keystroke("l");
17995 cx.executor().run_until_parked();
17996 cx.update_editor(|editor, _, _| {
17997 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17998 {
17999 assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]);
18000 } else {
18001 panic!("expected completion menu to be open");
18002 }
18003 });
18004
18005 // When filtering completions, consider the character after the '-' to
18006 // be the start of a subword.
18007 cx.set_state(r#"<p class="yelˇ" />"#);
18008 cx.simulate_keystroke("l");
18009 cx.executor().run_until_parked();
18010 cx.update_editor(|editor, _, _| {
18011 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18012 {
18013 assert_eq!(completion_menu_entries(menu), &["bg-yellow"]);
18014 } else {
18015 panic!("expected completion menu to be open");
18016 }
18017 });
18018}
18019
18020fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
18021 let entries = menu.entries.borrow();
18022 entries.iter().map(|mat| mat.string.clone()).collect()
18023}
18024
18025#[gpui::test]
18026async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
18027 init_test(cx, |settings| {
18028 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
18029 Formatter::Prettier,
18030 )))
18031 });
18032
18033 let fs = FakeFs::new(cx.executor());
18034 fs.insert_file(path!("/file.ts"), Default::default()).await;
18035
18036 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
18037 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18038
18039 language_registry.add(Arc::new(Language::new(
18040 LanguageConfig {
18041 name: "TypeScript".into(),
18042 matcher: LanguageMatcher {
18043 path_suffixes: vec!["ts".to_string()],
18044 ..Default::default()
18045 },
18046 ..Default::default()
18047 },
18048 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
18049 )));
18050 update_test_language_settings(cx, |settings| {
18051 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
18052 });
18053
18054 let test_plugin = "test_plugin";
18055 let _ = language_registry.register_fake_lsp(
18056 "TypeScript",
18057 FakeLspAdapter {
18058 prettier_plugins: vec![test_plugin],
18059 ..Default::default()
18060 },
18061 );
18062
18063 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
18064 let buffer = project
18065 .update(cx, |project, cx| {
18066 project.open_local_buffer(path!("/file.ts"), cx)
18067 })
18068 .await
18069 .unwrap();
18070
18071 let buffer_text = "one\ntwo\nthree\n";
18072 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
18073 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
18074 editor.update_in(cx, |editor, window, cx| {
18075 editor.set_text(buffer_text, window, cx)
18076 });
18077
18078 editor
18079 .update_in(cx, |editor, window, cx| {
18080 editor.perform_format(
18081 project.clone(),
18082 FormatTrigger::Manual,
18083 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18084 window,
18085 cx,
18086 )
18087 })
18088 .unwrap()
18089 .await;
18090 assert_eq!(
18091 editor.update(cx, |editor, cx| editor.text(cx)),
18092 buffer_text.to_string() + prettier_format_suffix,
18093 "Test prettier formatting was not applied to the original buffer text",
18094 );
18095
18096 update_test_language_settings(cx, |settings| {
18097 settings.defaults.formatter = Some(SelectedFormatter::Auto)
18098 });
18099 let format = editor.update_in(cx, |editor, window, cx| {
18100 editor.perform_format(
18101 project.clone(),
18102 FormatTrigger::Manual,
18103 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18104 window,
18105 cx,
18106 )
18107 });
18108 format.await.unwrap();
18109 assert_eq!(
18110 editor.update(cx, |editor, cx| editor.text(cx)),
18111 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
18112 "Autoformatting (via test prettier) was not applied to the original buffer text",
18113 );
18114}
18115
18116#[gpui::test]
18117async fn test_addition_reverts(cx: &mut TestAppContext) {
18118 init_test(cx, |_| {});
18119 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18120 let base_text = indoc! {r#"
18121 struct Row;
18122 struct Row1;
18123 struct Row2;
18124
18125 struct Row4;
18126 struct Row5;
18127 struct Row6;
18128
18129 struct Row8;
18130 struct Row9;
18131 struct Row10;"#};
18132
18133 // When addition hunks are not adjacent to carets, no hunk revert is performed
18134 assert_hunk_revert(
18135 indoc! {r#"struct Row;
18136 struct Row1;
18137 struct Row1.1;
18138 struct Row1.2;
18139 struct Row2;ˇ
18140
18141 struct Row4;
18142 struct Row5;
18143 struct Row6;
18144
18145 struct Row8;
18146 ˇstruct Row9;
18147 struct Row9.1;
18148 struct Row9.2;
18149 struct Row9.3;
18150 struct Row10;"#},
18151 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18152 indoc! {r#"struct Row;
18153 struct Row1;
18154 struct Row1.1;
18155 struct Row1.2;
18156 struct Row2;ˇ
18157
18158 struct Row4;
18159 struct Row5;
18160 struct Row6;
18161
18162 struct Row8;
18163 ˇstruct Row9;
18164 struct Row9.1;
18165 struct Row9.2;
18166 struct Row9.3;
18167 struct Row10;"#},
18168 base_text,
18169 &mut cx,
18170 );
18171 // Same for selections
18172 assert_hunk_revert(
18173 indoc! {r#"struct Row;
18174 struct Row1;
18175 struct Row2;
18176 struct Row2.1;
18177 struct Row2.2;
18178 «ˇ
18179 struct Row4;
18180 struct» Row5;
18181 «struct Row6;
18182 ˇ»
18183 struct Row9.1;
18184 struct Row9.2;
18185 struct Row9.3;
18186 struct Row8;
18187 struct Row9;
18188 struct Row10;"#},
18189 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18190 indoc! {r#"struct Row;
18191 struct Row1;
18192 struct Row2;
18193 struct Row2.1;
18194 struct Row2.2;
18195 «ˇ
18196 struct Row4;
18197 struct» Row5;
18198 «struct Row6;
18199 ˇ»
18200 struct Row9.1;
18201 struct Row9.2;
18202 struct Row9.3;
18203 struct Row8;
18204 struct Row9;
18205 struct Row10;"#},
18206 base_text,
18207 &mut cx,
18208 );
18209
18210 // When carets and selections intersect the addition hunks, those are reverted.
18211 // Adjacent carets got merged.
18212 assert_hunk_revert(
18213 indoc! {r#"struct Row;
18214 ˇ// something on the top
18215 struct Row1;
18216 struct Row2;
18217 struct Roˇw3.1;
18218 struct Row2.2;
18219 struct Row2.3;ˇ
18220
18221 struct Row4;
18222 struct ˇRow5.1;
18223 struct Row5.2;
18224 struct «Rowˇ»5.3;
18225 struct Row5;
18226 struct Row6;
18227 ˇ
18228 struct Row9.1;
18229 struct «Rowˇ»9.2;
18230 struct «ˇRow»9.3;
18231 struct Row8;
18232 struct Row9;
18233 «ˇ// something on bottom»
18234 struct Row10;"#},
18235 vec![
18236 DiffHunkStatusKind::Added,
18237 DiffHunkStatusKind::Added,
18238 DiffHunkStatusKind::Added,
18239 DiffHunkStatusKind::Added,
18240 DiffHunkStatusKind::Added,
18241 ],
18242 indoc! {r#"struct Row;
18243 ˇstruct Row1;
18244 struct Row2;
18245 ˇ
18246 struct Row4;
18247 ˇstruct Row5;
18248 struct Row6;
18249 ˇ
18250 ˇstruct Row8;
18251 struct Row9;
18252 ˇstruct Row10;"#},
18253 base_text,
18254 &mut cx,
18255 );
18256}
18257
18258#[gpui::test]
18259async fn test_modification_reverts(cx: &mut TestAppContext) {
18260 init_test(cx, |_| {});
18261 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18262 let base_text = indoc! {r#"
18263 struct Row;
18264 struct Row1;
18265 struct Row2;
18266
18267 struct Row4;
18268 struct Row5;
18269 struct Row6;
18270
18271 struct Row8;
18272 struct Row9;
18273 struct Row10;"#};
18274
18275 // Modification hunks behave the same as the addition ones.
18276 assert_hunk_revert(
18277 indoc! {r#"struct Row;
18278 struct Row1;
18279 struct Row33;
18280 ˇ
18281 struct Row4;
18282 struct Row5;
18283 struct Row6;
18284 ˇ
18285 struct Row99;
18286 struct Row9;
18287 struct Row10;"#},
18288 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18289 indoc! {r#"struct Row;
18290 struct Row1;
18291 struct Row33;
18292 ˇ
18293 struct Row4;
18294 struct Row5;
18295 struct Row6;
18296 ˇ
18297 struct Row99;
18298 struct Row9;
18299 struct Row10;"#},
18300 base_text,
18301 &mut cx,
18302 );
18303 assert_hunk_revert(
18304 indoc! {r#"struct Row;
18305 struct Row1;
18306 struct Row33;
18307 «ˇ
18308 struct Row4;
18309 struct» Row5;
18310 «struct Row6;
18311 ˇ»
18312 struct Row99;
18313 struct Row9;
18314 struct Row10;"#},
18315 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18316 indoc! {r#"struct Row;
18317 struct Row1;
18318 struct Row33;
18319 «ˇ
18320 struct Row4;
18321 struct» Row5;
18322 «struct Row6;
18323 ˇ»
18324 struct Row99;
18325 struct Row9;
18326 struct Row10;"#},
18327 base_text,
18328 &mut cx,
18329 );
18330
18331 assert_hunk_revert(
18332 indoc! {r#"ˇstruct Row1.1;
18333 struct Row1;
18334 «ˇstr»uct Row22;
18335
18336 struct ˇRow44;
18337 struct Row5;
18338 struct «Rˇ»ow66;ˇ
18339
18340 «struˇ»ct Row88;
18341 struct Row9;
18342 struct Row1011;ˇ"#},
18343 vec![
18344 DiffHunkStatusKind::Modified,
18345 DiffHunkStatusKind::Modified,
18346 DiffHunkStatusKind::Modified,
18347 DiffHunkStatusKind::Modified,
18348 DiffHunkStatusKind::Modified,
18349 DiffHunkStatusKind::Modified,
18350 ],
18351 indoc! {r#"struct Row;
18352 ˇstruct Row1;
18353 struct Row2;
18354 ˇ
18355 struct Row4;
18356 ˇstruct Row5;
18357 struct Row6;
18358 ˇ
18359 struct Row8;
18360 ˇstruct Row9;
18361 struct Row10;ˇ"#},
18362 base_text,
18363 &mut cx,
18364 );
18365}
18366
18367#[gpui::test]
18368async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
18369 init_test(cx, |_| {});
18370 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18371 let base_text = indoc! {r#"
18372 one
18373
18374 two
18375 three
18376 "#};
18377
18378 cx.set_head_text(base_text);
18379 cx.set_state("\nˇ\n");
18380 cx.executor().run_until_parked();
18381 cx.update_editor(|editor, _window, cx| {
18382 editor.expand_selected_diff_hunks(cx);
18383 });
18384 cx.executor().run_until_parked();
18385 cx.update_editor(|editor, window, cx| {
18386 editor.backspace(&Default::default(), window, cx);
18387 });
18388 cx.run_until_parked();
18389 cx.assert_state_with_diff(
18390 indoc! {r#"
18391
18392 - two
18393 - threeˇ
18394 +
18395 "#}
18396 .to_string(),
18397 );
18398}
18399
18400#[gpui::test]
18401async fn test_deletion_reverts(cx: &mut TestAppContext) {
18402 init_test(cx, |_| {});
18403 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18404 let base_text = indoc! {r#"struct Row;
18405struct Row1;
18406struct Row2;
18407
18408struct Row4;
18409struct Row5;
18410struct Row6;
18411
18412struct Row8;
18413struct Row9;
18414struct Row10;"#};
18415
18416 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
18417 assert_hunk_revert(
18418 indoc! {r#"struct Row;
18419 struct Row2;
18420
18421 ˇstruct Row4;
18422 struct Row5;
18423 struct Row6;
18424 ˇ
18425 struct Row8;
18426 struct Row10;"#},
18427 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18428 indoc! {r#"struct Row;
18429 struct Row2;
18430
18431 ˇstruct Row4;
18432 struct Row5;
18433 struct Row6;
18434 ˇ
18435 struct Row8;
18436 struct Row10;"#},
18437 base_text,
18438 &mut cx,
18439 );
18440 assert_hunk_revert(
18441 indoc! {r#"struct Row;
18442 struct Row2;
18443
18444 «ˇstruct Row4;
18445 struct» Row5;
18446 «struct Row6;
18447 ˇ»
18448 struct Row8;
18449 struct Row10;"#},
18450 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18451 indoc! {r#"struct Row;
18452 struct Row2;
18453
18454 «ˇstruct Row4;
18455 struct» Row5;
18456 «struct Row6;
18457 ˇ»
18458 struct Row8;
18459 struct Row10;"#},
18460 base_text,
18461 &mut cx,
18462 );
18463
18464 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
18465 assert_hunk_revert(
18466 indoc! {r#"struct Row;
18467 ˇstruct Row2;
18468
18469 struct Row4;
18470 struct Row5;
18471 struct Row6;
18472
18473 struct Row8;ˇ
18474 struct Row10;"#},
18475 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18476 indoc! {r#"struct Row;
18477 struct Row1;
18478 ˇstruct Row2;
18479
18480 struct Row4;
18481 struct Row5;
18482 struct Row6;
18483
18484 struct Row8;ˇ
18485 struct Row9;
18486 struct Row10;"#},
18487 base_text,
18488 &mut cx,
18489 );
18490 assert_hunk_revert(
18491 indoc! {r#"struct Row;
18492 struct Row2«ˇ;
18493 struct Row4;
18494 struct» Row5;
18495 «struct Row6;
18496
18497 struct Row8;ˇ»
18498 struct Row10;"#},
18499 vec![
18500 DiffHunkStatusKind::Deleted,
18501 DiffHunkStatusKind::Deleted,
18502 DiffHunkStatusKind::Deleted,
18503 ],
18504 indoc! {r#"struct Row;
18505 struct Row1;
18506 struct Row2«ˇ;
18507
18508 struct Row4;
18509 struct» Row5;
18510 «struct Row6;
18511
18512 struct Row8;ˇ»
18513 struct Row9;
18514 struct Row10;"#},
18515 base_text,
18516 &mut cx,
18517 );
18518}
18519
18520#[gpui::test]
18521async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
18522 init_test(cx, |_| {});
18523
18524 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
18525 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
18526 let base_text_3 =
18527 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
18528
18529 let text_1 = edit_first_char_of_every_line(base_text_1);
18530 let text_2 = edit_first_char_of_every_line(base_text_2);
18531 let text_3 = edit_first_char_of_every_line(base_text_3);
18532
18533 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
18534 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
18535 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
18536
18537 let multibuffer = cx.new(|cx| {
18538 let mut multibuffer = MultiBuffer::new(ReadWrite);
18539 multibuffer.push_excerpts(
18540 buffer_1.clone(),
18541 [
18542 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18543 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18544 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18545 ],
18546 cx,
18547 );
18548 multibuffer.push_excerpts(
18549 buffer_2.clone(),
18550 [
18551 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18552 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18553 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18554 ],
18555 cx,
18556 );
18557 multibuffer.push_excerpts(
18558 buffer_3.clone(),
18559 [
18560 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18561 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18562 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18563 ],
18564 cx,
18565 );
18566 multibuffer
18567 });
18568
18569 let fs = FakeFs::new(cx.executor());
18570 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
18571 let (editor, cx) = cx
18572 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
18573 editor.update_in(cx, |editor, _window, cx| {
18574 for (buffer, diff_base) in [
18575 (buffer_1.clone(), base_text_1),
18576 (buffer_2.clone(), base_text_2),
18577 (buffer_3.clone(), base_text_3),
18578 ] {
18579 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
18580 editor
18581 .buffer
18582 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
18583 }
18584 });
18585 cx.executor().run_until_parked();
18586
18587 editor.update_in(cx, |editor, window, cx| {
18588 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}");
18589 editor.select_all(&SelectAll, window, cx);
18590 editor.git_restore(&Default::default(), window, cx);
18591 });
18592 cx.executor().run_until_parked();
18593
18594 // When all ranges are selected, all buffer hunks are reverted.
18595 editor.update(cx, |editor, cx| {
18596 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");
18597 });
18598 buffer_1.update(cx, |buffer, _| {
18599 assert_eq!(buffer.text(), base_text_1);
18600 });
18601 buffer_2.update(cx, |buffer, _| {
18602 assert_eq!(buffer.text(), base_text_2);
18603 });
18604 buffer_3.update(cx, |buffer, _| {
18605 assert_eq!(buffer.text(), base_text_3);
18606 });
18607
18608 editor.update_in(cx, |editor, window, cx| {
18609 editor.undo(&Default::default(), window, cx);
18610 });
18611
18612 editor.update_in(cx, |editor, window, cx| {
18613 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18614 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
18615 });
18616 editor.git_restore(&Default::default(), window, cx);
18617 });
18618
18619 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
18620 // but not affect buffer_2 and its related excerpts.
18621 editor.update(cx, |editor, cx| {
18622 assert_eq!(
18623 editor.text(cx),
18624 "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}"
18625 );
18626 });
18627 buffer_1.update(cx, |buffer, _| {
18628 assert_eq!(buffer.text(), base_text_1);
18629 });
18630 buffer_2.update(cx, |buffer, _| {
18631 assert_eq!(
18632 buffer.text(),
18633 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
18634 );
18635 });
18636 buffer_3.update(cx, |buffer, _| {
18637 assert_eq!(
18638 buffer.text(),
18639 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
18640 );
18641 });
18642
18643 fn edit_first_char_of_every_line(text: &str) -> String {
18644 text.split('\n')
18645 .map(|line| format!("X{}", &line[1..]))
18646 .collect::<Vec<_>>()
18647 .join("\n")
18648 }
18649}
18650
18651#[gpui::test]
18652async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
18653 init_test(cx, |_| {});
18654
18655 let cols = 4;
18656 let rows = 10;
18657 let sample_text_1 = sample_text(rows, cols, 'a');
18658 assert_eq!(
18659 sample_text_1,
18660 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
18661 );
18662 let sample_text_2 = sample_text(rows, cols, 'l');
18663 assert_eq!(
18664 sample_text_2,
18665 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
18666 );
18667 let sample_text_3 = sample_text(rows, cols, 'v');
18668 assert_eq!(
18669 sample_text_3,
18670 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
18671 );
18672
18673 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
18674 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
18675 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
18676
18677 let multi_buffer = cx.new(|cx| {
18678 let mut multibuffer = MultiBuffer::new(ReadWrite);
18679 multibuffer.push_excerpts(
18680 buffer_1.clone(),
18681 [
18682 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18683 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18684 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18685 ],
18686 cx,
18687 );
18688 multibuffer.push_excerpts(
18689 buffer_2.clone(),
18690 [
18691 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18692 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18693 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18694 ],
18695 cx,
18696 );
18697 multibuffer.push_excerpts(
18698 buffer_3.clone(),
18699 [
18700 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18701 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18702 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18703 ],
18704 cx,
18705 );
18706 multibuffer
18707 });
18708
18709 let fs = FakeFs::new(cx.executor());
18710 fs.insert_tree(
18711 "/a",
18712 json!({
18713 "main.rs": sample_text_1,
18714 "other.rs": sample_text_2,
18715 "lib.rs": sample_text_3,
18716 }),
18717 )
18718 .await;
18719 let project = Project::test(fs, ["/a".as_ref()], cx).await;
18720 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18721 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18722 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18723 Editor::new(
18724 EditorMode::full(),
18725 multi_buffer,
18726 Some(project.clone()),
18727 window,
18728 cx,
18729 )
18730 });
18731 let multibuffer_item_id = workspace
18732 .update(cx, |workspace, window, cx| {
18733 assert!(
18734 workspace.active_item(cx).is_none(),
18735 "active item should be None before the first item is added"
18736 );
18737 workspace.add_item_to_active_pane(
18738 Box::new(multi_buffer_editor.clone()),
18739 None,
18740 true,
18741 window,
18742 cx,
18743 );
18744 let active_item = workspace
18745 .active_item(cx)
18746 .expect("should have an active item after adding the multi buffer");
18747 assert!(
18748 !active_item.is_singleton(cx),
18749 "A multi buffer was expected to active after adding"
18750 );
18751 active_item.item_id()
18752 })
18753 .unwrap();
18754 cx.executor().run_until_parked();
18755
18756 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18757 editor.change_selections(
18758 SelectionEffects::scroll(Autoscroll::Next),
18759 window,
18760 cx,
18761 |s| s.select_ranges(Some(1..2)),
18762 );
18763 editor.open_excerpts(&OpenExcerpts, window, cx);
18764 });
18765 cx.executor().run_until_parked();
18766 let first_item_id = workspace
18767 .update(cx, |workspace, window, cx| {
18768 let active_item = workspace
18769 .active_item(cx)
18770 .expect("should have an active item after navigating into the 1st buffer");
18771 let first_item_id = active_item.item_id();
18772 assert_ne!(
18773 first_item_id, multibuffer_item_id,
18774 "Should navigate into the 1st buffer and activate it"
18775 );
18776 assert!(
18777 active_item.is_singleton(cx),
18778 "New active item should be a singleton buffer"
18779 );
18780 assert_eq!(
18781 active_item
18782 .act_as::<Editor>(cx)
18783 .expect("should have navigated into an editor for the 1st buffer")
18784 .read(cx)
18785 .text(cx),
18786 sample_text_1
18787 );
18788
18789 workspace
18790 .go_back(workspace.active_pane().downgrade(), window, cx)
18791 .detach_and_log_err(cx);
18792
18793 first_item_id
18794 })
18795 .unwrap();
18796 cx.executor().run_until_parked();
18797 workspace
18798 .update(cx, |workspace, _, cx| {
18799 let active_item = workspace
18800 .active_item(cx)
18801 .expect("should have an active item after navigating back");
18802 assert_eq!(
18803 active_item.item_id(),
18804 multibuffer_item_id,
18805 "Should navigate back to the multi buffer"
18806 );
18807 assert!(!active_item.is_singleton(cx));
18808 })
18809 .unwrap();
18810
18811 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18812 editor.change_selections(
18813 SelectionEffects::scroll(Autoscroll::Next),
18814 window,
18815 cx,
18816 |s| s.select_ranges(Some(39..40)),
18817 );
18818 editor.open_excerpts(&OpenExcerpts, window, cx);
18819 });
18820 cx.executor().run_until_parked();
18821 let second_item_id = workspace
18822 .update(cx, |workspace, window, cx| {
18823 let active_item = workspace
18824 .active_item(cx)
18825 .expect("should have an active item after navigating into the 2nd buffer");
18826 let second_item_id = active_item.item_id();
18827 assert_ne!(
18828 second_item_id, multibuffer_item_id,
18829 "Should navigate away from the multibuffer"
18830 );
18831 assert_ne!(
18832 second_item_id, first_item_id,
18833 "Should navigate into the 2nd buffer and activate it"
18834 );
18835 assert!(
18836 active_item.is_singleton(cx),
18837 "New active item should be a singleton buffer"
18838 );
18839 assert_eq!(
18840 active_item
18841 .act_as::<Editor>(cx)
18842 .expect("should have navigated into an editor")
18843 .read(cx)
18844 .text(cx),
18845 sample_text_2
18846 );
18847
18848 workspace
18849 .go_back(workspace.active_pane().downgrade(), window, cx)
18850 .detach_and_log_err(cx);
18851
18852 second_item_id
18853 })
18854 .unwrap();
18855 cx.executor().run_until_parked();
18856 workspace
18857 .update(cx, |workspace, _, cx| {
18858 let active_item = workspace
18859 .active_item(cx)
18860 .expect("should have an active item after navigating back from the 2nd buffer");
18861 assert_eq!(
18862 active_item.item_id(),
18863 multibuffer_item_id,
18864 "Should navigate back from the 2nd buffer to the multi buffer"
18865 );
18866 assert!(!active_item.is_singleton(cx));
18867 })
18868 .unwrap();
18869
18870 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18871 editor.change_selections(
18872 SelectionEffects::scroll(Autoscroll::Next),
18873 window,
18874 cx,
18875 |s| s.select_ranges(Some(70..70)),
18876 );
18877 editor.open_excerpts(&OpenExcerpts, window, cx);
18878 });
18879 cx.executor().run_until_parked();
18880 workspace
18881 .update(cx, |workspace, window, cx| {
18882 let active_item = workspace
18883 .active_item(cx)
18884 .expect("should have an active item after navigating into the 3rd buffer");
18885 let third_item_id = active_item.item_id();
18886 assert_ne!(
18887 third_item_id, multibuffer_item_id,
18888 "Should navigate into the 3rd buffer and activate it"
18889 );
18890 assert_ne!(third_item_id, first_item_id);
18891 assert_ne!(third_item_id, second_item_id);
18892 assert!(
18893 active_item.is_singleton(cx),
18894 "New active item should be a singleton buffer"
18895 );
18896 assert_eq!(
18897 active_item
18898 .act_as::<Editor>(cx)
18899 .expect("should have navigated into an editor")
18900 .read(cx)
18901 .text(cx),
18902 sample_text_3
18903 );
18904
18905 workspace
18906 .go_back(workspace.active_pane().downgrade(), window, cx)
18907 .detach_and_log_err(cx);
18908 })
18909 .unwrap();
18910 cx.executor().run_until_parked();
18911 workspace
18912 .update(cx, |workspace, _, cx| {
18913 let active_item = workspace
18914 .active_item(cx)
18915 .expect("should have an active item after navigating back from the 3rd buffer");
18916 assert_eq!(
18917 active_item.item_id(),
18918 multibuffer_item_id,
18919 "Should navigate back from the 3rd buffer to the multi buffer"
18920 );
18921 assert!(!active_item.is_singleton(cx));
18922 })
18923 .unwrap();
18924}
18925
18926#[gpui::test]
18927async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
18928 init_test(cx, |_| {});
18929
18930 let mut cx = EditorTestContext::new(cx).await;
18931
18932 let diff_base = r#"
18933 use some::mod;
18934
18935 const A: u32 = 42;
18936
18937 fn main() {
18938 println!("hello");
18939
18940 println!("world");
18941 }
18942 "#
18943 .unindent();
18944
18945 cx.set_state(
18946 &r#"
18947 use some::modified;
18948
18949 ˇ
18950 fn main() {
18951 println!("hello there");
18952
18953 println!("around the");
18954 println!("world");
18955 }
18956 "#
18957 .unindent(),
18958 );
18959
18960 cx.set_head_text(&diff_base);
18961 executor.run_until_parked();
18962
18963 cx.update_editor(|editor, window, cx| {
18964 editor.go_to_next_hunk(&GoToHunk, window, cx);
18965 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
18966 });
18967 executor.run_until_parked();
18968 cx.assert_state_with_diff(
18969 r#"
18970 use some::modified;
18971
18972
18973 fn main() {
18974 - println!("hello");
18975 + ˇ println!("hello there");
18976
18977 println!("around the");
18978 println!("world");
18979 }
18980 "#
18981 .unindent(),
18982 );
18983
18984 cx.update_editor(|editor, window, cx| {
18985 for _ in 0..2 {
18986 editor.go_to_next_hunk(&GoToHunk, window, cx);
18987 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
18988 }
18989 });
18990 executor.run_until_parked();
18991 cx.assert_state_with_diff(
18992 r#"
18993 - use some::mod;
18994 + ˇuse some::modified;
18995
18996
18997 fn main() {
18998 - println!("hello");
18999 + println!("hello there");
19000
19001 + println!("around the");
19002 println!("world");
19003 }
19004 "#
19005 .unindent(),
19006 );
19007
19008 cx.update_editor(|editor, window, cx| {
19009 editor.go_to_next_hunk(&GoToHunk, window, cx);
19010 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19011 });
19012 executor.run_until_parked();
19013 cx.assert_state_with_diff(
19014 r#"
19015 - use some::mod;
19016 + use some::modified;
19017
19018 - const A: u32 = 42;
19019 ˇ
19020 fn main() {
19021 - println!("hello");
19022 + println!("hello there");
19023
19024 + println!("around the");
19025 println!("world");
19026 }
19027 "#
19028 .unindent(),
19029 );
19030
19031 cx.update_editor(|editor, window, cx| {
19032 editor.cancel(&Cancel, window, cx);
19033 });
19034
19035 cx.assert_state_with_diff(
19036 r#"
19037 use some::modified;
19038
19039 ˇ
19040 fn main() {
19041 println!("hello there");
19042
19043 println!("around the");
19044 println!("world");
19045 }
19046 "#
19047 .unindent(),
19048 );
19049}
19050
19051#[gpui::test]
19052async fn test_diff_base_change_with_expanded_diff_hunks(
19053 executor: BackgroundExecutor,
19054 cx: &mut TestAppContext,
19055) {
19056 init_test(cx, |_| {});
19057
19058 let mut cx = EditorTestContext::new(cx).await;
19059
19060 let diff_base = r#"
19061 use some::mod1;
19062 use some::mod2;
19063
19064 const A: u32 = 42;
19065 const B: u32 = 42;
19066 const C: u32 = 42;
19067
19068 fn main() {
19069 println!("hello");
19070
19071 println!("world");
19072 }
19073 "#
19074 .unindent();
19075
19076 cx.set_state(
19077 &r#"
19078 use some::mod2;
19079
19080 const A: u32 = 42;
19081 const C: u32 = 42;
19082
19083 fn main(ˇ) {
19084 //println!("hello");
19085
19086 println!("world");
19087 //
19088 //
19089 }
19090 "#
19091 .unindent(),
19092 );
19093
19094 cx.set_head_text(&diff_base);
19095 executor.run_until_parked();
19096
19097 cx.update_editor(|editor, window, cx| {
19098 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19099 });
19100 executor.run_until_parked();
19101 cx.assert_state_with_diff(
19102 r#"
19103 - use some::mod1;
19104 use some::mod2;
19105
19106 const A: u32 = 42;
19107 - const B: u32 = 42;
19108 const C: u32 = 42;
19109
19110 fn main(ˇ) {
19111 - println!("hello");
19112 + //println!("hello");
19113
19114 println!("world");
19115 + //
19116 + //
19117 }
19118 "#
19119 .unindent(),
19120 );
19121
19122 cx.set_head_text("new diff base!");
19123 executor.run_until_parked();
19124 cx.assert_state_with_diff(
19125 r#"
19126 - new diff base!
19127 + use some::mod2;
19128 +
19129 + const A: u32 = 42;
19130 + const C: u32 = 42;
19131 +
19132 + fn main(ˇ) {
19133 + //println!("hello");
19134 +
19135 + println!("world");
19136 + //
19137 + //
19138 + }
19139 "#
19140 .unindent(),
19141 );
19142}
19143
19144#[gpui::test]
19145async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
19146 init_test(cx, |_| {});
19147
19148 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19149 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19150 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19151 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19152 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
19153 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
19154
19155 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
19156 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
19157 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
19158
19159 let multi_buffer = cx.new(|cx| {
19160 let mut multibuffer = MultiBuffer::new(ReadWrite);
19161 multibuffer.push_excerpts(
19162 buffer_1.clone(),
19163 [
19164 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19165 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19166 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19167 ],
19168 cx,
19169 );
19170 multibuffer.push_excerpts(
19171 buffer_2.clone(),
19172 [
19173 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19174 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19175 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19176 ],
19177 cx,
19178 );
19179 multibuffer.push_excerpts(
19180 buffer_3.clone(),
19181 [
19182 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19183 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19184 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19185 ],
19186 cx,
19187 );
19188 multibuffer
19189 });
19190
19191 let editor =
19192 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19193 editor
19194 .update(cx, |editor, _window, cx| {
19195 for (buffer, diff_base) in [
19196 (buffer_1.clone(), file_1_old),
19197 (buffer_2.clone(), file_2_old),
19198 (buffer_3.clone(), file_3_old),
19199 ] {
19200 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
19201 editor
19202 .buffer
19203 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
19204 }
19205 })
19206 .unwrap();
19207
19208 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19209 cx.run_until_parked();
19210
19211 cx.assert_editor_state(
19212 &"
19213 ˇaaa
19214 ccc
19215 ddd
19216
19217 ggg
19218 hhh
19219
19220
19221 lll
19222 mmm
19223 NNN
19224
19225 qqq
19226 rrr
19227
19228 uuu
19229 111
19230 222
19231 333
19232
19233 666
19234 777
19235
19236 000
19237 !!!"
19238 .unindent(),
19239 );
19240
19241 cx.update_editor(|editor, window, cx| {
19242 editor.select_all(&SelectAll, window, cx);
19243 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19244 });
19245 cx.executor().run_until_parked();
19246
19247 cx.assert_state_with_diff(
19248 "
19249 «aaa
19250 - bbb
19251 ccc
19252 ddd
19253
19254 ggg
19255 hhh
19256
19257
19258 lll
19259 mmm
19260 - nnn
19261 + NNN
19262
19263 qqq
19264 rrr
19265
19266 uuu
19267 111
19268 222
19269 333
19270
19271 + 666
19272 777
19273
19274 000
19275 !!!ˇ»"
19276 .unindent(),
19277 );
19278}
19279
19280#[gpui::test]
19281async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
19282 init_test(cx, |_| {});
19283
19284 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
19285 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
19286
19287 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
19288 let multi_buffer = cx.new(|cx| {
19289 let mut multibuffer = MultiBuffer::new(ReadWrite);
19290 multibuffer.push_excerpts(
19291 buffer.clone(),
19292 [
19293 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
19294 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
19295 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
19296 ],
19297 cx,
19298 );
19299 multibuffer
19300 });
19301
19302 let editor =
19303 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19304 editor
19305 .update(cx, |editor, _window, cx| {
19306 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
19307 editor
19308 .buffer
19309 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
19310 })
19311 .unwrap();
19312
19313 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19314 cx.run_until_parked();
19315
19316 cx.update_editor(|editor, window, cx| {
19317 editor.expand_all_diff_hunks(&Default::default(), window, cx)
19318 });
19319 cx.executor().run_until_parked();
19320
19321 // When the start of a hunk coincides with the start of its excerpt,
19322 // the hunk is expanded. When the start of a hunk is earlier than
19323 // the start of its excerpt, the hunk is not expanded.
19324 cx.assert_state_with_diff(
19325 "
19326 ˇaaa
19327 - bbb
19328 + BBB
19329
19330 - ddd
19331 - eee
19332 + DDD
19333 + EEE
19334 fff
19335
19336 iii
19337 "
19338 .unindent(),
19339 );
19340}
19341
19342#[gpui::test]
19343async fn test_edits_around_expanded_insertion_hunks(
19344 executor: BackgroundExecutor,
19345 cx: &mut TestAppContext,
19346) {
19347 init_test(cx, |_| {});
19348
19349 let mut cx = EditorTestContext::new(cx).await;
19350
19351 let diff_base = r#"
19352 use some::mod1;
19353 use some::mod2;
19354
19355 const A: u32 = 42;
19356
19357 fn main() {
19358 println!("hello");
19359
19360 println!("world");
19361 }
19362 "#
19363 .unindent();
19364 executor.run_until_parked();
19365 cx.set_state(
19366 &r#"
19367 use some::mod1;
19368 use some::mod2;
19369
19370 const A: u32 = 42;
19371 const B: u32 = 42;
19372 const C: u32 = 42;
19373 ˇ
19374
19375 fn main() {
19376 println!("hello");
19377
19378 println!("world");
19379 }
19380 "#
19381 .unindent(),
19382 );
19383
19384 cx.set_head_text(&diff_base);
19385 executor.run_until_parked();
19386
19387 cx.update_editor(|editor, window, cx| {
19388 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19389 });
19390 executor.run_until_parked();
19391
19392 cx.assert_state_with_diff(
19393 r#"
19394 use some::mod1;
19395 use some::mod2;
19396
19397 const A: u32 = 42;
19398 + const B: u32 = 42;
19399 + const C: u32 = 42;
19400 + ˇ
19401
19402 fn main() {
19403 println!("hello");
19404
19405 println!("world");
19406 }
19407 "#
19408 .unindent(),
19409 );
19410
19411 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
19412 executor.run_until_parked();
19413
19414 cx.assert_state_with_diff(
19415 r#"
19416 use some::mod1;
19417 use some::mod2;
19418
19419 const A: u32 = 42;
19420 + const B: u32 = 42;
19421 + const C: u32 = 42;
19422 + const D: u32 = 42;
19423 + ˇ
19424
19425 fn main() {
19426 println!("hello");
19427
19428 println!("world");
19429 }
19430 "#
19431 .unindent(),
19432 );
19433
19434 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
19435 executor.run_until_parked();
19436
19437 cx.assert_state_with_diff(
19438 r#"
19439 use some::mod1;
19440 use some::mod2;
19441
19442 const A: u32 = 42;
19443 + const B: u32 = 42;
19444 + const C: u32 = 42;
19445 + const D: u32 = 42;
19446 + const E: u32 = 42;
19447 + ˇ
19448
19449 fn main() {
19450 println!("hello");
19451
19452 println!("world");
19453 }
19454 "#
19455 .unindent(),
19456 );
19457
19458 cx.update_editor(|editor, window, cx| {
19459 editor.delete_line(&DeleteLine, window, cx);
19460 });
19461 executor.run_until_parked();
19462
19463 cx.assert_state_with_diff(
19464 r#"
19465 use some::mod1;
19466 use some::mod2;
19467
19468 const A: u32 = 42;
19469 + const B: u32 = 42;
19470 + const C: u32 = 42;
19471 + const D: u32 = 42;
19472 + const E: u32 = 42;
19473 ˇ
19474 fn main() {
19475 println!("hello");
19476
19477 println!("world");
19478 }
19479 "#
19480 .unindent(),
19481 );
19482
19483 cx.update_editor(|editor, window, cx| {
19484 editor.move_up(&MoveUp, window, cx);
19485 editor.delete_line(&DeleteLine, window, cx);
19486 editor.move_up(&MoveUp, window, cx);
19487 editor.delete_line(&DeleteLine, window, cx);
19488 editor.move_up(&MoveUp, window, cx);
19489 editor.delete_line(&DeleteLine, window, cx);
19490 });
19491 executor.run_until_parked();
19492 cx.assert_state_with_diff(
19493 r#"
19494 use some::mod1;
19495 use some::mod2;
19496
19497 const A: u32 = 42;
19498 + const B: u32 = 42;
19499 ˇ
19500 fn main() {
19501 println!("hello");
19502
19503 println!("world");
19504 }
19505 "#
19506 .unindent(),
19507 );
19508
19509 cx.update_editor(|editor, window, cx| {
19510 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
19511 editor.delete_line(&DeleteLine, window, cx);
19512 });
19513 executor.run_until_parked();
19514 cx.assert_state_with_diff(
19515 r#"
19516 ˇ
19517 fn main() {
19518 println!("hello");
19519
19520 println!("world");
19521 }
19522 "#
19523 .unindent(),
19524 );
19525}
19526
19527#[gpui::test]
19528async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
19529 init_test(cx, |_| {});
19530
19531 let mut cx = EditorTestContext::new(cx).await;
19532 cx.set_head_text(indoc! { "
19533 one
19534 two
19535 three
19536 four
19537 five
19538 "
19539 });
19540 cx.set_state(indoc! { "
19541 one
19542 ˇthree
19543 five
19544 "});
19545 cx.run_until_parked();
19546 cx.update_editor(|editor, window, cx| {
19547 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19548 });
19549 cx.assert_state_with_diff(
19550 indoc! { "
19551 one
19552 - two
19553 ˇthree
19554 - four
19555 five
19556 "}
19557 .to_string(),
19558 );
19559 cx.update_editor(|editor, window, cx| {
19560 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19561 });
19562
19563 cx.assert_state_with_diff(
19564 indoc! { "
19565 one
19566 ˇthree
19567 five
19568 "}
19569 .to_string(),
19570 );
19571
19572 cx.set_state(indoc! { "
19573 one
19574 ˇTWO
19575 three
19576 four
19577 five
19578 "});
19579 cx.run_until_parked();
19580 cx.update_editor(|editor, window, cx| {
19581 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19582 });
19583
19584 cx.assert_state_with_diff(
19585 indoc! { "
19586 one
19587 - two
19588 + ˇTWO
19589 three
19590 four
19591 five
19592 "}
19593 .to_string(),
19594 );
19595 cx.update_editor(|editor, window, cx| {
19596 editor.move_up(&Default::default(), window, cx);
19597 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19598 });
19599 cx.assert_state_with_diff(
19600 indoc! { "
19601 one
19602 ˇTWO
19603 three
19604 four
19605 five
19606 "}
19607 .to_string(),
19608 );
19609}
19610
19611#[gpui::test]
19612async fn test_edits_around_expanded_deletion_hunks(
19613 executor: BackgroundExecutor,
19614 cx: &mut TestAppContext,
19615) {
19616 init_test(cx, |_| {});
19617
19618 let mut cx = EditorTestContext::new(cx).await;
19619
19620 let diff_base = r#"
19621 use some::mod1;
19622 use some::mod2;
19623
19624 const A: u32 = 42;
19625 const B: u32 = 42;
19626 const C: u32 = 42;
19627
19628
19629 fn main() {
19630 println!("hello");
19631
19632 println!("world");
19633 }
19634 "#
19635 .unindent();
19636 executor.run_until_parked();
19637 cx.set_state(
19638 &r#"
19639 use some::mod1;
19640 use some::mod2;
19641
19642 ˇconst B: u32 = 42;
19643 const C: u32 = 42;
19644
19645
19646 fn main() {
19647 println!("hello");
19648
19649 println!("world");
19650 }
19651 "#
19652 .unindent(),
19653 );
19654
19655 cx.set_head_text(&diff_base);
19656 executor.run_until_parked();
19657
19658 cx.update_editor(|editor, window, cx| {
19659 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19660 });
19661 executor.run_until_parked();
19662
19663 cx.assert_state_with_diff(
19664 r#"
19665 use some::mod1;
19666 use some::mod2;
19667
19668 - const A: u32 = 42;
19669 ˇconst B: u32 = 42;
19670 const C: u32 = 42;
19671
19672
19673 fn main() {
19674 println!("hello");
19675
19676 println!("world");
19677 }
19678 "#
19679 .unindent(),
19680 );
19681
19682 cx.update_editor(|editor, window, cx| {
19683 editor.delete_line(&DeleteLine, window, cx);
19684 });
19685 executor.run_until_parked();
19686 cx.assert_state_with_diff(
19687 r#"
19688 use some::mod1;
19689 use some::mod2;
19690
19691 - const A: u32 = 42;
19692 - const B: u32 = 42;
19693 ˇconst C: u32 = 42;
19694
19695
19696 fn main() {
19697 println!("hello");
19698
19699 println!("world");
19700 }
19701 "#
19702 .unindent(),
19703 );
19704
19705 cx.update_editor(|editor, window, cx| {
19706 editor.delete_line(&DeleteLine, window, cx);
19707 });
19708 executor.run_until_parked();
19709 cx.assert_state_with_diff(
19710 r#"
19711 use some::mod1;
19712 use some::mod2;
19713
19714 - const A: u32 = 42;
19715 - const B: u32 = 42;
19716 - const C: u32 = 42;
19717 ˇ
19718
19719 fn main() {
19720 println!("hello");
19721
19722 println!("world");
19723 }
19724 "#
19725 .unindent(),
19726 );
19727
19728 cx.update_editor(|editor, window, cx| {
19729 editor.handle_input("replacement", window, cx);
19730 });
19731 executor.run_until_parked();
19732 cx.assert_state_with_diff(
19733 r#"
19734 use some::mod1;
19735 use some::mod2;
19736
19737 - const A: u32 = 42;
19738 - const B: u32 = 42;
19739 - const C: u32 = 42;
19740 -
19741 + replacementˇ
19742
19743 fn main() {
19744 println!("hello");
19745
19746 println!("world");
19747 }
19748 "#
19749 .unindent(),
19750 );
19751}
19752
19753#[gpui::test]
19754async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19755 init_test(cx, |_| {});
19756
19757 let mut cx = EditorTestContext::new(cx).await;
19758
19759 let base_text = r#"
19760 one
19761 two
19762 three
19763 four
19764 five
19765 "#
19766 .unindent();
19767 executor.run_until_parked();
19768 cx.set_state(
19769 &r#"
19770 one
19771 two
19772 fˇour
19773 five
19774 "#
19775 .unindent(),
19776 );
19777
19778 cx.set_head_text(&base_text);
19779 executor.run_until_parked();
19780
19781 cx.update_editor(|editor, window, cx| {
19782 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19783 });
19784 executor.run_until_parked();
19785
19786 cx.assert_state_with_diff(
19787 r#"
19788 one
19789 two
19790 - three
19791 fˇour
19792 five
19793 "#
19794 .unindent(),
19795 );
19796
19797 cx.update_editor(|editor, window, cx| {
19798 editor.backspace(&Backspace, window, cx);
19799 editor.backspace(&Backspace, window, cx);
19800 });
19801 executor.run_until_parked();
19802 cx.assert_state_with_diff(
19803 r#"
19804 one
19805 two
19806 - threeˇ
19807 - four
19808 + our
19809 five
19810 "#
19811 .unindent(),
19812 );
19813}
19814
19815#[gpui::test]
19816async fn test_edit_after_expanded_modification_hunk(
19817 executor: BackgroundExecutor,
19818 cx: &mut TestAppContext,
19819) {
19820 init_test(cx, |_| {});
19821
19822 let mut cx = EditorTestContext::new(cx).await;
19823
19824 let diff_base = r#"
19825 use some::mod1;
19826 use some::mod2;
19827
19828 const A: u32 = 42;
19829 const B: u32 = 42;
19830 const C: u32 = 42;
19831 const D: u32 = 42;
19832
19833
19834 fn main() {
19835 println!("hello");
19836
19837 println!("world");
19838 }"#
19839 .unindent();
19840
19841 cx.set_state(
19842 &r#"
19843 use some::mod1;
19844 use some::mod2;
19845
19846 const A: u32 = 42;
19847 const B: u32 = 42;
19848 const C: u32 = 43ˇ
19849 const D: u32 = 42;
19850
19851
19852 fn main() {
19853 println!("hello");
19854
19855 println!("world");
19856 }"#
19857 .unindent(),
19858 );
19859
19860 cx.set_head_text(&diff_base);
19861 executor.run_until_parked();
19862 cx.update_editor(|editor, window, cx| {
19863 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19864 });
19865 executor.run_until_parked();
19866
19867 cx.assert_state_with_diff(
19868 r#"
19869 use some::mod1;
19870 use some::mod2;
19871
19872 const A: u32 = 42;
19873 const B: u32 = 42;
19874 - const C: u32 = 42;
19875 + const C: u32 = 43ˇ
19876 const D: u32 = 42;
19877
19878
19879 fn main() {
19880 println!("hello");
19881
19882 println!("world");
19883 }"#
19884 .unindent(),
19885 );
19886
19887 cx.update_editor(|editor, window, cx| {
19888 editor.handle_input("\nnew_line\n", window, cx);
19889 });
19890 executor.run_until_parked();
19891
19892 cx.assert_state_with_diff(
19893 r#"
19894 use some::mod1;
19895 use some::mod2;
19896
19897 const A: u32 = 42;
19898 const B: u32 = 42;
19899 - const C: u32 = 42;
19900 + const C: u32 = 43
19901 + new_line
19902 + ˇ
19903 const D: u32 = 42;
19904
19905
19906 fn main() {
19907 println!("hello");
19908
19909 println!("world");
19910 }"#
19911 .unindent(),
19912 );
19913}
19914
19915#[gpui::test]
19916async fn test_stage_and_unstage_added_file_hunk(
19917 executor: BackgroundExecutor,
19918 cx: &mut TestAppContext,
19919) {
19920 init_test(cx, |_| {});
19921
19922 let mut cx = EditorTestContext::new(cx).await;
19923 cx.update_editor(|editor, _, cx| {
19924 editor.set_expand_all_diff_hunks(cx);
19925 });
19926
19927 let working_copy = r#"
19928 ˇfn main() {
19929 println!("hello, world!");
19930 }
19931 "#
19932 .unindent();
19933
19934 cx.set_state(&working_copy);
19935 executor.run_until_parked();
19936
19937 cx.assert_state_with_diff(
19938 r#"
19939 + ˇfn main() {
19940 + println!("hello, world!");
19941 + }
19942 "#
19943 .unindent(),
19944 );
19945 cx.assert_index_text(None);
19946
19947 cx.update_editor(|editor, window, cx| {
19948 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19949 });
19950 executor.run_until_parked();
19951 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
19952 cx.assert_state_with_diff(
19953 r#"
19954 + ˇfn main() {
19955 + println!("hello, world!");
19956 + }
19957 "#
19958 .unindent(),
19959 );
19960
19961 cx.update_editor(|editor, window, cx| {
19962 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19963 });
19964 executor.run_until_parked();
19965 cx.assert_index_text(None);
19966}
19967
19968async fn setup_indent_guides_editor(
19969 text: &str,
19970 cx: &mut TestAppContext,
19971) -> (BufferId, EditorTestContext) {
19972 init_test(cx, |_| {});
19973
19974 let mut cx = EditorTestContext::new(cx).await;
19975
19976 let buffer_id = cx.update_editor(|editor, window, cx| {
19977 editor.set_text(text, window, cx);
19978 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
19979
19980 buffer_ids[0]
19981 });
19982
19983 (buffer_id, cx)
19984}
19985
19986fn assert_indent_guides(
19987 range: Range<u32>,
19988 expected: Vec<IndentGuide>,
19989 active_indices: Option<Vec<usize>>,
19990 cx: &mut EditorTestContext,
19991) {
19992 let indent_guides = cx.update_editor(|editor, window, cx| {
19993 let snapshot = editor.snapshot(window, cx).display_snapshot;
19994 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
19995 editor,
19996 MultiBufferRow(range.start)..MultiBufferRow(range.end),
19997 true,
19998 &snapshot,
19999 cx,
20000 );
20001
20002 indent_guides.sort_by(|a, b| {
20003 a.depth.cmp(&b.depth).then(
20004 a.start_row
20005 .cmp(&b.start_row)
20006 .then(a.end_row.cmp(&b.end_row)),
20007 )
20008 });
20009 indent_guides
20010 });
20011
20012 if let Some(expected) = active_indices {
20013 let active_indices = cx.update_editor(|editor, window, cx| {
20014 let snapshot = editor.snapshot(window, cx).display_snapshot;
20015 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
20016 });
20017
20018 assert_eq!(
20019 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
20020 expected,
20021 "Active indent guide indices do not match"
20022 );
20023 }
20024
20025 assert_eq!(indent_guides, expected, "Indent guides do not match");
20026}
20027
20028fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
20029 IndentGuide {
20030 buffer_id,
20031 start_row: MultiBufferRow(start_row),
20032 end_row: MultiBufferRow(end_row),
20033 depth,
20034 tab_size: 4,
20035 settings: IndentGuideSettings {
20036 enabled: true,
20037 line_width: 1,
20038 active_line_width: 1,
20039 coloring: IndentGuideColoring::default(),
20040 background_coloring: IndentGuideBackgroundColoring::default(),
20041 },
20042 }
20043}
20044
20045#[gpui::test]
20046async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
20047 let (buffer_id, mut cx) = setup_indent_guides_editor(
20048 &"
20049 fn main() {
20050 let a = 1;
20051 }"
20052 .unindent(),
20053 cx,
20054 )
20055 .await;
20056
20057 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20058}
20059
20060#[gpui::test]
20061async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
20062 let (buffer_id, mut cx) = setup_indent_guides_editor(
20063 &"
20064 fn main() {
20065 let a = 1;
20066 let b = 2;
20067 }"
20068 .unindent(),
20069 cx,
20070 )
20071 .await;
20072
20073 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
20074}
20075
20076#[gpui::test]
20077async fn test_indent_guide_nested(cx: &mut TestAppContext) {
20078 let (buffer_id, mut cx) = setup_indent_guides_editor(
20079 &"
20080 fn main() {
20081 let a = 1;
20082 if a == 3 {
20083 let b = 2;
20084 } else {
20085 let c = 3;
20086 }
20087 }"
20088 .unindent(),
20089 cx,
20090 )
20091 .await;
20092
20093 assert_indent_guides(
20094 0..8,
20095 vec![
20096 indent_guide(buffer_id, 1, 6, 0),
20097 indent_guide(buffer_id, 3, 3, 1),
20098 indent_guide(buffer_id, 5, 5, 1),
20099 ],
20100 None,
20101 &mut cx,
20102 );
20103}
20104
20105#[gpui::test]
20106async fn test_indent_guide_tab(cx: &mut TestAppContext) {
20107 let (buffer_id, mut cx) = setup_indent_guides_editor(
20108 &"
20109 fn main() {
20110 let a = 1;
20111 let b = 2;
20112 let c = 3;
20113 }"
20114 .unindent(),
20115 cx,
20116 )
20117 .await;
20118
20119 assert_indent_guides(
20120 0..5,
20121 vec![
20122 indent_guide(buffer_id, 1, 3, 0),
20123 indent_guide(buffer_id, 2, 2, 1),
20124 ],
20125 None,
20126 &mut cx,
20127 );
20128}
20129
20130#[gpui::test]
20131async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
20132 let (buffer_id, mut cx) = setup_indent_guides_editor(
20133 &"
20134 fn main() {
20135 let a = 1;
20136
20137 let c = 3;
20138 }"
20139 .unindent(),
20140 cx,
20141 )
20142 .await;
20143
20144 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
20145}
20146
20147#[gpui::test]
20148async fn test_indent_guide_complex(cx: &mut TestAppContext) {
20149 let (buffer_id, mut cx) = setup_indent_guides_editor(
20150 &"
20151 fn main() {
20152 let a = 1;
20153
20154 let c = 3;
20155
20156 if a == 3 {
20157 let b = 2;
20158 } else {
20159 let c = 3;
20160 }
20161 }"
20162 .unindent(),
20163 cx,
20164 )
20165 .await;
20166
20167 assert_indent_guides(
20168 0..11,
20169 vec![
20170 indent_guide(buffer_id, 1, 9, 0),
20171 indent_guide(buffer_id, 6, 6, 1),
20172 indent_guide(buffer_id, 8, 8, 1),
20173 ],
20174 None,
20175 &mut cx,
20176 );
20177}
20178
20179#[gpui::test]
20180async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
20181 let (buffer_id, mut cx) = setup_indent_guides_editor(
20182 &"
20183 fn main() {
20184 let a = 1;
20185
20186 let c = 3;
20187
20188 if a == 3 {
20189 let b = 2;
20190 } else {
20191 let c = 3;
20192 }
20193 }"
20194 .unindent(),
20195 cx,
20196 )
20197 .await;
20198
20199 assert_indent_guides(
20200 1..11,
20201 vec![
20202 indent_guide(buffer_id, 1, 9, 0),
20203 indent_guide(buffer_id, 6, 6, 1),
20204 indent_guide(buffer_id, 8, 8, 1),
20205 ],
20206 None,
20207 &mut cx,
20208 );
20209}
20210
20211#[gpui::test]
20212async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
20213 let (buffer_id, mut cx) = setup_indent_guides_editor(
20214 &"
20215 fn main() {
20216 let a = 1;
20217
20218 let c = 3;
20219
20220 if a == 3 {
20221 let b = 2;
20222 } else {
20223 let c = 3;
20224 }
20225 }"
20226 .unindent(),
20227 cx,
20228 )
20229 .await;
20230
20231 assert_indent_guides(
20232 1..10,
20233 vec![
20234 indent_guide(buffer_id, 1, 9, 0),
20235 indent_guide(buffer_id, 6, 6, 1),
20236 indent_guide(buffer_id, 8, 8, 1),
20237 ],
20238 None,
20239 &mut cx,
20240 );
20241}
20242
20243#[gpui::test]
20244async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
20245 let (buffer_id, mut cx) = setup_indent_guides_editor(
20246 &"
20247 fn main() {
20248 if a {
20249 b(
20250 c,
20251 d,
20252 )
20253 } else {
20254 e(
20255 f
20256 )
20257 }
20258 }"
20259 .unindent(),
20260 cx,
20261 )
20262 .await;
20263
20264 assert_indent_guides(
20265 0..11,
20266 vec![
20267 indent_guide(buffer_id, 1, 10, 0),
20268 indent_guide(buffer_id, 2, 5, 1),
20269 indent_guide(buffer_id, 7, 9, 1),
20270 indent_guide(buffer_id, 3, 4, 2),
20271 indent_guide(buffer_id, 8, 8, 2),
20272 ],
20273 None,
20274 &mut cx,
20275 );
20276
20277 cx.update_editor(|editor, window, cx| {
20278 editor.fold_at(MultiBufferRow(2), window, cx);
20279 assert_eq!(
20280 editor.display_text(cx),
20281 "
20282 fn main() {
20283 if a {
20284 b(⋯
20285 )
20286 } else {
20287 e(
20288 f
20289 )
20290 }
20291 }"
20292 .unindent()
20293 );
20294 });
20295
20296 assert_indent_guides(
20297 0..11,
20298 vec![
20299 indent_guide(buffer_id, 1, 10, 0),
20300 indent_guide(buffer_id, 2, 5, 1),
20301 indent_guide(buffer_id, 7, 9, 1),
20302 indent_guide(buffer_id, 8, 8, 2),
20303 ],
20304 None,
20305 &mut cx,
20306 );
20307}
20308
20309#[gpui::test]
20310async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
20311 let (buffer_id, mut cx) = setup_indent_guides_editor(
20312 &"
20313 block1
20314 block2
20315 block3
20316 block4
20317 block2
20318 block1
20319 block1"
20320 .unindent(),
20321 cx,
20322 )
20323 .await;
20324
20325 assert_indent_guides(
20326 1..10,
20327 vec![
20328 indent_guide(buffer_id, 1, 4, 0),
20329 indent_guide(buffer_id, 2, 3, 1),
20330 indent_guide(buffer_id, 3, 3, 2),
20331 ],
20332 None,
20333 &mut cx,
20334 );
20335}
20336
20337#[gpui::test]
20338async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
20339 let (buffer_id, mut cx) = setup_indent_guides_editor(
20340 &"
20341 block1
20342 block2
20343 block3
20344
20345 block1
20346 block1"
20347 .unindent(),
20348 cx,
20349 )
20350 .await;
20351
20352 assert_indent_guides(
20353 0..6,
20354 vec![
20355 indent_guide(buffer_id, 1, 2, 0),
20356 indent_guide(buffer_id, 2, 2, 1),
20357 ],
20358 None,
20359 &mut cx,
20360 );
20361}
20362
20363#[gpui::test]
20364async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
20365 let (buffer_id, mut cx) = setup_indent_guides_editor(
20366 &"
20367 function component() {
20368 \treturn (
20369 \t\t\t
20370 \t\t<div>
20371 \t\t\t<abc></abc>
20372 \t\t</div>
20373 \t)
20374 }"
20375 .unindent(),
20376 cx,
20377 )
20378 .await;
20379
20380 assert_indent_guides(
20381 0..8,
20382 vec![
20383 indent_guide(buffer_id, 1, 6, 0),
20384 indent_guide(buffer_id, 2, 5, 1),
20385 indent_guide(buffer_id, 4, 4, 2),
20386 ],
20387 None,
20388 &mut cx,
20389 );
20390}
20391
20392#[gpui::test]
20393async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
20394 let (buffer_id, mut cx) = setup_indent_guides_editor(
20395 &"
20396 function component() {
20397 \treturn (
20398 \t
20399 \t\t<div>
20400 \t\t\t<abc></abc>
20401 \t\t</div>
20402 \t)
20403 }"
20404 .unindent(),
20405 cx,
20406 )
20407 .await;
20408
20409 assert_indent_guides(
20410 0..8,
20411 vec![
20412 indent_guide(buffer_id, 1, 6, 0),
20413 indent_guide(buffer_id, 2, 5, 1),
20414 indent_guide(buffer_id, 4, 4, 2),
20415 ],
20416 None,
20417 &mut cx,
20418 );
20419}
20420
20421#[gpui::test]
20422async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
20423 let (buffer_id, mut cx) = setup_indent_guides_editor(
20424 &"
20425 block1
20426
20427
20428
20429 block2
20430 "
20431 .unindent(),
20432 cx,
20433 )
20434 .await;
20435
20436 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20437}
20438
20439#[gpui::test]
20440async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
20441 let (buffer_id, mut cx) = setup_indent_guides_editor(
20442 &"
20443 def a:
20444 \tb = 3
20445 \tif True:
20446 \t\tc = 4
20447 \t\td = 5
20448 \tprint(b)
20449 "
20450 .unindent(),
20451 cx,
20452 )
20453 .await;
20454
20455 assert_indent_guides(
20456 0..6,
20457 vec![
20458 indent_guide(buffer_id, 1, 5, 0),
20459 indent_guide(buffer_id, 3, 4, 1),
20460 ],
20461 None,
20462 &mut cx,
20463 );
20464}
20465
20466#[gpui::test]
20467async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
20468 let (buffer_id, mut cx) = setup_indent_guides_editor(
20469 &"
20470 fn main() {
20471 let a = 1;
20472 }"
20473 .unindent(),
20474 cx,
20475 )
20476 .await;
20477
20478 cx.update_editor(|editor, window, cx| {
20479 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20480 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20481 });
20482 });
20483
20484 assert_indent_guides(
20485 0..3,
20486 vec![indent_guide(buffer_id, 1, 1, 0)],
20487 Some(vec![0]),
20488 &mut cx,
20489 );
20490}
20491
20492#[gpui::test]
20493async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
20494 let (buffer_id, mut cx) = setup_indent_guides_editor(
20495 &"
20496 fn main() {
20497 if 1 == 2 {
20498 let a = 1;
20499 }
20500 }"
20501 .unindent(),
20502 cx,
20503 )
20504 .await;
20505
20506 cx.update_editor(|editor, window, cx| {
20507 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20508 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20509 });
20510 });
20511
20512 assert_indent_guides(
20513 0..4,
20514 vec![
20515 indent_guide(buffer_id, 1, 3, 0),
20516 indent_guide(buffer_id, 2, 2, 1),
20517 ],
20518 Some(vec![1]),
20519 &mut cx,
20520 );
20521
20522 cx.update_editor(|editor, window, cx| {
20523 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20524 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20525 });
20526 });
20527
20528 assert_indent_guides(
20529 0..4,
20530 vec![
20531 indent_guide(buffer_id, 1, 3, 0),
20532 indent_guide(buffer_id, 2, 2, 1),
20533 ],
20534 Some(vec![1]),
20535 &mut cx,
20536 );
20537
20538 cx.update_editor(|editor, window, cx| {
20539 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20540 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
20541 });
20542 });
20543
20544 assert_indent_guides(
20545 0..4,
20546 vec![
20547 indent_guide(buffer_id, 1, 3, 0),
20548 indent_guide(buffer_id, 2, 2, 1),
20549 ],
20550 Some(vec![0]),
20551 &mut cx,
20552 );
20553}
20554
20555#[gpui::test]
20556async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
20557 let (buffer_id, mut cx) = setup_indent_guides_editor(
20558 &"
20559 fn main() {
20560 let a = 1;
20561
20562 let b = 2;
20563 }"
20564 .unindent(),
20565 cx,
20566 )
20567 .await;
20568
20569 cx.update_editor(|editor, window, cx| {
20570 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20571 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20572 });
20573 });
20574
20575 assert_indent_guides(
20576 0..5,
20577 vec![indent_guide(buffer_id, 1, 3, 0)],
20578 Some(vec![0]),
20579 &mut cx,
20580 );
20581}
20582
20583#[gpui::test]
20584async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
20585 let (buffer_id, mut cx) = setup_indent_guides_editor(
20586 &"
20587 def m:
20588 a = 1
20589 pass"
20590 .unindent(),
20591 cx,
20592 )
20593 .await;
20594
20595 cx.update_editor(|editor, window, cx| {
20596 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20597 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20598 });
20599 });
20600
20601 assert_indent_guides(
20602 0..3,
20603 vec![indent_guide(buffer_id, 1, 2, 0)],
20604 Some(vec![0]),
20605 &mut cx,
20606 );
20607}
20608
20609#[gpui::test]
20610async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
20611 init_test(cx, |_| {});
20612 let mut cx = EditorTestContext::new(cx).await;
20613 let text = indoc! {
20614 "
20615 impl A {
20616 fn b() {
20617 0;
20618 3;
20619 5;
20620 6;
20621 7;
20622 }
20623 }
20624 "
20625 };
20626 let base_text = indoc! {
20627 "
20628 impl A {
20629 fn b() {
20630 0;
20631 1;
20632 2;
20633 3;
20634 4;
20635 }
20636 fn c() {
20637 5;
20638 6;
20639 7;
20640 }
20641 }
20642 "
20643 };
20644
20645 cx.update_editor(|editor, window, cx| {
20646 editor.set_text(text, window, cx);
20647
20648 editor.buffer().update(cx, |multibuffer, cx| {
20649 let buffer = multibuffer.as_singleton().unwrap();
20650 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
20651
20652 multibuffer.set_all_diff_hunks_expanded(cx);
20653 multibuffer.add_diff(diff, cx);
20654
20655 buffer.read(cx).remote_id()
20656 })
20657 });
20658 cx.run_until_parked();
20659
20660 cx.assert_state_with_diff(
20661 indoc! { "
20662 impl A {
20663 fn b() {
20664 0;
20665 - 1;
20666 - 2;
20667 3;
20668 - 4;
20669 - }
20670 - fn c() {
20671 5;
20672 6;
20673 7;
20674 }
20675 }
20676 ˇ"
20677 }
20678 .to_string(),
20679 );
20680
20681 let mut actual_guides = cx.update_editor(|editor, window, cx| {
20682 editor
20683 .snapshot(window, cx)
20684 .buffer_snapshot
20685 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
20686 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
20687 .collect::<Vec<_>>()
20688 });
20689 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
20690 assert_eq!(
20691 actual_guides,
20692 vec![
20693 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
20694 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
20695 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
20696 ]
20697 );
20698}
20699
20700#[gpui::test]
20701async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
20702 init_test(cx, |_| {});
20703 let mut cx = EditorTestContext::new(cx).await;
20704
20705 let diff_base = r#"
20706 a
20707 b
20708 c
20709 "#
20710 .unindent();
20711
20712 cx.set_state(
20713 &r#"
20714 ˇA
20715 b
20716 C
20717 "#
20718 .unindent(),
20719 );
20720 cx.set_head_text(&diff_base);
20721 cx.update_editor(|editor, window, cx| {
20722 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20723 });
20724 executor.run_until_parked();
20725
20726 let both_hunks_expanded = r#"
20727 - a
20728 + ˇA
20729 b
20730 - c
20731 + C
20732 "#
20733 .unindent();
20734
20735 cx.assert_state_with_diff(both_hunks_expanded.clone());
20736
20737 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20738 let snapshot = editor.snapshot(window, cx);
20739 let hunks = editor
20740 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
20741 .collect::<Vec<_>>();
20742 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20743 let buffer_id = hunks[0].buffer_id;
20744 hunks
20745 .into_iter()
20746 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20747 .collect::<Vec<_>>()
20748 });
20749 assert_eq!(hunk_ranges.len(), 2);
20750
20751 cx.update_editor(|editor, _, cx| {
20752 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20753 });
20754 executor.run_until_parked();
20755
20756 let second_hunk_expanded = r#"
20757 ˇA
20758 b
20759 - c
20760 + C
20761 "#
20762 .unindent();
20763
20764 cx.assert_state_with_diff(second_hunk_expanded);
20765
20766 cx.update_editor(|editor, _, cx| {
20767 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20768 });
20769 executor.run_until_parked();
20770
20771 cx.assert_state_with_diff(both_hunks_expanded.clone());
20772
20773 cx.update_editor(|editor, _, cx| {
20774 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20775 });
20776 executor.run_until_parked();
20777
20778 let first_hunk_expanded = r#"
20779 - a
20780 + ˇA
20781 b
20782 C
20783 "#
20784 .unindent();
20785
20786 cx.assert_state_with_diff(first_hunk_expanded);
20787
20788 cx.update_editor(|editor, _, cx| {
20789 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20790 });
20791 executor.run_until_parked();
20792
20793 cx.assert_state_with_diff(both_hunks_expanded);
20794
20795 cx.set_state(
20796 &r#"
20797 ˇA
20798 b
20799 "#
20800 .unindent(),
20801 );
20802 cx.run_until_parked();
20803
20804 // TODO this cursor position seems bad
20805 cx.assert_state_with_diff(
20806 r#"
20807 - ˇa
20808 + A
20809 b
20810 "#
20811 .unindent(),
20812 );
20813
20814 cx.update_editor(|editor, window, cx| {
20815 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20816 });
20817
20818 cx.assert_state_with_diff(
20819 r#"
20820 - ˇa
20821 + A
20822 b
20823 - c
20824 "#
20825 .unindent(),
20826 );
20827
20828 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20829 let snapshot = editor.snapshot(window, cx);
20830 let hunks = editor
20831 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
20832 .collect::<Vec<_>>();
20833 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20834 let buffer_id = hunks[0].buffer_id;
20835 hunks
20836 .into_iter()
20837 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20838 .collect::<Vec<_>>()
20839 });
20840 assert_eq!(hunk_ranges.len(), 2);
20841
20842 cx.update_editor(|editor, _, cx| {
20843 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20844 });
20845 executor.run_until_parked();
20846
20847 cx.assert_state_with_diff(
20848 r#"
20849 - ˇa
20850 + A
20851 b
20852 "#
20853 .unindent(),
20854 );
20855}
20856
20857#[gpui::test]
20858async fn test_toggle_deletion_hunk_at_start_of_file(
20859 executor: BackgroundExecutor,
20860 cx: &mut TestAppContext,
20861) {
20862 init_test(cx, |_| {});
20863 let mut cx = EditorTestContext::new(cx).await;
20864
20865 let diff_base = r#"
20866 a
20867 b
20868 c
20869 "#
20870 .unindent();
20871
20872 cx.set_state(
20873 &r#"
20874 ˇb
20875 c
20876 "#
20877 .unindent(),
20878 );
20879 cx.set_head_text(&diff_base);
20880 cx.update_editor(|editor, window, cx| {
20881 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20882 });
20883 executor.run_until_parked();
20884
20885 let hunk_expanded = r#"
20886 - a
20887 ˇb
20888 c
20889 "#
20890 .unindent();
20891
20892 cx.assert_state_with_diff(hunk_expanded.clone());
20893
20894 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20895 let snapshot = editor.snapshot(window, cx);
20896 let hunks = editor
20897 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
20898 .collect::<Vec<_>>();
20899 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20900 let buffer_id = hunks[0].buffer_id;
20901 hunks
20902 .into_iter()
20903 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20904 .collect::<Vec<_>>()
20905 });
20906 assert_eq!(hunk_ranges.len(), 1);
20907
20908 cx.update_editor(|editor, _, cx| {
20909 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20910 });
20911 executor.run_until_parked();
20912
20913 let hunk_collapsed = r#"
20914 ˇb
20915 c
20916 "#
20917 .unindent();
20918
20919 cx.assert_state_with_diff(hunk_collapsed);
20920
20921 cx.update_editor(|editor, _, cx| {
20922 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20923 });
20924 executor.run_until_parked();
20925
20926 cx.assert_state_with_diff(hunk_expanded);
20927}
20928
20929#[gpui::test]
20930async fn test_display_diff_hunks(cx: &mut TestAppContext) {
20931 init_test(cx, |_| {});
20932
20933 let fs = FakeFs::new(cx.executor());
20934 fs.insert_tree(
20935 path!("/test"),
20936 json!({
20937 ".git": {},
20938 "file-1": "ONE\n",
20939 "file-2": "TWO\n",
20940 "file-3": "THREE\n",
20941 }),
20942 )
20943 .await;
20944
20945 fs.set_head_for_repo(
20946 path!("/test/.git").as_ref(),
20947 &[
20948 ("file-1", "one\n".into()),
20949 ("file-2", "two\n".into()),
20950 ("file-3", "three\n".into()),
20951 ],
20952 "deadbeef",
20953 );
20954
20955 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
20956 let mut buffers = vec![];
20957 for i in 1..=3 {
20958 let buffer = project
20959 .update(cx, |project, cx| {
20960 let path = format!(path!("/test/file-{}"), i);
20961 project.open_local_buffer(path, cx)
20962 })
20963 .await
20964 .unwrap();
20965 buffers.push(buffer);
20966 }
20967
20968 let multibuffer = cx.new(|cx| {
20969 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
20970 multibuffer.set_all_diff_hunks_expanded(cx);
20971 for buffer in &buffers {
20972 let snapshot = buffer.read(cx).snapshot();
20973 multibuffer.set_excerpts_for_path(
20974 PathKey::namespaced(
20975 0,
20976 buffer.read(cx).file().unwrap().path().as_unix_str().into(),
20977 ),
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}