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! {"
6746 The quick« brownˇ»
6747 fox jumps overˇ
6748 the lazy dog"});
6749 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6750 cx.assert_editor_state(indoc! {"
6751 The quickˇ
6752 ˇthe lazy dog"});
6753
6754 cx.set_state(indoc! {"
6755 The quick« brownˇ»
6756 fox jumps overˇ
6757 the lazy dog"});
6758 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
6759 cx.assert_editor_state(indoc! {"
6760 The quickˇ
6761 fox jumps overˇthe lazy dog"});
6762
6763 cx.set_state(indoc! {"
6764 The quick« brownˇ»
6765 fox jumps overˇ
6766 the lazy dog"});
6767 cx.update_editor(|e, window, cx| {
6768 e.cut_to_end_of_line(
6769 &CutToEndOfLine {
6770 stop_at_newlines: true,
6771 },
6772 window,
6773 cx,
6774 )
6775 });
6776 cx.assert_editor_state(indoc! {"
6777 The quickˇ
6778 fox jumps overˇ
6779 the lazy dog"});
6780
6781 cx.set_state(indoc! {"
6782 The quick« brownˇ»
6783 fox jumps overˇ
6784 the lazy dog"});
6785 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
6786 cx.assert_editor_state(indoc! {"
6787 The quickˇ
6788 fox jumps overˇthe lazy dog"});
6789}
6790
6791#[gpui::test]
6792async fn test_clipboard(cx: &mut TestAppContext) {
6793 init_test(cx, |_| {});
6794
6795 let mut cx = EditorTestContext::new(cx).await;
6796
6797 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
6798 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6799 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
6800
6801 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
6802 cx.set_state("two ˇfour ˇsix ˇ");
6803 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6804 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
6805
6806 // Paste again but with only two cursors. Since the number of cursors doesn't
6807 // match the number of slices in the clipboard, the entire clipboard text
6808 // is pasted at each cursor.
6809 cx.set_state("ˇtwo one✅ four three six five ˇ");
6810 cx.update_editor(|e, window, cx| {
6811 e.handle_input("( ", window, cx);
6812 e.paste(&Paste, window, cx);
6813 e.handle_input(") ", window, cx);
6814 });
6815 cx.assert_editor_state(
6816 &([
6817 "( one✅ ",
6818 "three ",
6819 "five ) ˇtwo one✅ four three six five ( one✅ ",
6820 "three ",
6821 "five ) ˇ",
6822 ]
6823 .join("\n")),
6824 );
6825
6826 // Cut with three selections, one of which is full-line.
6827 cx.set_state(indoc! {"
6828 1«2ˇ»3
6829 4ˇ567
6830 «8ˇ»9"});
6831 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6832 cx.assert_editor_state(indoc! {"
6833 1ˇ3
6834 ˇ9"});
6835
6836 // Paste with three selections, noticing how the copied selection that was full-line
6837 // gets inserted before the second cursor.
6838 cx.set_state(indoc! {"
6839 1ˇ3
6840 9ˇ
6841 «oˇ»ne"});
6842 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6843 cx.assert_editor_state(indoc! {"
6844 12ˇ3
6845 4567
6846 9ˇ
6847 8ˇne"});
6848
6849 // Copy with a single cursor only, which writes the whole line into the clipboard.
6850 cx.set_state(indoc! {"
6851 The quick brown
6852 fox juˇmps over
6853 the lazy dog"});
6854 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6855 assert_eq!(
6856 cx.read_from_clipboard()
6857 .and_then(|item| item.text().as_deref().map(str::to_string)),
6858 Some("fox jumps over\n".to_string())
6859 );
6860
6861 // Paste with three selections, noticing how the copied full-line selection is inserted
6862 // before the empty selections but replaces the selection that is non-empty.
6863 cx.set_state(indoc! {"
6864 Tˇhe quick brown
6865 «foˇ»x jumps over
6866 tˇhe lazy dog"});
6867 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6868 cx.assert_editor_state(indoc! {"
6869 fox jumps over
6870 Tˇhe quick brown
6871 fox jumps over
6872 ˇx jumps over
6873 fox jumps over
6874 tˇhe lazy dog"});
6875}
6876
6877#[gpui::test]
6878async fn test_copy_trim(cx: &mut TestAppContext) {
6879 init_test(cx, |_| {});
6880
6881 let mut cx = EditorTestContext::new(cx).await;
6882 cx.set_state(
6883 r#" «for selection in selections.iter() {
6884 let mut start = selection.start;
6885 let mut end = selection.end;
6886 let is_entire_line = selection.is_empty();
6887 if is_entire_line {
6888 start = Point::new(start.row, 0);ˇ»
6889 end = cmp::min(max_point, Point::new(end.row + 1, 0));
6890 }
6891 "#,
6892 );
6893 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6894 assert_eq!(
6895 cx.read_from_clipboard()
6896 .and_then(|item| item.text().as_deref().map(str::to_string)),
6897 Some(
6898 "for selection in selections.iter() {
6899 let mut start = selection.start;
6900 let mut end = selection.end;
6901 let is_entire_line = selection.is_empty();
6902 if is_entire_line {
6903 start = Point::new(start.row, 0);"
6904 .to_string()
6905 ),
6906 "Regular copying preserves all indentation selected",
6907 );
6908 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6909 assert_eq!(
6910 cx.read_from_clipboard()
6911 .and_then(|item| item.text().as_deref().map(str::to_string)),
6912 Some(
6913 "for selection in selections.iter() {
6914let mut start = selection.start;
6915let mut end = selection.end;
6916let is_entire_line = selection.is_empty();
6917if is_entire_line {
6918 start = Point::new(start.row, 0);"
6919 .to_string()
6920 ),
6921 "Copying with stripping should strip all leading whitespaces"
6922 );
6923
6924 cx.set_state(
6925 r#" « for selection in selections.iter() {
6926 let mut start = selection.start;
6927 let mut end = selection.end;
6928 let is_entire_line = selection.is_empty();
6929 if is_entire_line {
6930 start = Point::new(start.row, 0);ˇ»
6931 end = cmp::min(max_point, Point::new(end.row + 1, 0));
6932 }
6933 "#,
6934 );
6935 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6936 assert_eq!(
6937 cx.read_from_clipboard()
6938 .and_then(|item| item.text().as_deref().map(str::to_string)),
6939 Some(
6940 " for selection in selections.iter() {
6941 let mut start = selection.start;
6942 let mut end = selection.end;
6943 let is_entire_line = selection.is_empty();
6944 if is_entire_line {
6945 start = Point::new(start.row, 0);"
6946 .to_string()
6947 ),
6948 "Regular copying preserves all indentation selected",
6949 );
6950 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6951 assert_eq!(
6952 cx.read_from_clipboard()
6953 .and_then(|item| item.text().as_deref().map(str::to_string)),
6954 Some(
6955 "for selection in selections.iter() {
6956let mut start = selection.start;
6957let mut end = selection.end;
6958let is_entire_line = selection.is_empty();
6959if is_entire_line {
6960 start = Point::new(start.row, 0);"
6961 .to_string()
6962 ),
6963 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
6964 );
6965
6966 cx.set_state(
6967 r#" «ˇ for selection in selections.iter() {
6968 let mut start = selection.start;
6969 let mut end = selection.end;
6970 let is_entire_line = selection.is_empty();
6971 if is_entire_line {
6972 start = Point::new(start.row, 0);»
6973 end = cmp::min(max_point, Point::new(end.row + 1, 0));
6974 }
6975 "#,
6976 );
6977 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6978 assert_eq!(
6979 cx.read_from_clipboard()
6980 .and_then(|item| item.text().as_deref().map(str::to_string)),
6981 Some(
6982 " for selection in selections.iter() {
6983 let mut start = selection.start;
6984 let mut end = selection.end;
6985 let is_entire_line = selection.is_empty();
6986 if is_entire_line {
6987 start = Point::new(start.row, 0);"
6988 .to_string()
6989 ),
6990 "Regular copying for reverse selection works the same",
6991 );
6992 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6993 assert_eq!(
6994 cx.read_from_clipboard()
6995 .and_then(|item| item.text().as_deref().map(str::to_string)),
6996 Some(
6997 "for selection in selections.iter() {
6998let mut start = selection.start;
6999let mut end = selection.end;
7000let is_entire_line = selection.is_empty();
7001if is_entire_line {
7002 start = Point::new(start.row, 0);"
7003 .to_string()
7004 ),
7005 "Copying with stripping for reverse selection works the same"
7006 );
7007
7008 cx.set_state(
7009 r#" for selection «in selections.iter() {
7010 let mut start = selection.start;
7011 let mut end = selection.end;
7012 let is_entire_line = selection.is_empty();
7013 if is_entire_line {
7014 start = Point::new(start.row, 0);ˇ»
7015 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7016 }
7017 "#,
7018 );
7019 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7020 assert_eq!(
7021 cx.read_from_clipboard()
7022 .and_then(|item| item.text().as_deref().map(str::to_string)),
7023 Some(
7024 "in selections.iter() {
7025 let mut start = selection.start;
7026 let mut end = selection.end;
7027 let is_entire_line = selection.is_empty();
7028 if is_entire_line {
7029 start = Point::new(start.row, 0);"
7030 .to_string()
7031 ),
7032 "When selecting past the indent, the copying works as usual",
7033 );
7034 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7035 assert_eq!(
7036 cx.read_from_clipboard()
7037 .and_then(|item| item.text().as_deref().map(str::to_string)),
7038 Some(
7039 "in selections.iter() {
7040 let mut start = selection.start;
7041 let mut end = selection.end;
7042 let is_entire_line = selection.is_empty();
7043 if is_entire_line {
7044 start = Point::new(start.row, 0);"
7045 .to_string()
7046 ),
7047 "When selecting past the indent, nothing is trimmed"
7048 );
7049
7050 cx.set_state(
7051 r#" «for selection in selections.iter() {
7052 let mut start = selection.start;
7053
7054 let mut end = selection.end;
7055 let is_entire_line = selection.is_empty();
7056 if is_entire_line {
7057 start = Point::new(start.row, 0);
7058ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
7059 }
7060 "#,
7061 );
7062 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7063 assert_eq!(
7064 cx.read_from_clipboard()
7065 .and_then(|item| item.text().as_deref().map(str::to_string)),
7066 Some(
7067 "for selection in selections.iter() {
7068let mut start = selection.start;
7069
7070let mut end = selection.end;
7071let is_entire_line = selection.is_empty();
7072if is_entire_line {
7073 start = Point::new(start.row, 0);
7074"
7075 .to_string()
7076 ),
7077 "Copying with stripping should ignore empty lines"
7078 );
7079}
7080
7081#[gpui::test]
7082async fn test_paste_multiline(cx: &mut TestAppContext) {
7083 init_test(cx, |_| {});
7084
7085 let mut cx = EditorTestContext::new(cx).await;
7086 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7087
7088 // Cut an indented block, without the leading whitespace.
7089 cx.set_state(indoc! {"
7090 const a: B = (
7091 c(),
7092 «d(
7093 e,
7094 f
7095 )ˇ»
7096 );
7097 "});
7098 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7099 cx.assert_editor_state(indoc! {"
7100 const a: B = (
7101 c(),
7102 ˇ
7103 );
7104 "});
7105
7106 // Paste it at the same position.
7107 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7108 cx.assert_editor_state(indoc! {"
7109 const a: B = (
7110 c(),
7111 d(
7112 e,
7113 f
7114 )ˇ
7115 );
7116 "});
7117
7118 // Paste it at a line with a lower indent level.
7119 cx.set_state(indoc! {"
7120 ˇ
7121 const a: B = (
7122 c(),
7123 );
7124 "});
7125 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7126 cx.assert_editor_state(indoc! {"
7127 d(
7128 e,
7129 f
7130 )ˇ
7131 const a: B = (
7132 c(),
7133 );
7134 "});
7135
7136 // Cut an indented block, with the leading whitespace.
7137 cx.set_state(indoc! {"
7138 const a: B = (
7139 c(),
7140 « d(
7141 e,
7142 f
7143 )
7144 ˇ»);
7145 "});
7146 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7147 cx.assert_editor_state(indoc! {"
7148 const a: B = (
7149 c(),
7150 ˇ);
7151 "});
7152
7153 // Paste it at the same position.
7154 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7155 cx.assert_editor_state(indoc! {"
7156 const a: B = (
7157 c(),
7158 d(
7159 e,
7160 f
7161 )
7162 ˇ);
7163 "});
7164
7165 // Paste it at a line with a higher indent level.
7166 cx.set_state(indoc! {"
7167 const a: B = (
7168 c(),
7169 d(
7170 e,
7171 fˇ
7172 )
7173 );
7174 "});
7175 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7176 cx.assert_editor_state(indoc! {"
7177 const a: B = (
7178 c(),
7179 d(
7180 e,
7181 f d(
7182 e,
7183 f
7184 )
7185 ˇ
7186 )
7187 );
7188 "});
7189
7190 // Copy an indented block, starting mid-line
7191 cx.set_state(indoc! {"
7192 const a: B = (
7193 c(),
7194 somethin«g(
7195 e,
7196 f
7197 )ˇ»
7198 );
7199 "});
7200 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7201
7202 // Paste it on a line with a lower indent level
7203 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
7204 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7205 cx.assert_editor_state(indoc! {"
7206 const a: B = (
7207 c(),
7208 something(
7209 e,
7210 f
7211 )
7212 );
7213 g(
7214 e,
7215 f
7216 )ˇ"});
7217}
7218
7219#[gpui::test]
7220async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
7221 init_test(cx, |_| {});
7222
7223 cx.write_to_clipboard(ClipboardItem::new_string(
7224 " d(\n e\n );\n".into(),
7225 ));
7226
7227 let mut cx = EditorTestContext::new(cx).await;
7228 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7229
7230 cx.set_state(indoc! {"
7231 fn a() {
7232 b();
7233 if c() {
7234 ˇ
7235 }
7236 }
7237 "});
7238
7239 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7240 cx.assert_editor_state(indoc! {"
7241 fn a() {
7242 b();
7243 if c() {
7244 d(
7245 e
7246 );
7247 ˇ
7248 }
7249 }
7250 "});
7251
7252 cx.set_state(indoc! {"
7253 fn a() {
7254 b();
7255 ˇ
7256 }
7257 "});
7258
7259 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7260 cx.assert_editor_state(indoc! {"
7261 fn a() {
7262 b();
7263 d(
7264 e
7265 );
7266 ˇ
7267 }
7268 "});
7269}
7270
7271#[gpui::test]
7272fn test_select_all(cx: &mut TestAppContext) {
7273 init_test(cx, |_| {});
7274
7275 let editor = cx.add_window(|window, cx| {
7276 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
7277 build_editor(buffer, window, cx)
7278 });
7279 _ = editor.update(cx, |editor, window, cx| {
7280 editor.select_all(&SelectAll, window, cx);
7281 assert_eq!(
7282 editor.selections.display_ranges(cx),
7283 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
7284 );
7285 });
7286}
7287
7288#[gpui::test]
7289fn test_select_line(cx: &mut TestAppContext) {
7290 init_test(cx, |_| {});
7291
7292 let editor = cx.add_window(|window, cx| {
7293 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
7294 build_editor(buffer, window, cx)
7295 });
7296 _ = editor.update(cx, |editor, window, cx| {
7297 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7298 s.select_display_ranges([
7299 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7300 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7301 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7302 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
7303 ])
7304 });
7305 editor.select_line(&SelectLine, window, cx);
7306 assert_eq!(
7307 editor.selections.display_ranges(cx),
7308 vec![
7309 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
7310 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
7311 ]
7312 );
7313 });
7314
7315 _ = editor.update(cx, |editor, window, cx| {
7316 editor.select_line(&SelectLine, window, cx);
7317 assert_eq!(
7318 editor.selections.display_ranges(cx),
7319 vec![
7320 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
7321 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
7322 ]
7323 );
7324 });
7325
7326 _ = editor.update(cx, |editor, window, cx| {
7327 editor.select_line(&SelectLine, window, cx);
7328 assert_eq!(
7329 editor.selections.display_ranges(cx),
7330 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
7331 );
7332 });
7333}
7334
7335#[gpui::test]
7336async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
7337 init_test(cx, |_| {});
7338 let mut cx = EditorTestContext::new(cx).await;
7339
7340 #[track_caller]
7341 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
7342 cx.set_state(initial_state);
7343 cx.update_editor(|e, window, cx| {
7344 e.split_selection_into_lines(&Default::default(), window, cx)
7345 });
7346 cx.assert_editor_state(expected_state);
7347 }
7348
7349 // Selection starts and ends at the middle of lines, left-to-right
7350 test(
7351 &mut cx,
7352 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
7353 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7354 );
7355 // Same thing, right-to-left
7356 test(
7357 &mut cx,
7358 "aa\nb«b\ncc\ndd\neˇ»e\nff",
7359 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7360 );
7361
7362 // Whole buffer, left-to-right, last line *doesn't* end with newline
7363 test(
7364 &mut cx,
7365 "«ˇaa\nbb\ncc\ndd\nee\nff»",
7366 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7367 );
7368 // Same thing, right-to-left
7369 test(
7370 &mut cx,
7371 "«aa\nbb\ncc\ndd\nee\nffˇ»",
7372 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7373 );
7374
7375 // Whole buffer, left-to-right, last line ends with newline
7376 test(
7377 &mut cx,
7378 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
7379 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7380 );
7381 // Same thing, right-to-left
7382 test(
7383 &mut cx,
7384 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
7385 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7386 );
7387
7388 // Starts at the end of a line, ends at the start of another
7389 test(
7390 &mut cx,
7391 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
7392 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
7393 );
7394}
7395
7396#[gpui::test]
7397async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
7398 init_test(cx, |_| {});
7399
7400 let editor = cx.add_window(|window, cx| {
7401 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
7402 build_editor(buffer, window, cx)
7403 });
7404
7405 // setup
7406 _ = editor.update(cx, |editor, window, cx| {
7407 editor.fold_creases(
7408 vec![
7409 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
7410 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
7411 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
7412 ],
7413 true,
7414 window,
7415 cx,
7416 );
7417 assert_eq!(
7418 editor.display_text(cx),
7419 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7420 );
7421 });
7422
7423 _ = editor.update(cx, |editor, window, cx| {
7424 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7425 s.select_display_ranges([
7426 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7427 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7428 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7429 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
7430 ])
7431 });
7432 editor.split_selection_into_lines(&Default::default(), window, cx);
7433 assert_eq!(
7434 editor.display_text(cx),
7435 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7436 );
7437 });
7438 EditorTestContext::for_editor(editor, cx)
7439 .await
7440 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
7441
7442 _ = editor.update(cx, |editor, window, cx| {
7443 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7444 s.select_display_ranges([
7445 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
7446 ])
7447 });
7448 editor.split_selection_into_lines(&Default::default(), window, cx);
7449 assert_eq!(
7450 editor.display_text(cx),
7451 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
7452 );
7453 assert_eq!(
7454 editor.selections.display_ranges(cx),
7455 [
7456 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
7457 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
7458 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
7459 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
7460 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
7461 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
7462 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
7463 ]
7464 );
7465 });
7466 EditorTestContext::for_editor(editor, cx)
7467 .await
7468 .assert_editor_state(
7469 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
7470 );
7471}
7472
7473#[gpui::test]
7474async fn test_add_selection_above_below(cx: &mut TestAppContext) {
7475 init_test(cx, |_| {});
7476
7477 let mut cx = EditorTestContext::new(cx).await;
7478
7479 cx.set_state(indoc!(
7480 r#"abc
7481 defˇghi
7482
7483 jk
7484 nlmo
7485 "#
7486 ));
7487
7488 cx.update_editor(|editor, window, cx| {
7489 editor.add_selection_above(&Default::default(), window, cx);
7490 });
7491
7492 cx.assert_editor_state(indoc!(
7493 r#"abcˇ
7494 defˇghi
7495
7496 jk
7497 nlmo
7498 "#
7499 ));
7500
7501 cx.update_editor(|editor, window, cx| {
7502 editor.add_selection_above(&Default::default(), window, cx);
7503 });
7504
7505 cx.assert_editor_state(indoc!(
7506 r#"abcˇ
7507 defˇghi
7508
7509 jk
7510 nlmo
7511 "#
7512 ));
7513
7514 cx.update_editor(|editor, window, cx| {
7515 editor.add_selection_below(&Default::default(), window, cx);
7516 });
7517
7518 cx.assert_editor_state(indoc!(
7519 r#"abc
7520 defˇghi
7521
7522 jk
7523 nlmo
7524 "#
7525 ));
7526
7527 cx.update_editor(|editor, window, cx| {
7528 editor.undo_selection(&Default::default(), window, cx);
7529 });
7530
7531 cx.assert_editor_state(indoc!(
7532 r#"abcˇ
7533 defˇghi
7534
7535 jk
7536 nlmo
7537 "#
7538 ));
7539
7540 cx.update_editor(|editor, window, cx| {
7541 editor.redo_selection(&Default::default(), window, cx);
7542 });
7543
7544 cx.assert_editor_state(indoc!(
7545 r#"abc
7546 defˇghi
7547
7548 jk
7549 nlmo
7550 "#
7551 ));
7552
7553 cx.update_editor(|editor, window, cx| {
7554 editor.add_selection_below(&Default::default(), window, cx);
7555 });
7556
7557 cx.assert_editor_state(indoc!(
7558 r#"abc
7559 defˇghi
7560 ˇ
7561 jk
7562 nlmo
7563 "#
7564 ));
7565
7566 cx.update_editor(|editor, window, cx| {
7567 editor.add_selection_below(&Default::default(), window, cx);
7568 });
7569
7570 cx.assert_editor_state(indoc!(
7571 r#"abc
7572 defˇghi
7573 ˇ
7574 jkˇ
7575 nlmo
7576 "#
7577 ));
7578
7579 cx.update_editor(|editor, window, cx| {
7580 editor.add_selection_below(&Default::default(), window, cx);
7581 });
7582
7583 cx.assert_editor_state(indoc!(
7584 r#"abc
7585 defˇghi
7586 ˇ
7587 jkˇ
7588 nlmˇo
7589 "#
7590 ));
7591
7592 cx.update_editor(|editor, window, cx| {
7593 editor.add_selection_below(&Default::default(), window, cx);
7594 });
7595
7596 cx.assert_editor_state(indoc!(
7597 r#"abc
7598 defˇghi
7599 ˇ
7600 jkˇ
7601 nlmˇo
7602 ˇ"#
7603 ));
7604
7605 // change selections
7606 cx.set_state(indoc!(
7607 r#"abc
7608 def«ˇg»hi
7609
7610 jk
7611 nlmo
7612 "#
7613 ));
7614
7615 cx.update_editor(|editor, window, cx| {
7616 editor.add_selection_below(&Default::default(), window, cx);
7617 });
7618
7619 cx.assert_editor_state(indoc!(
7620 r#"abc
7621 def«ˇg»hi
7622
7623 jk
7624 nlm«ˇo»
7625 "#
7626 ));
7627
7628 cx.update_editor(|editor, window, cx| {
7629 editor.add_selection_below(&Default::default(), window, cx);
7630 });
7631
7632 cx.assert_editor_state(indoc!(
7633 r#"abc
7634 def«ˇg»hi
7635
7636 jk
7637 nlm«ˇo»
7638 "#
7639 ));
7640
7641 cx.update_editor(|editor, window, cx| {
7642 editor.add_selection_above(&Default::default(), window, cx);
7643 });
7644
7645 cx.assert_editor_state(indoc!(
7646 r#"abc
7647 def«ˇg»hi
7648
7649 jk
7650 nlmo
7651 "#
7652 ));
7653
7654 cx.update_editor(|editor, window, cx| {
7655 editor.add_selection_above(&Default::default(), window, cx);
7656 });
7657
7658 cx.assert_editor_state(indoc!(
7659 r#"abc
7660 def«ˇg»hi
7661
7662 jk
7663 nlmo
7664 "#
7665 ));
7666
7667 // Change selections again
7668 cx.set_state(indoc!(
7669 r#"a«bc
7670 defgˇ»hi
7671
7672 jk
7673 nlmo
7674 "#
7675 ));
7676
7677 cx.update_editor(|editor, window, cx| {
7678 editor.add_selection_below(&Default::default(), window, cx);
7679 });
7680
7681 cx.assert_editor_state(indoc!(
7682 r#"a«bcˇ»
7683 d«efgˇ»hi
7684
7685 j«kˇ»
7686 nlmo
7687 "#
7688 ));
7689
7690 cx.update_editor(|editor, window, cx| {
7691 editor.add_selection_below(&Default::default(), window, cx);
7692 });
7693 cx.assert_editor_state(indoc!(
7694 r#"a«bcˇ»
7695 d«efgˇ»hi
7696
7697 j«kˇ»
7698 n«lmoˇ»
7699 "#
7700 ));
7701 cx.update_editor(|editor, window, cx| {
7702 editor.add_selection_above(&Default::default(), window, cx);
7703 });
7704
7705 cx.assert_editor_state(indoc!(
7706 r#"a«bcˇ»
7707 d«efgˇ»hi
7708
7709 j«kˇ»
7710 nlmo
7711 "#
7712 ));
7713
7714 // Change selections again
7715 cx.set_state(indoc!(
7716 r#"abc
7717 d«ˇefghi
7718
7719 jk
7720 nlm»o
7721 "#
7722 ));
7723
7724 cx.update_editor(|editor, window, cx| {
7725 editor.add_selection_above(&Default::default(), window, cx);
7726 });
7727
7728 cx.assert_editor_state(indoc!(
7729 r#"a«ˇbc»
7730 d«ˇef»ghi
7731
7732 j«ˇk»
7733 n«ˇlm»o
7734 "#
7735 ));
7736
7737 cx.update_editor(|editor, window, cx| {
7738 editor.add_selection_below(&Default::default(), window, cx);
7739 });
7740
7741 cx.assert_editor_state(indoc!(
7742 r#"abc
7743 d«ˇef»ghi
7744
7745 j«ˇk»
7746 n«ˇlm»o
7747 "#
7748 ));
7749}
7750
7751#[gpui::test]
7752async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
7753 init_test(cx, |_| {});
7754 let mut cx = EditorTestContext::new(cx).await;
7755
7756 cx.set_state(indoc!(
7757 r#"line onˇe
7758 liˇne two
7759 line three
7760 line four"#
7761 ));
7762
7763 cx.update_editor(|editor, window, cx| {
7764 editor.add_selection_below(&Default::default(), window, cx);
7765 });
7766
7767 // test multiple cursors expand in the same direction
7768 cx.assert_editor_state(indoc!(
7769 r#"line onˇe
7770 liˇne twˇo
7771 liˇne three
7772 line four"#
7773 ));
7774
7775 cx.update_editor(|editor, window, cx| {
7776 editor.add_selection_below(&Default::default(), window, cx);
7777 });
7778
7779 cx.update_editor(|editor, window, cx| {
7780 editor.add_selection_below(&Default::default(), window, cx);
7781 });
7782
7783 // test multiple cursors expand below overflow
7784 cx.assert_editor_state(indoc!(
7785 r#"line onˇe
7786 liˇne twˇo
7787 liˇne thˇree
7788 liˇne foˇur"#
7789 ));
7790
7791 cx.update_editor(|editor, window, cx| {
7792 editor.add_selection_above(&Default::default(), window, cx);
7793 });
7794
7795 // test multiple cursors retrieves back correctly
7796 cx.assert_editor_state(indoc!(
7797 r#"line onˇe
7798 liˇne twˇo
7799 liˇne thˇree
7800 line four"#
7801 ));
7802
7803 cx.update_editor(|editor, window, cx| {
7804 editor.add_selection_above(&Default::default(), window, cx);
7805 });
7806
7807 cx.update_editor(|editor, window, cx| {
7808 editor.add_selection_above(&Default::default(), window, cx);
7809 });
7810
7811 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
7812 cx.assert_editor_state(indoc!(
7813 r#"liˇne onˇe
7814 liˇne two
7815 line three
7816 line four"#
7817 ));
7818
7819 cx.update_editor(|editor, window, cx| {
7820 editor.undo_selection(&Default::default(), window, cx);
7821 });
7822
7823 // test undo
7824 cx.assert_editor_state(indoc!(
7825 r#"line onˇe
7826 liˇne twˇo
7827 line three
7828 line four"#
7829 ));
7830
7831 cx.update_editor(|editor, window, cx| {
7832 editor.redo_selection(&Default::default(), window, cx);
7833 });
7834
7835 // test redo
7836 cx.assert_editor_state(indoc!(
7837 r#"liˇne onˇe
7838 liˇne two
7839 line three
7840 line four"#
7841 ));
7842
7843 cx.set_state(indoc!(
7844 r#"abcd
7845 ef«ghˇ»
7846 ijkl
7847 «mˇ»nop"#
7848 ));
7849
7850 cx.update_editor(|editor, window, cx| {
7851 editor.add_selection_above(&Default::default(), window, cx);
7852 });
7853
7854 // test multiple selections expand in the same direction
7855 cx.assert_editor_state(indoc!(
7856 r#"ab«cdˇ»
7857 ef«ghˇ»
7858 «iˇ»jkl
7859 «mˇ»nop"#
7860 ));
7861
7862 cx.update_editor(|editor, window, cx| {
7863 editor.add_selection_above(&Default::default(), window, cx);
7864 });
7865
7866 // test multiple selection upward overflow
7867 cx.assert_editor_state(indoc!(
7868 r#"ab«cdˇ»
7869 «eˇ»f«ghˇ»
7870 «iˇ»jkl
7871 «mˇ»nop"#
7872 ));
7873
7874 cx.update_editor(|editor, window, cx| {
7875 editor.add_selection_below(&Default::default(), window, cx);
7876 });
7877
7878 // test multiple selection retrieves back correctly
7879 cx.assert_editor_state(indoc!(
7880 r#"abcd
7881 ef«ghˇ»
7882 «iˇ»jkl
7883 «mˇ»nop"#
7884 ));
7885
7886 cx.update_editor(|editor, window, cx| {
7887 editor.add_selection_below(&Default::default(), window, cx);
7888 });
7889
7890 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
7891 cx.assert_editor_state(indoc!(
7892 r#"abcd
7893 ef«ghˇ»
7894 ij«klˇ»
7895 «mˇ»nop"#
7896 ));
7897
7898 cx.update_editor(|editor, window, cx| {
7899 editor.undo_selection(&Default::default(), window, cx);
7900 });
7901
7902 // test undo
7903 cx.assert_editor_state(indoc!(
7904 r#"abcd
7905 ef«ghˇ»
7906 «iˇ»jkl
7907 «mˇ»nop"#
7908 ));
7909
7910 cx.update_editor(|editor, window, cx| {
7911 editor.redo_selection(&Default::default(), window, cx);
7912 });
7913
7914 // test redo
7915 cx.assert_editor_state(indoc!(
7916 r#"abcd
7917 ef«ghˇ»
7918 ij«klˇ»
7919 «mˇ»nop"#
7920 ));
7921}
7922
7923#[gpui::test]
7924async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
7925 init_test(cx, |_| {});
7926 let mut cx = EditorTestContext::new(cx).await;
7927
7928 cx.set_state(indoc!(
7929 r#"line onˇe
7930 liˇne two
7931 line three
7932 line four"#
7933 ));
7934
7935 cx.update_editor(|editor, window, cx| {
7936 editor.add_selection_below(&Default::default(), window, cx);
7937 editor.add_selection_below(&Default::default(), window, cx);
7938 editor.add_selection_below(&Default::default(), window, cx);
7939 });
7940
7941 // initial state with two multi cursor groups
7942 cx.assert_editor_state(indoc!(
7943 r#"line onˇe
7944 liˇne twˇo
7945 liˇne thˇree
7946 liˇne foˇur"#
7947 ));
7948
7949 // add single cursor in middle - simulate opt click
7950 cx.update_editor(|editor, window, cx| {
7951 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
7952 editor.begin_selection(new_cursor_point, true, 1, window, cx);
7953 editor.end_selection(window, cx);
7954 });
7955
7956 cx.assert_editor_state(indoc!(
7957 r#"line onˇe
7958 liˇne twˇo
7959 liˇneˇ thˇree
7960 liˇne foˇur"#
7961 ));
7962
7963 cx.update_editor(|editor, window, cx| {
7964 editor.add_selection_above(&Default::default(), window, cx);
7965 });
7966
7967 // test new added selection expands above and existing selection shrinks
7968 cx.assert_editor_state(indoc!(
7969 r#"line onˇe
7970 liˇneˇ twˇo
7971 liˇneˇ thˇree
7972 line four"#
7973 ));
7974
7975 cx.update_editor(|editor, window, cx| {
7976 editor.add_selection_above(&Default::default(), window, cx);
7977 });
7978
7979 // test new added selection expands above and existing selection shrinks
7980 cx.assert_editor_state(indoc!(
7981 r#"lineˇ onˇe
7982 liˇneˇ twˇo
7983 lineˇ three
7984 line four"#
7985 ));
7986
7987 // intial state with two selection groups
7988 cx.set_state(indoc!(
7989 r#"abcd
7990 ef«ghˇ»
7991 ijkl
7992 «mˇ»nop"#
7993 ));
7994
7995 cx.update_editor(|editor, window, cx| {
7996 editor.add_selection_above(&Default::default(), window, cx);
7997 editor.add_selection_above(&Default::default(), window, cx);
7998 });
7999
8000 cx.assert_editor_state(indoc!(
8001 r#"ab«cdˇ»
8002 «eˇ»f«ghˇ»
8003 «iˇ»jkl
8004 «mˇ»nop"#
8005 ));
8006
8007 // add single selection in middle - simulate opt drag
8008 cx.update_editor(|editor, window, cx| {
8009 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
8010 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8011 editor.update_selection(
8012 DisplayPoint::new(DisplayRow(2), 4),
8013 0,
8014 gpui::Point::<f32>::default(),
8015 window,
8016 cx,
8017 );
8018 editor.end_selection(window, cx);
8019 });
8020
8021 cx.assert_editor_state(indoc!(
8022 r#"ab«cdˇ»
8023 «eˇ»f«ghˇ»
8024 «iˇ»jk«lˇ»
8025 «mˇ»nop"#
8026 ));
8027
8028 cx.update_editor(|editor, window, cx| {
8029 editor.add_selection_below(&Default::default(), window, cx);
8030 });
8031
8032 // test new added selection expands below, others shrinks from above
8033 cx.assert_editor_state(indoc!(
8034 r#"abcd
8035 ef«ghˇ»
8036 «iˇ»jk«lˇ»
8037 «mˇ»no«pˇ»"#
8038 ));
8039}
8040
8041#[gpui::test]
8042async fn test_select_next(cx: &mut TestAppContext) {
8043 init_test(cx, |_| {});
8044
8045 let mut cx = EditorTestContext::new(cx).await;
8046 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8047
8048 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8049 .unwrap();
8050 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8051
8052 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8053 .unwrap();
8054 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8055
8056 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8057 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8058
8059 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8060 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8061
8062 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8063 .unwrap();
8064 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8065
8066 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8067 .unwrap();
8068 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8069
8070 // Test selection direction should be preserved
8071 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8072
8073 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8074 .unwrap();
8075 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
8076}
8077
8078#[gpui::test]
8079async fn test_select_all_matches(cx: &mut TestAppContext) {
8080 init_test(cx, |_| {});
8081
8082 let mut cx = EditorTestContext::new(cx).await;
8083
8084 // Test caret-only selections
8085 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8086 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8087 .unwrap();
8088 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8089
8090 // Test left-to-right selections
8091 cx.set_state("abc\n«abcˇ»\nabc");
8092 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8093 .unwrap();
8094 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
8095
8096 // Test right-to-left selections
8097 cx.set_state("abc\n«ˇabc»\nabc");
8098 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8099 .unwrap();
8100 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
8101
8102 // Test selecting whitespace with caret selection
8103 cx.set_state("abc\nˇ abc\nabc");
8104 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8105 .unwrap();
8106 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8107
8108 // Test selecting whitespace with left-to-right selection
8109 cx.set_state("abc\n«ˇ »abc\nabc");
8110 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8111 .unwrap();
8112 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
8113
8114 // Test no matches with right-to-left selection
8115 cx.set_state("abc\n« ˇ»abc\nabc");
8116 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8117 .unwrap();
8118 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8119
8120 // Test with a single word and clip_at_line_ends=true (#29823)
8121 cx.set_state("aˇbc");
8122 cx.update_editor(|e, window, cx| {
8123 e.set_clip_at_line_ends(true, cx);
8124 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8125 e.set_clip_at_line_ends(false, cx);
8126 });
8127 cx.assert_editor_state("«abcˇ»");
8128}
8129
8130#[gpui::test]
8131async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
8132 init_test(cx, |_| {});
8133
8134 let mut cx = EditorTestContext::new(cx).await;
8135
8136 let large_body_1 = "\nd".repeat(200);
8137 let large_body_2 = "\ne".repeat(200);
8138
8139 cx.set_state(&format!(
8140 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
8141 ));
8142 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
8143 let scroll_position = editor.scroll_position(cx);
8144 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
8145 scroll_position
8146 });
8147
8148 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8149 .unwrap();
8150 cx.assert_editor_state(&format!(
8151 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
8152 ));
8153 let scroll_position_after_selection =
8154 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
8155 assert_eq!(
8156 initial_scroll_position, scroll_position_after_selection,
8157 "Scroll position should not change after selecting all matches"
8158 );
8159}
8160
8161#[gpui::test]
8162async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
8163 init_test(cx, |_| {});
8164
8165 let mut cx = EditorLspTestContext::new_rust(
8166 lsp::ServerCapabilities {
8167 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8168 ..Default::default()
8169 },
8170 cx,
8171 )
8172 .await;
8173
8174 cx.set_state(indoc! {"
8175 line 1
8176 line 2
8177 linˇe 3
8178 line 4
8179 line 5
8180 "});
8181
8182 // Make an edit
8183 cx.update_editor(|editor, window, cx| {
8184 editor.handle_input("X", window, cx);
8185 });
8186
8187 // Move cursor to a different position
8188 cx.update_editor(|editor, window, cx| {
8189 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8190 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
8191 });
8192 });
8193
8194 cx.assert_editor_state(indoc! {"
8195 line 1
8196 line 2
8197 linXe 3
8198 line 4
8199 liˇne 5
8200 "});
8201
8202 cx.lsp
8203 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
8204 Ok(Some(vec![lsp::TextEdit::new(
8205 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8206 "PREFIX ".to_string(),
8207 )]))
8208 });
8209
8210 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
8211 .unwrap()
8212 .await
8213 .unwrap();
8214
8215 cx.assert_editor_state(indoc! {"
8216 PREFIX line 1
8217 line 2
8218 linXe 3
8219 line 4
8220 liˇne 5
8221 "});
8222
8223 // Undo formatting
8224 cx.update_editor(|editor, window, cx| {
8225 editor.undo(&Default::default(), window, cx);
8226 });
8227
8228 // Verify cursor moved back to position after edit
8229 cx.assert_editor_state(indoc! {"
8230 line 1
8231 line 2
8232 linXˇe 3
8233 line 4
8234 line 5
8235 "});
8236}
8237
8238#[gpui::test]
8239async fn test_undo_edit_prediction_scrolls_to_edit_pos(cx: &mut TestAppContext) {
8240 init_test(cx, |_| {});
8241
8242 let mut cx = EditorTestContext::new(cx).await;
8243
8244 let provider = cx.new(|_| FakeEditPredictionProvider::default());
8245 cx.update_editor(|editor, window, cx| {
8246 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
8247 });
8248
8249 cx.set_state(indoc! {"
8250 line 1
8251 line 2
8252 linˇe 3
8253 line 4
8254 line 5
8255 line 6
8256 line 7
8257 line 8
8258 line 9
8259 line 10
8260 "});
8261
8262 let snapshot = cx.buffer_snapshot();
8263 let edit_position = snapshot.anchor_after(Point::new(2, 4));
8264
8265 cx.update(|_, cx| {
8266 provider.update(cx, |provider, _| {
8267 provider.set_edit_prediction(Some(edit_prediction::EditPrediction {
8268 id: None,
8269 edits: vec![(edit_position..edit_position, "X".into())],
8270 edit_preview: None,
8271 }))
8272 })
8273 });
8274
8275 cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
8276 cx.update_editor(|editor, window, cx| {
8277 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
8278 });
8279
8280 cx.assert_editor_state(indoc! {"
8281 line 1
8282 line 2
8283 lineXˇ 3
8284 line 4
8285 line 5
8286 line 6
8287 line 7
8288 line 8
8289 line 9
8290 line 10
8291 "});
8292
8293 cx.update_editor(|editor, window, cx| {
8294 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8295 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
8296 });
8297 });
8298
8299 cx.assert_editor_state(indoc! {"
8300 line 1
8301 line 2
8302 lineX 3
8303 line 4
8304 line 5
8305 line 6
8306 line 7
8307 line 8
8308 line 9
8309 liˇne 10
8310 "});
8311
8312 cx.update_editor(|editor, window, cx| {
8313 editor.undo(&Default::default(), window, cx);
8314 });
8315
8316 cx.assert_editor_state(indoc! {"
8317 line 1
8318 line 2
8319 lineˇ 3
8320 line 4
8321 line 5
8322 line 6
8323 line 7
8324 line 8
8325 line 9
8326 line 10
8327 "});
8328}
8329
8330#[gpui::test]
8331async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
8332 init_test(cx, |_| {});
8333
8334 let mut cx = EditorTestContext::new(cx).await;
8335 cx.set_state(
8336 r#"let foo = 2;
8337lˇet foo = 2;
8338let fooˇ = 2;
8339let foo = 2;
8340let foo = ˇ2;"#,
8341 );
8342
8343 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8344 .unwrap();
8345 cx.assert_editor_state(
8346 r#"let foo = 2;
8347«letˇ» foo = 2;
8348let «fooˇ» = 2;
8349let foo = 2;
8350let foo = «2ˇ»;"#,
8351 );
8352
8353 // noop for multiple selections with different contents
8354 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8355 .unwrap();
8356 cx.assert_editor_state(
8357 r#"let foo = 2;
8358«letˇ» foo = 2;
8359let «fooˇ» = 2;
8360let foo = 2;
8361let foo = «2ˇ»;"#,
8362 );
8363
8364 // Test last selection direction should be preserved
8365 cx.set_state(
8366 r#"let foo = 2;
8367let foo = 2;
8368let «fooˇ» = 2;
8369let «ˇfoo» = 2;
8370let foo = 2;"#,
8371 );
8372
8373 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8374 .unwrap();
8375 cx.assert_editor_state(
8376 r#"let foo = 2;
8377let foo = 2;
8378let «fooˇ» = 2;
8379let «ˇfoo» = 2;
8380let «ˇfoo» = 2;"#,
8381 );
8382}
8383
8384#[gpui::test]
8385async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
8386 init_test(cx, |_| {});
8387
8388 let mut cx =
8389 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
8390
8391 cx.assert_editor_state(indoc! {"
8392 ˇbbb
8393 ccc
8394
8395 bbb
8396 ccc
8397 "});
8398 cx.dispatch_action(SelectPrevious::default());
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}
8415
8416#[gpui::test]
8417async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
8418 init_test(cx, |_| {});
8419
8420 let mut cx = EditorTestContext::new(cx).await;
8421 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8422
8423 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8424 .unwrap();
8425 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8426
8427 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8428 .unwrap();
8429 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8430
8431 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8432 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8433
8434 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8435 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8436
8437 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8438 .unwrap();
8439 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
8440
8441 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8442 .unwrap();
8443 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8444}
8445
8446#[gpui::test]
8447async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
8448 init_test(cx, |_| {});
8449
8450 let mut cx = EditorTestContext::new(cx).await;
8451 cx.set_state("aˇ");
8452
8453 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8454 .unwrap();
8455 cx.assert_editor_state("«aˇ»");
8456 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8457 .unwrap();
8458 cx.assert_editor_state("«aˇ»");
8459}
8460
8461#[gpui::test]
8462async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
8463 init_test(cx, |_| {});
8464
8465 let mut cx = EditorTestContext::new(cx).await;
8466 cx.set_state(
8467 r#"let foo = 2;
8468lˇet foo = 2;
8469let fooˇ = 2;
8470let foo = 2;
8471let foo = ˇ2;"#,
8472 );
8473
8474 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8475 .unwrap();
8476 cx.assert_editor_state(
8477 r#"let foo = 2;
8478«letˇ» foo = 2;
8479let «fooˇ» = 2;
8480let foo = 2;
8481let foo = «2ˇ»;"#,
8482 );
8483
8484 // noop for multiple selections with different contents
8485 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8486 .unwrap();
8487 cx.assert_editor_state(
8488 r#"let foo = 2;
8489«letˇ» foo = 2;
8490let «fooˇ» = 2;
8491let foo = 2;
8492let foo = «2ˇ»;"#,
8493 );
8494}
8495
8496#[gpui::test]
8497async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
8498 init_test(cx, |_| {});
8499
8500 let mut cx = EditorTestContext::new(cx).await;
8501 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8502
8503 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8504 .unwrap();
8505 // selection direction is preserved
8506 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8507
8508 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8509 .unwrap();
8510 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8511
8512 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8513 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8514
8515 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8516 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8517
8518 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8519 .unwrap();
8520 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
8521
8522 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8523 .unwrap();
8524 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
8525}
8526
8527#[gpui::test]
8528async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
8529 init_test(cx, |_| {});
8530
8531 let language = Arc::new(Language::new(
8532 LanguageConfig::default(),
8533 Some(tree_sitter_rust::LANGUAGE.into()),
8534 ));
8535
8536 let text = r#"
8537 use mod1::mod2::{mod3, mod4};
8538
8539 fn fn_1(param1: bool, param2: &str) {
8540 let var1 = "text";
8541 }
8542 "#
8543 .unindent();
8544
8545 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8546 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8547 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8548
8549 editor
8550 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8551 .await;
8552
8553 editor.update_in(cx, |editor, window, cx| {
8554 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8555 s.select_display_ranges([
8556 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
8557 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
8558 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
8559 ]);
8560 });
8561 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8562 });
8563 editor.update(cx, |editor, cx| {
8564 assert_text_with_selections(
8565 editor,
8566 indoc! {r#"
8567 use mod1::mod2::{mod3, «mod4ˇ»};
8568
8569 fn fn_1«ˇ(param1: bool, param2: &str)» {
8570 let var1 = "«ˇtext»";
8571 }
8572 "#},
8573 cx,
8574 );
8575 });
8576
8577 editor.update_in(cx, |editor, window, cx| {
8578 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8579 });
8580 editor.update(cx, |editor, cx| {
8581 assert_text_with_selections(
8582 editor,
8583 indoc! {r#"
8584 use mod1::mod2::«{mod3, mod4}ˇ»;
8585
8586 «ˇfn fn_1(param1: bool, param2: &str) {
8587 let var1 = "text";
8588 }»
8589 "#},
8590 cx,
8591 );
8592 });
8593
8594 editor.update_in(cx, |editor, window, cx| {
8595 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8596 });
8597 assert_eq!(
8598 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
8599 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
8600 );
8601
8602 // Trying to expand the selected syntax node one more time has no effect.
8603 editor.update_in(cx, |editor, window, cx| {
8604 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8605 });
8606 assert_eq!(
8607 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
8608 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
8609 );
8610
8611 editor.update_in(cx, |editor, window, cx| {
8612 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8613 });
8614 editor.update(cx, |editor, cx| {
8615 assert_text_with_selections(
8616 editor,
8617 indoc! {r#"
8618 use mod1::mod2::«{mod3, mod4}ˇ»;
8619
8620 «ˇfn fn_1(param1: bool, param2: &str) {
8621 let var1 = "text";
8622 }»
8623 "#},
8624 cx,
8625 );
8626 });
8627
8628 editor.update_in(cx, |editor, window, cx| {
8629 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8630 });
8631 editor.update(cx, |editor, cx| {
8632 assert_text_with_selections(
8633 editor,
8634 indoc! {r#"
8635 use mod1::mod2::{mod3, «mod4ˇ»};
8636
8637 fn fn_1«ˇ(param1: bool, param2: &str)» {
8638 let var1 = "«ˇtext»";
8639 }
8640 "#},
8641 cx,
8642 );
8643 });
8644
8645 editor.update_in(cx, |editor, window, cx| {
8646 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8647 });
8648 editor.update(cx, |editor, cx| {
8649 assert_text_with_selections(
8650 editor,
8651 indoc! {r#"
8652 use mod1::mod2::{mod3, moˇd4};
8653
8654 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
8655 let var1 = "teˇxt";
8656 }
8657 "#},
8658 cx,
8659 );
8660 });
8661
8662 // Trying to shrink the selected syntax node one more time has no effect.
8663 editor.update_in(cx, |editor, window, cx| {
8664 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8665 });
8666 editor.update_in(cx, |editor, _, cx| {
8667 assert_text_with_selections(
8668 editor,
8669 indoc! {r#"
8670 use mod1::mod2::{mod3, moˇd4};
8671
8672 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
8673 let var1 = "teˇxt";
8674 }
8675 "#},
8676 cx,
8677 );
8678 });
8679
8680 // Ensure that we keep expanding the selection if the larger selection starts or ends within
8681 // a fold.
8682 editor.update_in(cx, |editor, window, cx| {
8683 editor.fold_creases(
8684 vec![
8685 Crease::simple(
8686 Point::new(0, 21)..Point::new(0, 24),
8687 FoldPlaceholder::test(),
8688 ),
8689 Crease::simple(
8690 Point::new(3, 20)..Point::new(3, 22),
8691 FoldPlaceholder::test(),
8692 ),
8693 ],
8694 true,
8695 window,
8696 cx,
8697 );
8698 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8699 });
8700 editor.update(cx, |editor, cx| {
8701 assert_text_with_selections(
8702 editor,
8703 indoc! {r#"
8704 use mod1::mod2::«{mod3, mod4}ˇ»;
8705
8706 fn fn_1«ˇ(param1: bool, param2: &str)» {
8707 let var1 = "«ˇtext»";
8708 }
8709 "#},
8710 cx,
8711 );
8712 });
8713}
8714
8715#[gpui::test]
8716async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
8717 init_test(cx, |_| {});
8718
8719 let language = Arc::new(Language::new(
8720 LanguageConfig::default(),
8721 Some(tree_sitter_rust::LANGUAGE.into()),
8722 ));
8723
8724 let text = "let a = 2;";
8725
8726 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8727 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8728 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8729
8730 editor
8731 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8732 .await;
8733
8734 // Test case 1: Cursor at end of word
8735 editor.update_in(cx, |editor, window, cx| {
8736 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8737 s.select_display_ranges([
8738 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
8739 ]);
8740 });
8741 });
8742 editor.update(cx, |editor, cx| {
8743 assert_text_with_selections(editor, "let aˇ = 2;", cx);
8744 });
8745 editor.update_in(cx, |editor, window, cx| {
8746 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8747 });
8748 editor.update(cx, |editor, cx| {
8749 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
8750 });
8751 editor.update_in(cx, |editor, window, cx| {
8752 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8753 });
8754 editor.update(cx, |editor, cx| {
8755 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
8756 });
8757
8758 // Test case 2: Cursor at end of statement
8759 editor.update_in(cx, |editor, window, cx| {
8760 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8761 s.select_display_ranges([
8762 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
8763 ]);
8764 });
8765 });
8766 editor.update(cx, |editor, cx| {
8767 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
8768 });
8769 editor.update_in(cx, |editor, window, cx| {
8770 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8771 });
8772 editor.update(cx, |editor, cx| {
8773 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
8774 });
8775}
8776
8777#[gpui::test]
8778async fn test_select_larger_syntax_node_for_cursor_at_symbol(cx: &mut TestAppContext) {
8779 init_test(cx, |_| {});
8780
8781 let language = Arc::new(Language::new(
8782 LanguageConfig {
8783 name: "JavaScript".into(),
8784 ..Default::default()
8785 },
8786 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
8787 ));
8788
8789 let text = r#"
8790 let a = {
8791 key: "value",
8792 };
8793 "#
8794 .unindent();
8795
8796 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8797 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8798 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8799
8800 editor
8801 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8802 .await;
8803
8804 // Test case 1: Cursor after '{'
8805 editor.update_in(cx, |editor, window, cx| {
8806 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8807 s.select_display_ranges([
8808 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 9)
8809 ]);
8810 });
8811 });
8812 editor.update(cx, |editor, cx| {
8813 assert_text_with_selections(
8814 editor,
8815 indoc! {r#"
8816 let a = {ˇ
8817 key: "value",
8818 };
8819 "#},
8820 cx,
8821 );
8822 });
8823 editor.update_in(cx, |editor, window, cx| {
8824 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8825 });
8826 editor.update(cx, |editor, cx| {
8827 assert_text_with_selections(
8828 editor,
8829 indoc! {r#"
8830 let a = «ˇ{
8831 key: "value",
8832 }»;
8833 "#},
8834 cx,
8835 );
8836 });
8837
8838 // Test case 2: Cursor after ':'
8839 editor.update_in(cx, |editor, window, cx| {
8840 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8841 s.select_display_ranges([
8842 DisplayPoint::new(DisplayRow(1), 8)..DisplayPoint::new(DisplayRow(1), 8)
8843 ]);
8844 });
8845 });
8846 editor.update(cx, |editor, cx| {
8847 assert_text_with_selections(
8848 editor,
8849 indoc! {r#"
8850 let a = {
8851 key:ˇ "value",
8852 };
8853 "#},
8854 cx,
8855 );
8856 });
8857 editor.update_in(cx, |editor, window, cx| {
8858 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8859 });
8860 editor.update(cx, |editor, cx| {
8861 assert_text_with_selections(
8862 editor,
8863 indoc! {r#"
8864 let a = {
8865 «ˇkey: "value"»,
8866 };
8867 "#},
8868 cx,
8869 );
8870 });
8871 editor.update_in(cx, |editor, window, cx| {
8872 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8873 });
8874 editor.update(cx, |editor, cx| {
8875 assert_text_with_selections(
8876 editor,
8877 indoc! {r#"
8878 let a = «ˇ{
8879 key: "value",
8880 }»;
8881 "#},
8882 cx,
8883 );
8884 });
8885
8886 // Test case 3: Cursor after ','
8887 editor.update_in(cx, |editor, window, cx| {
8888 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8889 s.select_display_ranges([
8890 DisplayPoint::new(DisplayRow(1), 17)..DisplayPoint::new(DisplayRow(1), 17)
8891 ]);
8892 });
8893 });
8894 editor.update(cx, |editor, cx| {
8895 assert_text_with_selections(
8896 editor,
8897 indoc! {r#"
8898 let a = {
8899 key: "value",ˇ
8900 };
8901 "#},
8902 cx,
8903 );
8904 });
8905 editor.update_in(cx, |editor, window, cx| {
8906 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8907 });
8908 editor.update(cx, |editor, cx| {
8909 assert_text_with_selections(
8910 editor,
8911 indoc! {r#"
8912 let a = «ˇ{
8913 key: "value",
8914 }»;
8915 "#},
8916 cx,
8917 );
8918 });
8919
8920 // Test case 4: Cursor after ';'
8921 editor.update_in(cx, |editor, window, cx| {
8922 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8923 s.select_display_ranges([
8924 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)
8925 ]);
8926 });
8927 });
8928 editor.update(cx, |editor, cx| {
8929 assert_text_with_selections(
8930 editor,
8931 indoc! {r#"
8932 let a = {
8933 key: "value",
8934 };ˇ
8935 "#},
8936 cx,
8937 );
8938 });
8939 editor.update_in(cx, |editor, window, cx| {
8940 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8941 });
8942 editor.update(cx, |editor, cx| {
8943 assert_text_with_selections(
8944 editor,
8945 indoc! {r#"
8946 «ˇlet a = {
8947 key: "value",
8948 };
8949 »"#},
8950 cx,
8951 );
8952 });
8953}
8954
8955#[gpui::test]
8956async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
8957 init_test(cx, |_| {});
8958
8959 let language = Arc::new(Language::new(
8960 LanguageConfig::default(),
8961 Some(tree_sitter_rust::LANGUAGE.into()),
8962 ));
8963
8964 let text = r#"
8965 use mod1::mod2::{mod3, mod4};
8966
8967 fn fn_1(param1: bool, param2: &str) {
8968 let var1 = "hello world";
8969 }
8970 "#
8971 .unindent();
8972
8973 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8974 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8975 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8976
8977 editor
8978 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8979 .await;
8980
8981 // Test 1: Cursor on a letter of a string word
8982 editor.update_in(cx, |editor, window, cx| {
8983 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8984 s.select_display_ranges([
8985 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
8986 ]);
8987 });
8988 });
8989 editor.update_in(cx, |editor, window, cx| {
8990 assert_text_with_selections(
8991 editor,
8992 indoc! {r#"
8993 use mod1::mod2::{mod3, mod4};
8994
8995 fn fn_1(param1: bool, param2: &str) {
8996 let var1 = "hˇello world";
8997 }
8998 "#},
8999 cx,
9000 );
9001 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9002 assert_text_with_selections(
9003 editor,
9004 indoc! {r#"
9005 use mod1::mod2::{mod3, mod4};
9006
9007 fn fn_1(param1: bool, param2: &str) {
9008 let var1 = "«ˇhello» world";
9009 }
9010 "#},
9011 cx,
9012 );
9013 });
9014
9015 // Test 2: Partial selection within a word
9016 editor.update_in(cx, |editor, window, cx| {
9017 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9018 s.select_display_ranges([
9019 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
9020 ]);
9021 });
9022 });
9023 editor.update_in(cx, |editor, window, cx| {
9024 assert_text_with_selections(
9025 editor,
9026 indoc! {r#"
9027 use mod1::mod2::{mod3, mod4};
9028
9029 fn fn_1(param1: bool, param2: &str) {
9030 let var1 = "h«elˇ»lo world";
9031 }
9032 "#},
9033 cx,
9034 );
9035 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9036 assert_text_with_selections(
9037 editor,
9038 indoc! {r#"
9039 use mod1::mod2::{mod3, mod4};
9040
9041 fn fn_1(param1: bool, param2: &str) {
9042 let var1 = "«ˇhello» world";
9043 }
9044 "#},
9045 cx,
9046 );
9047 });
9048
9049 // Test 3: Complete word already selected
9050 editor.update_in(cx, |editor, window, cx| {
9051 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9052 s.select_display_ranges([
9053 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
9054 ]);
9055 });
9056 });
9057 editor.update_in(cx, |editor, window, cx| {
9058 assert_text_with_selections(
9059 editor,
9060 indoc! {r#"
9061 use mod1::mod2::{mod3, mod4};
9062
9063 fn fn_1(param1: bool, param2: &str) {
9064 let var1 = "«helloˇ» world";
9065 }
9066 "#},
9067 cx,
9068 );
9069 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9070 assert_text_with_selections(
9071 editor,
9072 indoc! {r#"
9073 use mod1::mod2::{mod3, mod4};
9074
9075 fn fn_1(param1: bool, param2: &str) {
9076 let var1 = "«hello worldˇ»";
9077 }
9078 "#},
9079 cx,
9080 );
9081 });
9082
9083 // Test 4: Selection spanning across words
9084 editor.update_in(cx, |editor, window, cx| {
9085 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9086 s.select_display_ranges([
9087 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
9088 ]);
9089 });
9090 });
9091 editor.update_in(cx, |editor, window, cx| {
9092 assert_text_with_selections(
9093 editor,
9094 indoc! {r#"
9095 use mod1::mod2::{mod3, mod4};
9096
9097 fn fn_1(param1: bool, param2: &str) {
9098 let var1 = "hel«lo woˇ»rld";
9099 }
9100 "#},
9101 cx,
9102 );
9103 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9104 assert_text_with_selections(
9105 editor,
9106 indoc! {r#"
9107 use mod1::mod2::{mod3, mod4};
9108
9109 fn fn_1(param1: bool, param2: &str) {
9110 let var1 = "«ˇhello world»";
9111 }
9112 "#},
9113 cx,
9114 );
9115 });
9116
9117 // Test 5: Expansion beyond string
9118 editor.update_in(cx, |editor, window, cx| {
9119 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9120 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9121 assert_text_with_selections(
9122 editor,
9123 indoc! {r#"
9124 use mod1::mod2::{mod3, mod4};
9125
9126 fn fn_1(param1: bool, param2: &str) {
9127 «ˇlet var1 = "hello world";»
9128 }
9129 "#},
9130 cx,
9131 );
9132 });
9133}
9134
9135#[gpui::test]
9136async fn test_unwrap_syntax_nodes(cx: &mut gpui::TestAppContext) {
9137 init_test(cx, |_| {});
9138
9139 let mut cx = EditorTestContext::new(cx).await;
9140
9141 let language = Arc::new(Language::new(
9142 LanguageConfig::default(),
9143 Some(tree_sitter_rust::LANGUAGE.into()),
9144 ));
9145
9146 cx.update_buffer(|buffer, cx| {
9147 buffer.set_language(Some(language), cx);
9148 });
9149
9150 cx.set_state(indoc! { r#"use mod1::{mod2::{«mod3ˇ», mod4}, mod5::{mod6, «mod7ˇ»}};"# });
9151 cx.update_editor(|editor, window, cx| {
9152 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9153 });
9154
9155 cx.assert_editor_state(indoc! { r#"use mod1::{mod2::«mod3ˇ», mod5::«mod7ˇ»};"# });
9156}
9157
9158#[gpui::test]
9159async fn test_fold_function_bodies(cx: &mut TestAppContext) {
9160 init_test(cx, |_| {});
9161
9162 let base_text = r#"
9163 impl A {
9164 // this is an uncommitted comment
9165
9166 fn b() {
9167 c();
9168 }
9169
9170 // this is another uncommitted comment
9171
9172 fn d() {
9173 // e
9174 // f
9175 }
9176 }
9177
9178 fn g() {
9179 // h
9180 }
9181 "#
9182 .unindent();
9183
9184 let text = r#"
9185 ˇimpl A {
9186
9187 fn b() {
9188 c();
9189 }
9190
9191 fn d() {
9192 // e
9193 // f
9194 }
9195 }
9196
9197 fn g() {
9198 // h
9199 }
9200 "#
9201 .unindent();
9202
9203 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9204 cx.set_state(&text);
9205 cx.set_head_text(&base_text);
9206 cx.update_editor(|editor, window, cx| {
9207 editor.expand_all_diff_hunks(&Default::default(), window, cx);
9208 });
9209
9210 cx.assert_state_with_diff(
9211 "
9212 ˇimpl A {
9213 - // this is an uncommitted comment
9214
9215 fn b() {
9216 c();
9217 }
9218
9219 - // this is another uncommitted comment
9220 -
9221 fn d() {
9222 // e
9223 // f
9224 }
9225 }
9226
9227 fn g() {
9228 // h
9229 }
9230 "
9231 .unindent(),
9232 );
9233
9234 let expected_display_text = "
9235 impl A {
9236 // this is an uncommitted comment
9237
9238 fn b() {
9239 ⋯
9240 }
9241
9242 // this is another uncommitted comment
9243
9244 fn d() {
9245 ⋯
9246 }
9247 }
9248
9249 fn g() {
9250 ⋯
9251 }
9252 "
9253 .unindent();
9254
9255 cx.update_editor(|editor, window, cx| {
9256 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
9257 assert_eq!(editor.display_text(cx), expected_display_text);
9258 });
9259}
9260
9261#[gpui::test]
9262async fn test_autoindent(cx: &mut TestAppContext) {
9263 init_test(cx, |_| {});
9264
9265 let language = Arc::new(
9266 Language::new(
9267 LanguageConfig {
9268 brackets: BracketPairConfig {
9269 pairs: vec![
9270 BracketPair {
9271 start: "{".to_string(),
9272 end: "}".to_string(),
9273 close: false,
9274 surround: false,
9275 newline: true,
9276 },
9277 BracketPair {
9278 start: "(".to_string(),
9279 end: ")".to_string(),
9280 close: false,
9281 surround: false,
9282 newline: true,
9283 },
9284 ],
9285 ..Default::default()
9286 },
9287 ..Default::default()
9288 },
9289 Some(tree_sitter_rust::LANGUAGE.into()),
9290 )
9291 .with_indents_query(
9292 r#"
9293 (_ "(" ")" @end) @indent
9294 (_ "{" "}" @end) @indent
9295 "#,
9296 )
9297 .unwrap(),
9298 );
9299
9300 let text = "fn a() {}";
9301
9302 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9303 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9304 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9305 editor
9306 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9307 .await;
9308
9309 editor.update_in(cx, |editor, window, cx| {
9310 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9311 s.select_ranges([5..5, 8..8, 9..9])
9312 });
9313 editor.newline(&Newline, window, cx);
9314 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
9315 assert_eq!(
9316 editor.selections.ranges(cx),
9317 &[
9318 Point::new(1, 4)..Point::new(1, 4),
9319 Point::new(3, 4)..Point::new(3, 4),
9320 Point::new(5, 0)..Point::new(5, 0)
9321 ]
9322 );
9323 });
9324}
9325
9326#[gpui::test]
9327async fn test_autoindent_disabled(cx: &mut TestAppContext) {
9328 init_test(cx, |settings| settings.defaults.auto_indent = Some(false));
9329
9330 let language = Arc::new(
9331 Language::new(
9332 LanguageConfig {
9333 brackets: BracketPairConfig {
9334 pairs: vec![
9335 BracketPair {
9336 start: "{".to_string(),
9337 end: "}".to_string(),
9338 close: false,
9339 surround: false,
9340 newline: true,
9341 },
9342 BracketPair {
9343 start: "(".to_string(),
9344 end: ")".to_string(),
9345 close: false,
9346 surround: false,
9347 newline: true,
9348 },
9349 ],
9350 ..Default::default()
9351 },
9352 ..Default::default()
9353 },
9354 Some(tree_sitter_rust::LANGUAGE.into()),
9355 )
9356 .with_indents_query(
9357 r#"
9358 (_ "(" ")" @end) @indent
9359 (_ "{" "}" @end) @indent
9360 "#,
9361 )
9362 .unwrap(),
9363 );
9364
9365 let text = "fn a() {}";
9366
9367 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9368 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9369 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9370 editor
9371 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9372 .await;
9373
9374 editor.update_in(cx, |editor, window, cx| {
9375 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9376 s.select_ranges([5..5, 8..8, 9..9])
9377 });
9378 editor.newline(&Newline, window, cx);
9379 assert_eq!(
9380 editor.text(cx),
9381 indoc!(
9382 "
9383 fn a(
9384
9385 ) {
9386
9387 }
9388 "
9389 )
9390 );
9391 assert_eq!(
9392 editor.selections.ranges(cx),
9393 &[
9394 Point::new(1, 0)..Point::new(1, 0),
9395 Point::new(3, 0)..Point::new(3, 0),
9396 Point::new(5, 0)..Point::new(5, 0)
9397 ]
9398 );
9399 });
9400}
9401
9402#[gpui::test]
9403async fn test_autoindent_disabled_with_nested_language(cx: &mut TestAppContext) {
9404 init_test(cx, |settings| {
9405 settings.defaults.auto_indent = Some(true);
9406 settings.languages.0.insert(
9407 "python".into(),
9408 LanguageSettingsContent {
9409 auto_indent: Some(false),
9410 ..Default::default()
9411 },
9412 );
9413 });
9414
9415 let mut cx = EditorTestContext::new(cx).await;
9416
9417 let injected_language = Arc::new(
9418 Language::new(
9419 LanguageConfig {
9420 brackets: BracketPairConfig {
9421 pairs: vec![
9422 BracketPair {
9423 start: "{".to_string(),
9424 end: "}".to_string(),
9425 close: false,
9426 surround: false,
9427 newline: true,
9428 },
9429 BracketPair {
9430 start: "(".to_string(),
9431 end: ")".to_string(),
9432 close: true,
9433 surround: false,
9434 newline: true,
9435 },
9436 ],
9437 ..Default::default()
9438 },
9439 name: "python".into(),
9440 ..Default::default()
9441 },
9442 Some(tree_sitter_python::LANGUAGE.into()),
9443 )
9444 .with_indents_query(
9445 r#"
9446 (_ "(" ")" @end) @indent
9447 (_ "{" "}" @end) @indent
9448 "#,
9449 )
9450 .unwrap(),
9451 );
9452
9453 let language = Arc::new(
9454 Language::new(
9455 LanguageConfig {
9456 brackets: BracketPairConfig {
9457 pairs: vec![
9458 BracketPair {
9459 start: "{".to_string(),
9460 end: "}".to_string(),
9461 close: false,
9462 surround: false,
9463 newline: true,
9464 },
9465 BracketPair {
9466 start: "(".to_string(),
9467 end: ")".to_string(),
9468 close: true,
9469 surround: false,
9470 newline: true,
9471 },
9472 ],
9473 ..Default::default()
9474 },
9475 name: LanguageName::new("rust"),
9476 ..Default::default()
9477 },
9478 Some(tree_sitter_rust::LANGUAGE.into()),
9479 )
9480 .with_indents_query(
9481 r#"
9482 (_ "(" ")" @end) @indent
9483 (_ "{" "}" @end) @indent
9484 "#,
9485 )
9486 .unwrap()
9487 .with_injection_query(
9488 r#"
9489 (macro_invocation
9490 macro: (identifier) @_macro_name
9491 (token_tree) @injection.content
9492 (#set! injection.language "python"))
9493 "#,
9494 )
9495 .unwrap(),
9496 );
9497
9498 cx.language_registry().add(injected_language);
9499 cx.language_registry().add(language.clone());
9500
9501 cx.update_buffer(|buffer, cx| {
9502 buffer.set_language(Some(language), cx);
9503 });
9504
9505 cx.set_state(r#"struct A {ˇ}"#);
9506
9507 cx.update_editor(|editor, window, cx| {
9508 editor.newline(&Default::default(), window, cx);
9509 });
9510
9511 cx.assert_editor_state(indoc!(
9512 "struct A {
9513 ˇ
9514 }"
9515 ));
9516
9517 cx.set_state(r#"select_biased!(ˇ)"#);
9518
9519 cx.update_editor(|editor, window, cx| {
9520 editor.newline(&Default::default(), window, cx);
9521 editor.handle_input("def ", window, cx);
9522 editor.handle_input("(", window, cx);
9523 editor.newline(&Default::default(), window, cx);
9524 editor.handle_input("a", window, cx);
9525 });
9526
9527 cx.assert_editor_state(indoc!(
9528 "select_biased!(
9529 def (
9530 aˇ
9531 )
9532 )"
9533 ));
9534}
9535
9536#[gpui::test]
9537async fn test_autoindent_selections(cx: &mut TestAppContext) {
9538 init_test(cx, |_| {});
9539
9540 {
9541 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9542 cx.set_state(indoc! {"
9543 impl A {
9544
9545 fn b() {}
9546
9547 «fn c() {
9548
9549 }ˇ»
9550 }
9551 "});
9552
9553 cx.update_editor(|editor, window, cx| {
9554 editor.autoindent(&Default::default(), window, cx);
9555 });
9556
9557 cx.assert_editor_state(indoc! {"
9558 impl A {
9559
9560 fn b() {}
9561
9562 «fn c() {
9563
9564 }ˇ»
9565 }
9566 "});
9567 }
9568
9569 {
9570 let mut cx = EditorTestContext::new_multibuffer(
9571 cx,
9572 [indoc! { "
9573 impl A {
9574 «
9575 // a
9576 fn b(){}
9577 »
9578 «
9579 }
9580 fn c(){}
9581 »
9582 "}],
9583 );
9584
9585 let buffer = cx.update_editor(|editor, _, cx| {
9586 let buffer = editor.buffer().update(cx, |buffer, _| {
9587 buffer.all_buffers().iter().next().unwrap().clone()
9588 });
9589 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
9590 buffer
9591 });
9592
9593 cx.run_until_parked();
9594 cx.update_editor(|editor, window, cx| {
9595 editor.select_all(&Default::default(), window, cx);
9596 editor.autoindent(&Default::default(), window, cx)
9597 });
9598 cx.run_until_parked();
9599
9600 cx.update(|_, cx| {
9601 assert_eq!(
9602 buffer.read(cx).text(),
9603 indoc! { "
9604 impl A {
9605
9606 // a
9607 fn b(){}
9608
9609
9610 }
9611 fn c(){}
9612
9613 " }
9614 )
9615 });
9616 }
9617}
9618
9619#[gpui::test]
9620async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
9621 init_test(cx, |_| {});
9622
9623 let mut cx = EditorTestContext::new(cx).await;
9624
9625 let language = Arc::new(Language::new(
9626 LanguageConfig {
9627 brackets: BracketPairConfig {
9628 pairs: vec![
9629 BracketPair {
9630 start: "{".to_string(),
9631 end: "}".to_string(),
9632 close: true,
9633 surround: true,
9634 newline: true,
9635 },
9636 BracketPair {
9637 start: "(".to_string(),
9638 end: ")".to_string(),
9639 close: true,
9640 surround: true,
9641 newline: true,
9642 },
9643 BracketPair {
9644 start: "/*".to_string(),
9645 end: " */".to_string(),
9646 close: true,
9647 surround: true,
9648 newline: true,
9649 },
9650 BracketPair {
9651 start: "[".to_string(),
9652 end: "]".to_string(),
9653 close: false,
9654 surround: false,
9655 newline: true,
9656 },
9657 BracketPair {
9658 start: "\"".to_string(),
9659 end: "\"".to_string(),
9660 close: true,
9661 surround: true,
9662 newline: false,
9663 },
9664 BracketPair {
9665 start: "<".to_string(),
9666 end: ">".to_string(),
9667 close: false,
9668 surround: true,
9669 newline: true,
9670 },
9671 ],
9672 ..Default::default()
9673 },
9674 autoclose_before: "})]".to_string(),
9675 ..Default::default()
9676 },
9677 Some(tree_sitter_rust::LANGUAGE.into()),
9678 ));
9679
9680 cx.language_registry().add(language.clone());
9681 cx.update_buffer(|buffer, cx| {
9682 buffer.set_language(Some(language), cx);
9683 });
9684
9685 cx.set_state(
9686 &r#"
9687 🏀ˇ
9688 εˇ
9689 ❤️ˇ
9690 "#
9691 .unindent(),
9692 );
9693
9694 // autoclose multiple nested brackets at multiple cursors
9695 cx.update_editor(|editor, window, cx| {
9696 editor.handle_input("{", window, cx);
9697 editor.handle_input("{", window, cx);
9698 editor.handle_input("{", window, cx);
9699 });
9700 cx.assert_editor_state(
9701 &"
9702 🏀{{{ˇ}}}
9703 ε{{{ˇ}}}
9704 ❤️{{{ˇ}}}
9705 "
9706 .unindent(),
9707 );
9708
9709 // insert a different closing bracket
9710 cx.update_editor(|editor, window, cx| {
9711 editor.handle_input(")", window, cx);
9712 });
9713 cx.assert_editor_state(
9714 &"
9715 🏀{{{)ˇ}}}
9716 ε{{{)ˇ}}}
9717 ❤️{{{)ˇ}}}
9718 "
9719 .unindent(),
9720 );
9721
9722 // skip over the auto-closed brackets when typing a closing bracket
9723 cx.update_editor(|editor, window, cx| {
9724 editor.move_right(&MoveRight, window, cx);
9725 editor.handle_input("}", window, cx);
9726 editor.handle_input("}", window, cx);
9727 editor.handle_input("}", window, cx);
9728 });
9729 cx.assert_editor_state(
9730 &"
9731 🏀{{{)}}}}ˇ
9732 ε{{{)}}}}ˇ
9733 ❤️{{{)}}}}ˇ
9734 "
9735 .unindent(),
9736 );
9737
9738 // autoclose multi-character pairs
9739 cx.set_state(
9740 &"
9741 ˇ
9742 ˇ
9743 "
9744 .unindent(),
9745 );
9746 cx.update_editor(|editor, window, cx| {
9747 editor.handle_input("/", window, cx);
9748 editor.handle_input("*", window, cx);
9749 });
9750 cx.assert_editor_state(
9751 &"
9752 /*ˇ */
9753 /*ˇ */
9754 "
9755 .unindent(),
9756 );
9757
9758 // one cursor autocloses a multi-character pair, one cursor
9759 // does not autoclose.
9760 cx.set_state(
9761 &"
9762 /ˇ
9763 ˇ
9764 "
9765 .unindent(),
9766 );
9767 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
9768 cx.assert_editor_state(
9769 &"
9770 /*ˇ */
9771 *ˇ
9772 "
9773 .unindent(),
9774 );
9775
9776 // Don't autoclose if the next character isn't whitespace and isn't
9777 // listed in the language's "autoclose_before" section.
9778 cx.set_state("ˇa b");
9779 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
9780 cx.assert_editor_state("{ˇa b");
9781
9782 // Don't autoclose if `close` is false for the bracket pair
9783 cx.set_state("ˇ");
9784 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
9785 cx.assert_editor_state("[ˇ");
9786
9787 // Surround with brackets if text is selected
9788 cx.set_state("«aˇ» b");
9789 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
9790 cx.assert_editor_state("{«aˇ»} b");
9791
9792 // Autoclose when not immediately after a word character
9793 cx.set_state("a ˇ");
9794 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
9795 cx.assert_editor_state("a \"ˇ\"");
9796
9797 // Autoclose pair where the start and end characters are the same
9798 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
9799 cx.assert_editor_state("a \"\"ˇ");
9800
9801 // Don't autoclose when immediately after a word character
9802 cx.set_state("aˇ");
9803 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
9804 cx.assert_editor_state("a\"ˇ");
9805
9806 // Do autoclose when after a non-word character
9807 cx.set_state("{ˇ");
9808 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
9809 cx.assert_editor_state("{\"ˇ\"");
9810
9811 // Non identical pairs autoclose regardless of preceding character
9812 cx.set_state("aˇ");
9813 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
9814 cx.assert_editor_state("a{ˇ}");
9815
9816 // Don't autoclose pair if autoclose is disabled
9817 cx.set_state("ˇ");
9818 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
9819 cx.assert_editor_state("<ˇ");
9820
9821 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
9822 cx.set_state("«aˇ» b");
9823 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
9824 cx.assert_editor_state("<«aˇ»> b");
9825}
9826
9827#[gpui::test]
9828async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
9829 init_test(cx, |settings| {
9830 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
9831 });
9832
9833 let mut cx = EditorTestContext::new(cx).await;
9834
9835 let language = Arc::new(Language::new(
9836 LanguageConfig {
9837 brackets: BracketPairConfig {
9838 pairs: vec![
9839 BracketPair {
9840 start: "{".to_string(),
9841 end: "}".to_string(),
9842 close: true,
9843 surround: true,
9844 newline: true,
9845 },
9846 BracketPair {
9847 start: "(".to_string(),
9848 end: ")".to_string(),
9849 close: true,
9850 surround: true,
9851 newline: true,
9852 },
9853 BracketPair {
9854 start: "[".to_string(),
9855 end: "]".to_string(),
9856 close: false,
9857 surround: false,
9858 newline: true,
9859 },
9860 ],
9861 ..Default::default()
9862 },
9863 autoclose_before: "})]".to_string(),
9864 ..Default::default()
9865 },
9866 Some(tree_sitter_rust::LANGUAGE.into()),
9867 ));
9868
9869 cx.language_registry().add(language.clone());
9870 cx.update_buffer(|buffer, cx| {
9871 buffer.set_language(Some(language), cx);
9872 });
9873
9874 cx.set_state(
9875 &"
9876 ˇ
9877 ˇ
9878 ˇ
9879 "
9880 .unindent(),
9881 );
9882
9883 // ensure only matching closing brackets are skipped over
9884 cx.update_editor(|editor, window, cx| {
9885 editor.handle_input("}", window, cx);
9886 editor.move_left(&MoveLeft, window, cx);
9887 editor.handle_input(")", window, cx);
9888 editor.move_left(&MoveLeft, window, cx);
9889 });
9890 cx.assert_editor_state(
9891 &"
9892 ˇ)}
9893 ˇ)}
9894 ˇ)}
9895 "
9896 .unindent(),
9897 );
9898
9899 // skip-over closing brackets at multiple cursors
9900 cx.update_editor(|editor, window, cx| {
9901 editor.handle_input(")", window, cx);
9902 editor.handle_input("}", window, cx);
9903 });
9904 cx.assert_editor_state(
9905 &"
9906 )}ˇ
9907 )}ˇ
9908 )}ˇ
9909 "
9910 .unindent(),
9911 );
9912
9913 // ignore non-close brackets
9914 cx.update_editor(|editor, window, cx| {
9915 editor.handle_input("]", window, cx);
9916 editor.move_left(&MoveLeft, window, cx);
9917 editor.handle_input("]", window, cx);
9918 });
9919 cx.assert_editor_state(
9920 &"
9921 )}]ˇ]
9922 )}]ˇ]
9923 )}]ˇ]
9924 "
9925 .unindent(),
9926 );
9927}
9928
9929#[gpui::test]
9930async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
9931 init_test(cx, |_| {});
9932
9933 let mut cx = EditorTestContext::new(cx).await;
9934
9935 let html_language = Arc::new(
9936 Language::new(
9937 LanguageConfig {
9938 name: "HTML".into(),
9939 brackets: BracketPairConfig {
9940 pairs: vec![
9941 BracketPair {
9942 start: "<".into(),
9943 end: ">".into(),
9944 close: true,
9945 ..Default::default()
9946 },
9947 BracketPair {
9948 start: "{".into(),
9949 end: "}".into(),
9950 close: true,
9951 ..Default::default()
9952 },
9953 BracketPair {
9954 start: "(".into(),
9955 end: ")".into(),
9956 close: true,
9957 ..Default::default()
9958 },
9959 ],
9960 ..Default::default()
9961 },
9962 autoclose_before: "})]>".into(),
9963 ..Default::default()
9964 },
9965 Some(tree_sitter_html::LANGUAGE.into()),
9966 )
9967 .with_injection_query(
9968 r#"
9969 (script_element
9970 (raw_text) @injection.content
9971 (#set! injection.language "javascript"))
9972 "#,
9973 )
9974 .unwrap(),
9975 );
9976
9977 let javascript_language = Arc::new(Language::new(
9978 LanguageConfig {
9979 name: "JavaScript".into(),
9980 brackets: BracketPairConfig {
9981 pairs: vec![
9982 BracketPair {
9983 start: "/*".into(),
9984 end: " */".into(),
9985 close: true,
9986 ..Default::default()
9987 },
9988 BracketPair {
9989 start: "{".into(),
9990 end: "}".into(),
9991 close: true,
9992 ..Default::default()
9993 },
9994 BracketPair {
9995 start: "(".into(),
9996 end: ")".into(),
9997 close: true,
9998 ..Default::default()
9999 },
10000 ],
10001 ..Default::default()
10002 },
10003 autoclose_before: "})]>".into(),
10004 ..Default::default()
10005 },
10006 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
10007 ));
10008
10009 cx.language_registry().add(html_language.clone());
10010 cx.language_registry().add(javascript_language);
10011 cx.executor().run_until_parked();
10012
10013 cx.update_buffer(|buffer, cx| {
10014 buffer.set_language(Some(html_language), cx);
10015 });
10016
10017 cx.set_state(
10018 &r#"
10019 <body>ˇ
10020 <script>
10021 var x = 1;ˇ
10022 </script>
10023 </body>ˇ
10024 "#
10025 .unindent(),
10026 );
10027
10028 // Precondition: different languages are active at different locations.
10029 cx.update_editor(|editor, window, cx| {
10030 let snapshot = editor.snapshot(window, cx);
10031 let cursors = editor.selections.ranges::<usize>(cx);
10032 let languages = cursors
10033 .iter()
10034 .map(|c| snapshot.language_at(c.start).unwrap().name())
10035 .collect::<Vec<_>>();
10036 assert_eq!(
10037 languages,
10038 &["HTML".into(), "JavaScript".into(), "HTML".into()]
10039 );
10040 });
10041
10042 // Angle brackets autoclose in HTML, but not JavaScript.
10043 cx.update_editor(|editor, window, cx| {
10044 editor.handle_input("<", window, cx);
10045 editor.handle_input("a", window, cx);
10046 });
10047 cx.assert_editor_state(
10048 &r#"
10049 <body><aˇ>
10050 <script>
10051 var x = 1;<aˇ
10052 </script>
10053 </body><aˇ>
10054 "#
10055 .unindent(),
10056 );
10057
10058 // Curly braces and parens autoclose in both HTML and JavaScript.
10059 cx.update_editor(|editor, window, cx| {
10060 editor.handle_input(" b=", window, cx);
10061 editor.handle_input("{", window, cx);
10062 editor.handle_input("c", window, cx);
10063 editor.handle_input("(", window, cx);
10064 });
10065 cx.assert_editor_state(
10066 &r#"
10067 <body><a b={c(ˇ)}>
10068 <script>
10069 var x = 1;<a b={c(ˇ)}
10070 </script>
10071 </body><a b={c(ˇ)}>
10072 "#
10073 .unindent(),
10074 );
10075
10076 // Brackets that were already autoclosed are skipped.
10077 cx.update_editor(|editor, window, cx| {
10078 editor.handle_input(")", window, cx);
10079 editor.handle_input("d", window, cx);
10080 editor.handle_input("}", window, cx);
10081 });
10082 cx.assert_editor_state(
10083 &r#"
10084 <body><a b={c()d}ˇ>
10085 <script>
10086 var x = 1;<a b={c()d}ˇ
10087 </script>
10088 </body><a b={c()d}ˇ>
10089 "#
10090 .unindent(),
10091 );
10092 cx.update_editor(|editor, window, cx| {
10093 editor.handle_input(">", window, cx);
10094 });
10095 cx.assert_editor_state(
10096 &r#"
10097 <body><a b={c()d}>ˇ
10098 <script>
10099 var x = 1;<a b={c()d}>ˇ
10100 </script>
10101 </body><a b={c()d}>ˇ
10102 "#
10103 .unindent(),
10104 );
10105
10106 // Reset
10107 cx.set_state(
10108 &r#"
10109 <body>ˇ
10110 <script>
10111 var x = 1;ˇ
10112 </script>
10113 </body>ˇ
10114 "#
10115 .unindent(),
10116 );
10117
10118 cx.update_editor(|editor, window, cx| {
10119 editor.handle_input("<", window, cx);
10120 });
10121 cx.assert_editor_state(
10122 &r#"
10123 <body><ˇ>
10124 <script>
10125 var x = 1;<ˇ
10126 </script>
10127 </body><ˇ>
10128 "#
10129 .unindent(),
10130 );
10131
10132 // When backspacing, the closing angle brackets are removed.
10133 cx.update_editor(|editor, window, cx| {
10134 editor.backspace(&Backspace, window, cx);
10135 });
10136 cx.assert_editor_state(
10137 &r#"
10138 <body>ˇ
10139 <script>
10140 var x = 1;ˇ
10141 </script>
10142 </body>ˇ
10143 "#
10144 .unindent(),
10145 );
10146
10147 // Block comments autoclose in JavaScript, but not HTML.
10148 cx.update_editor(|editor, window, cx| {
10149 editor.handle_input("/", window, cx);
10150 editor.handle_input("*", window, cx);
10151 });
10152 cx.assert_editor_state(
10153 &r#"
10154 <body>/*ˇ
10155 <script>
10156 var x = 1;/*ˇ */
10157 </script>
10158 </body>/*ˇ
10159 "#
10160 .unindent(),
10161 );
10162}
10163
10164#[gpui::test]
10165async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
10166 init_test(cx, |_| {});
10167
10168 let mut cx = EditorTestContext::new(cx).await;
10169
10170 let rust_language = Arc::new(
10171 Language::new(
10172 LanguageConfig {
10173 name: "Rust".into(),
10174 brackets: serde_json::from_value(json!([
10175 { "start": "{", "end": "}", "close": true, "newline": true },
10176 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
10177 ]))
10178 .unwrap(),
10179 autoclose_before: "})]>".into(),
10180 ..Default::default()
10181 },
10182 Some(tree_sitter_rust::LANGUAGE.into()),
10183 )
10184 .with_override_query("(string_literal) @string")
10185 .unwrap(),
10186 );
10187
10188 cx.language_registry().add(rust_language.clone());
10189 cx.update_buffer(|buffer, cx| {
10190 buffer.set_language(Some(rust_language), cx);
10191 });
10192
10193 cx.set_state(
10194 &r#"
10195 let x = ˇ
10196 "#
10197 .unindent(),
10198 );
10199
10200 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
10201 cx.update_editor(|editor, window, cx| {
10202 editor.handle_input("\"", window, cx);
10203 });
10204 cx.assert_editor_state(
10205 &r#"
10206 let x = "ˇ"
10207 "#
10208 .unindent(),
10209 );
10210
10211 // Inserting another quotation mark. The cursor moves across the existing
10212 // automatically-inserted quotation mark.
10213 cx.update_editor(|editor, window, cx| {
10214 editor.handle_input("\"", window, cx);
10215 });
10216 cx.assert_editor_state(
10217 &r#"
10218 let x = ""ˇ
10219 "#
10220 .unindent(),
10221 );
10222
10223 // Reset
10224 cx.set_state(
10225 &r#"
10226 let x = ˇ
10227 "#
10228 .unindent(),
10229 );
10230
10231 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
10232 cx.update_editor(|editor, window, cx| {
10233 editor.handle_input("\"", window, cx);
10234 editor.handle_input(" ", window, cx);
10235 editor.move_left(&Default::default(), window, cx);
10236 editor.handle_input("\\", window, cx);
10237 editor.handle_input("\"", window, cx);
10238 });
10239 cx.assert_editor_state(
10240 &r#"
10241 let x = "\"ˇ "
10242 "#
10243 .unindent(),
10244 );
10245
10246 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
10247 // mark. Nothing is inserted.
10248 cx.update_editor(|editor, window, cx| {
10249 editor.move_right(&Default::default(), window, cx);
10250 editor.handle_input("\"", window, cx);
10251 });
10252 cx.assert_editor_state(
10253 &r#"
10254 let x = "\" "ˇ
10255 "#
10256 .unindent(),
10257 );
10258}
10259
10260#[gpui::test]
10261async fn test_surround_with_pair(cx: &mut TestAppContext) {
10262 init_test(cx, |_| {});
10263
10264 let language = Arc::new(Language::new(
10265 LanguageConfig {
10266 brackets: BracketPairConfig {
10267 pairs: vec![
10268 BracketPair {
10269 start: "{".to_string(),
10270 end: "}".to_string(),
10271 close: true,
10272 surround: true,
10273 newline: true,
10274 },
10275 BracketPair {
10276 start: "/* ".to_string(),
10277 end: "*/".to_string(),
10278 close: true,
10279 surround: true,
10280 ..Default::default()
10281 },
10282 ],
10283 ..Default::default()
10284 },
10285 ..Default::default()
10286 },
10287 Some(tree_sitter_rust::LANGUAGE.into()),
10288 ));
10289
10290 let text = r#"
10291 a
10292 b
10293 c
10294 "#
10295 .unindent();
10296
10297 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10298 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10299 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10300 editor
10301 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10302 .await;
10303
10304 editor.update_in(cx, |editor, window, cx| {
10305 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10306 s.select_display_ranges([
10307 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10308 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10309 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
10310 ])
10311 });
10312
10313 editor.handle_input("{", window, cx);
10314 editor.handle_input("{", window, cx);
10315 editor.handle_input("{", window, cx);
10316 assert_eq!(
10317 editor.text(cx),
10318 "
10319 {{{a}}}
10320 {{{b}}}
10321 {{{c}}}
10322 "
10323 .unindent()
10324 );
10325 assert_eq!(
10326 editor.selections.display_ranges(cx),
10327 [
10328 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
10329 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
10330 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
10331 ]
10332 );
10333
10334 editor.undo(&Undo, window, cx);
10335 editor.undo(&Undo, window, cx);
10336 editor.undo(&Undo, window, cx);
10337 assert_eq!(
10338 editor.text(cx),
10339 "
10340 a
10341 b
10342 c
10343 "
10344 .unindent()
10345 );
10346 assert_eq!(
10347 editor.selections.display_ranges(cx),
10348 [
10349 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10350 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10351 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10352 ]
10353 );
10354
10355 // Ensure inserting the first character of a multi-byte bracket pair
10356 // doesn't surround the selections with the bracket.
10357 editor.handle_input("/", window, cx);
10358 assert_eq!(
10359 editor.text(cx),
10360 "
10361 /
10362 /
10363 /
10364 "
10365 .unindent()
10366 );
10367 assert_eq!(
10368 editor.selections.display_ranges(cx),
10369 [
10370 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10371 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10372 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10373 ]
10374 );
10375
10376 editor.undo(&Undo, window, cx);
10377 assert_eq!(
10378 editor.text(cx),
10379 "
10380 a
10381 b
10382 c
10383 "
10384 .unindent()
10385 );
10386 assert_eq!(
10387 editor.selections.display_ranges(cx),
10388 [
10389 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10390 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10391 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10392 ]
10393 );
10394
10395 // Ensure inserting the last character of a multi-byte bracket pair
10396 // doesn't surround the selections with the bracket.
10397 editor.handle_input("*", window, cx);
10398 assert_eq!(
10399 editor.text(cx),
10400 "
10401 *
10402 *
10403 *
10404 "
10405 .unindent()
10406 );
10407 assert_eq!(
10408 editor.selections.display_ranges(cx),
10409 [
10410 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10411 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10412 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10413 ]
10414 );
10415 });
10416}
10417
10418#[gpui::test]
10419async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
10420 init_test(cx, |_| {});
10421
10422 let language = Arc::new(Language::new(
10423 LanguageConfig {
10424 brackets: BracketPairConfig {
10425 pairs: vec![BracketPair {
10426 start: "{".to_string(),
10427 end: "}".to_string(),
10428 close: true,
10429 surround: true,
10430 newline: true,
10431 }],
10432 ..Default::default()
10433 },
10434 autoclose_before: "}".to_string(),
10435 ..Default::default()
10436 },
10437 Some(tree_sitter_rust::LANGUAGE.into()),
10438 ));
10439
10440 let text = r#"
10441 a
10442 b
10443 c
10444 "#
10445 .unindent();
10446
10447 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10448 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10449 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10450 editor
10451 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10452 .await;
10453
10454 editor.update_in(cx, |editor, window, cx| {
10455 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10456 s.select_ranges([
10457 Point::new(0, 1)..Point::new(0, 1),
10458 Point::new(1, 1)..Point::new(1, 1),
10459 Point::new(2, 1)..Point::new(2, 1),
10460 ])
10461 });
10462
10463 editor.handle_input("{", window, cx);
10464 editor.handle_input("{", window, cx);
10465 editor.handle_input("_", window, cx);
10466 assert_eq!(
10467 editor.text(cx),
10468 "
10469 a{{_}}
10470 b{{_}}
10471 c{{_}}
10472 "
10473 .unindent()
10474 );
10475 assert_eq!(
10476 editor.selections.ranges::<Point>(cx),
10477 [
10478 Point::new(0, 4)..Point::new(0, 4),
10479 Point::new(1, 4)..Point::new(1, 4),
10480 Point::new(2, 4)..Point::new(2, 4)
10481 ]
10482 );
10483
10484 editor.backspace(&Default::default(), window, cx);
10485 editor.backspace(&Default::default(), window, cx);
10486 assert_eq!(
10487 editor.text(cx),
10488 "
10489 a{}
10490 b{}
10491 c{}
10492 "
10493 .unindent()
10494 );
10495 assert_eq!(
10496 editor.selections.ranges::<Point>(cx),
10497 [
10498 Point::new(0, 2)..Point::new(0, 2),
10499 Point::new(1, 2)..Point::new(1, 2),
10500 Point::new(2, 2)..Point::new(2, 2)
10501 ]
10502 );
10503
10504 editor.delete_to_previous_word_start(&Default::default(), window, cx);
10505 assert_eq!(
10506 editor.text(cx),
10507 "
10508 a
10509 b
10510 c
10511 "
10512 .unindent()
10513 );
10514 assert_eq!(
10515 editor.selections.ranges::<Point>(cx),
10516 [
10517 Point::new(0, 1)..Point::new(0, 1),
10518 Point::new(1, 1)..Point::new(1, 1),
10519 Point::new(2, 1)..Point::new(2, 1)
10520 ]
10521 );
10522 });
10523}
10524
10525#[gpui::test]
10526async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
10527 init_test(cx, |settings| {
10528 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
10529 });
10530
10531 let mut cx = EditorTestContext::new(cx).await;
10532
10533 let language = Arc::new(Language::new(
10534 LanguageConfig {
10535 brackets: BracketPairConfig {
10536 pairs: vec![
10537 BracketPair {
10538 start: "{".to_string(),
10539 end: "}".to_string(),
10540 close: true,
10541 surround: true,
10542 newline: true,
10543 },
10544 BracketPair {
10545 start: "(".to_string(),
10546 end: ")".to_string(),
10547 close: true,
10548 surround: true,
10549 newline: true,
10550 },
10551 BracketPair {
10552 start: "[".to_string(),
10553 end: "]".to_string(),
10554 close: false,
10555 surround: true,
10556 newline: true,
10557 },
10558 ],
10559 ..Default::default()
10560 },
10561 autoclose_before: "})]".to_string(),
10562 ..Default::default()
10563 },
10564 Some(tree_sitter_rust::LANGUAGE.into()),
10565 ));
10566
10567 cx.language_registry().add(language.clone());
10568 cx.update_buffer(|buffer, cx| {
10569 buffer.set_language(Some(language), cx);
10570 });
10571
10572 cx.set_state(
10573 &"
10574 {(ˇ)}
10575 [[ˇ]]
10576 {(ˇ)}
10577 "
10578 .unindent(),
10579 );
10580
10581 cx.update_editor(|editor, window, cx| {
10582 editor.backspace(&Default::default(), window, cx);
10583 editor.backspace(&Default::default(), window, cx);
10584 });
10585
10586 cx.assert_editor_state(
10587 &"
10588 ˇ
10589 ˇ]]
10590 ˇ
10591 "
10592 .unindent(),
10593 );
10594
10595 cx.update_editor(|editor, window, cx| {
10596 editor.handle_input("{", window, cx);
10597 editor.handle_input("{", window, cx);
10598 editor.move_right(&MoveRight, window, cx);
10599 editor.move_right(&MoveRight, window, cx);
10600 editor.move_left(&MoveLeft, window, cx);
10601 editor.move_left(&MoveLeft, window, cx);
10602 editor.backspace(&Default::default(), window, cx);
10603 });
10604
10605 cx.assert_editor_state(
10606 &"
10607 {ˇ}
10608 {ˇ}]]
10609 {ˇ}
10610 "
10611 .unindent(),
10612 );
10613
10614 cx.update_editor(|editor, window, cx| {
10615 editor.backspace(&Default::default(), window, cx);
10616 });
10617
10618 cx.assert_editor_state(
10619 &"
10620 ˇ
10621 ˇ]]
10622 ˇ
10623 "
10624 .unindent(),
10625 );
10626}
10627
10628#[gpui::test]
10629async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
10630 init_test(cx, |_| {});
10631
10632 let language = Arc::new(Language::new(
10633 LanguageConfig::default(),
10634 Some(tree_sitter_rust::LANGUAGE.into()),
10635 ));
10636
10637 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
10638 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10639 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10640 editor
10641 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10642 .await;
10643
10644 editor.update_in(cx, |editor, window, cx| {
10645 editor.set_auto_replace_emoji_shortcode(true);
10646
10647 editor.handle_input("Hello ", window, cx);
10648 editor.handle_input(":wave", window, cx);
10649 assert_eq!(editor.text(cx), "Hello :wave".unindent());
10650
10651 editor.handle_input(":", window, cx);
10652 assert_eq!(editor.text(cx), "Hello 👋".unindent());
10653
10654 editor.handle_input(" :smile", window, cx);
10655 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
10656
10657 editor.handle_input(":", window, cx);
10658 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
10659
10660 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
10661 editor.handle_input(":wave", window, cx);
10662 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
10663
10664 editor.handle_input(":", window, cx);
10665 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
10666
10667 editor.handle_input(":1", window, cx);
10668 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
10669
10670 editor.handle_input(":", window, cx);
10671 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
10672
10673 // Ensure shortcode does not get replaced when it is part of a word
10674 editor.handle_input(" Test:wave", window, cx);
10675 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
10676
10677 editor.handle_input(":", window, cx);
10678 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
10679
10680 editor.set_auto_replace_emoji_shortcode(false);
10681
10682 // Ensure shortcode does not get replaced when auto replace is off
10683 editor.handle_input(" :wave", window, cx);
10684 assert_eq!(
10685 editor.text(cx),
10686 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
10687 );
10688
10689 editor.handle_input(":", window, cx);
10690 assert_eq!(
10691 editor.text(cx),
10692 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
10693 );
10694 });
10695}
10696
10697#[gpui::test]
10698async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
10699 init_test(cx, |_| {});
10700
10701 let (text, insertion_ranges) = marked_text_ranges(
10702 indoc! {"
10703 ˇ
10704 "},
10705 false,
10706 );
10707
10708 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
10709 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10710
10711 _ = editor.update_in(cx, |editor, window, cx| {
10712 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
10713
10714 editor
10715 .insert_snippet(&insertion_ranges, snippet, window, cx)
10716 .unwrap();
10717
10718 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
10719 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
10720 assert_eq!(editor.text(cx), expected_text);
10721 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
10722 }
10723
10724 assert(
10725 editor,
10726 cx,
10727 indoc! {"
10728 type «» =•
10729 "},
10730 );
10731
10732 assert!(editor.context_menu_visible(), "There should be a matches");
10733 });
10734}
10735
10736#[gpui::test]
10737async fn test_snippets(cx: &mut TestAppContext) {
10738 init_test(cx, |_| {});
10739
10740 let mut cx = EditorTestContext::new(cx).await;
10741
10742 cx.set_state(indoc! {"
10743 a.ˇ b
10744 a.ˇ b
10745 a.ˇ b
10746 "});
10747
10748 cx.update_editor(|editor, window, cx| {
10749 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
10750 let insertion_ranges = editor
10751 .selections
10752 .all(cx)
10753 .iter()
10754 .map(|s| s.range())
10755 .collect::<Vec<_>>();
10756 editor
10757 .insert_snippet(&insertion_ranges, snippet, window, cx)
10758 .unwrap();
10759 });
10760
10761 cx.assert_editor_state(indoc! {"
10762 a.f(«oneˇ», two, «threeˇ») b
10763 a.f(«oneˇ», two, «threeˇ») b
10764 a.f(«oneˇ», two, «threeˇ») b
10765 "});
10766
10767 // Can't move earlier than the first tab stop
10768 cx.update_editor(|editor, window, cx| {
10769 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
10770 });
10771 cx.assert_editor_state(indoc! {"
10772 a.f(«oneˇ», two, «threeˇ») b
10773 a.f(«oneˇ», two, «threeˇ») b
10774 a.f(«oneˇ», two, «threeˇ») b
10775 "});
10776
10777 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
10778 cx.assert_editor_state(indoc! {"
10779 a.f(one, «twoˇ», three) b
10780 a.f(one, «twoˇ», three) b
10781 a.f(one, «twoˇ», three) b
10782 "});
10783
10784 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
10785 cx.assert_editor_state(indoc! {"
10786 a.f(«oneˇ», two, «threeˇ») b
10787 a.f(«oneˇ», two, «threeˇ») b
10788 a.f(«oneˇ», two, «threeˇ») b
10789 "});
10790
10791 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
10792 cx.assert_editor_state(indoc! {"
10793 a.f(one, «twoˇ», three) b
10794 a.f(one, «twoˇ», three) b
10795 a.f(one, «twoˇ», three) b
10796 "});
10797 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
10798 cx.assert_editor_state(indoc! {"
10799 a.f(one, two, three)ˇ b
10800 a.f(one, two, three)ˇ b
10801 a.f(one, two, three)ˇ b
10802 "});
10803
10804 // As soon as the last tab stop is reached, snippet state is gone
10805 cx.update_editor(|editor, window, cx| {
10806 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
10807 });
10808 cx.assert_editor_state(indoc! {"
10809 a.f(one, two, three)ˇ b
10810 a.f(one, two, three)ˇ b
10811 a.f(one, two, three)ˇ b
10812 "});
10813}
10814
10815#[gpui::test]
10816async fn test_snippet_indentation(cx: &mut TestAppContext) {
10817 init_test(cx, |_| {});
10818
10819 let mut cx = EditorTestContext::new(cx).await;
10820
10821 cx.update_editor(|editor, window, cx| {
10822 let snippet = Snippet::parse(indoc! {"
10823 /*
10824 * Multiline comment with leading indentation
10825 *
10826 * $1
10827 */
10828 $0"})
10829 .unwrap();
10830 let insertion_ranges = editor
10831 .selections
10832 .all(cx)
10833 .iter()
10834 .map(|s| s.range())
10835 .collect::<Vec<_>>();
10836 editor
10837 .insert_snippet(&insertion_ranges, snippet, window, cx)
10838 .unwrap();
10839 });
10840
10841 cx.assert_editor_state(indoc! {"
10842 /*
10843 * Multiline comment with leading indentation
10844 *
10845 * ˇ
10846 */
10847 "});
10848
10849 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
10850 cx.assert_editor_state(indoc! {"
10851 /*
10852 * Multiline comment with leading indentation
10853 *
10854 *•
10855 */
10856 ˇ"});
10857}
10858
10859#[gpui::test]
10860async fn test_document_format_during_save(cx: &mut TestAppContext) {
10861 init_test(cx, |_| {});
10862
10863 let fs = FakeFs::new(cx.executor());
10864 fs.insert_file(path!("/file.rs"), Default::default()).await;
10865
10866 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
10867
10868 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10869 language_registry.add(rust_lang());
10870 let mut fake_servers = language_registry.register_fake_lsp(
10871 "Rust",
10872 FakeLspAdapter {
10873 capabilities: lsp::ServerCapabilities {
10874 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10875 ..Default::default()
10876 },
10877 ..Default::default()
10878 },
10879 );
10880
10881 let buffer = project
10882 .update(cx, |project, cx| {
10883 project.open_local_buffer(path!("/file.rs"), cx)
10884 })
10885 .await
10886 .unwrap();
10887
10888 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10889 let (editor, cx) = cx.add_window_view(|window, cx| {
10890 build_editor_with_project(project.clone(), buffer, window, cx)
10891 });
10892 editor.update_in(cx, |editor, window, cx| {
10893 editor.set_text("one\ntwo\nthree\n", window, cx)
10894 });
10895 assert!(cx.read(|cx| editor.is_dirty(cx)));
10896
10897 cx.executor().start_waiting();
10898 let fake_server = fake_servers.next().await.unwrap();
10899
10900 {
10901 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10902 move |params, _| async move {
10903 assert_eq!(
10904 params.text_document.uri,
10905 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
10906 );
10907 assert_eq!(params.options.tab_size, 4);
10908 Ok(Some(vec![lsp::TextEdit::new(
10909 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
10910 ", ".to_string(),
10911 )]))
10912 },
10913 );
10914 let save = editor
10915 .update_in(cx, |editor, window, cx| {
10916 editor.save(
10917 SaveOptions {
10918 format: true,
10919 autosave: false,
10920 },
10921 project.clone(),
10922 window,
10923 cx,
10924 )
10925 })
10926 .unwrap();
10927 cx.executor().start_waiting();
10928 save.await;
10929
10930 assert_eq!(
10931 editor.update(cx, |editor, cx| editor.text(cx)),
10932 "one, two\nthree\n"
10933 );
10934 assert!(!cx.read(|cx| editor.is_dirty(cx)));
10935 }
10936
10937 {
10938 editor.update_in(cx, |editor, window, cx| {
10939 editor.set_text("one\ntwo\nthree\n", window, cx)
10940 });
10941 assert!(cx.read(|cx| editor.is_dirty(cx)));
10942
10943 // Ensure we can still save even if formatting hangs.
10944 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10945 move |params, _| async move {
10946 assert_eq!(
10947 params.text_document.uri,
10948 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
10949 );
10950 futures::future::pending::<()>().await;
10951 unreachable!()
10952 },
10953 );
10954 let save = editor
10955 .update_in(cx, |editor, window, cx| {
10956 editor.save(
10957 SaveOptions {
10958 format: true,
10959 autosave: false,
10960 },
10961 project.clone(),
10962 window,
10963 cx,
10964 )
10965 })
10966 .unwrap();
10967 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
10968 cx.executor().start_waiting();
10969 save.await;
10970 assert_eq!(
10971 editor.update(cx, |editor, cx| editor.text(cx)),
10972 "one\ntwo\nthree\n"
10973 );
10974 }
10975
10976 // Set rust language override and assert overridden tabsize is sent to language server
10977 update_test_language_settings(cx, |settings| {
10978 settings.languages.0.insert(
10979 "Rust".into(),
10980 LanguageSettingsContent {
10981 tab_size: NonZeroU32::new(8),
10982 ..Default::default()
10983 },
10984 );
10985 });
10986
10987 {
10988 editor.update_in(cx, |editor, window, cx| {
10989 editor.set_text("somehting_new\n", window, cx)
10990 });
10991 assert!(cx.read(|cx| editor.is_dirty(cx)));
10992 let _formatting_request_signal = fake_server
10993 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
10994 assert_eq!(
10995 params.text_document.uri,
10996 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
10997 );
10998 assert_eq!(params.options.tab_size, 8);
10999 Ok(Some(vec![]))
11000 });
11001 let save = editor
11002 .update_in(cx, |editor, window, cx| {
11003 editor.save(
11004 SaveOptions {
11005 format: true,
11006 autosave: false,
11007 },
11008 project.clone(),
11009 window,
11010 cx,
11011 )
11012 })
11013 .unwrap();
11014 cx.executor().start_waiting();
11015 save.await;
11016 }
11017}
11018
11019#[gpui::test]
11020async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
11021 init_test(cx, |settings| {
11022 settings.defaults.ensure_final_newline_on_save = Some(false);
11023 });
11024
11025 let fs = FakeFs::new(cx.executor());
11026 fs.insert_file(path!("/file.txt"), "foo".into()).await;
11027
11028 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
11029
11030 let buffer = project
11031 .update(cx, |project, cx| {
11032 project.open_local_buffer(path!("/file.txt"), cx)
11033 })
11034 .await
11035 .unwrap();
11036
11037 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11038 let (editor, cx) = cx.add_window_view(|window, cx| {
11039 build_editor_with_project(project.clone(), buffer, window, cx)
11040 });
11041 editor.update_in(cx, |editor, window, cx| {
11042 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
11043 s.select_ranges([0..0])
11044 });
11045 });
11046 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11047
11048 editor.update_in(cx, |editor, window, cx| {
11049 editor.handle_input("\n", window, cx)
11050 });
11051 cx.run_until_parked();
11052 save(&editor, &project, cx).await;
11053 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11054
11055 editor.update_in(cx, |editor, window, cx| {
11056 editor.undo(&Default::default(), window, cx);
11057 });
11058 save(&editor, &project, cx).await;
11059 assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11060
11061 editor.update_in(cx, |editor, window, cx| {
11062 editor.redo(&Default::default(), window, cx);
11063 });
11064 cx.run_until_parked();
11065 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11066
11067 async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
11068 let save = editor
11069 .update_in(cx, |editor, window, cx| {
11070 editor.save(
11071 SaveOptions {
11072 format: true,
11073 autosave: false,
11074 },
11075 project.clone(),
11076 window,
11077 cx,
11078 )
11079 })
11080 .unwrap();
11081 cx.executor().start_waiting();
11082 save.await;
11083 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11084 }
11085}
11086
11087#[gpui::test]
11088async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
11089 init_test(cx, |_| {});
11090
11091 let cols = 4;
11092 let rows = 10;
11093 let sample_text_1 = sample_text(rows, cols, 'a');
11094 assert_eq!(
11095 sample_text_1,
11096 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
11097 );
11098 let sample_text_2 = sample_text(rows, cols, 'l');
11099 assert_eq!(
11100 sample_text_2,
11101 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
11102 );
11103 let sample_text_3 = sample_text(rows, cols, 'v');
11104 assert_eq!(
11105 sample_text_3,
11106 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
11107 );
11108
11109 let fs = FakeFs::new(cx.executor());
11110 fs.insert_tree(
11111 path!("/a"),
11112 json!({
11113 "main.rs": sample_text_1,
11114 "other.rs": sample_text_2,
11115 "lib.rs": sample_text_3,
11116 }),
11117 )
11118 .await;
11119
11120 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11121 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11122 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11123
11124 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11125 language_registry.add(rust_lang());
11126 let mut fake_servers = language_registry.register_fake_lsp(
11127 "Rust",
11128 FakeLspAdapter {
11129 capabilities: lsp::ServerCapabilities {
11130 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11131 ..Default::default()
11132 },
11133 ..Default::default()
11134 },
11135 );
11136
11137 let worktree = project.update(cx, |project, cx| {
11138 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
11139 assert_eq!(worktrees.len(), 1);
11140 worktrees.pop().unwrap()
11141 });
11142 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
11143
11144 let buffer_1 = project
11145 .update(cx, |project, cx| {
11146 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
11147 })
11148 .await
11149 .unwrap();
11150 let buffer_2 = project
11151 .update(cx, |project, cx| {
11152 project.open_buffer((worktree_id, rel_path("other.rs")), cx)
11153 })
11154 .await
11155 .unwrap();
11156 let buffer_3 = project
11157 .update(cx, |project, cx| {
11158 project.open_buffer((worktree_id, rel_path("lib.rs")), cx)
11159 })
11160 .await
11161 .unwrap();
11162
11163 let multi_buffer = cx.new(|cx| {
11164 let mut multi_buffer = MultiBuffer::new(ReadWrite);
11165 multi_buffer.push_excerpts(
11166 buffer_1.clone(),
11167 [
11168 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11169 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11170 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11171 ],
11172 cx,
11173 );
11174 multi_buffer.push_excerpts(
11175 buffer_2.clone(),
11176 [
11177 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11178 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11179 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11180 ],
11181 cx,
11182 );
11183 multi_buffer.push_excerpts(
11184 buffer_3.clone(),
11185 [
11186 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11187 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11188 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11189 ],
11190 cx,
11191 );
11192 multi_buffer
11193 });
11194 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
11195 Editor::new(
11196 EditorMode::full(),
11197 multi_buffer,
11198 Some(project.clone()),
11199 window,
11200 cx,
11201 )
11202 });
11203
11204 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11205 editor.change_selections(
11206 SelectionEffects::scroll(Autoscroll::Next),
11207 window,
11208 cx,
11209 |s| s.select_ranges(Some(1..2)),
11210 );
11211 editor.insert("|one|two|three|", window, cx);
11212 });
11213 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11214 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11215 editor.change_selections(
11216 SelectionEffects::scroll(Autoscroll::Next),
11217 window,
11218 cx,
11219 |s| s.select_ranges(Some(60..70)),
11220 );
11221 editor.insert("|four|five|six|", window, cx);
11222 });
11223 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11224
11225 // First two buffers should be edited, but not the third one.
11226 assert_eq!(
11227 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11228 "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}",
11229 );
11230 buffer_1.update(cx, |buffer, _| {
11231 assert!(buffer.is_dirty());
11232 assert_eq!(
11233 buffer.text(),
11234 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
11235 )
11236 });
11237 buffer_2.update(cx, |buffer, _| {
11238 assert!(buffer.is_dirty());
11239 assert_eq!(
11240 buffer.text(),
11241 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
11242 )
11243 });
11244 buffer_3.update(cx, |buffer, _| {
11245 assert!(!buffer.is_dirty());
11246 assert_eq!(buffer.text(), sample_text_3,)
11247 });
11248 cx.executor().run_until_parked();
11249
11250 cx.executor().start_waiting();
11251 let save = multi_buffer_editor
11252 .update_in(cx, |editor, window, cx| {
11253 editor.save(
11254 SaveOptions {
11255 format: true,
11256 autosave: false,
11257 },
11258 project.clone(),
11259 window,
11260 cx,
11261 )
11262 })
11263 .unwrap();
11264
11265 let fake_server = fake_servers.next().await.unwrap();
11266 fake_server
11267 .server
11268 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
11269 Ok(Some(vec![lsp::TextEdit::new(
11270 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11271 format!("[{} formatted]", params.text_document.uri),
11272 )]))
11273 })
11274 .detach();
11275 save.await;
11276
11277 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
11278 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
11279 assert_eq!(
11280 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11281 uri!(
11282 "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}"
11283 ),
11284 );
11285 buffer_1.update(cx, |buffer, _| {
11286 assert!(!buffer.is_dirty());
11287 assert_eq!(
11288 buffer.text(),
11289 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
11290 )
11291 });
11292 buffer_2.update(cx, |buffer, _| {
11293 assert!(!buffer.is_dirty());
11294 assert_eq!(
11295 buffer.text(),
11296 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
11297 )
11298 });
11299 buffer_3.update(cx, |buffer, _| {
11300 assert!(!buffer.is_dirty());
11301 assert_eq!(buffer.text(), sample_text_3,)
11302 });
11303}
11304
11305#[gpui::test]
11306async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
11307 init_test(cx, |_| {});
11308
11309 let fs = FakeFs::new(cx.executor());
11310 fs.insert_tree(
11311 path!("/dir"),
11312 json!({
11313 "file1.rs": "fn main() { println!(\"hello\"); }",
11314 "file2.rs": "fn test() { println!(\"test\"); }",
11315 "file3.rs": "fn other() { println!(\"other\"); }\n",
11316 }),
11317 )
11318 .await;
11319
11320 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
11321 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11322 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11323
11324 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11325 language_registry.add(rust_lang());
11326
11327 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
11328 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
11329
11330 // Open three buffers
11331 let buffer_1 = project
11332 .update(cx, |project, cx| {
11333 project.open_buffer((worktree_id, rel_path("file1.rs")), cx)
11334 })
11335 .await
11336 .unwrap();
11337 let buffer_2 = project
11338 .update(cx, |project, cx| {
11339 project.open_buffer((worktree_id, rel_path("file2.rs")), cx)
11340 })
11341 .await
11342 .unwrap();
11343 let buffer_3 = project
11344 .update(cx, |project, cx| {
11345 project.open_buffer((worktree_id, rel_path("file3.rs")), cx)
11346 })
11347 .await
11348 .unwrap();
11349
11350 // Create a multi-buffer with all three buffers
11351 let multi_buffer = cx.new(|cx| {
11352 let mut multi_buffer = MultiBuffer::new(ReadWrite);
11353 multi_buffer.push_excerpts(
11354 buffer_1.clone(),
11355 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11356 cx,
11357 );
11358 multi_buffer.push_excerpts(
11359 buffer_2.clone(),
11360 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11361 cx,
11362 );
11363 multi_buffer.push_excerpts(
11364 buffer_3.clone(),
11365 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11366 cx,
11367 );
11368 multi_buffer
11369 });
11370
11371 let editor = cx.new_window_entity(|window, cx| {
11372 Editor::new(
11373 EditorMode::full(),
11374 multi_buffer,
11375 Some(project.clone()),
11376 window,
11377 cx,
11378 )
11379 });
11380
11381 // Edit only the first buffer
11382 editor.update_in(cx, |editor, window, cx| {
11383 editor.change_selections(
11384 SelectionEffects::scroll(Autoscroll::Next),
11385 window,
11386 cx,
11387 |s| s.select_ranges(Some(10..10)),
11388 );
11389 editor.insert("// edited", window, cx);
11390 });
11391
11392 // Verify that only buffer 1 is dirty
11393 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
11394 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11395 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11396
11397 // Get write counts after file creation (files were created with initial content)
11398 // We expect each file to have been written once during creation
11399 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
11400 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
11401 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
11402
11403 // Perform autosave
11404 let save_task = editor.update_in(cx, |editor, window, cx| {
11405 editor.save(
11406 SaveOptions {
11407 format: true,
11408 autosave: true,
11409 },
11410 project.clone(),
11411 window,
11412 cx,
11413 )
11414 });
11415 save_task.await.unwrap();
11416
11417 // Only the dirty buffer should have been saved
11418 assert_eq!(
11419 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
11420 1,
11421 "Buffer 1 was dirty, so it should have been written once during autosave"
11422 );
11423 assert_eq!(
11424 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
11425 0,
11426 "Buffer 2 was clean, so it should not have been written during autosave"
11427 );
11428 assert_eq!(
11429 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
11430 0,
11431 "Buffer 3 was clean, so it should not have been written during autosave"
11432 );
11433
11434 // Verify buffer states after autosave
11435 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11436 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11437 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11438
11439 // Now perform a manual save (format = true)
11440 let save_task = editor.update_in(cx, |editor, window, cx| {
11441 editor.save(
11442 SaveOptions {
11443 format: true,
11444 autosave: false,
11445 },
11446 project.clone(),
11447 window,
11448 cx,
11449 )
11450 });
11451 save_task.await.unwrap();
11452
11453 // During manual save, clean buffers don't get written to disk
11454 // They just get did_save called for language server notifications
11455 assert_eq!(
11456 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
11457 1,
11458 "Buffer 1 should only have been written once total (during autosave, not manual save)"
11459 );
11460 assert_eq!(
11461 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
11462 0,
11463 "Buffer 2 should not have been written at all"
11464 );
11465 assert_eq!(
11466 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
11467 0,
11468 "Buffer 3 should not have been written at all"
11469 );
11470}
11471
11472async fn setup_range_format_test(
11473 cx: &mut TestAppContext,
11474) -> (
11475 Entity<Project>,
11476 Entity<Editor>,
11477 &mut gpui::VisualTestContext,
11478 lsp::FakeLanguageServer,
11479) {
11480 init_test(cx, |_| {});
11481
11482 let fs = FakeFs::new(cx.executor());
11483 fs.insert_file(path!("/file.rs"), Default::default()).await;
11484
11485 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11486
11487 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11488 language_registry.add(rust_lang());
11489 let mut fake_servers = language_registry.register_fake_lsp(
11490 "Rust",
11491 FakeLspAdapter {
11492 capabilities: lsp::ServerCapabilities {
11493 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
11494 ..lsp::ServerCapabilities::default()
11495 },
11496 ..FakeLspAdapter::default()
11497 },
11498 );
11499
11500 let buffer = project
11501 .update(cx, |project, cx| {
11502 project.open_local_buffer(path!("/file.rs"), cx)
11503 })
11504 .await
11505 .unwrap();
11506
11507 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11508 let (editor, cx) = cx.add_window_view(|window, cx| {
11509 build_editor_with_project(project.clone(), buffer, window, cx)
11510 });
11511
11512 cx.executor().start_waiting();
11513 let fake_server = fake_servers.next().await.unwrap();
11514
11515 (project, editor, cx, fake_server)
11516}
11517
11518#[gpui::test]
11519async fn test_range_format_on_save_success(cx: &mut TestAppContext) {
11520 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11521
11522 editor.update_in(cx, |editor, window, cx| {
11523 editor.set_text("one\ntwo\nthree\n", window, cx)
11524 });
11525 assert!(cx.read(|cx| editor.is_dirty(cx)));
11526
11527 let save = editor
11528 .update_in(cx, |editor, window, cx| {
11529 editor.save(
11530 SaveOptions {
11531 format: true,
11532 autosave: false,
11533 },
11534 project.clone(),
11535 window,
11536 cx,
11537 )
11538 })
11539 .unwrap();
11540 fake_server
11541 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
11542 assert_eq!(
11543 params.text_document.uri,
11544 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11545 );
11546 assert_eq!(params.options.tab_size, 4);
11547 Ok(Some(vec![lsp::TextEdit::new(
11548 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11549 ", ".to_string(),
11550 )]))
11551 })
11552 .next()
11553 .await;
11554 cx.executor().start_waiting();
11555 save.await;
11556 assert_eq!(
11557 editor.update(cx, |editor, cx| editor.text(cx)),
11558 "one, two\nthree\n"
11559 );
11560 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11561}
11562
11563#[gpui::test]
11564async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) {
11565 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11566
11567 editor.update_in(cx, |editor, window, cx| {
11568 editor.set_text("one\ntwo\nthree\n", window, cx)
11569 });
11570 assert!(cx.read(|cx| editor.is_dirty(cx)));
11571
11572 // Test that save still works when formatting hangs
11573 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
11574 move |params, _| async move {
11575 assert_eq!(
11576 params.text_document.uri,
11577 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11578 );
11579 futures::future::pending::<()>().await;
11580 unreachable!()
11581 },
11582 );
11583 let save = editor
11584 .update_in(cx, |editor, window, cx| {
11585 editor.save(
11586 SaveOptions {
11587 format: true,
11588 autosave: false,
11589 },
11590 project.clone(),
11591 window,
11592 cx,
11593 )
11594 })
11595 .unwrap();
11596 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11597 cx.executor().start_waiting();
11598 save.await;
11599 assert_eq!(
11600 editor.update(cx, |editor, cx| editor.text(cx)),
11601 "one\ntwo\nthree\n"
11602 );
11603 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11604}
11605
11606#[gpui::test]
11607async fn test_range_format_not_called_for_clean_buffer(cx: &mut TestAppContext) {
11608 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11609
11610 // Buffer starts clean, no formatting should be requested
11611 let save = editor
11612 .update_in(cx, |editor, window, cx| {
11613 editor.save(
11614 SaveOptions {
11615 format: false,
11616 autosave: false,
11617 },
11618 project.clone(),
11619 window,
11620 cx,
11621 )
11622 })
11623 .unwrap();
11624 let _pending_format_request = fake_server
11625 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
11626 panic!("Should not be invoked");
11627 })
11628 .next();
11629 cx.executor().start_waiting();
11630 save.await;
11631 cx.run_until_parked();
11632}
11633
11634#[gpui::test]
11635async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppContext) {
11636 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11637
11638 // Set Rust language override and assert overridden tabsize is sent to language server
11639 update_test_language_settings(cx, |settings| {
11640 settings.languages.0.insert(
11641 "Rust".into(),
11642 LanguageSettingsContent {
11643 tab_size: NonZeroU32::new(8),
11644 ..Default::default()
11645 },
11646 );
11647 });
11648
11649 editor.update_in(cx, |editor, window, cx| {
11650 editor.set_text("something_new\n", window, cx)
11651 });
11652 assert!(cx.read(|cx| editor.is_dirty(cx)));
11653 let save = editor
11654 .update_in(cx, |editor, window, cx| {
11655 editor.save(
11656 SaveOptions {
11657 format: true,
11658 autosave: false,
11659 },
11660 project.clone(),
11661 window,
11662 cx,
11663 )
11664 })
11665 .unwrap();
11666 fake_server
11667 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
11668 assert_eq!(
11669 params.text_document.uri,
11670 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11671 );
11672 assert_eq!(params.options.tab_size, 8);
11673 Ok(Some(Vec::new()))
11674 })
11675 .next()
11676 .await;
11677 save.await;
11678}
11679
11680#[gpui::test]
11681async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
11682 init_test(cx, |settings| {
11683 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
11684 Formatter::LanguageServer { name: None },
11685 )))
11686 });
11687
11688 let fs = FakeFs::new(cx.executor());
11689 fs.insert_file(path!("/file.rs"), Default::default()).await;
11690
11691 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11692
11693 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11694 language_registry.add(Arc::new(Language::new(
11695 LanguageConfig {
11696 name: "Rust".into(),
11697 matcher: LanguageMatcher {
11698 path_suffixes: vec!["rs".to_string()],
11699 ..Default::default()
11700 },
11701 ..LanguageConfig::default()
11702 },
11703 Some(tree_sitter_rust::LANGUAGE.into()),
11704 )));
11705 update_test_language_settings(cx, |settings| {
11706 // Enable Prettier formatting for the same buffer, and ensure
11707 // LSP is called instead of Prettier.
11708 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
11709 });
11710 let mut fake_servers = language_registry.register_fake_lsp(
11711 "Rust",
11712 FakeLspAdapter {
11713 capabilities: lsp::ServerCapabilities {
11714 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11715 ..Default::default()
11716 },
11717 ..Default::default()
11718 },
11719 );
11720
11721 let buffer = project
11722 .update(cx, |project, cx| {
11723 project.open_local_buffer(path!("/file.rs"), cx)
11724 })
11725 .await
11726 .unwrap();
11727
11728 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11729 let (editor, cx) = cx.add_window_view(|window, cx| {
11730 build_editor_with_project(project.clone(), buffer, window, cx)
11731 });
11732 editor.update_in(cx, |editor, window, cx| {
11733 editor.set_text("one\ntwo\nthree\n", window, cx)
11734 });
11735
11736 cx.executor().start_waiting();
11737 let fake_server = fake_servers.next().await.unwrap();
11738
11739 let format = editor
11740 .update_in(cx, |editor, window, cx| {
11741 editor.perform_format(
11742 project.clone(),
11743 FormatTrigger::Manual,
11744 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
11745 window,
11746 cx,
11747 )
11748 })
11749 .unwrap();
11750 fake_server
11751 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
11752 assert_eq!(
11753 params.text_document.uri,
11754 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11755 );
11756 assert_eq!(params.options.tab_size, 4);
11757 Ok(Some(vec![lsp::TextEdit::new(
11758 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11759 ", ".to_string(),
11760 )]))
11761 })
11762 .next()
11763 .await;
11764 cx.executor().start_waiting();
11765 format.await;
11766 assert_eq!(
11767 editor.update(cx, |editor, cx| editor.text(cx)),
11768 "one, two\nthree\n"
11769 );
11770
11771 editor.update_in(cx, |editor, window, cx| {
11772 editor.set_text("one\ntwo\nthree\n", window, cx)
11773 });
11774 // Ensure we don't lock if formatting hangs.
11775 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11776 move |params, _| async move {
11777 assert_eq!(
11778 params.text_document.uri,
11779 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11780 );
11781 futures::future::pending::<()>().await;
11782 unreachable!()
11783 },
11784 );
11785 let format = editor
11786 .update_in(cx, |editor, window, cx| {
11787 editor.perform_format(
11788 project,
11789 FormatTrigger::Manual,
11790 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
11791 window,
11792 cx,
11793 )
11794 })
11795 .unwrap();
11796 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11797 cx.executor().start_waiting();
11798 format.await;
11799 assert_eq!(
11800 editor.update(cx, |editor, cx| editor.text(cx)),
11801 "one\ntwo\nthree\n"
11802 );
11803}
11804
11805#[gpui::test]
11806async fn test_multiple_formatters(cx: &mut TestAppContext) {
11807 init_test(cx, |settings| {
11808 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
11809 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
11810 Formatter::LanguageServer { name: None },
11811 Formatter::CodeActions(
11812 [
11813 ("code-action-1".into(), true),
11814 ("code-action-2".into(), true),
11815 ]
11816 .into_iter()
11817 .collect(),
11818 ),
11819 ])))
11820 });
11821
11822 let fs = FakeFs::new(cx.executor());
11823 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
11824 .await;
11825
11826 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11827 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11828 language_registry.add(rust_lang());
11829
11830 let mut fake_servers = language_registry.register_fake_lsp(
11831 "Rust",
11832 FakeLspAdapter {
11833 capabilities: lsp::ServerCapabilities {
11834 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11835 execute_command_provider: Some(lsp::ExecuteCommandOptions {
11836 commands: vec!["the-command-for-code-action-1".into()],
11837 ..Default::default()
11838 }),
11839 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
11840 ..Default::default()
11841 },
11842 ..Default::default()
11843 },
11844 );
11845
11846 let buffer = project
11847 .update(cx, |project, cx| {
11848 project.open_local_buffer(path!("/file.rs"), cx)
11849 })
11850 .await
11851 .unwrap();
11852
11853 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11854 let (editor, cx) = cx.add_window_view(|window, cx| {
11855 build_editor_with_project(project.clone(), buffer, window, cx)
11856 });
11857
11858 cx.executor().start_waiting();
11859
11860 let fake_server = fake_servers.next().await.unwrap();
11861 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11862 move |_params, _| async move {
11863 Ok(Some(vec![lsp::TextEdit::new(
11864 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
11865 "applied-formatting\n".to_string(),
11866 )]))
11867 },
11868 );
11869 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
11870 move |params, _| async move {
11871 assert_eq!(
11872 params.context.only,
11873 Some(vec!["code-action-1".into(), "code-action-2".into()])
11874 );
11875 let uri = lsp::Uri::from_file_path(path!("/file.rs")).unwrap();
11876 Ok(Some(vec![
11877 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
11878 kind: Some("code-action-1".into()),
11879 edit: Some(lsp::WorkspaceEdit::new(
11880 [(
11881 uri.clone(),
11882 vec![lsp::TextEdit::new(
11883 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
11884 "applied-code-action-1-edit\n".to_string(),
11885 )],
11886 )]
11887 .into_iter()
11888 .collect(),
11889 )),
11890 command: Some(lsp::Command {
11891 command: "the-command-for-code-action-1".into(),
11892 ..Default::default()
11893 }),
11894 ..Default::default()
11895 }),
11896 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
11897 kind: Some("code-action-2".into()),
11898 edit: Some(lsp::WorkspaceEdit::new(
11899 [(
11900 uri,
11901 vec![lsp::TextEdit::new(
11902 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
11903 "applied-code-action-2-edit\n".to_string(),
11904 )],
11905 )]
11906 .into_iter()
11907 .collect(),
11908 )),
11909 ..Default::default()
11910 }),
11911 ]))
11912 },
11913 );
11914
11915 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
11916 move |params, _| async move { Ok(params) }
11917 });
11918
11919 let command_lock = Arc::new(futures::lock::Mutex::new(()));
11920 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
11921 let fake = fake_server.clone();
11922 let lock = command_lock.clone();
11923 move |params, _| {
11924 assert_eq!(params.command, "the-command-for-code-action-1");
11925 let fake = fake.clone();
11926 let lock = lock.clone();
11927 async move {
11928 lock.lock().await;
11929 fake.server
11930 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
11931 label: None,
11932 edit: lsp::WorkspaceEdit {
11933 changes: Some(
11934 [(
11935 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
11936 vec![lsp::TextEdit {
11937 range: lsp::Range::new(
11938 lsp::Position::new(0, 0),
11939 lsp::Position::new(0, 0),
11940 ),
11941 new_text: "applied-code-action-1-command\n".into(),
11942 }],
11943 )]
11944 .into_iter()
11945 .collect(),
11946 ),
11947 ..Default::default()
11948 },
11949 })
11950 .await
11951 .into_response()
11952 .unwrap();
11953 Ok(Some(json!(null)))
11954 }
11955 }
11956 });
11957
11958 cx.executor().start_waiting();
11959 editor
11960 .update_in(cx, |editor, window, cx| {
11961 editor.perform_format(
11962 project.clone(),
11963 FormatTrigger::Manual,
11964 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
11965 window,
11966 cx,
11967 )
11968 })
11969 .unwrap()
11970 .await;
11971 editor.update(cx, |editor, cx| {
11972 assert_eq!(
11973 editor.text(cx),
11974 r#"
11975 applied-code-action-2-edit
11976 applied-code-action-1-command
11977 applied-code-action-1-edit
11978 applied-formatting
11979 one
11980 two
11981 three
11982 "#
11983 .unindent()
11984 );
11985 });
11986
11987 editor.update_in(cx, |editor, window, cx| {
11988 editor.undo(&Default::default(), window, cx);
11989 assert_eq!(editor.text(cx), "one \ntwo \nthree");
11990 });
11991
11992 // Perform a manual edit while waiting for an LSP command
11993 // that's being run as part of a formatting code action.
11994 let lock_guard = command_lock.lock().await;
11995 let format = editor
11996 .update_in(cx, |editor, window, cx| {
11997 editor.perform_format(
11998 project.clone(),
11999 FormatTrigger::Manual,
12000 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12001 window,
12002 cx,
12003 )
12004 })
12005 .unwrap();
12006 cx.run_until_parked();
12007 editor.update(cx, |editor, cx| {
12008 assert_eq!(
12009 editor.text(cx),
12010 r#"
12011 applied-code-action-1-edit
12012 applied-formatting
12013 one
12014 two
12015 three
12016 "#
12017 .unindent()
12018 );
12019
12020 editor.buffer.update(cx, |buffer, cx| {
12021 let ix = buffer.len(cx);
12022 buffer.edit([(ix..ix, "edited\n")], None, cx);
12023 });
12024 });
12025
12026 // Allow the LSP command to proceed. Because the buffer was edited,
12027 // the second code action will not be run.
12028 drop(lock_guard);
12029 format.await;
12030 editor.update_in(cx, |editor, window, cx| {
12031 assert_eq!(
12032 editor.text(cx),
12033 r#"
12034 applied-code-action-1-command
12035 applied-code-action-1-edit
12036 applied-formatting
12037 one
12038 two
12039 three
12040 edited
12041 "#
12042 .unindent()
12043 );
12044
12045 // The manual edit is undone first, because it is the last thing the user did
12046 // (even though the command completed afterwards).
12047 editor.undo(&Default::default(), window, cx);
12048 assert_eq!(
12049 editor.text(cx),
12050 r#"
12051 applied-code-action-1-command
12052 applied-code-action-1-edit
12053 applied-formatting
12054 one
12055 two
12056 three
12057 "#
12058 .unindent()
12059 );
12060
12061 // All the formatting (including the command, which completed after the manual edit)
12062 // is undone together.
12063 editor.undo(&Default::default(), window, cx);
12064 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12065 });
12066}
12067
12068#[gpui::test]
12069async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
12070 init_test(cx, |settings| {
12071 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
12072 Formatter::LanguageServer { name: None },
12073 ])))
12074 });
12075
12076 let fs = FakeFs::new(cx.executor());
12077 fs.insert_file(path!("/file.ts"), Default::default()).await;
12078
12079 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12080
12081 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12082 language_registry.add(Arc::new(Language::new(
12083 LanguageConfig {
12084 name: "TypeScript".into(),
12085 matcher: LanguageMatcher {
12086 path_suffixes: vec!["ts".to_string()],
12087 ..Default::default()
12088 },
12089 ..LanguageConfig::default()
12090 },
12091 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
12092 )));
12093 update_test_language_settings(cx, |settings| {
12094 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
12095 });
12096 let mut fake_servers = language_registry.register_fake_lsp(
12097 "TypeScript",
12098 FakeLspAdapter {
12099 capabilities: lsp::ServerCapabilities {
12100 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
12101 ..Default::default()
12102 },
12103 ..Default::default()
12104 },
12105 );
12106
12107 let buffer = project
12108 .update(cx, |project, cx| {
12109 project.open_local_buffer(path!("/file.ts"), cx)
12110 })
12111 .await
12112 .unwrap();
12113
12114 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12115 let (editor, cx) = cx.add_window_view(|window, cx| {
12116 build_editor_with_project(project.clone(), buffer, window, cx)
12117 });
12118 editor.update_in(cx, |editor, window, cx| {
12119 editor.set_text(
12120 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12121 window,
12122 cx,
12123 )
12124 });
12125
12126 cx.executor().start_waiting();
12127 let fake_server = fake_servers.next().await.unwrap();
12128
12129 let format = editor
12130 .update_in(cx, |editor, window, cx| {
12131 editor.perform_code_action_kind(
12132 project.clone(),
12133 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12134 window,
12135 cx,
12136 )
12137 })
12138 .unwrap();
12139 fake_server
12140 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
12141 assert_eq!(
12142 params.text_document.uri,
12143 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12144 );
12145 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
12146 lsp::CodeAction {
12147 title: "Organize Imports".to_string(),
12148 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
12149 edit: Some(lsp::WorkspaceEdit {
12150 changes: Some(
12151 [(
12152 params.text_document.uri.clone(),
12153 vec![lsp::TextEdit::new(
12154 lsp::Range::new(
12155 lsp::Position::new(1, 0),
12156 lsp::Position::new(2, 0),
12157 ),
12158 "".to_string(),
12159 )],
12160 )]
12161 .into_iter()
12162 .collect(),
12163 ),
12164 ..Default::default()
12165 }),
12166 ..Default::default()
12167 },
12168 )]))
12169 })
12170 .next()
12171 .await;
12172 cx.executor().start_waiting();
12173 format.await;
12174 assert_eq!(
12175 editor.update(cx, |editor, cx| editor.text(cx)),
12176 "import { a } from 'module';\n\nconst x = a;\n"
12177 );
12178
12179 editor.update_in(cx, |editor, window, cx| {
12180 editor.set_text(
12181 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12182 window,
12183 cx,
12184 )
12185 });
12186 // Ensure we don't lock if code action hangs.
12187 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
12188 move |params, _| async move {
12189 assert_eq!(
12190 params.text_document.uri,
12191 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12192 );
12193 futures::future::pending::<()>().await;
12194 unreachable!()
12195 },
12196 );
12197 let format = editor
12198 .update_in(cx, |editor, window, cx| {
12199 editor.perform_code_action_kind(
12200 project,
12201 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12202 window,
12203 cx,
12204 )
12205 })
12206 .unwrap();
12207 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
12208 cx.executor().start_waiting();
12209 format.await;
12210 assert_eq!(
12211 editor.update(cx, |editor, cx| editor.text(cx)),
12212 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
12213 );
12214}
12215
12216#[gpui::test]
12217async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
12218 init_test(cx, |_| {});
12219
12220 let mut cx = EditorLspTestContext::new_rust(
12221 lsp::ServerCapabilities {
12222 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12223 ..Default::default()
12224 },
12225 cx,
12226 )
12227 .await;
12228
12229 cx.set_state(indoc! {"
12230 one.twoˇ
12231 "});
12232
12233 // The format request takes a long time. When it completes, it inserts
12234 // a newline and an indent before the `.`
12235 cx.lsp
12236 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
12237 let executor = cx.background_executor().clone();
12238 async move {
12239 executor.timer(Duration::from_millis(100)).await;
12240 Ok(Some(vec![lsp::TextEdit {
12241 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
12242 new_text: "\n ".into(),
12243 }]))
12244 }
12245 });
12246
12247 // Submit a format request.
12248 let format_1 = cx
12249 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12250 .unwrap();
12251 cx.executor().run_until_parked();
12252
12253 // Submit a second format request.
12254 let format_2 = cx
12255 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12256 .unwrap();
12257 cx.executor().run_until_parked();
12258
12259 // Wait for both format requests to complete
12260 cx.executor().advance_clock(Duration::from_millis(200));
12261 cx.executor().start_waiting();
12262 format_1.await.unwrap();
12263 cx.executor().start_waiting();
12264 format_2.await.unwrap();
12265
12266 // The formatting edits only happens once.
12267 cx.assert_editor_state(indoc! {"
12268 one
12269 .twoˇ
12270 "});
12271}
12272
12273#[gpui::test]
12274async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
12275 init_test(cx, |settings| {
12276 settings.defaults.formatter = Some(SelectedFormatter::Auto)
12277 });
12278
12279 let mut cx = EditorLspTestContext::new_rust(
12280 lsp::ServerCapabilities {
12281 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12282 ..Default::default()
12283 },
12284 cx,
12285 )
12286 .await;
12287
12288 // Set up a buffer white some trailing whitespace and no trailing newline.
12289 cx.set_state(
12290 &[
12291 "one ", //
12292 "twoˇ", //
12293 "three ", //
12294 "four", //
12295 ]
12296 .join("\n"),
12297 );
12298
12299 // Submit a format request.
12300 let format = cx
12301 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12302 .unwrap();
12303
12304 // Record which buffer changes have been sent to the language server
12305 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
12306 cx.lsp
12307 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
12308 let buffer_changes = buffer_changes.clone();
12309 move |params, _| {
12310 buffer_changes.lock().extend(
12311 params
12312 .content_changes
12313 .into_iter()
12314 .map(|e| (e.range.unwrap(), e.text)),
12315 );
12316 }
12317 });
12318
12319 // Handle formatting requests to the language server.
12320 cx.lsp
12321 .set_request_handler::<lsp::request::Formatting, _, _>({
12322 let buffer_changes = buffer_changes.clone();
12323 move |_, _| {
12324 // When formatting is requested, trailing whitespace has already been stripped,
12325 // and the trailing newline has already been added.
12326 assert_eq!(
12327 &buffer_changes.lock()[1..],
12328 &[
12329 (
12330 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
12331 "".into()
12332 ),
12333 (
12334 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
12335 "".into()
12336 ),
12337 (
12338 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
12339 "\n".into()
12340 ),
12341 ]
12342 );
12343
12344 // Insert blank lines between each line of the buffer.
12345 async move {
12346 Ok(Some(vec![
12347 lsp::TextEdit {
12348 range: lsp::Range::new(
12349 lsp::Position::new(1, 0),
12350 lsp::Position::new(1, 0),
12351 ),
12352 new_text: "\n".into(),
12353 },
12354 lsp::TextEdit {
12355 range: lsp::Range::new(
12356 lsp::Position::new(2, 0),
12357 lsp::Position::new(2, 0),
12358 ),
12359 new_text: "\n".into(),
12360 },
12361 ]))
12362 }
12363 }
12364 });
12365
12366 // After formatting the buffer, the trailing whitespace is stripped,
12367 // a newline is appended, and the edits provided by the language server
12368 // have been applied.
12369 format.await.unwrap();
12370 cx.assert_editor_state(
12371 &[
12372 "one", //
12373 "", //
12374 "twoˇ", //
12375 "", //
12376 "three", //
12377 "four", //
12378 "", //
12379 ]
12380 .join("\n"),
12381 );
12382
12383 // Undoing the formatting undoes the trailing whitespace removal, the
12384 // trailing newline, and the LSP edits.
12385 cx.update_buffer(|buffer, cx| buffer.undo(cx));
12386 cx.assert_editor_state(
12387 &[
12388 "one ", //
12389 "twoˇ", //
12390 "three ", //
12391 "four", //
12392 ]
12393 .join("\n"),
12394 );
12395}
12396
12397#[gpui::test]
12398async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
12399 cx: &mut TestAppContext,
12400) {
12401 init_test(cx, |_| {});
12402
12403 cx.update(|cx| {
12404 cx.update_global::<SettingsStore, _>(|settings, cx| {
12405 settings.update_user_settings(cx, |settings| {
12406 settings.editor.auto_signature_help = Some(true);
12407 });
12408 });
12409 });
12410
12411 let mut cx = EditorLspTestContext::new_rust(
12412 lsp::ServerCapabilities {
12413 signature_help_provider: Some(lsp::SignatureHelpOptions {
12414 ..Default::default()
12415 }),
12416 ..Default::default()
12417 },
12418 cx,
12419 )
12420 .await;
12421
12422 let language = Language::new(
12423 LanguageConfig {
12424 name: "Rust".into(),
12425 brackets: BracketPairConfig {
12426 pairs: vec![
12427 BracketPair {
12428 start: "{".to_string(),
12429 end: "}".to_string(),
12430 close: true,
12431 surround: true,
12432 newline: true,
12433 },
12434 BracketPair {
12435 start: "(".to_string(),
12436 end: ")".to_string(),
12437 close: true,
12438 surround: true,
12439 newline: true,
12440 },
12441 BracketPair {
12442 start: "/*".to_string(),
12443 end: " */".to_string(),
12444 close: true,
12445 surround: true,
12446 newline: true,
12447 },
12448 BracketPair {
12449 start: "[".to_string(),
12450 end: "]".to_string(),
12451 close: false,
12452 surround: false,
12453 newline: true,
12454 },
12455 BracketPair {
12456 start: "\"".to_string(),
12457 end: "\"".to_string(),
12458 close: true,
12459 surround: true,
12460 newline: false,
12461 },
12462 BracketPair {
12463 start: "<".to_string(),
12464 end: ">".to_string(),
12465 close: false,
12466 surround: true,
12467 newline: true,
12468 },
12469 ],
12470 ..Default::default()
12471 },
12472 autoclose_before: "})]".to_string(),
12473 ..Default::default()
12474 },
12475 Some(tree_sitter_rust::LANGUAGE.into()),
12476 );
12477 let language = Arc::new(language);
12478
12479 cx.language_registry().add(language.clone());
12480 cx.update_buffer(|buffer, cx| {
12481 buffer.set_language(Some(language), cx);
12482 });
12483
12484 cx.set_state(
12485 &r#"
12486 fn main() {
12487 sampleˇ
12488 }
12489 "#
12490 .unindent(),
12491 );
12492
12493 cx.update_editor(|editor, window, cx| {
12494 editor.handle_input("(", window, cx);
12495 });
12496 cx.assert_editor_state(
12497 &"
12498 fn main() {
12499 sample(ˇ)
12500 }
12501 "
12502 .unindent(),
12503 );
12504
12505 let mocked_response = lsp::SignatureHelp {
12506 signatures: vec![lsp::SignatureInformation {
12507 label: "fn sample(param1: u8, param2: u8)".to_string(),
12508 documentation: None,
12509 parameters: Some(vec![
12510 lsp::ParameterInformation {
12511 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12512 documentation: None,
12513 },
12514 lsp::ParameterInformation {
12515 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12516 documentation: None,
12517 },
12518 ]),
12519 active_parameter: None,
12520 }],
12521 active_signature: Some(0),
12522 active_parameter: Some(0),
12523 };
12524 handle_signature_help_request(&mut cx, mocked_response).await;
12525
12526 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12527 .await;
12528
12529 cx.editor(|editor, _, _| {
12530 let signature_help_state = editor.signature_help_state.popover().cloned();
12531 let signature = signature_help_state.unwrap();
12532 assert_eq!(
12533 signature.signatures[signature.current_signature].label,
12534 "fn sample(param1: u8, param2: u8)"
12535 );
12536 });
12537}
12538
12539#[gpui::test]
12540async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
12541 init_test(cx, |_| {});
12542
12543 cx.update(|cx| {
12544 cx.update_global::<SettingsStore, _>(|settings, cx| {
12545 settings.update_user_settings(cx, |settings| {
12546 settings.editor.auto_signature_help = Some(false);
12547 settings.editor.show_signature_help_after_edits = Some(false);
12548 });
12549 });
12550 });
12551
12552 let mut cx = EditorLspTestContext::new_rust(
12553 lsp::ServerCapabilities {
12554 signature_help_provider: Some(lsp::SignatureHelpOptions {
12555 ..Default::default()
12556 }),
12557 ..Default::default()
12558 },
12559 cx,
12560 )
12561 .await;
12562
12563 let language = Language::new(
12564 LanguageConfig {
12565 name: "Rust".into(),
12566 brackets: BracketPairConfig {
12567 pairs: vec![
12568 BracketPair {
12569 start: "{".to_string(),
12570 end: "}".to_string(),
12571 close: true,
12572 surround: true,
12573 newline: true,
12574 },
12575 BracketPair {
12576 start: "(".to_string(),
12577 end: ")".to_string(),
12578 close: true,
12579 surround: true,
12580 newline: true,
12581 },
12582 BracketPair {
12583 start: "/*".to_string(),
12584 end: " */".to_string(),
12585 close: true,
12586 surround: true,
12587 newline: true,
12588 },
12589 BracketPair {
12590 start: "[".to_string(),
12591 end: "]".to_string(),
12592 close: false,
12593 surround: false,
12594 newline: true,
12595 },
12596 BracketPair {
12597 start: "\"".to_string(),
12598 end: "\"".to_string(),
12599 close: true,
12600 surround: true,
12601 newline: false,
12602 },
12603 BracketPair {
12604 start: "<".to_string(),
12605 end: ">".to_string(),
12606 close: false,
12607 surround: true,
12608 newline: true,
12609 },
12610 ],
12611 ..Default::default()
12612 },
12613 autoclose_before: "})]".to_string(),
12614 ..Default::default()
12615 },
12616 Some(tree_sitter_rust::LANGUAGE.into()),
12617 );
12618 let language = Arc::new(language);
12619
12620 cx.language_registry().add(language.clone());
12621 cx.update_buffer(|buffer, cx| {
12622 buffer.set_language(Some(language), cx);
12623 });
12624
12625 // Ensure that signature_help is not called when no signature help is enabled.
12626 cx.set_state(
12627 &r#"
12628 fn main() {
12629 sampleˇ
12630 }
12631 "#
12632 .unindent(),
12633 );
12634 cx.update_editor(|editor, window, cx| {
12635 editor.handle_input("(", window, cx);
12636 });
12637 cx.assert_editor_state(
12638 &"
12639 fn main() {
12640 sample(ˇ)
12641 }
12642 "
12643 .unindent(),
12644 );
12645 cx.editor(|editor, _, _| {
12646 assert!(editor.signature_help_state.task().is_none());
12647 });
12648
12649 let mocked_response = lsp::SignatureHelp {
12650 signatures: vec![lsp::SignatureInformation {
12651 label: "fn sample(param1: u8, param2: u8)".to_string(),
12652 documentation: None,
12653 parameters: Some(vec![
12654 lsp::ParameterInformation {
12655 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12656 documentation: None,
12657 },
12658 lsp::ParameterInformation {
12659 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12660 documentation: None,
12661 },
12662 ]),
12663 active_parameter: None,
12664 }],
12665 active_signature: Some(0),
12666 active_parameter: Some(0),
12667 };
12668
12669 // Ensure that signature_help is called when enabled afte edits
12670 cx.update(|_, cx| {
12671 cx.update_global::<SettingsStore, _>(|settings, cx| {
12672 settings.update_user_settings(cx, |settings| {
12673 settings.editor.auto_signature_help = Some(false);
12674 settings.editor.show_signature_help_after_edits = Some(true);
12675 });
12676 });
12677 });
12678 cx.set_state(
12679 &r#"
12680 fn main() {
12681 sampleˇ
12682 }
12683 "#
12684 .unindent(),
12685 );
12686 cx.update_editor(|editor, window, cx| {
12687 editor.handle_input("(", window, cx);
12688 });
12689 cx.assert_editor_state(
12690 &"
12691 fn main() {
12692 sample(ˇ)
12693 }
12694 "
12695 .unindent(),
12696 );
12697 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12698 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12699 .await;
12700 cx.update_editor(|editor, _, _| {
12701 let signature_help_state = editor.signature_help_state.popover().cloned();
12702 assert!(signature_help_state.is_some());
12703 let signature = signature_help_state.unwrap();
12704 assert_eq!(
12705 signature.signatures[signature.current_signature].label,
12706 "fn sample(param1: u8, param2: u8)"
12707 );
12708 editor.signature_help_state = SignatureHelpState::default();
12709 });
12710
12711 // Ensure that signature_help is called when auto signature help override is enabled
12712 cx.update(|_, cx| {
12713 cx.update_global::<SettingsStore, _>(|settings, cx| {
12714 settings.update_user_settings(cx, |settings| {
12715 settings.editor.auto_signature_help = Some(true);
12716 settings.editor.show_signature_help_after_edits = Some(false);
12717 });
12718 });
12719 });
12720 cx.set_state(
12721 &r#"
12722 fn main() {
12723 sampleˇ
12724 }
12725 "#
12726 .unindent(),
12727 );
12728 cx.update_editor(|editor, window, cx| {
12729 editor.handle_input("(", window, cx);
12730 });
12731 cx.assert_editor_state(
12732 &"
12733 fn main() {
12734 sample(ˇ)
12735 }
12736 "
12737 .unindent(),
12738 );
12739 handle_signature_help_request(&mut cx, mocked_response).await;
12740 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12741 .await;
12742 cx.editor(|editor, _, _| {
12743 let signature_help_state = editor.signature_help_state.popover().cloned();
12744 assert!(signature_help_state.is_some());
12745 let signature = signature_help_state.unwrap();
12746 assert_eq!(
12747 signature.signatures[signature.current_signature].label,
12748 "fn sample(param1: u8, param2: u8)"
12749 );
12750 });
12751}
12752
12753#[gpui::test]
12754async fn test_signature_help(cx: &mut TestAppContext) {
12755 init_test(cx, |_| {});
12756 cx.update(|cx| {
12757 cx.update_global::<SettingsStore, _>(|settings, cx| {
12758 settings.update_user_settings(cx, |settings| {
12759 settings.editor.auto_signature_help = Some(true);
12760 });
12761 });
12762 });
12763
12764 let mut cx = EditorLspTestContext::new_rust(
12765 lsp::ServerCapabilities {
12766 signature_help_provider: Some(lsp::SignatureHelpOptions {
12767 ..Default::default()
12768 }),
12769 ..Default::default()
12770 },
12771 cx,
12772 )
12773 .await;
12774
12775 // A test that directly calls `show_signature_help`
12776 cx.update_editor(|editor, window, cx| {
12777 editor.show_signature_help(&ShowSignatureHelp, window, cx);
12778 });
12779
12780 let mocked_response = lsp::SignatureHelp {
12781 signatures: vec![lsp::SignatureInformation {
12782 label: "fn sample(param1: u8, param2: u8)".to_string(),
12783 documentation: None,
12784 parameters: Some(vec![
12785 lsp::ParameterInformation {
12786 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12787 documentation: None,
12788 },
12789 lsp::ParameterInformation {
12790 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12791 documentation: None,
12792 },
12793 ]),
12794 active_parameter: None,
12795 }],
12796 active_signature: Some(0),
12797 active_parameter: Some(0),
12798 };
12799 handle_signature_help_request(&mut cx, mocked_response).await;
12800
12801 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12802 .await;
12803
12804 cx.editor(|editor, _, _| {
12805 let signature_help_state = editor.signature_help_state.popover().cloned();
12806 assert!(signature_help_state.is_some());
12807 let signature = signature_help_state.unwrap();
12808 assert_eq!(
12809 signature.signatures[signature.current_signature].label,
12810 "fn sample(param1: u8, param2: u8)"
12811 );
12812 });
12813
12814 // When exiting outside from inside the brackets, `signature_help` is closed.
12815 cx.set_state(indoc! {"
12816 fn main() {
12817 sample(ˇ);
12818 }
12819
12820 fn sample(param1: u8, param2: u8) {}
12821 "});
12822
12823 cx.update_editor(|editor, window, cx| {
12824 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12825 s.select_ranges([0..0])
12826 });
12827 });
12828
12829 let mocked_response = lsp::SignatureHelp {
12830 signatures: Vec::new(),
12831 active_signature: None,
12832 active_parameter: None,
12833 };
12834 handle_signature_help_request(&mut cx, mocked_response).await;
12835
12836 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
12837 .await;
12838
12839 cx.editor(|editor, _, _| {
12840 assert!(!editor.signature_help_state.is_shown());
12841 });
12842
12843 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
12844 cx.set_state(indoc! {"
12845 fn main() {
12846 sample(ˇ);
12847 }
12848
12849 fn sample(param1: u8, param2: u8) {}
12850 "});
12851
12852 let mocked_response = lsp::SignatureHelp {
12853 signatures: vec![lsp::SignatureInformation {
12854 label: "fn sample(param1: u8, param2: u8)".to_string(),
12855 documentation: None,
12856 parameters: Some(vec![
12857 lsp::ParameterInformation {
12858 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12859 documentation: None,
12860 },
12861 lsp::ParameterInformation {
12862 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12863 documentation: None,
12864 },
12865 ]),
12866 active_parameter: None,
12867 }],
12868 active_signature: Some(0),
12869 active_parameter: Some(0),
12870 };
12871 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12872 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12873 .await;
12874 cx.editor(|editor, _, _| {
12875 assert!(editor.signature_help_state.is_shown());
12876 });
12877
12878 // Restore the popover with more parameter input
12879 cx.set_state(indoc! {"
12880 fn main() {
12881 sample(param1, param2ˇ);
12882 }
12883
12884 fn sample(param1: u8, param2: u8) {}
12885 "});
12886
12887 let mocked_response = lsp::SignatureHelp {
12888 signatures: vec![lsp::SignatureInformation {
12889 label: "fn sample(param1: u8, param2: u8)".to_string(),
12890 documentation: None,
12891 parameters: Some(vec![
12892 lsp::ParameterInformation {
12893 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12894 documentation: None,
12895 },
12896 lsp::ParameterInformation {
12897 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12898 documentation: None,
12899 },
12900 ]),
12901 active_parameter: None,
12902 }],
12903 active_signature: Some(0),
12904 active_parameter: Some(1),
12905 };
12906 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12907 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12908 .await;
12909
12910 // When selecting a range, the popover is gone.
12911 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
12912 cx.update_editor(|editor, window, cx| {
12913 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12914 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
12915 })
12916 });
12917 cx.assert_editor_state(indoc! {"
12918 fn main() {
12919 sample(param1, «ˇparam2»);
12920 }
12921
12922 fn sample(param1: u8, param2: u8) {}
12923 "});
12924 cx.editor(|editor, _, _| {
12925 assert!(!editor.signature_help_state.is_shown());
12926 });
12927
12928 // When unselecting again, the popover is back if within the brackets.
12929 cx.update_editor(|editor, window, cx| {
12930 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12931 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
12932 })
12933 });
12934 cx.assert_editor_state(indoc! {"
12935 fn main() {
12936 sample(param1, ˇparam2);
12937 }
12938
12939 fn sample(param1: u8, param2: u8) {}
12940 "});
12941 handle_signature_help_request(&mut cx, mocked_response).await;
12942 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12943 .await;
12944 cx.editor(|editor, _, _| {
12945 assert!(editor.signature_help_state.is_shown());
12946 });
12947
12948 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
12949 cx.update_editor(|editor, window, cx| {
12950 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12951 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
12952 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
12953 })
12954 });
12955 cx.assert_editor_state(indoc! {"
12956 fn main() {
12957 sample(param1, ˇparam2);
12958 }
12959
12960 fn sample(param1: u8, param2: u8) {}
12961 "});
12962
12963 let mocked_response = lsp::SignatureHelp {
12964 signatures: vec![lsp::SignatureInformation {
12965 label: "fn sample(param1: u8, param2: u8)".to_string(),
12966 documentation: None,
12967 parameters: Some(vec![
12968 lsp::ParameterInformation {
12969 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12970 documentation: None,
12971 },
12972 lsp::ParameterInformation {
12973 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12974 documentation: None,
12975 },
12976 ]),
12977 active_parameter: None,
12978 }],
12979 active_signature: Some(0),
12980 active_parameter: Some(1),
12981 };
12982 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12983 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12984 .await;
12985 cx.update_editor(|editor, _, cx| {
12986 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
12987 });
12988 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
12989 .await;
12990 cx.update_editor(|editor, window, cx| {
12991 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12992 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
12993 })
12994 });
12995 cx.assert_editor_state(indoc! {"
12996 fn main() {
12997 sample(param1, «ˇparam2»);
12998 }
12999
13000 fn sample(param1: u8, param2: u8) {}
13001 "});
13002 cx.update_editor(|editor, window, cx| {
13003 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13004 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13005 })
13006 });
13007 cx.assert_editor_state(indoc! {"
13008 fn main() {
13009 sample(param1, ˇparam2);
13010 }
13011
13012 fn sample(param1: u8, param2: u8) {}
13013 "});
13014 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
13015 .await;
13016}
13017
13018#[gpui::test]
13019async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
13020 init_test(cx, |_| {});
13021
13022 let mut cx = EditorLspTestContext::new_rust(
13023 lsp::ServerCapabilities {
13024 signature_help_provider: Some(lsp::SignatureHelpOptions {
13025 ..Default::default()
13026 }),
13027 ..Default::default()
13028 },
13029 cx,
13030 )
13031 .await;
13032
13033 cx.set_state(indoc! {"
13034 fn main() {
13035 overloadedˇ
13036 }
13037 "});
13038
13039 cx.update_editor(|editor, window, cx| {
13040 editor.handle_input("(", window, cx);
13041 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13042 });
13043
13044 // Mock response with 3 signatures
13045 let mocked_response = lsp::SignatureHelp {
13046 signatures: vec![
13047 lsp::SignatureInformation {
13048 label: "fn overloaded(x: i32)".to_string(),
13049 documentation: None,
13050 parameters: Some(vec![lsp::ParameterInformation {
13051 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13052 documentation: None,
13053 }]),
13054 active_parameter: None,
13055 },
13056 lsp::SignatureInformation {
13057 label: "fn overloaded(x: i32, y: i32)".to_string(),
13058 documentation: None,
13059 parameters: Some(vec![
13060 lsp::ParameterInformation {
13061 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13062 documentation: None,
13063 },
13064 lsp::ParameterInformation {
13065 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13066 documentation: None,
13067 },
13068 ]),
13069 active_parameter: None,
13070 },
13071 lsp::SignatureInformation {
13072 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
13073 documentation: None,
13074 parameters: Some(vec![
13075 lsp::ParameterInformation {
13076 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13077 documentation: None,
13078 },
13079 lsp::ParameterInformation {
13080 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13081 documentation: None,
13082 },
13083 lsp::ParameterInformation {
13084 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
13085 documentation: None,
13086 },
13087 ]),
13088 active_parameter: None,
13089 },
13090 ],
13091 active_signature: Some(1),
13092 active_parameter: Some(0),
13093 };
13094 handle_signature_help_request(&mut cx, mocked_response).await;
13095
13096 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13097 .await;
13098
13099 // Verify we have multiple signatures and the right one is selected
13100 cx.editor(|editor, _, _| {
13101 let popover = editor.signature_help_state.popover().cloned().unwrap();
13102 assert_eq!(popover.signatures.len(), 3);
13103 // active_signature was 1, so that should be the current
13104 assert_eq!(popover.current_signature, 1);
13105 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
13106 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
13107 assert_eq!(
13108 popover.signatures[2].label,
13109 "fn overloaded(x: i32, y: i32, z: i32)"
13110 );
13111 });
13112
13113 // Test navigation functionality
13114 cx.update_editor(|editor, window, cx| {
13115 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13116 });
13117
13118 cx.editor(|editor, _, _| {
13119 let popover = editor.signature_help_state.popover().cloned().unwrap();
13120 assert_eq!(popover.current_signature, 2);
13121 });
13122
13123 // Test wrap around
13124 cx.update_editor(|editor, window, cx| {
13125 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13126 });
13127
13128 cx.editor(|editor, _, _| {
13129 let popover = editor.signature_help_state.popover().cloned().unwrap();
13130 assert_eq!(popover.current_signature, 0);
13131 });
13132
13133 // Test previous navigation
13134 cx.update_editor(|editor, window, cx| {
13135 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
13136 });
13137
13138 cx.editor(|editor, _, _| {
13139 let popover = editor.signature_help_state.popover().cloned().unwrap();
13140 assert_eq!(popover.current_signature, 2);
13141 });
13142}
13143
13144#[gpui::test]
13145async fn test_completion_mode(cx: &mut TestAppContext) {
13146 init_test(cx, |_| {});
13147 let mut cx = EditorLspTestContext::new_rust(
13148 lsp::ServerCapabilities {
13149 completion_provider: Some(lsp::CompletionOptions {
13150 resolve_provider: Some(true),
13151 ..Default::default()
13152 }),
13153 ..Default::default()
13154 },
13155 cx,
13156 )
13157 .await;
13158
13159 struct Run {
13160 run_description: &'static str,
13161 initial_state: String,
13162 buffer_marked_text: String,
13163 completion_label: &'static str,
13164 completion_text: &'static str,
13165 expected_with_insert_mode: String,
13166 expected_with_replace_mode: String,
13167 expected_with_replace_subsequence_mode: String,
13168 expected_with_replace_suffix_mode: String,
13169 }
13170
13171 let runs = [
13172 Run {
13173 run_description: "Start of word matches completion text",
13174 initial_state: "before ediˇ after".into(),
13175 buffer_marked_text: "before <edi|> after".into(),
13176 completion_label: "editor",
13177 completion_text: "editor",
13178 expected_with_insert_mode: "before editorˇ after".into(),
13179 expected_with_replace_mode: "before editorˇ after".into(),
13180 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13181 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13182 },
13183 Run {
13184 run_description: "Accept same text at the middle of the word",
13185 initial_state: "before ediˇtor after".into(),
13186 buffer_marked_text: "before <edi|tor> after".into(),
13187 completion_label: "editor",
13188 completion_text: "editor",
13189 expected_with_insert_mode: "before editorˇtor after".into(),
13190 expected_with_replace_mode: "before editorˇ after".into(),
13191 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13192 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13193 },
13194 Run {
13195 run_description: "End of word matches completion text -- cursor at end",
13196 initial_state: "before torˇ after".into(),
13197 buffer_marked_text: "before <tor|> after".into(),
13198 completion_label: "editor",
13199 completion_text: "editor",
13200 expected_with_insert_mode: "before editorˇ after".into(),
13201 expected_with_replace_mode: "before editorˇ after".into(),
13202 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13203 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13204 },
13205 Run {
13206 run_description: "End of word matches completion text -- cursor at start",
13207 initial_state: "before ˇtor after".into(),
13208 buffer_marked_text: "before <|tor> after".into(),
13209 completion_label: "editor",
13210 completion_text: "editor",
13211 expected_with_insert_mode: "before editorˇtor after".into(),
13212 expected_with_replace_mode: "before editorˇ after".into(),
13213 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13214 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13215 },
13216 Run {
13217 run_description: "Prepend text containing whitespace",
13218 initial_state: "pˇfield: bool".into(),
13219 buffer_marked_text: "<p|field>: bool".into(),
13220 completion_label: "pub ",
13221 completion_text: "pub ",
13222 expected_with_insert_mode: "pub ˇfield: bool".into(),
13223 expected_with_replace_mode: "pub ˇ: bool".into(),
13224 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
13225 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
13226 },
13227 Run {
13228 run_description: "Add element to start of list",
13229 initial_state: "[element_ˇelement_2]".into(),
13230 buffer_marked_text: "[<element_|element_2>]".into(),
13231 completion_label: "element_1",
13232 completion_text: "element_1",
13233 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
13234 expected_with_replace_mode: "[element_1ˇ]".into(),
13235 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
13236 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
13237 },
13238 Run {
13239 run_description: "Add element to start of list -- first and second elements are equal",
13240 initial_state: "[elˇelement]".into(),
13241 buffer_marked_text: "[<el|element>]".into(),
13242 completion_label: "element",
13243 completion_text: "element",
13244 expected_with_insert_mode: "[elementˇelement]".into(),
13245 expected_with_replace_mode: "[elementˇ]".into(),
13246 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
13247 expected_with_replace_suffix_mode: "[elementˇ]".into(),
13248 },
13249 Run {
13250 run_description: "Ends with matching suffix",
13251 initial_state: "SubˇError".into(),
13252 buffer_marked_text: "<Sub|Error>".into(),
13253 completion_label: "SubscriptionError",
13254 completion_text: "SubscriptionError",
13255 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
13256 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13257 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13258 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
13259 },
13260 Run {
13261 run_description: "Suffix is a subsequence -- contiguous",
13262 initial_state: "SubˇErr".into(),
13263 buffer_marked_text: "<Sub|Err>".into(),
13264 completion_label: "SubscriptionError",
13265 completion_text: "SubscriptionError",
13266 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
13267 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13268 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13269 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
13270 },
13271 Run {
13272 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
13273 initial_state: "Suˇscrirr".into(),
13274 buffer_marked_text: "<Su|scrirr>".into(),
13275 completion_label: "SubscriptionError",
13276 completion_text: "SubscriptionError",
13277 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
13278 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13279 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13280 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
13281 },
13282 Run {
13283 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
13284 initial_state: "foo(indˇix)".into(),
13285 buffer_marked_text: "foo(<ind|ix>)".into(),
13286 completion_label: "node_index",
13287 completion_text: "node_index",
13288 expected_with_insert_mode: "foo(node_indexˇix)".into(),
13289 expected_with_replace_mode: "foo(node_indexˇ)".into(),
13290 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
13291 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
13292 },
13293 Run {
13294 run_description: "Replace range ends before cursor - should extend to cursor",
13295 initial_state: "before editˇo after".into(),
13296 buffer_marked_text: "before <{ed}>it|o after".into(),
13297 completion_label: "editor",
13298 completion_text: "editor",
13299 expected_with_insert_mode: "before editorˇo after".into(),
13300 expected_with_replace_mode: "before editorˇo after".into(),
13301 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
13302 expected_with_replace_suffix_mode: "before editorˇo after".into(),
13303 },
13304 Run {
13305 run_description: "Uses label for suffix matching",
13306 initial_state: "before ediˇtor after".into(),
13307 buffer_marked_text: "before <edi|tor> after".into(),
13308 completion_label: "editor",
13309 completion_text: "editor()",
13310 expected_with_insert_mode: "before editor()ˇtor after".into(),
13311 expected_with_replace_mode: "before editor()ˇ after".into(),
13312 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
13313 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
13314 },
13315 Run {
13316 run_description: "Case insensitive subsequence and suffix matching",
13317 initial_state: "before EDiˇtoR after".into(),
13318 buffer_marked_text: "before <EDi|toR> after".into(),
13319 completion_label: "editor",
13320 completion_text: "editor",
13321 expected_with_insert_mode: "before editorˇtoR after".into(),
13322 expected_with_replace_mode: "before editorˇ after".into(),
13323 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13324 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13325 },
13326 ];
13327
13328 for run in runs {
13329 let run_variations = [
13330 (LspInsertMode::Insert, run.expected_with_insert_mode),
13331 (LspInsertMode::Replace, run.expected_with_replace_mode),
13332 (
13333 LspInsertMode::ReplaceSubsequence,
13334 run.expected_with_replace_subsequence_mode,
13335 ),
13336 (
13337 LspInsertMode::ReplaceSuffix,
13338 run.expected_with_replace_suffix_mode,
13339 ),
13340 ];
13341
13342 for (lsp_insert_mode, expected_text) in run_variations {
13343 eprintln!(
13344 "run = {:?}, mode = {lsp_insert_mode:.?}",
13345 run.run_description,
13346 );
13347
13348 update_test_language_settings(&mut cx, |settings| {
13349 settings.defaults.completions = Some(CompletionSettingsContent {
13350 lsp_insert_mode: Some(lsp_insert_mode),
13351 words: Some(WordsCompletionMode::Disabled),
13352 words_min_length: Some(0),
13353 ..Default::default()
13354 });
13355 });
13356
13357 cx.set_state(&run.initial_state);
13358 cx.update_editor(|editor, window, cx| {
13359 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13360 });
13361
13362 let counter = Arc::new(AtomicUsize::new(0));
13363 handle_completion_request_with_insert_and_replace(
13364 &mut cx,
13365 &run.buffer_marked_text,
13366 vec![(run.completion_label, run.completion_text)],
13367 counter.clone(),
13368 )
13369 .await;
13370 cx.condition(|editor, _| editor.context_menu_visible())
13371 .await;
13372 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13373
13374 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13375 editor
13376 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13377 .unwrap()
13378 });
13379 cx.assert_editor_state(&expected_text);
13380 handle_resolve_completion_request(&mut cx, None).await;
13381 apply_additional_edits.await.unwrap();
13382 }
13383 }
13384}
13385
13386#[gpui::test]
13387async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
13388 init_test(cx, |_| {});
13389 let mut cx = EditorLspTestContext::new_rust(
13390 lsp::ServerCapabilities {
13391 completion_provider: Some(lsp::CompletionOptions {
13392 resolve_provider: Some(true),
13393 ..Default::default()
13394 }),
13395 ..Default::default()
13396 },
13397 cx,
13398 )
13399 .await;
13400
13401 let initial_state = "SubˇError";
13402 let buffer_marked_text = "<Sub|Error>";
13403 let completion_text = "SubscriptionError";
13404 let expected_with_insert_mode = "SubscriptionErrorˇError";
13405 let expected_with_replace_mode = "SubscriptionErrorˇ";
13406
13407 update_test_language_settings(&mut cx, |settings| {
13408 settings.defaults.completions = Some(CompletionSettingsContent {
13409 words: Some(WordsCompletionMode::Disabled),
13410 words_min_length: Some(0),
13411 // set the opposite here to ensure that the action is overriding the default behavior
13412 lsp_insert_mode: Some(LspInsertMode::Insert),
13413 ..Default::default()
13414 });
13415 });
13416
13417 cx.set_state(initial_state);
13418 cx.update_editor(|editor, window, cx| {
13419 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13420 });
13421
13422 let counter = Arc::new(AtomicUsize::new(0));
13423 handle_completion_request_with_insert_and_replace(
13424 &mut cx,
13425 buffer_marked_text,
13426 vec![(completion_text, completion_text)],
13427 counter.clone(),
13428 )
13429 .await;
13430 cx.condition(|editor, _| editor.context_menu_visible())
13431 .await;
13432 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13433
13434 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13435 editor
13436 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13437 .unwrap()
13438 });
13439 cx.assert_editor_state(expected_with_replace_mode);
13440 handle_resolve_completion_request(&mut cx, None).await;
13441 apply_additional_edits.await.unwrap();
13442
13443 update_test_language_settings(&mut cx, |settings| {
13444 settings.defaults.completions = Some(CompletionSettingsContent {
13445 words: Some(WordsCompletionMode::Disabled),
13446 words_min_length: Some(0),
13447 // set the opposite here to ensure that the action is overriding the default behavior
13448 lsp_insert_mode: Some(LspInsertMode::Replace),
13449 ..Default::default()
13450 });
13451 });
13452
13453 cx.set_state(initial_state);
13454 cx.update_editor(|editor, window, cx| {
13455 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13456 });
13457 handle_completion_request_with_insert_and_replace(
13458 &mut cx,
13459 buffer_marked_text,
13460 vec![(completion_text, completion_text)],
13461 counter.clone(),
13462 )
13463 .await;
13464 cx.condition(|editor, _| editor.context_menu_visible())
13465 .await;
13466 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
13467
13468 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13469 editor
13470 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
13471 .unwrap()
13472 });
13473 cx.assert_editor_state(expected_with_insert_mode);
13474 handle_resolve_completion_request(&mut cx, None).await;
13475 apply_additional_edits.await.unwrap();
13476}
13477
13478#[gpui::test]
13479async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
13480 init_test(cx, |_| {});
13481 let mut cx = EditorLspTestContext::new_rust(
13482 lsp::ServerCapabilities {
13483 completion_provider: Some(lsp::CompletionOptions {
13484 resolve_provider: Some(true),
13485 ..Default::default()
13486 }),
13487 ..Default::default()
13488 },
13489 cx,
13490 )
13491 .await;
13492
13493 // scenario: surrounding text matches completion text
13494 let completion_text = "to_offset";
13495 let initial_state = indoc! {"
13496 1. buf.to_offˇsuffix
13497 2. buf.to_offˇsuf
13498 3. buf.to_offˇfix
13499 4. buf.to_offˇ
13500 5. into_offˇensive
13501 6. ˇsuffix
13502 7. let ˇ //
13503 8. aaˇzz
13504 9. buf.to_off«zzzzzˇ»suffix
13505 10. buf.«ˇzzzzz»suffix
13506 11. to_off«ˇzzzzz»
13507
13508 buf.to_offˇsuffix // newest cursor
13509 "};
13510 let completion_marked_buffer = indoc! {"
13511 1. buf.to_offsuffix
13512 2. buf.to_offsuf
13513 3. buf.to_offfix
13514 4. buf.to_off
13515 5. into_offensive
13516 6. suffix
13517 7. let //
13518 8. aazz
13519 9. buf.to_offzzzzzsuffix
13520 10. buf.zzzzzsuffix
13521 11. to_offzzzzz
13522
13523 buf.<to_off|suffix> // newest cursor
13524 "};
13525 let expected = indoc! {"
13526 1. buf.to_offsetˇ
13527 2. buf.to_offsetˇsuf
13528 3. buf.to_offsetˇfix
13529 4. buf.to_offsetˇ
13530 5. into_offsetˇensive
13531 6. to_offsetˇsuffix
13532 7. let to_offsetˇ //
13533 8. aato_offsetˇzz
13534 9. buf.to_offsetˇ
13535 10. buf.to_offsetˇsuffix
13536 11. to_offsetˇ
13537
13538 buf.to_offsetˇ // newest cursor
13539 "};
13540 cx.set_state(initial_state);
13541 cx.update_editor(|editor, window, cx| {
13542 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13543 });
13544 handle_completion_request_with_insert_and_replace(
13545 &mut cx,
13546 completion_marked_buffer,
13547 vec![(completion_text, completion_text)],
13548 Arc::new(AtomicUsize::new(0)),
13549 )
13550 .await;
13551 cx.condition(|editor, _| editor.context_menu_visible())
13552 .await;
13553 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13554 editor
13555 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13556 .unwrap()
13557 });
13558 cx.assert_editor_state(expected);
13559 handle_resolve_completion_request(&mut cx, None).await;
13560 apply_additional_edits.await.unwrap();
13561
13562 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
13563 let completion_text = "foo_and_bar";
13564 let initial_state = indoc! {"
13565 1. ooanbˇ
13566 2. zooanbˇ
13567 3. ooanbˇz
13568 4. zooanbˇz
13569 5. ooanˇ
13570 6. oanbˇ
13571
13572 ooanbˇ
13573 "};
13574 let completion_marked_buffer = indoc! {"
13575 1. ooanb
13576 2. zooanb
13577 3. ooanbz
13578 4. zooanbz
13579 5. ooan
13580 6. oanb
13581
13582 <ooanb|>
13583 "};
13584 let expected = indoc! {"
13585 1. foo_and_barˇ
13586 2. zfoo_and_barˇ
13587 3. foo_and_barˇz
13588 4. zfoo_and_barˇz
13589 5. ooanfoo_and_barˇ
13590 6. oanbfoo_and_barˇ
13591
13592 foo_and_barˇ
13593 "};
13594 cx.set_state(initial_state);
13595 cx.update_editor(|editor, window, cx| {
13596 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13597 });
13598 handle_completion_request_with_insert_and_replace(
13599 &mut cx,
13600 completion_marked_buffer,
13601 vec![(completion_text, completion_text)],
13602 Arc::new(AtomicUsize::new(0)),
13603 )
13604 .await;
13605 cx.condition(|editor, _| editor.context_menu_visible())
13606 .await;
13607 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13608 editor
13609 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13610 .unwrap()
13611 });
13612 cx.assert_editor_state(expected);
13613 handle_resolve_completion_request(&mut cx, None).await;
13614 apply_additional_edits.await.unwrap();
13615
13616 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
13617 // (expects the same as if it was inserted at the end)
13618 let completion_text = "foo_and_bar";
13619 let initial_state = indoc! {"
13620 1. ooˇanb
13621 2. zooˇanb
13622 3. ooˇanbz
13623 4. zooˇanbz
13624
13625 ooˇanb
13626 "};
13627 let completion_marked_buffer = indoc! {"
13628 1. ooanb
13629 2. zooanb
13630 3. ooanbz
13631 4. zooanbz
13632
13633 <oo|anb>
13634 "};
13635 let expected = indoc! {"
13636 1. foo_and_barˇ
13637 2. zfoo_and_barˇ
13638 3. foo_and_barˇz
13639 4. zfoo_and_barˇz
13640
13641 foo_and_barˇ
13642 "};
13643 cx.set_state(initial_state);
13644 cx.update_editor(|editor, window, cx| {
13645 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13646 });
13647 handle_completion_request_with_insert_and_replace(
13648 &mut cx,
13649 completion_marked_buffer,
13650 vec![(completion_text, completion_text)],
13651 Arc::new(AtomicUsize::new(0)),
13652 )
13653 .await;
13654 cx.condition(|editor, _| editor.context_menu_visible())
13655 .await;
13656 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13657 editor
13658 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13659 .unwrap()
13660 });
13661 cx.assert_editor_state(expected);
13662 handle_resolve_completion_request(&mut cx, None).await;
13663 apply_additional_edits.await.unwrap();
13664}
13665
13666// This used to crash
13667#[gpui::test]
13668async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
13669 init_test(cx, |_| {});
13670
13671 let buffer_text = indoc! {"
13672 fn main() {
13673 10.satu;
13674
13675 //
13676 // separate cursors so they open in different excerpts (manually reproducible)
13677 //
13678
13679 10.satu20;
13680 }
13681 "};
13682 let multibuffer_text_with_selections = indoc! {"
13683 fn main() {
13684 10.satuˇ;
13685
13686 //
13687
13688 //
13689
13690 10.satuˇ20;
13691 }
13692 "};
13693 let expected_multibuffer = indoc! {"
13694 fn main() {
13695 10.saturating_sub()ˇ;
13696
13697 //
13698
13699 //
13700
13701 10.saturating_sub()ˇ;
13702 }
13703 "};
13704
13705 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
13706 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
13707
13708 let fs = FakeFs::new(cx.executor());
13709 fs.insert_tree(
13710 path!("/a"),
13711 json!({
13712 "main.rs": buffer_text,
13713 }),
13714 )
13715 .await;
13716
13717 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13718 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13719 language_registry.add(rust_lang());
13720 let mut fake_servers = language_registry.register_fake_lsp(
13721 "Rust",
13722 FakeLspAdapter {
13723 capabilities: lsp::ServerCapabilities {
13724 completion_provider: Some(lsp::CompletionOptions {
13725 resolve_provider: None,
13726 ..lsp::CompletionOptions::default()
13727 }),
13728 ..lsp::ServerCapabilities::default()
13729 },
13730 ..FakeLspAdapter::default()
13731 },
13732 );
13733 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13734 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13735 let buffer = project
13736 .update(cx, |project, cx| {
13737 project.open_local_buffer(path!("/a/main.rs"), cx)
13738 })
13739 .await
13740 .unwrap();
13741
13742 let multi_buffer = cx.new(|cx| {
13743 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
13744 multi_buffer.push_excerpts(
13745 buffer.clone(),
13746 [ExcerptRange::new(0..first_excerpt_end)],
13747 cx,
13748 );
13749 multi_buffer.push_excerpts(
13750 buffer.clone(),
13751 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
13752 cx,
13753 );
13754 multi_buffer
13755 });
13756
13757 let editor = workspace
13758 .update(cx, |_, window, cx| {
13759 cx.new(|cx| {
13760 Editor::new(
13761 EditorMode::Full {
13762 scale_ui_elements_with_buffer_font_size: false,
13763 show_active_line_background: false,
13764 sized_by_content: false,
13765 },
13766 multi_buffer.clone(),
13767 Some(project.clone()),
13768 window,
13769 cx,
13770 )
13771 })
13772 })
13773 .unwrap();
13774
13775 let pane = workspace
13776 .update(cx, |workspace, _, _| workspace.active_pane().clone())
13777 .unwrap();
13778 pane.update_in(cx, |pane, window, cx| {
13779 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
13780 });
13781
13782 let fake_server = fake_servers.next().await.unwrap();
13783
13784 editor.update_in(cx, |editor, window, cx| {
13785 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13786 s.select_ranges([
13787 Point::new(1, 11)..Point::new(1, 11),
13788 Point::new(7, 11)..Point::new(7, 11),
13789 ])
13790 });
13791
13792 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
13793 });
13794
13795 editor.update_in(cx, |editor, window, cx| {
13796 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13797 });
13798
13799 fake_server
13800 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13801 let completion_item = lsp::CompletionItem {
13802 label: "saturating_sub()".into(),
13803 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
13804 lsp::InsertReplaceEdit {
13805 new_text: "saturating_sub()".to_owned(),
13806 insert: lsp::Range::new(
13807 lsp::Position::new(7, 7),
13808 lsp::Position::new(7, 11),
13809 ),
13810 replace: lsp::Range::new(
13811 lsp::Position::new(7, 7),
13812 lsp::Position::new(7, 13),
13813 ),
13814 },
13815 )),
13816 ..lsp::CompletionItem::default()
13817 };
13818
13819 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
13820 })
13821 .next()
13822 .await
13823 .unwrap();
13824
13825 cx.condition(&editor, |editor, _| editor.context_menu_visible())
13826 .await;
13827
13828 editor
13829 .update_in(cx, |editor, window, cx| {
13830 editor
13831 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13832 .unwrap()
13833 })
13834 .await
13835 .unwrap();
13836
13837 editor.update(cx, |editor, cx| {
13838 assert_text_with_selections(editor, expected_multibuffer, cx);
13839 })
13840}
13841
13842#[gpui::test]
13843async fn test_completion(cx: &mut TestAppContext) {
13844 init_test(cx, |_| {});
13845
13846 let mut cx = EditorLspTestContext::new_rust(
13847 lsp::ServerCapabilities {
13848 completion_provider: Some(lsp::CompletionOptions {
13849 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
13850 resolve_provider: Some(true),
13851 ..Default::default()
13852 }),
13853 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
13854 ..Default::default()
13855 },
13856 cx,
13857 )
13858 .await;
13859 let counter = Arc::new(AtomicUsize::new(0));
13860
13861 cx.set_state(indoc! {"
13862 oneˇ
13863 two
13864 three
13865 "});
13866 cx.simulate_keystroke(".");
13867 handle_completion_request(
13868 indoc! {"
13869 one.|<>
13870 two
13871 three
13872 "},
13873 vec!["first_completion", "second_completion"],
13874 true,
13875 counter.clone(),
13876 &mut cx,
13877 )
13878 .await;
13879 cx.condition(|editor, _| editor.context_menu_visible())
13880 .await;
13881 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13882
13883 let _handler = handle_signature_help_request(
13884 &mut cx,
13885 lsp::SignatureHelp {
13886 signatures: vec![lsp::SignatureInformation {
13887 label: "test signature".to_string(),
13888 documentation: None,
13889 parameters: Some(vec![lsp::ParameterInformation {
13890 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
13891 documentation: None,
13892 }]),
13893 active_parameter: None,
13894 }],
13895 active_signature: None,
13896 active_parameter: None,
13897 },
13898 );
13899 cx.update_editor(|editor, window, cx| {
13900 assert!(
13901 !editor.signature_help_state.is_shown(),
13902 "No signature help was called for"
13903 );
13904 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13905 });
13906 cx.run_until_parked();
13907 cx.update_editor(|editor, _, _| {
13908 assert!(
13909 !editor.signature_help_state.is_shown(),
13910 "No signature help should be shown when completions menu is open"
13911 );
13912 });
13913
13914 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13915 editor.context_menu_next(&Default::default(), window, cx);
13916 editor
13917 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13918 .unwrap()
13919 });
13920 cx.assert_editor_state(indoc! {"
13921 one.second_completionˇ
13922 two
13923 three
13924 "});
13925
13926 handle_resolve_completion_request(
13927 &mut cx,
13928 Some(vec![
13929 (
13930 //This overlaps with the primary completion edit which is
13931 //misbehavior from the LSP spec, test that we filter it out
13932 indoc! {"
13933 one.second_ˇcompletion
13934 two
13935 threeˇ
13936 "},
13937 "overlapping additional edit",
13938 ),
13939 (
13940 indoc! {"
13941 one.second_completion
13942 two
13943 threeˇ
13944 "},
13945 "\nadditional edit",
13946 ),
13947 ]),
13948 )
13949 .await;
13950 apply_additional_edits.await.unwrap();
13951 cx.assert_editor_state(indoc! {"
13952 one.second_completionˇ
13953 two
13954 three
13955 additional edit
13956 "});
13957
13958 cx.set_state(indoc! {"
13959 one.second_completion
13960 twoˇ
13961 threeˇ
13962 additional edit
13963 "});
13964 cx.simulate_keystroke(" ");
13965 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
13966 cx.simulate_keystroke("s");
13967 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
13968
13969 cx.assert_editor_state(indoc! {"
13970 one.second_completion
13971 two sˇ
13972 three sˇ
13973 additional edit
13974 "});
13975 handle_completion_request(
13976 indoc! {"
13977 one.second_completion
13978 two s
13979 three <s|>
13980 additional edit
13981 "},
13982 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
13983 true,
13984 counter.clone(),
13985 &mut cx,
13986 )
13987 .await;
13988 cx.condition(|editor, _| editor.context_menu_visible())
13989 .await;
13990 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
13991
13992 cx.simulate_keystroke("i");
13993
13994 handle_completion_request(
13995 indoc! {"
13996 one.second_completion
13997 two si
13998 three <si|>
13999 additional edit
14000 "},
14001 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14002 true,
14003 counter.clone(),
14004 &mut cx,
14005 )
14006 .await;
14007 cx.condition(|editor, _| editor.context_menu_visible())
14008 .await;
14009 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14010
14011 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14012 editor
14013 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14014 .unwrap()
14015 });
14016 cx.assert_editor_state(indoc! {"
14017 one.second_completion
14018 two sixth_completionˇ
14019 three sixth_completionˇ
14020 additional edit
14021 "});
14022
14023 apply_additional_edits.await.unwrap();
14024
14025 update_test_language_settings(&mut cx, |settings| {
14026 settings.defaults.show_completions_on_input = Some(false);
14027 });
14028 cx.set_state("editorˇ");
14029 cx.simulate_keystroke(".");
14030 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14031 cx.simulate_keystrokes("c l o");
14032 cx.assert_editor_state("editor.cloˇ");
14033 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14034 cx.update_editor(|editor, window, cx| {
14035 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14036 });
14037 handle_completion_request(
14038 "editor.<clo|>",
14039 vec!["close", "clobber"],
14040 true,
14041 counter.clone(),
14042 &mut cx,
14043 )
14044 .await;
14045 cx.condition(|editor, _| editor.context_menu_visible())
14046 .await;
14047 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
14048
14049 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14050 editor
14051 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14052 .unwrap()
14053 });
14054 cx.assert_editor_state("editor.clobberˇ");
14055 handle_resolve_completion_request(&mut cx, None).await;
14056 apply_additional_edits.await.unwrap();
14057}
14058
14059#[gpui::test]
14060async fn test_completion_reuse(cx: &mut TestAppContext) {
14061 init_test(cx, |_| {});
14062
14063 let mut cx = EditorLspTestContext::new_rust(
14064 lsp::ServerCapabilities {
14065 completion_provider: Some(lsp::CompletionOptions {
14066 trigger_characters: Some(vec![".".to_string()]),
14067 ..Default::default()
14068 }),
14069 ..Default::default()
14070 },
14071 cx,
14072 )
14073 .await;
14074
14075 let counter = Arc::new(AtomicUsize::new(0));
14076 cx.set_state("objˇ");
14077 cx.simulate_keystroke(".");
14078
14079 // Initial completion request returns complete results
14080 let is_incomplete = false;
14081 handle_completion_request(
14082 "obj.|<>",
14083 vec!["a", "ab", "abc"],
14084 is_incomplete,
14085 counter.clone(),
14086 &mut cx,
14087 )
14088 .await;
14089 cx.run_until_parked();
14090 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14091 cx.assert_editor_state("obj.ˇ");
14092 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14093
14094 // Type "a" - filters existing completions
14095 cx.simulate_keystroke("a");
14096 cx.run_until_parked();
14097 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14098 cx.assert_editor_state("obj.aˇ");
14099 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14100
14101 // Type "b" - filters existing completions
14102 cx.simulate_keystroke("b");
14103 cx.run_until_parked();
14104 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14105 cx.assert_editor_state("obj.abˇ");
14106 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14107
14108 // Type "c" - filters existing completions
14109 cx.simulate_keystroke("c");
14110 cx.run_until_parked();
14111 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14112 cx.assert_editor_state("obj.abcˇ");
14113 check_displayed_completions(vec!["abc"], &mut cx);
14114
14115 // Backspace to delete "c" - filters existing completions
14116 cx.update_editor(|editor, window, cx| {
14117 editor.backspace(&Backspace, window, cx);
14118 });
14119 cx.run_until_parked();
14120 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14121 cx.assert_editor_state("obj.abˇ");
14122 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14123
14124 // Moving cursor to the left dismisses menu.
14125 cx.update_editor(|editor, window, cx| {
14126 editor.move_left(&MoveLeft, window, cx);
14127 });
14128 cx.run_until_parked();
14129 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14130 cx.assert_editor_state("obj.aˇb");
14131 cx.update_editor(|editor, _, _| {
14132 assert_eq!(editor.context_menu_visible(), false);
14133 });
14134
14135 // Type "b" - new request
14136 cx.simulate_keystroke("b");
14137 let is_incomplete = false;
14138 handle_completion_request(
14139 "obj.<ab|>a",
14140 vec!["ab", "abc"],
14141 is_incomplete,
14142 counter.clone(),
14143 &mut cx,
14144 )
14145 .await;
14146 cx.run_until_parked();
14147 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14148 cx.assert_editor_state("obj.abˇb");
14149 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14150
14151 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
14152 cx.update_editor(|editor, window, cx| {
14153 editor.backspace(&Backspace, window, cx);
14154 });
14155 let is_incomplete = false;
14156 handle_completion_request(
14157 "obj.<a|>b",
14158 vec!["a", "ab", "abc"],
14159 is_incomplete,
14160 counter.clone(),
14161 &mut cx,
14162 )
14163 .await;
14164 cx.run_until_parked();
14165 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14166 cx.assert_editor_state("obj.aˇb");
14167 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14168
14169 // Backspace to delete "a" - dismisses menu.
14170 cx.update_editor(|editor, window, cx| {
14171 editor.backspace(&Backspace, window, cx);
14172 });
14173 cx.run_until_parked();
14174 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14175 cx.assert_editor_state("obj.ˇb");
14176 cx.update_editor(|editor, _, _| {
14177 assert_eq!(editor.context_menu_visible(), false);
14178 });
14179}
14180
14181#[gpui::test]
14182async fn test_word_completion(cx: &mut TestAppContext) {
14183 let lsp_fetch_timeout_ms = 10;
14184 init_test(cx, |language_settings| {
14185 language_settings.defaults.completions = Some(CompletionSettingsContent {
14186 words_min_length: Some(0),
14187 lsp_fetch_timeout_ms: Some(10),
14188 lsp_insert_mode: Some(LspInsertMode::Insert),
14189 ..Default::default()
14190 });
14191 });
14192
14193 let mut cx = EditorLspTestContext::new_rust(
14194 lsp::ServerCapabilities {
14195 completion_provider: Some(lsp::CompletionOptions {
14196 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14197 ..lsp::CompletionOptions::default()
14198 }),
14199 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14200 ..lsp::ServerCapabilities::default()
14201 },
14202 cx,
14203 )
14204 .await;
14205
14206 let throttle_completions = Arc::new(AtomicBool::new(false));
14207
14208 let lsp_throttle_completions = throttle_completions.clone();
14209 let _completion_requests_handler =
14210 cx.lsp
14211 .server
14212 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
14213 let lsp_throttle_completions = lsp_throttle_completions.clone();
14214 let cx = cx.clone();
14215 async move {
14216 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
14217 cx.background_executor()
14218 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
14219 .await;
14220 }
14221 Ok(Some(lsp::CompletionResponse::Array(vec![
14222 lsp::CompletionItem {
14223 label: "first".into(),
14224 ..lsp::CompletionItem::default()
14225 },
14226 lsp::CompletionItem {
14227 label: "last".into(),
14228 ..lsp::CompletionItem::default()
14229 },
14230 ])))
14231 }
14232 });
14233
14234 cx.set_state(indoc! {"
14235 oneˇ
14236 two
14237 three
14238 "});
14239 cx.simulate_keystroke(".");
14240 cx.executor().run_until_parked();
14241 cx.condition(|editor, _| editor.context_menu_visible())
14242 .await;
14243 cx.update_editor(|editor, window, cx| {
14244 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14245 {
14246 assert_eq!(
14247 completion_menu_entries(menu),
14248 &["first", "last"],
14249 "When LSP server is fast to reply, no fallback word completions are used"
14250 );
14251 } else {
14252 panic!("expected completion menu to be open");
14253 }
14254 editor.cancel(&Cancel, window, cx);
14255 });
14256 cx.executor().run_until_parked();
14257 cx.condition(|editor, _| !editor.context_menu_visible())
14258 .await;
14259
14260 throttle_completions.store(true, atomic::Ordering::Release);
14261 cx.simulate_keystroke(".");
14262 cx.executor()
14263 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
14264 cx.executor().run_until_parked();
14265 cx.condition(|editor, _| editor.context_menu_visible())
14266 .await;
14267 cx.update_editor(|editor, _, _| {
14268 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14269 {
14270 assert_eq!(completion_menu_entries(menu), &["one", "three", "two"],
14271 "When LSP server is slow, document words can be shown instead, if configured accordingly");
14272 } else {
14273 panic!("expected completion menu to be open");
14274 }
14275 });
14276}
14277
14278#[gpui::test]
14279async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
14280 init_test(cx, |language_settings| {
14281 language_settings.defaults.completions = Some(CompletionSettingsContent {
14282 words: Some(WordsCompletionMode::Enabled),
14283 words_min_length: Some(0),
14284 lsp_insert_mode: Some(LspInsertMode::Insert),
14285 ..Default::default()
14286 });
14287 });
14288
14289 let mut cx = EditorLspTestContext::new_rust(
14290 lsp::ServerCapabilities {
14291 completion_provider: Some(lsp::CompletionOptions {
14292 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14293 ..lsp::CompletionOptions::default()
14294 }),
14295 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14296 ..lsp::ServerCapabilities::default()
14297 },
14298 cx,
14299 )
14300 .await;
14301
14302 let _completion_requests_handler =
14303 cx.lsp
14304 .server
14305 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14306 Ok(Some(lsp::CompletionResponse::Array(vec![
14307 lsp::CompletionItem {
14308 label: "first".into(),
14309 ..lsp::CompletionItem::default()
14310 },
14311 lsp::CompletionItem {
14312 label: "last".into(),
14313 ..lsp::CompletionItem::default()
14314 },
14315 ])))
14316 });
14317
14318 cx.set_state(indoc! {"ˇ
14319 first
14320 last
14321 second
14322 "});
14323 cx.simulate_keystroke(".");
14324 cx.executor().run_until_parked();
14325 cx.condition(|editor, _| editor.context_menu_visible())
14326 .await;
14327 cx.update_editor(|editor, _, _| {
14328 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14329 {
14330 assert_eq!(
14331 completion_menu_entries(menu),
14332 &["first", "last", "second"],
14333 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
14334 );
14335 } else {
14336 panic!("expected completion menu to be open");
14337 }
14338 });
14339}
14340
14341#[gpui::test]
14342async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
14343 init_test(cx, |language_settings| {
14344 language_settings.defaults.completions = Some(CompletionSettingsContent {
14345 words: Some(WordsCompletionMode::Disabled),
14346 words_min_length: Some(0),
14347 lsp_insert_mode: Some(LspInsertMode::Insert),
14348 ..Default::default()
14349 });
14350 });
14351
14352 let mut cx = EditorLspTestContext::new_rust(
14353 lsp::ServerCapabilities {
14354 completion_provider: Some(lsp::CompletionOptions {
14355 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14356 ..lsp::CompletionOptions::default()
14357 }),
14358 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14359 ..lsp::ServerCapabilities::default()
14360 },
14361 cx,
14362 )
14363 .await;
14364
14365 let _completion_requests_handler =
14366 cx.lsp
14367 .server
14368 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14369 panic!("LSP completions should not be queried when dealing with word completions")
14370 });
14371
14372 cx.set_state(indoc! {"ˇ
14373 first
14374 last
14375 second
14376 "});
14377 cx.update_editor(|editor, window, cx| {
14378 editor.show_word_completions(&ShowWordCompletions, window, cx);
14379 });
14380 cx.executor().run_until_parked();
14381 cx.condition(|editor, _| editor.context_menu_visible())
14382 .await;
14383 cx.update_editor(|editor, _, _| {
14384 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14385 {
14386 assert_eq!(
14387 completion_menu_entries(menu),
14388 &["first", "last", "second"],
14389 "`ShowWordCompletions` action should show word completions"
14390 );
14391 } else {
14392 panic!("expected completion menu to be open");
14393 }
14394 });
14395
14396 cx.simulate_keystroke("l");
14397 cx.executor().run_until_parked();
14398 cx.condition(|editor, _| editor.context_menu_visible())
14399 .await;
14400 cx.update_editor(|editor, _, _| {
14401 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14402 {
14403 assert_eq!(
14404 completion_menu_entries(menu),
14405 &["last"],
14406 "After showing word completions, further editing should filter them and not query the LSP"
14407 );
14408 } else {
14409 panic!("expected completion menu to be open");
14410 }
14411 });
14412}
14413
14414#[gpui::test]
14415async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
14416 init_test(cx, |language_settings| {
14417 language_settings.defaults.completions = Some(CompletionSettingsContent {
14418 words_min_length: Some(0),
14419 lsp: Some(false),
14420 lsp_insert_mode: Some(LspInsertMode::Insert),
14421 ..Default::default()
14422 });
14423 });
14424
14425 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14426
14427 cx.set_state(indoc! {"ˇ
14428 0_usize
14429 let
14430 33
14431 4.5f32
14432 "});
14433 cx.update_editor(|editor, window, cx| {
14434 editor.show_completions(&ShowCompletions::default(), window, cx);
14435 });
14436 cx.executor().run_until_parked();
14437 cx.condition(|editor, _| editor.context_menu_visible())
14438 .await;
14439 cx.update_editor(|editor, window, cx| {
14440 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14441 {
14442 assert_eq!(
14443 completion_menu_entries(menu),
14444 &["let"],
14445 "With no digits in the completion query, no digits should be in the word completions"
14446 );
14447 } else {
14448 panic!("expected completion menu to be open");
14449 }
14450 editor.cancel(&Cancel, window, cx);
14451 });
14452
14453 cx.set_state(indoc! {"3ˇ
14454 0_usize
14455 let
14456 3
14457 33.35f32
14458 "});
14459 cx.update_editor(|editor, window, cx| {
14460 editor.show_completions(&ShowCompletions::default(), window, cx);
14461 });
14462 cx.executor().run_until_parked();
14463 cx.condition(|editor, _| editor.context_menu_visible())
14464 .await;
14465 cx.update_editor(|editor, _, _| {
14466 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14467 {
14468 assert_eq!(completion_menu_entries(menu), &["33", "35f32"], "The digit is in the completion query, \
14469 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
14470 } else {
14471 panic!("expected completion menu to be open");
14472 }
14473 });
14474}
14475
14476#[gpui::test]
14477async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) {
14478 init_test(cx, |language_settings| {
14479 language_settings.defaults.completions = Some(CompletionSettingsContent {
14480 words: Some(WordsCompletionMode::Enabled),
14481 words_min_length: Some(3),
14482 lsp_insert_mode: Some(LspInsertMode::Insert),
14483 ..Default::default()
14484 });
14485 });
14486
14487 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14488 cx.set_state(indoc! {"ˇ
14489 wow
14490 wowen
14491 wowser
14492 "});
14493 cx.simulate_keystroke("w");
14494 cx.executor().run_until_parked();
14495 cx.update_editor(|editor, _, _| {
14496 if editor.context_menu.borrow_mut().is_some() {
14497 panic!(
14498 "expected completion menu to be hidden, as words completion threshold is not met"
14499 );
14500 }
14501 });
14502
14503 cx.update_editor(|editor, window, cx| {
14504 editor.show_word_completions(&ShowWordCompletions, window, cx);
14505 });
14506 cx.executor().run_until_parked();
14507 cx.update_editor(|editor, window, cx| {
14508 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14509 {
14510 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");
14511 } else {
14512 panic!("expected completion menu to be open after the word completions are called with an action");
14513 }
14514
14515 editor.cancel(&Cancel, window, cx);
14516 });
14517 cx.update_editor(|editor, _, _| {
14518 if editor.context_menu.borrow_mut().is_some() {
14519 panic!("expected completion menu to be hidden after canceling");
14520 }
14521 });
14522
14523 cx.simulate_keystroke("o");
14524 cx.executor().run_until_parked();
14525 cx.update_editor(|editor, _, _| {
14526 if editor.context_menu.borrow_mut().is_some() {
14527 panic!(
14528 "expected completion menu to be hidden, as words completion threshold is not met still"
14529 );
14530 }
14531 });
14532
14533 cx.simulate_keystroke("w");
14534 cx.executor().run_until_parked();
14535 cx.update_editor(|editor, _, _| {
14536 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14537 {
14538 assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word");
14539 } else {
14540 panic!("expected completion menu to be open after the word completions threshold is met");
14541 }
14542 });
14543}
14544
14545#[gpui::test]
14546async fn test_word_completions_disabled(cx: &mut TestAppContext) {
14547 init_test(cx, |language_settings| {
14548 language_settings.defaults.completions = Some(CompletionSettingsContent {
14549 words: Some(WordsCompletionMode::Enabled),
14550 words_min_length: Some(0),
14551 lsp_insert_mode: Some(LspInsertMode::Insert),
14552 ..Default::default()
14553 });
14554 });
14555
14556 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14557 cx.update_editor(|editor, _, _| {
14558 editor.disable_word_completions();
14559 });
14560 cx.set_state(indoc! {"ˇ
14561 wow
14562 wowen
14563 wowser
14564 "});
14565 cx.simulate_keystroke("w");
14566 cx.executor().run_until_parked();
14567 cx.update_editor(|editor, _, _| {
14568 if editor.context_menu.borrow_mut().is_some() {
14569 panic!(
14570 "expected completion menu to be hidden, as words completion are disabled for this editor"
14571 );
14572 }
14573 });
14574
14575 cx.update_editor(|editor, window, cx| {
14576 editor.show_word_completions(&ShowWordCompletions, window, cx);
14577 });
14578 cx.executor().run_until_parked();
14579 cx.update_editor(|editor, _, _| {
14580 if editor.context_menu.borrow_mut().is_some() {
14581 panic!(
14582 "expected completion menu to be hidden even if called for explicitly, as words completion are disabled for this editor"
14583 );
14584 }
14585 });
14586}
14587
14588fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
14589 let position = || lsp::Position {
14590 line: params.text_document_position.position.line,
14591 character: params.text_document_position.position.character,
14592 };
14593 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14594 range: lsp::Range {
14595 start: position(),
14596 end: position(),
14597 },
14598 new_text: text.to_string(),
14599 }))
14600}
14601
14602#[gpui::test]
14603async fn test_multiline_completion(cx: &mut TestAppContext) {
14604 init_test(cx, |_| {});
14605
14606 let fs = FakeFs::new(cx.executor());
14607 fs.insert_tree(
14608 path!("/a"),
14609 json!({
14610 "main.ts": "a",
14611 }),
14612 )
14613 .await;
14614
14615 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14616 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14617 let typescript_language = Arc::new(Language::new(
14618 LanguageConfig {
14619 name: "TypeScript".into(),
14620 matcher: LanguageMatcher {
14621 path_suffixes: vec!["ts".to_string()],
14622 ..LanguageMatcher::default()
14623 },
14624 line_comments: vec!["// ".into()],
14625 ..LanguageConfig::default()
14626 },
14627 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
14628 ));
14629 language_registry.add(typescript_language.clone());
14630 let mut fake_servers = language_registry.register_fake_lsp(
14631 "TypeScript",
14632 FakeLspAdapter {
14633 capabilities: lsp::ServerCapabilities {
14634 completion_provider: Some(lsp::CompletionOptions {
14635 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14636 ..lsp::CompletionOptions::default()
14637 }),
14638 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14639 ..lsp::ServerCapabilities::default()
14640 },
14641 // Emulate vtsls label generation
14642 label_for_completion: Some(Box::new(|item, _| {
14643 let text = if let Some(description) = item
14644 .label_details
14645 .as_ref()
14646 .and_then(|label_details| label_details.description.as_ref())
14647 {
14648 format!("{} {}", item.label, description)
14649 } else if let Some(detail) = &item.detail {
14650 format!("{} {}", item.label, detail)
14651 } else {
14652 item.label.clone()
14653 };
14654 let len = text.len();
14655 Some(language::CodeLabel {
14656 text,
14657 runs: Vec::new(),
14658 filter_range: 0..len,
14659 })
14660 })),
14661 ..FakeLspAdapter::default()
14662 },
14663 );
14664 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14665 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14666 let worktree_id = workspace
14667 .update(cx, |workspace, _window, cx| {
14668 workspace.project().update(cx, |project, cx| {
14669 project.worktrees(cx).next().unwrap().read(cx).id()
14670 })
14671 })
14672 .unwrap();
14673 let _buffer = project
14674 .update(cx, |project, cx| {
14675 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
14676 })
14677 .await
14678 .unwrap();
14679 let editor = workspace
14680 .update(cx, |workspace, window, cx| {
14681 workspace.open_path((worktree_id, rel_path("main.ts")), None, true, window, cx)
14682 })
14683 .unwrap()
14684 .await
14685 .unwrap()
14686 .downcast::<Editor>()
14687 .unwrap();
14688 let fake_server = fake_servers.next().await.unwrap();
14689
14690 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
14691 let multiline_label_2 = "a\nb\nc\n";
14692 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
14693 let multiline_description = "d\ne\nf\n";
14694 let multiline_detail_2 = "g\nh\ni\n";
14695
14696 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
14697 move |params, _| async move {
14698 Ok(Some(lsp::CompletionResponse::Array(vec![
14699 lsp::CompletionItem {
14700 label: multiline_label.to_string(),
14701 text_edit: gen_text_edit(¶ms, "new_text_1"),
14702 ..lsp::CompletionItem::default()
14703 },
14704 lsp::CompletionItem {
14705 label: "single line label 1".to_string(),
14706 detail: Some(multiline_detail.to_string()),
14707 text_edit: gen_text_edit(¶ms, "new_text_2"),
14708 ..lsp::CompletionItem::default()
14709 },
14710 lsp::CompletionItem {
14711 label: "single line label 2".to_string(),
14712 label_details: Some(lsp::CompletionItemLabelDetails {
14713 description: Some(multiline_description.to_string()),
14714 detail: None,
14715 }),
14716 text_edit: gen_text_edit(¶ms, "new_text_2"),
14717 ..lsp::CompletionItem::default()
14718 },
14719 lsp::CompletionItem {
14720 label: multiline_label_2.to_string(),
14721 detail: Some(multiline_detail_2.to_string()),
14722 text_edit: gen_text_edit(¶ms, "new_text_3"),
14723 ..lsp::CompletionItem::default()
14724 },
14725 lsp::CompletionItem {
14726 label: "Label with many spaces and \t but without newlines".to_string(),
14727 detail: Some(
14728 "Details with many spaces and \t but without newlines".to_string(),
14729 ),
14730 text_edit: gen_text_edit(¶ms, "new_text_4"),
14731 ..lsp::CompletionItem::default()
14732 },
14733 ])))
14734 },
14735 );
14736
14737 editor.update_in(cx, |editor, window, cx| {
14738 cx.focus_self(window);
14739 editor.move_to_end(&MoveToEnd, window, cx);
14740 editor.handle_input(".", window, cx);
14741 });
14742 cx.run_until_parked();
14743 completion_handle.next().await.unwrap();
14744
14745 editor.update(cx, |editor, _| {
14746 assert!(editor.context_menu_visible());
14747 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14748 {
14749 let completion_labels = menu
14750 .completions
14751 .borrow()
14752 .iter()
14753 .map(|c| c.label.text.clone())
14754 .collect::<Vec<_>>();
14755 assert_eq!(
14756 completion_labels,
14757 &[
14758 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
14759 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
14760 "single line label 2 d e f ",
14761 "a b c g h i ",
14762 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
14763 ],
14764 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
14765 );
14766
14767 for completion in menu
14768 .completions
14769 .borrow()
14770 .iter() {
14771 assert_eq!(
14772 completion.label.filter_range,
14773 0..completion.label.text.len(),
14774 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
14775 );
14776 }
14777 } else {
14778 panic!("expected completion menu to be open");
14779 }
14780 });
14781}
14782
14783#[gpui::test]
14784async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
14785 init_test(cx, |_| {});
14786 let mut cx = EditorLspTestContext::new_rust(
14787 lsp::ServerCapabilities {
14788 completion_provider: Some(lsp::CompletionOptions {
14789 trigger_characters: Some(vec![".".to_string()]),
14790 ..Default::default()
14791 }),
14792 ..Default::default()
14793 },
14794 cx,
14795 )
14796 .await;
14797 cx.lsp
14798 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14799 Ok(Some(lsp::CompletionResponse::Array(vec![
14800 lsp::CompletionItem {
14801 label: "first".into(),
14802 ..Default::default()
14803 },
14804 lsp::CompletionItem {
14805 label: "last".into(),
14806 ..Default::default()
14807 },
14808 ])))
14809 });
14810 cx.set_state("variableˇ");
14811 cx.simulate_keystroke(".");
14812 cx.executor().run_until_parked();
14813
14814 cx.update_editor(|editor, _, _| {
14815 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14816 {
14817 assert_eq!(completion_menu_entries(menu), &["first", "last"]);
14818 } else {
14819 panic!("expected completion menu to be open");
14820 }
14821 });
14822
14823 cx.update_editor(|editor, window, cx| {
14824 editor.move_page_down(&MovePageDown::default(), window, cx);
14825 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14826 {
14827 assert!(
14828 menu.selected_item == 1,
14829 "expected PageDown to select the last item from the context menu"
14830 );
14831 } else {
14832 panic!("expected completion menu to stay open after PageDown");
14833 }
14834 });
14835
14836 cx.update_editor(|editor, window, cx| {
14837 editor.move_page_up(&MovePageUp::default(), window, cx);
14838 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14839 {
14840 assert!(
14841 menu.selected_item == 0,
14842 "expected PageUp to select the first item from the context menu"
14843 );
14844 } else {
14845 panic!("expected completion menu to stay open after PageUp");
14846 }
14847 });
14848}
14849
14850#[gpui::test]
14851async fn test_as_is_completions(cx: &mut TestAppContext) {
14852 init_test(cx, |_| {});
14853 let mut cx = EditorLspTestContext::new_rust(
14854 lsp::ServerCapabilities {
14855 completion_provider: Some(lsp::CompletionOptions {
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: "unsafe".into(),
14868 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14869 range: lsp::Range {
14870 start: lsp::Position {
14871 line: 1,
14872 character: 2,
14873 },
14874 end: lsp::Position {
14875 line: 1,
14876 character: 3,
14877 },
14878 },
14879 new_text: "unsafe".to_string(),
14880 })),
14881 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
14882 ..Default::default()
14883 },
14884 ])))
14885 });
14886 cx.set_state("fn a() {}\n nˇ");
14887 cx.executor().run_until_parked();
14888 cx.update_editor(|editor, window, cx| {
14889 editor.show_completions(
14890 &ShowCompletions {
14891 trigger: Some("\n".into()),
14892 },
14893 window,
14894 cx,
14895 );
14896 });
14897 cx.executor().run_until_parked();
14898
14899 cx.update_editor(|editor, window, cx| {
14900 editor.confirm_completion(&Default::default(), window, cx)
14901 });
14902 cx.executor().run_until_parked();
14903 cx.assert_editor_state("fn a() {}\n unsafeˇ");
14904}
14905
14906#[gpui::test]
14907async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
14908 init_test(cx, |_| {});
14909 let language =
14910 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
14911 let mut cx = EditorLspTestContext::new(
14912 language,
14913 lsp::ServerCapabilities {
14914 completion_provider: Some(lsp::CompletionOptions {
14915 ..lsp::CompletionOptions::default()
14916 }),
14917 ..lsp::ServerCapabilities::default()
14918 },
14919 cx,
14920 )
14921 .await;
14922
14923 cx.set_state(
14924 "#ifndef BAR_H
14925#define BAR_H
14926
14927#include <stdbool.h>
14928
14929int fn_branch(bool do_branch1, bool do_branch2);
14930
14931#endif // BAR_H
14932ˇ",
14933 );
14934 cx.executor().run_until_parked();
14935 cx.update_editor(|editor, window, cx| {
14936 editor.handle_input("#", window, cx);
14937 });
14938 cx.executor().run_until_parked();
14939 cx.update_editor(|editor, window, cx| {
14940 editor.handle_input("i", window, cx);
14941 });
14942 cx.executor().run_until_parked();
14943 cx.update_editor(|editor, window, cx| {
14944 editor.handle_input("n", window, cx);
14945 });
14946 cx.executor().run_until_parked();
14947 cx.assert_editor_state(
14948 "#ifndef BAR_H
14949#define BAR_H
14950
14951#include <stdbool.h>
14952
14953int fn_branch(bool do_branch1, bool do_branch2);
14954
14955#endif // BAR_H
14956#inˇ",
14957 );
14958
14959 cx.lsp
14960 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14961 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
14962 is_incomplete: false,
14963 item_defaults: None,
14964 items: vec![lsp::CompletionItem {
14965 kind: Some(lsp::CompletionItemKind::SNIPPET),
14966 label_details: Some(lsp::CompletionItemLabelDetails {
14967 detail: Some("header".to_string()),
14968 description: None,
14969 }),
14970 label: " include".to_string(),
14971 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14972 range: lsp::Range {
14973 start: lsp::Position {
14974 line: 8,
14975 character: 1,
14976 },
14977 end: lsp::Position {
14978 line: 8,
14979 character: 1,
14980 },
14981 },
14982 new_text: "include \"$0\"".to_string(),
14983 })),
14984 sort_text: Some("40b67681include".to_string()),
14985 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
14986 filter_text: Some("include".to_string()),
14987 insert_text: Some("include \"$0\"".to_string()),
14988 ..lsp::CompletionItem::default()
14989 }],
14990 })))
14991 });
14992 cx.update_editor(|editor, window, cx| {
14993 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14994 });
14995 cx.executor().run_until_parked();
14996 cx.update_editor(|editor, window, cx| {
14997 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
14998 });
14999 cx.executor().run_until_parked();
15000 cx.assert_editor_state(
15001 "#ifndef BAR_H
15002#define BAR_H
15003
15004#include <stdbool.h>
15005
15006int fn_branch(bool do_branch1, bool do_branch2);
15007
15008#endif // BAR_H
15009#include \"ˇ\"",
15010 );
15011
15012 cx.lsp
15013 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15014 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15015 is_incomplete: true,
15016 item_defaults: None,
15017 items: vec![lsp::CompletionItem {
15018 kind: Some(lsp::CompletionItemKind::FILE),
15019 label: "AGL/".to_string(),
15020 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15021 range: lsp::Range {
15022 start: lsp::Position {
15023 line: 8,
15024 character: 10,
15025 },
15026 end: lsp::Position {
15027 line: 8,
15028 character: 11,
15029 },
15030 },
15031 new_text: "AGL/".to_string(),
15032 })),
15033 sort_text: Some("40b67681AGL/".to_string()),
15034 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
15035 filter_text: Some("AGL/".to_string()),
15036 insert_text: Some("AGL/".to_string()),
15037 ..lsp::CompletionItem::default()
15038 }],
15039 })))
15040 });
15041 cx.update_editor(|editor, window, cx| {
15042 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15043 });
15044 cx.executor().run_until_parked();
15045 cx.update_editor(|editor, window, cx| {
15046 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15047 });
15048 cx.executor().run_until_parked();
15049 cx.assert_editor_state(
15050 r##"#ifndef BAR_H
15051#define BAR_H
15052
15053#include <stdbool.h>
15054
15055int fn_branch(bool do_branch1, bool do_branch2);
15056
15057#endif // BAR_H
15058#include "AGL/ˇ"##,
15059 );
15060
15061 cx.update_editor(|editor, window, cx| {
15062 editor.handle_input("\"", window, cx);
15063 });
15064 cx.executor().run_until_parked();
15065 cx.assert_editor_state(
15066 r##"#ifndef BAR_H
15067#define BAR_H
15068
15069#include <stdbool.h>
15070
15071int fn_branch(bool do_branch1, bool do_branch2);
15072
15073#endif // BAR_H
15074#include "AGL/"ˇ"##,
15075 );
15076}
15077
15078#[gpui::test]
15079async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
15080 init_test(cx, |_| {});
15081
15082 let mut cx = EditorLspTestContext::new_rust(
15083 lsp::ServerCapabilities {
15084 completion_provider: Some(lsp::CompletionOptions {
15085 trigger_characters: Some(vec![".".to_string()]),
15086 resolve_provider: Some(true),
15087 ..Default::default()
15088 }),
15089 ..Default::default()
15090 },
15091 cx,
15092 )
15093 .await;
15094
15095 cx.set_state("fn main() { let a = 2ˇ; }");
15096 cx.simulate_keystroke(".");
15097 let completion_item = lsp::CompletionItem {
15098 label: "Some".into(),
15099 kind: Some(lsp::CompletionItemKind::SNIPPET),
15100 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
15101 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
15102 kind: lsp::MarkupKind::Markdown,
15103 value: "```rust\nSome(2)\n```".to_string(),
15104 })),
15105 deprecated: Some(false),
15106 sort_text: Some("Some".to_string()),
15107 filter_text: Some("Some".to_string()),
15108 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15109 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15110 range: lsp::Range {
15111 start: lsp::Position {
15112 line: 0,
15113 character: 22,
15114 },
15115 end: lsp::Position {
15116 line: 0,
15117 character: 22,
15118 },
15119 },
15120 new_text: "Some(2)".to_string(),
15121 })),
15122 additional_text_edits: Some(vec![lsp::TextEdit {
15123 range: lsp::Range {
15124 start: lsp::Position {
15125 line: 0,
15126 character: 20,
15127 },
15128 end: lsp::Position {
15129 line: 0,
15130 character: 22,
15131 },
15132 },
15133 new_text: "".to_string(),
15134 }]),
15135 ..Default::default()
15136 };
15137
15138 let closure_completion_item = completion_item.clone();
15139 let counter = Arc::new(AtomicUsize::new(0));
15140 let counter_clone = counter.clone();
15141 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15142 let task_completion_item = closure_completion_item.clone();
15143 counter_clone.fetch_add(1, atomic::Ordering::Release);
15144 async move {
15145 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15146 is_incomplete: true,
15147 item_defaults: None,
15148 items: vec![task_completion_item],
15149 })))
15150 }
15151 });
15152
15153 cx.condition(|editor, _| editor.context_menu_visible())
15154 .await;
15155 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
15156 assert!(request.next().await.is_some());
15157 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15158
15159 cx.simulate_keystrokes("S o m");
15160 cx.condition(|editor, _| editor.context_menu_visible())
15161 .await;
15162 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
15163 assert!(request.next().await.is_some());
15164 assert!(request.next().await.is_some());
15165 assert!(request.next().await.is_some());
15166 request.close();
15167 assert!(request.next().await.is_none());
15168 assert_eq!(
15169 counter.load(atomic::Ordering::Acquire),
15170 4,
15171 "With the completions menu open, only one LSP request should happen per input"
15172 );
15173}
15174
15175#[gpui::test]
15176async fn test_toggle_comment(cx: &mut TestAppContext) {
15177 init_test(cx, |_| {});
15178 let mut cx = EditorTestContext::new(cx).await;
15179 let language = Arc::new(Language::new(
15180 LanguageConfig {
15181 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15182 ..Default::default()
15183 },
15184 Some(tree_sitter_rust::LANGUAGE.into()),
15185 ));
15186 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15187
15188 // If multiple selections intersect a line, the line is only toggled once.
15189 cx.set_state(indoc! {"
15190 fn a() {
15191 «//b();
15192 ˇ»// «c();
15193 //ˇ» d();
15194 }
15195 "});
15196
15197 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15198
15199 cx.assert_editor_state(indoc! {"
15200 fn a() {
15201 «b();
15202 c();
15203 ˇ» d();
15204 }
15205 "});
15206
15207 // The comment prefix is inserted at the same column for every line in a
15208 // selection.
15209 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15210
15211 cx.assert_editor_state(indoc! {"
15212 fn a() {
15213 // «b();
15214 // c();
15215 ˇ»// d();
15216 }
15217 "});
15218
15219 // If a selection ends at the beginning of a line, that line is not toggled.
15220 cx.set_selections_state(indoc! {"
15221 fn a() {
15222 // b();
15223 «// c();
15224 ˇ» // d();
15225 }
15226 "});
15227
15228 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15229
15230 cx.assert_editor_state(indoc! {"
15231 fn a() {
15232 // b();
15233 «c();
15234 ˇ» // d();
15235 }
15236 "});
15237
15238 // If a selection span a single line and is empty, the line is toggled.
15239 cx.set_state(indoc! {"
15240 fn a() {
15241 a();
15242 b();
15243 ˇ
15244 }
15245 "});
15246
15247 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15248
15249 cx.assert_editor_state(indoc! {"
15250 fn a() {
15251 a();
15252 b();
15253 //•ˇ
15254 }
15255 "});
15256
15257 // If a selection span multiple lines, empty lines are not toggled.
15258 cx.set_state(indoc! {"
15259 fn a() {
15260 «a();
15261
15262 c();ˇ»
15263 }
15264 "});
15265
15266 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15267
15268 cx.assert_editor_state(indoc! {"
15269 fn a() {
15270 // «a();
15271
15272 // c();ˇ»
15273 }
15274 "});
15275
15276 // If a selection includes multiple comment prefixes, all lines are uncommented.
15277 cx.set_state(indoc! {"
15278 fn a() {
15279 «// a();
15280 /// b();
15281 //! c();ˇ»
15282 }
15283 "});
15284
15285 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15286
15287 cx.assert_editor_state(indoc! {"
15288 fn a() {
15289 «a();
15290 b();
15291 c();ˇ»
15292 }
15293 "});
15294}
15295
15296#[gpui::test]
15297async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
15298 init_test(cx, |_| {});
15299 let mut cx = EditorTestContext::new(cx).await;
15300 let language = Arc::new(Language::new(
15301 LanguageConfig {
15302 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15303 ..Default::default()
15304 },
15305 Some(tree_sitter_rust::LANGUAGE.into()),
15306 ));
15307 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15308
15309 let toggle_comments = &ToggleComments {
15310 advance_downwards: false,
15311 ignore_indent: true,
15312 };
15313
15314 // If multiple selections intersect a line, the line is only toggled once.
15315 cx.set_state(indoc! {"
15316 fn a() {
15317 // «b();
15318 // c();
15319 // ˇ» d();
15320 }
15321 "});
15322
15323 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15324
15325 cx.assert_editor_state(indoc! {"
15326 fn a() {
15327 «b();
15328 c();
15329 ˇ» d();
15330 }
15331 "});
15332
15333 // The comment prefix is inserted at the beginning of each line
15334 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15335
15336 cx.assert_editor_state(indoc! {"
15337 fn a() {
15338 // «b();
15339 // c();
15340 // ˇ» d();
15341 }
15342 "});
15343
15344 // If a selection ends at the beginning of a line, that line is not toggled.
15345 cx.set_selections_state(indoc! {"
15346 fn a() {
15347 // b();
15348 // «c();
15349 ˇ»// d();
15350 }
15351 "});
15352
15353 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15354
15355 cx.assert_editor_state(indoc! {"
15356 fn a() {
15357 // b();
15358 «c();
15359 ˇ»// d();
15360 }
15361 "});
15362
15363 // If a selection span a single line and is empty, the line is toggled.
15364 cx.set_state(indoc! {"
15365 fn a() {
15366 a();
15367 b();
15368 ˇ
15369 }
15370 "});
15371
15372 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15373
15374 cx.assert_editor_state(indoc! {"
15375 fn a() {
15376 a();
15377 b();
15378 //ˇ
15379 }
15380 "});
15381
15382 // If a selection span multiple lines, empty lines are not toggled.
15383 cx.set_state(indoc! {"
15384 fn a() {
15385 «a();
15386
15387 c();ˇ»
15388 }
15389 "});
15390
15391 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15392
15393 cx.assert_editor_state(indoc! {"
15394 fn a() {
15395 // «a();
15396
15397 // c();ˇ»
15398 }
15399 "});
15400
15401 // If a selection includes multiple comment prefixes, all lines are uncommented.
15402 cx.set_state(indoc! {"
15403 fn a() {
15404 // «a();
15405 /// b();
15406 //! c();ˇ»
15407 }
15408 "});
15409
15410 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15411
15412 cx.assert_editor_state(indoc! {"
15413 fn a() {
15414 «a();
15415 b();
15416 c();ˇ»
15417 }
15418 "});
15419}
15420
15421#[gpui::test]
15422async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
15423 init_test(cx, |_| {});
15424
15425 let language = Arc::new(Language::new(
15426 LanguageConfig {
15427 line_comments: vec!["// ".into()],
15428 ..Default::default()
15429 },
15430 Some(tree_sitter_rust::LANGUAGE.into()),
15431 ));
15432
15433 let mut cx = EditorTestContext::new(cx).await;
15434
15435 cx.language_registry().add(language.clone());
15436 cx.update_buffer(|buffer, cx| {
15437 buffer.set_language(Some(language), cx);
15438 });
15439
15440 let toggle_comments = &ToggleComments {
15441 advance_downwards: true,
15442 ignore_indent: false,
15443 };
15444
15445 // Single cursor on one line -> advance
15446 // Cursor moves horizontally 3 characters as well on non-blank line
15447 cx.set_state(indoc!(
15448 "fn a() {
15449 ˇdog();
15450 cat();
15451 }"
15452 ));
15453 cx.update_editor(|editor, window, cx| {
15454 editor.toggle_comments(toggle_comments, window, cx);
15455 });
15456 cx.assert_editor_state(indoc!(
15457 "fn a() {
15458 // dog();
15459 catˇ();
15460 }"
15461 ));
15462
15463 // Single selection on one line -> don't advance
15464 cx.set_state(indoc!(
15465 "fn a() {
15466 «dog()ˇ»;
15467 cat();
15468 }"
15469 ));
15470 cx.update_editor(|editor, window, cx| {
15471 editor.toggle_comments(toggle_comments, window, cx);
15472 });
15473 cx.assert_editor_state(indoc!(
15474 "fn a() {
15475 // «dog()ˇ»;
15476 cat();
15477 }"
15478 ));
15479
15480 // Multiple cursors on one line -> advance
15481 cx.set_state(indoc!(
15482 "fn a() {
15483 ˇdˇog();
15484 cat();
15485 }"
15486 ));
15487 cx.update_editor(|editor, window, cx| {
15488 editor.toggle_comments(toggle_comments, window, cx);
15489 });
15490 cx.assert_editor_state(indoc!(
15491 "fn a() {
15492 // dog();
15493 catˇ(ˇ);
15494 }"
15495 ));
15496
15497 // Multiple cursors on one line, with selection -> don't advance
15498 cx.set_state(indoc!(
15499 "fn a() {
15500 ˇdˇog«()ˇ»;
15501 cat();
15502 }"
15503 ));
15504 cx.update_editor(|editor, window, cx| {
15505 editor.toggle_comments(toggle_comments, window, cx);
15506 });
15507 cx.assert_editor_state(indoc!(
15508 "fn a() {
15509 // ˇdˇog«()ˇ»;
15510 cat();
15511 }"
15512 ));
15513
15514 // Single cursor on one line -> advance
15515 // Cursor moves to column 0 on blank line
15516 cx.set_state(indoc!(
15517 "fn a() {
15518 ˇdog();
15519
15520 cat();
15521 }"
15522 ));
15523 cx.update_editor(|editor, window, cx| {
15524 editor.toggle_comments(toggle_comments, window, cx);
15525 });
15526 cx.assert_editor_state(indoc!(
15527 "fn a() {
15528 // dog();
15529 ˇ
15530 cat();
15531 }"
15532 ));
15533
15534 // Single cursor on one line -> advance
15535 // Cursor starts and ends at column 0
15536 cx.set_state(indoc!(
15537 "fn a() {
15538 ˇ dog();
15539 cat();
15540 }"
15541 ));
15542 cx.update_editor(|editor, window, cx| {
15543 editor.toggle_comments(toggle_comments, window, cx);
15544 });
15545 cx.assert_editor_state(indoc!(
15546 "fn a() {
15547 // dog();
15548 ˇ cat();
15549 }"
15550 ));
15551}
15552
15553#[gpui::test]
15554async fn test_toggle_block_comment(cx: &mut TestAppContext) {
15555 init_test(cx, |_| {});
15556
15557 let mut cx = EditorTestContext::new(cx).await;
15558
15559 let html_language = Arc::new(
15560 Language::new(
15561 LanguageConfig {
15562 name: "HTML".into(),
15563 block_comment: Some(BlockCommentConfig {
15564 start: "<!-- ".into(),
15565 prefix: "".into(),
15566 end: " -->".into(),
15567 tab_size: 0,
15568 }),
15569 ..Default::default()
15570 },
15571 Some(tree_sitter_html::LANGUAGE.into()),
15572 )
15573 .with_injection_query(
15574 r#"
15575 (script_element
15576 (raw_text) @injection.content
15577 (#set! injection.language "javascript"))
15578 "#,
15579 )
15580 .unwrap(),
15581 );
15582
15583 let javascript_language = Arc::new(Language::new(
15584 LanguageConfig {
15585 name: "JavaScript".into(),
15586 line_comments: vec!["// ".into()],
15587 ..Default::default()
15588 },
15589 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
15590 ));
15591
15592 cx.language_registry().add(html_language.clone());
15593 cx.language_registry().add(javascript_language);
15594 cx.update_buffer(|buffer, cx| {
15595 buffer.set_language(Some(html_language), cx);
15596 });
15597
15598 // Toggle comments for empty selections
15599 cx.set_state(
15600 &r#"
15601 <p>A</p>ˇ
15602 <p>B</p>ˇ
15603 <p>C</p>ˇ
15604 "#
15605 .unindent(),
15606 );
15607 cx.update_editor(|editor, window, cx| {
15608 editor.toggle_comments(&ToggleComments::default(), window, cx)
15609 });
15610 cx.assert_editor_state(
15611 &r#"
15612 <!-- <p>A</p>ˇ -->
15613 <!-- <p>B</p>ˇ -->
15614 <!-- <p>C</p>ˇ -->
15615 "#
15616 .unindent(),
15617 );
15618 cx.update_editor(|editor, window, cx| {
15619 editor.toggle_comments(&ToggleComments::default(), window, cx)
15620 });
15621 cx.assert_editor_state(
15622 &r#"
15623 <p>A</p>ˇ
15624 <p>B</p>ˇ
15625 <p>C</p>ˇ
15626 "#
15627 .unindent(),
15628 );
15629
15630 // Toggle comments for mixture of empty and non-empty selections, where
15631 // multiple selections occupy a given line.
15632 cx.set_state(
15633 &r#"
15634 <p>A«</p>
15635 <p>ˇ»B</p>ˇ
15636 <p>C«</p>
15637 <p>ˇ»D</p>ˇ
15638 "#
15639 .unindent(),
15640 );
15641
15642 cx.update_editor(|editor, window, cx| {
15643 editor.toggle_comments(&ToggleComments::default(), window, cx)
15644 });
15645 cx.assert_editor_state(
15646 &r#"
15647 <!-- <p>A«</p>
15648 <p>ˇ»B</p>ˇ -->
15649 <!-- <p>C«</p>
15650 <p>ˇ»D</p>ˇ -->
15651 "#
15652 .unindent(),
15653 );
15654 cx.update_editor(|editor, window, cx| {
15655 editor.toggle_comments(&ToggleComments::default(), window, cx)
15656 });
15657 cx.assert_editor_state(
15658 &r#"
15659 <p>A«</p>
15660 <p>ˇ»B</p>ˇ
15661 <p>C«</p>
15662 <p>ˇ»D</p>ˇ
15663 "#
15664 .unindent(),
15665 );
15666
15667 // Toggle comments when different languages are active for different
15668 // selections.
15669 cx.set_state(
15670 &r#"
15671 ˇ<script>
15672 ˇvar x = new Y();
15673 ˇ</script>
15674 "#
15675 .unindent(),
15676 );
15677 cx.executor().run_until_parked();
15678 cx.update_editor(|editor, window, cx| {
15679 editor.toggle_comments(&ToggleComments::default(), window, cx)
15680 });
15681 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
15682 // Uncommenting and commenting from this position brings in even more wrong artifacts.
15683 cx.assert_editor_state(
15684 &r#"
15685 <!-- ˇ<script> -->
15686 // ˇvar x = new Y();
15687 <!-- ˇ</script> -->
15688 "#
15689 .unindent(),
15690 );
15691}
15692
15693#[gpui::test]
15694fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
15695 init_test(cx, |_| {});
15696
15697 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
15698 let multibuffer = cx.new(|cx| {
15699 let mut multibuffer = MultiBuffer::new(ReadWrite);
15700 multibuffer.push_excerpts(
15701 buffer.clone(),
15702 [
15703 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
15704 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
15705 ],
15706 cx,
15707 );
15708 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
15709 multibuffer
15710 });
15711
15712 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
15713 editor.update_in(cx, |editor, window, cx| {
15714 assert_eq!(editor.text(cx), "aaaa\nbbbb");
15715 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15716 s.select_ranges([
15717 Point::new(0, 0)..Point::new(0, 0),
15718 Point::new(1, 0)..Point::new(1, 0),
15719 ])
15720 });
15721
15722 editor.handle_input("X", window, cx);
15723 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
15724 assert_eq!(
15725 editor.selections.ranges(cx),
15726 [
15727 Point::new(0, 1)..Point::new(0, 1),
15728 Point::new(1, 1)..Point::new(1, 1),
15729 ]
15730 );
15731
15732 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
15733 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15734 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
15735 });
15736 editor.backspace(&Default::default(), window, cx);
15737 assert_eq!(editor.text(cx), "Xa\nbbb");
15738 assert_eq!(
15739 editor.selections.ranges(cx),
15740 [Point::new(1, 0)..Point::new(1, 0)]
15741 );
15742
15743 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15744 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
15745 });
15746 editor.backspace(&Default::default(), window, cx);
15747 assert_eq!(editor.text(cx), "X\nbb");
15748 assert_eq!(
15749 editor.selections.ranges(cx),
15750 [Point::new(0, 1)..Point::new(0, 1)]
15751 );
15752 });
15753}
15754
15755#[gpui::test]
15756fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
15757 init_test(cx, |_| {});
15758
15759 let markers = vec![('[', ']').into(), ('(', ')').into()];
15760 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
15761 indoc! {"
15762 [aaaa
15763 (bbbb]
15764 cccc)",
15765 },
15766 markers.clone(),
15767 );
15768 let excerpt_ranges = markers.into_iter().map(|marker| {
15769 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
15770 ExcerptRange::new(context)
15771 });
15772 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
15773 let multibuffer = cx.new(|cx| {
15774 let mut multibuffer = MultiBuffer::new(ReadWrite);
15775 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
15776 multibuffer
15777 });
15778
15779 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
15780 editor.update_in(cx, |editor, window, cx| {
15781 let (expected_text, selection_ranges) = marked_text_ranges(
15782 indoc! {"
15783 aaaa
15784 bˇbbb
15785 bˇbbˇb
15786 cccc"
15787 },
15788 true,
15789 );
15790 assert_eq!(editor.text(cx), expected_text);
15791 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15792 s.select_ranges(selection_ranges)
15793 });
15794
15795 editor.handle_input("X", window, cx);
15796
15797 let (expected_text, expected_selections) = marked_text_ranges(
15798 indoc! {"
15799 aaaa
15800 bXˇbbXb
15801 bXˇbbXˇb
15802 cccc"
15803 },
15804 false,
15805 );
15806 assert_eq!(editor.text(cx), expected_text);
15807 assert_eq!(editor.selections.ranges(cx), expected_selections);
15808
15809 editor.newline(&Newline, window, cx);
15810 let (expected_text, expected_selections) = marked_text_ranges(
15811 indoc! {"
15812 aaaa
15813 bX
15814 ˇbbX
15815 b
15816 bX
15817 ˇbbX
15818 ˇb
15819 cccc"
15820 },
15821 false,
15822 );
15823 assert_eq!(editor.text(cx), expected_text);
15824 assert_eq!(editor.selections.ranges(cx), expected_selections);
15825 });
15826}
15827
15828#[gpui::test]
15829fn test_refresh_selections(cx: &mut TestAppContext) {
15830 init_test(cx, |_| {});
15831
15832 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
15833 let mut excerpt1_id = None;
15834 let multibuffer = cx.new(|cx| {
15835 let mut multibuffer = MultiBuffer::new(ReadWrite);
15836 excerpt1_id = multibuffer
15837 .push_excerpts(
15838 buffer.clone(),
15839 [
15840 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
15841 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
15842 ],
15843 cx,
15844 )
15845 .into_iter()
15846 .next();
15847 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
15848 multibuffer
15849 });
15850
15851 let editor = cx.add_window(|window, cx| {
15852 let mut editor = build_editor(multibuffer.clone(), window, cx);
15853 let snapshot = editor.snapshot(window, cx);
15854 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15855 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
15856 });
15857 editor.begin_selection(
15858 Point::new(2, 1).to_display_point(&snapshot),
15859 true,
15860 1,
15861 window,
15862 cx,
15863 );
15864 assert_eq!(
15865 editor.selections.ranges(cx),
15866 [
15867 Point::new(1, 3)..Point::new(1, 3),
15868 Point::new(2, 1)..Point::new(2, 1),
15869 ]
15870 );
15871 editor
15872 });
15873
15874 // Refreshing selections is a no-op when excerpts haven't changed.
15875 _ = editor.update(cx, |editor, window, cx| {
15876 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
15877 assert_eq!(
15878 editor.selections.ranges(cx),
15879 [
15880 Point::new(1, 3)..Point::new(1, 3),
15881 Point::new(2, 1)..Point::new(2, 1),
15882 ]
15883 );
15884 });
15885
15886 multibuffer.update(cx, |multibuffer, cx| {
15887 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
15888 });
15889 _ = editor.update(cx, |editor, window, cx| {
15890 // Removing an excerpt causes the first selection to become degenerate.
15891 assert_eq!(
15892 editor.selections.ranges(cx),
15893 [
15894 Point::new(0, 0)..Point::new(0, 0),
15895 Point::new(0, 1)..Point::new(0, 1)
15896 ]
15897 );
15898
15899 // Refreshing selections will relocate the first selection to the original buffer
15900 // location.
15901 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
15902 assert_eq!(
15903 editor.selections.ranges(cx),
15904 [
15905 Point::new(0, 1)..Point::new(0, 1),
15906 Point::new(0, 3)..Point::new(0, 3)
15907 ]
15908 );
15909 assert!(editor.selections.pending_anchor().is_some());
15910 });
15911}
15912
15913#[gpui::test]
15914fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
15915 init_test(cx, |_| {});
15916
15917 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
15918 let mut excerpt1_id = None;
15919 let multibuffer = cx.new(|cx| {
15920 let mut multibuffer = MultiBuffer::new(ReadWrite);
15921 excerpt1_id = multibuffer
15922 .push_excerpts(
15923 buffer.clone(),
15924 [
15925 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
15926 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
15927 ],
15928 cx,
15929 )
15930 .into_iter()
15931 .next();
15932 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
15933 multibuffer
15934 });
15935
15936 let editor = cx.add_window(|window, cx| {
15937 let mut editor = build_editor(multibuffer.clone(), window, cx);
15938 let snapshot = editor.snapshot(window, cx);
15939 editor.begin_selection(
15940 Point::new(1, 3).to_display_point(&snapshot),
15941 false,
15942 1,
15943 window,
15944 cx,
15945 );
15946 assert_eq!(
15947 editor.selections.ranges(cx),
15948 [Point::new(1, 3)..Point::new(1, 3)]
15949 );
15950 editor
15951 });
15952
15953 multibuffer.update(cx, |multibuffer, cx| {
15954 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
15955 });
15956 _ = editor.update(cx, |editor, window, cx| {
15957 assert_eq!(
15958 editor.selections.ranges(cx),
15959 [Point::new(0, 0)..Point::new(0, 0)]
15960 );
15961
15962 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
15963 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
15964 assert_eq!(
15965 editor.selections.ranges(cx),
15966 [Point::new(0, 3)..Point::new(0, 3)]
15967 );
15968 assert!(editor.selections.pending_anchor().is_some());
15969 });
15970}
15971
15972#[gpui::test]
15973async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
15974 init_test(cx, |_| {});
15975
15976 let language = Arc::new(
15977 Language::new(
15978 LanguageConfig {
15979 brackets: BracketPairConfig {
15980 pairs: vec![
15981 BracketPair {
15982 start: "{".to_string(),
15983 end: "}".to_string(),
15984 close: true,
15985 surround: true,
15986 newline: true,
15987 },
15988 BracketPair {
15989 start: "/* ".to_string(),
15990 end: " */".to_string(),
15991 close: true,
15992 surround: true,
15993 newline: true,
15994 },
15995 ],
15996 ..Default::default()
15997 },
15998 ..Default::default()
15999 },
16000 Some(tree_sitter_rust::LANGUAGE.into()),
16001 )
16002 .with_indents_query("")
16003 .unwrap(),
16004 );
16005
16006 let text = concat!(
16007 "{ }\n", //
16008 " x\n", //
16009 " /* */\n", //
16010 "x\n", //
16011 "{{} }\n", //
16012 );
16013
16014 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
16015 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
16016 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
16017 editor
16018 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
16019 .await;
16020
16021 editor.update_in(cx, |editor, window, cx| {
16022 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16023 s.select_display_ranges([
16024 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
16025 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
16026 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
16027 ])
16028 });
16029 editor.newline(&Newline, window, cx);
16030
16031 assert_eq!(
16032 editor.buffer().read(cx).read(cx).text(),
16033 concat!(
16034 "{ \n", // Suppress rustfmt
16035 "\n", //
16036 "}\n", //
16037 " x\n", //
16038 " /* \n", //
16039 " \n", //
16040 " */\n", //
16041 "x\n", //
16042 "{{} \n", //
16043 "}\n", //
16044 )
16045 );
16046 });
16047}
16048
16049#[gpui::test]
16050fn test_highlighted_ranges(cx: &mut TestAppContext) {
16051 init_test(cx, |_| {});
16052
16053 let editor = cx.add_window(|window, cx| {
16054 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
16055 build_editor(buffer, window, cx)
16056 });
16057
16058 _ = editor.update(cx, |editor, window, cx| {
16059 struct Type1;
16060 struct Type2;
16061
16062 let buffer = editor.buffer.read(cx).snapshot(cx);
16063
16064 let anchor_range =
16065 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
16066
16067 editor.highlight_background::<Type1>(
16068 &[
16069 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
16070 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
16071 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
16072 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
16073 ],
16074 |_| Hsla::red(),
16075 cx,
16076 );
16077 editor.highlight_background::<Type2>(
16078 &[
16079 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
16080 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
16081 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
16082 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
16083 ],
16084 |_| Hsla::green(),
16085 cx,
16086 );
16087
16088 let snapshot = editor.snapshot(window, cx);
16089 let highlighted_ranges = editor.sorted_background_highlights_in_range(
16090 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
16091 &snapshot,
16092 cx.theme(),
16093 );
16094 assert_eq!(
16095 highlighted_ranges,
16096 &[
16097 (
16098 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
16099 Hsla::green(),
16100 ),
16101 (
16102 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
16103 Hsla::red(),
16104 ),
16105 (
16106 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
16107 Hsla::green(),
16108 ),
16109 (
16110 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16111 Hsla::red(),
16112 ),
16113 ]
16114 );
16115 assert_eq!(
16116 editor.sorted_background_highlights_in_range(
16117 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
16118 &snapshot,
16119 cx.theme(),
16120 ),
16121 &[(
16122 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16123 Hsla::red(),
16124 )]
16125 );
16126 });
16127}
16128
16129#[gpui::test]
16130async fn test_following(cx: &mut TestAppContext) {
16131 init_test(cx, |_| {});
16132
16133 let fs = FakeFs::new(cx.executor());
16134 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16135
16136 let buffer = project.update(cx, |project, cx| {
16137 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, false, cx);
16138 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
16139 });
16140 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
16141 let follower = cx.update(|cx| {
16142 cx.open_window(
16143 WindowOptions {
16144 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
16145 gpui::Point::new(px(0.), px(0.)),
16146 gpui::Point::new(px(10.), px(80.)),
16147 ))),
16148 ..Default::default()
16149 },
16150 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
16151 )
16152 .unwrap()
16153 });
16154
16155 let is_still_following = Rc::new(RefCell::new(true));
16156 let follower_edit_event_count = Rc::new(RefCell::new(0));
16157 let pending_update = Rc::new(RefCell::new(None));
16158 let leader_entity = leader.root(cx).unwrap();
16159 let follower_entity = follower.root(cx).unwrap();
16160 _ = follower.update(cx, {
16161 let update = pending_update.clone();
16162 let is_still_following = is_still_following.clone();
16163 let follower_edit_event_count = follower_edit_event_count.clone();
16164 |_, window, cx| {
16165 cx.subscribe_in(
16166 &leader_entity,
16167 window,
16168 move |_, leader, event, window, cx| {
16169 leader.read(cx).add_event_to_update_proto(
16170 event,
16171 &mut update.borrow_mut(),
16172 window,
16173 cx,
16174 );
16175 },
16176 )
16177 .detach();
16178
16179 cx.subscribe_in(
16180 &follower_entity,
16181 window,
16182 move |_, _, event: &EditorEvent, _window, _cx| {
16183 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
16184 *is_still_following.borrow_mut() = false;
16185 }
16186
16187 if let EditorEvent::BufferEdited = event {
16188 *follower_edit_event_count.borrow_mut() += 1;
16189 }
16190 },
16191 )
16192 .detach();
16193 }
16194 });
16195
16196 // Update the selections only
16197 _ = leader.update(cx, |leader, window, cx| {
16198 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16199 s.select_ranges([1..1])
16200 });
16201 });
16202 follower
16203 .update(cx, |follower, window, cx| {
16204 follower.apply_update_proto(
16205 &project,
16206 pending_update.borrow_mut().take().unwrap(),
16207 window,
16208 cx,
16209 )
16210 })
16211 .unwrap()
16212 .await
16213 .unwrap();
16214 _ = follower.update(cx, |follower, _, cx| {
16215 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
16216 });
16217 assert!(*is_still_following.borrow());
16218 assert_eq!(*follower_edit_event_count.borrow(), 0);
16219
16220 // Update the scroll position only
16221 _ = leader.update(cx, |leader, window, cx| {
16222 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16223 });
16224 follower
16225 .update(cx, |follower, window, cx| {
16226 follower.apply_update_proto(
16227 &project,
16228 pending_update.borrow_mut().take().unwrap(),
16229 window,
16230 cx,
16231 )
16232 })
16233 .unwrap()
16234 .await
16235 .unwrap();
16236 assert_eq!(
16237 follower
16238 .update(cx, |follower, _, cx| follower.scroll_position(cx))
16239 .unwrap(),
16240 gpui::Point::new(1.5, 3.5)
16241 );
16242 assert!(*is_still_following.borrow());
16243 assert_eq!(*follower_edit_event_count.borrow(), 0);
16244
16245 // Update the selections and scroll position. The follower's scroll position is updated
16246 // via autoscroll, not via the leader's exact scroll position.
16247 _ = leader.update(cx, |leader, window, cx| {
16248 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16249 s.select_ranges([0..0])
16250 });
16251 leader.request_autoscroll(Autoscroll::newest(), cx);
16252 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16253 });
16254 follower
16255 .update(cx, |follower, window, cx| {
16256 follower.apply_update_proto(
16257 &project,
16258 pending_update.borrow_mut().take().unwrap(),
16259 window,
16260 cx,
16261 )
16262 })
16263 .unwrap()
16264 .await
16265 .unwrap();
16266 _ = follower.update(cx, |follower, _, cx| {
16267 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
16268 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
16269 });
16270 assert!(*is_still_following.borrow());
16271
16272 // Creating a pending selection that precedes another selection
16273 _ = leader.update(cx, |leader, window, cx| {
16274 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16275 s.select_ranges([1..1])
16276 });
16277 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
16278 });
16279 follower
16280 .update(cx, |follower, window, cx| {
16281 follower.apply_update_proto(
16282 &project,
16283 pending_update.borrow_mut().take().unwrap(),
16284 window,
16285 cx,
16286 )
16287 })
16288 .unwrap()
16289 .await
16290 .unwrap();
16291 _ = follower.update(cx, |follower, _, cx| {
16292 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
16293 });
16294 assert!(*is_still_following.borrow());
16295
16296 // Extend the pending selection so that it surrounds another selection
16297 _ = leader.update(cx, |leader, window, cx| {
16298 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
16299 });
16300 follower
16301 .update(cx, |follower, window, cx| {
16302 follower.apply_update_proto(
16303 &project,
16304 pending_update.borrow_mut().take().unwrap(),
16305 window,
16306 cx,
16307 )
16308 })
16309 .unwrap()
16310 .await
16311 .unwrap();
16312 _ = follower.update(cx, |follower, _, cx| {
16313 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
16314 });
16315
16316 // Scrolling locally breaks the follow
16317 _ = follower.update(cx, |follower, window, cx| {
16318 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
16319 follower.set_scroll_anchor(
16320 ScrollAnchor {
16321 anchor: top_anchor,
16322 offset: gpui::Point::new(0.0, 0.5),
16323 },
16324 window,
16325 cx,
16326 );
16327 });
16328 assert!(!(*is_still_following.borrow()));
16329}
16330
16331#[gpui::test]
16332async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
16333 init_test(cx, |_| {});
16334
16335 let fs = FakeFs::new(cx.executor());
16336 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16337 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16338 let pane = workspace
16339 .update(cx, |workspace, _, _| workspace.active_pane().clone())
16340 .unwrap();
16341
16342 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16343
16344 let leader = pane.update_in(cx, |_, window, cx| {
16345 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
16346 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
16347 });
16348
16349 // Start following the editor when it has no excerpts.
16350 let mut state_message =
16351 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16352 let workspace_entity = workspace.root(cx).unwrap();
16353 let follower_1 = cx
16354 .update_window(*workspace.deref(), |_, window, cx| {
16355 Editor::from_state_proto(
16356 workspace_entity,
16357 ViewId {
16358 creator: CollaboratorId::PeerId(PeerId::default()),
16359 id: 0,
16360 },
16361 &mut state_message,
16362 window,
16363 cx,
16364 )
16365 })
16366 .unwrap()
16367 .unwrap()
16368 .await
16369 .unwrap();
16370
16371 let update_message = Rc::new(RefCell::new(None));
16372 follower_1.update_in(cx, {
16373 let update = update_message.clone();
16374 |_, window, cx| {
16375 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
16376 leader.read(cx).add_event_to_update_proto(
16377 event,
16378 &mut update.borrow_mut(),
16379 window,
16380 cx,
16381 );
16382 })
16383 .detach();
16384 }
16385 });
16386
16387 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
16388 (
16389 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, false, cx),
16390 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, false, cx),
16391 )
16392 });
16393
16394 // Insert some excerpts.
16395 leader.update(cx, |leader, cx| {
16396 leader.buffer.update(cx, |multibuffer, cx| {
16397 multibuffer.set_excerpts_for_path(
16398 PathKey::namespaced(1, "b.txt".into()),
16399 buffer_1.clone(),
16400 vec![
16401 Point::row_range(0..3),
16402 Point::row_range(1..6),
16403 Point::row_range(12..15),
16404 ],
16405 0,
16406 cx,
16407 );
16408 multibuffer.set_excerpts_for_path(
16409 PathKey::namespaced(1, "a.txt".into()),
16410 buffer_2.clone(),
16411 vec![Point::row_range(0..6), Point::row_range(8..12)],
16412 0,
16413 cx,
16414 );
16415 });
16416 });
16417
16418 // Apply the update of adding the excerpts.
16419 follower_1
16420 .update_in(cx, |follower, window, cx| {
16421 follower.apply_update_proto(
16422 &project,
16423 update_message.borrow().clone().unwrap(),
16424 window,
16425 cx,
16426 )
16427 })
16428 .await
16429 .unwrap();
16430 assert_eq!(
16431 follower_1.update(cx, |editor, cx| editor.text(cx)),
16432 leader.update(cx, |editor, cx| editor.text(cx))
16433 );
16434 update_message.borrow_mut().take();
16435
16436 // Start following separately after it already has excerpts.
16437 let mut state_message =
16438 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16439 let workspace_entity = workspace.root(cx).unwrap();
16440 let follower_2 = cx
16441 .update_window(*workspace.deref(), |_, window, cx| {
16442 Editor::from_state_proto(
16443 workspace_entity,
16444 ViewId {
16445 creator: CollaboratorId::PeerId(PeerId::default()),
16446 id: 0,
16447 },
16448 &mut state_message,
16449 window,
16450 cx,
16451 )
16452 })
16453 .unwrap()
16454 .unwrap()
16455 .await
16456 .unwrap();
16457 assert_eq!(
16458 follower_2.update(cx, |editor, cx| editor.text(cx)),
16459 leader.update(cx, |editor, cx| editor.text(cx))
16460 );
16461
16462 // Remove some excerpts.
16463 leader.update(cx, |leader, cx| {
16464 leader.buffer.update(cx, |multibuffer, cx| {
16465 let excerpt_ids = multibuffer.excerpt_ids();
16466 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
16467 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
16468 });
16469 });
16470
16471 // Apply the update of removing the excerpts.
16472 follower_1
16473 .update_in(cx, |follower, window, cx| {
16474 follower.apply_update_proto(
16475 &project,
16476 update_message.borrow().clone().unwrap(),
16477 window,
16478 cx,
16479 )
16480 })
16481 .await
16482 .unwrap();
16483 follower_2
16484 .update_in(cx, |follower, window, cx| {
16485 follower.apply_update_proto(
16486 &project,
16487 update_message.borrow().clone().unwrap(),
16488 window,
16489 cx,
16490 )
16491 })
16492 .await
16493 .unwrap();
16494 update_message.borrow_mut().take();
16495 assert_eq!(
16496 follower_1.update(cx, |editor, cx| editor.text(cx)),
16497 leader.update(cx, |editor, cx| editor.text(cx))
16498 );
16499}
16500
16501#[gpui::test]
16502async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16503 init_test(cx, |_| {});
16504
16505 let mut cx = EditorTestContext::new(cx).await;
16506 let lsp_store =
16507 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
16508
16509 cx.set_state(indoc! {"
16510 ˇfn func(abc def: i32) -> u32 {
16511 }
16512 "});
16513
16514 cx.update(|_, cx| {
16515 lsp_store.update(cx, |lsp_store, cx| {
16516 lsp_store
16517 .update_diagnostics(
16518 LanguageServerId(0),
16519 lsp::PublishDiagnosticsParams {
16520 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
16521 version: None,
16522 diagnostics: vec![
16523 lsp::Diagnostic {
16524 range: lsp::Range::new(
16525 lsp::Position::new(0, 11),
16526 lsp::Position::new(0, 12),
16527 ),
16528 severity: Some(lsp::DiagnosticSeverity::ERROR),
16529 ..Default::default()
16530 },
16531 lsp::Diagnostic {
16532 range: lsp::Range::new(
16533 lsp::Position::new(0, 12),
16534 lsp::Position::new(0, 15),
16535 ),
16536 severity: Some(lsp::DiagnosticSeverity::ERROR),
16537 ..Default::default()
16538 },
16539 lsp::Diagnostic {
16540 range: lsp::Range::new(
16541 lsp::Position::new(0, 25),
16542 lsp::Position::new(0, 28),
16543 ),
16544 severity: Some(lsp::DiagnosticSeverity::ERROR),
16545 ..Default::default()
16546 },
16547 ],
16548 },
16549 None,
16550 DiagnosticSourceKind::Pushed,
16551 &[],
16552 cx,
16553 )
16554 .unwrap()
16555 });
16556 });
16557
16558 executor.run_until_parked();
16559
16560 cx.update_editor(|editor, window, cx| {
16561 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16562 });
16563
16564 cx.assert_editor_state(indoc! {"
16565 fn func(abc def: i32) -> ˇu32 {
16566 }
16567 "});
16568
16569 cx.update_editor(|editor, window, cx| {
16570 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16571 });
16572
16573 cx.assert_editor_state(indoc! {"
16574 fn func(abc ˇdef: i32) -> u32 {
16575 }
16576 "});
16577
16578 cx.update_editor(|editor, window, cx| {
16579 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16580 });
16581
16582 cx.assert_editor_state(indoc! {"
16583 fn func(abcˇ def: i32) -> u32 {
16584 }
16585 "});
16586
16587 cx.update_editor(|editor, window, cx| {
16588 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16589 });
16590
16591 cx.assert_editor_state(indoc! {"
16592 fn func(abc def: i32) -> ˇu32 {
16593 }
16594 "});
16595}
16596
16597#[gpui::test]
16598async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16599 init_test(cx, |_| {});
16600
16601 let mut cx = EditorTestContext::new(cx).await;
16602
16603 let diff_base = r#"
16604 use some::mod;
16605
16606 const A: u32 = 42;
16607
16608 fn main() {
16609 println!("hello");
16610
16611 println!("world");
16612 }
16613 "#
16614 .unindent();
16615
16616 // Edits are modified, removed, modified, added
16617 cx.set_state(
16618 &r#"
16619 use some::modified;
16620
16621 ˇ
16622 fn main() {
16623 println!("hello there");
16624
16625 println!("around the");
16626 println!("world");
16627 }
16628 "#
16629 .unindent(),
16630 );
16631
16632 cx.set_head_text(&diff_base);
16633 executor.run_until_parked();
16634
16635 cx.update_editor(|editor, window, cx| {
16636 //Wrap around the bottom of the buffer
16637 for _ in 0..3 {
16638 editor.go_to_next_hunk(&GoToHunk, window, cx);
16639 }
16640 });
16641
16642 cx.assert_editor_state(
16643 &r#"
16644 ˇuse some::modified;
16645
16646
16647 fn main() {
16648 println!("hello there");
16649
16650 println!("around the");
16651 println!("world");
16652 }
16653 "#
16654 .unindent(),
16655 );
16656
16657 cx.update_editor(|editor, window, cx| {
16658 //Wrap around the top of the buffer
16659 for _ in 0..2 {
16660 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16661 }
16662 });
16663
16664 cx.assert_editor_state(
16665 &r#"
16666 use some::modified;
16667
16668
16669 fn main() {
16670 ˇ println!("hello there");
16671
16672 println!("around the");
16673 println!("world");
16674 }
16675 "#
16676 .unindent(),
16677 );
16678
16679 cx.update_editor(|editor, window, cx| {
16680 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16681 });
16682
16683 cx.assert_editor_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.update_editor(|editor, window, cx| {
16699 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16700 });
16701
16702 cx.assert_editor_state(
16703 &r#"
16704 ˇuse some::modified;
16705
16706
16707 fn main() {
16708 println!("hello there");
16709
16710 println!("around the");
16711 println!("world");
16712 }
16713 "#
16714 .unindent(),
16715 );
16716
16717 cx.update_editor(|editor, window, cx| {
16718 for _ in 0..2 {
16719 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16720 }
16721 });
16722
16723 cx.assert_editor_state(
16724 &r#"
16725 use some::modified;
16726
16727
16728 fn main() {
16729 ˇ println!("hello there");
16730
16731 println!("around the");
16732 println!("world");
16733 }
16734 "#
16735 .unindent(),
16736 );
16737
16738 cx.update_editor(|editor, window, cx| {
16739 editor.fold(&Fold, window, cx);
16740 });
16741
16742 cx.update_editor(|editor, window, cx| {
16743 editor.go_to_next_hunk(&GoToHunk, window, cx);
16744 });
16745
16746 cx.assert_editor_state(
16747 &r#"
16748 ˇuse some::modified;
16749
16750
16751 fn main() {
16752 println!("hello there");
16753
16754 println!("around the");
16755 println!("world");
16756 }
16757 "#
16758 .unindent(),
16759 );
16760}
16761
16762#[test]
16763fn test_split_words() {
16764 fn split(text: &str) -> Vec<&str> {
16765 split_words(text).collect()
16766 }
16767
16768 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
16769 assert_eq!(split("hello_world"), &["hello_", "world"]);
16770 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
16771 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
16772 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
16773 assert_eq!(split("helloworld"), &["helloworld"]);
16774
16775 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
16776}
16777
16778#[gpui::test]
16779async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
16780 init_test(cx, |_| {});
16781
16782 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
16783 let mut assert = |before, after| {
16784 let _state_context = cx.set_state(before);
16785 cx.run_until_parked();
16786 cx.update_editor(|editor, window, cx| {
16787 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
16788 });
16789 cx.run_until_parked();
16790 cx.assert_editor_state(after);
16791 };
16792
16793 // Outside bracket jumps to outside of matching bracket
16794 assert("console.logˇ(var);", "console.log(var)ˇ;");
16795 assert("console.log(var)ˇ;", "console.logˇ(var);");
16796
16797 // Inside bracket jumps to inside of matching bracket
16798 assert("console.log(ˇvar);", "console.log(varˇ);");
16799 assert("console.log(varˇ);", "console.log(ˇvar);");
16800
16801 // When outside a bracket and inside, favor jumping to the inside bracket
16802 assert(
16803 "console.log('foo', [1, 2, 3]ˇ);",
16804 "console.log(ˇ'foo', [1, 2, 3]);",
16805 );
16806 assert(
16807 "console.log(ˇ'foo', [1, 2, 3]);",
16808 "console.log('foo', [1, 2, 3]ˇ);",
16809 );
16810
16811 // Bias forward if two options are equally likely
16812 assert(
16813 "let result = curried_fun()ˇ();",
16814 "let result = curried_fun()()ˇ;",
16815 );
16816
16817 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
16818 assert(
16819 indoc! {"
16820 function test() {
16821 console.log('test')ˇ
16822 }"},
16823 indoc! {"
16824 function test() {
16825 console.logˇ('test')
16826 }"},
16827 );
16828}
16829
16830#[gpui::test]
16831async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
16832 init_test(cx, |_| {});
16833
16834 let fs = FakeFs::new(cx.executor());
16835 fs.insert_tree(
16836 path!("/a"),
16837 json!({
16838 "main.rs": "fn main() { let a = 5; }",
16839 "other.rs": "// Test file",
16840 }),
16841 )
16842 .await;
16843 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
16844
16845 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
16846 language_registry.add(Arc::new(Language::new(
16847 LanguageConfig {
16848 name: "Rust".into(),
16849 matcher: LanguageMatcher {
16850 path_suffixes: vec!["rs".to_string()],
16851 ..Default::default()
16852 },
16853 brackets: BracketPairConfig {
16854 pairs: vec![BracketPair {
16855 start: "{".to_string(),
16856 end: "}".to_string(),
16857 close: true,
16858 surround: true,
16859 newline: true,
16860 }],
16861 disabled_scopes_by_bracket_ix: Vec::new(),
16862 },
16863 ..Default::default()
16864 },
16865 Some(tree_sitter_rust::LANGUAGE.into()),
16866 )));
16867 let mut fake_servers = language_registry.register_fake_lsp(
16868 "Rust",
16869 FakeLspAdapter {
16870 capabilities: lsp::ServerCapabilities {
16871 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
16872 first_trigger_character: "{".to_string(),
16873 more_trigger_character: None,
16874 }),
16875 ..Default::default()
16876 },
16877 ..Default::default()
16878 },
16879 );
16880
16881 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16882
16883 let cx = &mut VisualTestContext::from_window(*workspace, cx);
16884
16885 let worktree_id = workspace
16886 .update(cx, |workspace, _, cx| {
16887 workspace.project().update(cx, |project, cx| {
16888 project.worktrees(cx).next().unwrap().read(cx).id()
16889 })
16890 })
16891 .unwrap();
16892
16893 let buffer = project
16894 .update(cx, |project, cx| {
16895 project.open_local_buffer(path!("/a/main.rs"), cx)
16896 })
16897 .await
16898 .unwrap();
16899 let editor_handle = workspace
16900 .update(cx, |workspace, window, cx| {
16901 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
16902 })
16903 .unwrap()
16904 .await
16905 .unwrap()
16906 .downcast::<Editor>()
16907 .unwrap();
16908
16909 cx.executor().start_waiting();
16910 let fake_server = fake_servers.next().await.unwrap();
16911
16912 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
16913 |params, _| async move {
16914 assert_eq!(
16915 params.text_document_position.text_document.uri,
16916 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
16917 );
16918 assert_eq!(
16919 params.text_document_position.position,
16920 lsp::Position::new(0, 21),
16921 );
16922
16923 Ok(Some(vec![lsp::TextEdit {
16924 new_text: "]".to_string(),
16925 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
16926 }]))
16927 },
16928 );
16929
16930 editor_handle.update_in(cx, |editor, window, cx| {
16931 window.focus(&editor.focus_handle(cx));
16932 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16933 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
16934 });
16935 editor.handle_input("{", window, cx);
16936 });
16937
16938 cx.executor().run_until_parked();
16939
16940 buffer.update(cx, |buffer, _| {
16941 assert_eq!(
16942 buffer.text(),
16943 "fn main() { let a = {5}; }",
16944 "No extra braces from on type formatting should appear in the buffer"
16945 )
16946 });
16947}
16948
16949#[gpui::test(iterations = 20, seeds(31))]
16950async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
16951 init_test(cx, |_| {});
16952
16953 let mut cx = EditorLspTestContext::new_rust(
16954 lsp::ServerCapabilities {
16955 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
16956 first_trigger_character: ".".to_string(),
16957 more_trigger_character: None,
16958 }),
16959 ..Default::default()
16960 },
16961 cx,
16962 )
16963 .await;
16964
16965 cx.update_buffer(|buffer, _| {
16966 // This causes autoindent to be async.
16967 buffer.set_sync_parse_timeout(Duration::ZERO)
16968 });
16969
16970 cx.set_state("fn c() {\n d()ˇ\n}\n");
16971 cx.simulate_keystroke("\n");
16972 cx.run_until_parked();
16973
16974 let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap());
16975 let mut request =
16976 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
16977 let buffer_cloned = buffer_cloned.clone();
16978 async move {
16979 buffer_cloned.update(&mut cx, |buffer, _| {
16980 assert_eq!(
16981 buffer.text(),
16982 "fn c() {\n d()\n .\n}\n",
16983 "OnTypeFormatting should triggered after autoindent applied"
16984 )
16985 })?;
16986
16987 Ok(Some(vec![]))
16988 }
16989 });
16990
16991 cx.simulate_keystroke(".");
16992 cx.run_until_parked();
16993
16994 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
16995 assert!(request.next().await.is_some());
16996 request.close();
16997 assert!(request.next().await.is_none());
16998}
16999
17000#[gpui::test]
17001async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
17002 init_test(cx, |_| {});
17003
17004 let fs = FakeFs::new(cx.executor());
17005 fs.insert_tree(
17006 path!("/a"),
17007 json!({
17008 "main.rs": "fn main() { let a = 5; }",
17009 "other.rs": "// Test file",
17010 }),
17011 )
17012 .await;
17013
17014 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17015
17016 let server_restarts = Arc::new(AtomicUsize::new(0));
17017 let closure_restarts = Arc::clone(&server_restarts);
17018 let language_server_name = "test language server";
17019 let language_name: LanguageName = "Rust".into();
17020
17021 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17022 language_registry.add(Arc::new(Language::new(
17023 LanguageConfig {
17024 name: language_name.clone(),
17025 matcher: LanguageMatcher {
17026 path_suffixes: vec!["rs".to_string()],
17027 ..Default::default()
17028 },
17029 ..Default::default()
17030 },
17031 Some(tree_sitter_rust::LANGUAGE.into()),
17032 )));
17033 let mut fake_servers = language_registry.register_fake_lsp(
17034 "Rust",
17035 FakeLspAdapter {
17036 name: language_server_name,
17037 initialization_options: Some(json!({
17038 "testOptionValue": true
17039 })),
17040 initializer: Some(Box::new(move |fake_server| {
17041 let task_restarts = Arc::clone(&closure_restarts);
17042 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
17043 task_restarts.fetch_add(1, atomic::Ordering::Release);
17044 futures::future::ready(Ok(()))
17045 });
17046 })),
17047 ..Default::default()
17048 },
17049 );
17050
17051 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17052 let _buffer = project
17053 .update(cx, |project, cx| {
17054 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
17055 })
17056 .await
17057 .unwrap();
17058 let _fake_server = fake_servers.next().await.unwrap();
17059 update_test_language_settings(cx, |language_settings| {
17060 language_settings.languages.0.insert(
17061 language_name.clone().0,
17062 LanguageSettingsContent {
17063 tab_size: NonZeroU32::new(8),
17064 ..Default::default()
17065 },
17066 );
17067 });
17068 cx.executor().run_until_parked();
17069 assert_eq!(
17070 server_restarts.load(atomic::Ordering::Acquire),
17071 0,
17072 "Should not restart LSP server on an unrelated change"
17073 );
17074
17075 update_test_project_settings(cx, |project_settings| {
17076 project_settings.lsp.insert(
17077 "Some other server name".into(),
17078 LspSettings {
17079 binary: None,
17080 settings: None,
17081 initialization_options: Some(json!({
17082 "some other init value": false
17083 })),
17084 enable_lsp_tasks: false,
17085 fetch: None,
17086 },
17087 );
17088 });
17089 cx.executor().run_until_parked();
17090 assert_eq!(
17091 server_restarts.load(atomic::Ordering::Acquire),
17092 0,
17093 "Should not restart LSP server on an unrelated LSP settings change"
17094 );
17095
17096 update_test_project_settings(cx, |project_settings| {
17097 project_settings.lsp.insert(
17098 language_server_name.into(),
17099 LspSettings {
17100 binary: None,
17101 settings: None,
17102 initialization_options: Some(json!({
17103 "anotherInitValue": false
17104 })),
17105 enable_lsp_tasks: false,
17106 fetch: None,
17107 },
17108 );
17109 });
17110 cx.executor().run_until_parked();
17111 assert_eq!(
17112 server_restarts.load(atomic::Ordering::Acquire),
17113 1,
17114 "Should restart LSP server on a related LSP settings change"
17115 );
17116
17117 update_test_project_settings(cx, |project_settings| {
17118 project_settings.lsp.insert(
17119 language_server_name.into(),
17120 LspSettings {
17121 binary: None,
17122 settings: None,
17123 initialization_options: Some(json!({
17124 "anotherInitValue": false
17125 })),
17126 enable_lsp_tasks: false,
17127 fetch: None,
17128 },
17129 );
17130 });
17131 cx.executor().run_until_parked();
17132 assert_eq!(
17133 server_restarts.load(atomic::Ordering::Acquire),
17134 1,
17135 "Should not restart LSP server on a related LSP settings change that is the same"
17136 );
17137
17138 update_test_project_settings(cx, |project_settings| {
17139 project_settings.lsp.insert(
17140 language_server_name.into(),
17141 LspSettings {
17142 binary: None,
17143 settings: None,
17144 initialization_options: None,
17145 enable_lsp_tasks: false,
17146 fetch: None,
17147 },
17148 );
17149 });
17150 cx.executor().run_until_parked();
17151 assert_eq!(
17152 server_restarts.load(atomic::Ordering::Acquire),
17153 2,
17154 "Should restart LSP server on another related LSP settings change"
17155 );
17156}
17157
17158#[gpui::test]
17159async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
17160 init_test(cx, |_| {});
17161
17162 let mut cx = EditorLspTestContext::new_rust(
17163 lsp::ServerCapabilities {
17164 completion_provider: Some(lsp::CompletionOptions {
17165 trigger_characters: Some(vec![".".to_string()]),
17166 resolve_provider: Some(true),
17167 ..Default::default()
17168 }),
17169 ..Default::default()
17170 },
17171 cx,
17172 )
17173 .await;
17174
17175 cx.set_state("fn main() { let a = 2ˇ; }");
17176 cx.simulate_keystroke(".");
17177 let completion_item = lsp::CompletionItem {
17178 label: "some".into(),
17179 kind: Some(lsp::CompletionItemKind::SNIPPET),
17180 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
17181 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
17182 kind: lsp::MarkupKind::Markdown,
17183 value: "```rust\nSome(2)\n```".to_string(),
17184 })),
17185 deprecated: Some(false),
17186 sort_text: Some("fffffff2".to_string()),
17187 filter_text: Some("some".to_string()),
17188 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
17189 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17190 range: lsp::Range {
17191 start: lsp::Position {
17192 line: 0,
17193 character: 22,
17194 },
17195 end: lsp::Position {
17196 line: 0,
17197 character: 22,
17198 },
17199 },
17200 new_text: "Some(2)".to_string(),
17201 })),
17202 additional_text_edits: Some(vec![lsp::TextEdit {
17203 range: lsp::Range {
17204 start: lsp::Position {
17205 line: 0,
17206 character: 20,
17207 },
17208 end: lsp::Position {
17209 line: 0,
17210 character: 22,
17211 },
17212 },
17213 new_text: "".to_string(),
17214 }]),
17215 ..Default::default()
17216 };
17217
17218 let closure_completion_item = completion_item.clone();
17219 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17220 let task_completion_item = closure_completion_item.clone();
17221 async move {
17222 Ok(Some(lsp::CompletionResponse::Array(vec![
17223 task_completion_item,
17224 ])))
17225 }
17226 });
17227
17228 request.next().await;
17229
17230 cx.condition(|editor, _| editor.context_menu_visible())
17231 .await;
17232 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
17233 editor
17234 .confirm_completion(&ConfirmCompletion::default(), window, cx)
17235 .unwrap()
17236 });
17237 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
17238
17239 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
17240 let task_completion_item = completion_item.clone();
17241 async move { Ok(task_completion_item) }
17242 })
17243 .next()
17244 .await
17245 .unwrap();
17246 apply_additional_edits.await.unwrap();
17247 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
17248}
17249
17250#[gpui::test]
17251async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
17252 init_test(cx, |_| {});
17253
17254 let mut cx = EditorLspTestContext::new_rust(
17255 lsp::ServerCapabilities {
17256 completion_provider: Some(lsp::CompletionOptions {
17257 trigger_characters: Some(vec![".".to_string()]),
17258 resolve_provider: Some(true),
17259 ..Default::default()
17260 }),
17261 ..Default::default()
17262 },
17263 cx,
17264 )
17265 .await;
17266
17267 cx.set_state("fn main() { let a = 2ˇ; }");
17268 cx.simulate_keystroke(".");
17269
17270 let item1 = lsp::CompletionItem {
17271 label: "method id()".to_string(),
17272 filter_text: Some("id".to_string()),
17273 detail: None,
17274 documentation: None,
17275 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17276 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17277 new_text: ".id".to_string(),
17278 })),
17279 ..lsp::CompletionItem::default()
17280 };
17281
17282 let item2 = lsp::CompletionItem {
17283 label: "other".to_string(),
17284 filter_text: Some("other".to_string()),
17285 detail: None,
17286 documentation: None,
17287 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17288 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17289 new_text: ".other".to_string(),
17290 })),
17291 ..lsp::CompletionItem::default()
17292 };
17293
17294 let item1 = item1.clone();
17295 cx.set_request_handler::<lsp::request::Completion, _, _>({
17296 let item1 = item1.clone();
17297 move |_, _, _| {
17298 let item1 = item1.clone();
17299 let item2 = item2.clone();
17300 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
17301 }
17302 })
17303 .next()
17304 .await;
17305
17306 cx.condition(|editor, _| editor.context_menu_visible())
17307 .await;
17308 cx.update_editor(|editor, _, _| {
17309 let context_menu = editor.context_menu.borrow_mut();
17310 let context_menu = context_menu
17311 .as_ref()
17312 .expect("Should have the context menu deployed");
17313 match context_menu {
17314 CodeContextMenu::Completions(completions_menu) => {
17315 let completions = completions_menu.completions.borrow_mut();
17316 assert_eq!(
17317 completions
17318 .iter()
17319 .map(|completion| &completion.label.text)
17320 .collect::<Vec<_>>(),
17321 vec!["method id()", "other"]
17322 )
17323 }
17324 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17325 }
17326 });
17327
17328 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
17329 let item1 = item1.clone();
17330 move |_, item_to_resolve, _| {
17331 let item1 = item1.clone();
17332 async move {
17333 if item1 == item_to_resolve {
17334 Ok(lsp::CompletionItem {
17335 label: "method id()".to_string(),
17336 filter_text: Some("id".to_string()),
17337 detail: Some("Now resolved!".to_string()),
17338 documentation: Some(lsp::Documentation::String("Docs".to_string())),
17339 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17340 range: lsp::Range::new(
17341 lsp::Position::new(0, 22),
17342 lsp::Position::new(0, 22),
17343 ),
17344 new_text: ".id".to_string(),
17345 })),
17346 ..lsp::CompletionItem::default()
17347 })
17348 } else {
17349 Ok(item_to_resolve)
17350 }
17351 }
17352 }
17353 })
17354 .next()
17355 .await
17356 .unwrap();
17357 cx.run_until_parked();
17358
17359 cx.update_editor(|editor, window, cx| {
17360 editor.context_menu_next(&Default::default(), window, cx);
17361 });
17362
17363 cx.update_editor(|editor, _, _| {
17364 let context_menu = editor.context_menu.borrow_mut();
17365 let context_menu = context_menu
17366 .as_ref()
17367 .expect("Should have the context menu deployed");
17368 match context_menu {
17369 CodeContextMenu::Completions(completions_menu) => {
17370 let completions = completions_menu.completions.borrow_mut();
17371 assert_eq!(
17372 completions
17373 .iter()
17374 .map(|completion| &completion.label.text)
17375 .collect::<Vec<_>>(),
17376 vec!["method id() Now resolved!", "other"],
17377 "Should update first completion label, but not second as the filter text did not match."
17378 );
17379 }
17380 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17381 }
17382 });
17383}
17384
17385#[gpui::test]
17386async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
17387 init_test(cx, |_| {});
17388 let mut cx = EditorLspTestContext::new_rust(
17389 lsp::ServerCapabilities {
17390 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
17391 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
17392 completion_provider: Some(lsp::CompletionOptions {
17393 resolve_provider: Some(true),
17394 ..Default::default()
17395 }),
17396 ..Default::default()
17397 },
17398 cx,
17399 )
17400 .await;
17401 cx.set_state(indoc! {"
17402 struct TestStruct {
17403 field: i32
17404 }
17405
17406 fn mainˇ() {
17407 let unused_var = 42;
17408 let test_struct = TestStruct { field: 42 };
17409 }
17410 "});
17411 let symbol_range = cx.lsp_range(indoc! {"
17412 struct TestStruct {
17413 field: i32
17414 }
17415
17416 «fn main»() {
17417 let unused_var = 42;
17418 let test_struct = TestStruct { field: 42 };
17419 }
17420 "});
17421 let mut hover_requests =
17422 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
17423 Ok(Some(lsp::Hover {
17424 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
17425 kind: lsp::MarkupKind::Markdown,
17426 value: "Function documentation".to_string(),
17427 }),
17428 range: Some(symbol_range),
17429 }))
17430 });
17431
17432 // Case 1: Test that code action menu hide hover popover
17433 cx.dispatch_action(Hover);
17434 hover_requests.next().await;
17435 cx.condition(|editor, _| editor.hover_state.visible()).await;
17436 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
17437 move |_, _, _| async move {
17438 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
17439 lsp::CodeAction {
17440 title: "Remove unused variable".to_string(),
17441 kind: Some(CodeActionKind::QUICKFIX),
17442 edit: Some(lsp::WorkspaceEdit {
17443 changes: Some(
17444 [(
17445 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
17446 vec![lsp::TextEdit {
17447 range: lsp::Range::new(
17448 lsp::Position::new(5, 4),
17449 lsp::Position::new(5, 27),
17450 ),
17451 new_text: "".to_string(),
17452 }],
17453 )]
17454 .into_iter()
17455 .collect(),
17456 ),
17457 ..Default::default()
17458 }),
17459 ..Default::default()
17460 },
17461 )]))
17462 },
17463 );
17464 cx.update_editor(|editor, window, cx| {
17465 editor.toggle_code_actions(
17466 &ToggleCodeActions {
17467 deployed_from: None,
17468 quick_launch: false,
17469 },
17470 window,
17471 cx,
17472 );
17473 });
17474 code_action_requests.next().await;
17475 cx.run_until_parked();
17476 cx.condition(|editor, _| editor.context_menu_visible())
17477 .await;
17478 cx.update_editor(|editor, _, _| {
17479 assert!(
17480 !editor.hover_state.visible(),
17481 "Hover popover should be hidden when code action menu is shown"
17482 );
17483 // Hide code actions
17484 editor.context_menu.take();
17485 });
17486
17487 // Case 2: Test that code completions hide hover popover
17488 cx.dispatch_action(Hover);
17489 hover_requests.next().await;
17490 cx.condition(|editor, _| editor.hover_state.visible()).await;
17491 let counter = Arc::new(AtomicUsize::new(0));
17492 let mut completion_requests =
17493 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17494 let counter = counter.clone();
17495 async move {
17496 counter.fetch_add(1, atomic::Ordering::Release);
17497 Ok(Some(lsp::CompletionResponse::Array(vec![
17498 lsp::CompletionItem {
17499 label: "main".into(),
17500 kind: Some(lsp::CompletionItemKind::FUNCTION),
17501 detail: Some("() -> ()".to_string()),
17502 ..Default::default()
17503 },
17504 lsp::CompletionItem {
17505 label: "TestStruct".into(),
17506 kind: Some(lsp::CompletionItemKind::STRUCT),
17507 detail: Some("struct TestStruct".to_string()),
17508 ..Default::default()
17509 },
17510 ])))
17511 }
17512 });
17513 cx.update_editor(|editor, window, cx| {
17514 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
17515 });
17516 completion_requests.next().await;
17517 cx.condition(|editor, _| editor.context_menu_visible())
17518 .await;
17519 cx.update_editor(|editor, _, _| {
17520 assert!(
17521 !editor.hover_state.visible(),
17522 "Hover popover should be hidden when completion menu is shown"
17523 );
17524 });
17525}
17526
17527#[gpui::test]
17528async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
17529 init_test(cx, |_| {});
17530
17531 let mut cx = EditorLspTestContext::new_rust(
17532 lsp::ServerCapabilities {
17533 completion_provider: Some(lsp::CompletionOptions {
17534 trigger_characters: Some(vec![".".to_string()]),
17535 resolve_provider: Some(true),
17536 ..Default::default()
17537 }),
17538 ..Default::default()
17539 },
17540 cx,
17541 )
17542 .await;
17543
17544 cx.set_state("fn main() { let a = 2ˇ; }");
17545 cx.simulate_keystroke(".");
17546
17547 let unresolved_item_1 = lsp::CompletionItem {
17548 label: "id".to_string(),
17549 filter_text: Some("id".to_string()),
17550 detail: None,
17551 documentation: None,
17552 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17553 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17554 new_text: ".id".to_string(),
17555 })),
17556 ..lsp::CompletionItem::default()
17557 };
17558 let resolved_item_1 = lsp::CompletionItem {
17559 additional_text_edits: Some(vec![lsp::TextEdit {
17560 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
17561 new_text: "!!".to_string(),
17562 }]),
17563 ..unresolved_item_1.clone()
17564 };
17565 let unresolved_item_2 = lsp::CompletionItem {
17566 label: "other".to_string(),
17567 filter_text: Some("other".to_string()),
17568 detail: None,
17569 documentation: None,
17570 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17571 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17572 new_text: ".other".to_string(),
17573 })),
17574 ..lsp::CompletionItem::default()
17575 };
17576 let resolved_item_2 = lsp::CompletionItem {
17577 additional_text_edits: Some(vec![lsp::TextEdit {
17578 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
17579 new_text: "??".to_string(),
17580 }]),
17581 ..unresolved_item_2.clone()
17582 };
17583
17584 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
17585 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
17586 cx.lsp
17587 .server
17588 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
17589 let unresolved_item_1 = unresolved_item_1.clone();
17590 let resolved_item_1 = resolved_item_1.clone();
17591 let unresolved_item_2 = unresolved_item_2.clone();
17592 let resolved_item_2 = resolved_item_2.clone();
17593 let resolve_requests_1 = resolve_requests_1.clone();
17594 let resolve_requests_2 = resolve_requests_2.clone();
17595 move |unresolved_request, _| {
17596 let unresolved_item_1 = unresolved_item_1.clone();
17597 let resolved_item_1 = resolved_item_1.clone();
17598 let unresolved_item_2 = unresolved_item_2.clone();
17599 let resolved_item_2 = resolved_item_2.clone();
17600 let resolve_requests_1 = resolve_requests_1.clone();
17601 let resolve_requests_2 = resolve_requests_2.clone();
17602 async move {
17603 if unresolved_request == unresolved_item_1 {
17604 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
17605 Ok(resolved_item_1.clone())
17606 } else if unresolved_request == unresolved_item_2 {
17607 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
17608 Ok(resolved_item_2.clone())
17609 } else {
17610 panic!("Unexpected completion item {unresolved_request:?}")
17611 }
17612 }
17613 }
17614 })
17615 .detach();
17616
17617 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17618 let unresolved_item_1 = unresolved_item_1.clone();
17619 let unresolved_item_2 = unresolved_item_2.clone();
17620 async move {
17621 Ok(Some(lsp::CompletionResponse::Array(vec![
17622 unresolved_item_1,
17623 unresolved_item_2,
17624 ])))
17625 }
17626 })
17627 .next()
17628 .await;
17629
17630 cx.condition(|editor, _| editor.context_menu_visible())
17631 .await;
17632 cx.update_editor(|editor, _, _| {
17633 let context_menu = editor.context_menu.borrow_mut();
17634 let context_menu = context_menu
17635 .as_ref()
17636 .expect("Should have the context menu deployed");
17637 match context_menu {
17638 CodeContextMenu::Completions(completions_menu) => {
17639 let completions = completions_menu.completions.borrow_mut();
17640 assert_eq!(
17641 completions
17642 .iter()
17643 .map(|completion| &completion.label.text)
17644 .collect::<Vec<_>>(),
17645 vec!["id", "other"]
17646 )
17647 }
17648 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17649 }
17650 });
17651 cx.run_until_parked();
17652
17653 cx.update_editor(|editor, window, cx| {
17654 editor.context_menu_next(&ContextMenuNext, window, cx);
17655 });
17656 cx.run_until_parked();
17657 cx.update_editor(|editor, window, cx| {
17658 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
17659 });
17660 cx.run_until_parked();
17661 cx.update_editor(|editor, window, cx| {
17662 editor.context_menu_next(&ContextMenuNext, window, cx);
17663 });
17664 cx.run_until_parked();
17665 cx.update_editor(|editor, window, cx| {
17666 editor
17667 .compose_completion(&ComposeCompletion::default(), window, cx)
17668 .expect("No task returned")
17669 })
17670 .await
17671 .expect("Completion failed");
17672 cx.run_until_parked();
17673
17674 cx.update_editor(|editor, _, cx| {
17675 assert_eq!(
17676 resolve_requests_1.load(atomic::Ordering::Acquire),
17677 1,
17678 "Should always resolve once despite multiple selections"
17679 );
17680 assert_eq!(
17681 resolve_requests_2.load(atomic::Ordering::Acquire),
17682 1,
17683 "Should always resolve once after multiple selections and applying the completion"
17684 );
17685 assert_eq!(
17686 editor.text(cx),
17687 "fn main() { let a = ??.other; }",
17688 "Should use resolved data when applying the completion"
17689 );
17690 });
17691}
17692
17693#[gpui::test]
17694async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
17695 init_test(cx, |_| {});
17696
17697 let item_0 = lsp::CompletionItem {
17698 label: "abs".into(),
17699 insert_text: Some("abs".into()),
17700 data: Some(json!({ "very": "special"})),
17701 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
17702 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
17703 lsp::InsertReplaceEdit {
17704 new_text: "abs".to_string(),
17705 insert: lsp::Range::default(),
17706 replace: lsp::Range::default(),
17707 },
17708 )),
17709 ..lsp::CompletionItem::default()
17710 };
17711 let items = iter::once(item_0.clone())
17712 .chain((11..51).map(|i| lsp::CompletionItem {
17713 label: format!("item_{}", i),
17714 insert_text: Some(format!("item_{}", i)),
17715 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
17716 ..lsp::CompletionItem::default()
17717 }))
17718 .collect::<Vec<_>>();
17719
17720 let default_commit_characters = vec!["?".to_string()];
17721 let default_data = json!({ "default": "data"});
17722 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
17723 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
17724 let default_edit_range = lsp::Range {
17725 start: lsp::Position {
17726 line: 0,
17727 character: 5,
17728 },
17729 end: lsp::Position {
17730 line: 0,
17731 character: 5,
17732 },
17733 };
17734
17735 let mut cx = EditorLspTestContext::new_rust(
17736 lsp::ServerCapabilities {
17737 completion_provider: Some(lsp::CompletionOptions {
17738 trigger_characters: Some(vec![".".to_string()]),
17739 resolve_provider: Some(true),
17740 ..Default::default()
17741 }),
17742 ..Default::default()
17743 },
17744 cx,
17745 )
17746 .await;
17747
17748 cx.set_state("fn main() { let a = 2ˇ; }");
17749 cx.simulate_keystroke(".");
17750
17751 let completion_data = default_data.clone();
17752 let completion_characters = default_commit_characters.clone();
17753 let completion_items = items.clone();
17754 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17755 let default_data = completion_data.clone();
17756 let default_commit_characters = completion_characters.clone();
17757 let items = completion_items.clone();
17758 async move {
17759 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
17760 items,
17761 item_defaults: Some(lsp::CompletionListItemDefaults {
17762 data: Some(default_data.clone()),
17763 commit_characters: Some(default_commit_characters.clone()),
17764 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
17765 default_edit_range,
17766 )),
17767 insert_text_format: Some(default_insert_text_format),
17768 insert_text_mode: Some(default_insert_text_mode),
17769 }),
17770 ..lsp::CompletionList::default()
17771 })))
17772 }
17773 })
17774 .next()
17775 .await;
17776
17777 let resolved_items = Arc::new(Mutex::new(Vec::new()));
17778 cx.lsp
17779 .server
17780 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
17781 let closure_resolved_items = resolved_items.clone();
17782 move |item_to_resolve, _| {
17783 let closure_resolved_items = closure_resolved_items.clone();
17784 async move {
17785 closure_resolved_items.lock().push(item_to_resolve.clone());
17786 Ok(item_to_resolve)
17787 }
17788 }
17789 })
17790 .detach();
17791
17792 cx.condition(|editor, _| editor.context_menu_visible())
17793 .await;
17794 cx.run_until_parked();
17795 cx.update_editor(|editor, _, _| {
17796 let menu = editor.context_menu.borrow_mut();
17797 match menu.as_ref().expect("should have the completions menu") {
17798 CodeContextMenu::Completions(completions_menu) => {
17799 assert_eq!(
17800 completions_menu
17801 .entries
17802 .borrow()
17803 .iter()
17804 .map(|mat| mat.string.clone())
17805 .collect::<Vec<String>>(),
17806 items
17807 .iter()
17808 .map(|completion| completion.label.clone())
17809 .collect::<Vec<String>>()
17810 );
17811 }
17812 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
17813 }
17814 });
17815 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
17816 // with 4 from the end.
17817 assert_eq!(
17818 *resolved_items.lock(),
17819 [&items[0..16], &items[items.len() - 4..items.len()]]
17820 .concat()
17821 .iter()
17822 .cloned()
17823 .map(|mut item| {
17824 if item.data.is_none() {
17825 item.data = Some(default_data.clone());
17826 }
17827 item
17828 })
17829 .collect::<Vec<lsp::CompletionItem>>(),
17830 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
17831 );
17832 resolved_items.lock().clear();
17833
17834 cx.update_editor(|editor, window, cx| {
17835 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
17836 });
17837 cx.run_until_parked();
17838 // Completions that have already been resolved are skipped.
17839 assert_eq!(
17840 *resolved_items.lock(),
17841 items[items.len() - 17..items.len() - 4]
17842 .iter()
17843 .cloned()
17844 .map(|mut item| {
17845 if item.data.is_none() {
17846 item.data = Some(default_data.clone());
17847 }
17848 item
17849 })
17850 .collect::<Vec<lsp::CompletionItem>>()
17851 );
17852 resolved_items.lock().clear();
17853}
17854
17855#[gpui::test]
17856async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
17857 init_test(cx, |_| {});
17858
17859 let mut cx = EditorLspTestContext::new(
17860 Language::new(
17861 LanguageConfig {
17862 matcher: LanguageMatcher {
17863 path_suffixes: vec!["jsx".into()],
17864 ..Default::default()
17865 },
17866 overrides: [(
17867 "element".into(),
17868 LanguageConfigOverride {
17869 completion_query_characters: Override::Set(['-'].into_iter().collect()),
17870 ..Default::default()
17871 },
17872 )]
17873 .into_iter()
17874 .collect(),
17875 ..Default::default()
17876 },
17877 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
17878 )
17879 .with_override_query("(jsx_self_closing_element) @element")
17880 .unwrap(),
17881 lsp::ServerCapabilities {
17882 completion_provider: Some(lsp::CompletionOptions {
17883 trigger_characters: Some(vec![":".to_string()]),
17884 ..Default::default()
17885 }),
17886 ..Default::default()
17887 },
17888 cx,
17889 )
17890 .await;
17891
17892 cx.lsp
17893 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
17894 Ok(Some(lsp::CompletionResponse::Array(vec![
17895 lsp::CompletionItem {
17896 label: "bg-blue".into(),
17897 ..Default::default()
17898 },
17899 lsp::CompletionItem {
17900 label: "bg-red".into(),
17901 ..Default::default()
17902 },
17903 lsp::CompletionItem {
17904 label: "bg-yellow".into(),
17905 ..Default::default()
17906 },
17907 ])))
17908 });
17909
17910 cx.set_state(r#"<p class="bgˇ" />"#);
17911
17912 // Trigger completion when typing a dash, because the dash is an extra
17913 // word character in the 'element' scope, which contains the cursor.
17914 cx.simulate_keystroke("-");
17915 cx.executor().run_until_parked();
17916 cx.update_editor(|editor, _, _| {
17917 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17918 {
17919 assert_eq!(
17920 completion_menu_entries(menu),
17921 &["bg-blue", "bg-red", "bg-yellow"]
17922 );
17923 } else {
17924 panic!("expected completion menu to be open");
17925 }
17926 });
17927
17928 cx.simulate_keystroke("l");
17929 cx.executor().run_until_parked();
17930 cx.update_editor(|editor, _, _| {
17931 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17932 {
17933 assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]);
17934 } else {
17935 panic!("expected completion menu to be open");
17936 }
17937 });
17938
17939 // When filtering completions, consider the character after the '-' to
17940 // be the start of a subword.
17941 cx.set_state(r#"<p class="yelˇ" />"#);
17942 cx.simulate_keystroke("l");
17943 cx.executor().run_until_parked();
17944 cx.update_editor(|editor, _, _| {
17945 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17946 {
17947 assert_eq!(completion_menu_entries(menu), &["bg-yellow"]);
17948 } else {
17949 panic!("expected completion menu to be open");
17950 }
17951 });
17952}
17953
17954fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
17955 let entries = menu.entries.borrow();
17956 entries.iter().map(|mat| mat.string.clone()).collect()
17957}
17958
17959#[gpui::test]
17960async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
17961 init_test(cx, |settings| {
17962 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
17963 Formatter::Prettier,
17964 )))
17965 });
17966
17967 let fs = FakeFs::new(cx.executor());
17968 fs.insert_file(path!("/file.ts"), Default::default()).await;
17969
17970 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
17971 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17972
17973 language_registry.add(Arc::new(Language::new(
17974 LanguageConfig {
17975 name: "TypeScript".into(),
17976 matcher: LanguageMatcher {
17977 path_suffixes: vec!["ts".to_string()],
17978 ..Default::default()
17979 },
17980 ..Default::default()
17981 },
17982 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
17983 )));
17984 update_test_language_settings(cx, |settings| {
17985 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
17986 });
17987
17988 let test_plugin = "test_plugin";
17989 let _ = language_registry.register_fake_lsp(
17990 "TypeScript",
17991 FakeLspAdapter {
17992 prettier_plugins: vec![test_plugin],
17993 ..Default::default()
17994 },
17995 );
17996
17997 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
17998 let buffer = project
17999 .update(cx, |project, cx| {
18000 project.open_local_buffer(path!("/file.ts"), cx)
18001 })
18002 .await
18003 .unwrap();
18004
18005 let buffer_text = "one\ntwo\nthree\n";
18006 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
18007 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
18008 editor.update_in(cx, |editor, window, cx| {
18009 editor.set_text(buffer_text, window, cx)
18010 });
18011
18012 editor
18013 .update_in(cx, |editor, window, cx| {
18014 editor.perform_format(
18015 project.clone(),
18016 FormatTrigger::Manual,
18017 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18018 window,
18019 cx,
18020 )
18021 })
18022 .unwrap()
18023 .await;
18024 assert_eq!(
18025 editor.update(cx, |editor, cx| editor.text(cx)),
18026 buffer_text.to_string() + prettier_format_suffix,
18027 "Test prettier formatting was not applied to the original buffer text",
18028 );
18029
18030 update_test_language_settings(cx, |settings| {
18031 settings.defaults.formatter = Some(SelectedFormatter::Auto)
18032 });
18033 let format = editor.update_in(cx, |editor, window, cx| {
18034 editor.perform_format(
18035 project.clone(),
18036 FormatTrigger::Manual,
18037 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18038 window,
18039 cx,
18040 )
18041 });
18042 format.await.unwrap();
18043 assert_eq!(
18044 editor.update(cx, |editor, cx| editor.text(cx)),
18045 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
18046 "Autoformatting (via test prettier) was not applied to the original buffer text",
18047 );
18048}
18049
18050#[gpui::test]
18051async fn test_addition_reverts(cx: &mut TestAppContext) {
18052 init_test(cx, |_| {});
18053 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18054 let base_text = indoc! {r#"
18055 struct Row;
18056 struct Row1;
18057 struct Row2;
18058
18059 struct Row4;
18060 struct Row5;
18061 struct Row6;
18062
18063 struct Row8;
18064 struct Row9;
18065 struct Row10;"#};
18066
18067 // When addition hunks are not adjacent to carets, no hunk revert is performed
18068 assert_hunk_revert(
18069 indoc! {r#"struct Row;
18070 struct Row1;
18071 struct Row1.1;
18072 struct Row1.2;
18073 struct Row2;ˇ
18074
18075 struct Row4;
18076 struct Row5;
18077 struct Row6;
18078
18079 struct Row8;
18080 ˇstruct Row9;
18081 struct Row9.1;
18082 struct Row9.2;
18083 struct Row9.3;
18084 struct Row10;"#},
18085 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18086 indoc! {r#"struct Row;
18087 struct Row1;
18088 struct Row1.1;
18089 struct Row1.2;
18090 struct Row2;ˇ
18091
18092 struct Row4;
18093 struct Row5;
18094 struct Row6;
18095
18096 struct Row8;
18097 ˇstruct Row9;
18098 struct Row9.1;
18099 struct Row9.2;
18100 struct Row9.3;
18101 struct Row10;"#},
18102 base_text,
18103 &mut cx,
18104 );
18105 // Same for selections
18106 assert_hunk_revert(
18107 indoc! {r#"struct Row;
18108 struct Row1;
18109 struct Row2;
18110 struct Row2.1;
18111 struct Row2.2;
18112 «ˇ
18113 struct Row4;
18114 struct» Row5;
18115 «struct Row6;
18116 ˇ»
18117 struct Row9.1;
18118 struct Row9.2;
18119 struct Row9.3;
18120 struct Row8;
18121 struct Row9;
18122 struct Row10;"#},
18123 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18124 indoc! {r#"struct Row;
18125 struct Row1;
18126 struct Row2;
18127 struct Row2.1;
18128 struct Row2.2;
18129 «ˇ
18130 struct Row4;
18131 struct» Row5;
18132 «struct Row6;
18133 ˇ»
18134 struct Row9.1;
18135 struct Row9.2;
18136 struct Row9.3;
18137 struct Row8;
18138 struct Row9;
18139 struct Row10;"#},
18140 base_text,
18141 &mut cx,
18142 );
18143
18144 // When carets and selections intersect the addition hunks, those are reverted.
18145 // Adjacent carets got merged.
18146 assert_hunk_revert(
18147 indoc! {r#"struct Row;
18148 ˇ// something on the top
18149 struct Row1;
18150 struct Row2;
18151 struct Roˇw3.1;
18152 struct Row2.2;
18153 struct Row2.3;ˇ
18154
18155 struct Row4;
18156 struct ˇRow5.1;
18157 struct Row5.2;
18158 struct «Rowˇ»5.3;
18159 struct Row5;
18160 struct Row6;
18161 ˇ
18162 struct Row9.1;
18163 struct «Rowˇ»9.2;
18164 struct «ˇRow»9.3;
18165 struct Row8;
18166 struct Row9;
18167 «ˇ// something on bottom»
18168 struct Row10;"#},
18169 vec![
18170 DiffHunkStatusKind::Added,
18171 DiffHunkStatusKind::Added,
18172 DiffHunkStatusKind::Added,
18173 DiffHunkStatusKind::Added,
18174 DiffHunkStatusKind::Added,
18175 ],
18176 indoc! {r#"struct Row;
18177 ˇstruct Row1;
18178 struct Row2;
18179 ˇ
18180 struct Row4;
18181 ˇstruct Row5;
18182 struct Row6;
18183 ˇ
18184 ˇstruct Row8;
18185 struct Row9;
18186 ˇstruct Row10;"#},
18187 base_text,
18188 &mut cx,
18189 );
18190}
18191
18192#[gpui::test]
18193async fn test_modification_reverts(cx: &mut TestAppContext) {
18194 init_test(cx, |_| {});
18195 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18196 let base_text = indoc! {r#"
18197 struct Row;
18198 struct Row1;
18199 struct Row2;
18200
18201 struct Row4;
18202 struct Row5;
18203 struct Row6;
18204
18205 struct Row8;
18206 struct Row9;
18207 struct Row10;"#};
18208
18209 // Modification hunks behave the same as the addition ones.
18210 assert_hunk_revert(
18211 indoc! {r#"struct Row;
18212 struct Row1;
18213 struct Row33;
18214 ˇ
18215 struct Row4;
18216 struct Row5;
18217 struct Row6;
18218 ˇ
18219 struct Row99;
18220 struct Row9;
18221 struct Row10;"#},
18222 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18223 indoc! {r#"struct Row;
18224 struct Row1;
18225 struct Row33;
18226 ˇ
18227 struct Row4;
18228 struct Row5;
18229 struct Row6;
18230 ˇ
18231 struct Row99;
18232 struct Row9;
18233 struct Row10;"#},
18234 base_text,
18235 &mut cx,
18236 );
18237 assert_hunk_revert(
18238 indoc! {r#"struct Row;
18239 struct Row1;
18240 struct Row33;
18241 «ˇ
18242 struct Row4;
18243 struct» Row5;
18244 «struct Row6;
18245 ˇ»
18246 struct Row99;
18247 struct Row9;
18248 struct Row10;"#},
18249 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18250 indoc! {r#"struct Row;
18251 struct Row1;
18252 struct Row33;
18253 «ˇ
18254 struct Row4;
18255 struct» Row5;
18256 «struct Row6;
18257 ˇ»
18258 struct Row99;
18259 struct Row9;
18260 struct Row10;"#},
18261 base_text,
18262 &mut cx,
18263 );
18264
18265 assert_hunk_revert(
18266 indoc! {r#"ˇstruct Row1.1;
18267 struct Row1;
18268 «ˇstr»uct Row22;
18269
18270 struct ˇRow44;
18271 struct Row5;
18272 struct «Rˇ»ow66;ˇ
18273
18274 «struˇ»ct Row88;
18275 struct Row9;
18276 struct Row1011;ˇ"#},
18277 vec![
18278 DiffHunkStatusKind::Modified,
18279 DiffHunkStatusKind::Modified,
18280 DiffHunkStatusKind::Modified,
18281 DiffHunkStatusKind::Modified,
18282 DiffHunkStatusKind::Modified,
18283 DiffHunkStatusKind::Modified,
18284 ],
18285 indoc! {r#"struct Row;
18286 ˇstruct Row1;
18287 struct Row2;
18288 ˇ
18289 struct Row4;
18290 ˇstruct Row5;
18291 struct Row6;
18292 ˇ
18293 struct Row8;
18294 ˇstruct Row9;
18295 struct Row10;ˇ"#},
18296 base_text,
18297 &mut cx,
18298 );
18299}
18300
18301#[gpui::test]
18302async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
18303 init_test(cx, |_| {});
18304 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18305 let base_text = indoc! {r#"
18306 one
18307
18308 two
18309 three
18310 "#};
18311
18312 cx.set_head_text(base_text);
18313 cx.set_state("\nˇ\n");
18314 cx.executor().run_until_parked();
18315 cx.update_editor(|editor, _window, cx| {
18316 editor.expand_selected_diff_hunks(cx);
18317 });
18318 cx.executor().run_until_parked();
18319 cx.update_editor(|editor, window, cx| {
18320 editor.backspace(&Default::default(), window, cx);
18321 });
18322 cx.run_until_parked();
18323 cx.assert_state_with_diff(
18324 indoc! {r#"
18325
18326 - two
18327 - threeˇ
18328 +
18329 "#}
18330 .to_string(),
18331 );
18332}
18333
18334#[gpui::test]
18335async fn test_deletion_reverts(cx: &mut TestAppContext) {
18336 init_test(cx, |_| {});
18337 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18338 let base_text = indoc! {r#"struct Row;
18339struct Row1;
18340struct Row2;
18341
18342struct Row4;
18343struct Row5;
18344struct Row6;
18345
18346struct Row8;
18347struct Row9;
18348struct Row10;"#};
18349
18350 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
18351 assert_hunk_revert(
18352 indoc! {r#"struct Row;
18353 struct Row2;
18354
18355 ˇstruct Row4;
18356 struct Row5;
18357 struct Row6;
18358 ˇ
18359 struct Row8;
18360 struct Row10;"#},
18361 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18362 indoc! {r#"struct Row;
18363 struct Row2;
18364
18365 ˇstruct Row4;
18366 struct Row5;
18367 struct Row6;
18368 ˇ
18369 struct Row8;
18370 struct Row10;"#},
18371 base_text,
18372 &mut cx,
18373 );
18374 assert_hunk_revert(
18375 indoc! {r#"struct Row;
18376 struct Row2;
18377
18378 «ˇstruct Row4;
18379 struct» Row5;
18380 «struct Row6;
18381 ˇ»
18382 struct Row8;
18383 struct Row10;"#},
18384 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18385 indoc! {r#"struct Row;
18386 struct Row2;
18387
18388 «ˇstruct Row4;
18389 struct» Row5;
18390 «struct Row6;
18391 ˇ»
18392 struct Row8;
18393 struct Row10;"#},
18394 base_text,
18395 &mut cx,
18396 );
18397
18398 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
18399 assert_hunk_revert(
18400 indoc! {r#"struct Row;
18401 ˇstruct Row2;
18402
18403 struct Row4;
18404 struct Row5;
18405 struct Row6;
18406
18407 struct Row8;ˇ
18408 struct Row10;"#},
18409 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18410 indoc! {r#"struct Row;
18411 struct Row1;
18412 ˇstruct Row2;
18413
18414 struct Row4;
18415 struct Row5;
18416 struct Row6;
18417
18418 struct Row8;ˇ
18419 struct Row9;
18420 struct Row10;"#},
18421 base_text,
18422 &mut cx,
18423 );
18424 assert_hunk_revert(
18425 indoc! {r#"struct Row;
18426 struct Row2«ˇ;
18427 struct Row4;
18428 struct» Row5;
18429 «struct Row6;
18430
18431 struct Row8;ˇ»
18432 struct Row10;"#},
18433 vec![
18434 DiffHunkStatusKind::Deleted,
18435 DiffHunkStatusKind::Deleted,
18436 DiffHunkStatusKind::Deleted,
18437 ],
18438 indoc! {r#"struct Row;
18439 struct Row1;
18440 struct Row2«ˇ;
18441
18442 struct Row4;
18443 struct» Row5;
18444 «struct Row6;
18445
18446 struct Row8;ˇ»
18447 struct Row9;
18448 struct Row10;"#},
18449 base_text,
18450 &mut cx,
18451 );
18452}
18453
18454#[gpui::test]
18455async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
18456 init_test(cx, |_| {});
18457
18458 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
18459 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
18460 let base_text_3 =
18461 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
18462
18463 let text_1 = edit_first_char_of_every_line(base_text_1);
18464 let text_2 = edit_first_char_of_every_line(base_text_2);
18465 let text_3 = edit_first_char_of_every_line(base_text_3);
18466
18467 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
18468 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
18469 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
18470
18471 let multibuffer = cx.new(|cx| {
18472 let mut multibuffer = MultiBuffer::new(ReadWrite);
18473 multibuffer.push_excerpts(
18474 buffer_1.clone(),
18475 [
18476 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18477 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18478 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18479 ],
18480 cx,
18481 );
18482 multibuffer.push_excerpts(
18483 buffer_2.clone(),
18484 [
18485 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18486 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18487 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18488 ],
18489 cx,
18490 );
18491 multibuffer.push_excerpts(
18492 buffer_3.clone(),
18493 [
18494 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18495 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18496 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18497 ],
18498 cx,
18499 );
18500 multibuffer
18501 });
18502
18503 let fs = FakeFs::new(cx.executor());
18504 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
18505 let (editor, cx) = cx
18506 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
18507 editor.update_in(cx, |editor, _window, cx| {
18508 for (buffer, diff_base) in [
18509 (buffer_1.clone(), base_text_1),
18510 (buffer_2.clone(), base_text_2),
18511 (buffer_3.clone(), base_text_3),
18512 ] {
18513 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
18514 editor
18515 .buffer
18516 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
18517 }
18518 });
18519 cx.executor().run_until_parked();
18520
18521 editor.update_in(cx, |editor, window, cx| {
18522 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}");
18523 editor.select_all(&SelectAll, window, cx);
18524 editor.git_restore(&Default::default(), window, cx);
18525 });
18526 cx.executor().run_until_parked();
18527
18528 // When all ranges are selected, all buffer hunks are reverted.
18529 editor.update(cx, |editor, cx| {
18530 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");
18531 });
18532 buffer_1.update(cx, |buffer, _| {
18533 assert_eq!(buffer.text(), base_text_1);
18534 });
18535 buffer_2.update(cx, |buffer, _| {
18536 assert_eq!(buffer.text(), base_text_2);
18537 });
18538 buffer_3.update(cx, |buffer, _| {
18539 assert_eq!(buffer.text(), base_text_3);
18540 });
18541
18542 editor.update_in(cx, |editor, window, cx| {
18543 editor.undo(&Default::default(), window, cx);
18544 });
18545
18546 editor.update_in(cx, |editor, window, cx| {
18547 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18548 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
18549 });
18550 editor.git_restore(&Default::default(), window, cx);
18551 });
18552
18553 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
18554 // but not affect buffer_2 and its related excerpts.
18555 editor.update(cx, |editor, cx| {
18556 assert_eq!(
18557 editor.text(cx),
18558 "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}"
18559 );
18560 });
18561 buffer_1.update(cx, |buffer, _| {
18562 assert_eq!(buffer.text(), base_text_1);
18563 });
18564 buffer_2.update(cx, |buffer, _| {
18565 assert_eq!(
18566 buffer.text(),
18567 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
18568 );
18569 });
18570 buffer_3.update(cx, |buffer, _| {
18571 assert_eq!(
18572 buffer.text(),
18573 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
18574 );
18575 });
18576
18577 fn edit_first_char_of_every_line(text: &str) -> String {
18578 text.split('\n')
18579 .map(|line| format!("X{}", &line[1..]))
18580 .collect::<Vec<_>>()
18581 .join("\n")
18582 }
18583}
18584
18585#[gpui::test]
18586async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
18587 init_test(cx, |_| {});
18588
18589 let cols = 4;
18590 let rows = 10;
18591 let sample_text_1 = sample_text(rows, cols, 'a');
18592 assert_eq!(
18593 sample_text_1,
18594 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
18595 );
18596 let sample_text_2 = sample_text(rows, cols, 'l');
18597 assert_eq!(
18598 sample_text_2,
18599 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
18600 );
18601 let sample_text_3 = sample_text(rows, cols, 'v');
18602 assert_eq!(
18603 sample_text_3,
18604 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
18605 );
18606
18607 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
18608 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
18609 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
18610
18611 let multi_buffer = cx.new(|cx| {
18612 let mut multibuffer = MultiBuffer::new(ReadWrite);
18613 multibuffer.push_excerpts(
18614 buffer_1.clone(),
18615 [
18616 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18617 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18618 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18619 ],
18620 cx,
18621 );
18622 multibuffer.push_excerpts(
18623 buffer_2.clone(),
18624 [
18625 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18626 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18627 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18628 ],
18629 cx,
18630 );
18631 multibuffer.push_excerpts(
18632 buffer_3.clone(),
18633 [
18634 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18635 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18636 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18637 ],
18638 cx,
18639 );
18640 multibuffer
18641 });
18642
18643 let fs = FakeFs::new(cx.executor());
18644 fs.insert_tree(
18645 "/a",
18646 json!({
18647 "main.rs": sample_text_1,
18648 "other.rs": sample_text_2,
18649 "lib.rs": sample_text_3,
18650 }),
18651 )
18652 .await;
18653 let project = Project::test(fs, ["/a".as_ref()], cx).await;
18654 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18655 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18656 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18657 Editor::new(
18658 EditorMode::full(),
18659 multi_buffer,
18660 Some(project.clone()),
18661 window,
18662 cx,
18663 )
18664 });
18665 let multibuffer_item_id = workspace
18666 .update(cx, |workspace, window, cx| {
18667 assert!(
18668 workspace.active_item(cx).is_none(),
18669 "active item should be None before the first item is added"
18670 );
18671 workspace.add_item_to_active_pane(
18672 Box::new(multi_buffer_editor.clone()),
18673 None,
18674 true,
18675 window,
18676 cx,
18677 );
18678 let active_item = workspace
18679 .active_item(cx)
18680 .expect("should have an active item after adding the multi buffer");
18681 assert!(
18682 !active_item.is_singleton(cx),
18683 "A multi buffer was expected to active after adding"
18684 );
18685 active_item.item_id()
18686 })
18687 .unwrap();
18688 cx.executor().run_until_parked();
18689
18690 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18691 editor.change_selections(
18692 SelectionEffects::scroll(Autoscroll::Next),
18693 window,
18694 cx,
18695 |s| s.select_ranges(Some(1..2)),
18696 );
18697 editor.open_excerpts(&OpenExcerpts, window, cx);
18698 });
18699 cx.executor().run_until_parked();
18700 let first_item_id = workspace
18701 .update(cx, |workspace, window, cx| {
18702 let active_item = workspace
18703 .active_item(cx)
18704 .expect("should have an active item after navigating into the 1st buffer");
18705 let first_item_id = active_item.item_id();
18706 assert_ne!(
18707 first_item_id, multibuffer_item_id,
18708 "Should navigate into the 1st buffer and activate it"
18709 );
18710 assert!(
18711 active_item.is_singleton(cx),
18712 "New active item should be a singleton buffer"
18713 );
18714 assert_eq!(
18715 active_item
18716 .act_as::<Editor>(cx)
18717 .expect("should have navigated into an editor for the 1st buffer")
18718 .read(cx)
18719 .text(cx),
18720 sample_text_1
18721 );
18722
18723 workspace
18724 .go_back(workspace.active_pane().downgrade(), window, cx)
18725 .detach_and_log_err(cx);
18726
18727 first_item_id
18728 })
18729 .unwrap();
18730 cx.executor().run_until_parked();
18731 workspace
18732 .update(cx, |workspace, _, cx| {
18733 let active_item = workspace
18734 .active_item(cx)
18735 .expect("should have an active item after navigating back");
18736 assert_eq!(
18737 active_item.item_id(),
18738 multibuffer_item_id,
18739 "Should navigate back to the multi buffer"
18740 );
18741 assert!(!active_item.is_singleton(cx));
18742 })
18743 .unwrap();
18744
18745 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18746 editor.change_selections(
18747 SelectionEffects::scroll(Autoscroll::Next),
18748 window,
18749 cx,
18750 |s| s.select_ranges(Some(39..40)),
18751 );
18752 editor.open_excerpts(&OpenExcerpts, window, cx);
18753 });
18754 cx.executor().run_until_parked();
18755 let second_item_id = workspace
18756 .update(cx, |workspace, window, cx| {
18757 let active_item = workspace
18758 .active_item(cx)
18759 .expect("should have an active item after navigating into the 2nd buffer");
18760 let second_item_id = active_item.item_id();
18761 assert_ne!(
18762 second_item_id, multibuffer_item_id,
18763 "Should navigate away from the multibuffer"
18764 );
18765 assert_ne!(
18766 second_item_id, first_item_id,
18767 "Should navigate into the 2nd buffer and activate it"
18768 );
18769 assert!(
18770 active_item.is_singleton(cx),
18771 "New active item should be a singleton buffer"
18772 );
18773 assert_eq!(
18774 active_item
18775 .act_as::<Editor>(cx)
18776 .expect("should have navigated into an editor")
18777 .read(cx)
18778 .text(cx),
18779 sample_text_2
18780 );
18781
18782 workspace
18783 .go_back(workspace.active_pane().downgrade(), window, cx)
18784 .detach_and_log_err(cx);
18785
18786 second_item_id
18787 })
18788 .unwrap();
18789 cx.executor().run_until_parked();
18790 workspace
18791 .update(cx, |workspace, _, cx| {
18792 let active_item = workspace
18793 .active_item(cx)
18794 .expect("should have an active item after navigating back from the 2nd buffer");
18795 assert_eq!(
18796 active_item.item_id(),
18797 multibuffer_item_id,
18798 "Should navigate back from the 2nd buffer to the multi buffer"
18799 );
18800 assert!(!active_item.is_singleton(cx));
18801 })
18802 .unwrap();
18803
18804 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18805 editor.change_selections(
18806 SelectionEffects::scroll(Autoscroll::Next),
18807 window,
18808 cx,
18809 |s| s.select_ranges(Some(70..70)),
18810 );
18811 editor.open_excerpts(&OpenExcerpts, window, cx);
18812 });
18813 cx.executor().run_until_parked();
18814 workspace
18815 .update(cx, |workspace, window, cx| {
18816 let active_item = workspace
18817 .active_item(cx)
18818 .expect("should have an active item after navigating into the 3rd buffer");
18819 let third_item_id = active_item.item_id();
18820 assert_ne!(
18821 third_item_id, multibuffer_item_id,
18822 "Should navigate into the 3rd buffer and activate it"
18823 );
18824 assert_ne!(third_item_id, first_item_id);
18825 assert_ne!(third_item_id, second_item_id);
18826 assert!(
18827 active_item.is_singleton(cx),
18828 "New active item should be a singleton buffer"
18829 );
18830 assert_eq!(
18831 active_item
18832 .act_as::<Editor>(cx)
18833 .expect("should have navigated into an editor")
18834 .read(cx)
18835 .text(cx),
18836 sample_text_3
18837 );
18838
18839 workspace
18840 .go_back(workspace.active_pane().downgrade(), window, cx)
18841 .detach_and_log_err(cx);
18842 })
18843 .unwrap();
18844 cx.executor().run_until_parked();
18845 workspace
18846 .update(cx, |workspace, _, cx| {
18847 let active_item = workspace
18848 .active_item(cx)
18849 .expect("should have an active item after navigating back from the 3rd buffer");
18850 assert_eq!(
18851 active_item.item_id(),
18852 multibuffer_item_id,
18853 "Should navigate back from the 3rd buffer to the multi buffer"
18854 );
18855 assert!(!active_item.is_singleton(cx));
18856 })
18857 .unwrap();
18858}
18859
18860#[gpui::test]
18861async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
18862 init_test(cx, |_| {});
18863
18864 let mut cx = EditorTestContext::new(cx).await;
18865
18866 let diff_base = r#"
18867 use some::mod;
18868
18869 const A: u32 = 42;
18870
18871 fn main() {
18872 println!("hello");
18873
18874 println!("world");
18875 }
18876 "#
18877 .unindent();
18878
18879 cx.set_state(
18880 &r#"
18881 use some::modified;
18882
18883 ˇ
18884 fn main() {
18885 println!("hello there");
18886
18887 println!("around the");
18888 println!("world");
18889 }
18890 "#
18891 .unindent(),
18892 );
18893
18894 cx.set_head_text(&diff_base);
18895 executor.run_until_parked();
18896
18897 cx.update_editor(|editor, window, cx| {
18898 editor.go_to_next_hunk(&GoToHunk, window, cx);
18899 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
18900 });
18901 executor.run_until_parked();
18902 cx.assert_state_with_diff(
18903 r#"
18904 use some::modified;
18905
18906
18907 fn main() {
18908 - println!("hello");
18909 + ˇ println!("hello there");
18910
18911 println!("around the");
18912 println!("world");
18913 }
18914 "#
18915 .unindent(),
18916 );
18917
18918 cx.update_editor(|editor, window, cx| {
18919 for _ in 0..2 {
18920 editor.go_to_next_hunk(&GoToHunk, window, cx);
18921 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
18922 }
18923 });
18924 executor.run_until_parked();
18925 cx.assert_state_with_diff(
18926 r#"
18927 - use some::mod;
18928 + ˇuse some::modified;
18929
18930
18931 fn main() {
18932 - println!("hello");
18933 + println!("hello there");
18934
18935 + println!("around the");
18936 println!("world");
18937 }
18938 "#
18939 .unindent(),
18940 );
18941
18942 cx.update_editor(|editor, window, cx| {
18943 editor.go_to_next_hunk(&GoToHunk, window, cx);
18944 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
18945 });
18946 executor.run_until_parked();
18947 cx.assert_state_with_diff(
18948 r#"
18949 - use some::mod;
18950 + use some::modified;
18951
18952 - const A: u32 = 42;
18953 ˇ
18954 fn main() {
18955 - println!("hello");
18956 + println!("hello there");
18957
18958 + println!("around the");
18959 println!("world");
18960 }
18961 "#
18962 .unindent(),
18963 );
18964
18965 cx.update_editor(|editor, window, cx| {
18966 editor.cancel(&Cancel, window, cx);
18967 });
18968
18969 cx.assert_state_with_diff(
18970 r#"
18971 use some::modified;
18972
18973 ˇ
18974 fn main() {
18975 println!("hello there");
18976
18977 println!("around the");
18978 println!("world");
18979 }
18980 "#
18981 .unindent(),
18982 );
18983}
18984
18985#[gpui::test]
18986async fn test_diff_base_change_with_expanded_diff_hunks(
18987 executor: BackgroundExecutor,
18988 cx: &mut TestAppContext,
18989) {
18990 init_test(cx, |_| {});
18991
18992 let mut cx = EditorTestContext::new(cx).await;
18993
18994 let diff_base = r#"
18995 use some::mod1;
18996 use some::mod2;
18997
18998 const A: u32 = 42;
18999 const B: u32 = 42;
19000 const C: u32 = 42;
19001
19002 fn main() {
19003 println!("hello");
19004
19005 println!("world");
19006 }
19007 "#
19008 .unindent();
19009
19010 cx.set_state(
19011 &r#"
19012 use some::mod2;
19013
19014 const A: u32 = 42;
19015 const C: u32 = 42;
19016
19017 fn main(ˇ) {
19018 //println!("hello");
19019
19020 println!("world");
19021 //
19022 //
19023 }
19024 "#
19025 .unindent(),
19026 );
19027
19028 cx.set_head_text(&diff_base);
19029 executor.run_until_parked();
19030
19031 cx.update_editor(|editor, window, cx| {
19032 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19033 });
19034 executor.run_until_parked();
19035 cx.assert_state_with_diff(
19036 r#"
19037 - use some::mod1;
19038 use some::mod2;
19039
19040 const A: u32 = 42;
19041 - const B: u32 = 42;
19042 const C: u32 = 42;
19043
19044 fn main(ˇ) {
19045 - println!("hello");
19046 + //println!("hello");
19047
19048 println!("world");
19049 + //
19050 + //
19051 }
19052 "#
19053 .unindent(),
19054 );
19055
19056 cx.set_head_text("new diff base!");
19057 executor.run_until_parked();
19058 cx.assert_state_with_diff(
19059 r#"
19060 - new diff base!
19061 + use some::mod2;
19062 +
19063 + const A: u32 = 42;
19064 + const C: u32 = 42;
19065 +
19066 + fn main(ˇ) {
19067 + //println!("hello");
19068 +
19069 + println!("world");
19070 + //
19071 + //
19072 + }
19073 "#
19074 .unindent(),
19075 );
19076}
19077
19078#[gpui::test]
19079async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
19080 init_test(cx, |_| {});
19081
19082 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19083 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19084 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19085 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19086 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
19087 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
19088
19089 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
19090 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
19091 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
19092
19093 let multi_buffer = cx.new(|cx| {
19094 let mut multibuffer = MultiBuffer::new(ReadWrite);
19095 multibuffer.push_excerpts(
19096 buffer_1.clone(),
19097 [
19098 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19099 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19100 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19101 ],
19102 cx,
19103 );
19104 multibuffer.push_excerpts(
19105 buffer_2.clone(),
19106 [
19107 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19108 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19109 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19110 ],
19111 cx,
19112 );
19113 multibuffer.push_excerpts(
19114 buffer_3.clone(),
19115 [
19116 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19117 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19118 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19119 ],
19120 cx,
19121 );
19122 multibuffer
19123 });
19124
19125 let editor =
19126 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19127 editor
19128 .update(cx, |editor, _window, cx| {
19129 for (buffer, diff_base) in [
19130 (buffer_1.clone(), file_1_old),
19131 (buffer_2.clone(), file_2_old),
19132 (buffer_3.clone(), file_3_old),
19133 ] {
19134 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
19135 editor
19136 .buffer
19137 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
19138 }
19139 })
19140 .unwrap();
19141
19142 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19143 cx.run_until_parked();
19144
19145 cx.assert_editor_state(
19146 &"
19147 ˇaaa
19148 ccc
19149 ddd
19150
19151 ggg
19152 hhh
19153
19154
19155 lll
19156 mmm
19157 NNN
19158
19159 qqq
19160 rrr
19161
19162 uuu
19163 111
19164 222
19165 333
19166
19167 666
19168 777
19169
19170 000
19171 !!!"
19172 .unindent(),
19173 );
19174
19175 cx.update_editor(|editor, window, cx| {
19176 editor.select_all(&SelectAll, window, cx);
19177 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19178 });
19179 cx.executor().run_until_parked();
19180
19181 cx.assert_state_with_diff(
19182 "
19183 «aaa
19184 - bbb
19185 ccc
19186 ddd
19187
19188 ggg
19189 hhh
19190
19191
19192 lll
19193 mmm
19194 - nnn
19195 + NNN
19196
19197 qqq
19198 rrr
19199
19200 uuu
19201 111
19202 222
19203 333
19204
19205 + 666
19206 777
19207
19208 000
19209 !!!ˇ»"
19210 .unindent(),
19211 );
19212}
19213
19214#[gpui::test]
19215async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
19216 init_test(cx, |_| {});
19217
19218 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
19219 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
19220
19221 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
19222 let multi_buffer = cx.new(|cx| {
19223 let mut multibuffer = MultiBuffer::new(ReadWrite);
19224 multibuffer.push_excerpts(
19225 buffer.clone(),
19226 [
19227 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
19228 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
19229 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
19230 ],
19231 cx,
19232 );
19233 multibuffer
19234 });
19235
19236 let editor =
19237 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19238 editor
19239 .update(cx, |editor, _window, cx| {
19240 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
19241 editor
19242 .buffer
19243 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
19244 })
19245 .unwrap();
19246
19247 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19248 cx.run_until_parked();
19249
19250 cx.update_editor(|editor, window, cx| {
19251 editor.expand_all_diff_hunks(&Default::default(), window, cx)
19252 });
19253 cx.executor().run_until_parked();
19254
19255 // When the start of a hunk coincides with the start of its excerpt,
19256 // the hunk is expanded. When the start of a hunk is earlier than
19257 // the start of its excerpt, the hunk is not expanded.
19258 cx.assert_state_with_diff(
19259 "
19260 ˇaaa
19261 - bbb
19262 + BBB
19263
19264 - ddd
19265 - eee
19266 + DDD
19267 + EEE
19268 fff
19269
19270 iii
19271 "
19272 .unindent(),
19273 );
19274}
19275
19276#[gpui::test]
19277async fn test_edits_around_expanded_insertion_hunks(
19278 executor: BackgroundExecutor,
19279 cx: &mut TestAppContext,
19280) {
19281 init_test(cx, |_| {});
19282
19283 let mut cx = EditorTestContext::new(cx).await;
19284
19285 let diff_base = r#"
19286 use some::mod1;
19287 use some::mod2;
19288
19289 const A: u32 = 42;
19290
19291 fn main() {
19292 println!("hello");
19293
19294 println!("world");
19295 }
19296 "#
19297 .unindent();
19298 executor.run_until_parked();
19299 cx.set_state(
19300 &r#"
19301 use some::mod1;
19302 use some::mod2;
19303
19304 const A: u32 = 42;
19305 const B: u32 = 42;
19306 const C: u32 = 42;
19307 ˇ
19308
19309 fn main() {
19310 println!("hello");
19311
19312 println!("world");
19313 }
19314 "#
19315 .unindent(),
19316 );
19317
19318 cx.set_head_text(&diff_base);
19319 executor.run_until_parked();
19320
19321 cx.update_editor(|editor, window, cx| {
19322 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19323 });
19324 executor.run_until_parked();
19325
19326 cx.assert_state_with_diff(
19327 r#"
19328 use some::mod1;
19329 use some::mod2;
19330
19331 const A: u32 = 42;
19332 + const B: u32 = 42;
19333 + const C: u32 = 42;
19334 + ˇ
19335
19336 fn main() {
19337 println!("hello");
19338
19339 println!("world");
19340 }
19341 "#
19342 .unindent(),
19343 );
19344
19345 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
19346 executor.run_until_parked();
19347
19348 cx.assert_state_with_diff(
19349 r#"
19350 use some::mod1;
19351 use some::mod2;
19352
19353 const A: u32 = 42;
19354 + const B: u32 = 42;
19355 + const C: u32 = 42;
19356 + const D: u32 = 42;
19357 + ˇ
19358
19359 fn main() {
19360 println!("hello");
19361
19362 println!("world");
19363 }
19364 "#
19365 .unindent(),
19366 );
19367
19368 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
19369 executor.run_until_parked();
19370
19371 cx.assert_state_with_diff(
19372 r#"
19373 use some::mod1;
19374 use some::mod2;
19375
19376 const A: u32 = 42;
19377 + const B: u32 = 42;
19378 + const C: u32 = 42;
19379 + const D: u32 = 42;
19380 + const E: u32 = 42;
19381 + ˇ
19382
19383 fn main() {
19384 println!("hello");
19385
19386 println!("world");
19387 }
19388 "#
19389 .unindent(),
19390 );
19391
19392 cx.update_editor(|editor, window, cx| {
19393 editor.delete_line(&DeleteLine, window, cx);
19394 });
19395 executor.run_until_parked();
19396
19397 cx.assert_state_with_diff(
19398 r#"
19399 use some::mod1;
19400 use some::mod2;
19401
19402 const A: u32 = 42;
19403 + const B: u32 = 42;
19404 + const C: u32 = 42;
19405 + const D: u32 = 42;
19406 + const E: u32 = 42;
19407 ˇ
19408 fn main() {
19409 println!("hello");
19410
19411 println!("world");
19412 }
19413 "#
19414 .unindent(),
19415 );
19416
19417 cx.update_editor(|editor, window, cx| {
19418 editor.move_up(&MoveUp, window, cx);
19419 editor.delete_line(&DeleteLine, window, cx);
19420 editor.move_up(&MoveUp, window, cx);
19421 editor.delete_line(&DeleteLine, window, cx);
19422 editor.move_up(&MoveUp, window, cx);
19423 editor.delete_line(&DeleteLine, window, cx);
19424 });
19425 executor.run_until_parked();
19426 cx.assert_state_with_diff(
19427 r#"
19428 use some::mod1;
19429 use some::mod2;
19430
19431 const A: u32 = 42;
19432 + const B: u32 = 42;
19433 ˇ
19434 fn main() {
19435 println!("hello");
19436
19437 println!("world");
19438 }
19439 "#
19440 .unindent(),
19441 );
19442
19443 cx.update_editor(|editor, window, cx| {
19444 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
19445 editor.delete_line(&DeleteLine, window, cx);
19446 });
19447 executor.run_until_parked();
19448 cx.assert_state_with_diff(
19449 r#"
19450 ˇ
19451 fn main() {
19452 println!("hello");
19453
19454 println!("world");
19455 }
19456 "#
19457 .unindent(),
19458 );
19459}
19460
19461#[gpui::test]
19462async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
19463 init_test(cx, |_| {});
19464
19465 let mut cx = EditorTestContext::new(cx).await;
19466 cx.set_head_text(indoc! { "
19467 one
19468 two
19469 three
19470 four
19471 five
19472 "
19473 });
19474 cx.set_state(indoc! { "
19475 one
19476 ˇthree
19477 five
19478 "});
19479 cx.run_until_parked();
19480 cx.update_editor(|editor, window, cx| {
19481 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19482 });
19483 cx.assert_state_with_diff(
19484 indoc! { "
19485 one
19486 - two
19487 ˇthree
19488 - four
19489 five
19490 "}
19491 .to_string(),
19492 );
19493 cx.update_editor(|editor, window, cx| {
19494 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19495 });
19496
19497 cx.assert_state_with_diff(
19498 indoc! { "
19499 one
19500 ˇthree
19501 five
19502 "}
19503 .to_string(),
19504 );
19505
19506 cx.set_state(indoc! { "
19507 one
19508 ˇTWO
19509 three
19510 four
19511 five
19512 "});
19513 cx.run_until_parked();
19514 cx.update_editor(|editor, window, cx| {
19515 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19516 });
19517
19518 cx.assert_state_with_diff(
19519 indoc! { "
19520 one
19521 - two
19522 + ˇTWO
19523 three
19524 four
19525 five
19526 "}
19527 .to_string(),
19528 );
19529 cx.update_editor(|editor, window, cx| {
19530 editor.move_up(&Default::default(), window, cx);
19531 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19532 });
19533 cx.assert_state_with_diff(
19534 indoc! { "
19535 one
19536 ˇTWO
19537 three
19538 four
19539 five
19540 "}
19541 .to_string(),
19542 );
19543}
19544
19545#[gpui::test]
19546async fn test_edits_around_expanded_deletion_hunks(
19547 executor: BackgroundExecutor,
19548 cx: &mut TestAppContext,
19549) {
19550 init_test(cx, |_| {});
19551
19552 let mut cx = EditorTestContext::new(cx).await;
19553
19554 let diff_base = r#"
19555 use some::mod1;
19556 use some::mod2;
19557
19558 const A: u32 = 42;
19559 const B: u32 = 42;
19560 const C: u32 = 42;
19561
19562
19563 fn main() {
19564 println!("hello");
19565
19566 println!("world");
19567 }
19568 "#
19569 .unindent();
19570 executor.run_until_parked();
19571 cx.set_state(
19572 &r#"
19573 use some::mod1;
19574 use some::mod2;
19575
19576 ˇconst B: u32 = 42;
19577 const C: u32 = 42;
19578
19579
19580 fn main() {
19581 println!("hello");
19582
19583 println!("world");
19584 }
19585 "#
19586 .unindent(),
19587 );
19588
19589 cx.set_head_text(&diff_base);
19590 executor.run_until_parked();
19591
19592 cx.update_editor(|editor, window, cx| {
19593 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19594 });
19595 executor.run_until_parked();
19596
19597 cx.assert_state_with_diff(
19598 r#"
19599 use some::mod1;
19600 use some::mod2;
19601
19602 - const A: u32 = 42;
19603 ˇconst B: u32 = 42;
19604 const C: u32 = 42;
19605
19606
19607 fn main() {
19608 println!("hello");
19609
19610 println!("world");
19611 }
19612 "#
19613 .unindent(),
19614 );
19615
19616 cx.update_editor(|editor, window, cx| {
19617 editor.delete_line(&DeleteLine, window, cx);
19618 });
19619 executor.run_until_parked();
19620 cx.assert_state_with_diff(
19621 r#"
19622 use some::mod1;
19623 use some::mod2;
19624
19625 - const A: u32 = 42;
19626 - const B: u32 = 42;
19627 ˇconst C: u32 = 42;
19628
19629
19630 fn main() {
19631 println!("hello");
19632
19633 println!("world");
19634 }
19635 "#
19636 .unindent(),
19637 );
19638
19639 cx.update_editor(|editor, window, cx| {
19640 editor.delete_line(&DeleteLine, window, cx);
19641 });
19642 executor.run_until_parked();
19643 cx.assert_state_with_diff(
19644 r#"
19645 use some::mod1;
19646 use some::mod2;
19647
19648 - const A: u32 = 42;
19649 - const B: u32 = 42;
19650 - const C: u32 = 42;
19651 ˇ
19652
19653 fn main() {
19654 println!("hello");
19655
19656 println!("world");
19657 }
19658 "#
19659 .unindent(),
19660 );
19661
19662 cx.update_editor(|editor, window, cx| {
19663 editor.handle_input("replacement", window, cx);
19664 });
19665 executor.run_until_parked();
19666 cx.assert_state_with_diff(
19667 r#"
19668 use some::mod1;
19669 use some::mod2;
19670
19671 - const A: u32 = 42;
19672 - const B: u32 = 42;
19673 - const C: u32 = 42;
19674 -
19675 + replacementˇ
19676
19677 fn main() {
19678 println!("hello");
19679
19680 println!("world");
19681 }
19682 "#
19683 .unindent(),
19684 );
19685}
19686
19687#[gpui::test]
19688async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19689 init_test(cx, |_| {});
19690
19691 let mut cx = EditorTestContext::new(cx).await;
19692
19693 let base_text = r#"
19694 one
19695 two
19696 three
19697 four
19698 five
19699 "#
19700 .unindent();
19701 executor.run_until_parked();
19702 cx.set_state(
19703 &r#"
19704 one
19705 two
19706 fˇour
19707 five
19708 "#
19709 .unindent(),
19710 );
19711
19712 cx.set_head_text(&base_text);
19713 executor.run_until_parked();
19714
19715 cx.update_editor(|editor, window, cx| {
19716 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19717 });
19718 executor.run_until_parked();
19719
19720 cx.assert_state_with_diff(
19721 r#"
19722 one
19723 two
19724 - three
19725 fˇour
19726 five
19727 "#
19728 .unindent(),
19729 );
19730
19731 cx.update_editor(|editor, window, cx| {
19732 editor.backspace(&Backspace, window, cx);
19733 editor.backspace(&Backspace, window, cx);
19734 });
19735 executor.run_until_parked();
19736 cx.assert_state_with_diff(
19737 r#"
19738 one
19739 two
19740 - threeˇ
19741 - four
19742 + our
19743 five
19744 "#
19745 .unindent(),
19746 );
19747}
19748
19749#[gpui::test]
19750async fn test_edit_after_expanded_modification_hunk(
19751 executor: BackgroundExecutor,
19752 cx: &mut TestAppContext,
19753) {
19754 init_test(cx, |_| {});
19755
19756 let mut cx = EditorTestContext::new(cx).await;
19757
19758 let diff_base = r#"
19759 use some::mod1;
19760 use some::mod2;
19761
19762 const A: u32 = 42;
19763 const B: u32 = 42;
19764 const C: u32 = 42;
19765 const D: u32 = 42;
19766
19767
19768 fn main() {
19769 println!("hello");
19770
19771 println!("world");
19772 }"#
19773 .unindent();
19774
19775 cx.set_state(
19776 &r#"
19777 use some::mod1;
19778 use some::mod2;
19779
19780 const A: u32 = 42;
19781 const B: u32 = 42;
19782 const C: u32 = 43ˇ
19783 const D: u32 = 42;
19784
19785
19786 fn main() {
19787 println!("hello");
19788
19789 println!("world");
19790 }"#
19791 .unindent(),
19792 );
19793
19794 cx.set_head_text(&diff_base);
19795 executor.run_until_parked();
19796 cx.update_editor(|editor, window, cx| {
19797 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19798 });
19799 executor.run_until_parked();
19800
19801 cx.assert_state_with_diff(
19802 r#"
19803 use some::mod1;
19804 use some::mod2;
19805
19806 const A: u32 = 42;
19807 const B: u32 = 42;
19808 - const C: u32 = 42;
19809 + const C: u32 = 43ˇ
19810 const D: u32 = 42;
19811
19812
19813 fn main() {
19814 println!("hello");
19815
19816 println!("world");
19817 }"#
19818 .unindent(),
19819 );
19820
19821 cx.update_editor(|editor, window, cx| {
19822 editor.handle_input("\nnew_line\n", window, cx);
19823 });
19824 executor.run_until_parked();
19825
19826 cx.assert_state_with_diff(
19827 r#"
19828 use some::mod1;
19829 use some::mod2;
19830
19831 const A: u32 = 42;
19832 const B: u32 = 42;
19833 - const C: u32 = 42;
19834 + const C: u32 = 43
19835 + new_line
19836 + ˇ
19837 const D: u32 = 42;
19838
19839
19840 fn main() {
19841 println!("hello");
19842
19843 println!("world");
19844 }"#
19845 .unindent(),
19846 );
19847}
19848
19849#[gpui::test]
19850async fn test_stage_and_unstage_added_file_hunk(
19851 executor: BackgroundExecutor,
19852 cx: &mut TestAppContext,
19853) {
19854 init_test(cx, |_| {});
19855
19856 let mut cx = EditorTestContext::new(cx).await;
19857 cx.update_editor(|editor, _, cx| {
19858 editor.set_expand_all_diff_hunks(cx);
19859 });
19860
19861 let working_copy = r#"
19862 ˇfn main() {
19863 println!("hello, world!");
19864 }
19865 "#
19866 .unindent();
19867
19868 cx.set_state(&working_copy);
19869 executor.run_until_parked();
19870
19871 cx.assert_state_with_diff(
19872 r#"
19873 + ˇfn main() {
19874 + println!("hello, world!");
19875 + }
19876 "#
19877 .unindent(),
19878 );
19879 cx.assert_index_text(None);
19880
19881 cx.update_editor(|editor, window, cx| {
19882 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19883 });
19884 executor.run_until_parked();
19885 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
19886 cx.assert_state_with_diff(
19887 r#"
19888 + ˇfn main() {
19889 + println!("hello, world!");
19890 + }
19891 "#
19892 .unindent(),
19893 );
19894
19895 cx.update_editor(|editor, window, cx| {
19896 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19897 });
19898 executor.run_until_parked();
19899 cx.assert_index_text(None);
19900}
19901
19902async fn setup_indent_guides_editor(
19903 text: &str,
19904 cx: &mut TestAppContext,
19905) -> (BufferId, EditorTestContext) {
19906 init_test(cx, |_| {});
19907
19908 let mut cx = EditorTestContext::new(cx).await;
19909
19910 let buffer_id = cx.update_editor(|editor, window, cx| {
19911 editor.set_text(text, window, cx);
19912 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
19913
19914 buffer_ids[0]
19915 });
19916
19917 (buffer_id, cx)
19918}
19919
19920fn assert_indent_guides(
19921 range: Range<u32>,
19922 expected: Vec<IndentGuide>,
19923 active_indices: Option<Vec<usize>>,
19924 cx: &mut EditorTestContext,
19925) {
19926 let indent_guides = cx.update_editor(|editor, window, cx| {
19927 let snapshot = editor.snapshot(window, cx).display_snapshot;
19928 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
19929 editor,
19930 MultiBufferRow(range.start)..MultiBufferRow(range.end),
19931 true,
19932 &snapshot,
19933 cx,
19934 );
19935
19936 indent_guides.sort_by(|a, b| {
19937 a.depth.cmp(&b.depth).then(
19938 a.start_row
19939 .cmp(&b.start_row)
19940 .then(a.end_row.cmp(&b.end_row)),
19941 )
19942 });
19943 indent_guides
19944 });
19945
19946 if let Some(expected) = active_indices {
19947 let active_indices = cx.update_editor(|editor, window, cx| {
19948 let snapshot = editor.snapshot(window, cx).display_snapshot;
19949 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
19950 });
19951
19952 assert_eq!(
19953 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
19954 expected,
19955 "Active indent guide indices do not match"
19956 );
19957 }
19958
19959 assert_eq!(indent_guides, expected, "Indent guides do not match");
19960}
19961
19962fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
19963 IndentGuide {
19964 buffer_id,
19965 start_row: MultiBufferRow(start_row),
19966 end_row: MultiBufferRow(end_row),
19967 depth,
19968 tab_size: 4,
19969 settings: IndentGuideSettings {
19970 enabled: true,
19971 line_width: 1,
19972 active_line_width: 1,
19973 coloring: IndentGuideColoring::default(),
19974 background_coloring: IndentGuideBackgroundColoring::default(),
19975 },
19976 }
19977}
19978
19979#[gpui::test]
19980async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
19981 let (buffer_id, mut cx) = setup_indent_guides_editor(
19982 &"
19983 fn main() {
19984 let a = 1;
19985 }"
19986 .unindent(),
19987 cx,
19988 )
19989 .await;
19990
19991 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
19992}
19993
19994#[gpui::test]
19995async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
19996 let (buffer_id, mut cx) = setup_indent_guides_editor(
19997 &"
19998 fn main() {
19999 let a = 1;
20000 let b = 2;
20001 }"
20002 .unindent(),
20003 cx,
20004 )
20005 .await;
20006
20007 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
20008}
20009
20010#[gpui::test]
20011async fn test_indent_guide_nested(cx: &mut TestAppContext) {
20012 let (buffer_id, mut cx) = setup_indent_guides_editor(
20013 &"
20014 fn main() {
20015 let a = 1;
20016 if a == 3 {
20017 let b = 2;
20018 } else {
20019 let c = 3;
20020 }
20021 }"
20022 .unindent(),
20023 cx,
20024 )
20025 .await;
20026
20027 assert_indent_guides(
20028 0..8,
20029 vec![
20030 indent_guide(buffer_id, 1, 6, 0),
20031 indent_guide(buffer_id, 3, 3, 1),
20032 indent_guide(buffer_id, 5, 5, 1),
20033 ],
20034 None,
20035 &mut cx,
20036 );
20037}
20038
20039#[gpui::test]
20040async fn test_indent_guide_tab(cx: &mut TestAppContext) {
20041 let (buffer_id, mut cx) = setup_indent_guides_editor(
20042 &"
20043 fn main() {
20044 let a = 1;
20045 let b = 2;
20046 let c = 3;
20047 }"
20048 .unindent(),
20049 cx,
20050 )
20051 .await;
20052
20053 assert_indent_guides(
20054 0..5,
20055 vec![
20056 indent_guide(buffer_id, 1, 3, 0),
20057 indent_guide(buffer_id, 2, 2, 1),
20058 ],
20059 None,
20060 &mut cx,
20061 );
20062}
20063
20064#[gpui::test]
20065async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
20066 let (buffer_id, mut cx) = setup_indent_guides_editor(
20067 &"
20068 fn main() {
20069 let a = 1;
20070
20071 let c = 3;
20072 }"
20073 .unindent(),
20074 cx,
20075 )
20076 .await;
20077
20078 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
20079}
20080
20081#[gpui::test]
20082async fn test_indent_guide_complex(cx: &mut TestAppContext) {
20083 let (buffer_id, mut cx) = setup_indent_guides_editor(
20084 &"
20085 fn main() {
20086 let a = 1;
20087
20088 let c = 3;
20089
20090 if a == 3 {
20091 let b = 2;
20092 } else {
20093 let c = 3;
20094 }
20095 }"
20096 .unindent(),
20097 cx,
20098 )
20099 .await;
20100
20101 assert_indent_guides(
20102 0..11,
20103 vec![
20104 indent_guide(buffer_id, 1, 9, 0),
20105 indent_guide(buffer_id, 6, 6, 1),
20106 indent_guide(buffer_id, 8, 8, 1),
20107 ],
20108 None,
20109 &mut cx,
20110 );
20111}
20112
20113#[gpui::test]
20114async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
20115 let (buffer_id, mut cx) = setup_indent_guides_editor(
20116 &"
20117 fn main() {
20118 let a = 1;
20119
20120 let c = 3;
20121
20122 if a == 3 {
20123 let b = 2;
20124 } else {
20125 let c = 3;
20126 }
20127 }"
20128 .unindent(),
20129 cx,
20130 )
20131 .await;
20132
20133 assert_indent_guides(
20134 1..11,
20135 vec![
20136 indent_guide(buffer_id, 1, 9, 0),
20137 indent_guide(buffer_id, 6, 6, 1),
20138 indent_guide(buffer_id, 8, 8, 1),
20139 ],
20140 None,
20141 &mut cx,
20142 );
20143}
20144
20145#[gpui::test]
20146async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
20147 let (buffer_id, mut cx) = setup_indent_guides_editor(
20148 &"
20149 fn main() {
20150 let a = 1;
20151
20152 let c = 3;
20153
20154 if a == 3 {
20155 let b = 2;
20156 } else {
20157 let c = 3;
20158 }
20159 }"
20160 .unindent(),
20161 cx,
20162 )
20163 .await;
20164
20165 assert_indent_guides(
20166 1..10,
20167 vec![
20168 indent_guide(buffer_id, 1, 9, 0),
20169 indent_guide(buffer_id, 6, 6, 1),
20170 indent_guide(buffer_id, 8, 8, 1),
20171 ],
20172 None,
20173 &mut cx,
20174 );
20175}
20176
20177#[gpui::test]
20178async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
20179 let (buffer_id, mut cx) = setup_indent_guides_editor(
20180 &"
20181 fn main() {
20182 if a {
20183 b(
20184 c,
20185 d,
20186 )
20187 } else {
20188 e(
20189 f
20190 )
20191 }
20192 }"
20193 .unindent(),
20194 cx,
20195 )
20196 .await;
20197
20198 assert_indent_guides(
20199 0..11,
20200 vec![
20201 indent_guide(buffer_id, 1, 10, 0),
20202 indent_guide(buffer_id, 2, 5, 1),
20203 indent_guide(buffer_id, 7, 9, 1),
20204 indent_guide(buffer_id, 3, 4, 2),
20205 indent_guide(buffer_id, 8, 8, 2),
20206 ],
20207 None,
20208 &mut cx,
20209 );
20210
20211 cx.update_editor(|editor, window, cx| {
20212 editor.fold_at(MultiBufferRow(2), window, cx);
20213 assert_eq!(
20214 editor.display_text(cx),
20215 "
20216 fn main() {
20217 if a {
20218 b(⋯
20219 )
20220 } else {
20221 e(
20222 f
20223 )
20224 }
20225 }"
20226 .unindent()
20227 );
20228 });
20229
20230 assert_indent_guides(
20231 0..11,
20232 vec![
20233 indent_guide(buffer_id, 1, 10, 0),
20234 indent_guide(buffer_id, 2, 5, 1),
20235 indent_guide(buffer_id, 7, 9, 1),
20236 indent_guide(buffer_id, 8, 8, 2),
20237 ],
20238 None,
20239 &mut cx,
20240 );
20241}
20242
20243#[gpui::test]
20244async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
20245 let (buffer_id, mut cx) = setup_indent_guides_editor(
20246 &"
20247 block1
20248 block2
20249 block3
20250 block4
20251 block2
20252 block1
20253 block1"
20254 .unindent(),
20255 cx,
20256 )
20257 .await;
20258
20259 assert_indent_guides(
20260 1..10,
20261 vec![
20262 indent_guide(buffer_id, 1, 4, 0),
20263 indent_guide(buffer_id, 2, 3, 1),
20264 indent_guide(buffer_id, 3, 3, 2),
20265 ],
20266 None,
20267 &mut cx,
20268 );
20269}
20270
20271#[gpui::test]
20272async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
20273 let (buffer_id, mut cx) = setup_indent_guides_editor(
20274 &"
20275 block1
20276 block2
20277 block3
20278
20279 block1
20280 block1"
20281 .unindent(),
20282 cx,
20283 )
20284 .await;
20285
20286 assert_indent_guides(
20287 0..6,
20288 vec![
20289 indent_guide(buffer_id, 1, 2, 0),
20290 indent_guide(buffer_id, 2, 2, 1),
20291 ],
20292 None,
20293 &mut cx,
20294 );
20295}
20296
20297#[gpui::test]
20298async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
20299 let (buffer_id, mut cx) = setup_indent_guides_editor(
20300 &"
20301 function component() {
20302 \treturn (
20303 \t\t\t
20304 \t\t<div>
20305 \t\t\t<abc></abc>
20306 \t\t</div>
20307 \t)
20308 }"
20309 .unindent(),
20310 cx,
20311 )
20312 .await;
20313
20314 assert_indent_guides(
20315 0..8,
20316 vec![
20317 indent_guide(buffer_id, 1, 6, 0),
20318 indent_guide(buffer_id, 2, 5, 1),
20319 indent_guide(buffer_id, 4, 4, 2),
20320 ],
20321 None,
20322 &mut cx,
20323 );
20324}
20325
20326#[gpui::test]
20327async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
20328 let (buffer_id, mut cx) = setup_indent_guides_editor(
20329 &"
20330 function component() {
20331 \treturn (
20332 \t
20333 \t\t<div>
20334 \t\t\t<abc></abc>
20335 \t\t</div>
20336 \t)
20337 }"
20338 .unindent(),
20339 cx,
20340 )
20341 .await;
20342
20343 assert_indent_guides(
20344 0..8,
20345 vec![
20346 indent_guide(buffer_id, 1, 6, 0),
20347 indent_guide(buffer_id, 2, 5, 1),
20348 indent_guide(buffer_id, 4, 4, 2),
20349 ],
20350 None,
20351 &mut cx,
20352 );
20353}
20354
20355#[gpui::test]
20356async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
20357 let (buffer_id, mut cx) = setup_indent_guides_editor(
20358 &"
20359 block1
20360
20361
20362
20363 block2
20364 "
20365 .unindent(),
20366 cx,
20367 )
20368 .await;
20369
20370 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20371}
20372
20373#[gpui::test]
20374async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
20375 let (buffer_id, mut cx) = setup_indent_guides_editor(
20376 &"
20377 def a:
20378 \tb = 3
20379 \tif True:
20380 \t\tc = 4
20381 \t\td = 5
20382 \tprint(b)
20383 "
20384 .unindent(),
20385 cx,
20386 )
20387 .await;
20388
20389 assert_indent_guides(
20390 0..6,
20391 vec![
20392 indent_guide(buffer_id, 1, 5, 0),
20393 indent_guide(buffer_id, 3, 4, 1),
20394 ],
20395 None,
20396 &mut cx,
20397 );
20398}
20399
20400#[gpui::test]
20401async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
20402 let (buffer_id, mut cx) = setup_indent_guides_editor(
20403 &"
20404 fn main() {
20405 let a = 1;
20406 }"
20407 .unindent(),
20408 cx,
20409 )
20410 .await;
20411
20412 cx.update_editor(|editor, window, cx| {
20413 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20414 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20415 });
20416 });
20417
20418 assert_indent_guides(
20419 0..3,
20420 vec![indent_guide(buffer_id, 1, 1, 0)],
20421 Some(vec![0]),
20422 &mut cx,
20423 );
20424}
20425
20426#[gpui::test]
20427async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
20428 let (buffer_id, mut cx) = setup_indent_guides_editor(
20429 &"
20430 fn main() {
20431 if 1 == 2 {
20432 let a = 1;
20433 }
20434 }"
20435 .unindent(),
20436 cx,
20437 )
20438 .await;
20439
20440 cx.update_editor(|editor, window, cx| {
20441 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20442 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20443 });
20444 });
20445
20446 assert_indent_guides(
20447 0..4,
20448 vec![
20449 indent_guide(buffer_id, 1, 3, 0),
20450 indent_guide(buffer_id, 2, 2, 1),
20451 ],
20452 Some(vec![1]),
20453 &mut cx,
20454 );
20455
20456 cx.update_editor(|editor, window, cx| {
20457 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20458 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20459 });
20460 });
20461
20462 assert_indent_guides(
20463 0..4,
20464 vec![
20465 indent_guide(buffer_id, 1, 3, 0),
20466 indent_guide(buffer_id, 2, 2, 1),
20467 ],
20468 Some(vec![1]),
20469 &mut cx,
20470 );
20471
20472 cx.update_editor(|editor, window, cx| {
20473 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20474 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
20475 });
20476 });
20477
20478 assert_indent_guides(
20479 0..4,
20480 vec![
20481 indent_guide(buffer_id, 1, 3, 0),
20482 indent_guide(buffer_id, 2, 2, 1),
20483 ],
20484 Some(vec![0]),
20485 &mut cx,
20486 );
20487}
20488
20489#[gpui::test]
20490async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
20491 let (buffer_id, mut cx) = setup_indent_guides_editor(
20492 &"
20493 fn main() {
20494 let a = 1;
20495
20496 let b = 2;
20497 }"
20498 .unindent(),
20499 cx,
20500 )
20501 .await;
20502
20503 cx.update_editor(|editor, window, cx| {
20504 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20505 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20506 });
20507 });
20508
20509 assert_indent_guides(
20510 0..5,
20511 vec![indent_guide(buffer_id, 1, 3, 0)],
20512 Some(vec![0]),
20513 &mut cx,
20514 );
20515}
20516
20517#[gpui::test]
20518async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
20519 let (buffer_id, mut cx) = setup_indent_guides_editor(
20520 &"
20521 def m:
20522 a = 1
20523 pass"
20524 .unindent(),
20525 cx,
20526 )
20527 .await;
20528
20529 cx.update_editor(|editor, window, cx| {
20530 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20531 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20532 });
20533 });
20534
20535 assert_indent_guides(
20536 0..3,
20537 vec![indent_guide(buffer_id, 1, 2, 0)],
20538 Some(vec![0]),
20539 &mut cx,
20540 );
20541}
20542
20543#[gpui::test]
20544async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
20545 init_test(cx, |_| {});
20546 let mut cx = EditorTestContext::new(cx).await;
20547 let text = indoc! {
20548 "
20549 impl A {
20550 fn b() {
20551 0;
20552 3;
20553 5;
20554 6;
20555 7;
20556 }
20557 }
20558 "
20559 };
20560 let base_text = indoc! {
20561 "
20562 impl A {
20563 fn b() {
20564 0;
20565 1;
20566 2;
20567 3;
20568 4;
20569 }
20570 fn c() {
20571 5;
20572 6;
20573 7;
20574 }
20575 }
20576 "
20577 };
20578
20579 cx.update_editor(|editor, window, cx| {
20580 editor.set_text(text, window, cx);
20581
20582 editor.buffer().update(cx, |multibuffer, cx| {
20583 let buffer = multibuffer.as_singleton().unwrap();
20584 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
20585
20586 multibuffer.set_all_diff_hunks_expanded(cx);
20587 multibuffer.add_diff(diff, cx);
20588
20589 buffer.read(cx).remote_id()
20590 })
20591 });
20592 cx.run_until_parked();
20593
20594 cx.assert_state_with_diff(
20595 indoc! { "
20596 impl A {
20597 fn b() {
20598 0;
20599 - 1;
20600 - 2;
20601 3;
20602 - 4;
20603 - }
20604 - fn c() {
20605 5;
20606 6;
20607 7;
20608 }
20609 }
20610 ˇ"
20611 }
20612 .to_string(),
20613 );
20614
20615 let mut actual_guides = cx.update_editor(|editor, window, cx| {
20616 editor
20617 .snapshot(window, cx)
20618 .buffer_snapshot
20619 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
20620 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
20621 .collect::<Vec<_>>()
20622 });
20623 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
20624 assert_eq!(
20625 actual_guides,
20626 vec![
20627 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
20628 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
20629 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
20630 ]
20631 );
20632}
20633
20634#[gpui::test]
20635async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
20636 init_test(cx, |_| {});
20637 let mut cx = EditorTestContext::new(cx).await;
20638
20639 let diff_base = r#"
20640 a
20641 b
20642 c
20643 "#
20644 .unindent();
20645
20646 cx.set_state(
20647 &r#"
20648 ˇA
20649 b
20650 C
20651 "#
20652 .unindent(),
20653 );
20654 cx.set_head_text(&diff_base);
20655 cx.update_editor(|editor, window, cx| {
20656 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20657 });
20658 executor.run_until_parked();
20659
20660 let both_hunks_expanded = r#"
20661 - a
20662 + ˇA
20663 b
20664 - c
20665 + C
20666 "#
20667 .unindent();
20668
20669 cx.assert_state_with_diff(both_hunks_expanded.clone());
20670
20671 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20672 let snapshot = editor.snapshot(window, cx);
20673 let hunks = editor
20674 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
20675 .collect::<Vec<_>>();
20676 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20677 let buffer_id = hunks[0].buffer_id;
20678 hunks
20679 .into_iter()
20680 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20681 .collect::<Vec<_>>()
20682 });
20683 assert_eq!(hunk_ranges.len(), 2);
20684
20685 cx.update_editor(|editor, _, cx| {
20686 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20687 });
20688 executor.run_until_parked();
20689
20690 let second_hunk_expanded = r#"
20691 ˇA
20692 b
20693 - c
20694 + C
20695 "#
20696 .unindent();
20697
20698 cx.assert_state_with_diff(second_hunk_expanded);
20699
20700 cx.update_editor(|editor, _, cx| {
20701 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20702 });
20703 executor.run_until_parked();
20704
20705 cx.assert_state_with_diff(both_hunks_expanded.clone());
20706
20707 cx.update_editor(|editor, _, cx| {
20708 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20709 });
20710 executor.run_until_parked();
20711
20712 let first_hunk_expanded = r#"
20713 - a
20714 + ˇA
20715 b
20716 C
20717 "#
20718 .unindent();
20719
20720 cx.assert_state_with_diff(first_hunk_expanded);
20721
20722 cx.update_editor(|editor, _, cx| {
20723 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20724 });
20725 executor.run_until_parked();
20726
20727 cx.assert_state_with_diff(both_hunks_expanded);
20728
20729 cx.set_state(
20730 &r#"
20731 ˇA
20732 b
20733 "#
20734 .unindent(),
20735 );
20736 cx.run_until_parked();
20737
20738 // TODO this cursor position seems bad
20739 cx.assert_state_with_diff(
20740 r#"
20741 - ˇa
20742 + A
20743 b
20744 "#
20745 .unindent(),
20746 );
20747
20748 cx.update_editor(|editor, window, cx| {
20749 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20750 });
20751
20752 cx.assert_state_with_diff(
20753 r#"
20754 - ˇa
20755 + A
20756 b
20757 - c
20758 "#
20759 .unindent(),
20760 );
20761
20762 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20763 let snapshot = editor.snapshot(window, cx);
20764 let hunks = editor
20765 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
20766 .collect::<Vec<_>>();
20767 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20768 let buffer_id = hunks[0].buffer_id;
20769 hunks
20770 .into_iter()
20771 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20772 .collect::<Vec<_>>()
20773 });
20774 assert_eq!(hunk_ranges.len(), 2);
20775
20776 cx.update_editor(|editor, _, cx| {
20777 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20778 });
20779 executor.run_until_parked();
20780
20781 cx.assert_state_with_diff(
20782 r#"
20783 - ˇa
20784 + A
20785 b
20786 "#
20787 .unindent(),
20788 );
20789}
20790
20791#[gpui::test]
20792async fn test_toggle_deletion_hunk_at_start_of_file(
20793 executor: BackgroundExecutor,
20794 cx: &mut TestAppContext,
20795) {
20796 init_test(cx, |_| {});
20797 let mut cx = EditorTestContext::new(cx).await;
20798
20799 let diff_base = r#"
20800 a
20801 b
20802 c
20803 "#
20804 .unindent();
20805
20806 cx.set_state(
20807 &r#"
20808 ˇb
20809 c
20810 "#
20811 .unindent(),
20812 );
20813 cx.set_head_text(&diff_base);
20814 cx.update_editor(|editor, window, cx| {
20815 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20816 });
20817 executor.run_until_parked();
20818
20819 let hunk_expanded = r#"
20820 - a
20821 ˇb
20822 c
20823 "#
20824 .unindent();
20825
20826 cx.assert_state_with_diff(hunk_expanded.clone());
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(), 1);
20841
20842 cx.update_editor(|editor, _, cx| {
20843 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20844 });
20845 executor.run_until_parked();
20846
20847 let hunk_collapsed = r#"
20848 ˇb
20849 c
20850 "#
20851 .unindent();
20852
20853 cx.assert_state_with_diff(hunk_collapsed);
20854
20855 cx.update_editor(|editor, _, cx| {
20856 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20857 });
20858 executor.run_until_parked();
20859
20860 cx.assert_state_with_diff(hunk_expanded);
20861}
20862
20863#[gpui::test]
20864async fn test_display_diff_hunks(cx: &mut TestAppContext) {
20865 init_test(cx, |_| {});
20866
20867 let fs = FakeFs::new(cx.executor());
20868 fs.insert_tree(
20869 path!("/test"),
20870 json!({
20871 ".git": {},
20872 "file-1": "ONE\n",
20873 "file-2": "TWO\n",
20874 "file-3": "THREE\n",
20875 }),
20876 )
20877 .await;
20878
20879 fs.set_head_for_repo(
20880 path!("/test/.git").as_ref(),
20881 &[
20882 ("file-1", "one\n".into()),
20883 ("file-2", "two\n".into()),
20884 ("file-3", "three\n".into()),
20885 ],
20886 "deadbeef",
20887 );
20888
20889 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
20890 let mut buffers = vec![];
20891 for i in 1..=3 {
20892 let buffer = project
20893 .update(cx, |project, cx| {
20894 let path = format!(path!("/test/file-{}"), i);
20895 project.open_local_buffer(path, cx)
20896 })
20897 .await
20898 .unwrap();
20899 buffers.push(buffer);
20900 }
20901
20902 let multibuffer = cx.new(|cx| {
20903 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
20904 multibuffer.set_all_diff_hunks_expanded(cx);
20905 for buffer in &buffers {
20906 let snapshot = buffer.read(cx).snapshot();
20907 multibuffer.set_excerpts_for_path(
20908 PathKey::namespaced(
20909 0,
20910 buffer.read(cx).file().unwrap().path().as_unix_str().into(),
20911 ),
20912 buffer.clone(),
20913 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
20914 2,
20915 cx,
20916 );
20917 }
20918 multibuffer
20919 });
20920
20921 let editor = cx.add_window(|window, cx| {
20922 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
20923 });
20924 cx.run_until_parked();
20925
20926 let snapshot = editor
20927 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
20928 .unwrap();
20929 let hunks = snapshot
20930 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
20931 .map(|hunk| match hunk {
20932 DisplayDiffHunk::Unfolded {
20933 display_row_range, ..
20934 } => display_row_range,
20935 DisplayDiffHunk::Folded { .. } => unreachable!(),
20936 })
20937 .collect::<Vec<_>>();
20938 assert_eq!(
20939 hunks,
20940 [
20941 DisplayRow(2)..DisplayRow(4),
20942 DisplayRow(7)..DisplayRow(9),
20943 DisplayRow(12)..DisplayRow(14),
20944 ]
20945 );
20946}
20947
20948#[gpui::test]
20949async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
20950 init_test(cx, |_| {});
20951
20952 let mut cx = EditorTestContext::new(cx).await;
20953 cx.set_head_text(indoc! { "
20954 one
20955 two
20956 three
20957 four
20958 five
20959 "
20960 });
20961 cx.set_index_text(indoc! { "
20962 one
20963 two
20964 three
20965 four
20966 five
20967 "
20968 });
20969 cx.set_state(indoc! {"
20970 one
20971 TWO
20972 ˇTHREE
20973 FOUR
20974 five
20975 "});
20976 cx.run_until_parked();
20977 cx.update_editor(|editor, window, cx| {
20978 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20979 });
20980 cx.run_until_parked();
20981 cx.assert_index_text(Some(indoc! {"
20982 one
20983 TWO
20984 THREE
20985 FOUR
20986 five
20987 "}));
20988 cx.set_state(indoc! { "
20989 one
20990 TWO
20991 ˇTHREE-HUNDRED
20992 FOUR
20993 five
20994 "});
20995 cx.run_until_parked();
20996 cx.update_editor(|editor, window, cx| {
20997 let snapshot = editor.snapshot(window, cx);
20998 let hunks = editor
20999 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
21000 .collect::<Vec<_>>();
21001 assert_eq!(hunks.len(), 1);
21002 assert_eq!(
21003 hunks[0].status(),
21004 DiffHunkStatus {
21005 kind: DiffHunkStatusKind::Modified,
21006 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
21007 }
21008 );
21009
21010 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21011 });
21012 cx.run_until_parked();
21013 cx.assert_index_text(Some(indoc! {"
21014 one
21015 TWO
21016 THREE-HUNDRED
21017 FOUR
21018 five
21019 "}));
21020}
21021
21022#[gpui::test]
21023fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
21024 init_test(cx, |_| {});
21025
21026 let editor = cx.add_window(|window, cx| {
21027 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
21028 build_editor(buffer, window, cx)
21029 });
21030
21031 let render_args = Arc::new(Mutex::new(None));
21032 let snapshot = editor
21033 .update(cx, |editor, window, cx| {
21034 let snapshot = editor.buffer().read(cx).snapshot(cx);
21035 let range =
21036 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
21037
21038 struct RenderArgs {
21039 row: MultiBufferRow,
21040 folded: bool,
21041 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
21042 }
21043
21044 let crease = Crease::inline(
21045 range,
21046 FoldPlaceholder::test(),
21047 {
21048 let toggle_callback = render_args.clone();
21049 move |row, folded, callback, _window, _cx| {
21050 *toggle_callback.lock() = Some(RenderArgs {
21051 row,
21052 folded,
21053 callback,
21054 });
21055 div()
21056 }
21057 },
21058 |_row, _folded, _window, _cx| div(),
21059 );
21060
21061 editor.insert_creases(Some(crease), cx);
21062 let snapshot = editor.snapshot(window, cx);
21063 let _div =
21064 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
21065 snapshot
21066 })
21067 .unwrap();
21068
21069 let render_args = render_args.lock().take().unwrap();
21070 assert_eq!(render_args.row, MultiBufferRow(1));
21071 assert!(!render_args.folded);
21072 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21073
21074 cx.update_window(*editor, |_, window, cx| {
21075 (render_args.callback)(true, window, cx)
21076 })
21077 .unwrap();
21078 let snapshot = editor
21079 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21080 .unwrap();
21081 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
21082
21083 cx.update_window(*editor, |_, window, cx| {
21084 (render_args.callback)(false, window, cx)
21085 })
21086 .unwrap();
21087 let snapshot = editor
21088 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21089 .unwrap();
21090 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21091}
21092
21093#[gpui::test]
21094async fn test_input_text(cx: &mut TestAppContext) {
21095 init_test(cx, |_| {});
21096 let mut cx = EditorTestContext::new(cx).await;
21097
21098 cx.set_state(
21099 &r#"ˇone
21100 two
21101
21102 three
21103 fourˇ
21104 five
21105
21106 siˇx"#
21107 .unindent(),
21108 );
21109
21110 cx.dispatch_action(HandleInput(String::new()));
21111 cx.assert_editor_state(
21112 &r#"ˇone
21113 two
21114
21115 three
21116 fourˇ
21117 five
21118
21119 siˇx"#
21120 .unindent(),
21121 );
21122
21123 cx.dispatch_action(HandleInput("AAAA".to_string()));
21124 cx.assert_editor_state(
21125 &r#"AAAAˇone
21126 two
21127
21128 three
21129 fourAAAAˇ
21130 five
21131
21132 siAAAAˇx"#
21133 .unindent(),
21134 );
21135}
21136
21137#[gpui::test]
21138async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
21139 init_test(cx, |_| {});
21140
21141 let mut cx = EditorTestContext::new(cx).await;
21142 cx.set_state(
21143 r#"let foo = 1;
21144let foo = 2;
21145let foo = 3;
21146let fooˇ = 4;
21147let foo = 5;
21148let foo = 6;
21149let foo = 7;
21150let foo = 8;
21151let foo = 9;
21152let foo = 10;
21153let foo = 11;
21154let foo = 12;
21155let foo = 13;
21156let foo = 14;
21157let foo = 15;"#,
21158 );
21159
21160 cx.update_editor(|e, window, cx| {
21161 assert_eq!(
21162 e.next_scroll_position,
21163 NextScrollCursorCenterTopBottom::Center,
21164 "Default next scroll direction is center",
21165 );
21166
21167 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21168 assert_eq!(
21169 e.next_scroll_position,
21170 NextScrollCursorCenterTopBottom::Top,
21171 "After center, next scroll direction should be top",
21172 );
21173
21174 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21175 assert_eq!(
21176 e.next_scroll_position,
21177 NextScrollCursorCenterTopBottom::Bottom,
21178 "After top, next scroll direction should be bottom",
21179 );
21180
21181 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21182 assert_eq!(
21183 e.next_scroll_position,
21184 NextScrollCursorCenterTopBottom::Center,
21185 "After bottom, scrolling should start over",
21186 );
21187
21188 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21189 assert_eq!(
21190 e.next_scroll_position,
21191 NextScrollCursorCenterTopBottom::Top,
21192 "Scrolling continues if retriggered fast enough"
21193 );
21194 });
21195
21196 cx.executor()
21197 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
21198 cx.executor().run_until_parked();
21199 cx.update_editor(|e, _, _| {
21200 assert_eq!(
21201 e.next_scroll_position,
21202 NextScrollCursorCenterTopBottom::Center,
21203 "If scrolling is not triggered fast enough, it should reset"
21204 );
21205 });
21206}
21207
21208#[gpui::test]
21209async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
21210 init_test(cx, |_| {});
21211 let mut cx = EditorLspTestContext::new_rust(
21212 lsp::ServerCapabilities {
21213 definition_provider: Some(lsp::OneOf::Left(true)),
21214 references_provider: Some(lsp::OneOf::Left(true)),
21215 ..lsp::ServerCapabilities::default()
21216 },
21217 cx,
21218 )
21219 .await;
21220
21221 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
21222 let go_to_definition = cx
21223 .lsp
21224 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21225 move |params, _| async move {
21226 if empty_go_to_definition {
21227 Ok(None)
21228 } else {
21229 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
21230 uri: params.text_document_position_params.text_document.uri,
21231 range: lsp::Range::new(
21232 lsp::Position::new(4, 3),
21233 lsp::Position::new(4, 6),
21234 ),
21235 })))
21236 }
21237 },
21238 );
21239 let references = cx
21240 .lsp
21241 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21242 Ok(Some(vec![lsp::Location {
21243 uri: params.text_document_position.text_document.uri,
21244 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
21245 }]))
21246 });
21247 (go_to_definition, references)
21248 };
21249
21250 cx.set_state(
21251 &r#"fn one() {
21252 let mut a = ˇtwo();
21253 }
21254
21255 fn two() {}"#
21256 .unindent(),
21257 );
21258 set_up_lsp_handlers(false, &mut cx);
21259 let navigated = cx
21260 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21261 .await
21262 .expect("Failed to navigate to definition");
21263 assert_eq!(
21264 navigated,
21265 Navigated::Yes,
21266 "Should have navigated to definition from the GetDefinition response"
21267 );
21268 cx.assert_editor_state(
21269 &r#"fn one() {
21270 let mut a = two();
21271 }
21272
21273 fn «twoˇ»() {}"#
21274 .unindent(),
21275 );
21276
21277 let editors = cx.update_workspace(|workspace, _, cx| {
21278 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21279 });
21280 cx.update_editor(|_, _, test_editor_cx| {
21281 assert_eq!(
21282 editors.len(),
21283 1,
21284 "Initially, only one, test, editor should be open in the workspace"
21285 );
21286 assert_eq!(
21287 test_editor_cx.entity(),
21288 editors.last().expect("Asserted len is 1").clone()
21289 );
21290 });
21291
21292 set_up_lsp_handlers(true, &mut cx);
21293 let navigated = cx
21294 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21295 .await
21296 .expect("Failed to navigate to lookup references");
21297 assert_eq!(
21298 navigated,
21299 Navigated::Yes,
21300 "Should have navigated to references as a fallback after empty GoToDefinition response"
21301 );
21302 // We should not change the selections in the existing file,
21303 // if opening another milti buffer with the references
21304 cx.assert_editor_state(
21305 &r#"fn one() {
21306 let mut a = two();
21307 }
21308
21309 fn «twoˇ»() {}"#
21310 .unindent(),
21311 );
21312 let editors = cx.update_workspace(|workspace, _, cx| {
21313 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21314 });
21315 cx.update_editor(|_, _, test_editor_cx| {
21316 assert_eq!(
21317 editors.len(),
21318 2,
21319 "After falling back to references search, we open a new editor with the results"
21320 );
21321 let references_fallback_text = editors
21322 .into_iter()
21323 .find(|new_editor| *new_editor != test_editor_cx.entity())
21324 .expect("Should have one non-test editor now")
21325 .read(test_editor_cx)
21326 .text(test_editor_cx);
21327 assert_eq!(
21328 references_fallback_text, "fn one() {\n let mut a = two();\n}",
21329 "Should use the range from the references response and not the GoToDefinition one"
21330 );
21331 });
21332}
21333
21334#[gpui::test]
21335async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
21336 init_test(cx, |_| {});
21337 cx.update(|cx| {
21338 let mut editor_settings = EditorSettings::get_global(cx).clone();
21339 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
21340 EditorSettings::override_global(editor_settings, cx);
21341 });
21342 let mut cx = EditorLspTestContext::new_rust(
21343 lsp::ServerCapabilities {
21344 definition_provider: Some(lsp::OneOf::Left(true)),
21345 references_provider: Some(lsp::OneOf::Left(true)),
21346 ..lsp::ServerCapabilities::default()
21347 },
21348 cx,
21349 )
21350 .await;
21351 let original_state = r#"fn one() {
21352 let mut a = ˇtwo();
21353 }
21354
21355 fn two() {}"#
21356 .unindent();
21357 cx.set_state(&original_state);
21358
21359 let mut go_to_definition = cx
21360 .lsp
21361 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21362 move |_, _| async move { Ok(None) },
21363 );
21364 let _references = cx
21365 .lsp
21366 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
21367 panic!("Should not call for references with no go to definition fallback")
21368 });
21369
21370 let navigated = cx
21371 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21372 .await
21373 .expect("Failed to navigate to lookup references");
21374 go_to_definition
21375 .next()
21376 .await
21377 .expect("Should have called the go_to_definition handler");
21378
21379 assert_eq!(
21380 navigated,
21381 Navigated::No,
21382 "Should have navigated to references as a fallback after empty GoToDefinition response"
21383 );
21384 cx.assert_editor_state(&original_state);
21385 let editors = cx.update_workspace(|workspace, _, cx| {
21386 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21387 });
21388 cx.update_editor(|_, _, _| {
21389 assert_eq!(
21390 editors.len(),
21391 1,
21392 "After unsuccessful fallback, no other editor should have been opened"
21393 );
21394 });
21395}
21396
21397#[gpui::test]
21398async fn test_find_all_references_editor_reuse(cx: &mut TestAppContext) {
21399 init_test(cx, |_| {});
21400 let mut cx = EditorLspTestContext::new_rust(
21401 lsp::ServerCapabilities {
21402 references_provider: Some(lsp::OneOf::Left(true)),
21403 ..lsp::ServerCapabilities::default()
21404 },
21405 cx,
21406 )
21407 .await;
21408
21409 cx.set_state(
21410 &r#"
21411 fn one() {
21412 let mut a = two();
21413 }
21414
21415 fn ˇtwo() {}"#
21416 .unindent(),
21417 );
21418 cx.lsp
21419 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21420 Ok(Some(vec![
21421 lsp::Location {
21422 uri: params.text_document_position.text_document.uri.clone(),
21423 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
21424 },
21425 lsp::Location {
21426 uri: params.text_document_position.text_document.uri,
21427 range: lsp::Range::new(lsp::Position::new(4, 4), lsp::Position::new(4, 7)),
21428 },
21429 ]))
21430 });
21431 let navigated = cx
21432 .update_editor(|editor, window, cx| {
21433 editor.find_all_references(&FindAllReferences, window, cx)
21434 })
21435 .unwrap()
21436 .await
21437 .expect("Failed to navigate to references");
21438 assert_eq!(
21439 navigated,
21440 Navigated::Yes,
21441 "Should have navigated to references from the FindAllReferences response"
21442 );
21443 cx.assert_editor_state(
21444 &r#"fn one() {
21445 let mut a = two();
21446 }
21447
21448 fn ˇtwo() {}"#
21449 .unindent(),
21450 );
21451
21452 let editors = cx.update_workspace(|workspace, _, cx| {
21453 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21454 });
21455 cx.update_editor(|_, _, _| {
21456 assert_eq!(editors.len(), 2, "We should have opened a new multibuffer");
21457 });
21458
21459 cx.set_state(
21460 &r#"fn one() {
21461 let mut a = ˇtwo();
21462 }
21463
21464 fn two() {}"#
21465 .unindent(),
21466 );
21467 let navigated = cx
21468 .update_editor(|editor, window, cx| {
21469 editor.find_all_references(&FindAllReferences, window, cx)
21470 })
21471 .unwrap()
21472 .await
21473 .expect("Failed to navigate to references");
21474 assert_eq!(
21475 navigated,
21476 Navigated::Yes,
21477 "Should have navigated to references from the FindAllReferences response"
21478 );
21479 cx.assert_editor_state(
21480 &r#"fn one() {
21481 let mut a = ˇtwo();
21482 }
21483
21484 fn two() {}"#
21485 .unindent(),
21486 );
21487 let editors = cx.update_workspace(|workspace, _, cx| {
21488 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21489 });
21490 cx.update_editor(|_, _, _| {
21491 assert_eq!(
21492 editors.len(),
21493 2,
21494 "should have re-used the previous multibuffer"
21495 );
21496 });
21497
21498 cx.set_state(
21499 &r#"fn one() {
21500 let mut a = ˇtwo();
21501 }
21502 fn three() {}
21503 fn two() {}"#
21504 .unindent(),
21505 );
21506 cx.lsp
21507 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21508 Ok(Some(vec![
21509 lsp::Location {
21510 uri: params.text_document_position.text_document.uri.clone(),
21511 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
21512 },
21513 lsp::Location {
21514 uri: params.text_document_position.text_document.uri,
21515 range: lsp::Range::new(lsp::Position::new(5, 4), lsp::Position::new(5, 7)),
21516 },
21517 ]))
21518 });
21519 let navigated = cx
21520 .update_editor(|editor, window, cx| {
21521 editor.find_all_references(&FindAllReferences, window, cx)
21522 })
21523 .unwrap()
21524 .await
21525 .expect("Failed to navigate to references");
21526 assert_eq!(
21527 navigated,
21528 Navigated::Yes,
21529 "Should have navigated to references from the FindAllReferences response"
21530 );
21531 cx.assert_editor_state(
21532 &r#"fn one() {
21533 let mut a = ˇtwo();
21534 }
21535 fn three() {}
21536 fn two() {}"#
21537 .unindent(),
21538 );
21539 let editors = cx.update_workspace(|workspace, _, cx| {
21540 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21541 });
21542 cx.update_editor(|_, _, _| {
21543 assert_eq!(
21544 editors.len(),
21545 3,
21546 "should have used a new multibuffer as offsets changed"
21547 );
21548 });
21549}
21550#[gpui::test]
21551async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
21552 init_test(cx, |_| {});
21553
21554 let language = Arc::new(Language::new(
21555 LanguageConfig::default(),
21556 Some(tree_sitter_rust::LANGUAGE.into()),
21557 ));
21558
21559 let text = r#"
21560 #[cfg(test)]
21561 mod tests() {
21562 #[test]
21563 fn runnable_1() {
21564 let a = 1;
21565 }
21566
21567 #[test]
21568 fn runnable_2() {
21569 let a = 1;
21570 let b = 2;
21571 }
21572 }
21573 "#
21574 .unindent();
21575
21576 let fs = FakeFs::new(cx.executor());
21577 fs.insert_file("/file.rs", Default::default()).await;
21578
21579 let project = Project::test(fs, ["/a".as_ref()], cx).await;
21580 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21581 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21582 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
21583 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
21584
21585 let editor = cx.new_window_entity(|window, cx| {
21586 Editor::new(
21587 EditorMode::full(),
21588 multi_buffer,
21589 Some(project.clone()),
21590 window,
21591 cx,
21592 )
21593 });
21594
21595 editor.update_in(cx, |editor, window, cx| {
21596 let snapshot = editor.buffer().read(cx).snapshot(cx);
21597 editor.tasks.insert(
21598 (buffer.read(cx).remote_id(), 3),
21599 RunnableTasks {
21600 templates: vec![],
21601 offset: snapshot.anchor_before(43),
21602 column: 0,
21603 extra_variables: HashMap::default(),
21604 context_range: BufferOffset(43)..BufferOffset(85),
21605 },
21606 );
21607 editor.tasks.insert(
21608 (buffer.read(cx).remote_id(), 8),
21609 RunnableTasks {
21610 templates: vec![],
21611 offset: snapshot.anchor_before(86),
21612 column: 0,
21613 extra_variables: HashMap::default(),
21614 context_range: BufferOffset(86)..BufferOffset(191),
21615 },
21616 );
21617
21618 // Test finding task when cursor is inside function body
21619 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21620 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
21621 });
21622 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
21623 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
21624
21625 // Test finding task when cursor is on function name
21626 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21627 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
21628 });
21629 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
21630 assert_eq!(row, 8, "Should find task when cursor is on function name");
21631 });
21632}
21633
21634#[gpui::test]
21635async fn test_folding_buffers(cx: &mut TestAppContext) {
21636 init_test(cx, |_| {});
21637
21638 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
21639 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
21640 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
21641
21642 let fs = FakeFs::new(cx.executor());
21643 fs.insert_tree(
21644 path!("/a"),
21645 json!({
21646 "first.rs": sample_text_1,
21647 "second.rs": sample_text_2,
21648 "third.rs": sample_text_3,
21649 }),
21650 )
21651 .await;
21652 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21653 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21654 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21655 let worktree = project.update(cx, |project, cx| {
21656 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
21657 assert_eq!(worktrees.len(), 1);
21658 worktrees.pop().unwrap()
21659 });
21660 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
21661
21662 let buffer_1 = project
21663 .update(cx, |project, cx| {
21664 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
21665 })
21666 .await
21667 .unwrap();
21668 let buffer_2 = project
21669 .update(cx, |project, cx| {
21670 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
21671 })
21672 .await
21673 .unwrap();
21674 let buffer_3 = project
21675 .update(cx, |project, cx| {
21676 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
21677 })
21678 .await
21679 .unwrap();
21680
21681 let multi_buffer = cx.new(|cx| {
21682 let mut multi_buffer = MultiBuffer::new(ReadWrite);
21683 multi_buffer.push_excerpts(
21684 buffer_1.clone(),
21685 [
21686 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21687 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21688 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21689 ],
21690 cx,
21691 );
21692 multi_buffer.push_excerpts(
21693 buffer_2.clone(),
21694 [
21695 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21696 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21697 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21698 ],
21699 cx,
21700 );
21701 multi_buffer.push_excerpts(
21702 buffer_3.clone(),
21703 [
21704 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21705 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21706 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21707 ],
21708 cx,
21709 );
21710 multi_buffer
21711 });
21712 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
21713 Editor::new(
21714 EditorMode::full(),
21715 multi_buffer.clone(),
21716 Some(project.clone()),
21717 window,
21718 cx,
21719 )
21720 });
21721
21722 assert_eq!(
21723 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21724 "\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",
21725 );
21726
21727 multi_buffer_editor.update(cx, |editor, cx| {
21728 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
21729 });
21730 assert_eq!(
21731 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21732 "\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",
21733 "After folding the first buffer, its text should not be displayed"
21734 );
21735
21736 multi_buffer_editor.update(cx, |editor, cx| {
21737 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
21738 });
21739 assert_eq!(
21740 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21741 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
21742 "After folding the second buffer, its text should not be displayed"
21743 );
21744
21745 multi_buffer_editor.update(cx, |editor, cx| {
21746 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
21747 });
21748 assert_eq!(
21749 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21750 "\n\n\n\n\n",
21751 "After folding the third buffer, its text should not be displayed"
21752 );
21753
21754 // Emulate selection inside the fold logic, that should work
21755 multi_buffer_editor.update_in(cx, |editor, window, cx| {
21756 editor
21757 .snapshot(window, cx)
21758 .next_line_boundary(Point::new(0, 4));
21759 });
21760
21761 multi_buffer_editor.update(cx, |editor, cx| {
21762 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
21763 });
21764 assert_eq!(
21765 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21766 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
21767 "After unfolding the second buffer, its text should be displayed"
21768 );
21769
21770 // Typing inside of buffer 1 causes that buffer to be unfolded.
21771 multi_buffer_editor.update_in(cx, |editor, window, cx| {
21772 assert_eq!(
21773 multi_buffer
21774 .read(cx)
21775 .snapshot(cx)
21776 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
21777 .collect::<String>(),
21778 "bbbb"
21779 );
21780 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
21781 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
21782 });
21783 editor.handle_input("B", window, cx);
21784 });
21785
21786 assert_eq!(
21787 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21788 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
21789 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
21790 );
21791
21792 multi_buffer_editor.update(cx, |editor, cx| {
21793 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
21794 });
21795 assert_eq!(
21796 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21797 "\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",
21798 "After unfolding the all buffers, all original text should be displayed"
21799 );
21800}
21801
21802#[gpui::test]
21803async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
21804 init_test(cx, |_| {});
21805
21806 let sample_text_1 = "1111\n2222\n3333".to_string();
21807 let sample_text_2 = "4444\n5555\n6666".to_string();
21808 let sample_text_3 = "7777\n8888\n9999".to_string();
21809
21810 let fs = FakeFs::new(cx.executor());
21811 fs.insert_tree(
21812 path!("/a"),
21813 json!({
21814 "first.rs": sample_text_1,
21815 "second.rs": sample_text_2,
21816 "third.rs": sample_text_3,
21817 }),
21818 )
21819 .await;
21820 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21821 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21822 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21823 let worktree = project.update(cx, |project, cx| {
21824 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
21825 assert_eq!(worktrees.len(), 1);
21826 worktrees.pop().unwrap()
21827 });
21828 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
21829
21830 let buffer_1 = project
21831 .update(cx, |project, cx| {
21832 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
21833 })
21834 .await
21835 .unwrap();
21836 let buffer_2 = project
21837 .update(cx, |project, cx| {
21838 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
21839 })
21840 .await
21841 .unwrap();
21842 let buffer_3 = project
21843 .update(cx, |project, cx| {
21844 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
21845 })
21846 .await
21847 .unwrap();
21848
21849 let multi_buffer = cx.new(|cx| {
21850 let mut multi_buffer = MultiBuffer::new(ReadWrite);
21851 multi_buffer.push_excerpts(
21852 buffer_1.clone(),
21853 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
21854 cx,
21855 );
21856 multi_buffer.push_excerpts(
21857 buffer_2.clone(),
21858 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
21859 cx,
21860 );
21861 multi_buffer.push_excerpts(
21862 buffer_3.clone(),
21863 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
21864 cx,
21865 );
21866 multi_buffer
21867 });
21868
21869 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
21870 Editor::new(
21871 EditorMode::full(),
21872 multi_buffer,
21873 Some(project.clone()),
21874 window,
21875 cx,
21876 )
21877 });
21878
21879 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
21880 assert_eq!(
21881 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21882 full_text,
21883 );
21884
21885 multi_buffer_editor.update(cx, |editor, cx| {
21886 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
21887 });
21888 assert_eq!(
21889 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21890 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
21891 "After folding the first buffer, its text should not be displayed"
21892 );
21893
21894 multi_buffer_editor.update(cx, |editor, cx| {
21895 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
21896 });
21897
21898 assert_eq!(
21899 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21900 "\n\n\n\n\n\n7777\n8888\n9999",
21901 "After folding the second buffer, its text should not be displayed"
21902 );
21903
21904 multi_buffer_editor.update(cx, |editor, cx| {
21905 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
21906 });
21907 assert_eq!(
21908 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21909 "\n\n\n\n\n",
21910 "After folding the third buffer, its text should not be displayed"
21911 );
21912
21913 multi_buffer_editor.update(cx, |editor, cx| {
21914 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
21915 });
21916 assert_eq!(
21917 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21918 "\n\n\n\n4444\n5555\n6666\n\n",
21919 "After unfolding the second buffer, its text should be displayed"
21920 );
21921
21922 multi_buffer_editor.update(cx, |editor, cx| {
21923 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
21924 });
21925 assert_eq!(
21926 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21927 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
21928 "After unfolding the first buffer, its text should be displayed"
21929 );
21930
21931 multi_buffer_editor.update(cx, |editor, cx| {
21932 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
21933 });
21934 assert_eq!(
21935 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21936 full_text,
21937 "After unfolding all buffers, all original text should be displayed"
21938 );
21939}
21940
21941#[gpui::test]
21942async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
21943 init_test(cx, |_| {});
21944
21945 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
21946
21947 let fs = FakeFs::new(cx.executor());
21948 fs.insert_tree(
21949 path!("/a"),
21950 json!({
21951 "main.rs": sample_text,
21952 }),
21953 )
21954 .await;
21955 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21956 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21957 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21958 let worktree = project.update(cx, |project, cx| {
21959 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
21960 assert_eq!(worktrees.len(), 1);
21961 worktrees.pop().unwrap()
21962 });
21963 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
21964
21965 let buffer_1 = project
21966 .update(cx, |project, cx| {
21967 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
21968 })
21969 .await
21970 .unwrap();
21971
21972 let multi_buffer = cx.new(|cx| {
21973 let mut multi_buffer = MultiBuffer::new(ReadWrite);
21974 multi_buffer.push_excerpts(
21975 buffer_1.clone(),
21976 [ExcerptRange::new(
21977 Point::new(0, 0)
21978 ..Point::new(
21979 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
21980 0,
21981 ),
21982 )],
21983 cx,
21984 );
21985 multi_buffer
21986 });
21987 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
21988 Editor::new(
21989 EditorMode::full(),
21990 multi_buffer,
21991 Some(project.clone()),
21992 window,
21993 cx,
21994 )
21995 });
21996
21997 let selection_range = Point::new(1, 0)..Point::new(2, 0);
21998 multi_buffer_editor.update_in(cx, |editor, window, cx| {
21999 enum TestHighlight {}
22000 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
22001 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
22002 editor.highlight_text::<TestHighlight>(
22003 vec![highlight_range.clone()],
22004 HighlightStyle::color(Hsla::green()),
22005 cx,
22006 );
22007 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22008 s.select_ranges(Some(highlight_range))
22009 });
22010 });
22011
22012 let full_text = format!("\n\n{sample_text}");
22013 assert_eq!(
22014 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22015 full_text,
22016 );
22017}
22018
22019#[gpui::test]
22020async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
22021 init_test(cx, |_| {});
22022 cx.update(|cx| {
22023 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
22024 "keymaps/default-linux.json",
22025 cx,
22026 )
22027 .unwrap();
22028 cx.bind_keys(default_key_bindings);
22029 });
22030
22031 let (editor, cx) = cx.add_window_view(|window, cx| {
22032 let multi_buffer = MultiBuffer::build_multi(
22033 [
22034 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
22035 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
22036 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
22037 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
22038 ],
22039 cx,
22040 );
22041 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
22042
22043 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
22044 // fold all but the second buffer, so that we test navigating between two
22045 // adjacent folded buffers, as well as folded buffers at the start and
22046 // end the multibuffer
22047 editor.fold_buffer(buffer_ids[0], cx);
22048 editor.fold_buffer(buffer_ids[2], cx);
22049 editor.fold_buffer(buffer_ids[3], cx);
22050
22051 editor
22052 });
22053 cx.simulate_resize(size(px(1000.), px(1000.)));
22054
22055 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
22056 cx.assert_excerpts_with_selections(indoc! {"
22057 [EXCERPT]
22058 ˇ[FOLDED]
22059 [EXCERPT]
22060 a1
22061 b1
22062 [EXCERPT]
22063 [FOLDED]
22064 [EXCERPT]
22065 [FOLDED]
22066 "
22067 });
22068 cx.simulate_keystroke("down");
22069 cx.assert_excerpts_with_selections(indoc! {"
22070 [EXCERPT]
22071 [FOLDED]
22072 [EXCERPT]
22073 ˇa1
22074 b1
22075 [EXCERPT]
22076 [FOLDED]
22077 [EXCERPT]
22078 [FOLDED]
22079 "
22080 });
22081 cx.simulate_keystroke("down");
22082 cx.assert_excerpts_with_selections(indoc! {"
22083 [EXCERPT]
22084 [FOLDED]
22085 [EXCERPT]
22086 a1
22087 ˇb1
22088 [EXCERPT]
22089 [FOLDED]
22090 [EXCERPT]
22091 [FOLDED]
22092 "
22093 });
22094 cx.simulate_keystroke("down");
22095 cx.assert_excerpts_with_selections(indoc! {"
22096 [EXCERPT]
22097 [FOLDED]
22098 [EXCERPT]
22099 a1
22100 b1
22101 ˇ[EXCERPT]
22102 [FOLDED]
22103 [EXCERPT]
22104 [FOLDED]
22105 "
22106 });
22107 cx.simulate_keystroke("down");
22108 cx.assert_excerpts_with_selections(indoc! {"
22109 [EXCERPT]
22110 [FOLDED]
22111 [EXCERPT]
22112 a1
22113 b1
22114 [EXCERPT]
22115 ˇ[FOLDED]
22116 [EXCERPT]
22117 [FOLDED]
22118 "
22119 });
22120 for _ in 0..5 {
22121 cx.simulate_keystroke("down");
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 }
22135
22136 cx.simulate_keystroke("up");
22137 cx.assert_excerpts_with_selections(indoc! {"
22138 [EXCERPT]
22139 [FOLDED]
22140 [EXCERPT]
22141 a1
22142 b1
22143 [EXCERPT]
22144 ˇ[FOLDED]
22145 [EXCERPT]
22146 [FOLDED]
22147 "
22148 });
22149 cx.simulate_keystroke("up");
22150 cx.assert_excerpts_with_selections(indoc! {"
22151 [EXCERPT]
22152 [FOLDED]
22153 [EXCERPT]
22154 a1
22155 b1
22156 ˇ[EXCERPT]
22157 [FOLDED]
22158 [EXCERPT]
22159 [FOLDED]
22160 "
22161 });
22162 cx.simulate_keystroke("up");
22163 cx.assert_excerpts_with_selections(indoc! {"
22164 [EXCERPT]
22165 [FOLDED]
22166 [EXCERPT]
22167 a1
22168 ˇb1
22169 [EXCERPT]
22170 [FOLDED]
22171 [EXCERPT]
22172 [FOLDED]
22173 "
22174 });
22175 cx.simulate_keystroke("up");
22176 cx.assert_excerpts_with_selections(indoc! {"
22177 [EXCERPT]
22178 [FOLDED]
22179 [EXCERPT]
22180 ˇa1
22181 b1
22182 [EXCERPT]
22183 [FOLDED]
22184 [EXCERPT]
22185 [FOLDED]
22186 "
22187 });
22188 for _ in 0..5 {
22189 cx.simulate_keystroke("up");
22190 cx.assert_excerpts_with_selections(indoc! {"
22191 [EXCERPT]
22192 ˇ[FOLDED]
22193 [EXCERPT]
22194 a1
22195 b1
22196 [EXCERPT]
22197 [FOLDED]
22198 [EXCERPT]
22199 [FOLDED]
22200 "
22201 });
22202 }
22203}
22204
22205#[gpui::test]
22206async fn test_edit_prediction_text(cx: &mut TestAppContext) {
22207 init_test(cx, |_| {});
22208
22209 // Simple insertion
22210 assert_highlighted_edits(
22211 "Hello, world!",
22212 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
22213 true,
22214 cx,
22215 |highlighted_edits, cx| {
22216 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
22217 assert_eq!(highlighted_edits.highlights.len(), 1);
22218 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
22219 assert_eq!(
22220 highlighted_edits.highlights[0].1.background_color,
22221 Some(cx.theme().status().created_background)
22222 );
22223 },
22224 )
22225 .await;
22226
22227 // Replacement
22228 assert_highlighted_edits(
22229 "This is a test.",
22230 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
22231 false,
22232 cx,
22233 |highlighted_edits, cx| {
22234 assert_eq!(highlighted_edits.text, "That is a test.");
22235 assert_eq!(highlighted_edits.highlights.len(), 1);
22236 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
22237 assert_eq!(
22238 highlighted_edits.highlights[0].1.background_color,
22239 Some(cx.theme().status().created_background)
22240 );
22241 },
22242 )
22243 .await;
22244
22245 // Multiple edits
22246 assert_highlighted_edits(
22247 "Hello, world!",
22248 vec![
22249 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
22250 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
22251 ],
22252 false,
22253 cx,
22254 |highlighted_edits, cx| {
22255 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
22256 assert_eq!(highlighted_edits.highlights.len(), 2);
22257 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
22258 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
22259 assert_eq!(
22260 highlighted_edits.highlights[0].1.background_color,
22261 Some(cx.theme().status().created_background)
22262 );
22263 assert_eq!(
22264 highlighted_edits.highlights[1].1.background_color,
22265 Some(cx.theme().status().created_background)
22266 );
22267 },
22268 )
22269 .await;
22270
22271 // Multiple lines with edits
22272 assert_highlighted_edits(
22273 "First line\nSecond line\nThird line\nFourth line",
22274 vec![
22275 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
22276 (
22277 Point::new(2, 0)..Point::new(2, 10),
22278 "New third line".to_string(),
22279 ),
22280 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
22281 ],
22282 false,
22283 cx,
22284 |highlighted_edits, cx| {
22285 assert_eq!(
22286 highlighted_edits.text,
22287 "Second modified\nNew third line\nFourth updated line"
22288 );
22289 assert_eq!(highlighted_edits.highlights.len(), 3);
22290 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
22291 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
22292 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
22293 for highlight in &highlighted_edits.highlights {
22294 assert_eq!(
22295 highlight.1.background_color,
22296 Some(cx.theme().status().created_background)
22297 );
22298 }
22299 },
22300 )
22301 .await;
22302}
22303
22304#[gpui::test]
22305async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
22306 init_test(cx, |_| {});
22307
22308 // Deletion
22309 assert_highlighted_edits(
22310 "Hello, world!",
22311 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
22312 true,
22313 cx,
22314 |highlighted_edits, cx| {
22315 assert_eq!(highlighted_edits.text, "Hello, world!");
22316 assert_eq!(highlighted_edits.highlights.len(), 1);
22317 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
22318 assert_eq!(
22319 highlighted_edits.highlights[0].1.background_color,
22320 Some(cx.theme().status().deleted_background)
22321 );
22322 },
22323 )
22324 .await;
22325
22326 // Insertion
22327 assert_highlighted_edits(
22328 "Hello, world!",
22329 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
22330 true,
22331 cx,
22332 |highlighted_edits, cx| {
22333 assert_eq!(highlighted_edits.highlights.len(), 1);
22334 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
22335 assert_eq!(
22336 highlighted_edits.highlights[0].1.background_color,
22337 Some(cx.theme().status().created_background)
22338 );
22339 },
22340 )
22341 .await;
22342}
22343
22344async fn assert_highlighted_edits(
22345 text: &str,
22346 edits: Vec<(Range<Point>, String)>,
22347 include_deletions: bool,
22348 cx: &mut TestAppContext,
22349 assertion_fn: impl Fn(HighlightedText, &App),
22350) {
22351 let window = cx.add_window(|window, cx| {
22352 let buffer = MultiBuffer::build_simple(text, cx);
22353 Editor::new(EditorMode::full(), buffer, None, window, cx)
22354 });
22355 let cx = &mut VisualTestContext::from_window(*window, cx);
22356
22357 let (buffer, snapshot) = window
22358 .update(cx, |editor, _window, cx| {
22359 (
22360 editor.buffer().clone(),
22361 editor.buffer().read(cx).snapshot(cx),
22362 )
22363 })
22364 .unwrap();
22365
22366 let edits = edits
22367 .into_iter()
22368 .map(|(range, edit)| {
22369 (
22370 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
22371 edit,
22372 )
22373 })
22374 .collect::<Vec<_>>();
22375
22376 let text_anchor_edits = edits
22377 .clone()
22378 .into_iter()
22379 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
22380 .collect::<Vec<_>>();
22381
22382 let edit_preview = window
22383 .update(cx, |_, _window, cx| {
22384 buffer
22385 .read(cx)
22386 .as_singleton()
22387 .unwrap()
22388 .read(cx)
22389 .preview_edits(text_anchor_edits.into(), cx)
22390 })
22391 .unwrap()
22392 .await;
22393
22394 cx.update(|_window, cx| {
22395 let highlighted_edits = edit_prediction_edit_text(
22396 snapshot.as_singleton().unwrap().2,
22397 &edits,
22398 &edit_preview,
22399 include_deletions,
22400 cx,
22401 );
22402 assertion_fn(highlighted_edits, cx)
22403 });
22404}
22405
22406#[track_caller]
22407fn assert_breakpoint(
22408 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
22409 path: &Arc<Path>,
22410 expected: Vec<(u32, Breakpoint)>,
22411) {
22412 if expected.is_empty() {
22413 assert!(!breakpoints.contains_key(path), "{}", path.display());
22414 } else {
22415 let mut breakpoint = breakpoints
22416 .get(path)
22417 .unwrap()
22418 .iter()
22419 .map(|breakpoint| {
22420 (
22421 breakpoint.row,
22422 Breakpoint {
22423 message: breakpoint.message.clone(),
22424 state: breakpoint.state,
22425 condition: breakpoint.condition.clone(),
22426 hit_condition: breakpoint.hit_condition.clone(),
22427 },
22428 )
22429 })
22430 .collect::<Vec<_>>();
22431
22432 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
22433
22434 assert_eq!(expected, breakpoint);
22435 }
22436}
22437
22438fn add_log_breakpoint_at_cursor(
22439 editor: &mut Editor,
22440 log_message: &str,
22441 window: &mut Window,
22442 cx: &mut Context<Editor>,
22443) {
22444 let (anchor, bp) = editor
22445 .breakpoints_at_cursors(window, cx)
22446 .first()
22447 .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
22448 .unwrap_or_else(|| {
22449 let cursor_position: Point = editor.selections.newest(cx).head();
22450
22451 let breakpoint_position = editor
22452 .snapshot(window, cx)
22453 .display_snapshot
22454 .buffer_snapshot
22455 .anchor_before(Point::new(cursor_position.row, 0));
22456
22457 (breakpoint_position, Breakpoint::new_log(log_message))
22458 });
22459
22460 editor.edit_breakpoint_at_anchor(
22461 anchor,
22462 bp,
22463 BreakpointEditAction::EditLogMessage(log_message.into()),
22464 cx,
22465 );
22466}
22467
22468#[gpui::test]
22469async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
22470 init_test(cx, |_| {});
22471
22472 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22473 let fs = FakeFs::new(cx.executor());
22474 fs.insert_tree(
22475 path!("/a"),
22476 json!({
22477 "main.rs": sample_text,
22478 }),
22479 )
22480 .await;
22481 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22482 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22483 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22484
22485 let fs = FakeFs::new(cx.executor());
22486 fs.insert_tree(
22487 path!("/a"),
22488 json!({
22489 "main.rs": sample_text,
22490 }),
22491 )
22492 .await;
22493 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22494 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22495 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22496 let worktree_id = workspace
22497 .update(cx, |workspace, _window, cx| {
22498 workspace.project().update(cx, |project, cx| {
22499 project.worktrees(cx).next().unwrap().read(cx).id()
22500 })
22501 })
22502 .unwrap();
22503
22504 let buffer = project
22505 .update(cx, |project, cx| {
22506 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22507 })
22508 .await
22509 .unwrap();
22510
22511 let (editor, cx) = cx.add_window_view(|window, cx| {
22512 Editor::new(
22513 EditorMode::full(),
22514 MultiBuffer::build_from_buffer(buffer, cx),
22515 Some(project.clone()),
22516 window,
22517 cx,
22518 )
22519 });
22520
22521 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22522 let abs_path = project.read_with(cx, |project, cx| {
22523 project
22524 .absolute_path(&project_path, cx)
22525 .map(Arc::from)
22526 .unwrap()
22527 });
22528
22529 // assert we can add breakpoint on the first line
22530 editor.update_in(cx, |editor, window, cx| {
22531 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22532 editor.move_to_end(&MoveToEnd, window, cx);
22533 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22534 });
22535
22536 let breakpoints = editor.update(cx, |editor, cx| {
22537 editor
22538 .breakpoint_store()
22539 .as_ref()
22540 .unwrap()
22541 .read(cx)
22542 .all_source_breakpoints(cx)
22543 });
22544
22545 assert_eq!(1, breakpoints.len());
22546 assert_breakpoint(
22547 &breakpoints,
22548 &abs_path,
22549 vec![
22550 (0, Breakpoint::new_standard()),
22551 (3, Breakpoint::new_standard()),
22552 ],
22553 );
22554
22555 editor.update_in(cx, |editor, window, cx| {
22556 editor.move_to_beginning(&MoveToBeginning, window, cx);
22557 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22558 });
22559
22560 let breakpoints = editor.update(cx, |editor, cx| {
22561 editor
22562 .breakpoint_store()
22563 .as_ref()
22564 .unwrap()
22565 .read(cx)
22566 .all_source_breakpoints(cx)
22567 });
22568
22569 assert_eq!(1, breakpoints.len());
22570 assert_breakpoint(
22571 &breakpoints,
22572 &abs_path,
22573 vec![(3, Breakpoint::new_standard())],
22574 );
22575
22576 editor.update_in(cx, |editor, window, cx| {
22577 editor.move_to_end(&MoveToEnd, window, cx);
22578 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22579 });
22580
22581 let breakpoints = editor.update(cx, |editor, cx| {
22582 editor
22583 .breakpoint_store()
22584 .as_ref()
22585 .unwrap()
22586 .read(cx)
22587 .all_source_breakpoints(cx)
22588 });
22589
22590 assert_eq!(0, breakpoints.len());
22591 assert_breakpoint(&breakpoints, &abs_path, vec![]);
22592}
22593
22594#[gpui::test]
22595async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
22596 init_test(cx, |_| {});
22597
22598 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22599
22600 let fs = FakeFs::new(cx.executor());
22601 fs.insert_tree(
22602 path!("/a"),
22603 json!({
22604 "main.rs": sample_text,
22605 }),
22606 )
22607 .await;
22608 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22609 let (workspace, cx) =
22610 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
22611
22612 let worktree_id = workspace.update(cx, |workspace, cx| {
22613 workspace.project().update(cx, |project, cx| {
22614 project.worktrees(cx).next().unwrap().read(cx).id()
22615 })
22616 });
22617
22618 let buffer = project
22619 .update(cx, |project, cx| {
22620 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22621 })
22622 .await
22623 .unwrap();
22624
22625 let (editor, cx) = cx.add_window_view(|window, cx| {
22626 Editor::new(
22627 EditorMode::full(),
22628 MultiBuffer::build_from_buffer(buffer, cx),
22629 Some(project.clone()),
22630 window,
22631 cx,
22632 )
22633 });
22634
22635 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22636 let abs_path = project.read_with(cx, |project, cx| {
22637 project
22638 .absolute_path(&project_path, cx)
22639 .map(Arc::from)
22640 .unwrap()
22641 });
22642
22643 editor.update_in(cx, |editor, window, cx| {
22644 add_log_breakpoint_at_cursor(editor, "hello world", 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_breakpoint(
22657 &breakpoints,
22658 &abs_path,
22659 vec![(0, Breakpoint::new_log("hello world"))],
22660 );
22661
22662 // Removing a log message from a log breakpoint should remove it
22663 editor.update_in(cx, |editor, window, cx| {
22664 add_log_breakpoint_at_cursor(editor, "", window, cx);
22665 });
22666
22667 let breakpoints = editor.update(cx, |editor, cx| {
22668 editor
22669 .breakpoint_store()
22670 .as_ref()
22671 .unwrap()
22672 .read(cx)
22673 .all_source_breakpoints(cx)
22674 });
22675
22676 assert_breakpoint(&breakpoints, &abs_path, vec![]);
22677
22678 editor.update_in(cx, |editor, window, cx| {
22679 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22680 editor.move_to_end(&MoveToEnd, window, cx);
22681 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22682 // Not adding a log message to a standard breakpoint shouldn't remove it
22683 add_log_breakpoint_at_cursor(editor, "", window, cx);
22684 });
22685
22686 let breakpoints = editor.update(cx, |editor, cx| {
22687 editor
22688 .breakpoint_store()
22689 .as_ref()
22690 .unwrap()
22691 .read(cx)
22692 .all_source_breakpoints(cx)
22693 });
22694
22695 assert_breakpoint(
22696 &breakpoints,
22697 &abs_path,
22698 vec![
22699 (0, Breakpoint::new_standard()),
22700 (3, Breakpoint::new_standard()),
22701 ],
22702 );
22703
22704 editor.update_in(cx, |editor, window, cx| {
22705 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
22706 });
22707
22708 let breakpoints = editor.update(cx, |editor, cx| {
22709 editor
22710 .breakpoint_store()
22711 .as_ref()
22712 .unwrap()
22713 .read(cx)
22714 .all_source_breakpoints(cx)
22715 });
22716
22717 assert_breakpoint(
22718 &breakpoints,
22719 &abs_path,
22720 vec![
22721 (0, Breakpoint::new_standard()),
22722 (3, Breakpoint::new_log("hello world")),
22723 ],
22724 );
22725
22726 editor.update_in(cx, |editor, window, cx| {
22727 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
22728 });
22729
22730 let breakpoints = editor.update(cx, |editor, cx| {
22731 editor
22732 .breakpoint_store()
22733 .as_ref()
22734 .unwrap()
22735 .read(cx)
22736 .all_source_breakpoints(cx)
22737 });
22738
22739 assert_breakpoint(
22740 &breakpoints,
22741 &abs_path,
22742 vec![
22743 (0, Breakpoint::new_standard()),
22744 (3, Breakpoint::new_log("hello Earth!!")),
22745 ],
22746 );
22747}
22748
22749/// This also tests that Editor::breakpoint_at_cursor_head is working properly
22750/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
22751/// or when breakpoints were placed out of order. This tests for a regression too
22752#[gpui::test]
22753async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
22754 init_test(cx, |_| {});
22755
22756 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22757 let fs = FakeFs::new(cx.executor());
22758 fs.insert_tree(
22759 path!("/a"),
22760 json!({
22761 "main.rs": sample_text,
22762 }),
22763 )
22764 .await;
22765 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22766 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22767 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22768
22769 let fs = FakeFs::new(cx.executor());
22770 fs.insert_tree(
22771 path!("/a"),
22772 json!({
22773 "main.rs": sample_text,
22774 }),
22775 )
22776 .await;
22777 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22778 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22779 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22780 let worktree_id = workspace
22781 .update(cx, |workspace, _window, cx| {
22782 workspace.project().update(cx, |project, cx| {
22783 project.worktrees(cx).next().unwrap().read(cx).id()
22784 })
22785 })
22786 .unwrap();
22787
22788 let buffer = project
22789 .update(cx, |project, cx| {
22790 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22791 })
22792 .await
22793 .unwrap();
22794
22795 let (editor, cx) = cx.add_window_view(|window, cx| {
22796 Editor::new(
22797 EditorMode::full(),
22798 MultiBuffer::build_from_buffer(buffer, cx),
22799 Some(project.clone()),
22800 window,
22801 cx,
22802 )
22803 });
22804
22805 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22806 let abs_path = project.read_with(cx, |project, cx| {
22807 project
22808 .absolute_path(&project_path, cx)
22809 .map(Arc::from)
22810 .unwrap()
22811 });
22812
22813 // assert we can add breakpoint on the first line
22814 editor.update_in(cx, |editor, window, cx| {
22815 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22816 editor.move_to_end(&MoveToEnd, window, cx);
22817 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22818 editor.move_up(&MoveUp, window, cx);
22819 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22820 });
22821
22822 let breakpoints = editor.update(cx, |editor, cx| {
22823 editor
22824 .breakpoint_store()
22825 .as_ref()
22826 .unwrap()
22827 .read(cx)
22828 .all_source_breakpoints(cx)
22829 });
22830
22831 assert_eq!(1, breakpoints.len());
22832 assert_breakpoint(
22833 &breakpoints,
22834 &abs_path,
22835 vec![
22836 (0, Breakpoint::new_standard()),
22837 (2, Breakpoint::new_standard()),
22838 (3, Breakpoint::new_standard()),
22839 ],
22840 );
22841
22842 editor.update_in(cx, |editor, window, cx| {
22843 editor.move_to_beginning(&MoveToBeginning, window, cx);
22844 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22845 editor.move_to_end(&MoveToEnd, window, cx);
22846 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22847 // Disabling a breakpoint that doesn't exist should do nothing
22848 editor.move_up(&MoveUp, window, cx);
22849 editor.move_up(&MoveUp, window, cx);
22850 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22851 });
22852
22853 let breakpoints = editor.update(cx, |editor, cx| {
22854 editor
22855 .breakpoint_store()
22856 .as_ref()
22857 .unwrap()
22858 .read(cx)
22859 .all_source_breakpoints(cx)
22860 });
22861
22862 let disable_breakpoint = {
22863 let mut bp = Breakpoint::new_standard();
22864 bp.state = BreakpointState::Disabled;
22865 bp
22866 };
22867
22868 assert_eq!(1, breakpoints.len());
22869 assert_breakpoint(
22870 &breakpoints,
22871 &abs_path,
22872 vec![
22873 (0, disable_breakpoint.clone()),
22874 (2, Breakpoint::new_standard()),
22875 (3, disable_breakpoint.clone()),
22876 ],
22877 );
22878
22879 editor.update_in(cx, |editor, window, cx| {
22880 editor.move_to_beginning(&MoveToBeginning, window, cx);
22881 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
22882 editor.move_to_end(&MoveToEnd, window, cx);
22883 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
22884 editor.move_up(&MoveUp, window, cx);
22885 editor.disable_breakpoint(&actions::DisableBreakpoint, 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, disable_breakpoint),
22904 (3, Breakpoint::new_standard()),
22905 ],
22906 );
22907}
22908
22909#[gpui::test]
22910async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
22911 init_test(cx, |_| {});
22912 let capabilities = lsp::ServerCapabilities {
22913 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
22914 prepare_provider: Some(true),
22915 work_done_progress_options: Default::default(),
22916 })),
22917 ..Default::default()
22918 };
22919 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
22920
22921 cx.set_state(indoc! {"
22922 struct Fˇoo {}
22923 "});
22924
22925 cx.update_editor(|editor, _, cx| {
22926 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
22927 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
22928 editor.highlight_background::<DocumentHighlightRead>(
22929 &[highlight_range],
22930 |theme| theme.colors().editor_document_highlight_read_background,
22931 cx,
22932 );
22933 });
22934
22935 let mut prepare_rename_handler = cx
22936 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
22937 move |_, _, _| async move {
22938 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
22939 start: lsp::Position {
22940 line: 0,
22941 character: 7,
22942 },
22943 end: lsp::Position {
22944 line: 0,
22945 character: 10,
22946 },
22947 })))
22948 },
22949 );
22950 let prepare_rename_task = cx
22951 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
22952 .expect("Prepare rename was not started");
22953 prepare_rename_handler.next().await.unwrap();
22954 prepare_rename_task.await.expect("Prepare rename failed");
22955
22956 let mut rename_handler =
22957 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
22958 let edit = lsp::TextEdit {
22959 range: lsp::Range {
22960 start: lsp::Position {
22961 line: 0,
22962 character: 7,
22963 },
22964 end: lsp::Position {
22965 line: 0,
22966 character: 10,
22967 },
22968 },
22969 new_text: "FooRenamed".to_string(),
22970 };
22971 Ok(Some(lsp::WorkspaceEdit::new(
22972 // Specify the same edit twice
22973 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
22974 )))
22975 });
22976 let rename_task = cx
22977 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
22978 .expect("Confirm rename was not started");
22979 rename_handler.next().await.unwrap();
22980 rename_task.await.expect("Confirm rename failed");
22981 cx.run_until_parked();
22982
22983 // Despite two edits, only one is actually applied as those are identical
22984 cx.assert_editor_state(indoc! {"
22985 struct FooRenamedˇ {}
22986 "});
22987}
22988
22989#[gpui::test]
22990async fn test_rename_without_prepare(cx: &mut TestAppContext) {
22991 init_test(cx, |_| {});
22992 // These capabilities indicate that the server does not support prepare rename.
22993 let capabilities = lsp::ServerCapabilities {
22994 rename_provider: Some(lsp::OneOf::Left(true)),
22995 ..Default::default()
22996 };
22997 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
22998
22999 cx.set_state(indoc! {"
23000 struct Fˇoo {}
23001 "});
23002
23003 cx.update_editor(|editor, _window, cx| {
23004 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
23005 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
23006 editor.highlight_background::<DocumentHighlightRead>(
23007 &[highlight_range],
23008 |theme| theme.colors().editor_document_highlight_read_background,
23009 cx,
23010 );
23011 });
23012
23013 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23014 .expect("Prepare rename was not started")
23015 .await
23016 .expect("Prepare rename failed");
23017
23018 let mut rename_handler =
23019 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23020 let edit = lsp::TextEdit {
23021 range: lsp::Range {
23022 start: lsp::Position {
23023 line: 0,
23024 character: 7,
23025 },
23026 end: lsp::Position {
23027 line: 0,
23028 character: 10,
23029 },
23030 },
23031 new_text: "FooRenamed".to_string(),
23032 };
23033 Ok(Some(lsp::WorkspaceEdit::new(
23034 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
23035 )))
23036 });
23037 let rename_task = cx
23038 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23039 .expect("Confirm rename was not started");
23040 rename_handler.next().await.unwrap();
23041 rename_task.await.expect("Confirm rename failed");
23042 cx.run_until_parked();
23043
23044 // Correct range is renamed, as `surrounding_word` is used to find it.
23045 cx.assert_editor_state(indoc! {"
23046 struct FooRenamedˇ {}
23047 "});
23048}
23049
23050#[gpui::test]
23051async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
23052 init_test(cx, |_| {});
23053 let mut cx = EditorTestContext::new(cx).await;
23054
23055 let language = Arc::new(
23056 Language::new(
23057 LanguageConfig::default(),
23058 Some(tree_sitter_html::LANGUAGE.into()),
23059 )
23060 .with_brackets_query(
23061 r#"
23062 ("<" @open "/>" @close)
23063 ("</" @open ">" @close)
23064 ("<" @open ">" @close)
23065 ("\"" @open "\"" @close)
23066 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
23067 "#,
23068 )
23069 .unwrap(),
23070 );
23071 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23072
23073 cx.set_state(indoc! {"
23074 <span>ˇ</span>
23075 "});
23076 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23077 cx.assert_editor_state(indoc! {"
23078 <span>
23079 ˇ
23080 </span>
23081 "});
23082
23083 cx.set_state(indoc! {"
23084 <span><span></span>ˇ</span>
23085 "});
23086 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23087 cx.assert_editor_state(indoc! {"
23088 <span><span></span>
23089 ˇ</span>
23090 "});
23091
23092 cx.set_state(indoc! {"
23093 <span>ˇ
23094 </span>
23095 "});
23096 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23097 cx.assert_editor_state(indoc! {"
23098 <span>
23099 ˇ
23100 </span>
23101 "});
23102}
23103
23104#[gpui::test(iterations = 10)]
23105async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
23106 init_test(cx, |_| {});
23107
23108 let fs = FakeFs::new(cx.executor());
23109 fs.insert_tree(
23110 path!("/dir"),
23111 json!({
23112 "a.ts": "a",
23113 }),
23114 )
23115 .await;
23116
23117 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
23118 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23119 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23120
23121 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23122 language_registry.add(Arc::new(Language::new(
23123 LanguageConfig {
23124 name: "TypeScript".into(),
23125 matcher: LanguageMatcher {
23126 path_suffixes: vec!["ts".to_string()],
23127 ..Default::default()
23128 },
23129 ..Default::default()
23130 },
23131 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
23132 )));
23133 let mut fake_language_servers = language_registry.register_fake_lsp(
23134 "TypeScript",
23135 FakeLspAdapter {
23136 capabilities: lsp::ServerCapabilities {
23137 code_lens_provider: Some(lsp::CodeLensOptions {
23138 resolve_provider: Some(true),
23139 }),
23140 execute_command_provider: Some(lsp::ExecuteCommandOptions {
23141 commands: vec!["_the/command".to_string()],
23142 ..lsp::ExecuteCommandOptions::default()
23143 }),
23144 ..lsp::ServerCapabilities::default()
23145 },
23146 ..FakeLspAdapter::default()
23147 },
23148 );
23149
23150 let editor = workspace
23151 .update(cx, |workspace, window, cx| {
23152 workspace.open_abs_path(
23153 PathBuf::from(path!("/dir/a.ts")),
23154 OpenOptions::default(),
23155 window,
23156 cx,
23157 )
23158 })
23159 .unwrap()
23160 .await
23161 .unwrap()
23162 .downcast::<Editor>()
23163 .unwrap();
23164 cx.executor().run_until_parked();
23165
23166 let fake_server = fake_language_servers.next().await.unwrap();
23167
23168 let buffer = editor.update(cx, |editor, cx| {
23169 editor
23170 .buffer()
23171 .read(cx)
23172 .as_singleton()
23173 .expect("have opened a single file by path")
23174 });
23175
23176 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
23177 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
23178 drop(buffer_snapshot);
23179 let actions = cx
23180 .update_window(*workspace, |_, window, cx| {
23181 project.code_actions(&buffer, anchor..anchor, window, cx)
23182 })
23183 .unwrap();
23184
23185 fake_server
23186 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23187 Ok(Some(vec![
23188 lsp::CodeLens {
23189 range: lsp::Range::default(),
23190 command: Some(lsp::Command {
23191 title: "Code lens command".to_owned(),
23192 command: "_the/command".to_owned(),
23193 arguments: None,
23194 }),
23195 data: None,
23196 },
23197 lsp::CodeLens {
23198 range: lsp::Range::default(),
23199 command: Some(lsp::Command {
23200 title: "Command not in capabilities".to_owned(),
23201 command: "not in capabilities".to_owned(),
23202 arguments: None,
23203 }),
23204 data: None,
23205 },
23206 lsp::CodeLens {
23207 range: lsp::Range {
23208 start: lsp::Position {
23209 line: 1,
23210 character: 1,
23211 },
23212 end: lsp::Position {
23213 line: 1,
23214 character: 1,
23215 },
23216 },
23217 command: Some(lsp::Command {
23218 title: "Command not in range".to_owned(),
23219 command: "_the/command".to_owned(),
23220 arguments: None,
23221 }),
23222 data: None,
23223 },
23224 ]))
23225 })
23226 .next()
23227 .await;
23228
23229 let actions = actions.await.unwrap();
23230 assert_eq!(
23231 actions.len(),
23232 1,
23233 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
23234 );
23235 let action = actions[0].clone();
23236 let apply = project.update(cx, |project, cx| {
23237 project.apply_code_action(buffer.clone(), action, true, cx)
23238 });
23239
23240 // Resolving the code action does not populate its edits. In absence of
23241 // edits, we must execute the given command.
23242 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
23243 |mut lens, _| async move {
23244 let lens_command = lens.command.as_mut().expect("should have a command");
23245 assert_eq!(lens_command.title, "Code lens command");
23246 lens_command.arguments = Some(vec![json!("the-argument")]);
23247 Ok(lens)
23248 },
23249 );
23250
23251 // While executing the command, the language server sends the editor
23252 // a `workspaceEdit` request.
23253 fake_server
23254 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
23255 let fake = fake_server.clone();
23256 move |params, _| {
23257 assert_eq!(params.command, "_the/command");
23258 let fake = fake.clone();
23259 async move {
23260 fake.server
23261 .request::<lsp::request::ApplyWorkspaceEdit>(
23262 lsp::ApplyWorkspaceEditParams {
23263 label: None,
23264 edit: lsp::WorkspaceEdit {
23265 changes: Some(
23266 [(
23267 lsp::Uri::from_file_path(path!("/dir/a.ts")).unwrap(),
23268 vec![lsp::TextEdit {
23269 range: lsp::Range::new(
23270 lsp::Position::new(0, 0),
23271 lsp::Position::new(0, 0),
23272 ),
23273 new_text: "X".into(),
23274 }],
23275 )]
23276 .into_iter()
23277 .collect(),
23278 ),
23279 ..lsp::WorkspaceEdit::default()
23280 },
23281 },
23282 )
23283 .await
23284 .into_response()
23285 .unwrap();
23286 Ok(Some(json!(null)))
23287 }
23288 }
23289 })
23290 .next()
23291 .await;
23292
23293 // Applying the code lens command returns a project transaction containing the edits
23294 // sent by the language server in its `workspaceEdit` request.
23295 let transaction = apply.await.unwrap();
23296 assert!(transaction.0.contains_key(&buffer));
23297 buffer.update(cx, |buffer, cx| {
23298 assert_eq!(buffer.text(), "Xa");
23299 buffer.undo(cx);
23300 assert_eq!(buffer.text(), "a");
23301 });
23302
23303 let actions_after_edits = cx
23304 .update_window(*workspace, |_, window, cx| {
23305 project.code_actions(&buffer, anchor..anchor, window, cx)
23306 })
23307 .unwrap()
23308 .await
23309 .unwrap();
23310 assert_eq!(
23311 actions, actions_after_edits,
23312 "For the same selection, same code lens actions should be returned"
23313 );
23314
23315 let _responses =
23316 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23317 panic!("No more code lens requests are expected");
23318 });
23319 editor.update_in(cx, |editor, window, cx| {
23320 editor.select_all(&SelectAll, window, cx);
23321 });
23322 cx.executor().run_until_parked();
23323 let new_actions = cx
23324 .update_window(*workspace, |_, window, cx| {
23325 project.code_actions(&buffer, anchor..anchor, window, cx)
23326 })
23327 .unwrap()
23328 .await
23329 .unwrap();
23330 assert_eq!(
23331 actions, new_actions,
23332 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
23333 );
23334}
23335
23336#[gpui::test]
23337async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
23338 init_test(cx, |_| {});
23339
23340 let fs = FakeFs::new(cx.executor());
23341 let main_text = r#"fn main() {
23342println!("1");
23343println!("2");
23344println!("3");
23345println!("4");
23346println!("5");
23347}"#;
23348 let lib_text = "mod foo {}";
23349 fs.insert_tree(
23350 path!("/a"),
23351 json!({
23352 "lib.rs": lib_text,
23353 "main.rs": main_text,
23354 }),
23355 )
23356 .await;
23357
23358 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23359 let (workspace, cx) =
23360 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23361 let worktree_id = workspace.update(cx, |workspace, cx| {
23362 workspace.project().update(cx, |project, cx| {
23363 project.worktrees(cx).next().unwrap().read(cx).id()
23364 })
23365 });
23366
23367 let expected_ranges = vec![
23368 Point::new(0, 0)..Point::new(0, 0),
23369 Point::new(1, 0)..Point::new(1, 1),
23370 Point::new(2, 0)..Point::new(2, 2),
23371 Point::new(3, 0)..Point::new(3, 3),
23372 ];
23373
23374 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23375 let editor_1 = workspace
23376 .update_in(cx, |workspace, window, cx| {
23377 workspace.open_path(
23378 (worktree_id, rel_path("main.rs")),
23379 Some(pane_1.downgrade()),
23380 true,
23381 window,
23382 cx,
23383 )
23384 })
23385 .unwrap()
23386 .await
23387 .downcast::<Editor>()
23388 .unwrap();
23389 pane_1.update(cx, |pane, cx| {
23390 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23391 open_editor.update(cx, |editor, cx| {
23392 assert_eq!(
23393 editor.display_text(cx),
23394 main_text,
23395 "Original main.rs text on initial open",
23396 );
23397 assert_eq!(
23398 editor
23399 .selections
23400 .all::<Point>(cx)
23401 .into_iter()
23402 .map(|s| s.range())
23403 .collect::<Vec<_>>(),
23404 vec![Point::zero()..Point::zero()],
23405 "Default selections on initial open",
23406 );
23407 })
23408 });
23409 editor_1.update_in(cx, |editor, window, cx| {
23410 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23411 s.select_ranges(expected_ranges.clone());
23412 });
23413 });
23414
23415 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
23416 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
23417 });
23418 let editor_2 = workspace
23419 .update_in(cx, |workspace, window, cx| {
23420 workspace.open_path(
23421 (worktree_id, rel_path("main.rs")),
23422 Some(pane_2.downgrade()),
23423 true,
23424 window,
23425 cx,
23426 )
23427 })
23428 .unwrap()
23429 .await
23430 .downcast::<Editor>()
23431 .unwrap();
23432 pane_2.update(cx, |pane, cx| {
23433 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23434 open_editor.update(cx, |editor, cx| {
23435 assert_eq!(
23436 editor.display_text(cx),
23437 main_text,
23438 "Original main.rs text on initial open in another panel",
23439 );
23440 assert_eq!(
23441 editor
23442 .selections
23443 .all::<Point>(cx)
23444 .into_iter()
23445 .map(|s| s.range())
23446 .collect::<Vec<_>>(),
23447 vec![Point::zero()..Point::zero()],
23448 "Default selections on initial open in another panel",
23449 );
23450 })
23451 });
23452
23453 editor_2.update_in(cx, |editor, window, cx| {
23454 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
23455 });
23456
23457 let _other_editor_1 = workspace
23458 .update_in(cx, |workspace, window, cx| {
23459 workspace.open_path(
23460 (worktree_id, rel_path("lib.rs")),
23461 Some(pane_1.downgrade()),
23462 true,
23463 window,
23464 cx,
23465 )
23466 })
23467 .unwrap()
23468 .await
23469 .downcast::<Editor>()
23470 .unwrap();
23471 pane_1
23472 .update_in(cx, |pane, window, cx| {
23473 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23474 })
23475 .await
23476 .unwrap();
23477 drop(editor_1);
23478 pane_1.update(cx, |pane, cx| {
23479 pane.active_item()
23480 .unwrap()
23481 .downcast::<Editor>()
23482 .unwrap()
23483 .update(cx, |editor, cx| {
23484 assert_eq!(
23485 editor.display_text(cx),
23486 lib_text,
23487 "Other file should be open and active",
23488 );
23489 });
23490 assert_eq!(pane.items().count(), 1, "No other editors should be open");
23491 });
23492
23493 let _other_editor_2 = workspace
23494 .update_in(cx, |workspace, window, cx| {
23495 workspace.open_path(
23496 (worktree_id, rel_path("lib.rs")),
23497 Some(pane_2.downgrade()),
23498 true,
23499 window,
23500 cx,
23501 )
23502 })
23503 .unwrap()
23504 .await
23505 .downcast::<Editor>()
23506 .unwrap();
23507 pane_2
23508 .update_in(cx, |pane, window, cx| {
23509 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23510 })
23511 .await
23512 .unwrap();
23513 drop(editor_2);
23514 pane_2.update(cx, |pane, cx| {
23515 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23516 open_editor.update(cx, |editor, cx| {
23517 assert_eq!(
23518 editor.display_text(cx),
23519 lib_text,
23520 "Other file should be open and active in another panel too",
23521 );
23522 });
23523 assert_eq!(
23524 pane.items().count(),
23525 1,
23526 "No other editors should be open in another pane",
23527 );
23528 });
23529
23530 let _editor_1_reopened = workspace
23531 .update_in(cx, |workspace, window, cx| {
23532 workspace.open_path(
23533 (worktree_id, rel_path("main.rs")),
23534 Some(pane_1.downgrade()),
23535 true,
23536 window,
23537 cx,
23538 )
23539 })
23540 .unwrap()
23541 .await
23542 .downcast::<Editor>()
23543 .unwrap();
23544 let _editor_2_reopened = workspace
23545 .update_in(cx, |workspace, window, cx| {
23546 workspace.open_path(
23547 (worktree_id, rel_path("main.rs")),
23548 Some(pane_2.downgrade()),
23549 true,
23550 window,
23551 cx,
23552 )
23553 })
23554 .unwrap()
23555 .await
23556 .downcast::<Editor>()
23557 .unwrap();
23558 pane_1.update(cx, |pane, cx| {
23559 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23560 open_editor.update(cx, |editor, cx| {
23561 assert_eq!(
23562 editor.display_text(cx),
23563 main_text,
23564 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
23565 );
23566 assert_eq!(
23567 editor
23568 .selections
23569 .all::<Point>(cx)
23570 .into_iter()
23571 .map(|s| s.range())
23572 .collect::<Vec<_>>(),
23573 expected_ranges,
23574 "Previous editor in the 1st panel had selections and should get them restored on reopen",
23575 );
23576 })
23577 });
23578 pane_2.update(cx, |pane, cx| {
23579 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23580 open_editor.update(cx, |editor, cx| {
23581 assert_eq!(
23582 editor.display_text(cx),
23583 r#"fn main() {
23584⋯rintln!("1");
23585⋯intln!("2");
23586⋯ntln!("3");
23587println!("4");
23588println!("5");
23589}"#,
23590 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
23591 );
23592 assert_eq!(
23593 editor
23594 .selections
23595 .all::<Point>(cx)
23596 .into_iter()
23597 .map(|s| s.range())
23598 .collect::<Vec<_>>(),
23599 vec![Point::zero()..Point::zero()],
23600 "Previous editor in the 2nd pane had no selections changed hence should restore none",
23601 );
23602 })
23603 });
23604}
23605
23606#[gpui::test]
23607async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
23608 init_test(cx, |_| {});
23609
23610 let fs = FakeFs::new(cx.executor());
23611 let main_text = r#"fn main() {
23612println!("1");
23613println!("2");
23614println!("3");
23615println!("4");
23616println!("5");
23617}"#;
23618 let lib_text = "mod foo {}";
23619 fs.insert_tree(
23620 path!("/a"),
23621 json!({
23622 "lib.rs": lib_text,
23623 "main.rs": main_text,
23624 }),
23625 )
23626 .await;
23627
23628 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23629 let (workspace, cx) =
23630 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23631 let worktree_id = workspace.update(cx, |workspace, cx| {
23632 workspace.project().update(cx, |project, cx| {
23633 project.worktrees(cx).next().unwrap().read(cx).id()
23634 })
23635 });
23636
23637 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23638 let editor = workspace
23639 .update_in(cx, |workspace, window, cx| {
23640 workspace.open_path(
23641 (worktree_id, rel_path("main.rs")),
23642 Some(pane.downgrade()),
23643 true,
23644 window,
23645 cx,
23646 )
23647 })
23648 .unwrap()
23649 .await
23650 .downcast::<Editor>()
23651 .unwrap();
23652 pane.update(cx, |pane, cx| {
23653 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23654 open_editor.update(cx, |editor, cx| {
23655 assert_eq!(
23656 editor.display_text(cx),
23657 main_text,
23658 "Original main.rs text on initial open",
23659 );
23660 })
23661 });
23662 editor.update_in(cx, |editor, window, cx| {
23663 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
23664 });
23665
23666 cx.update_global(|store: &mut SettingsStore, cx| {
23667 store.update_user_settings(cx, |s| {
23668 s.workspace.restore_on_file_reopen = Some(false);
23669 });
23670 });
23671 editor.update_in(cx, |editor, window, cx| {
23672 editor.fold_ranges(
23673 vec![
23674 Point::new(1, 0)..Point::new(1, 1),
23675 Point::new(2, 0)..Point::new(2, 2),
23676 Point::new(3, 0)..Point::new(3, 3),
23677 ],
23678 false,
23679 window,
23680 cx,
23681 );
23682 });
23683 pane.update_in(cx, |pane, window, cx| {
23684 pane.close_all_items(&CloseAllItems::default(), window, cx)
23685 })
23686 .await
23687 .unwrap();
23688 pane.update(cx, |pane, _| {
23689 assert!(pane.active_item().is_none());
23690 });
23691 cx.update_global(|store: &mut SettingsStore, cx| {
23692 store.update_user_settings(cx, |s| {
23693 s.workspace.restore_on_file_reopen = Some(true);
23694 });
23695 });
23696
23697 let _editor_reopened = workspace
23698 .update_in(cx, |workspace, window, cx| {
23699 workspace.open_path(
23700 (worktree_id, rel_path("main.rs")),
23701 Some(pane.downgrade()),
23702 true,
23703 window,
23704 cx,
23705 )
23706 })
23707 .unwrap()
23708 .await
23709 .downcast::<Editor>()
23710 .unwrap();
23711 pane.update(cx, |pane, cx| {
23712 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23713 open_editor.update(cx, |editor, cx| {
23714 assert_eq!(
23715 editor.display_text(cx),
23716 main_text,
23717 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
23718 );
23719 })
23720 });
23721}
23722
23723#[gpui::test]
23724async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
23725 struct EmptyModalView {
23726 focus_handle: gpui::FocusHandle,
23727 }
23728 impl EventEmitter<DismissEvent> for EmptyModalView {}
23729 impl Render for EmptyModalView {
23730 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
23731 div()
23732 }
23733 }
23734 impl Focusable for EmptyModalView {
23735 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
23736 self.focus_handle.clone()
23737 }
23738 }
23739 impl workspace::ModalView for EmptyModalView {}
23740 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
23741 EmptyModalView {
23742 focus_handle: cx.focus_handle(),
23743 }
23744 }
23745
23746 init_test(cx, |_| {});
23747
23748 let fs = FakeFs::new(cx.executor());
23749 let project = Project::test(fs, [], cx).await;
23750 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23751 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
23752 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23753 let editor = cx.new_window_entity(|window, cx| {
23754 Editor::new(
23755 EditorMode::full(),
23756 buffer,
23757 Some(project.clone()),
23758 window,
23759 cx,
23760 )
23761 });
23762 workspace
23763 .update(cx, |workspace, window, cx| {
23764 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
23765 })
23766 .unwrap();
23767 editor.update_in(cx, |editor, window, cx| {
23768 editor.open_context_menu(&OpenContextMenu, window, cx);
23769 assert!(editor.mouse_context_menu.is_some());
23770 });
23771 workspace
23772 .update(cx, |workspace, window, cx| {
23773 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
23774 })
23775 .unwrap();
23776 cx.read(|cx| {
23777 assert!(editor.read(cx).mouse_context_menu.is_none());
23778 });
23779}
23780
23781fn set_linked_edit_ranges(
23782 opening: (Point, Point),
23783 closing: (Point, Point),
23784 editor: &mut Editor,
23785 cx: &mut Context<Editor>,
23786) {
23787 let Some((buffer, _)) = editor
23788 .buffer
23789 .read(cx)
23790 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
23791 else {
23792 panic!("Failed to get buffer for selection position");
23793 };
23794 let buffer = buffer.read(cx);
23795 let buffer_id = buffer.remote_id();
23796 let opening_range = buffer.anchor_before(opening.0)..buffer.anchor_after(opening.1);
23797 let closing_range = buffer.anchor_before(closing.0)..buffer.anchor_after(closing.1);
23798 let mut linked_ranges = HashMap::default();
23799 linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
23800 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
23801}
23802
23803#[gpui::test]
23804async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
23805 init_test(cx, |_| {});
23806
23807 let fs = FakeFs::new(cx.executor());
23808 fs.insert_file(path!("/file.html"), Default::default())
23809 .await;
23810
23811 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
23812
23813 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23814 let html_language = Arc::new(Language::new(
23815 LanguageConfig {
23816 name: "HTML".into(),
23817 matcher: LanguageMatcher {
23818 path_suffixes: vec!["html".to_string()],
23819 ..LanguageMatcher::default()
23820 },
23821 brackets: BracketPairConfig {
23822 pairs: vec![BracketPair {
23823 start: "<".into(),
23824 end: ">".into(),
23825 close: true,
23826 ..Default::default()
23827 }],
23828 ..Default::default()
23829 },
23830 ..Default::default()
23831 },
23832 Some(tree_sitter_html::LANGUAGE.into()),
23833 ));
23834 language_registry.add(html_language);
23835 let mut fake_servers = language_registry.register_fake_lsp(
23836 "HTML",
23837 FakeLspAdapter {
23838 capabilities: lsp::ServerCapabilities {
23839 completion_provider: Some(lsp::CompletionOptions {
23840 resolve_provider: Some(true),
23841 ..Default::default()
23842 }),
23843 ..Default::default()
23844 },
23845 ..Default::default()
23846 },
23847 );
23848
23849 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23850 let cx = &mut VisualTestContext::from_window(*workspace, cx);
23851
23852 let worktree_id = workspace
23853 .update(cx, |workspace, _window, cx| {
23854 workspace.project().update(cx, |project, cx| {
23855 project.worktrees(cx).next().unwrap().read(cx).id()
23856 })
23857 })
23858 .unwrap();
23859 project
23860 .update(cx, |project, cx| {
23861 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
23862 })
23863 .await
23864 .unwrap();
23865 let editor = workspace
23866 .update(cx, |workspace, window, cx| {
23867 workspace.open_path((worktree_id, rel_path("file.html")), None, true, window, cx)
23868 })
23869 .unwrap()
23870 .await
23871 .unwrap()
23872 .downcast::<Editor>()
23873 .unwrap();
23874
23875 let fake_server = fake_servers.next().await.unwrap();
23876 editor.update_in(cx, |editor, window, cx| {
23877 editor.set_text("<ad></ad>", window, cx);
23878 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
23879 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
23880 });
23881 set_linked_edit_ranges(
23882 (Point::new(0, 1), Point::new(0, 3)),
23883 (Point::new(0, 6), Point::new(0, 8)),
23884 editor,
23885 cx,
23886 );
23887 });
23888 let mut completion_handle =
23889 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
23890 Ok(Some(lsp::CompletionResponse::Array(vec![
23891 lsp::CompletionItem {
23892 label: "head".to_string(),
23893 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
23894 lsp::InsertReplaceEdit {
23895 new_text: "head".to_string(),
23896 insert: lsp::Range::new(
23897 lsp::Position::new(0, 1),
23898 lsp::Position::new(0, 3),
23899 ),
23900 replace: lsp::Range::new(
23901 lsp::Position::new(0, 1),
23902 lsp::Position::new(0, 3),
23903 ),
23904 },
23905 )),
23906 ..Default::default()
23907 },
23908 ])))
23909 });
23910 editor.update_in(cx, |editor, window, cx| {
23911 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
23912 });
23913 cx.run_until_parked();
23914 completion_handle.next().await.unwrap();
23915 editor.update(cx, |editor, _| {
23916 assert!(
23917 editor.context_menu_visible(),
23918 "Completion menu should be visible"
23919 );
23920 });
23921 editor.update_in(cx, |editor, window, cx| {
23922 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
23923 });
23924 cx.executor().run_until_parked();
23925 editor.update(cx, |editor, cx| {
23926 assert_eq!(editor.text(cx), "<head></head>");
23927 });
23928}
23929
23930#[gpui::test]
23931async fn test_linked_edits_on_typing_punctuation(cx: &mut TestAppContext) {
23932 init_test(cx, |_| {});
23933
23934 let mut cx = EditorTestContext::new(cx).await;
23935 let language = Arc::new(Language::new(
23936 LanguageConfig {
23937 name: "TSX".into(),
23938 matcher: LanguageMatcher {
23939 path_suffixes: vec!["tsx".to_string()],
23940 ..LanguageMatcher::default()
23941 },
23942 brackets: BracketPairConfig {
23943 pairs: vec![BracketPair {
23944 start: "<".into(),
23945 end: ">".into(),
23946 close: true,
23947 ..Default::default()
23948 }],
23949 ..Default::default()
23950 },
23951 linked_edit_characters: HashSet::from_iter(['.']),
23952 ..Default::default()
23953 },
23954 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
23955 ));
23956 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23957
23958 // Test typing > does not extend linked pair
23959 cx.set_state("<divˇ<div></div>");
23960 cx.update_editor(|editor, _, cx| {
23961 set_linked_edit_ranges(
23962 (Point::new(0, 1), Point::new(0, 4)),
23963 (Point::new(0, 11), Point::new(0, 14)),
23964 editor,
23965 cx,
23966 );
23967 });
23968 cx.update_editor(|editor, window, cx| {
23969 editor.handle_input(">", window, cx);
23970 });
23971 cx.assert_editor_state("<div>ˇ<div></div>");
23972
23973 // Test typing . do extend linked pair
23974 cx.set_state("<Animatedˇ></Animated>");
23975 cx.update_editor(|editor, _, cx| {
23976 set_linked_edit_ranges(
23977 (Point::new(0, 1), Point::new(0, 9)),
23978 (Point::new(0, 12), Point::new(0, 20)),
23979 editor,
23980 cx,
23981 );
23982 });
23983 cx.update_editor(|editor, window, cx| {
23984 editor.handle_input(".", window, cx);
23985 });
23986 cx.assert_editor_state("<Animated.ˇ></Animated.>");
23987 cx.update_editor(|editor, _, cx| {
23988 set_linked_edit_ranges(
23989 (Point::new(0, 1), Point::new(0, 10)),
23990 (Point::new(0, 13), Point::new(0, 21)),
23991 editor,
23992 cx,
23993 );
23994 });
23995 cx.update_editor(|editor, window, cx| {
23996 editor.handle_input("V", window, cx);
23997 });
23998 cx.assert_editor_state("<Animated.Vˇ></Animated.V>");
23999}
24000
24001#[gpui::test]
24002async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
24003 init_test(cx, |_| {});
24004
24005 let fs = FakeFs::new(cx.executor());
24006 fs.insert_tree(
24007 path!("/root"),
24008 json!({
24009 "a": {
24010 "main.rs": "fn main() {}",
24011 },
24012 "foo": {
24013 "bar": {
24014 "external_file.rs": "pub mod external {}",
24015 }
24016 }
24017 }),
24018 )
24019 .await;
24020
24021 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
24022 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24023 language_registry.add(rust_lang());
24024 let _fake_servers = language_registry.register_fake_lsp(
24025 "Rust",
24026 FakeLspAdapter {
24027 ..FakeLspAdapter::default()
24028 },
24029 );
24030 let (workspace, cx) =
24031 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24032 let worktree_id = workspace.update(cx, |workspace, cx| {
24033 workspace.project().update(cx, |project, cx| {
24034 project.worktrees(cx).next().unwrap().read(cx).id()
24035 })
24036 });
24037
24038 let assert_language_servers_count =
24039 |expected: usize, context: &str, cx: &mut VisualTestContext| {
24040 project.update(cx, |project, cx| {
24041 let current = project
24042 .lsp_store()
24043 .read(cx)
24044 .as_local()
24045 .unwrap()
24046 .language_servers
24047 .len();
24048 assert_eq!(expected, current, "{context}");
24049 });
24050 };
24051
24052 assert_language_servers_count(
24053 0,
24054 "No servers should be running before any file is open",
24055 cx,
24056 );
24057 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
24058 let main_editor = workspace
24059 .update_in(cx, |workspace, window, cx| {
24060 workspace.open_path(
24061 (worktree_id, rel_path("main.rs")),
24062 Some(pane.downgrade()),
24063 true,
24064 window,
24065 cx,
24066 )
24067 })
24068 .unwrap()
24069 .await
24070 .downcast::<Editor>()
24071 .unwrap();
24072 pane.update(cx, |pane, cx| {
24073 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24074 open_editor.update(cx, |editor, cx| {
24075 assert_eq!(
24076 editor.display_text(cx),
24077 "fn main() {}",
24078 "Original main.rs text on initial open",
24079 );
24080 });
24081 assert_eq!(open_editor, main_editor);
24082 });
24083 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
24084
24085 let external_editor = workspace
24086 .update_in(cx, |workspace, window, cx| {
24087 workspace.open_abs_path(
24088 PathBuf::from("/root/foo/bar/external_file.rs"),
24089 OpenOptions::default(),
24090 window,
24091 cx,
24092 )
24093 })
24094 .await
24095 .expect("opening external file")
24096 .downcast::<Editor>()
24097 .expect("downcasted external file's open element to editor");
24098 pane.update(cx, |pane, cx| {
24099 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24100 open_editor.update(cx, |editor, cx| {
24101 assert_eq!(
24102 editor.display_text(cx),
24103 "pub mod external {}",
24104 "External file is open now",
24105 );
24106 });
24107 assert_eq!(open_editor, external_editor);
24108 });
24109 assert_language_servers_count(
24110 1,
24111 "Second, external, *.rs file should join the existing server",
24112 cx,
24113 );
24114
24115 pane.update_in(cx, |pane, window, cx| {
24116 pane.close_active_item(&CloseActiveItem::default(), window, cx)
24117 })
24118 .await
24119 .unwrap();
24120 pane.update_in(cx, |pane, window, cx| {
24121 pane.navigate_backward(&Default::default(), window, cx);
24122 });
24123 cx.run_until_parked();
24124 pane.update(cx, |pane, cx| {
24125 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24126 open_editor.update(cx, |editor, cx| {
24127 assert_eq!(
24128 editor.display_text(cx),
24129 "pub mod external {}",
24130 "External file is open now",
24131 );
24132 });
24133 });
24134 assert_language_servers_count(
24135 1,
24136 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
24137 cx,
24138 );
24139
24140 cx.update(|_, cx| {
24141 workspace::reload(cx);
24142 });
24143 assert_language_servers_count(
24144 1,
24145 "After reloading the worktree with local and external files opened, only one project should be started",
24146 cx,
24147 );
24148}
24149
24150#[gpui::test]
24151async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
24152 init_test(cx, |_| {});
24153
24154 let mut cx = EditorTestContext::new(cx).await;
24155 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24156 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24157
24158 // test cursor move to start of each line on tab
24159 // for `if`, `elif`, `else`, `while`, `with` and `for`
24160 cx.set_state(indoc! {"
24161 def main():
24162 ˇ for item in items:
24163 ˇ while item.active:
24164 ˇ if item.value > 10:
24165 ˇ continue
24166 ˇ elif item.value < 0:
24167 ˇ break
24168 ˇ else:
24169 ˇ with item.context() as ctx:
24170 ˇ yield count
24171 ˇ else:
24172 ˇ log('while else')
24173 ˇ else:
24174 ˇ log('for else')
24175 "});
24176 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24177 cx.assert_editor_state(indoc! {"
24178 def main():
24179 ˇfor item in items:
24180 ˇwhile item.active:
24181 ˇif item.value > 10:
24182 ˇcontinue
24183 ˇelif item.value < 0:
24184 ˇbreak
24185 ˇelse:
24186 ˇwith item.context() as ctx:
24187 ˇyield count
24188 ˇelse:
24189 ˇlog('while else')
24190 ˇelse:
24191 ˇlog('for else')
24192 "});
24193 // test relative indent is preserved when tab
24194 // for `if`, `elif`, `else`, `while`, `with` and `for`
24195 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24196 cx.assert_editor_state(indoc! {"
24197 def main():
24198 ˇfor item in items:
24199 ˇwhile item.active:
24200 ˇif item.value > 10:
24201 ˇcontinue
24202 ˇelif item.value < 0:
24203 ˇbreak
24204 ˇelse:
24205 ˇwith item.context() as ctx:
24206 ˇyield count
24207 ˇelse:
24208 ˇlog('while else')
24209 ˇelse:
24210 ˇlog('for else')
24211 "});
24212
24213 // test cursor move to start of each line on tab
24214 // for `try`, `except`, `else`, `finally`, `match` and `def`
24215 cx.set_state(indoc! {"
24216 def main():
24217 ˇ try:
24218 ˇ fetch()
24219 ˇ except ValueError:
24220 ˇ handle_error()
24221 ˇ else:
24222 ˇ match value:
24223 ˇ case _:
24224 ˇ finally:
24225 ˇ def status():
24226 ˇ return 0
24227 "});
24228 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24229 cx.assert_editor_state(indoc! {"
24230 def main():
24231 ˇtry:
24232 ˇfetch()
24233 ˇexcept ValueError:
24234 ˇhandle_error()
24235 ˇelse:
24236 ˇmatch value:
24237 ˇcase _:
24238 ˇfinally:
24239 ˇdef status():
24240 ˇreturn 0
24241 "});
24242 // test relative indent is preserved when tab
24243 // for `try`, `except`, `else`, `finally`, `match` and `def`
24244 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24245 cx.assert_editor_state(indoc! {"
24246 def main():
24247 ˇtry:
24248 ˇfetch()
24249 ˇexcept ValueError:
24250 ˇhandle_error()
24251 ˇelse:
24252 ˇmatch value:
24253 ˇcase _:
24254 ˇfinally:
24255 ˇdef status():
24256 ˇreturn 0
24257 "});
24258}
24259
24260#[gpui::test]
24261async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
24262 init_test(cx, |_| {});
24263
24264 let mut cx = EditorTestContext::new(cx).await;
24265 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24266 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24267
24268 // test `else` auto outdents when typed inside `if` block
24269 cx.set_state(indoc! {"
24270 def main():
24271 if i == 2:
24272 return
24273 ˇ
24274 "});
24275 cx.update_editor(|editor, window, cx| {
24276 editor.handle_input("else:", window, cx);
24277 });
24278 cx.assert_editor_state(indoc! {"
24279 def main():
24280 if i == 2:
24281 return
24282 else:ˇ
24283 "});
24284
24285 // test `except` auto outdents when typed inside `try` block
24286 cx.set_state(indoc! {"
24287 def main():
24288 try:
24289 i = 2
24290 ˇ
24291 "});
24292 cx.update_editor(|editor, window, cx| {
24293 editor.handle_input("except:", window, cx);
24294 });
24295 cx.assert_editor_state(indoc! {"
24296 def main():
24297 try:
24298 i = 2
24299 except:ˇ
24300 "});
24301
24302 // test `else` auto outdents when typed inside `except` block
24303 cx.set_state(indoc! {"
24304 def main():
24305 try:
24306 i = 2
24307 except:
24308 j = 2
24309 ˇ
24310 "});
24311 cx.update_editor(|editor, window, cx| {
24312 editor.handle_input("else:", window, cx);
24313 });
24314 cx.assert_editor_state(indoc! {"
24315 def main():
24316 try:
24317 i = 2
24318 except:
24319 j = 2
24320 else:ˇ
24321 "});
24322
24323 // test `finally` auto outdents when typed inside `else` block
24324 cx.set_state(indoc! {"
24325 def main():
24326 try:
24327 i = 2
24328 except:
24329 j = 2
24330 else:
24331 k = 2
24332 ˇ
24333 "});
24334 cx.update_editor(|editor, window, cx| {
24335 editor.handle_input("finally:", window, cx);
24336 });
24337 cx.assert_editor_state(indoc! {"
24338 def main():
24339 try:
24340 i = 2
24341 except:
24342 j = 2
24343 else:
24344 k = 2
24345 finally:ˇ
24346 "});
24347
24348 // test `else` does not outdents when typed inside `except` block right after for block
24349 cx.set_state(indoc! {"
24350 def main():
24351 try:
24352 i = 2
24353 except:
24354 for i in range(n):
24355 pass
24356 ˇ
24357 "});
24358 cx.update_editor(|editor, window, cx| {
24359 editor.handle_input("else:", window, cx);
24360 });
24361 cx.assert_editor_state(indoc! {"
24362 def main():
24363 try:
24364 i = 2
24365 except:
24366 for i in range(n):
24367 pass
24368 else:ˇ
24369 "});
24370
24371 // test `finally` auto outdents when typed inside `else` block right after for block
24372 cx.set_state(indoc! {"
24373 def main():
24374 try:
24375 i = 2
24376 except:
24377 j = 2
24378 else:
24379 for i in range(n):
24380 pass
24381 ˇ
24382 "});
24383 cx.update_editor(|editor, window, cx| {
24384 editor.handle_input("finally:", window, cx);
24385 });
24386 cx.assert_editor_state(indoc! {"
24387 def main():
24388 try:
24389 i = 2
24390 except:
24391 j = 2
24392 else:
24393 for i in range(n):
24394 pass
24395 finally:ˇ
24396 "});
24397
24398 // test `except` outdents to inner "try" block
24399 cx.set_state(indoc! {"
24400 def main():
24401 try:
24402 i = 2
24403 if i == 2:
24404 try:
24405 i = 3
24406 ˇ
24407 "});
24408 cx.update_editor(|editor, window, cx| {
24409 editor.handle_input("except:", window, cx);
24410 });
24411 cx.assert_editor_state(indoc! {"
24412 def main():
24413 try:
24414 i = 2
24415 if i == 2:
24416 try:
24417 i = 3
24418 except:ˇ
24419 "});
24420
24421 // test `except` outdents to outer "try" block
24422 cx.set_state(indoc! {"
24423 def main():
24424 try:
24425 i = 2
24426 if i == 2:
24427 try:
24428 i = 3
24429 ˇ
24430 "});
24431 cx.update_editor(|editor, window, cx| {
24432 editor.handle_input("except:", window, cx);
24433 });
24434 cx.assert_editor_state(indoc! {"
24435 def main():
24436 try:
24437 i = 2
24438 if i == 2:
24439 try:
24440 i = 3
24441 except:ˇ
24442 "});
24443
24444 // test `else` stays at correct indent when typed after `for` block
24445 cx.set_state(indoc! {"
24446 def main():
24447 for i in range(10):
24448 if i == 3:
24449 break
24450 ˇ
24451 "});
24452 cx.update_editor(|editor, window, cx| {
24453 editor.handle_input("else:", window, cx);
24454 });
24455 cx.assert_editor_state(indoc! {"
24456 def main():
24457 for i in range(10):
24458 if i == 3:
24459 break
24460 else:ˇ
24461 "});
24462
24463 // test does not outdent on typing after line with square brackets
24464 cx.set_state(indoc! {"
24465 def f() -> list[str]:
24466 ˇ
24467 "});
24468 cx.update_editor(|editor, window, cx| {
24469 editor.handle_input("a", window, cx);
24470 });
24471 cx.assert_editor_state(indoc! {"
24472 def f() -> list[str]:
24473 aˇ
24474 "});
24475
24476 // test does not outdent on typing : after case keyword
24477 cx.set_state(indoc! {"
24478 match 1:
24479 caseˇ
24480 "});
24481 cx.update_editor(|editor, window, cx| {
24482 editor.handle_input(":", window, cx);
24483 });
24484 cx.assert_editor_state(indoc! {"
24485 match 1:
24486 case:ˇ
24487 "});
24488}
24489
24490#[gpui::test]
24491async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
24492 init_test(cx, |_| {});
24493 update_test_language_settings(cx, |settings| {
24494 settings.defaults.extend_comment_on_newline = Some(false);
24495 });
24496 let mut cx = EditorTestContext::new(cx).await;
24497 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24498 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24499
24500 // test correct indent after newline on comment
24501 cx.set_state(indoc! {"
24502 # COMMENT:ˇ
24503 "});
24504 cx.update_editor(|editor, window, cx| {
24505 editor.newline(&Newline, window, cx);
24506 });
24507 cx.assert_editor_state(indoc! {"
24508 # COMMENT:
24509 ˇ
24510 "});
24511
24512 // test correct indent after newline in brackets
24513 cx.set_state(indoc! {"
24514 {ˇ}
24515 "});
24516 cx.update_editor(|editor, window, cx| {
24517 editor.newline(&Newline, window, cx);
24518 });
24519 cx.run_until_parked();
24520 cx.assert_editor_state(indoc! {"
24521 {
24522 ˇ
24523 }
24524 "});
24525
24526 cx.set_state(indoc! {"
24527 (ˇ)
24528 "});
24529 cx.update_editor(|editor, window, cx| {
24530 editor.newline(&Newline, window, cx);
24531 });
24532 cx.run_until_parked();
24533 cx.assert_editor_state(indoc! {"
24534 (
24535 ˇ
24536 )
24537 "});
24538
24539 // do not indent after empty lists or dictionaries
24540 cx.set_state(indoc! {"
24541 a = []ˇ
24542 "});
24543 cx.update_editor(|editor, window, cx| {
24544 editor.newline(&Newline, window, cx);
24545 });
24546 cx.run_until_parked();
24547 cx.assert_editor_state(indoc! {"
24548 a = []
24549 ˇ
24550 "});
24551}
24552
24553#[gpui::test]
24554async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
24555 init_test(cx, |_| {});
24556
24557 let mut cx = EditorTestContext::new(cx).await;
24558 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24559 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24560
24561 // test cursor move to start of each line on tab
24562 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
24563 cx.set_state(indoc! {"
24564 function main() {
24565 ˇ for item in $items; do
24566 ˇ while [ -n \"$item\" ]; do
24567 ˇ if [ \"$value\" -gt 10 ]; then
24568 ˇ continue
24569 ˇ elif [ \"$value\" -lt 0 ]; then
24570 ˇ break
24571 ˇ else
24572 ˇ echo \"$item\"
24573 ˇ fi
24574 ˇ done
24575 ˇ done
24576 ˇ}
24577 "});
24578 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24579 cx.assert_editor_state(indoc! {"
24580 function main() {
24581 ˇfor item in $items; do
24582 ˇwhile [ -n \"$item\" ]; do
24583 ˇif [ \"$value\" -gt 10 ]; then
24584 ˇcontinue
24585 ˇelif [ \"$value\" -lt 0 ]; then
24586 ˇbreak
24587 ˇelse
24588 ˇecho \"$item\"
24589 ˇfi
24590 ˇdone
24591 ˇdone
24592 ˇ}
24593 "});
24594 // test relative indent is preserved when tab
24595 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24596 cx.assert_editor_state(indoc! {"
24597 function main() {
24598 ˇfor item in $items; do
24599 ˇwhile [ -n \"$item\" ]; do
24600 ˇif [ \"$value\" -gt 10 ]; then
24601 ˇcontinue
24602 ˇelif [ \"$value\" -lt 0 ]; then
24603 ˇbreak
24604 ˇelse
24605 ˇecho \"$item\"
24606 ˇfi
24607 ˇdone
24608 ˇdone
24609 ˇ}
24610 "});
24611
24612 // test cursor move to start of each line on tab
24613 // for `case` statement with patterns
24614 cx.set_state(indoc! {"
24615 function handle() {
24616 ˇ case \"$1\" in
24617 ˇ start)
24618 ˇ echo \"a\"
24619 ˇ ;;
24620 ˇ stop)
24621 ˇ echo \"b\"
24622 ˇ ;;
24623 ˇ *)
24624 ˇ echo \"c\"
24625 ˇ ;;
24626 ˇ esac
24627 ˇ}
24628 "});
24629 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24630 cx.assert_editor_state(indoc! {"
24631 function handle() {
24632 ˇcase \"$1\" in
24633 ˇstart)
24634 ˇecho \"a\"
24635 ˇ;;
24636 ˇstop)
24637 ˇecho \"b\"
24638 ˇ;;
24639 ˇ*)
24640 ˇecho \"c\"
24641 ˇ;;
24642 ˇesac
24643 ˇ}
24644 "});
24645}
24646
24647#[gpui::test]
24648async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
24649 init_test(cx, |_| {});
24650
24651 let mut cx = EditorTestContext::new(cx).await;
24652 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24653 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24654
24655 // test indents on comment insert
24656 cx.set_state(indoc! {"
24657 function main() {
24658 ˇ for item in $items; do
24659 ˇ while [ -n \"$item\" ]; do
24660 ˇ if [ \"$value\" -gt 10 ]; then
24661 ˇ continue
24662 ˇ elif [ \"$value\" -lt 0 ]; then
24663 ˇ break
24664 ˇ else
24665 ˇ echo \"$item\"
24666 ˇ fi
24667 ˇ done
24668 ˇ done
24669 ˇ}
24670 "});
24671 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
24672 cx.assert_editor_state(indoc! {"
24673 function main() {
24674 #ˇ for item in $items; do
24675 #ˇ while [ -n \"$item\" ]; do
24676 #ˇ if [ \"$value\" -gt 10 ]; then
24677 #ˇ continue
24678 #ˇ elif [ \"$value\" -lt 0 ]; then
24679 #ˇ break
24680 #ˇ else
24681 #ˇ echo \"$item\"
24682 #ˇ fi
24683 #ˇ done
24684 #ˇ done
24685 #ˇ}
24686 "});
24687}
24688
24689#[gpui::test]
24690async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
24691 init_test(cx, |_| {});
24692
24693 let mut cx = EditorTestContext::new(cx).await;
24694 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24695 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24696
24697 // test `else` auto outdents when typed inside `if` block
24698 cx.set_state(indoc! {"
24699 if [ \"$1\" = \"test\" ]; then
24700 echo \"foo bar\"
24701 ˇ
24702 "});
24703 cx.update_editor(|editor, window, cx| {
24704 editor.handle_input("else", window, cx);
24705 });
24706 cx.assert_editor_state(indoc! {"
24707 if [ \"$1\" = \"test\" ]; then
24708 echo \"foo bar\"
24709 elseˇ
24710 "});
24711
24712 // test `elif` auto outdents when typed inside `if` block
24713 cx.set_state(indoc! {"
24714 if [ \"$1\" = \"test\" ]; then
24715 echo \"foo bar\"
24716 ˇ
24717 "});
24718 cx.update_editor(|editor, window, cx| {
24719 editor.handle_input("elif", window, cx);
24720 });
24721 cx.assert_editor_state(indoc! {"
24722 if [ \"$1\" = \"test\" ]; then
24723 echo \"foo bar\"
24724 elifˇ
24725 "});
24726
24727 // test `fi` auto outdents when typed inside `else` block
24728 cx.set_state(indoc! {"
24729 if [ \"$1\" = \"test\" ]; then
24730 echo \"foo bar\"
24731 else
24732 echo \"bar baz\"
24733 ˇ
24734 "});
24735 cx.update_editor(|editor, window, cx| {
24736 editor.handle_input("fi", window, cx);
24737 });
24738 cx.assert_editor_state(indoc! {"
24739 if [ \"$1\" = \"test\" ]; then
24740 echo \"foo bar\"
24741 else
24742 echo \"bar baz\"
24743 fiˇ
24744 "});
24745
24746 // test `done` auto outdents when typed inside `while` block
24747 cx.set_state(indoc! {"
24748 while read line; do
24749 echo \"$line\"
24750 ˇ
24751 "});
24752 cx.update_editor(|editor, window, cx| {
24753 editor.handle_input("done", window, cx);
24754 });
24755 cx.assert_editor_state(indoc! {"
24756 while read line; do
24757 echo \"$line\"
24758 doneˇ
24759 "});
24760
24761 // test `done` auto outdents when typed inside `for` block
24762 cx.set_state(indoc! {"
24763 for file in *.txt; do
24764 cat \"$file\"
24765 ˇ
24766 "});
24767 cx.update_editor(|editor, window, cx| {
24768 editor.handle_input("done", window, cx);
24769 });
24770 cx.assert_editor_state(indoc! {"
24771 for file in *.txt; do
24772 cat \"$file\"
24773 doneˇ
24774 "});
24775
24776 // test `esac` auto outdents when typed inside `case` block
24777 cx.set_state(indoc! {"
24778 case \"$1\" in
24779 start)
24780 echo \"foo bar\"
24781 ;;
24782 stop)
24783 echo \"bar baz\"
24784 ;;
24785 ˇ
24786 "});
24787 cx.update_editor(|editor, window, cx| {
24788 editor.handle_input("esac", window, cx);
24789 });
24790 cx.assert_editor_state(indoc! {"
24791 case \"$1\" in
24792 start)
24793 echo \"foo bar\"
24794 ;;
24795 stop)
24796 echo \"bar baz\"
24797 ;;
24798 esacˇ
24799 "});
24800
24801 // test `*)` auto outdents when typed inside `case` block
24802 cx.set_state(indoc! {"
24803 case \"$1\" in
24804 start)
24805 echo \"foo bar\"
24806 ;;
24807 ˇ
24808 "});
24809 cx.update_editor(|editor, window, cx| {
24810 editor.handle_input("*)", window, cx);
24811 });
24812 cx.assert_editor_state(indoc! {"
24813 case \"$1\" in
24814 start)
24815 echo \"foo bar\"
24816 ;;
24817 *)ˇ
24818 "});
24819
24820 // test `fi` outdents to correct level with nested if blocks
24821 cx.set_state(indoc! {"
24822 if [ \"$1\" = \"test\" ]; then
24823 echo \"outer if\"
24824 if [ \"$2\" = \"debug\" ]; then
24825 echo \"inner if\"
24826 ˇ
24827 "});
24828 cx.update_editor(|editor, window, cx| {
24829 editor.handle_input("fi", window, cx);
24830 });
24831 cx.assert_editor_state(indoc! {"
24832 if [ \"$1\" = \"test\" ]; then
24833 echo \"outer if\"
24834 if [ \"$2\" = \"debug\" ]; then
24835 echo \"inner if\"
24836 fiˇ
24837 "});
24838}
24839
24840#[gpui::test]
24841async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
24842 init_test(cx, |_| {});
24843 update_test_language_settings(cx, |settings| {
24844 settings.defaults.extend_comment_on_newline = Some(false);
24845 });
24846 let mut cx = EditorTestContext::new(cx).await;
24847 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24848 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24849
24850 // test correct indent after newline on comment
24851 cx.set_state(indoc! {"
24852 # COMMENT:ˇ
24853 "});
24854 cx.update_editor(|editor, window, cx| {
24855 editor.newline(&Newline, window, cx);
24856 });
24857 cx.assert_editor_state(indoc! {"
24858 # COMMENT:
24859 ˇ
24860 "});
24861
24862 // test correct indent after newline after `then`
24863 cx.set_state(indoc! {"
24864
24865 if [ \"$1\" = \"test\" ]; thenˇ
24866 "});
24867 cx.update_editor(|editor, window, cx| {
24868 editor.newline(&Newline, window, cx);
24869 });
24870 cx.run_until_parked();
24871 cx.assert_editor_state(indoc! {"
24872
24873 if [ \"$1\" = \"test\" ]; then
24874 ˇ
24875 "});
24876
24877 // test correct indent after newline after `else`
24878 cx.set_state(indoc! {"
24879 if [ \"$1\" = \"test\" ]; then
24880 elseˇ
24881 "});
24882 cx.update_editor(|editor, window, cx| {
24883 editor.newline(&Newline, window, cx);
24884 });
24885 cx.run_until_parked();
24886 cx.assert_editor_state(indoc! {"
24887 if [ \"$1\" = \"test\" ]; then
24888 else
24889 ˇ
24890 "});
24891
24892 // test correct indent after newline after `elif`
24893 cx.set_state(indoc! {"
24894 if [ \"$1\" = \"test\" ]; then
24895 elifˇ
24896 "});
24897 cx.update_editor(|editor, window, cx| {
24898 editor.newline(&Newline, window, cx);
24899 });
24900 cx.run_until_parked();
24901 cx.assert_editor_state(indoc! {"
24902 if [ \"$1\" = \"test\" ]; then
24903 elif
24904 ˇ
24905 "});
24906
24907 // test correct indent after newline after `do`
24908 cx.set_state(indoc! {"
24909 for file in *.txt; doˇ
24910 "});
24911 cx.update_editor(|editor, window, cx| {
24912 editor.newline(&Newline, window, cx);
24913 });
24914 cx.run_until_parked();
24915 cx.assert_editor_state(indoc! {"
24916 for file in *.txt; do
24917 ˇ
24918 "});
24919
24920 // test correct indent after newline after case pattern
24921 cx.set_state(indoc! {"
24922 case \"$1\" in
24923 start)ˇ
24924 "});
24925 cx.update_editor(|editor, window, cx| {
24926 editor.newline(&Newline, window, cx);
24927 });
24928 cx.run_until_parked();
24929 cx.assert_editor_state(indoc! {"
24930 case \"$1\" in
24931 start)
24932 ˇ
24933 "});
24934
24935 // test correct indent after newline after case pattern
24936 cx.set_state(indoc! {"
24937 case \"$1\" in
24938 start)
24939 ;;
24940 *)ˇ
24941 "});
24942 cx.update_editor(|editor, window, cx| {
24943 editor.newline(&Newline, window, cx);
24944 });
24945 cx.run_until_parked();
24946 cx.assert_editor_state(indoc! {"
24947 case \"$1\" in
24948 start)
24949 ;;
24950 *)
24951 ˇ
24952 "});
24953
24954 // test correct indent after newline after function opening brace
24955 cx.set_state(indoc! {"
24956 function test() {ˇ}
24957 "});
24958 cx.update_editor(|editor, window, cx| {
24959 editor.newline(&Newline, window, cx);
24960 });
24961 cx.run_until_parked();
24962 cx.assert_editor_state(indoc! {"
24963 function test() {
24964 ˇ
24965 }
24966 "});
24967
24968 // test no extra indent after semicolon on same line
24969 cx.set_state(indoc! {"
24970 echo \"test\";ˇ
24971 "});
24972 cx.update_editor(|editor, window, cx| {
24973 editor.newline(&Newline, window, cx);
24974 });
24975 cx.run_until_parked();
24976 cx.assert_editor_state(indoc! {"
24977 echo \"test\";
24978 ˇ
24979 "});
24980}
24981
24982fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
24983 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
24984 point..point
24985}
24986
24987#[track_caller]
24988fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
24989 let (text, ranges) = marked_text_ranges(marked_text, true);
24990 assert_eq!(editor.text(cx), text);
24991 assert_eq!(
24992 editor.selections.ranges(cx),
24993 ranges,
24994 "Assert selections are {}",
24995 marked_text
24996 );
24997}
24998
24999pub fn handle_signature_help_request(
25000 cx: &mut EditorLspTestContext,
25001 mocked_response: lsp::SignatureHelp,
25002) -> impl Future<Output = ()> + use<> {
25003 let mut request =
25004 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
25005 let mocked_response = mocked_response.clone();
25006 async move { Ok(Some(mocked_response)) }
25007 });
25008
25009 async move {
25010 request.next().await;
25011 }
25012}
25013
25014#[track_caller]
25015pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
25016 cx.update_editor(|editor, _, _| {
25017 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
25018 let entries = menu.entries.borrow();
25019 let entries = entries
25020 .iter()
25021 .map(|entry| entry.string.as_str())
25022 .collect::<Vec<_>>();
25023 assert_eq!(entries, expected);
25024 } else {
25025 panic!("Expected completions menu");
25026 }
25027 });
25028}
25029
25030/// Handle completion request passing a marked string specifying where the completion
25031/// should be triggered from using '|' character, what range should be replaced, and what completions
25032/// should be returned using '<' and '>' to delimit the range.
25033///
25034/// Also see `handle_completion_request_with_insert_and_replace`.
25035#[track_caller]
25036pub fn handle_completion_request(
25037 marked_string: &str,
25038 completions: Vec<&'static str>,
25039 is_incomplete: bool,
25040 counter: Arc<AtomicUsize>,
25041 cx: &mut EditorLspTestContext,
25042) -> impl Future<Output = ()> {
25043 let complete_from_marker: TextRangeMarker = '|'.into();
25044 let replace_range_marker: TextRangeMarker = ('<', '>').into();
25045 let (_, mut marked_ranges) = marked_text_ranges_by(
25046 marked_string,
25047 vec![complete_from_marker.clone(), replace_range_marker.clone()],
25048 );
25049
25050 let complete_from_position =
25051 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
25052 let replace_range =
25053 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
25054
25055 let mut request =
25056 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
25057 let completions = completions.clone();
25058 counter.fetch_add(1, atomic::Ordering::Release);
25059 async move {
25060 assert_eq!(params.text_document_position.text_document.uri, url.clone());
25061 assert_eq!(
25062 params.text_document_position.position,
25063 complete_from_position
25064 );
25065 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
25066 is_incomplete,
25067 item_defaults: None,
25068 items: completions
25069 .iter()
25070 .map(|completion_text| lsp::CompletionItem {
25071 label: completion_text.to_string(),
25072 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
25073 range: replace_range,
25074 new_text: completion_text.to_string(),
25075 })),
25076 ..Default::default()
25077 })
25078 .collect(),
25079 })))
25080 }
25081 });
25082
25083 async move {
25084 request.next().await;
25085 }
25086}
25087
25088/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
25089/// given instead, which also contains an `insert` range.
25090///
25091/// This function uses markers to define ranges:
25092/// - `|` marks the cursor position
25093/// - `<>` marks the replace range
25094/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
25095pub fn handle_completion_request_with_insert_and_replace(
25096 cx: &mut EditorLspTestContext,
25097 marked_string: &str,
25098 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
25099 counter: Arc<AtomicUsize>,
25100) -> impl Future<Output = ()> {
25101 let complete_from_marker: TextRangeMarker = '|'.into();
25102 let replace_range_marker: TextRangeMarker = ('<', '>').into();
25103 let insert_range_marker: TextRangeMarker = ('{', '}').into();
25104
25105 let (_, mut marked_ranges) = marked_text_ranges_by(
25106 marked_string,
25107 vec![
25108 complete_from_marker.clone(),
25109 replace_range_marker.clone(),
25110 insert_range_marker.clone(),
25111 ],
25112 );
25113
25114 let complete_from_position =
25115 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
25116 let replace_range =
25117 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
25118
25119 let insert_range = match marked_ranges.remove(&insert_range_marker) {
25120 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
25121 _ => lsp::Range {
25122 start: replace_range.start,
25123 end: complete_from_position,
25124 },
25125 };
25126
25127 let mut request =
25128 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
25129 let completions = completions.clone();
25130 counter.fetch_add(1, atomic::Ordering::Release);
25131 async move {
25132 assert_eq!(params.text_document_position.text_document.uri, url.clone());
25133 assert_eq!(
25134 params.text_document_position.position, complete_from_position,
25135 "marker `|` position doesn't match",
25136 );
25137 Ok(Some(lsp::CompletionResponse::Array(
25138 completions
25139 .iter()
25140 .map(|(label, new_text)| lsp::CompletionItem {
25141 label: label.to_string(),
25142 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
25143 lsp::InsertReplaceEdit {
25144 insert: insert_range,
25145 replace: replace_range,
25146 new_text: new_text.to_string(),
25147 },
25148 )),
25149 ..Default::default()
25150 })
25151 .collect(),
25152 )))
25153 }
25154 });
25155
25156 async move {
25157 request.next().await;
25158 }
25159}
25160
25161fn handle_resolve_completion_request(
25162 cx: &mut EditorLspTestContext,
25163 edits: Option<Vec<(&'static str, &'static str)>>,
25164) -> impl Future<Output = ()> {
25165 let edits = edits.map(|edits| {
25166 edits
25167 .iter()
25168 .map(|(marked_string, new_text)| {
25169 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
25170 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
25171 lsp::TextEdit::new(replace_range, new_text.to_string())
25172 })
25173 .collect::<Vec<_>>()
25174 });
25175
25176 let mut request =
25177 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
25178 let edits = edits.clone();
25179 async move {
25180 Ok(lsp::CompletionItem {
25181 additional_text_edits: edits,
25182 ..Default::default()
25183 })
25184 }
25185 });
25186
25187 async move {
25188 request.next().await;
25189 }
25190}
25191
25192pub(crate) fn update_test_language_settings(
25193 cx: &mut TestAppContext,
25194 f: impl Fn(&mut AllLanguageSettingsContent),
25195) {
25196 cx.update(|cx| {
25197 SettingsStore::update_global(cx, |store, cx| {
25198 store.update_user_settings(cx, |settings| f(&mut settings.project.all_languages));
25199 });
25200 });
25201}
25202
25203pub(crate) fn update_test_project_settings(
25204 cx: &mut TestAppContext,
25205 f: impl Fn(&mut ProjectSettingsContent),
25206) {
25207 cx.update(|cx| {
25208 SettingsStore::update_global(cx, |store, cx| {
25209 store.update_user_settings(cx, |settings| f(&mut settings.project));
25210 });
25211 });
25212}
25213
25214pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
25215 cx.update(|cx| {
25216 assets::Assets.load_test_fonts(cx);
25217 let store = SettingsStore::test(cx);
25218 cx.set_global(store);
25219 theme::init(theme::LoadThemes::JustBase, cx);
25220 release_channel::init(SemanticVersion::default(), cx);
25221 client::init_settings(cx);
25222 language::init(cx);
25223 Project::init_settings(cx);
25224 workspace::init_settings(cx);
25225 crate::init(cx);
25226 });
25227 zlog::init_test();
25228 update_test_language_settings(cx, f);
25229}
25230
25231#[track_caller]
25232fn assert_hunk_revert(
25233 not_reverted_text_with_selections: &str,
25234 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
25235 expected_reverted_text_with_selections: &str,
25236 base_text: &str,
25237 cx: &mut EditorLspTestContext,
25238) {
25239 cx.set_state(not_reverted_text_with_selections);
25240 cx.set_head_text(base_text);
25241 cx.executor().run_until_parked();
25242
25243 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
25244 let snapshot = editor.snapshot(window, cx);
25245 let reverted_hunk_statuses = snapshot
25246 .buffer_snapshot
25247 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
25248 .map(|hunk| hunk.status().kind)
25249 .collect::<Vec<_>>();
25250
25251 editor.git_restore(&Default::default(), window, cx);
25252 reverted_hunk_statuses
25253 });
25254 cx.executor().run_until_parked();
25255 cx.assert_editor_state(expected_reverted_text_with_selections);
25256 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
25257}
25258
25259#[gpui::test(iterations = 10)]
25260async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
25261 init_test(cx, |_| {});
25262
25263 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
25264 let counter = diagnostic_requests.clone();
25265
25266 let fs = FakeFs::new(cx.executor());
25267 fs.insert_tree(
25268 path!("/a"),
25269 json!({
25270 "first.rs": "fn main() { let a = 5; }",
25271 "second.rs": "// Test file",
25272 }),
25273 )
25274 .await;
25275
25276 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25277 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25278 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25279
25280 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25281 language_registry.add(rust_lang());
25282 let mut fake_servers = language_registry.register_fake_lsp(
25283 "Rust",
25284 FakeLspAdapter {
25285 capabilities: lsp::ServerCapabilities {
25286 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
25287 lsp::DiagnosticOptions {
25288 identifier: None,
25289 inter_file_dependencies: true,
25290 workspace_diagnostics: true,
25291 work_done_progress_options: Default::default(),
25292 },
25293 )),
25294 ..Default::default()
25295 },
25296 ..Default::default()
25297 },
25298 );
25299
25300 let editor = workspace
25301 .update(cx, |workspace, window, cx| {
25302 workspace.open_abs_path(
25303 PathBuf::from(path!("/a/first.rs")),
25304 OpenOptions::default(),
25305 window,
25306 cx,
25307 )
25308 })
25309 .unwrap()
25310 .await
25311 .unwrap()
25312 .downcast::<Editor>()
25313 .unwrap();
25314 let fake_server = fake_servers.next().await.unwrap();
25315 let server_id = fake_server.server.server_id();
25316 let mut first_request = fake_server
25317 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
25318 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
25319 let result_id = Some(new_result_id.to_string());
25320 assert_eq!(
25321 params.text_document.uri,
25322 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25323 );
25324 async move {
25325 Ok(lsp::DocumentDiagnosticReportResult::Report(
25326 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
25327 related_documents: None,
25328 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
25329 items: Vec::new(),
25330 result_id,
25331 },
25332 }),
25333 ))
25334 }
25335 });
25336
25337 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
25338 project.update(cx, |project, cx| {
25339 let buffer_id = editor
25340 .read(cx)
25341 .buffer()
25342 .read(cx)
25343 .as_singleton()
25344 .expect("created a singleton buffer")
25345 .read(cx)
25346 .remote_id();
25347 let buffer_result_id = project
25348 .lsp_store()
25349 .read(cx)
25350 .result_id(server_id, buffer_id, cx);
25351 assert_eq!(expected, buffer_result_id);
25352 });
25353 };
25354
25355 ensure_result_id(None, cx);
25356 cx.executor().advance_clock(Duration::from_millis(60));
25357 cx.executor().run_until_parked();
25358 assert_eq!(
25359 diagnostic_requests.load(atomic::Ordering::Acquire),
25360 1,
25361 "Opening file should trigger diagnostic request"
25362 );
25363 first_request
25364 .next()
25365 .await
25366 .expect("should have sent the first diagnostics pull request");
25367 ensure_result_id(Some("1".to_string()), cx);
25368
25369 // Editing should trigger diagnostics
25370 editor.update_in(cx, |editor, window, cx| {
25371 editor.handle_input("2", window, cx)
25372 });
25373 cx.executor().advance_clock(Duration::from_millis(60));
25374 cx.executor().run_until_parked();
25375 assert_eq!(
25376 diagnostic_requests.load(atomic::Ordering::Acquire),
25377 2,
25378 "Editing should trigger diagnostic request"
25379 );
25380 ensure_result_id(Some("2".to_string()), cx);
25381
25382 // Moving cursor should not trigger diagnostic request
25383 editor.update_in(cx, |editor, window, cx| {
25384 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25385 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
25386 });
25387 });
25388 cx.executor().advance_clock(Duration::from_millis(60));
25389 cx.executor().run_until_parked();
25390 assert_eq!(
25391 diagnostic_requests.load(atomic::Ordering::Acquire),
25392 2,
25393 "Cursor movement should not trigger diagnostic request"
25394 );
25395 ensure_result_id(Some("2".to_string()), cx);
25396 // Multiple rapid edits should be debounced
25397 for _ in 0..5 {
25398 editor.update_in(cx, |editor, window, cx| {
25399 editor.handle_input("x", window, cx)
25400 });
25401 }
25402 cx.executor().advance_clock(Duration::from_millis(60));
25403 cx.executor().run_until_parked();
25404
25405 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
25406 assert!(
25407 final_requests <= 4,
25408 "Multiple rapid edits should be debounced (got {final_requests} requests)",
25409 );
25410 ensure_result_id(Some(final_requests.to_string()), cx);
25411}
25412
25413#[gpui::test]
25414async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
25415 // Regression test for issue #11671
25416 // Previously, adding a cursor after moving multiple cursors would reset
25417 // the cursor count instead of adding to the existing cursors.
25418 init_test(cx, |_| {});
25419 let mut cx = EditorTestContext::new(cx).await;
25420
25421 // Create a simple buffer with cursor at start
25422 cx.set_state(indoc! {"
25423 ˇaaaa
25424 bbbb
25425 cccc
25426 dddd
25427 eeee
25428 ffff
25429 gggg
25430 hhhh"});
25431
25432 // Add 2 cursors below (so we have 3 total)
25433 cx.update_editor(|editor, window, cx| {
25434 editor.add_selection_below(&Default::default(), window, cx);
25435 editor.add_selection_below(&Default::default(), window, cx);
25436 });
25437
25438 // Verify we have 3 cursors
25439 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
25440 assert_eq!(
25441 initial_count, 3,
25442 "Should have 3 cursors after adding 2 below"
25443 );
25444
25445 // Move down one line
25446 cx.update_editor(|editor, window, cx| {
25447 editor.move_down(&MoveDown, window, cx);
25448 });
25449
25450 // Add another cursor below
25451 cx.update_editor(|editor, window, cx| {
25452 editor.add_selection_below(&Default::default(), window, cx);
25453 });
25454
25455 // Should now have 4 cursors (3 original + 1 new)
25456 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
25457 assert_eq!(
25458 final_count, 4,
25459 "Should have 4 cursors after moving and adding another"
25460 );
25461}
25462
25463#[gpui::test(iterations = 10)]
25464async fn test_document_colors(cx: &mut TestAppContext) {
25465 let expected_color = Rgba {
25466 r: 0.33,
25467 g: 0.33,
25468 b: 0.33,
25469 a: 0.33,
25470 };
25471
25472 init_test(cx, |_| {});
25473
25474 let fs = FakeFs::new(cx.executor());
25475 fs.insert_tree(
25476 path!("/a"),
25477 json!({
25478 "first.rs": "fn main() { let a = 5; }",
25479 }),
25480 )
25481 .await;
25482
25483 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25484 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25485 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25486
25487 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25488 language_registry.add(rust_lang());
25489 let mut fake_servers = language_registry.register_fake_lsp(
25490 "Rust",
25491 FakeLspAdapter {
25492 capabilities: lsp::ServerCapabilities {
25493 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
25494 ..lsp::ServerCapabilities::default()
25495 },
25496 name: "rust-analyzer",
25497 ..FakeLspAdapter::default()
25498 },
25499 );
25500 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
25501 "Rust",
25502 FakeLspAdapter {
25503 capabilities: lsp::ServerCapabilities {
25504 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
25505 ..lsp::ServerCapabilities::default()
25506 },
25507 name: "not-rust-analyzer",
25508 ..FakeLspAdapter::default()
25509 },
25510 );
25511
25512 let editor = workspace
25513 .update(cx, |workspace, window, cx| {
25514 workspace.open_abs_path(
25515 PathBuf::from(path!("/a/first.rs")),
25516 OpenOptions::default(),
25517 window,
25518 cx,
25519 )
25520 })
25521 .unwrap()
25522 .await
25523 .unwrap()
25524 .downcast::<Editor>()
25525 .unwrap();
25526 let fake_language_server = fake_servers.next().await.unwrap();
25527 let fake_language_server_without_capabilities =
25528 fake_servers_without_capabilities.next().await.unwrap();
25529 let requests_made = Arc::new(AtomicUsize::new(0));
25530 let closure_requests_made = Arc::clone(&requests_made);
25531 let mut color_request_handle = fake_language_server
25532 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
25533 let requests_made = Arc::clone(&closure_requests_made);
25534 async move {
25535 assert_eq!(
25536 params.text_document.uri,
25537 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25538 );
25539 requests_made.fetch_add(1, atomic::Ordering::Release);
25540 Ok(vec![
25541 lsp::ColorInformation {
25542 range: lsp::Range {
25543 start: lsp::Position {
25544 line: 0,
25545 character: 0,
25546 },
25547 end: lsp::Position {
25548 line: 0,
25549 character: 1,
25550 },
25551 },
25552 color: lsp::Color {
25553 red: 0.33,
25554 green: 0.33,
25555 blue: 0.33,
25556 alpha: 0.33,
25557 },
25558 },
25559 lsp::ColorInformation {
25560 range: lsp::Range {
25561 start: lsp::Position {
25562 line: 0,
25563 character: 0,
25564 },
25565 end: lsp::Position {
25566 line: 0,
25567 character: 1,
25568 },
25569 },
25570 color: lsp::Color {
25571 red: 0.33,
25572 green: 0.33,
25573 blue: 0.33,
25574 alpha: 0.33,
25575 },
25576 },
25577 ])
25578 }
25579 });
25580
25581 let _handle = fake_language_server_without_capabilities
25582 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
25583 panic!("Should not be called");
25584 });
25585 cx.executor().advance_clock(Duration::from_millis(100));
25586 color_request_handle.next().await.unwrap();
25587 cx.run_until_parked();
25588 assert_eq!(
25589 1,
25590 requests_made.load(atomic::Ordering::Acquire),
25591 "Should query for colors once per editor open"
25592 );
25593 editor.update_in(cx, |editor, _, cx| {
25594 assert_eq!(
25595 vec![expected_color],
25596 extract_color_inlays(editor, cx),
25597 "Should have an initial inlay"
25598 );
25599 });
25600
25601 // opening another file in a split should not influence the LSP query counter
25602 workspace
25603 .update(cx, |workspace, window, cx| {
25604 assert_eq!(
25605 workspace.panes().len(),
25606 1,
25607 "Should have one pane with one editor"
25608 );
25609 workspace.move_item_to_pane_in_direction(
25610 &MoveItemToPaneInDirection {
25611 direction: SplitDirection::Right,
25612 focus: false,
25613 clone: true,
25614 },
25615 window,
25616 cx,
25617 );
25618 })
25619 .unwrap();
25620 cx.run_until_parked();
25621 workspace
25622 .update(cx, |workspace, _, cx| {
25623 let panes = workspace.panes();
25624 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
25625 for pane in panes {
25626 let editor = pane
25627 .read(cx)
25628 .active_item()
25629 .and_then(|item| item.downcast::<Editor>())
25630 .expect("Should have opened an editor in each split");
25631 let editor_file = editor
25632 .read(cx)
25633 .buffer()
25634 .read(cx)
25635 .as_singleton()
25636 .expect("test deals with singleton buffers")
25637 .read(cx)
25638 .file()
25639 .expect("test buffese should have a file")
25640 .path();
25641 assert_eq!(
25642 editor_file.as_ref(),
25643 rel_path("first.rs"),
25644 "Both editors should be opened for the same file"
25645 )
25646 }
25647 })
25648 .unwrap();
25649
25650 cx.executor().advance_clock(Duration::from_millis(500));
25651 let save = editor.update_in(cx, |editor, window, cx| {
25652 editor.move_to_end(&MoveToEnd, window, cx);
25653 editor.handle_input("dirty", window, cx);
25654 editor.save(
25655 SaveOptions {
25656 format: true,
25657 autosave: true,
25658 },
25659 project.clone(),
25660 window,
25661 cx,
25662 )
25663 });
25664 save.await.unwrap();
25665
25666 color_request_handle.next().await.unwrap();
25667 cx.run_until_parked();
25668 assert_eq!(
25669 3,
25670 requests_made.load(atomic::Ordering::Acquire),
25671 "Should query for colors once per save and once per formatting after save"
25672 );
25673
25674 drop(editor);
25675 let close = workspace
25676 .update(cx, |workspace, window, cx| {
25677 workspace.active_pane().update(cx, |pane, cx| {
25678 pane.close_active_item(&CloseActiveItem::default(), window, cx)
25679 })
25680 })
25681 .unwrap();
25682 close.await.unwrap();
25683 let close = workspace
25684 .update(cx, |workspace, window, cx| {
25685 workspace.active_pane().update(cx, |pane, cx| {
25686 pane.close_active_item(&CloseActiveItem::default(), window, cx)
25687 })
25688 })
25689 .unwrap();
25690 close.await.unwrap();
25691 assert_eq!(
25692 3,
25693 requests_made.load(atomic::Ordering::Acquire),
25694 "After saving and closing all editors, no extra requests should be made"
25695 );
25696 workspace
25697 .update(cx, |workspace, _, cx| {
25698 assert!(
25699 workspace.active_item(cx).is_none(),
25700 "Should close all editors"
25701 )
25702 })
25703 .unwrap();
25704
25705 workspace
25706 .update(cx, |workspace, window, cx| {
25707 workspace.active_pane().update(cx, |pane, cx| {
25708 pane.navigate_backward(&workspace::GoBack, window, cx);
25709 })
25710 })
25711 .unwrap();
25712 cx.executor().advance_clock(Duration::from_millis(100));
25713 cx.run_until_parked();
25714 let editor = workspace
25715 .update(cx, |workspace, _, cx| {
25716 workspace
25717 .active_item(cx)
25718 .expect("Should have reopened the editor again after navigating back")
25719 .downcast::<Editor>()
25720 .expect("Should be an editor")
25721 })
25722 .unwrap();
25723 color_request_handle.next().await.unwrap();
25724 assert_eq!(
25725 3,
25726 requests_made.load(atomic::Ordering::Acquire),
25727 "Cache should be reused on buffer close and reopen"
25728 );
25729 editor.update(cx, |editor, cx| {
25730 assert_eq!(
25731 vec![expected_color],
25732 extract_color_inlays(editor, cx),
25733 "Should have an initial inlay"
25734 );
25735 });
25736
25737 drop(color_request_handle);
25738 let closure_requests_made = Arc::clone(&requests_made);
25739 let mut empty_color_request_handle = fake_language_server
25740 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
25741 let requests_made = Arc::clone(&closure_requests_made);
25742 async move {
25743 assert_eq!(
25744 params.text_document.uri,
25745 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25746 );
25747 requests_made.fetch_add(1, atomic::Ordering::Release);
25748 Ok(Vec::new())
25749 }
25750 });
25751 let save = editor.update_in(cx, |editor, window, cx| {
25752 editor.move_to_end(&MoveToEnd, window, cx);
25753 editor.handle_input("dirty_again", window, cx);
25754 editor.save(
25755 SaveOptions {
25756 format: false,
25757 autosave: true,
25758 },
25759 project.clone(),
25760 window,
25761 cx,
25762 )
25763 });
25764 save.await.unwrap();
25765
25766 empty_color_request_handle.next().await.unwrap();
25767 cx.run_until_parked();
25768 assert_eq!(
25769 4,
25770 requests_made.load(atomic::Ordering::Acquire),
25771 "Should query for colors once per save only, as formatting was not requested"
25772 );
25773 editor.update(cx, |editor, cx| {
25774 assert_eq!(
25775 Vec::<Rgba>::new(),
25776 extract_color_inlays(editor, cx),
25777 "Should clear all colors when the server returns an empty response"
25778 );
25779 });
25780}
25781
25782#[gpui::test]
25783async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
25784 init_test(cx, |_| {});
25785 let (editor, cx) = cx.add_window_view(Editor::single_line);
25786 editor.update_in(cx, |editor, window, cx| {
25787 editor.set_text("oops\n\nwow\n", window, cx)
25788 });
25789 cx.run_until_parked();
25790 editor.update(cx, |editor, cx| {
25791 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
25792 });
25793 editor.update(cx, |editor, cx| editor.edit([(3..5, "")], cx));
25794 cx.run_until_parked();
25795 editor.update(cx, |editor, cx| {
25796 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
25797 });
25798}
25799
25800#[gpui::test]
25801async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
25802 init_test(cx, |_| {});
25803
25804 cx.update(|cx| {
25805 register_project_item::<Editor>(cx);
25806 });
25807
25808 let fs = FakeFs::new(cx.executor());
25809 fs.insert_tree("/root1", json!({})).await;
25810 fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
25811 .await;
25812
25813 let project = Project::test(fs, ["/root1".as_ref()], cx).await;
25814 let (workspace, cx) =
25815 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
25816
25817 let worktree_id = project.update(cx, |project, cx| {
25818 project.worktrees(cx).next().unwrap().read(cx).id()
25819 });
25820
25821 let handle = workspace
25822 .update_in(cx, |workspace, window, cx| {
25823 let project_path = (worktree_id, rel_path("one.pdf"));
25824 workspace.open_path(project_path, None, true, window, cx)
25825 })
25826 .await
25827 .unwrap();
25828
25829 assert_eq!(
25830 handle.to_any().entity_type(),
25831 TypeId::of::<InvalidBufferView>()
25832 );
25833}
25834
25835#[gpui::test]
25836async fn test_select_next_prev_syntax_node(cx: &mut TestAppContext) {
25837 init_test(cx, |_| {});
25838
25839 let language = Arc::new(Language::new(
25840 LanguageConfig::default(),
25841 Some(tree_sitter_rust::LANGUAGE.into()),
25842 ));
25843
25844 // Test hierarchical sibling navigation
25845 let text = r#"
25846 fn outer() {
25847 if condition {
25848 let a = 1;
25849 }
25850 let b = 2;
25851 }
25852
25853 fn another() {
25854 let c = 3;
25855 }
25856 "#;
25857
25858 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
25859 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
25860 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
25861
25862 // Wait for parsing to complete
25863 editor
25864 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
25865 .await;
25866
25867 editor.update_in(cx, |editor, window, cx| {
25868 // Start by selecting "let a = 1;" inside the if block
25869 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25870 s.select_display_ranges([
25871 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 26)
25872 ]);
25873 });
25874
25875 let initial_selection = editor.selections.display_ranges(cx);
25876 assert_eq!(initial_selection.len(), 1, "Should have one selection");
25877
25878 // Test select next sibling - should move up levels to find the next sibling
25879 // Since "let a = 1;" has no siblings in the if block, it should move up
25880 // to find "let b = 2;" which is a sibling of the if block
25881 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
25882 let next_selection = editor.selections.display_ranges(cx);
25883
25884 // Should have a selection and it should be different from the initial
25885 assert_eq!(
25886 next_selection.len(),
25887 1,
25888 "Should have one selection after next"
25889 );
25890 assert_ne!(
25891 next_selection[0], initial_selection[0],
25892 "Next sibling selection should be different"
25893 );
25894
25895 // Test hierarchical navigation by going to the end of the current function
25896 // and trying to navigate to the next function
25897 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25898 s.select_display_ranges([
25899 DisplayPoint::new(DisplayRow(5), 12)..DisplayPoint::new(DisplayRow(5), 22)
25900 ]);
25901 });
25902
25903 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
25904 let function_next_selection = editor.selections.display_ranges(cx);
25905
25906 // Should move to the next function
25907 assert_eq!(
25908 function_next_selection.len(),
25909 1,
25910 "Should have one selection after function next"
25911 );
25912
25913 // Test select previous sibling navigation
25914 editor.select_prev_syntax_node(&SelectPreviousSyntaxNode, window, cx);
25915 let prev_selection = editor.selections.display_ranges(cx);
25916
25917 // Should have a selection and it should be different
25918 assert_eq!(
25919 prev_selection.len(),
25920 1,
25921 "Should have one selection after prev"
25922 );
25923 assert_ne!(
25924 prev_selection[0], function_next_selection[0],
25925 "Previous sibling selection should be different from next"
25926 );
25927 });
25928}
25929
25930#[gpui::test]
25931async fn test_next_prev_document_highlight(cx: &mut TestAppContext) {
25932 init_test(cx, |_| {});
25933
25934 let mut cx = EditorTestContext::new(cx).await;
25935 cx.set_state(
25936 "let ˇvariable = 42;
25937let another = variable + 1;
25938let result = variable * 2;",
25939 );
25940
25941 // Set up document highlights manually (simulating LSP response)
25942 cx.update_editor(|editor, _window, cx| {
25943 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
25944
25945 // Create highlights for "variable" occurrences
25946 let highlight_ranges = [
25947 Point::new(0, 4)..Point::new(0, 12), // First "variable"
25948 Point::new(1, 14)..Point::new(1, 22), // Second "variable"
25949 Point::new(2, 13)..Point::new(2, 21), // Third "variable"
25950 ];
25951
25952 let anchor_ranges: Vec<_> = highlight_ranges
25953 .iter()
25954 .map(|range| range.clone().to_anchors(&buffer_snapshot))
25955 .collect();
25956
25957 editor.highlight_background::<DocumentHighlightRead>(
25958 &anchor_ranges,
25959 |theme| theme.colors().editor_document_highlight_read_background,
25960 cx,
25961 );
25962 });
25963
25964 // Go to next highlight - should move to second "variable"
25965 cx.update_editor(|editor, window, cx| {
25966 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
25967 });
25968 cx.assert_editor_state(
25969 "let variable = 42;
25970let another = ˇvariable + 1;
25971let result = variable * 2;",
25972 );
25973
25974 // Go to next highlight - should move to third "variable"
25975 cx.update_editor(|editor, window, cx| {
25976 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
25977 });
25978 cx.assert_editor_state(
25979 "let variable = 42;
25980let another = variable + 1;
25981let result = ˇvariable * 2;",
25982 );
25983
25984 // Go to next highlight - should stay at third "variable" (no wrap-around)
25985 cx.update_editor(|editor, window, cx| {
25986 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
25987 });
25988 cx.assert_editor_state(
25989 "let variable = 42;
25990let another = variable + 1;
25991let result = ˇvariable * 2;",
25992 );
25993
25994 // Now test going backwards from third position
25995 cx.update_editor(|editor, window, cx| {
25996 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
25997 });
25998 cx.assert_editor_state(
25999 "let variable = 42;
26000let another = ˇvariable + 1;
26001let result = variable * 2;",
26002 );
26003
26004 // Go to previous highlight - should move to first "variable"
26005 cx.update_editor(|editor, window, cx| {
26006 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26007 });
26008 cx.assert_editor_state(
26009 "let ˇvariable = 42;
26010let another = variable + 1;
26011let result = variable * 2;",
26012 );
26013
26014 // Go to previous highlight - should stay on first "variable"
26015 cx.update_editor(|editor, window, cx| {
26016 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26017 });
26018 cx.assert_editor_state(
26019 "let ˇvariable = 42;
26020let another = variable + 1;
26021let result = variable * 2;",
26022 );
26023}
26024
26025#[gpui::test]
26026async fn test_paste_url_from_other_app_creates_markdown_link_over_selected_text(
26027 cx: &mut gpui::TestAppContext,
26028) {
26029 init_test(cx, |_| {});
26030
26031 let url = "https://zed.dev";
26032
26033 let markdown_language = Arc::new(Language::new(
26034 LanguageConfig {
26035 name: "Markdown".into(),
26036 ..LanguageConfig::default()
26037 },
26038 None,
26039 ));
26040
26041 let mut cx = EditorTestContext::new(cx).await;
26042 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26043 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)");
26044
26045 cx.update_editor(|editor, window, cx| {
26046 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26047 editor.paste(&Paste, window, cx);
26048 });
26049
26050 cx.assert_editor_state(&format!(
26051 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)"
26052 ));
26053}
26054
26055#[gpui::test]
26056async fn test_paste_url_from_zed_copy_creates_markdown_link_over_selected_text(
26057 cx: &mut gpui::TestAppContext,
26058) {
26059 init_test(cx, |_| {});
26060
26061 let url = "https://zed.dev";
26062
26063 let markdown_language = Arc::new(Language::new(
26064 LanguageConfig {
26065 name: "Markdown".into(),
26066 ..LanguageConfig::default()
26067 },
26068 None,
26069 ));
26070
26071 let mut cx = EditorTestContext::new(cx).await;
26072 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26073 cx.set_state(&format!(
26074 "Hello, editor.\nZed is great (see this link: )\n«{url}ˇ»"
26075 ));
26076
26077 cx.update_editor(|editor, window, cx| {
26078 editor.copy(&Copy, window, cx);
26079 });
26080
26081 cx.set_state(&format!(
26082 "Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)\n{url}"
26083 ));
26084
26085 cx.update_editor(|editor, window, cx| {
26086 editor.paste(&Paste, window, cx);
26087 });
26088
26089 cx.assert_editor_state(&format!(
26090 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)\n{url}"
26091 ));
26092}
26093
26094#[gpui::test]
26095async fn test_paste_url_from_other_app_replaces_existing_url_without_creating_markdown_link(
26096 cx: &mut gpui::TestAppContext,
26097) {
26098 init_test(cx, |_| {});
26099
26100 let url = "https://zed.dev";
26101
26102 let markdown_language = Arc::new(Language::new(
26103 LanguageConfig {
26104 name: "Markdown".into(),
26105 ..LanguageConfig::default()
26106 },
26107 None,
26108 ));
26109
26110 let mut cx = EditorTestContext::new(cx).await;
26111 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26112 cx.set_state("Please visit zed's homepage: «https://www.apple.comˇ»");
26113
26114 cx.update_editor(|editor, window, cx| {
26115 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26116 editor.paste(&Paste, window, cx);
26117 });
26118
26119 cx.assert_editor_state(&format!("Please visit zed's homepage: {url}ˇ"));
26120}
26121
26122#[gpui::test]
26123async fn test_paste_plain_text_from_other_app_replaces_selection_without_creating_markdown_link(
26124 cx: &mut gpui::TestAppContext,
26125) {
26126 init_test(cx, |_| {});
26127
26128 let text = "Awesome";
26129
26130 let markdown_language = Arc::new(Language::new(
26131 LanguageConfig {
26132 name: "Markdown".into(),
26133 ..LanguageConfig::default()
26134 },
26135 None,
26136 ));
26137
26138 let mut cx = EditorTestContext::new(cx).await;
26139 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26140 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat»");
26141
26142 cx.update_editor(|editor, window, cx| {
26143 cx.write_to_clipboard(ClipboardItem::new_string(text.to_string()));
26144 editor.paste(&Paste, window, cx);
26145 });
26146
26147 cx.assert_editor_state(&format!("Hello, {text}ˇ.\nZed is {text}ˇ"));
26148}
26149
26150#[gpui::test]
26151async fn test_paste_url_from_other_app_without_creating_markdown_link_in_non_markdown_language(
26152 cx: &mut gpui::TestAppContext,
26153) {
26154 init_test(cx, |_| {});
26155
26156 let url = "https://zed.dev";
26157
26158 let markdown_language = Arc::new(Language::new(
26159 LanguageConfig {
26160 name: "Rust".into(),
26161 ..LanguageConfig::default()
26162 },
26163 None,
26164 ));
26165
26166 let mut cx = EditorTestContext::new(cx).await;
26167 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26168 cx.set_state("// Hello, «editorˇ».\n// Zed is «ˇgreat» (see this link: ˇ)");
26169
26170 cx.update_editor(|editor, window, cx| {
26171 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26172 editor.paste(&Paste, window, cx);
26173 });
26174
26175 cx.assert_editor_state(&format!(
26176 "// Hello, {url}ˇ.\n// Zed is {url}ˇ (see this link: {url}ˇ)"
26177 ));
26178}
26179
26180#[gpui::test]
26181async fn test_paste_url_from_other_app_creates_markdown_link_selectively_in_multi_buffer(
26182 cx: &mut TestAppContext,
26183) {
26184 init_test(cx, |_| {});
26185
26186 let url = "https://zed.dev";
26187
26188 let markdown_language = Arc::new(Language::new(
26189 LanguageConfig {
26190 name: "Markdown".into(),
26191 ..LanguageConfig::default()
26192 },
26193 None,
26194 ));
26195
26196 let (editor, cx) = cx.add_window_view(|window, cx| {
26197 let multi_buffer = MultiBuffer::build_multi(
26198 [
26199 ("this will embed -> link", vec![Point::row_range(0..1)]),
26200 ("this will replace -> link", vec![Point::row_range(0..1)]),
26201 ],
26202 cx,
26203 );
26204 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
26205 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26206 s.select_ranges(vec![
26207 Point::new(0, 19)..Point::new(0, 23),
26208 Point::new(1, 21)..Point::new(1, 25),
26209 ])
26210 });
26211 let first_buffer_id = multi_buffer
26212 .read(cx)
26213 .excerpt_buffer_ids()
26214 .into_iter()
26215 .next()
26216 .unwrap();
26217 let first_buffer = multi_buffer.read(cx).buffer(first_buffer_id).unwrap();
26218 first_buffer.update(cx, |buffer, cx| {
26219 buffer.set_language(Some(markdown_language.clone()), cx);
26220 });
26221
26222 editor
26223 });
26224 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
26225
26226 cx.update_editor(|editor, window, cx| {
26227 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26228 editor.paste(&Paste, window, cx);
26229 });
26230
26231 cx.assert_editor_state(&format!(
26232 "this will embed -> [link]({url})ˇ\nthis will replace -> {url}ˇ"
26233 ));
26234}
26235
26236#[track_caller]
26237fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
26238 editor
26239 .all_inlays(cx)
26240 .into_iter()
26241 .filter_map(|inlay| inlay.get_color())
26242 .map(Rgba::from)
26243 .collect()
26244}