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, channel::oneshot};
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::<f64>::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::<f64>::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_f64())
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 let (_, positions) = marked_text_ranges(
1260 &"
1261 class Foo:
1262 # Hello!
1263
1264 def a():
1265 print(1)
1266
1267 def b():
1268 p«riˇ»nt(2)
1269
1270
1271 class Bar:
1272 # World!
1273
1274 def a():
1275 «ˇprint(1)
1276
1277 def b():
1278 print(2)»
1279
1280
1281 "
1282 .unindent(),
1283 true,
1284 );
1285
1286 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
1287 s.select_ranges(positions)
1288 });
1289
1290 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1291 assert_eq!(
1292 editor.display_text(cx),
1293 "
1294 class Foo:
1295 # Hello!
1296
1297 def a():⋯
1298
1299 def b():
1300 print(2)
1301
1302
1303 class Bar:
1304 # World!
1305
1306 def a():
1307 print(1)
1308
1309 def b():
1310 print(2)
1311
1312
1313 "
1314 .unindent(),
1315 );
1316 });
1317}
1318
1319#[gpui::test]
1320fn test_move_cursor(cx: &mut TestAppContext) {
1321 init_test(cx, |_| {});
1322
1323 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1324 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1325
1326 buffer.update(cx, |buffer, cx| {
1327 buffer.edit(
1328 vec![
1329 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1330 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1331 ],
1332 None,
1333 cx,
1334 );
1335 });
1336 _ = editor.update(cx, |editor, window, cx| {
1337 assert_eq!(
1338 editor.selections.display_ranges(cx),
1339 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1340 );
1341
1342 editor.move_down(&MoveDown, window, cx);
1343 assert_eq!(
1344 editor.selections.display_ranges(cx),
1345 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1346 );
1347
1348 editor.move_right(&MoveRight, window, cx);
1349 assert_eq!(
1350 editor.selections.display_ranges(cx),
1351 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1352 );
1353
1354 editor.move_left(&MoveLeft, window, cx);
1355 assert_eq!(
1356 editor.selections.display_ranges(cx),
1357 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1358 );
1359
1360 editor.move_up(&MoveUp, window, cx);
1361 assert_eq!(
1362 editor.selections.display_ranges(cx),
1363 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1364 );
1365
1366 editor.move_to_end(&MoveToEnd, window, cx);
1367 assert_eq!(
1368 editor.selections.display_ranges(cx),
1369 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1370 );
1371
1372 editor.move_to_beginning(&MoveToBeginning, window, cx);
1373 assert_eq!(
1374 editor.selections.display_ranges(cx),
1375 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1376 );
1377
1378 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1379 s.select_display_ranges([
1380 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1381 ]);
1382 });
1383 editor.select_to_beginning(&SelectToBeginning, window, cx);
1384 assert_eq!(
1385 editor.selections.display_ranges(cx),
1386 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1387 );
1388
1389 editor.select_to_end(&SelectToEnd, window, cx);
1390 assert_eq!(
1391 editor.selections.display_ranges(cx),
1392 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1393 );
1394 });
1395}
1396
1397#[gpui::test]
1398fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1399 init_test(cx, |_| {});
1400
1401 let editor = cx.add_window(|window, cx| {
1402 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1403 build_editor(buffer, window, cx)
1404 });
1405
1406 assert_eq!('🟥'.len_utf8(), 4);
1407 assert_eq!('α'.len_utf8(), 2);
1408
1409 _ = editor.update(cx, |editor, window, cx| {
1410 editor.fold_creases(
1411 vec![
1412 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1413 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1414 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1415 ],
1416 true,
1417 window,
1418 cx,
1419 );
1420 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1421
1422 editor.move_right(&MoveRight, window, cx);
1423 assert_eq!(
1424 editor.selections.display_ranges(cx),
1425 &[empty_range(0, "🟥".len())]
1426 );
1427 editor.move_right(&MoveRight, window, cx);
1428 assert_eq!(
1429 editor.selections.display_ranges(cx),
1430 &[empty_range(0, "🟥🟧".len())]
1431 );
1432 editor.move_right(&MoveRight, window, cx);
1433 assert_eq!(
1434 editor.selections.display_ranges(cx),
1435 &[empty_range(0, "🟥🟧⋯".len())]
1436 );
1437
1438 editor.move_down(&MoveDown, window, cx);
1439 assert_eq!(
1440 editor.selections.display_ranges(cx),
1441 &[empty_range(1, "ab⋯e".len())]
1442 );
1443 editor.move_left(&MoveLeft, window, cx);
1444 assert_eq!(
1445 editor.selections.display_ranges(cx),
1446 &[empty_range(1, "ab⋯".len())]
1447 );
1448 editor.move_left(&MoveLeft, window, cx);
1449 assert_eq!(
1450 editor.selections.display_ranges(cx),
1451 &[empty_range(1, "ab".len())]
1452 );
1453 editor.move_left(&MoveLeft, window, cx);
1454 assert_eq!(
1455 editor.selections.display_ranges(cx),
1456 &[empty_range(1, "a".len())]
1457 );
1458
1459 editor.move_down(&MoveDown, window, cx);
1460 assert_eq!(
1461 editor.selections.display_ranges(cx),
1462 &[empty_range(2, "α".len())]
1463 );
1464 editor.move_right(&MoveRight, window, cx);
1465 assert_eq!(
1466 editor.selections.display_ranges(cx),
1467 &[empty_range(2, "αβ".len())]
1468 );
1469 editor.move_right(&MoveRight, window, cx);
1470 assert_eq!(
1471 editor.selections.display_ranges(cx),
1472 &[empty_range(2, "αβ⋯".len())]
1473 );
1474 editor.move_right(&MoveRight, window, cx);
1475 assert_eq!(
1476 editor.selections.display_ranges(cx),
1477 &[empty_range(2, "αβ⋯ε".len())]
1478 );
1479
1480 editor.move_up(&MoveUp, window, cx);
1481 assert_eq!(
1482 editor.selections.display_ranges(cx),
1483 &[empty_range(1, "ab⋯e".len())]
1484 );
1485 editor.move_down(&MoveDown, window, cx);
1486 assert_eq!(
1487 editor.selections.display_ranges(cx),
1488 &[empty_range(2, "αβ⋯ε".len())]
1489 );
1490 editor.move_up(&MoveUp, window, cx);
1491 assert_eq!(
1492 editor.selections.display_ranges(cx),
1493 &[empty_range(1, "ab⋯e".len())]
1494 );
1495
1496 editor.move_up(&MoveUp, window, cx);
1497 assert_eq!(
1498 editor.selections.display_ranges(cx),
1499 &[empty_range(0, "🟥🟧".len())]
1500 );
1501 editor.move_left(&MoveLeft, window, cx);
1502 assert_eq!(
1503 editor.selections.display_ranges(cx),
1504 &[empty_range(0, "🟥".len())]
1505 );
1506 editor.move_left(&MoveLeft, window, cx);
1507 assert_eq!(
1508 editor.selections.display_ranges(cx),
1509 &[empty_range(0, "".len())]
1510 );
1511 });
1512}
1513
1514#[gpui::test]
1515fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1516 init_test(cx, |_| {});
1517
1518 let editor = cx.add_window(|window, cx| {
1519 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1520 build_editor(buffer, window, cx)
1521 });
1522 _ = editor.update(cx, |editor, window, cx| {
1523 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1524 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1525 });
1526
1527 // moving above start of document should move selection to start of document,
1528 // but the next move down should still be at the original goal_x
1529 editor.move_up(&MoveUp, window, cx);
1530 assert_eq!(
1531 editor.selections.display_ranges(cx),
1532 &[empty_range(0, "".len())]
1533 );
1534
1535 editor.move_down(&MoveDown, window, cx);
1536 assert_eq!(
1537 editor.selections.display_ranges(cx),
1538 &[empty_range(1, "abcd".len())]
1539 );
1540
1541 editor.move_down(&MoveDown, window, cx);
1542 assert_eq!(
1543 editor.selections.display_ranges(cx),
1544 &[empty_range(2, "αβγ".len())]
1545 );
1546
1547 editor.move_down(&MoveDown, window, cx);
1548 assert_eq!(
1549 editor.selections.display_ranges(cx),
1550 &[empty_range(3, "abcd".len())]
1551 );
1552
1553 editor.move_down(&MoveDown, window, cx);
1554 assert_eq!(
1555 editor.selections.display_ranges(cx),
1556 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1557 );
1558
1559 // moving past end of document should not change goal_x
1560 editor.move_down(&MoveDown, window, cx);
1561 assert_eq!(
1562 editor.selections.display_ranges(cx),
1563 &[empty_range(5, "".len())]
1564 );
1565
1566 editor.move_down(&MoveDown, window, cx);
1567 assert_eq!(
1568 editor.selections.display_ranges(cx),
1569 &[empty_range(5, "".len())]
1570 );
1571
1572 editor.move_up(&MoveUp, window, cx);
1573 assert_eq!(
1574 editor.selections.display_ranges(cx),
1575 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1576 );
1577
1578 editor.move_up(&MoveUp, window, cx);
1579 assert_eq!(
1580 editor.selections.display_ranges(cx),
1581 &[empty_range(3, "abcd".len())]
1582 );
1583
1584 editor.move_up(&MoveUp, window, cx);
1585 assert_eq!(
1586 editor.selections.display_ranges(cx),
1587 &[empty_range(2, "αβγ".len())]
1588 );
1589 });
1590}
1591
1592#[gpui::test]
1593fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1594 init_test(cx, |_| {});
1595 let move_to_beg = MoveToBeginningOfLine {
1596 stop_at_soft_wraps: true,
1597 stop_at_indent: true,
1598 };
1599
1600 let delete_to_beg = DeleteToBeginningOfLine {
1601 stop_at_indent: false,
1602 };
1603
1604 let move_to_end = MoveToEndOfLine {
1605 stop_at_soft_wraps: true,
1606 };
1607
1608 let editor = cx.add_window(|window, cx| {
1609 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1610 build_editor(buffer, window, cx)
1611 });
1612 _ = editor.update(cx, |editor, window, cx| {
1613 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1614 s.select_display_ranges([
1615 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1616 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1617 ]);
1618 });
1619 });
1620
1621 _ = editor.update(cx, |editor, window, cx| {
1622 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1623 assert_eq!(
1624 editor.selections.display_ranges(cx),
1625 &[
1626 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1627 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1628 ]
1629 );
1630 });
1631
1632 _ = editor.update(cx, |editor, window, cx| {
1633 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1634 assert_eq!(
1635 editor.selections.display_ranges(cx),
1636 &[
1637 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1638 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1639 ]
1640 );
1641 });
1642
1643 _ = editor.update(cx, |editor, window, cx| {
1644 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1645 assert_eq!(
1646 editor.selections.display_ranges(cx),
1647 &[
1648 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1649 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1650 ]
1651 );
1652 });
1653
1654 _ = editor.update(cx, |editor, window, cx| {
1655 editor.move_to_end_of_line(&move_to_end, window, cx);
1656 assert_eq!(
1657 editor.selections.display_ranges(cx),
1658 &[
1659 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1660 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1661 ]
1662 );
1663 });
1664
1665 // Moving to the end of line again is a no-op.
1666 _ = editor.update(cx, |editor, window, cx| {
1667 editor.move_to_end_of_line(&move_to_end, window, cx);
1668 assert_eq!(
1669 editor.selections.display_ranges(cx),
1670 &[
1671 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1672 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1673 ]
1674 );
1675 });
1676
1677 _ = editor.update(cx, |editor, window, cx| {
1678 editor.move_left(&MoveLeft, window, cx);
1679 editor.select_to_beginning_of_line(
1680 &SelectToBeginningOfLine {
1681 stop_at_soft_wraps: true,
1682 stop_at_indent: true,
1683 },
1684 window,
1685 cx,
1686 );
1687 assert_eq!(
1688 editor.selections.display_ranges(cx),
1689 &[
1690 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1691 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1692 ]
1693 );
1694 });
1695
1696 _ = editor.update(cx, |editor, window, cx| {
1697 editor.select_to_beginning_of_line(
1698 &SelectToBeginningOfLine {
1699 stop_at_soft_wraps: true,
1700 stop_at_indent: true,
1701 },
1702 window,
1703 cx,
1704 );
1705 assert_eq!(
1706 editor.selections.display_ranges(cx),
1707 &[
1708 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1709 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1710 ]
1711 );
1712 });
1713
1714 _ = editor.update(cx, |editor, window, cx| {
1715 editor.select_to_beginning_of_line(
1716 &SelectToBeginningOfLine {
1717 stop_at_soft_wraps: true,
1718 stop_at_indent: true,
1719 },
1720 window,
1721 cx,
1722 );
1723 assert_eq!(
1724 editor.selections.display_ranges(cx),
1725 &[
1726 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1727 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1728 ]
1729 );
1730 });
1731
1732 _ = editor.update(cx, |editor, window, cx| {
1733 editor.select_to_end_of_line(
1734 &SelectToEndOfLine {
1735 stop_at_soft_wraps: true,
1736 },
1737 window,
1738 cx,
1739 );
1740 assert_eq!(
1741 editor.selections.display_ranges(cx),
1742 &[
1743 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1744 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1745 ]
1746 );
1747 });
1748
1749 _ = editor.update(cx, |editor, window, cx| {
1750 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1751 assert_eq!(editor.display_text(cx), "ab\n de");
1752 assert_eq!(
1753 editor.selections.display_ranges(cx),
1754 &[
1755 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1756 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1757 ]
1758 );
1759 });
1760
1761 _ = editor.update(cx, |editor, window, cx| {
1762 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1763 assert_eq!(editor.display_text(cx), "\n");
1764 assert_eq!(
1765 editor.selections.display_ranges(cx),
1766 &[
1767 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1768 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1769 ]
1770 );
1771 });
1772}
1773
1774#[gpui::test]
1775fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1776 init_test(cx, |_| {});
1777 let move_to_beg = MoveToBeginningOfLine {
1778 stop_at_soft_wraps: false,
1779 stop_at_indent: false,
1780 };
1781
1782 let move_to_end = MoveToEndOfLine {
1783 stop_at_soft_wraps: false,
1784 };
1785
1786 let editor = cx.add_window(|window, cx| {
1787 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1788 build_editor(buffer, window, cx)
1789 });
1790
1791 _ = editor.update(cx, |editor, window, cx| {
1792 editor.set_wrap_width(Some(140.0.into()), cx);
1793
1794 // We expect the following lines after wrapping
1795 // ```
1796 // thequickbrownfox
1797 // jumpedoverthelazydo
1798 // gs
1799 // ```
1800 // The final `gs` was soft-wrapped onto a new line.
1801 assert_eq!(
1802 "thequickbrownfox\njumpedoverthelaz\nydogs",
1803 editor.display_text(cx),
1804 );
1805
1806 // First, let's assert behavior on the first line, that was not soft-wrapped.
1807 // Start the cursor at the `k` on the first line
1808 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1809 s.select_display_ranges([
1810 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1811 ]);
1812 });
1813
1814 // Moving to the beginning of the line should put us at the beginning of the line.
1815 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1816 assert_eq!(
1817 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1818 editor.selections.display_ranges(cx)
1819 );
1820
1821 // Moving to the end of the line should put us at the end of the line.
1822 editor.move_to_end_of_line(&move_to_end, window, cx);
1823 assert_eq!(
1824 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1825 editor.selections.display_ranges(cx)
1826 );
1827
1828 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1829 // Start the cursor at the last line (`y` that was wrapped to a new line)
1830 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1831 s.select_display_ranges([
1832 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1833 ]);
1834 });
1835
1836 // Moving to the beginning of the line should put us at the start of the second line of
1837 // display text, i.e., the `j`.
1838 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1839 assert_eq!(
1840 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1841 editor.selections.display_ranges(cx)
1842 );
1843
1844 // Moving to the beginning of the line again should be a no-op.
1845 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1846 assert_eq!(
1847 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1848 editor.selections.display_ranges(cx)
1849 );
1850
1851 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1852 // next display line.
1853 editor.move_to_end_of_line(&move_to_end, window, cx);
1854 assert_eq!(
1855 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1856 editor.selections.display_ranges(cx)
1857 );
1858
1859 // Moving to the end of the line again should be a no-op.
1860 editor.move_to_end_of_line(&move_to_end, window, cx);
1861 assert_eq!(
1862 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1863 editor.selections.display_ranges(cx)
1864 );
1865 });
1866}
1867
1868#[gpui::test]
1869fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1870 init_test(cx, |_| {});
1871
1872 let move_to_beg = MoveToBeginningOfLine {
1873 stop_at_soft_wraps: true,
1874 stop_at_indent: true,
1875 };
1876
1877 let select_to_beg = SelectToBeginningOfLine {
1878 stop_at_soft_wraps: true,
1879 stop_at_indent: true,
1880 };
1881
1882 let delete_to_beg = DeleteToBeginningOfLine {
1883 stop_at_indent: true,
1884 };
1885
1886 let move_to_end = MoveToEndOfLine {
1887 stop_at_soft_wraps: false,
1888 };
1889
1890 let editor = cx.add_window(|window, cx| {
1891 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1892 build_editor(buffer, window, cx)
1893 });
1894
1895 _ = editor.update(cx, |editor, window, cx| {
1896 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1897 s.select_display_ranges([
1898 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1899 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1900 ]);
1901 });
1902
1903 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1904 // and the second cursor at the first non-whitespace character in the line.
1905 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1906 assert_eq!(
1907 editor.selections.display_ranges(cx),
1908 &[
1909 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1910 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1911 ]
1912 );
1913
1914 // Moving to the beginning of the line again should be a no-op for the first cursor,
1915 // and should move the second cursor to the beginning of the line.
1916 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1917 assert_eq!(
1918 editor.selections.display_ranges(cx),
1919 &[
1920 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1921 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1922 ]
1923 );
1924
1925 // Moving to the beginning of the line again should still be a no-op for the first cursor,
1926 // and should move the second cursor back to the first non-whitespace character in the line.
1927 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1928 assert_eq!(
1929 editor.selections.display_ranges(cx),
1930 &[
1931 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1932 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1933 ]
1934 );
1935
1936 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
1937 // and to the first non-whitespace character in the line for the second cursor.
1938 editor.move_to_end_of_line(&move_to_end, window, cx);
1939 editor.move_left(&MoveLeft, window, cx);
1940 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1941 assert_eq!(
1942 editor.selections.display_ranges(cx),
1943 &[
1944 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1945 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1946 ]
1947 );
1948
1949 // Selecting to the beginning of the line again should be a no-op for the first cursor,
1950 // and should select to the beginning of the line for the second cursor.
1951 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1952 assert_eq!(
1953 editor.selections.display_ranges(cx),
1954 &[
1955 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1956 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1957 ]
1958 );
1959
1960 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
1961 // and should delete to the first non-whitespace character in the line for the second cursor.
1962 editor.move_to_end_of_line(&move_to_end, window, cx);
1963 editor.move_left(&MoveLeft, window, cx);
1964 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1965 assert_eq!(editor.text(cx), "c\n f");
1966 });
1967}
1968
1969#[gpui::test]
1970fn test_beginning_of_line_with_cursor_between_line_start_and_indent(cx: &mut TestAppContext) {
1971 init_test(cx, |_| {});
1972
1973 let move_to_beg = MoveToBeginningOfLine {
1974 stop_at_soft_wraps: true,
1975 stop_at_indent: true,
1976 };
1977
1978 let editor = cx.add_window(|window, cx| {
1979 let buffer = MultiBuffer::build_simple(" hello\nworld", cx);
1980 build_editor(buffer, window, cx)
1981 });
1982
1983 _ = editor.update(cx, |editor, window, cx| {
1984 // test cursor between line_start and indent_start
1985 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1986 s.select_display_ranges([
1987 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3)
1988 ]);
1989 });
1990
1991 // cursor should move to line_start
1992 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1993 assert_eq!(
1994 editor.selections.display_ranges(cx),
1995 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1996 );
1997
1998 // cursor should move to indent_start
1999 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2000 assert_eq!(
2001 editor.selections.display_ranges(cx),
2002 &[DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 4)]
2003 );
2004
2005 // cursor should move to back to line_start
2006 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2007 assert_eq!(
2008 editor.selections.display_ranges(cx),
2009 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
2010 );
2011 });
2012}
2013
2014#[gpui::test]
2015fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
2016 init_test(cx, |_| {});
2017
2018 let editor = cx.add_window(|window, cx| {
2019 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
2020 build_editor(buffer, window, cx)
2021 });
2022 _ = editor.update(cx, |editor, window, cx| {
2023 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2024 s.select_display_ranges([
2025 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
2026 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
2027 ])
2028 });
2029 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2030 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
2031
2032 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2033 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
2034
2035 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2036 assert_selection_ranges("use ˇstd::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
2037
2038 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2039 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
2040
2041 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2042 assert_selection_ranges("ˇuse std::str::{foo, ˇbar}\n\n {baz.qux()}", editor, cx);
2043
2044 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2045 assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
2046
2047 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2048 assert_selection_ranges("use stdˇ::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
2049
2050 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2051 assert_selection_ranges("use std::ˇstr::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
2052
2053 editor.move_right(&MoveRight, window, cx);
2054 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
2055 assert_selection_ranges(
2056 "use std::«ˇs»tr::{foo, bar}\n«ˇ\n» {baz.qux()}",
2057 editor,
2058 cx,
2059 );
2060
2061 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
2062 assert_selection_ranges(
2063 "use std«ˇ::s»tr::{foo, bar«ˇ}\n\n» {baz.qux()}",
2064 editor,
2065 cx,
2066 );
2067
2068 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
2069 assert_selection_ranges(
2070 "use std::«ˇs»tr::{foo, bar}«ˇ\n\n» {baz.qux()}",
2071 editor,
2072 cx,
2073 );
2074 });
2075}
2076
2077#[gpui::test]
2078fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
2079 init_test(cx, |_| {});
2080
2081 let editor = cx.add_window(|window, cx| {
2082 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
2083 build_editor(buffer, window, cx)
2084 });
2085
2086 _ = editor.update(cx, |editor, window, cx| {
2087 editor.set_wrap_width(Some(140.0.into()), cx);
2088 assert_eq!(
2089 editor.display_text(cx),
2090 "use one::{\n two::three::\n four::five\n};"
2091 );
2092
2093 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2094 s.select_display_ranges([
2095 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
2096 ]);
2097 });
2098
2099 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2100 assert_eq!(
2101 editor.selections.display_ranges(cx),
2102 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
2103 );
2104
2105 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2106 assert_eq!(
2107 editor.selections.display_ranges(cx),
2108 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2109 );
2110
2111 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2112 assert_eq!(
2113 editor.selections.display_ranges(cx),
2114 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2115 );
2116
2117 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2118 assert_eq!(
2119 editor.selections.display_ranges(cx),
2120 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2121 );
2122
2123 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2124 assert_eq!(
2125 editor.selections.display_ranges(cx),
2126 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2127 );
2128
2129 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2130 assert_eq!(
2131 editor.selections.display_ranges(cx),
2132 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2133 );
2134 });
2135}
2136
2137#[gpui::test]
2138async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2139 init_test(cx, |_| {});
2140 let mut cx = EditorTestContext::new(cx).await;
2141
2142 let line_height = cx.editor(|editor, window, _| {
2143 editor
2144 .style()
2145 .unwrap()
2146 .text
2147 .line_height_in_pixels(window.rem_size())
2148 });
2149 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2150
2151 cx.set_state(
2152 &r#"ˇone
2153 two
2154
2155 three
2156 fourˇ
2157 five
2158
2159 six"#
2160 .unindent(),
2161 );
2162
2163 cx.update_editor(|editor, window, cx| {
2164 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2165 });
2166 cx.assert_editor_state(
2167 &r#"one
2168 two
2169 ˇ
2170 three
2171 four
2172 five
2173 ˇ
2174 six"#
2175 .unindent(),
2176 );
2177
2178 cx.update_editor(|editor, window, cx| {
2179 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2180 });
2181 cx.assert_editor_state(
2182 &r#"one
2183 two
2184
2185 three
2186 four
2187 five
2188 ˇ
2189 sixˇ"#
2190 .unindent(),
2191 );
2192
2193 cx.update_editor(|editor, window, cx| {
2194 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2195 });
2196 cx.assert_editor_state(
2197 &r#"one
2198 two
2199
2200 three
2201 four
2202 five
2203
2204 sixˇ"#
2205 .unindent(),
2206 );
2207
2208 cx.update_editor(|editor, window, cx| {
2209 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2210 });
2211 cx.assert_editor_state(
2212 &r#"one
2213 two
2214
2215 three
2216 four
2217 five
2218 ˇ
2219 six"#
2220 .unindent(),
2221 );
2222
2223 cx.update_editor(|editor, window, cx| {
2224 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2225 });
2226 cx.assert_editor_state(
2227 &r#"one
2228 two
2229 ˇ
2230 three
2231 four
2232 five
2233
2234 six"#
2235 .unindent(),
2236 );
2237
2238 cx.update_editor(|editor, window, cx| {
2239 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2240 });
2241 cx.assert_editor_state(
2242 &r#"ˇone
2243 two
2244
2245 three
2246 four
2247 five
2248
2249 six"#
2250 .unindent(),
2251 );
2252}
2253
2254#[gpui::test]
2255async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2256 init_test(cx, |_| {});
2257 let mut cx = EditorTestContext::new(cx).await;
2258 let line_height = cx.editor(|editor, window, _| {
2259 editor
2260 .style()
2261 .unwrap()
2262 .text
2263 .line_height_in_pixels(window.rem_size())
2264 });
2265 let window = cx.window;
2266 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2267
2268 cx.set_state(
2269 r#"ˇone
2270 two
2271 three
2272 four
2273 five
2274 six
2275 seven
2276 eight
2277 nine
2278 ten
2279 "#,
2280 );
2281
2282 cx.update_editor(|editor, window, cx| {
2283 assert_eq!(
2284 editor.snapshot(window, cx).scroll_position(),
2285 gpui::Point::new(0., 0.)
2286 );
2287 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2288 assert_eq!(
2289 editor.snapshot(window, cx).scroll_position(),
2290 gpui::Point::new(0., 3.)
2291 );
2292 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2293 assert_eq!(
2294 editor.snapshot(window, cx).scroll_position(),
2295 gpui::Point::new(0., 6.)
2296 );
2297 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2298 assert_eq!(
2299 editor.snapshot(window, cx).scroll_position(),
2300 gpui::Point::new(0., 3.)
2301 );
2302
2303 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2304 assert_eq!(
2305 editor.snapshot(window, cx).scroll_position(),
2306 gpui::Point::new(0., 1.)
2307 );
2308 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2309 assert_eq!(
2310 editor.snapshot(window, cx).scroll_position(),
2311 gpui::Point::new(0., 3.)
2312 );
2313 });
2314}
2315
2316#[gpui::test]
2317async fn test_autoscroll(cx: &mut TestAppContext) {
2318 init_test(cx, |_| {});
2319 let mut cx = EditorTestContext::new(cx).await;
2320
2321 let line_height = cx.update_editor(|editor, window, cx| {
2322 editor.set_vertical_scroll_margin(2, cx);
2323 editor
2324 .style()
2325 .unwrap()
2326 .text
2327 .line_height_in_pixels(window.rem_size())
2328 });
2329 let window = cx.window;
2330 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2331
2332 cx.set_state(
2333 r#"ˇone
2334 two
2335 three
2336 four
2337 five
2338 six
2339 seven
2340 eight
2341 nine
2342 ten
2343 "#,
2344 );
2345 cx.update_editor(|editor, window, cx| {
2346 assert_eq!(
2347 editor.snapshot(window, cx).scroll_position(),
2348 gpui::Point::new(0., 0.0)
2349 );
2350 });
2351
2352 // Add a cursor below the visible area. Since both cursors cannot fit
2353 // on screen, the editor autoscrolls to reveal the newest cursor, and
2354 // allows the vertical scroll margin below that cursor.
2355 cx.update_editor(|editor, window, cx| {
2356 editor.change_selections(Default::default(), window, cx, |selections| {
2357 selections.select_ranges([
2358 Point::new(0, 0)..Point::new(0, 0),
2359 Point::new(6, 0)..Point::new(6, 0),
2360 ]);
2361 })
2362 });
2363 cx.update_editor(|editor, window, cx| {
2364 assert_eq!(
2365 editor.snapshot(window, cx).scroll_position(),
2366 gpui::Point::new(0., 3.0)
2367 );
2368 });
2369
2370 // Move down. The editor cursor scrolls down to track the newest cursor.
2371 cx.update_editor(|editor, window, cx| {
2372 editor.move_down(&Default::default(), window, cx);
2373 });
2374 cx.update_editor(|editor, window, cx| {
2375 assert_eq!(
2376 editor.snapshot(window, cx).scroll_position(),
2377 gpui::Point::new(0., 4.0)
2378 );
2379 });
2380
2381 // Add a cursor above the visible area. Since both cursors fit on screen,
2382 // the editor scrolls to show both.
2383 cx.update_editor(|editor, window, cx| {
2384 editor.change_selections(Default::default(), window, cx, |selections| {
2385 selections.select_ranges([
2386 Point::new(1, 0)..Point::new(1, 0),
2387 Point::new(6, 0)..Point::new(6, 0),
2388 ]);
2389 })
2390 });
2391 cx.update_editor(|editor, window, cx| {
2392 assert_eq!(
2393 editor.snapshot(window, cx).scroll_position(),
2394 gpui::Point::new(0., 1.0)
2395 );
2396 });
2397}
2398
2399#[gpui::test]
2400async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2401 init_test(cx, |_| {});
2402 let mut cx = EditorTestContext::new(cx).await;
2403
2404 let line_height = cx.editor(|editor, window, _cx| {
2405 editor
2406 .style()
2407 .unwrap()
2408 .text
2409 .line_height_in_pixels(window.rem_size())
2410 });
2411 let window = cx.window;
2412 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2413 cx.set_state(
2414 &r#"
2415 ˇone
2416 two
2417 threeˇ
2418 four
2419 five
2420 six
2421 seven
2422 eight
2423 nine
2424 ten
2425 "#
2426 .unindent(),
2427 );
2428
2429 cx.update_editor(|editor, window, cx| {
2430 editor.move_page_down(&MovePageDown::default(), window, cx)
2431 });
2432 cx.assert_editor_state(
2433 &r#"
2434 one
2435 two
2436 three
2437 ˇfour
2438 five
2439 sixˇ
2440 seven
2441 eight
2442 nine
2443 ten
2444 "#
2445 .unindent(),
2446 );
2447
2448 cx.update_editor(|editor, window, cx| {
2449 editor.move_page_down(&MovePageDown::default(), window, cx)
2450 });
2451 cx.assert_editor_state(
2452 &r#"
2453 one
2454 two
2455 three
2456 four
2457 five
2458 six
2459 ˇseven
2460 eight
2461 nineˇ
2462 ten
2463 "#
2464 .unindent(),
2465 );
2466
2467 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2468 cx.assert_editor_state(
2469 &r#"
2470 one
2471 two
2472 three
2473 ˇfour
2474 five
2475 sixˇ
2476 seven
2477 eight
2478 nine
2479 ten
2480 "#
2481 .unindent(),
2482 );
2483
2484 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2485 cx.assert_editor_state(
2486 &r#"
2487 ˇone
2488 two
2489 threeˇ
2490 four
2491 five
2492 six
2493 seven
2494 eight
2495 nine
2496 ten
2497 "#
2498 .unindent(),
2499 );
2500
2501 // Test select collapsing
2502 cx.update_editor(|editor, window, cx| {
2503 editor.move_page_down(&MovePageDown::default(), window, cx);
2504 editor.move_page_down(&MovePageDown::default(), window, cx);
2505 editor.move_page_down(&MovePageDown::default(), window, cx);
2506 });
2507 cx.assert_editor_state(
2508 &r#"
2509 one
2510 two
2511 three
2512 four
2513 five
2514 six
2515 seven
2516 eight
2517 nine
2518 ˇten
2519 ˇ"#
2520 .unindent(),
2521 );
2522}
2523
2524#[gpui::test]
2525async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2526 init_test(cx, |_| {});
2527 let mut cx = EditorTestContext::new(cx).await;
2528 cx.set_state("one «two threeˇ» four");
2529 cx.update_editor(|editor, window, cx| {
2530 editor.delete_to_beginning_of_line(
2531 &DeleteToBeginningOfLine {
2532 stop_at_indent: false,
2533 },
2534 window,
2535 cx,
2536 );
2537 assert_eq!(editor.text(cx), " four");
2538 });
2539}
2540
2541#[gpui::test]
2542async fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2543 init_test(cx, |_| {});
2544
2545 let mut cx = EditorTestContext::new(cx).await;
2546
2547 // For an empty selection, the preceding word fragment is deleted.
2548 // For non-empty selections, only selected characters are deleted.
2549 cx.set_state("onˇe two t«hreˇ»e four");
2550 cx.update_editor(|editor, window, cx| {
2551 editor.delete_to_previous_word_start(
2552 &DeleteToPreviousWordStart {
2553 ignore_newlines: false,
2554 ignore_brackets: false,
2555 },
2556 window,
2557 cx,
2558 );
2559 });
2560 cx.assert_editor_state("ˇe two tˇe four");
2561
2562 cx.set_state("e tˇwo te «fˇ»our");
2563 cx.update_editor(|editor, window, cx| {
2564 editor.delete_to_next_word_end(
2565 &DeleteToNextWordEnd {
2566 ignore_newlines: false,
2567 ignore_brackets: false,
2568 },
2569 window,
2570 cx,
2571 );
2572 });
2573 cx.assert_editor_state("e tˇ te ˇour");
2574}
2575
2576#[gpui::test]
2577async fn test_delete_whitespaces(cx: &mut TestAppContext) {
2578 init_test(cx, |_| {});
2579
2580 let mut cx = EditorTestContext::new(cx).await;
2581
2582 cx.set_state("here is some text ˇwith a space");
2583 cx.update_editor(|editor, window, cx| {
2584 editor.delete_to_previous_word_start(
2585 &DeleteToPreviousWordStart {
2586 ignore_newlines: false,
2587 ignore_brackets: true,
2588 },
2589 window,
2590 cx,
2591 );
2592 });
2593 // Continuous whitespace sequences are removed entirely, words behind them are not affected by the deletion action.
2594 cx.assert_editor_state("here is some textˇwith a space");
2595
2596 cx.set_state("here is some text ˇwith a space");
2597 cx.update_editor(|editor, window, cx| {
2598 editor.delete_to_previous_word_start(
2599 &DeleteToPreviousWordStart {
2600 ignore_newlines: false,
2601 ignore_brackets: false,
2602 },
2603 window,
2604 cx,
2605 );
2606 });
2607 cx.assert_editor_state("here is some textˇwith a space");
2608
2609 cx.set_state("here is some textˇ with a space");
2610 cx.update_editor(|editor, window, cx| {
2611 editor.delete_to_next_word_end(
2612 &DeleteToNextWordEnd {
2613 ignore_newlines: false,
2614 ignore_brackets: true,
2615 },
2616 window,
2617 cx,
2618 );
2619 });
2620 // Same happens in the other direction.
2621 cx.assert_editor_state("here is some textˇwith a space");
2622
2623 cx.set_state("here is some textˇ with a space");
2624 cx.update_editor(|editor, window, cx| {
2625 editor.delete_to_next_word_end(
2626 &DeleteToNextWordEnd {
2627 ignore_newlines: false,
2628 ignore_brackets: false,
2629 },
2630 window,
2631 cx,
2632 );
2633 });
2634 cx.assert_editor_state("here is some textˇwith a space");
2635
2636 cx.set_state("here is some textˇ with a space");
2637 cx.update_editor(|editor, window, cx| {
2638 editor.delete_to_next_word_end(
2639 &DeleteToNextWordEnd {
2640 ignore_newlines: true,
2641 ignore_brackets: false,
2642 },
2643 window,
2644 cx,
2645 );
2646 });
2647 cx.assert_editor_state("here is some textˇwith a space");
2648 cx.update_editor(|editor, window, cx| {
2649 editor.delete_to_previous_word_start(
2650 &DeleteToPreviousWordStart {
2651 ignore_newlines: true,
2652 ignore_brackets: false,
2653 },
2654 window,
2655 cx,
2656 );
2657 });
2658 cx.assert_editor_state("here is some ˇwith a space");
2659 cx.update_editor(|editor, window, cx| {
2660 editor.delete_to_previous_word_start(
2661 &DeleteToPreviousWordStart {
2662 ignore_newlines: true,
2663 ignore_brackets: false,
2664 },
2665 window,
2666 cx,
2667 );
2668 });
2669 // Single whitespaces are removed with the word behind them.
2670 cx.assert_editor_state("here is ˇwith a space");
2671 cx.update_editor(|editor, window, cx| {
2672 editor.delete_to_previous_word_start(
2673 &DeleteToPreviousWordStart {
2674 ignore_newlines: true,
2675 ignore_brackets: false,
2676 },
2677 window,
2678 cx,
2679 );
2680 });
2681 cx.assert_editor_state("here ˇwith a space");
2682 cx.update_editor(|editor, window, cx| {
2683 editor.delete_to_previous_word_start(
2684 &DeleteToPreviousWordStart {
2685 ignore_newlines: true,
2686 ignore_brackets: false,
2687 },
2688 window,
2689 cx,
2690 );
2691 });
2692 cx.assert_editor_state("ˇwith a space");
2693 cx.update_editor(|editor, window, cx| {
2694 editor.delete_to_previous_word_start(
2695 &DeleteToPreviousWordStart {
2696 ignore_newlines: true,
2697 ignore_brackets: false,
2698 },
2699 window,
2700 cx,
2701 );
2702 });
2703 cx.assert_editor_state("ˇwith a space");
2704 cx.update_editor(|editor, window, cx| {
2705 editor.delete_to_next_word_end(
2706 &DeleteToNextWordEnd {
2707 ignore_newlines: true,
2708 ignore_brackets: false,
2709 },
2710 window,
2711 cx,
2712 );
2713 });
2714 // Same happens in the other direction.
2715 cx.assert_editor_state("ˇ a space");
2716 cx.update_editor(|editor, window, cx| {
2717 editor.delete_to_next_word_end(
2718 &DeleteToNextWordEnd {
2719 ignore_newlines: true,
2720 ignore_brackets: false,
2721 },
2722 window,
2723 cx,
2724 );
2725 });
2726 cx.assert_editor_state("ˇ space");
2727 cx.update_editor(|editor, window, cx| {
2728 editor.delete_to_next_word_end(
2729 &DeleteToNextWordEnd {
2730 ignore_newlines: true,
2731 ignore_brackets: false,
2732 },
2733 window,
2734 cx,
2735 );
2736 });
2737 cx.assert_editor_state("ˇ");
2738 cx.update_editor(|editor, window, cx| {
2739 editor.delete_to_next_word_end(
2740 &DeleteToNextWordEnd {
2741 ignore_newlines: true,
2742 ignore_brackets: false,
2743 },
2744 window,
2745 cx,
2746 );
2747 });
2748 cx.assert_editor_state("ˇ");
2749 cx.update_editor(|editor, window, cx| {
2750 editor.delete_to_previous_word_start(
2751 &DeleteToPreviousWordStart {
2752 ignore_newlines: true,
2753 ignore_brackets: false,
2754 },
2755 window,
2756 cx,
2757 );
2758 });
2759 cx.assert_editor_state("ˇ");
2760}
2761
2762#[gpui::test]
2763async fn test_delete_to_bracket(cx: &mut TestAppContext) {
2764 init_test(cx, |_| {});
2765
2766 let language = Arc::new(
2767 Language::new(
2768 LanguageConfig {
2769 brackets: BracketPairConfig {
2770 pairs: vec![
2771 BracketPair {
2772 start: "\"".to_string(),
2773 end: "\"".to_string(),
2774 close: true,
2775 surround: true,
2776 newline: false,
2777 },
2778 BracketPair {
2779 start: "(".to_string(),
2780 end: ")".to_string(),
2781 close: true,
2782 surround: true,
2783 newline: true,
2784 },
2785 ],
2786 ..BracketPairConfig::default()
2787 },
2788 ..LanguageConfig::default()
2789 },
2790 Some(tree_sitter_rust::LANGUAGE.into()),
2791 )
2792 .with_brackets_query(
2793 r#"
2794 ("(" @open ")" @close)
2795 ("\"" @open "\"" @close)
2796 "#,
2797 )
2798 .unwrap(),
2799 );
2800
2801 let mut cx = EditorTestContext::new(cx).await;
2802 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2803
2804 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2805 cx.update_editor(|editor, window, cx| {
2806 editor.delete_to_previous_word_start(
2807 &DeleteToPreviousWordStart {
2808 ignore_newlines: true,
2809 ignore_brackets: false,
2810 },
2811 window,
2812 cx,
2813 );
2814 });
2815 // Deletion stops before brackets if asked to not ignore them.
2816 cx.assert_editor_state(r#"macro!("ˇCOMMENT");"#);
2817 cx.update_editor(|editor, window, cx| {
2818 editor.delete_to_previous_word_start(
2819 &DeleteToPreviousWordStart {
2820 ignore_newlines: true,
2821 ignore_brackets: false,
2822 },
2823 window,
2824 cx,
2825 );
2826 });
2827 // Deletion has to remove a single bracket and then stop again.
2828 cx.assert_editor_state(r#"macro!(ˇCOMMENT");"#);
2829
2830 cx.update_editor(|editor, window, cx| {
2831 editor.delete_to_previous_word_start(
2832 &DeleteToPreviousWordStart {
2833 ignore_newlines: true,
2834 ignore_brackets: false,
2835 },
2836 window,
2837 cx,
2838 );
2839 });
2840 cx.assert_editor_state(r#"macro!ˇCOMMENT");"#);
2841
2842 cx.update_editor(|editor, window, cx| {
2843 editor.delete_to_previous_word_start(
2844 &DeleteToPreviousWordStart {
2845 ignore_newlines: true,
2846 ignore_brackets: false,
2847 },
2848 window,
2849 cx,
2850 );
2851 });
2852 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2853
2854 cx.update_editor(|editor, window, cx| {
2855 editor.delete_to_previous_word_start(
2856 &DeleteToPreviousWordStart {
2857 ignore_newlines: true,
2858 ignore_brackets: false,
2859 },
2860 window,
2861 cx,
2862 );
2863 });
2864 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2865
2866 cx.update_editor(|editor, window, cx| {
2867 editor.delete_to_next_word_end(
2868 &DeleteToNextWordEnd {
2869 ignore_newlines: true,
2870 ignore_brackets: false,
2871 },
2872 window,
2873 cx,
2874 );
2875 });
2876 // Brackets on the right are not paired anymore, hence deletion does not stop at them
2877 cx.assert_editor_state(r#"ˇ");"#);
2878
2879 cx.update_editor(|editor, window, cx| {
2880 editor.delete_to_next_word_end(
2881 &DeleteToNextWordEnd {
2882 ignore_newlines: true,
2883 ignore_brackets: false,
2884 },
2885 window,
2886 cx,
2887 );
2888 });
2889 cx.assert_editor_state(r#"ˇ"#);
2890
2891 cx.update_editor(|editor, window, cx| {
2892 editor.delete_to_next_word_end(
2893 &DeleteToNextWordEnd {
2894 ignore_newlines: true,
2895 ignore_brackets: false,
2896 },
2897 window,
2898 cx,
2899 );
2900 });
2901 cx.assert_editor_state(r#"ˇ"#);
2902
2903 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2904 cx.update_editor(|editor, window, cx| {
2905 editor.delete_to_previous_word_start(
2906 &DeleteToPreviousWordStart {
2907 ignore_newlines: true,
2908 ignore_brackets: true,
2909 },
2910 window,
2911 cx,
2912 );
2913 });
2914 cx.assert_editor_state(r#"macroˇCOMMENT");"#);
2915}
2916
2917#[gpui::test]
2918fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2919 init_test(cx, |_| {});
2920
2921 let editor = cx.add_window(|window, cx| {
2922 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2923 build_editor(buffer, window, cx)
2924 });
2925 let del_to_prev_word_start = DeleteToPreviousWordStart {
2926 ignore_newlines: false,
2927 ignore_brackets: false,
2928 };
2929 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2930 ignore_newlines: true,
2931 ignore_brackets: false,
2932 };
2933
2934 _ = editor.update(cx, |editor, window, cx| {
2935 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2936 s.select_display_ranges([
2937 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2938 ])
2939 });
2940 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2941 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
2942 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2943 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
2944 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2945 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
2946 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2947 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
2948 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2949 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
2950 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2951 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2952 });
2953}
2954
2955#[gpui::test]
2956fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
2957 init_test(cx, |_| {});
2958
2959 let editor = cx.add_window(|window, cx| {
2960 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
2961 build_editor(buffer, window, cx)
2962 });
2963 let del_to_next_word_end = DeleteToNextWordEnd {
2964 ignore_newlines: false,
2965 ignore_brackets: false,
2966 };
2967 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
2968 ignore_newlines: true,
2969 ignore_brackets: false,
2970 };
2971
2972 _ = editor.update(cx, |editor, window, cx| {
2973 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2974 s.select_display_ranges([
2975 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2976 ])
2977 });
2978 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2979 assert_eq!(
2980 editor.buffer.read(cx).read(cx).text(),
2981 "one\n two\nthree\n four"
2982 );
2983 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2984 assert_eq!(
2985 editor.buffer.read(cx).read(cx).text(),
2986 "\n two\nthree\n four"
2987 );
2988 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2989 assert_eq!(
2990 editor.buffer.read(cx).read(cx).text(),
2991 "two\nthree\n four"
2992 );
2993 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2994 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
2995 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2996 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
2997 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2998 assert_eq!(editor.buffer.read(cx).read(cx).text(), "four");
2999 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3000 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
3001 });
3002}
3003
3004#[gpui::test]
3005fn test_newline(cx: &mut TestAppContext) {
3006 init_test(cx, |_| {});
3007
3008 let editor = cx.add_window(|window, cx| {
3009 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
3010 build_editor(buffer, window, cx)
3011 });
3012
3013 _ = editor.update(cx, |editor, window, cx| {
3014 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3015 s.select_display_ranges([
3016 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
3017 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
3018 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
3019 ])
3020 });
3021
3022 editor.newline(&Newline, window, cx);
3023 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
3024 });
3025}
3026
3027#[gpui::test]
3028fn test_newline_with_old_selections(cx: &mut TestAppContext) {
3029 init_test(cx, |_| {});
3030
3031 let editor = cx.add_window(|window, cx| {
3032 let buffer = MultiBuffer::build_simple(
3033 "
3034 a
3035 b(
3036 X
3037 )
3038 c(
3039 X
3040 )
3041 "
3042 .unindent()
3043 .as_str(),
3044 cx,
3045 );
3046 let mut editor = build_editor(buffer, window, cx);
3047 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3048 s.select_ranges([
3049 Point::new(2, 4)..Point::new(2, 5),
3050 Point::new(5, 4)..Point::new(5, 5),
3051 ])
3052 });
3053 editor
3054 });
3055
3056 _ = editor.update(cx, |editor, window, cx| {
3057 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3058 editor.buffer.update(cx, |buffer, cx| {
3059 buffer.edit(
3060 [
3061 (Point::new(1, 2)..Point::new(3, 0), ""),
3062 (Point::new(4, 2)..Point::new(6, 0), ""),
3063 ],
3064 None,
3065 cx,
3066 );
3067 assert_eq!(
3068 buffer.read(cx).text(),
3069 "
3070 a
3071 b()
3072 c()
3073 "
3074 .unindent()
3075 );
3076 });
3077 assert_eq!(
3078 editor.selections.ranges(cx),
3079 &[
3080 Point::new(1, 2)..Point::new(1, 2),
3081 Point::new(2, 2)..Point::new(2, 2),
3082 ],
3083 );
3084
3085 editor.newline(&Newline, window, cx);
3086 assert_eq!(
3087 editor.text(cx),
3088 "
3089 a
3090 b(
3091 )
3092 c(
3093 )
3094 "
3095 .unindent()
3096 );
3097
3098 // The selections are moved after the inserted newlines
3099 assert_eq!(
3100 editor.selections.ranges(cx),
3101 &[
3102 Point::new(2, 0)..Point::new(2, 0),
3103 Point::new(4, 0)..Point::new(4, 0),
3104 ],
3105 );
3106 });
3107}
3108
3109#[gpui::test]
3110async fn test_newline_above(cx: &mut TestAppContext) {
3111 init_test(cx, |settings| {
3112 settings.defaults.tab_size = NonZeroU32::new(4)
3113 });
3114
3115 let language = Arc::new(
3116 Language::new(
3117 LanguageConfig::default(),
3118 Some(tree_sitter_rust::LANGUAGE.into()),
3119 )
3120 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3121 .unwrap(),
3122 );
3123
3124 let mut cx = EditorTestContext::new(cx).await;
3125 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3126 cx.set_state(indoc! {"
3127 const a: ˇA = (
3128 (ˇ
3129 «const_functionˇ»(ˇ),
3130 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3131 )ˇ
3132 ˇ);ˇ
3133 "});
3134
3135 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
3136 cx.assert_editor_state(indoc! {"
3137 ˇ
3138 const a: A = (
3139 ˇ
3140 (
3141 ˇ
3142 ˇ
3143 const_function(),
3144 ˇ
3145 ˇ
3146 ˇ
3147 ˇ
3148 something_else,
3149 ˇ
3150 )
3151 ˇ
3152 ˇ
3153 );
3154 "});
3155}
3156
3157#[gpui::test]
3158async fn test_newline_below(cx: &mut TestAppContext) {
3159 init_test(cx, |settings| {
3160 settings.defaults.tab_size = NonZeroU32::new(4)
3161 });
3162
3163 let language = Arc::new(
3164 Language::new(
3165 LanguageConfig::default(),
3166 Some(tree_sitter_rust::LANGUAGE.into()),
3167 )
3168 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3169 .unwrap(),
3170 );
3171
3172 let mut cx = EditorTestContext::new(cx).await;
3173 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3174 cx.set_state(indoc! {"
3175 const a: ˇA = (
3176 (ˇ
3177 «const_functionˇ»(ˇ),
3178 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3179 )ˇ
3180 ˇ);ˇ
3181 "});
3182
3183 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
3184 cx.assert_editor_state(indoc! {"
3185 const a: A = (
3186 ˇ
3187 (
3188 ˇ
3189 const_function(),
3190 ˇ
3191 ˇ
3192 something_else,
3193 ˇ
3194 ˇ
3195 ˇ
3196 ˇ
3197 )
3198 ˇ
3199 );
3200 ˇ
3201 ˇ
3202 "});
3203}
3204
3205#[gpui::test]
3206async fn test_newline_comments(cx: &mut TestAppContext) {
3207 init_test(cx, |settings| {
3208 settings.defaults.tab_size = NonZeroU32::new(4)
3209 });
3210
3211 let language = Arc::new(Language::new(
3212 LanguageConfig {
3213 line_comments: vec!["// ".into()],
3214 ..LanguageConfig::default()
3215 },
3216 None,
3217 ));
3218 {
3219 let mut cx = EditorTestContext::new(cx).await;
3220 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3221 cx.set_state(indoc! {"
3222 // Fooˇ
3223 "});
3224
3225 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3226 cx.assert_editor_state(indoc! {"
3227 // Foo
3228 // ˇ
3229 "});
3230 // Ensure that we add comment prefix when existing line contains space
3231 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3232 cx.assert_editor_state(
3233 indoc! {"
3234 // Foo
3235 //s
3236 // ˇ
3237 "}
3238 .replace("s", " ") // s is used as space placeholder to prevent format on save
3239 .as_str(),
3240 );
3241 // Ensure that we add comment prefix when existing line does not contain space
3242 cx.set_state(indoc! {"
3243 // Foo
3244 //ˇ
3245 "});
3246 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3247 cx.assert_editor_state(indoc! {"
3248 // Foo
3249 //
3250 // ˇ
3251 "});
3252 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
3253 cx.set_state(indoc! {"
3254 ˇ// Foo
3255 "});
3256 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3257 cx.assert_editor_state(indoc! {"
3258
3259 ˇ// Foo
3260 "});
3261 }
3262 // Ensure that comment continuations can be disabled.
3263 update_test_language_settings(cx, |settings| {
3264 settings.defaults.extend_comment_on_newline = Some(false);
3265 });
3266 let mut cx = EditorTestContext::new(cx).await;
3267 cx.set_state(indoc! {"
3268 // Fooˇ
3269 "});
3270 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3271 cx.assert_editor_state(indoc! {"
3272 // Foo
3273 ˇ
3274 "});
3275}
3276
3277#[gpui::test]
3278async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
3279 init_test(cx, |settings| {
3280 settings.defaults.tab_size = NonZeroU32::new(4)
3281 });
3282
3283 let language = Arc::new(Language::new(
3284 LanguageConfig {
3285 line_comments: vec!["// ".into(), "/// ".into()],
3286 ..LanguageConfig::default()
3287 },
3288 None,
3289 ));
3290 {
3291 let mut cx = EditorTestContext::new(cx).await;
3292 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3293 cx.set_state(indoc! {"
3294 //ˇ
3295 "});
3296 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3297 cx.assert_editor_state(indoc! {"
3298 //
3299 // ˇ
3300 "});
3301
3302 cx.set_state(indoc! {"
3303 ///ˇ
3304 "});
3305 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3306 cx.assert_editor_state(indoc! {"
3307 ///
3308 /// ˇ
3309 "});
3310 }
3311}
3312
3313#[gpui::test]
3314async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
3315 init_test(cx, |settings| {
3316 settings.defaults.tab_size = NonZeroU32::new(4)
3317 });
3318
3319 let language = Arc::new(
3320 Language::new(
3321 LanguageConfig {
3322 documentation_comment: Some(language::BlockCommentConfig {
3323 start: "/**".into(),
3324 end: "*/".into(),
3325 prefix: "* ".into(),
3326 tab_size: 1,
3327 }),
3328
3329 ..LanguageConfig::default()
3330 },
3331 Some(tree_sitter_rust::LANGUAGE.into()),
3332 )
3333 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
3334 .unwrap(),
3335 );
3336
3337 {
3338 let mut cx = EditorTestContext::new(cx).await;
3339 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3340 cx.set_state(indoc! {"
3341 /**ˇ
3342 "});
3343
3344 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3345 cx.assert_editor_state(indoc! {"
3346 /**
3347 * ˇ
3348 "});
3349 // Ensure that if cursor is before the comment start,
3350 // we do not actually insert a comment prefix.
3351 cx.set_state(indoc! {"
3352 ˇ/**
3353 "});
3354 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3355 cx.assert_editor_state(indoc! {"
3356
3357 ˇ/**
3358 "});
3359 // Ensure that if cursor is between it doesn't add comment prefix.
3360 cx.set_state(indoc! {"
3361 /*ˇ*
3362 "});
3363 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3364 cx.assert_editor_state(indoc! {"
3365 /*
3366 ˇ*
3367 "});
3368 // Ensure that if suffix exists on same line after cursor it adds new line.
3369 cx.set_state(indoc! {"
3370 /**ˇ*/
3371 "});
3372 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3373 cx.assert_editor_state(indoc! {"
3374 /**
3375 * ˇ
3376 */
3377 "});
3378 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3379 cx.set_state(indoc! {"
3380 /**ˇ */
3381 "});
3382 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3383 cx.assert_editor_state(indoc! {"
3384 /**
3385 * ˇ
3386 */
3387 "});
3388 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3389 cx.set_state(indoc! {"
3390 /** ˇ*/
3391 "});
3392 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3393 cx.assert_editor_state(
3394 indoc! {"
3395 /**s
3396 * ˇ
3397 */
3398 "}
3399 .replace("s", " ") // s is used as space placeholder to prevent format on save
3400 .as_str(),
3401 );
3402 // Ensure that delimiter space is preserved when newline on already
3403 // spaced delimiter.
3404 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3405 cx.assert_editor_state(
3406 indoc! {"
3407 /**s
3408 *s
3409 * ˇ
3410 */
3411 "}
3412 .replace("s", " ") // s is used as space placeholder to prevent format on save
3413 .as_str(),
3414 );
3415 // Ensure that delimiter space is preserved when space is not
3416 // on existing delimiter.
3417 cx.set_state(indoc! {"
3418 /**
3419 *ˇ
3420 */
3421 "});
3422 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3423 cx.assert_editor_state(indoc! {"
3424 /**
3425 *
3426 * ˇ
3427 */
3428 "});
3429 // Ensure that if suffix exists on same line after cursor it
3430 // doesn't add extra new line if prefix is not on same line.
3431 cx.set_state(indoc! {"
3432 /**
3433 ˇ*/
3434 "});
3435 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3436 cx.assert_editor_state(indoc! {"
3437 /**
3438
3439 ˇ*/
3440 "});
3441 // Ensure that it detects suffix after existing prefix.
3442 cx.set_state(indoc! {"
3443 /**ˇ/
3444 "});
3445 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3446 cx.assert_editor_state(indoc! {"
3447 /**
3448 ˇ/
3449 "});
3450 // Ensure that if suffix exists on same line before
3451 // cursor it does not add comment prefix.
3452 cx.set_state(indoc! {"
3453 /** */ˇ
3454 "});
3455 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3456 cx.assert_editor_state(indoc! {"
3457 /** */
3458 ˇ
3459 "});
3460 // Ensure that if suffix exists on same line before
3461 // cursor it does not add comment prefix.
3462 cx.set_state(indoc! {"
3463 /**
3464 *
3465 */ˇ
3466 "});
3467 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3468 cx.assert_editor_state(indoc! {"
3469 /**
3470 *
3471 */
3472 ˇ
3473 "});
3474
3475 // Ensure that inline comment followed by code
3476 // doesn't add comment prefix on newline
3477 cx.set_state(indoc! {"
3478 /** */ textˇ
3479 "});
3480 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3481 cx.assert_editor_state(indoc! {"
3482 /** */ text
3483 ˇ
3484 "});
3485
3486 // Ensure that text after comment end tag
3487 // doesn't add comment prefix on newline
3488 cx.set_state(indoc! {"
3489 /**
3490 *
3491 */ˇtext
3492 "});
3493 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3494 cx.assert_editor_state(indoc! {"
3495 /**
3496 *
3497 */
3498 ˇtext
3499 "});
3500
3501 // Ensure if not comment block it doesn't
3502 // add comment prefix on newline
3503 cx.set_state(indoc! {"
3504 * textˇ
3505 "});
3506 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3507 cx.assert_editor_state(indoc! {"
3508 * text
3509 ˇ
3510 "});
3511 }
3512 // Ensure that comment continuations can be disabled.
3513 update_test_language_settings(cx, |settings| {
3514 settings.defaults.extend_comment_on_newline = Some(false);
3515 });
3516 let mut cx = EditorTestContext::new(cx).await;
3517 cx.set_state(indoc! {"
3518 /**ˇ
3519 "});
3520 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3521 cx.assert_editor_state(indoc! {"
3522 /**
3523 ˇ
3524 "});
3525}
3526
3527#[gpui::test]
3528async fn test_newline_comments_with_block_comment(cx: &mut TestAppContext) {
3529 init_test(cx, |settings| {
3530 settings.defaults.tab_size = NonZeroU32::new(4)
3531 });
3532
3533 let lua_language = Arc::new(Language::new(
3534 LanguageConfig {
3535 line_comments: vec!["--".into()],
3536 block_comment: Some(language::BlockCommentConfig {
3537 start: "--[[".into(),
3538 prefix: "".into(),
3539 end: "]]".into(),
3540 tab_size: 0,
3541 }),
3542 ..LanguageConfig::default()
3543 },
3544 None,
3545 ));
3546
3547 let mut cx = EditorTestContext::new(cx).await;
3548 cx.update_buffer(|buffer, cx| buffer.set_language(Some(lua_language), cx));
3549
3550 // Line with line comment should extend
3551 cx.set_state(indoc! {"
3552 --ˇ
3553 "});
3554 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3555 cx.assert_editor_state(indoc! {"
3556 --
3557 --ˇ
3558 "});
3559
3560 // Line with block comment that matches line comment should not extend
3561 cx.set_state(indoc! {"
3562 --[[ˇ
3563 "});
3564 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3565 cx.assert_editor_state(indoc! {"
3566 --[[
3567 ˇ
3568 "});
3569}
3570
3571#[gpui::test]
3572fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3573 init_test(cx, |_| {});
3574
3575 let editor = cx.add_window(|window, cx| {
3576 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3577 let mut editor = build_editor(buffer, window, cx);
3578 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3579 s.select_ranges([3..4, 11..12, 19..20])
3580 });
3581 editor
3582 });
3583
3584 _ = editor.update(cx, |editor, window, cx| {
3585 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3586 editor.buffer.update(cx, |buffer, cx| {
3587 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
3588 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3589 });
3590 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
3591
3592 editor.insert("Z", window, cx);
3593 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3594
3595 // The selections are moved after the inserted characters
3596 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
3597 });
3598}
3599
3600#[gpui::test]
3601async fn test_tab(cx: &mut TestAppContext) {
3602 init_test(cx, |settings| {
3603 settings.defaults.tab_size = NonZeroU32::new(3)
3604 });
3605
3606 let mut cx = EditorTestContext::new(cx).await;
3607 cx.set_state(indoc! {"
3608 ˇabˇc
3609 ˇ🏀ˇ🏀ˇefg
3610 dˇ
3611 "});
3612 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3613 cx.assert_editor_state(indoc! {"
3614 ˇab ˇc
3615 ˇ🏀 ˇ🏀 ˇefg
3616 d ˇ
3617 "});
3618
3619 cx.set_state(indoc! {"
3620 a
3621 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3622 "});
3623 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3624 cx.assert_editor_state(indoc! {"
3625 a
3626 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3627 "});
3628}
3629
3630#[gpui::test]
3631async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3632 init_test(cx, |_| {});
3633
3634 let mut cx = EditorTestContext::new(cx).await;
3635 let language = Arc::new(
3636 Language::new(
3637 LanguageConfig::default(),
3638 Some(tree_sitter_rust::LANGUAGE.into()),
3639 )
3640 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3641 .unwrap(),
3642 );
3643 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3644
3645 // test when all cursors are not at suggested indent
3646 // then simply move to their suggested indent location
3647 cx.set_state(indoc! {"
3648 const a: B = (
3649 c(
3650 ˇ
3651 ˇ )
3652 );
3653 "});
3654 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3655 cx.assert_editor_state(indoc! {"
3656 const a: B = (
3657 c(
3658 ˇ
3659 ˇ)
3660 );
3661 "});
3662
3663 // test cursor already at suggested indent not moving when
3664 // other cursors are yet to reach their suggested indents
3665 cx.set_state(indoc! {"
3666 ˇ
3667 const a: B = (
3668 c(
3669 d(
3670 ˇ
3671 )
3672 ˇ
3673 ˇ )
3674 );
3675 "});
3676 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3677 cx.assert_editor_state(indoc! {"
3678 ˇ
3679 const a: B = (
3680 c(
3681 d(
3682 ˇ
3683 )
3684 ˇ
3685 ˇ)
3686 );
3687 "});
3688 // test when all cursors are at suggested indent then tab is inserted
3689 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3690 cx.assert_editor_state(indoc! {"
3691 ˇ
3692 const a: B = (
3693 c(
3694 d(
3695 ˇ
3696 )
3697 ˇ
3698 ˇ)
3699 );
3700 "});
3701
3702 // test when current indent is less than suggested indent,
3703 // we adjust line to match suggested indent and move cursor to it
3704 //
3705 // when no other cursor is at word boundary, all of them should move
3706 cx.set_state(indoc! {"
3707 const a: B = (
3708 c(
3709 d(
3710 ˇ
3711 ˇ )
3712 ˇ )
3713 );
3714 "});
3715 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3716 cx.assert_editor_state(indoc! {"
3717 const a: B = (
3718 c(
3719 d(
3720 ˇ
3721 ˇ)
3722 ˇ)
3723 );
3724 "});
3725
3726 // test when current indent is less than suggested indent,
3727 // we adjust line to match suggested indent and move cursor to it
3728 //
3729 // when some other cursor is at word boundary, it should not move
3730 cx.set_state(indoc! {"
3731 const a: B = (
3732 c(
3733 d(
3734 ˇ
3735 ˇ )
3736 ˇ)
3737 );
3738 "});
3739 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3740 cx.assert_editor_state(indoc! {"
3741 const a: B = (
3742 c(
3743 d(
3744 ˇ
3745 ˇ)
3746 ˇ)
3747 );
3748 "});
3749
3750 // test when current indent is more than suggested indent,
3751 // we just move cursor to current indent instead of suggested indent
3752 //
3753 // when no other cursor is at word boundary, all of them should move
3754 cx.set_state(indoc! {"
3755 const a: B = (
3756 c(
3757 d(
3758 ˇ
3759 ˇ )
3760 ˇ )
3761 );
3762 "});
3763 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3764 cx.assert_editor_state(indoc! {"
3765 const a: B = (
3766 c(
3767 d(
3768 ˇ
3769 ˇ)
3770 ˇ)
3771 );
3772 "});
3773 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3774 cx.assert_editor_state(indoc! {"
3775 const a: B = (
3776 c(
3777 d(
3778 ˇ
3779 ˇ)
3780 ˇ)
3781 );
3782 "});
3783
3784 // test when current indent is more than suggested indent,
3785 // we just move cursor to current indent instead of suggested indent
3786 //
3787 // when some other cursor is at word boundary, it doesn't move
3788 cx.set_state(indoc! {"
3789 const a: B = (
3790 c(
3791 d(
3792 ˇ
3793 ˇ )
3794 ˇ)
3795 );
3796 "});
3797 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3798 cx.assert_editor_state(indoc! {"
3799 const a: B = (
3800 c(
3801 d(
3802 ˇ
3803 ˇ)
3804 ˇ)
3805 );
3806 "});
3807
3808 // handle auto-indent when there are multiple cursors on the same line
3809 cx.set_state(indoc! {"
3810 const a: B = (
3811 c(
3812 ˇ ˇ
3813 ˇ )
3814 );
3815 "});
3816 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3817 cx.assert_editor_state(indoc! {"
3818 const a: B = (
3819 c(
3820 ˇ
3821 ˇ)
3822 );
3823 "});
3824}
3825
3826#[gpui::test]
3827async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3828 init_test(cx, |settings| {
3829 settings.defaults.tab_size = NonZeroU32::new(3)
3830 });
3831
3832 let mut cx = EditorTestContext::new(cx).await;
3833 cx.set_state(indoc! {"
3834 ˇ
3835 \t ˇ
3836 \t ˇ
3837 \t ˇ
3838 \t \t\t \t \t\t \t\t \t \t ˇ
3839 "});
3840
3841 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3842 cx.assert_editor_state(indoc! {"
3843 ˇ
3844 \t ˇ
3845 \t ˇ
3846 \t ˇ
3847 \t \t\t \t \t\t \t\t \t \t ˇ
3848 "});
3849}
3850
3851#[gpui::test]
3852async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3853 init_test(cx, |settings| {
3854 settings.defaults.tab_size = NonZeroU32::new(4)
3855 });
3856
3857 let language = Arc::new(
3858 Language::new(
3859 LanguageConfig::default(),
3860 Some(tree_sitter_rust::LANGUAGE.into()),
3861 )
3862 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3863 .unwrap(),
3864 );
3865
3866 let mut cx = EditorTestContext::new(cx).await;
3867 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3868 cx.set_state(indoc! {"
3869 fn a() {
3870 if b {
3871 \t ˇc
3872 }
3873 }
3874 "});
3875
3876 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3877 cx.assert_editor_state(indoc! {"
3878 fn a() {
3879 if b {
3880 ˇc
3881 }
3882 }
3883 "});
3884}
3885
3886#[gpui::test]
3887async fn test_indent_outdent(cx: &mut TestAppContext) {
3888 init_test(cx, |settings| {
3889 settings.defaults.tab_size = NonZeroU32::new(4);
3890 });
3891
3892 let mut cx = EditorTestContext::new(cx).await;
3893
3894 cx.set_state(indoc! {"
3895 «oneˇ» «twoˇ»
3896 three
3897 four
3898 "});
3899 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3900 cx.assert_editor_state(indoc! {"
3901 «oneˇ» «twoˇ»
3902 three
3903 four
3904 "});
3905
3906 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3907 cx.assert_editor_state(indoc! {"
3908 «oneˇ» «twoˇ»
3909 three
3910 four
3911 "});
3912
3913 // select across line ending
3914 cx.set_state(indoc! {"
3915 one two
3916 t«hree
3917 ˇ» four
3918 "});
3919 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3920 cx.assert_editor_state(indoc! {"
3921 one two
3922 t«hree
3923 ˇ» four
3924 "});
3925
3926 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3927 cx.assert_editor_state(indoc! {"
3928 one two
3929 t«hree
3930 ˇ» four
3931 "});
3932
3933 // Ensure that indenting/outdenting works when the cursor is at column 0.
3934 cx.set_state(indoc! {"
3935 one two
3936 ˇthree
3937 four
3938 "});
3939 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3940 cx.assert_editor_state(indoc! {"
3941 one two
3942 ˇthree
3943 four
3944 "});
3945
3946 cx.set_state(indoc! {"
3947 one two
3948 ˇ three
3949 four
3950 "});
3951 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3952 cx.assert_editor_state(indoc! {"
3953 one two
3954 ˇthree
3955 four
3956 "});
3957}
3958
3959#[gpui::test]
3960async fn test_indent_yaml_comments_with_multiple_cursors(cx: &mut TestAppContext) {
3961 // This is a regression test for issue #33761
3962 init_test(cx, |_| {});
3963
3964 let mut cx = EditorTestContext::new(cx).await;
3965 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3966 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3967
3968 cx.set_state(
3969 r#"ˇ# ingress:
3970ˇ# api:
3971ˇ# enabled: false
3972ˇ# pathType: Prefix
3973ˇ# console:
3974ˇ# enabled: false
3975ˇ# pathType: Prefix
3976"#,
3977 );
3978
3979 // Press tab to indent all lines
3980 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3981
3982 cx.assert_editor_state(
3983 r#" ˇ# ingress:
3984 ˇ# api:
3985 ˇ# enabled: false
3986 ˇ# pathType: Prefix
3987 ˇ# console:
3988 ˇ# enabled: false
3989 ˇ# pathType: Prefix
3990"#,
3991 );
3992}
3993
3994#[gpui::test]
3995async fn test_indent_yaml_non_comments_with_multiple_cursors(cx: &mut TestAppContext) {
3996 // This is a test to make sure our fix for issue #33761 didn't break anything
3997 init_test(cx, |_| {});
3998
3999 let mut cx = EditorTestContext::new(cx).await;
4000 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
4001 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
4002
4003 cx.set_state(
4004 r#"ˇingress:
4005ˇ api:
4006ˇ enabled: false
4007ˇ pathType: Prefix
4008"#,
4009 );
4010
4011 // Press tab to indent all lines
4012 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4013
4014 cx.assert_editor_state(
4015 r#"ˇingress:
4016 ˇapi:
4017 ˇenabled: false
4018 ˇpathType: Prefix
4019"#,
4020 );
4021}
4022
4023#[gpui::test]
4024async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
4025 init_test(cx, |settings| {
4026 settings.defaults.hard_tabs = Some(true);
4027 });
4028
4029 let mut cx = EditorTestContext::new(cx).await;
4030
4031 // select two ranges on one line
4032 cx.set_state(indoc! {"
4033 «oneˇ» «twoˇ»
4034 three
4035 four
4036 "});
4037 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4038 cx.assert_editor_state(indoc! {"
4039 \t«oneˇ» «twoˇ»
4040 three
4041 four
4042 "});
4043 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4044 cx.assert_editor_state(indoc! {"
4045 \t\t«oneˇ» «twoˇ»
4046 three
4047 four
4048 "});
4049 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4050 cx.assert_editor_state(indoc! {"
4051 \t«oneˇ» «twoˇ»
4052 three
4053 four
4054 "});
4055 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4056 cx.assert_editor_state(indoc! {"
4057 «oneˇ» «twoˇ»
4058 three
4059 four
4060 "});
4061
4062 // select across a line ending
4063 cx.set_state(indoc! {"
4064 one two
4065 t«hree
4066 ˇ»four
4067 "});
4068 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4069 cx.assert_editor_state(indoc! {"
4070 one two
4071 \tt«hree
4072 ˇ»four
4073 "});
4074 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4075 cx.assert_editor_state(indoc! {"
4076 one two
4077 \t\tt«hree
4078 ˇ»four
4079 "});
4080 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4081 cx.assert_editor_state(indoc! {"
4082 one two
4083 \tt«hree
4084 ˇ»four
4085 "});
4086 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4087 cx.assert_editor_state(indoc! {"
4088 one two
4089 t«hree
4090 ˇ»four
4091 "});
4092
4093 // Ensure that indenting/outdenting works when the cursor is at column 0.
4094 cx.set_state(indoc! {"
4095 one two
4096 ˇthree
4097 four
4098 "});
4099 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4100 cx.assert_editor_state(indoc! {"
4101 one two
4102 ˇthree
4103 four
4104 "});
4105 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4106 cx.assert_editor_state(indoc! {"
4107 one two
4108 \tˇthree
4109 four
4110 "});
4111 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4112 cx.assert_editor_state(indoc! {"
4113 one two
4114 ˇthree
4115 four
4116 "});
4117}
4118
4119#[gpui::test]
4120fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
4121 init_test(cx, |settings| {
4122 settings.languages.0.extend([
4123 (
4124 "TOML".into(),
4125 LanguageSettingsContent {
4126 tab_size: NonZeroU32::new(2),
4127 ..Default::default()
4128 },
4129 ),
4130 (
4131 "Rust".into(),
4132 LanguageSettingsContent {
4133 tab_size: NonZeroU32::new(4),
4134 ..Default::default()
4135 },
4136 ),
4137 ]);
4138 });
4139
4140 let toml_language = Arc::new(Language::new(
4141 LanguageConfig {
4142 name: "TOML".into(),
4143 ..Default::default()
4144 },
4145 None,
4146 ));
4147 let rust_language = Arc::new(Language::new(
4148 LanguageConfig {
4149 name: "Rust".into(),
4150 ..Default::default()
4151 },
4152 None,
4153 ));
4154
4155 let toml_buffer =
4156 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
4157 let rust_buffer =
4158 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
4159 let multibuffer = cx.new(|cx| {
4160 let mut multibuffer = MultiBuffer::new(ReadWrite);
4161 multibuffer.push_excerpts(
4162 toml_buffer.clone(),
4163 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
4164 cx,
4165 );
4166 multibuffer.push_excerpts(
4167 rust_buffer.clone(),
4168 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
4169 cx,
4170 );
4171 multibuffer
4172 });
4173
4174 cx.add_window(|window, cx| {
4175 let mut editor = build_editor(multibuffer, window, cx);
4176
4177 assert_eq!(
4178 editor.text(cx),
4179 indoc! {"
4180 a = 1
4181 b = 2
4182
4183 const c: usize = 3;
4184 "}
4185 );
4186
4187 select_ranges(
4188 &mut editor,
4189 indoc! {"
4190 «aˇ» = 1
4191 b = 2
4192
4193 «const c:ˇ» usize = 3;
4194 "},
4195 window,
4196 cx,
4197 );
4198
4199 editor.tab(&Tab, window, cx);
4200 assert_text_with_selections(
4201 &mut editor,
4202 indoc! {"
4203 «aˇ» = 1
4204 b = 2
4205
4206 «const c:ˇ» usize = 3;
4207 "},
4208 cx,
4209 );
4210 editor.backtab(&Backtab, window, cx);
4211 assert_text_with_selections(
4212 &mut editor,
4213 indoc! {"
4214 «aˇ» = 1
4215 b = 2
4216
4217 «const c:ˇ» usize = 3;
4218 "},
4219 cx,
4220 );
4221
4222 editor
4223 });
4224}
4225
4226#[gpui::test]
4227async fn test_backspace(cx: &mut TestAppContext) {
4228 init_test(cx, |_| {});
4229
4230 let mut cx = EditorTestContext::new(cx).await;
4231
4232 // Basic backspace
4233 cx.set_state(indoc! {"
4234 onˇe two three
4235 fou«rˇ» five six
4236 seven «ˇeight nine
4237 »ten
4238 "});
4239 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4240 cx.assert_editor_state(indoc! {"
4241 oˇe two three
4242 fouˇ five six
4243 seven ˇten
4244 "});
4245
4246 // Test backspace inside and around indents
4247 cx.set_state(indoc! {"
4248 zero
4249 ˇone
4250 ˇtwo
4251 ˇ ˇ ˇ three
4252 ˇ ˇ four
4253 "});
4254 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4255 cx.assert_editor_state(indoc! {"
4256 zero
4257 ˇone
4258 ˇtwo
4259 ˇ threeˇ four
4260 "});
4261}
4262
4263#[gpui::test]
4264async fn test_delete(cx: &mut TestAppContext) {
4265 init_test(cx, |_| {});
4266
4267 let mut cx = EditorTestContext::new(cx).await;
4268 cx.set_state(indoc! {"
4269 onˇe two three
4270 fou«rˇ» five six
4271 seven «ˇeight nine
4272 »ten
4273 "});
4274 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
4275 cx.assert_editor_state(indoc! {"
4276 onˇ two three
4277 fouˇ five six
4278 seven ˇten
4279 "});
4280}
4281
4282#[gpui::test]
4283fn test_delete_line(cx: &mut TestAppContext) {
4284 init_test(cx, |_| {});
4285
4286 let editor = cx.add_window(|window, cx| {
4287 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4288 build_editor(buffer, window, cx)
4289 });
4290 _ = editor.update(cx, |editor, window, cx| {
4291 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4292 s.select_display_ranges([
4293 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4294 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4295 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4296 ])
4297 });
4298 editor.delete_line(&DeleteLine, window, cx);
4299 assert_eq!(editor.display_text(cx), "ghi");
4300 assert_eq!(
4301 editor.selections.display_ranges(cx),
4302 vec![
4303 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4304 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
4305 ]
4306 );
4307 });
4308
4309 let editor = cx.add_window(|window, cx| {
4310 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4311 build_editor(buffer, window, cx)
4312 });
4313 _ = editor.update(cx, |editor, window, cx| {
4314 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4315 s.select_display_ranges([
4316 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
4317 ])
4318 });
4319 editor.delete_line(&DeleteLine, window, cx);
4320 assert_eq!(editor.display_text(cx), "ghi\n");
4321 assert_eq!(
4322 editor.selections.display_ranges(cx),
4323 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
4324 );
4325 });
4326}
4327
4328#[gpui::test]
4329fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
4330 init_test(cx, |_| {});
4331
4332 cx.add_window(|window, cx| {
4333 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4334 let mut editor = build_editor(buffer.clone(), window, cx);
4335 let buffer = buffer.read(cx).as_singleton().unwrap();
4336
4337 assert_eq!(
4338 editor.selections.ranges::<Point>(cx),
4339 &[Point::new(0, 0)..Point::new(0, 0)]
4340 );
4341
4342 // When on single line, replace newline at end by space
4343 editor.join_lines(&JoinLines, window, cx);
4344 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4345 assert_eq!(
4346 editor.selections.ranges::<Point>(cx),
4347 &[Point::new(0, 3)..Point::new(0, 3)]
4348 );
4349
4350 // When multiple lines are selected, remove newlines that are spanned by the selection
4351 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4352 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
4353 });
4354 editor.join_lines(&JoinLines, window, cx);
4355 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
4356 assert_eq!(
4357 editor.selections.ranges::<Point>(cx),
4358 &[Point::new(0, 11)..Point::new(0, 11)]
4359 );
4360
4361 // Undo should be transactional
4362 editor.undo(&Undo, window, cx);
4363 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4364 assert_eq!(
4365 editor.selections.ranges::<Point>(cx),
4366 &[Point::new(0, 5)..Point::new(2, 2)]
4367 );
4368
4369 // When joining an empty line don't insert a space
4370 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4371 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
4372 });
4373 editor.join_lines(&JoinLines, window, cx);
4374 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
4375 assert_eq!(
4376 editor.selections.ranges::<Point>(cx),
4377 [Point::new(2, 3)..Point::new(2, 3)]
4378 );
4379
4380 // We can remove trailing newlines
4381 editor.join_lines(&JoinLines, window, cx);
4382 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4383 assert_eq!(
4384 editor.selections.ranges::<Point>(cx),
4385 [Point::new(2, 3)..Point::new(2, 3)]
4386 );
4387
4388 // We don't blow up on the last line
4389 editor.join_lines(&JoinLines, window, cx);
4390 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4391 assert_eq!(
4392 editor.selections.ranges::<Point>(cx),
4393 [Point::new(2, 3)..Point::new(2, 3)]
4394 );
4395
4396 // reset to test indentation
4397 editor.buffer.update(cx, |buffer, cx| {
4398 buffer.edit(
4399 [
4400 (Point::new(1, 0)..Point::new(1, 2), " "),
4401 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
4402 ],
4403 None,
4404 cx,
4405 )
4406 });
4407
4408 // We remove any leading spaces
4409 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
4410 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4411 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
4412 });
4413 editor.join_lines(&JoinLines, window, cx);
4414 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
4415
4416 // We don't insert a space for a line containing only spaces
4417 editor.join_lines(&JoinLines, window, cx);
4418 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
4419
4420 // We ignore any leading tabs
4421 editor.join_lines(&JoinLines, window, cx);
4422 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
4423
4424 editor
4425 });
4426}
4427
4428#[gpui::test]
4429fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
4430 init_test(cx, |_| {});
4431
4432 cx.add_window(|window, cx| {
4433 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4434 let mut editor = build_editor(buffer.clone(), window, cx);
4435 let buffer = buffer.read(cx).as_singleton().unwrap();
4436
4437 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4438 s.select_ranges([
4439 Point::new(0, 2)..Point::new(1, 1),
4440 Point::new(1, 2)..Point::new(1, 2),
4441 Point::new(3, 1)..Point::new(3, 2),
4442 ])
4443 });
4444
4445 editor.join_lines(&JoinLines, window, cx);
4446 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
4447
4448 assert_eq!(
4449 editor.selections.ranges::<Point>(cx),
4450 [
4451 Point::new(0, 7)..Point::new(0, 7),
4452 Point::new(1, 3)..Point::new(1, 3)
4453 ]
4454 );
4455 editor
4456 });
4457}
4458
4459#[gpui::test]
4460async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
4461 init_test(cx, |_| {});
4462
4463 let mut cx = EditorTestContext::new(cx).await;
4464
4465 let diff_base = r#"
4466 Line 0
4467 Line 1
4468 Line 2
4469 Line 3
4470 "#
4471 .unindent();
4472
4473 cx.set_state(
4474 &r#"
4475 ˇLine 0
4476 Line 1
4477 Line 2
4478 Line 3
4479 "#
4480 .unindent(),
4481 );
4482
4483 cx.set_head_text(&diff_base);
4484 executor.run_until_parked();
4485
4486 // Join lines
4487 cx.update_editor(|editor, window, cx| {
4488 editor.join_lines(&JoinLines, window, cx);
4489 });
4490 executor.run_until_parked();
4491
4492 cx.assert_editor_state(
4493 &r#"
4494 Line 0ˇ Line 1
4495 Line 2
4496 Line 3
4497 "#
4498 .unindent(),
4499 );
4500 // Join again
4501 cx.update_editor(|editor, window, cx| {
4502 editor.join_lines(&JoinLines, window, cx);
4503 });
4504 executor.run_until_parked();
4505
4506 cx.assert_editor_state(
4507 &r#"
4508 Line 0 Line 1ˇ Line 2
4509 Line 3
4510 "#
4511 .unindent(),
4512 );
4513}
4514
4515#[gpui::test]
4516async fn test_custom_newlines_cause_no_false_positive_diffs(
4517 executor: BackgroundExecutor,
4518 cx: &mut TestAppContext,
4519) {
4520 init_test(cx, |_| {});
4521 let mut cx = EditorTestContext::new(cx).await;
4522 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
4523 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
4524 executor.run_until_parked();
4525
4526 cx.update_editor(|editor, window, cx| {
4527 let snapshot = editor.snapshot(window, cx);
4528 assert_eq!(
4529 snapshot
4530 .buffer_snapshot()
4531 .diff_hunks_in_range(0..snapshot.buffer_snapshot().len())
4532 .collect::<Vec<_>>(),
4533 Vec::new(),
4534 "Should not have any diffs for files with custom newlines"
4535 );
4536 });
4537}
4538
4539#[gpui::test]
4540async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppContext) {
4541 init_test(cx, |_| {});
4542
4543 let mut cx = EditorTestContext::new(cx).await;
4544
4545 // Test sort_lines_case_insensitive()
4546 cx.set_state(indoc! {"
4547 «z
4548 y
4549 x
4550 Z
4551 Y
4552 Xˇ»
4553 "});
4554 cx.update_editor(|e, window, cx| {
4555 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
4556 });
4557 cx.assert_editor_state(indoc! {"
4558 «x
4559 X
4560 y
4561 Y
4562 z
4563 Zˇ»
4564 "});
4565
4566 // Test sort_lines_by_length()
4567 //
4568 // Demonstrates:
4569 // - ∞ is 3 bytes UTF-8, but sorted by its char count (1)
4570 // - sort is stable
4571 cx.set_state(indoc! {"
4572 «123
4573 æ
4574 12
4575 ∞
4576 1
4577 æˇ»
4578 "});
4579 cx.update_editor(|e, window, cx| e.sort_lines_by_length(&SortLinesByLength, window, cx));
4580 cx.assert_editor_state(indoc! {"
4581 «æ
4582 ∞
4583 1
4584 æ
4585 12
4586 123ˇ»
4587 "});
4588
4589 // Test reverse_lines()
4590 cx.set_state(indoc! {"
4591 «5
4592 4
4593 3
4594 2
4595 1ˇ»
4596 "});
4597 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4598 cx.assert_editor_state(indoc! {"
4599 «1
4600 2
4601 3
4602 4
4603 5ˇ»
4604 "});
4605
4606 // Skip testing shuffle_line()
4607
4608 // From here on out, test more complex cases of manipulate_immutable_lines() with a single driver method: sort_lines_case_sensitive()
4609 // Since all methods calling manipulate_immutable_lines() are doing the exact same general thing (reordering lines)
4610
4611 // Don't manipulate when cursor is on single line, but expand the selection
4612 cx.set_state(indoc! {"
4613 ddˇdd
4614 ccc
4615 bb
4616 a
4617 "});
4618 cx.update_editor(|e, window, cx| {
4619 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4620 });
4621 cx.assert_editor_state(indoc! {"
4622 «ddddˇ»
4623 ccc
4624 bb
4625 a
4626 "});
4627
4628 // Basic manipulate case
4629 // Start selection moves to column 0
4630 // End of selection shrinks to fit shorter line
4631 cx.set_state(indoc! {"
4632 dd«d
4633 ccc
4634 bb
4635 aaaaaˇ»
4636 "});
4637 cx.update_editor(|e, window, cx| {
4638 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4639 });
4640 cx.assert_editor_state(indoc! {"
4641 «aaaaa
4642 bb
4643 ccc
4644 dddˇ»
4645 "});
4646
4647 // Manipulate case with newlines
4648 cx.set_state(indoc! {"
4649 dd«d
4650 ccc
4651
4652 bb
4653 aaaaa
4654
4655 ˇ»
4656 "});
4657 cx.update_editor(|e, window, cx| {
4658 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4659 });
4660 cx.assert_editor_state(indoc! {"
4661 «
4662
4663 aaaaa
4664 bb
4665 ccc
4666 dddˇ»
4667
4668 "});
4669
4670 // Adding new line
4671 cx.set_state(indoc! {"
4672 aa«a
4673 bbˇ»b
4674 "});
4675 cx.update_editor(|e, window, cx| {
4676 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added_line"))
4677 });
4678 cx.assert_editor_state(indoc! {"
4679 «aaa
4680 bbb
4681 added_lineˇ»
4682 "});
4683
4684 // Removing line
4685 cx.set_state(indoc! {"
4686 aa«a
4687 bbbˇ»
4688 "});
4689 cx.update_editor(|e, window, cx| {
4690 e.manipulate_immutable_lines(window, cx, |lines| {
4691 lines.pop();
4692 })
4693 });
4694 cx.assert_editor_state(indoc! {"
4695 «aaaˇ»
4696 "});
4697
4698 // Removing all lines
4699 cx.set_state(indoc! {"
4700 aa«a
4701 bbbˇ»
4702 "});
4703 cx.update_editor(|e, window, cx| {
4704 e.manipulate_immutable_lines(window, cx, |lines| {
4705 lines.drain(..);
4706 })
4707 });
4708 cx.assert_editor_state(indoc! {"
4709 ˇ
4710 "});
4711}
4712
4713#[gpui::test]
4714async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4715 init_test(cx, |_| {});
4716
4717 let mut cx = EditorTestContext::new(cx).await;
4718
4719 // Consider continuous selection as single selection
4720 cx.set_state(indoc! {"
4721 Aaa«aa
4722 cˇ»c«c
4723 bb
4724 aaaˇ»aa
4725 "});
4726 cx.update_editor(|e, window, cx| {
4727 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4728 });
4729 cx.assert_editor_state(indoc! {"
4730 «Aaaaa
4731 ccc
4732 bb
4733 aaaaaˇ»
4734 "});
4735
4736 cx.set_state(indoc! {"
4737 Aaa«aa
4738 cˇ»c«c
4739 bb
4740 aaaˇ»aa
4741 "});
4742 cx.update_editor(|e, window, cx| {
4743 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4744 });
4745 cx.assert_editor_state(indoc! {"
4746 «Aaaaa
4747 ccc
4748 bbˇ»
4749 "});
4750
4751 // Consider non continuous selection as distinct dedup operations
4752 cx.set_state(indoc! {"
4753 «aaaaa
4754 bb
4755 aaaaa
4756 aaaaaˇ»
4757
4758 aaa«aaˇ»
4759 "});
4760 cx.update_editor(|e, window, cx| {
4761 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4762 });
4763 cx.assert_editor_state(indoc! {"
4764 «aaaaa
4765 bbˇ»
4766
4767 «aaaaaˇ»
4768 "});
4769}
4770
4771#[gpui::test]
4772async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4773 init_test(cx, |_| {});
4774
4775 let mut cx = EditorTestContext::new(cx).await;
4776
4777 cx.set_state(indoc! {"
4778 «Aaa
4779 aAa
4780 Aaaˇ»
4781 "});
4782 cx.update_editor(|e, window, cx| {
4783 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4784 });
4785 cx.assert_editor_state(indoc! {"
4786 «Aaa
4787 aAaˇ»
4788 "});
4789
4790 cx.set_state(indoc! {"
4791 «Aaa
4792 aAa
4793 aaAˇ»
4794 "});
4795 cx.update_editor(|e, window, cx| {
4796 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4797 });
4798 cx.assert_editor_state(indoc! {"
4799 «Aaaˇ»
4800 "});
4801}
4802
4803#[gpui::test]
4804async fn test_wrap_in_tag_single_selection(cx: &mut TestAppContext) {
4805 init_test(cx, |_| {});
4806
4807 let mut cx = EditorTestContext::new(cx).await;
4808
4809 let js_language = Arc::new(Language::new(
4810 LanguageConfig {
4811 name: "JavaScript".into(),
4812 wrap_characters: Some(language::WrapCharactersConfig {
4813 start_prefix: "<".into(),
4814 start_suffix: ">".into(),
4815 end_prefix: "</".into(),
4816 end_suffix: ">".into(),
4817 }),
4818 ..LanguageConfig::default()
4819 },
4820 None,
4821 ));
4822
4823 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
4824
4825 cx.set_state(indoc! {"
4826 «testˇ»
4827 "});
4828 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4829 cx.assert_editor_state(indoc! {"
4830 <«ˇ»>test</«ˇ»>
4831 "});
4832
4833 cx.set_state(indoc! {"
4834 «test
4835 testˇ»
4836 "});
4837 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4838 cx.assert_editor_state(indoc! {"
4839 <«ˇ»>test
4840 test</«ˇ»>
4841 "});
4842
4843 cx.set_state(indoc! {"
4844 teˇst
4845 "});
4846 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4847 cx.assert_editor_state(indoc! {"
4848 te<«ˇ»></«ˇ»>st
4849 "});
4850}
4851
4852#[gpui::test]
4853async fn test_wrap_in_tag_multi_selection(cx: &mut TestAppContext) {
4854 init_test(cx, |_| {});
4855
4856 let mut cx = EditorTestContext::new(cx).await;
4857
4858 let js_language = Arc::new(Language::new(
4859 LanguageConfig {
4860 name: "JavaScript".into(),
4861 wrap_characters: Some(language::WrapCharactersConfig {
4862 start_prefix: "<".into(),
4863 start_suffix: ">".into(),
4864 end_prefix: "</".into(),
4865 end_suffix: ">".into(),
4866 }),
4867 ..LanguageConfig::default()
4868 },
4869 None,
4870 ));
4871
4872 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
4873
4874 cx.set_state(indoc! {"
4875 «testˇ»
4876 «testˇ» «testˇ»
4877 «testˇ»
4878 "});
4879 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4880 cx.assert_editor_state(indoc! {"
4881 <«ˇ»>test</«ˇ»>
4882 <«ˇ»>test</«ˇ»> <«ˇ»>test</«ˇ»>
4883 <«ˇ»>test</«ˇ»>
4884 "});
4885
4886 cx.set_state(indoc! {"
4887 «test
4888 testˇ»
4889 «test
4890 testˇ»
4891 "});
4892 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4893 cx.assert_editor_state(indoc! {"
4894 <«ˇ»>test
4895 test</«ˇ»>
4896 <«ˇ»>test
4897 test</«ˇ»>
4898 "});
4899}
4900
4901#[gpui::test]
4902async fn test_wrap_in_tag_does_nothing_in_unsupported_languages(cx: &mut TestAppContext) {
4903 init_test(cx, |_| {});
4904
4905 let mut cx = EditorTestContext::new(cx).await;
4906
4907 let plaintext_language = Arc::new(Language::new(
4908 LanguageConfig {
4909 name: "Plain Text".into(),
4910 ..LanguageConfig::default()
4911 },
4912 None,
4913 ));
4914
4915 cx.update_buffer(|buffer, cx| buffer.set_language(Some(plaintext_language), cx));
4916
4917 cx.set_state(indoc! {"
4918 «testˇ»
4919 "});
4920 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4921 cx.assert_editor_state(indoc! {"
4922 «testˇ»
4923 "});
4924}
4925
4926#[gpui::test]
4927async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
4928 init_test(cx, |_| {});
4929
4930 let mut cx = EditorTestContext::new(cx).await;
4931
4932 // Manipulate with multiple selections on a single line
4933 cx.set_state(indoc! {"
4934 dd«dd
4935 cˇ»c«c
4936 bb
4937 aaaˇ»aa
4938 "});
4939 cx.update_editor(|e, window, cx| {
4940 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4941 });
4942 cx.assert_editor_state(indoc! {"
4943 «aaaaa
4944 bb
4945 ccc
4946 ddddˇ»
4947 "});
4948
4949 // Manipulate with multiple disjoin selections
4950 cx.set_state(indoc! {"
4951 5«
4952 4
4953 3
4954 2
4955 1ˇ»
4956
4957 dd«dd
4958 ccc
4959 bb
4960 aaaˇ»aa
4961 "});
4962 cx.update_editor(|e, window, cx| {
4963 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4964 });
4965 cx.assert_editor_state(indoc! {"
4966 «1
4967 2
4968 3
4969 4
4970 5ˇ»
4971
4972 «aaaaa
4973 bb
4974 ccc
4975 ddddˇ»
4976 "});
4977
4978 // Adding lines on each selection
4979 cx.set_state(indoc! {"
4980 2«
4981 1ˇ»
4982
4983 bb«bb
4984 aaaˇ»aa
4985 "});
4986 cx.update_editor(|e, window, cx| {
4987 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added line"))
4988 });
4989 cx.assert_editor_state(indoc! {"
4990 «2
4991 1
4992 added lineˇ»
4993
4994 «bbbb
4995 aaaaa
4996 added lineˇ»
4997 "});
4998
4999 // Removing lines on each selection
5000 cx.set_state(indoc! {"
5001 2«
5002 1ˇ»
5003
5004 bb«bb
5005 aaaˇ»aa
5006 "});
5007 cx.update_editor(|e, window, cx| {
5008 e.manipulate_immutable_lines(window, cx, |lines| {
5009 lines.pop();
5010 })
5011 });
5012 cx.assert_editor_state(indoc! {"
5013 «2ˇ»
5014
5015 «bbbbˇ»
5016 "});
5017}
5018
5019#[gpui::test]
5020async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) {
5021 init_test(cx, |settings| {
5022 settings.defaults.tab_size = NonZeroU32::new(3)
5023 });
5024
5025 let mut cx = EditorTestContext::new(cx).await;
5026
5027 // MULTI SELECTION
5028 // Ln.1 "«" tests empty lines
5029 // Ln.9 tests just leading whitespace
5030 cx.set_state(indoc! {"
5031 «
5032 abc // No indentationˇ»
5033 «\tabc // 1 tabˇ»
5034 \t\tabc « ˇ» // 2 tabs
5035 \t ab«c // Tab followed by space
5036 \tabc // Space followed by tab (3 spaces should be the result)
5037 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5038 abˇ»ˇc ˇ ˇ // Already space indented«
5039 \t
5040 \tabc\tdef // Only the leading tab is manipulatedˇ»
5041 "});
5042 cx.update_editor(|e, window, cx| {
5043 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5044 });
5045 cx.assert_editor_state(
5046 indoc! {"
5047 «
5048 abc // No indentation
5049 abc // 1 tab
5050 abc // 2 tabs
5051 abc // Tab followed by space
5052 abc // Space followed by tab (3 spaces should be the result)
5053 abc // Mixed indentation (tab conversion depends on the column)
5054 abc // Already space indented
5055 ·
5056 abc\tdef // Only the leading tab is manipulatedˇ»
5057 "}
5058 .replace("·", "")
5059 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5060 );
5061
5062 // Test on just a few lines, the others should remain unchanged
5063 // Only lines (3, 5, 10, 11) should change
5064 cx.set_state(
5065 indoc! {"
5066 ·
5067 abc // No indentation
5068 \tabcˇ // 1 tab
5069 \t\tabc // 2 tabs
5070 \t abcˇ // Tab followed by space
5071 \tabc // Space followed by tab (3 spaces should be the result)
5072 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5073 abc // Already space indented
5074 «\t
5075 \tabc\tdef // Only the leading tab is manipulatedˇ»
5076 "}
5077 .replace("·", "")
5078 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5079 );
5080 cx.update_editor(|e, window, cx| {
5081 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5082 });
5083 cx.assert_editor_state(
5084 indoc! {"
5085 ·
5086 abc // No indentation
5087 « abc // 1 tabˇ»
5088 \t\tabc // 2 tabs
5089 « abc // Tab followed by spaceˇ»
5090 \tabc // Space followed by tab (3 spaces should be the result)
5091 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5092 abc // Already space indented
5093 « ·
5094 abc\tdef // Only the leading tab is manipulatedˇ»
5095 "}
5096 .replace("·", "")
5097 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5098 );
5099
5100 // SINGLE SELECTION
5101 // Ln.1 "«" tests empty lines
5102 // Ln.9 tests just leading whitespace
5103 cx.set_state(indoc! {"
5104 «
5105 abc // No indentation
5106 \tabc // 1 tab
5107 \t\tabc // 2 tabs
5108 \t abc // Tab followed by space
5109 \tabc // Space followed by tab (3 spaces should be the result)
5110 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5111 abc // Already space indented
5112 \t
5113 \tabc\tdef // Only the leading tab is manipulatedˇ»
5114 "});
5115 cx.update_editor(|e, window, cx| {
5116 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5117 });
5118 cx.assert_editor_state(
5119 indoc! {"
5120 «
5121 abc // No indentation
5122 abc // 1 tab
5123 abc // 2 tabs
5124 abc // Tab followed by space
5125 abc // Space followed by tab (3 spaces should be the result)
5126 abc // Mixed indentation (tab conversion depends on the column)
5127 abc // Already space indented
5128 ·
5129 abc\tdef // Only the leading tab is manipulatedˇ»
5130 "}
5131 .replace("·", "")
5132 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5133 );
5134}
5135
5136#[gpui::test]
5137async fn test_convert_indentation_to_tabs(cx: &mut TestAppContext) {
5138 init_test(cx, |settings| {
5139 settings.defaults.tab_size = NonZeroU32::new(3)
5140 });
5141
5142 let mut cx = EditorTestContext::new(cx).await;
5143
5144 // MULTI SELECTION
5145 // Ln.1 "«" tests empty lines
5146 // Ln.11 tests just leading whitespace
5147 cx.set_state(indoc! {"
5148 «
5149 abˇ»ˇc // No indentation
5150 abc ˇ ˇ // 1 space (< 3 so dont convert)
5151 abc « // 2 spaces (< 3 so dont convert)
5152 abc // 3 spaces (convert)
5153 abc ˇ» // 5 spaces (1 tab + 2 spaces)
5154 «\tˇ»\t«\tˇ»abc // 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 \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5158 \tˇ» «\t
5159 abcˇ» \t ˇˇˇ // Only the leading spaces should be converted
5160 "});
5161 cx.update_editor(|e, window, cx| {
5162 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5163 });
5164 cx.assert_editor_state(indoc! {"
5165 «
5166 abc // No indentation
5167 abc // 1 space (< 3 so dont convert)
5168 abc // 2 spaces (< 3 so dont convert)
5169 \tabc // 3 spaces (convert)
5170 \t abc // 5 spaces (1 tab + 2 spaces)
5171 \t\t\tabc // Already tab indented
5172 \t abc // Tab followed by space
5173 \tabc // Space followed by tab (should be consumed due to tab)
5174 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5175 \t\t\t
5176 \tabc \t // Only the leading spaces should be convertedˇ»
5177 "});
5178
5179 // Test on just a few lines, the other should remain unchanged
5180 // Only lines (4, 8, 11, 12) should change
5181 cx.set_state(
5182 indoc! {"
5183 ·
5184 abc // No indentation
5185 abc // 1 space (< 3 so dont convert)
5186 abc // 2 spaces (< 3 so dont convert)
5187 « abc // 3 spaces (convert)ˇ»
5188 abc // 5 spaces (1 tab + 2 spaces)
5189 \t\t\tabc // Already tab indented
5190 \t abc // Tab followed by space
5191 \tabc ˇ // Space followed by tab (should be consumed due to tab)
5192 \t\t \tabc // Mixed indentation
5193 \t \t \t \tabc // Mixed indentation
5194 \t \tˇ
5195 « abc \t // Only the leading spaces should be convertedˇ»
5196 "}
5197 .replace("·", "")
5198 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5199 );
5200 cx.update_editor(|e, window, cx| {
5201 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5202 });
5203 cx.assert_editor_state(
5204 indoc! {"
5205 ·
5206 abc // No indentation
5207 abc // 1 space (< 3 so dont convert)
5208 abc // 2 spaces (< 3 so dont convert)
5209 «\tabc // 3 spaces (convert)ˇ»
5210 abc // 5 spaces (1 tab + 2 spaces)
5211 \t\t\tabc // Already tab indented
5212 \t abc // Tab followed by space
5213 «\tabc // Space followed by tab (should be consumed due to tab)ˇ»
5214 \t\t \tabc // Mixed indentation
5215 \t \t \t \tabc // Mixed indentation
5216 «\t\t\t
5217 \tabc \t // Only the leading spaces should be convertedˇ»
5218 "}
5219 .replace("·", "")
5220 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5221 );
5222
5223 // SINGLE SELECTION
5224 // Ln.1 "«" tests empty lines
5225 // Ln.11 tests just leading whitespace
5226 cx.set_state(indoc! {"
5227 «
5228 abc // No indentation
5229 abc // 1 space (< 3 so dont convert)
5230 abc // 2 spaces (< 3 so dont convert)
5231 abc // 3 spaces (convert)
5232 abc // 5 spaces (1 tab + 2 spaces)
5233 \t\t\tabc // Already tab indented
5234 \t abc // Tab followed by space
5235 \tabc // Space followed by tab (should be consumed due to tab)
5236 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5237 \t \t
5238 abc \t // Only the leading spaces should be convertedˇ»
5239 "});
5240 cx.update_editor(|e, window, cx| {
5241 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5242 });
5243 cx.assert_editor_state(indoc! {"
5244 «
5245 abc // No indentation
5246 abc // 1 space (< 3 so dont convert)
5247 abc // 2 spaces (< 3 so dont convert)
5248 \tabc // 3 spaces (convert)
5249 \t abc // 5 spaces (1 tab + 2 spaces)
5250 \t\t\tabc // Already tab indented
5251 \t abc // Tab followed by space
5252 \tabc // Space followed by tab (should be consumed due to tab)
5253 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5254 \t\t\t
5255 \tabc \t // Only the leading spaces should be convertedˇ»
5256 "});
5257}
5258
5259#[gpui::test]
5260async fn test_toggle_case(cx: &mut TestAppContext) {
5261 init_test(cx, |_| {});
5262
5263 let mut cx = EditorTestContext::new(cx).await;
5264
5265 // If all lower case -> upper case
5266 cx.set_state(indoc! {"
5267 «hello worldˇ»
5268 "});
5269 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5270 cx.assert_editor_state(indoc! {"
5271 «HELLO WORLDˇ»
5272 "});
5273
5274 // If all upper case -> lower case
5275 cx.set_state(indoc! {"
5276 «HELLO WORLDˇ»
5277 "});
5278 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5279 cx.assert_editor_state(indoc! {"
5280 «hello worldˇ»
5281 "});
5282
5283 // If any upper case characters are identified -> lower case
5284 // This matches JetBrains IDEs
5285 cx.set_state(indoc! {"
5286 «hEllo worldˇ»
5287 "});
5288 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5289 cx.assert_editor_state(indoc! {"
5290 «hello worldˇ»
5291 "});
5292}
5293
5294#[gpui::test]
5295async fn test_convert_to_sentence_case(cx: &mut TestAppContext) {
5296 init_test(cx, |_| {});
5297
5298 let mut cx = EditorTestContext::new(cx).await;
5299
5300 cx.set_state(indoc! {"
5301 «implement-windows-supportˇ»
5302 "});
5303 cx.update_editor(|e, window, cx| {
5304 e.convert_to_sentence_case(&ConvertToSentenceCase, window, cx)
5305 });
5306 cx.assert_editor_state(indoc! {"
5307 «Implement windows supportˇ»
5308 "});
5309}
5310
5311#[gpui::test]
5312async fn test_manipulate_text(cx: &mut TestAppContext) {
5313 init_test(cx, |_| {});
5314
5315 let mut cx = EditorTestContext::new(cx).await;
5316
5317 // Test convert_to_upper_case()
5318 cx.set_state(indoc! {"
5319 «hello worldˇ»
5320 "});
5321 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5322 cx.assert_editor_state(indoc! {"
5323 «HELLO WORLDˇ»
5324 "});
5325
5326 // Test convert_to_lower_case()
5327 cx.set_state(indoc! {"
5328 «HELLO WORLDˇ»
5329 "});
5330 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
5331 cx.assert_editor_state(indoc! {"
5332 «hello worldˇ»
5333 "});
5334
5335 // Test multiple line, single selection case
5336 cx.set_state(indoc! {"
5337 «The quick brown
5338 fox jumps over
5339 the lazy dogˇ»
5340 "});
5341 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
5342 cx.assert_editor_state(indoc! {"
5343 «The Quick Brown
5344 Fox Jumps Over
5345 The Lazy Dogˇ»
5346 "});
5347
5348 // Test multiple line, single selection case
5349 cx.set_state(indoc! {"
5350 «The quick brown
5351 fox jumps over
5352 the lazy dogˇ»
5353 "});
5354 cx.update_editor(|e, window, cx| {
5355 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
5356 });
5357 cx.assert_editor_state(indoc! {"
5358 «TheQuickBrown
5359 FoxJumpsOver
5360 TheLazyDogˇ»
5361 "});
5362
5363 // From here on out, test more complex cases of manipulate_text()
5364
5365 // Test no selection case - should affect words cursors are in
5366 // Cursor at beginning, middle, and end of word
5367 cx.set_state(indoc! {"
5368 ˇhello big beauˇtiful worldˇ
5369 "});
5370 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5371 cx.assert_editor_state(indoc! {"
5372 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
5373 "});
5374
5375 // Test multiple selections on a single line and across multiple lines
5376 cx.set_state(indoc! {"
5377 «Theˇ» quick «brown
5378 foxˇ» jumps «overˇ»
5379 the «lazyˇ» dog
5380 "});
5381 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5382 cx.assert_editor_state(indoc! {"
5383 «THEˇ» quick «BROWN
5384 FOXˇ» jumps «OVERˇ»
5385 the «LAZYˇ» dog
5386 "});
5387
5388 // Test case where text length grows
5389 cx.set_state(indoc! {"
5390 «tschüߡ»
5391 "});
5392 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5393 cx.assert_editor_state(indoc! {"
5394 «TSCHÜSSˇ»
5395 "});
5396
5397 // Test to make sure we don't crash when text shrinks
5398 cx.set_state(indoc! {"
5399 aaa_bbbˇ
5400 "});
5401 cx.update_editor(|e, window, cx| {
5402 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5403 });
5404 cx.assert_editor_state(indoc! {"
5405 «aaaBbbˇ»
5406 "});
5407
5408 // Test to make sure we all aware of the fact that each word can grow and shrink
5409 // Final selections should be aware of this fact
5410 cx.set_state(indoc! {"
5411 aaa_bˇbb bbˇb_ccc ˇccc_ddd
5412 "});
5413 cx.update_editor(|e, window, cx| {
5414 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5415 });
5416 cx.assert_editor_state(indoc! {"
5417 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
5418 "});
5419
5420 cx.set_state(indoc! {"
5421 «hElLo, WoRld!ˇ»
5422 "});
5423 cx.update_editor(|e, window, cx| {
5424 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
5425 });
5426 cx.assert_editor_state(indoc! {"
5427 «HeLlO, wOrLD!ˇ»
5428 "});
5429
5430 // Test selections with `line_mode() = true`.
5431 cx.update_editor(|editor, _window, _cx| editor.selections.set_line_mode(true));
5432 cx.set_state(indoc! {"
5433 «The quick brown
5434 fox jumps over
5435 tˇ»he lazy dog
5436 "});
5437 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5438 cx.assert_editor_state(indoc! {"
5439 «THE QUICK BROWN
5440 FOX JUMPS OVER
5441 THE LAZY DOGˇ»
5442 "});
5443}
5444
5445#[gpui::test]
5446fn test_duplicate_line(cx: &mut TestAppContext) {
5447 init_test(cx, |_| {});
5448
5449 let editor = cx.add_window(|window, cx| {
5450 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5451 build_editor(buffer, window, cx)
5452 });
5453 _ = editor.update(cx, |editor, window, cx| {
5454 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5455 s.select_display_ranges([
5456 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5457 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5458 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5459 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5460 ])
5461 });
5462 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5463 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5464 assert_eq!(
5465 editor.selections.display_ranges(cx),
5466 vec![
5467 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
5468 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
5469 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5470 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
5471 ]
5472 );
5473 });
5474
5475 let editor = cx.add_window(|window, cx| {
5476 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5477 build_editor(buffer, window, cx)
5478 });
5479 _ = editor.update(cx, |editor, window, cx| {
5480 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5481 s.select_display_ranges([
5482 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5483 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5484 ])
5485 });
5486 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5487 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5488 assert_eq!(
5489 editor.selections.display_ranges(cx),
5490 vec![
5491 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
5492 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
5493 ]
5494 );
5495 });
5496
5497 // With `move_upwards` the selections stay in place, except for
5498 // the lines inserted above them
5499 let editor = cx.add_window(|window, cx| {
5500 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5501 build_editor(buffer, window, cx)
5502 });
5503 _ = editor.update(cx, |editor, window, cx| {
5504 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5505 s.select_display_ranges([
5506 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5507 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5508 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5509 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5510 ])
5511 });
5512 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5513 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5514 assert_eq!(
5515 editor.selections.display_ranges(cx),
5516 vec![
5517 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5518 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5519 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
5520 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
5521 ]
5522 );
5523 });
5524
5525 let editor = cx.add_window(|window, cx| {
5526 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5527 build_editor(buffer, window, cx)
5528 });
5529 _ = editor.update(cx, |editor, window, cx| {
5530 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5531 s.select_display_ranges([
5532 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5533 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5534 ])
5535 });
5536 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5537 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5538 assert_eq!(
5539 editor.selections.display_ranges(cx),
5540 vec![
5541 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5542 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5543 ]
5544 );
5545 });
5546
5547 let editor = cx.add_window(|window, cx| {
5548 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5549 build_editor(buffer, window, cx)
5550 });
5551 _ = editor.update(cx, |editor, window, cx| {
5552 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5553 s.select_display_ranges([
5554 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5555 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5556 ])
5557 });
5558 editor.duplicate_selection(&DuplicateSelection, window, cx);
5559 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
5560 assert_eq!(
5561 editor.selections.display_ranges(cx),
5562 vec![
5563 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5564 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
5565 ]
5566 );
5567 });
5568}
5569
5570#[gpui::test]
5571fn test_move_line_up_down(cx: &mut TestAppContext) {
5572 init_test(cx, |_| {});
5573
5574 let editor = cx.add_window(|window, cx| {
5575 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5576 build_editor(buffer, window, cx)
5577 });
5578 _ = editor.update(cx, |editor, window, cx| {
5579 editor.fold_creases(
5580 vec![
5581 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5582 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5583 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5584 ],
5585 true,
5586 window,
5587 cx,
5588 );
5589 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5590 s.select_display_ranges([
5591 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5592 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5593 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5594 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
5595 ])
5596 });
5597 assert_eq!(
5598 editor.display_text(cx),
5599 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
5600 );
5601
5602 editor.move_line_up(&MoveLineUp, window, cx);
5603 assert_eq!(
5604 editor.display_text(cx),
5605 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
5606 );
5607 assert_eq!(
5608 editor.selections.display_ranges(cx),
5609 vec![
5610 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5611 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5612 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5613 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5614 ]
5615 );
5616 });
5617
5618 _ = editor.update(cx, |editor, window, cx| {
5619 editor.move_line_down(&MoveLineDown, window, cx);
5620 assert_eq!(
5621 editor.display_text(cx),
5622 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
5623 );
5624 assert_eq!(
5625 editor.selections.display_ranges(cx),
5626 vec![
5627 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5628 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5629 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5630 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5631 ]
5632 );
5633 });
5634
5635 _ = editor.update(cx, |editor, window, cx| {
5636 editor.move_line_down(&MoveLineDown, window, cx);
5637 assert_eq!(
5638 editor.display_text(cx),
5639 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
5640 );
5641 assert_eq!(
5642 editor.selections.display_ranges(cx),
5643 vec![
5644 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5645 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5646 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5647 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5648 ]
5649 );
5650 });
5651
5652 _ = editor.update(cx, |editor, window, cx| {
5653 editor.move_line_up(&MoveLineUp, window, cx);
5654 assert_eq!(
5655 editor.display_text(cx),
5656 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
5657 );
5658 assert_eq!(
5659 editor.selections.display_ranges(cx),
5660 vec![
5661 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5662 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5663 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5664 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5665 ]
5666 );
5667 });
5668}
5669
5670#[gpui::test]
5671fn test_move_line_up_selection_at_end_of_fold(cx: &mut TestAppContext) {
5672 init_test(cx, |_| {});
5673 let editor = cx.add_window(|window, cx| {
5674 let buffer = MultiBuffer::build_simple("\n\n\n\n\n\naaaa\nbbbb\ncccc", cx);
5675 build_editor(buffer, window, cx)
5676 });
5677 _ = editor.update(cx, |editor, window, cx| {
5678 editor.fold_creases(
5679 vec![Crease::simple(
5680 Point::new(6, 4)..Point::new(7, 4),
5681 FoldPlaceholder::test(),
5682 )],
5683 true,
5684 window,
5685 cx,
5686 );
5687 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5688 s.select_ranges([Point::new(7, 4)..Point::new(7, 4)])
5689 });
5690 assert_eq!(editor.display_text(cx), "\n\n\n\n\n\naaaa⋯\ncccc");
5691 editor.move_line_up(&MoveLineUp, window, cx);
5692 let buffer_text = editor.buffer.read(cx).snapshot(cx).text();
5693 assert_eq!(buffer_text, "\n\n\n\n\naaaa\nbbbb\n\ncccc");
5694 });
5695}
5696
5697#[gpui::test]
5698fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
5699 init_test(cx, |_| {});
5700
5701 let editor = cx.add_window(|window, cx| {
5702 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5703 build_editor(buffer, window, cx)
5704 });
5705 _ = editor.update(cx, |editor, window, cx| {
5706 let snapshot = editor.buffer.read(cx).snapshot(cx);
5707 editor.insert_blocks(
5708 [BlockProperties {
5709 style: BlockStyle::Fixed,
5710 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
5711 height: Some(1),
5712 render: Arc::new(|_| div().into_any()),
5713 priority: 0,
5714 }],
5715 Some(Autoscroll::fit()),
5716 cx,
5717 );
5718 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5719 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
5720 });
5721 editor.move_line_down(&MoveLineDown, window, cx);
5722 });
5723}
5724
5725#[gpui::test]
5726async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
5727 init_test(cx, |_| {});
5728
5729 let mut cx = EditorTestContext::new(cx).await;
5730 cx.set_state(
5731 &"
5732 ˇzero
5733 one
5734 two
5735 three
5736 four
5737 five
5738 "
5739 .unindent(),
5740 );
5741
5742 // Create a four-line block that replaces three lines of text.
5743 cx.update_editor(|editor, window, cx| {
5744 let snapshot = editor.snapshot(window, cx);
5745 let snapshot = &snapshot.buffer_snapshot();
5746 let placement = BlockPlacement::Replace(
5747 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
5748 );
5749 editor.insert_blocks(
5750 [BlockProperties {
5751 placement,
5752 height: Some(4),
5753 style: BlockStyle::Sticky,
5754 render: Arc::new(|_| gpui::div().into_any_element()),
5755 priority: 0,
5756 }],
5757 None,
5758 cx,
5759 );
5760 });
5761
5762 // Move down so that the cursor touches the block.
5763 cx.update_editor(|editor, window, cx| {
5764 editor.move_down(&Default::default(), window, cx);
5765 });
5766 cx.assert_editor_state(
5767 &"
5768 zero
5769 «one
5770 two
5771 threeˇ»
5772 four
5773 five
5774 "
5775 .unindent(),
5776 );
5777
5778 // Move down past the block.
5779 cx.update_editor(|editor, window, cx| {
5780 editor.move_down(&Default::default(), window, cx);
5781 });
5782 cx.assert_editor_state(
5783 &"
5784 zero
5785 one
5786 two
5787 three
5788 ˇfour
5789 five
5790 "
5791 .unindent(),
5792 );
5793}
5794
5795#[gpui::test]
5796fn test_transpose(cx: &mut TestAppContext) {
5797 init_test(cx, |_| {});
5798
5799 _ = cx.add_window(|window, cx| {
5800 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
5801 editor.set_style(EditorStyle::default(), window, cx);
5802 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5803 s.select_ranges([1..1])
5804 });
5805 editor.transpose(&Default::default(), window, cx);
5806 assert_eq!(editor.text(cx), "bac");
5807 assert_eq!(editor.selections.ranges(cx), [2..2]);
5808
5809 editor.transpose(&Default::default(), window, cx);
5810 assert_eq!(editor.text(cx), "bca");
5811 assert_eq!(editor.selections.ranges(cx), [3..3]);
5812
5813 editor.transpose(&Default::default(), window, cx);
5814 assert_eq!(editor.text(cx), "bac");
5815 assert_eq!(editor.selections.ranges(cx), [3..3]);
5816
5817 editor
5818 });
5819
5820 _ = cx.add_window(|window, cx| {
5821 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", 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([3..3])
5825 });
5826 editor.transpose(&Default::default(), window, cx);
5827 assert_eq!(editor.text(cx), "acb\nde");
5828 assert_eq!(editor.selections.ranges(cx), [3..3]);
5829
5830 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5831 s.select_ranges([4..4])
5832 });
5833 editor.transpose(&Default::default(), window, cx);
5834 assert_eq!(editor.text(cx), "acbd\ne");
5835 assert_eq!(editor.selections.ranges(cx), [5..5]);
5836
5837 editor.transpose(&Default::default(), window, cx);
5838 assert_eq!(editor.text(cx), "acbde\n");
5839 assert_eq!(editor.selections.ranges(cx), [6..6]);
5840
5841 editor.transpose(&Default::default(), window, cx);
5842 assert_eq!(editor.text(cx), "acbd\ne");
5843 assert_eq!(editor.selections.ranges(cx), [6..6]);
5844
5845 editor
5846 });
5847
5848 _ = cx.add_window(|window, cx| {
5849 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5850 editor.set_style(EditorStyle::default(), window, cx);
5851 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5852 s.select_ranges([1..1, 2..2, 4..4])
5853 });
5854 editor.transpose(&Default::default(), window, cx);
5855 assert_eq!(editor.text(cx), "bacd\ne");
5856 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
5857
5858 editor.transpose(&Default::default(), window, cx);
5859 assert_eq!(editor.text(cx), "bcade\n");
5860 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
5861
5862 editor.transpose(&Default::default(), window, cx);
5863 assert_eq!(editor.text(cx), "bcda\ne");
5864 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5865
5866 editor.transpose(&Default::default(), window, cx);
5867 assert_eq!(editor.text(cx), "bcade\n");
5868 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5869
5870 editor.transpose(&Default::default(), window, cx);
5871 assert_eq!(editor.text(cx), "bcaed\n");
5872 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
5873
5874 editor
5875 });
5876
5877 _ = cx.add_window(|window, cx| {
5878 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
5879 editor.set_style(EditorStyle::default(), window, cx);
5880 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5881 s.select_ranges([4..4])
5882 });
5883 editor.transpose(&Default::default(), window, cx);
5884 assert_eq!(editor.text(cx), "🏀🍐✋");
5885 assert_eq!(editor.selections.ranges(cx), [8..8]);
5886
5887 editor.transpose(&Default::default(), window, cx);
5888 assert_eq!(editor.text(cx), "🏀✋🍐");
5889 assert_eq!(editor.selections.ranges(cx), [11..11]);
5890
5891 editor.transpose(&Default::default(), window, cx);
5892 assert_eq!(editor.text(cx), "🏀🍐✋");
5893 assert_eq!(editor.selections.ranges(cx), [11..11]);
5894
5895 editor
5896 });
5897}
5898
5899#[gpui::test]
5900async fn test_rewrap(cx: &mut TestAppContext) {
5901 init_test(cx, |settings| {
5902 settings.languages.0.extend([
5903 (
5904 "Markdown".into(),
5905 LanguageSettingsContent {
5906 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5907 preferred_line_length: Some(40),
5908 ..Default::default()
5909 },
5910 ),
5911 (
5912 "Plain Text".into(),
5913 LanguageSettingsContent {
5914 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5915 preferred_line_length: Some(40),
5916 ..Default::default()
5917 },
5918 ),
5919 (
5920 "C++".into(),
5921 LanguageSettingsContent {
5922 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5923 preferred_line_length: Some(40),
5924 ..Default::default()
5925 },
5926 ),
5927 (
5928 "Python".into(),
5929 LanguageSettingsContent {
5930 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5931 preferred_line_length: Some(40),
5932 ..Default::default()
5933 },
5934 ),
5935 (
5936 "Rust".into(),
5937 LanguageSettingsContent {
5938 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5939 preferred_line_length: Some(40),
5940 ..Default::default()
5941 },
5942 ),
5943 ])
5944 });
5945
5946 let mut cx = EditorTestContext::new(cx).await;
5947
5948 let cpp_language = Arc::new(Language::new(
5949 LanguageConfig {
5950 name: "C++".into(),
5951 line_comments: vec!["// ".into()],
5952 ..LanguageConfig::default()
5953 },
5954 None,
5955 ));
5956 let python_language = Arc::new(Language::new(
5957 LanguageConfig {
5958 name: "Python".into(),
5959 line_comments: vec!["# ".into()],
5960 ..LanguageConfig::default()
5961 },
5962 None,
5963 ));
5964 let markdown_language = Arc::new(Language::new(
5965 LanguageConfig {
5966 name: "Markdown".into(),
5967 rewrap_prefixes: vec![
5968 regex::Regex::new("\\d+\\.\\s+").unwrap(),
5969 regex::Regex::new("[-*+]\\s+").unwrap(),
5970 ],
5971 ..LanguageConfig::default()
5972 },
5973 None,
5974 ));
5975 let rust_language = Arc::new(
5976 Language::new(
5977 LanguageConfig {
5978 name: "Rust".into(),
5979 line_comments: vec!["// ".into(), "/// ".into()],
5980 ..LanguageConfig::default()
5981 },
5982 Some(tree_sitter_rust::LANGUAGE.into()),
5983 )
5984 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
5985 .unwrap(),
5986 );
5987
5988 let plaintext_language = Arc::new(Language::new(
5989 LanguageConfig {
5990 name: "Plain Text".into(),
5991 ..LanguageConfig::default()
5992 },
5993 None,
5994 ));
5995
5996 // Test basic rewrapping of a long line with a cursor
5997 assert_rewrap(
5998 indoc! {"
5999 // ˇThis is a long comment that needs to be wrapped.
6000 "},
6001 indoc! {"
6002 // ˇThis is a long comment that needs to
6003 // be wrapped.
6004 "},
6005 cpp_language.clone(),
6006 &mut cx,
6007 );
6008
6009 // Test rewrapping a full selection
6010 assert_rewrap(
6011 indoc! {"
6012 «// This selected long comment needs to be wrapped.ˇ»"
6013 },
6014 indoc! {"
6015 «// This selected long comment needs to
6016 // be wrapped.ˇ»"
6017 },
6018 cpp_language.clone(),
6019 &mut cx,
6020 );
6021
6022 // Test multiple cursors on different lines within the same paragraph are preserved after rewrapping
6023 assert_rewrap(
6024 indoc! {"
6025 // ˇThis is the first line.
6026 // Thisˇ is the second line.
6027 // This is the thirdˇ line, all part of one paragraph.
6028 "},
6029 indoc! {"
6030 // ˇThis is the first line. Thisˇ is the
6031 // second line. This is the thirdˇ line,
6032 // all part of one paragraph.
6033 "},
6034 cpp_language.clone(),
6035 &mut cx,
6036 );
6037
6038 // Test multiple cursors in different paragraphs trigger separate rewraps
6039 assert_rewrap(
6040 indoc! {"
6041 // ˇThis is the first paragraph, first line.
6042 // ˇThis is the first paragraph, second line.
6043
6044 // ˇThis is the second paragraph, first line.
6045 // ˇThis is the second paragraph, second line.
6046 "},
6047 indoc! {"
6048 // ˇThis is the first paragraph, first
6049 // line. ˇThis is the first paragraph,
6050 // second line.
6051
6052 // ˇThis is the second paragraph, first
6053 // line. ˇThis is the second paragraph,
6054 // second line.
6055 "},
6056 cpp_language.clone(),
6057 &mut cx,
6058 );
6059
6060 // Test that change in comment prefix (e.g., `//` to `///`) trigger seperate rewraps
6061 assert_rewrap(
6062 indoc! {"
6063 «// A regular long long comment to be wrapped.
6064 /// A documentation long comment to be wrapped.ˇ»
6065 "},
6066 indoc! {"
6067 «// A regular long long comment to be
6068 // wrapped.
6069 /// A documentation long comment to be
6070 /// wrapped.ˇ»
6071 "},
6072 rust_language.clone(),
6073 &mut cx,
6074 );
6075
6076 // Test that change in indentation level trigger seperate rewraps
6077 assert_rewrap(
6078 indoc! {"
6079 fn foo() {
6080 «// This is a long comment at the base indent.
6081 // This is a long comment at the next indent.ˇ»
6082 }
6083 "},
6084 indoc! {"
6085 fn foo() {
6086 «// This is a long comment at the
6087 // base indent.
6088 // This is a long comment at the
6089 // next indent.ˇ»
6090 }
6091 "},
6092 rust_language.clone(),
6093 &mut cx,
6094 );
6095
6096 // Test that different comment prefix characters (e.g., '#') are handled correctly
6097 assert_rewrap(
6098 indoc! {"
6099 # ˇThis is a long comment using a pound sign.
6100 "},
6101 indoc! {"
6102 # ˇThis is a long comment using a pound
6103 # sign.
6104 "},
6105 python_language,
6106 &mut cx,
6107 );
6108
6109 // Test rewrapping only affects comments, not code even when selected
6110 assert_rewrap(
6111 indoc! {"
6112 «/// This doc comment is long and should be wrapped.
6113 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6114 "},
6115 indoc! {"
6116 «/// This doc comment is long and should
6117 /// be wrapped.
6118 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6119 "},
6120 rust_language.clone(),
6121 &mut cx,
6122 );
6123
6124 // Test that rewrapping works in Markdown documents where `allow_rewrap` is `Anywhere`
6125 assert_rewrap(
6126 indoc! {"
6127 # Header
6128
6129 A long long long line of markdown text to wrap.ˇ
6130 "},
6131 indoc! {"
6132 # Header
6133
6134 A long long long line of markdown text
6135 to wrap.ˇ
6136 "},
6137 markdown_language.clone(),
6138 &mut cx,
6139 );
6140
6141 // Test that rewrapping boundary works and preserves relative indent for Markdown documents
6142 assert_rewrap(
6143 indoc! {"
6144 «1. This is a numbered list item that is very long and needs to be wrapped properly.
6145 2. This is a numbered list item that is very long and needs to be wrapped properly.
6146 - This is an unordered list item that is also very long and should not merge with the numbered item.ˇ»
6147 "},
6148 indoc! {"
6149 «1. This is a numbered list item that is
6150 very long and needs to be wrapped
6151 properly.
6152 2. This is a numbered list item that is
6153 very long and needs to be wrapped
6154 properly.
6155 - This is an unordered list item that is
6156 also very long and should not merge
6157 with the numbered item.ˇ»
6158 "},
6159 markdown_language.clone(),
6160 &mut cx,
6161 );
6162
6163 // Test that rewrapping add indents for rewrapping boundary if not exists already.
6164 assert_rewrap(
6165 indoc! {"
6166 «1. This is a numbered list item that is
6167 very long and needs to be wrapped
6168 properly.
6169 2. This is a numbered list item that is
6170 very long and needs to be wrapped
6171 properly.
6172 - This is an unordered list item that is
6173 also very long and should not merge with
6174 the numbered item.ˇ»
6175 "},
6176 indoc! {"
6177 «1. This is a numbered list item that is
6178 very long and needs to be wrapped
6179 properly.
6180 2. This is a numbered list item that is
6181 very long and needs to be wrapped
6182 properly.
6183 - This is an unordered list item that is
6184 also very long and should not merge
6185 with the numbered item.ˇ»
6186 "},
6187 markdown_language.clone(),
6188 &mut cx,
6189 );
6190
6191 // Test that rewrapping maintain indents even when they already exists.
6192 assert_rewrap(
6193 indoc! {"
6194 «1. This is a numbered list
6195 item that is very long and needs to be wrapped properly.
6196 2. This is a numbered list
6197 item that is very long and needs to be wrapped properly.
6198 - This is an unordered list item that is also very long and
6199 should not merge with the numbered item.ˇ»
6200 "},
6201 indoc! {"
6202 «1. This is a numbered list item that is
6203 very long and needs to be wrapped
6204 properly.
6205 2. This is a numbered list item that is
6206 very long and needs to be wrapped
6207 properly.
6208 - This is an unordered list item that is
6209 also very long and should not merge
6210 with the numbered item.ˇ»
6211 "},
6212 markdown_language,
6213 &mut cx,
6214 );
6215
6216 // Test that rewrapping works in plain text where `allow_rewrap` is `Anywhere`
6217 assert_rewrap(
6218 indoc! {"
6219 ˇThis is a very long line of plain text that will be wrapped.
6220 "},
6221 indoc! {"
6222 ˇThis is a very long line of plain text
6223 that will be wrapped.
6224 "},
6225 plaintext_language.clone(),
6226 &mut cx,
6227 );
6228
6229 // Test that non-commented code acts as a paragraph boundary within a selection
6230 assert_rewrap(
6231 indoc! {"
6232 «// This is the first long comment block to be wrapped.
6233 fn my_func(a: u32);
6234 // This is the second long comment block to be wrapped.ˇ»
6235 "},
6236 indoc! {"
6237 «// This is the first long comment block
6238 // to be wrapped.
6239 fn my_func(a: u32);
6240 // This is the second long comment block
6241 // to be wrapped.ˇ»
6242 "},
6243 rust_language,
6244 &mut cx,
6245 );
6246
6247 // Test rewrapping multiple selections, including ones with blank lines or tabs
6248 assert_rewrap(
6249 indoc! {"
6250 «ˇThis is a very long line that will be wrapped.
6251
6252 This is another paragraph in the same selection.»
6253
6254 «\tThis is a very long indented line that will be wrapped.ˇ»
6255 "},
6256 indoc! {"
6257 «ˇThis is a very long line that will be
6258 wrapped.
6259
6260 This is another paragraph in the same
6261 selection.»
6262
6263 «\tThis is a very long indented line
6264 \tthat will be wrapped.ˇ»
6265 "},
6266 plaintext_language,
6267 &mut cx,
6268 );
6269
6270 // Test that an empty comment line acts as a paragraph boundary
6271 assert_rewrap(
6272 indoc! {"
6273 // ˇThis is a long comment that will be wrapped.
6274 //
6275 // And this is another long comment that will also be wrapped.ˇ
6276 "},
6277 indoc! {"
6278 // ˇThis is a long comment that will be
6279 // wrapped.
6280 //
6281 // And this is another long comment that
6282 // will also be wrapped.ˇ
6283 "},
6284 cpp_language,
6285 &mut cx,
6286 );
6287
6288 #[track_caller]
6289 fn assert_rewrap(
6290 unwrapped_text: &str,
6291 wrapped_text: &str,
6292 language: Arc<Language>,
6293 cx: &mut EditorTestContext,
6294 ) {
6295 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6296 cx.set_state(unwrapped_text);
6297 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6298 cx.assert_editor_state(wrapped_text);
6299 }
6300}
6301
6302#[gpui::test]
6303async fn test_rewrap_block_comments(cx: &mut TestAppContext) {
6304 init_test(cx, |settings| {
6305 settings.languages.0.extend([(
6306 "Rust".into(),
6307 LanguageSettingsContent {
6308 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6309 preferred_line_length: Some(40),
6310 ..Default::default()
6311 },
6312 )])
6313 });
6314
6315 let mut cx = EditorTestContext::new(cx).await;
6316
6317 let rust_lang = Arc::new(
6318 Language::new(
6319 LanguageConfig {
6320 name: "Rust".into(),
6321 line_comments: vec!["// ".into()],
6322 block_comment: Some(BlockCommentConfig {
6323 start: "/*".into(),
6324 end: "*/".into(),
6325 prefix: "* ".into(),
6326 tab_size: 1,
6327 }),
6328 documentation_comment: Some(BlockCommentConfig {
6329 start: "/**".into(),
6330 end: "*/".into(),
6331 prefix: "* ".into(),
6332 tab_size: 1,
6333 }),
6334
6335 ..LanguageConfig::default()
6336 },
6337 Some(tree_sitter_rust::LANGUAGE.into()),
6338 )
6339 .with_override_query("[(line_comment) (block_comment)] @comment.inclusive")
6340 .unwrap(),
6341 );
6342
6343 // regular block comment
6344 assert_rewrap(
6345 indoc! {"
6346 /*
6347 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6348 */
6349 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6350 "},
6351 indoc! {"
6352 /*
6353 *ˇ Lorem ipsum dolor sit amet,
6354 * consectetur adipiscing elit.
6355 */
6356 /*
6357 *ˇ Lorem ipsum dolor sit amet,
6358 * consectetur adipiscing elit.
6359 */
6360 "},
6361 rust_lang.clone(),
6362 &mut cx,
6363 );
6364
6365 // indent is respected
6366 assert_rewrap(
6367 indoc! {"
6368 {}
6369 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6370 "},
6371 indoc! {"
6372 {}
6373 /*
6374 *ˇ Lorem ipsum dolor sit amet,
6375 * consectetur adipiscing elit.
6376 */
6377 "},
6378 rust_lang.clone(),
6379 &mut cx,
6380 );
6381
6382 // short block comments with inline delimiters
6383 assert_rewrap(
6384 indoc! {"
6385 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6386 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6387 */
6388 /*
6389 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6390 "},
6391 indoc! {"
6392 /*
6393 *ˇ Lorem ipsum dolor sit amet,
6394 * consectetur adipiscing elit.
6395 */
6396 /*
6397 *ˇ Lorem ipsum dolor sit amet,
6398 * consectetur adipiscing elit.
6399 */
6400 /*
6401 *ˇ Lorem ipsum dolor sit amet,
6402 * consectetur adipiscing elit.
6403 */
6404 "},
6405 rust_lang.clone(),
6406 &mut cx,
6407 );
6408
6409 // multiline block comment with inline start/end delimiters
6410 assert_rewrap(
6411 indoc! {"
6412 /*ˇ Lorem ipsum dolor sit amet,
6413 * consectetur adipiscing elit. */
6414 "},
6415 indoc! {"
6416 /*
6417 *ˇ Lorem ipsum dolor sit amet,
6418 * consectetur adipiscing elit.
6419 */
6420 "},
6421 rust_lang.clone(),
6422 &mut cx,
6423 );
6424
6425 // block comment rewrap still respects paragraph bounds
6426 assert_rewrap(
6427 indoc! {"
6428 /*
6429 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6430 *
6431 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6432 */
6433 "},
6434 indoc! {"
6435 /*
6436 *ˇ Lorem ipsum dolor sit amet,
6437 * consectetur adipiscing elit.
6438 *
6439 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6440 */
6441 "},
6442 rust_lang.clone(),
6443 &mut cx,
6444 );
6445
6446 // documentation comments
6447 assert_rewrap(
6448 indoc! {"
6449 /**ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6450 /**
6451 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6452 */
6453 "},
6454 indoc! {"
6455 /**
6456 *ˇ Lorem ipsum dolor sit amet,
6457 * consectetur adipiscing elit.
6458 */
6459 /**
6460 *ˇ Lorem ipsum dolor sit amet,
6461 * consectetur adipiscing elit.
6462 */
6463 "},
6464 rust_lang.clone(),
6465 &mut cx,
6466 );
6467
6468 // different, adjacent comments
6469 assert_rewrap(
6470 indoc! {"
6471 /**
6472 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6473 */
6474 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6475 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6476 "},
6477 indoc! {"
6478 /**
6479 *ˇ Lorem ipsum dolor sit amet,
6480 * consectetur adipiscing elit.
6481 */
6482 /*
6483 *ˇ Lorem ipsum dolor sit amet,
6484 * consectetur adipiscing elit.
6485 */
6486 //ˇ Lorem ipsum dolor sit amet,
6487 // consectetur adipiscing elit.
6488 "},
6489 rust_lang.clone(),
6490 &mut cx,
6491 );
6492
6493 // selection w/ single short block comment
6494 assert_rewrap(
6495 indoc! {"
6496 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6497 "},
6498 indoc! {"
6499 «/*
6500 * Lorem ipsum dolor sit amet,
6501 * consectetur adipiscing elit.
6502 */ˇ»
6503 "},
6504 rust_lang.clone(),
6505 &mut cx,
6506 );
6507
6508 // rewrapping a single comment w/ abutting comments
6509 assert_rewrap(
6510 indoc! {"
6511 /* ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. */
6512 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6513 "},
6514 indoc! {"
6515 /*
6516 * ˇLorem ipsum dolor sit amet,
6517 * consectetur adipiscing elit.
6518 */
6519 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6520 "},
6521 rust_lang.clone(),
6522 &mut cx,
6523 );
6524
6525 // selection w/ non-abutting short block comments
6526 assert_rewrap(
6527 indoc! {"
6528 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6529
6530 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6531 "},
6532 indoc! {"
6533 «/*
6534 * Lorem ipsum dolor sit amet,
6535 * consectetur adipiscing elit.
6536 */
6537
6538 /*
6539 * Lorem ipsum dolor sit amet,
6540 * consectetur adipiscing elit.
6541 */ˇ»
6542 "},
6543 rust_lang.clone(),
6544 &mut cx,
6545 );
6546
6547 // selection of multiline block comments
6548 assert_rewrap(
6549 indoc! {"
6550 «/* Lorem ipsum dolor sit amet,
6551 * consectetur adipiscing elit. */ˇ»
6552 "},
6553 indoc! {"
6554 «/*
6555 * Lorem ipsum dolor sit amet,
6556 * consectetur adipiscing elit.
6557 */ˇ»
6558 "},
6559 rust_lang.clone(),
6560 &mut cx,
6561 );
6562
6563 // partial selection of multiline block comments
6564 assert_rewrap(
6565 indoc! {"
6566 «/* Lorem ipsum dolor sit amet,ˇ»
6567 * consectetur adipiscing elit. */
6568 /* Lorem ipsum dolor sit amet,
6569 «* consectetur adipiscing elit. */ˇ»
6570 "},
6571 indoc! {"
6572 «/*
6573 * Lorem ipsum dolor sit amet,ˇ»
6574 * consectetur adipiscing elit. */
6575 /* Lorem ipsum dolor sit amet,
6576 «* consectetur adipiscing elit.
6577 */ˇ»
6578 "},
6579 rust_lang.clone(),
6580 &mut cx,
6581 );
6582
6583 // selection w/ abutting short block comments
6584 // TODO: should not be combined; should rewrap as 2 comments
6585 assert_rewrap(
6586 indoc! {"
6587 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6588 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6589 "},
6590 // desired behavior:
6591 // indoc! {"
6592 // «/*
6593 // * Lorem ipsum dolor sit amet,
6594 // * consectetur adipiscing elit.
6595 // */
6596 // /*
6597 // * Lorem ipsum dolor sit amet,
6598 // * consectetur adipiscing elit.
6599 // */ˇ»
6600 // "},
6601 // actual behaviour:
6602 indoc! {"
6603 «/*
6604 * Lorem ipsum dolor sit amet,
6605 * consectetur adipiscing elit. Lorem
6606 * ipsum dolor sit amet, consectetur
6607 * adipiscing elit.
6608 */ˇ»
6609 "},
6610 rust_lang.clone(),
6611 &mut cx,
6612 );
6613
6614 // TODO: same as above, but with delimiters on separate line
6615 // assert_rewrap(
6616 // indoc! {"
6617 // «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6618 // */
6619 // /*
6620 // * Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6621 // "},
6622 // // desired:
6623 // // indoc! {"
6624 // // «/*
6625 // // * Lorem ipsum dolor sit amet,
6626 // // * consectetur adipiscing elit.
6627 // // */
6628 // // /*
6629 // // * Lorem ipsum dolor sit amet,
6630 // // * consectetur adipiscing elit.
6631 // // */ˇ»
6632 // // "},
6633 // // actual: (but with trailing w/s on the empty lines)
6634 // indoc! {"
6635 // «/*
6636 // * Lorem ipsum dolor sit amet,
6637 // * consectetur adipiscing elit.
6638 // *
6639 // */
6640 // /*
6641 // *
6642 // * Lorem ipsum dolor sit amet,
6643 // * consectetur adipiscing elit.
6644 // */ˇ»
6645 // "},
6646 // rust_lang.clone(),
6647 // &mut cx,
6648 // );
6649
6650 // TODO these are unhandled edge cases; not correct, just documenting known issues
6651 assert_rewrap(
6652 indoc! {"
6653 /*
6654 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6655 */
6656 /*
6657 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6658 /*ˇ Lorem ipsum dolor sit amet */ /* consectetur adipiscing elit. */
6659 "},
6660 // desired:
6661 // indoc! {"
6662 // /*
6663 // *ˇ Lorem ipsum dolor sit amet,
6664 // * consectetur adipiscing elit.
6665 // */
6666 // /*
6667 // *ˇ Lorem ipsum dolor sit amet,
6668 // * consectetur adipiscing elit.
6669 // */
6670 // /*
6671 // *ˇ Lorem ipsum dolor sit amet
6672 // */ /* consectetur adipiscing elit. */
6673 // "},
6674 // actual:
6675 indoc! {"
6676 /*
6677 //ˇ Lorem ipsum dolor sit amet,
6678 // consectetur adipiscing elit.
6679 */
6680 /*
6681 * //ˇ Lorem ipsum dolor sit amet,
6682 * consectetur adipiscing elit.
6683 */
6684 /*
6685 *ˇ Lorem ipsum dolor sit amet */ /*
6686 * consectetur adipiscing elit.
6687 */
6688 "},
6689 rust_lang,
6690 &mut cx,
6691 );
6692
6693 #[track_caller]
6694 fn assert_rewrap(
6695 unwrapped_text: &str,
6696 wrapped_text: &str,
6697 language: Arc<Language>,
6698 cx: &mut EditorTestContext,
6699 ) {
6700 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6701 cx.set_state(unwrapped_text);
6702 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6703 cx.assert_editor_state(wrapped_text);
6704 }
6705}
6706
6707#[gpui::test]
6708async fn test_hard_wrap(cx: &mut TestAppContext) {
6709 init_test(cx, |_| {});
6710 let mut cx = EditorTestContext::new(cx).await;
6711
6712 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
6713 cx.update_editor(|editor, _, cx| {
6714 editor.set_hard_wrap(Some(14), cx);
6715 });
6716
6717 cx.set_state(indoc!(
6718 "
6719 one two three ˇ
6720 "
6721 ));
6722 cx.simulate_input("four");
6723 cx.run_until_parked();
6724
6725 cx.assert_editor_state(indoc!(
6726 "
6727 one two three
6728 fourˇ
6729 "
6730 ));
6731
6732 cx.update_editor(|editor, window, cx| {
6733 editor.newline(&Default::default(), window, cx);
6734 });
6735 cx.run_until_parked();
6736 cx.assert_editor_state(indoc!(
6737 "
6738 one two three
6739 four
6740 ˇ
6741 "
6742 ));
6743
6744 cx.simulate_input("five");
6745 cx.run_until_parked();
6746 cx.assert_editor_state(indoc!(
6747 "
6748 one two three
6749 four
6750 fiveˇ
6751 "
6752 ));
6753
6754 cx.update_editor(|editor, window, cx| {
6755 editor.newline(&Default::default(), window, cx);
6756 });
6757 cx.run_until_parked();
6758 cx.simulate_input("# ");
6759 cx.run_until_parked();
6760 cx.assert_editor_state(indoc!(
6761 "
6762 one two three
6763 four
6764 five
6765 # ˇ
6766 "
6767 ));
6768
6769 cx.update_editor(|editor, window, cx| {
6770 editor.newline(&Default::default(), window, cx);
6771 });
6772 cx.run_until_parked();
6773 cx.assert_editor_state(indoc!(
6774 "
6775 one two three
6776 four
6777 five
6778 #\x20
6779 #ˇ
6780 "
6781 ));
6782
6783 cx.simulate_input(" 6");
6784 cx.run_until_parked();
6785 cx.assert_editor_state(indoc!(
6786 "
6787 one two three
6788 four
6789 five
6790 #
6791 # 6ˇ
6792 "
6793 ));
6794}
6795
6796#[gpui::test]
6797async fn test_cut_line_ends(cx: &mut TestAppContext) {
6798 init_test(cx, |_| {});
6799
6800 let mut cx = EditorTestContext::new(cx).await;
6801
6802 cx.set_state(indoc! {"The quick brownˇ"});
6803 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
6804 cx.assert_editor_state(indoc! {"The quick brownˇ"});
6805
6806 cx.set_state(indoc! {"The emacs foxˇ"});
6807 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
6808 cx.assert_editor_state(indoc! {"The emacs foxˇ"});
6809
6810 cx.set_state(indoc! {"
6811 The quick« brownˇ»
6812 fox jumps overˇ
6813 the lazy dog"});
6814 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6815 cx.assert_editor_state(indoc! {"
6816 The quickˇ
6817 ˇthe lazy dog"});
6818
6819 cx.set_state(indoc! {"
6820 The quick« brownˇ»
6821 fox jumps overˇ
6822 the lazy dog"});
6823 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
6824 cx.assert_editor_state(indoc! {"
6825 The quickˇ
6826 fox jumps overˇthe lazy dog"});
6827
6828 cx.set_state(indoc! {"
6829 The quick« brownˇ»
6830 fox jumps overˇ
6831 the lazy dog"});
6832 cx.update_editor(|e, window, cx| {
6833 e.cut_to_end_of_line(
6834 &CutToEndOfLine {
6835 stop_at_newlines: true,
6836 },
6837 window,
6838 cx,
6839 )
6840 });
6841 cx.assert_editor_state(indoc! {"
6842 The quickˇ
6843 fox jumps overˇ
6844 the lazy dog"});
6845
6846 cx.set_state(indoc! {"
6847 The quick« brownˇ»
6848 fox jumps overˇ
6849 the lazy dog"});
6850 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
6851 cx.assert_editor_state(indoc! {"
6852 The quickˇ
6853 fox jumps overˇthe lazy dog"});
6854}
6855
6856#[gpui::test]
6857async fn test_clipboard(cx: &mut TestAppContext) {
6858 init_test(cx, |_| {});
6859
6860 let mut cx = EditorTestContext::new(cx).await;
6861
6862 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
6863 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6864 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
6865
6866 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
6867 cx.set_state("two ˇfour ˇsix ˇ");
6868 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6869 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
6870
6871 // Paste again but with only two cursors. Since the number of cursors doesn't
6872 // match the number of slices in the clipboard, the entire clipboard text
6873 // is pasted at each cursor.
6874 cx.set_state("ˇtwo one✅ four three six five ˇ");
6875 cx.update_editor(|e, window, cx| {
6876 e.handle_input("( ", window, cx);
6877 e.paste(&Paste, window, cx);
6878 e.handle_input(") ", window, cx);
6879 });
6880 cx.assert_editor_state(
6881 &([
6882 "( one✅ ",
6883 "three ",
6884 "five ) ˇtwo one✅ four three six five ( one✅ ",
6885 "three ",
6886 "five ) ˇ",
6887 ]
6888 .join("\n")),
6889 );
6890
6891 // Cut with three selections, one of which is full-line.
6892 cx.set_state(indoc! {"
6893 1«2ˇ»3
6894 4ˇ567
6895 «8ˇ»9"});
6896 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6897 cx.assert_editor_state(indoc! {"
6898 1ˇ3
6899 ˇ9"});
6900
6901 // Paste with three selections, noticing how the copied selection that was full-line
6902 // gets inserted before the second cursor.
6903 cx.set_state(indoc! {"
6904 1ˇ3
6905 9ˇ
6906 «oˇ»ne"});
6907 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6908 cx.assert_editor_state(indoc! {"
6909 12ˇ3
6910 4567
6911 9ˇ
6912 8ˇne"});
6913
6914 // Copy with a single cursor only, which writes the whole line into the clipboard.
6915 cx.set_state(indoc! {"
6916 The quick brown
6917 fox juˇmps over
6918 the lazy dog"});
6919 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6920 assert_eq!(
6921 cx.read_from_clipboard()
6922 .and_then(|item| item.text().as_deref().map(str::to_string)),
6923 Some("fox jumps over\n".to_string())
6924 );
6925
6926 // Paste with three selections, noticing how the copied full-line selection is inserted
6927 // before the empty selections but replaces the selection that is non-empty.
6928 cx.set_state(indoc! {"
6929 Tˇhe quick brown
6930 «foˇ»x jumps over
6931 tˇhe lazy dog"});
6932 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6933 cx.assert_editor_state(indoc! {"
6934 fox jumps over
6935 Tˇhe quick brown
6936 fox jumps over
6937 ˇx jumps over
6938 fox jumps over
6939 tˇhe lazy dog"});
6940}
6941
6942#[gpui::test]
6943async fn test_copy_trim(cx: &mut TestAppContext) {
6944 init_test(cx, |_| {});
6945
6946 let mut cx = EditorTestContext::new(cx).await;
6947 cx.set_state(
6948 r#" «for selection in selections.iter() {
6949 let mut start = selection.start;
6950 let mut end = selection.end;
6951 let is_entire_line = selection.is_empty();
6952 if is_entire_line {
6953 start = Point::new(start.row, 0);ˇ»
6954 end = cmp::min(max_point, Point::new(end.row + 1, 0));
6955 }
6956 "#,
6957 );
6958 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6959 assert_eq!(
6960 cx.read_from_clipboard()
6961 .and_then(|item| item.text().as_deref().map(str::to_string)),
6962 Some(
6963 "for selection in selections.iter() {
6964 let mut start = selection.start;
6965 let mut end = selection.end;
6966 let is_entire_line = selection.is_empty();
6967 if is_entire_line {
6968 start = Point::new(start.row, 0);"
6969 .to_string()
6970 ),
6971 "Regular copying preserves all indentation selected",
6972 );
6973 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6974 assert_eq!(
6975 cx.read_from_clipboard()
6976 .and_then(|item| item.text().as_deref().map(str::to_string)),
6977 Some(
6978 "for selection in selections.iter() {
6979let mut start = selection.start;
6980let mut end = selection.end;
6981let is_entire_line = selection.is_empty();
6982if is_entire_line {
6983 start = Point::new(start.row, 0);"
6984 .to_string()
6985 ),
6986 "Copying with stripping should strip all leading whitespaces"
6987 );
6988
6989 cx.set_state(
6990 r#" « for selection in selections.iter() {
6991 let mut start = selection.start;
6992 let mut end = selection.end;
6993 let is_entire_line = selection.is_empty();
6994 if is_entire_line {
6995 start = Point::new(start.row, 0);ˇ»
6996 end = cmp::min(max_point, Point::new(end.row + 1, 0));
6997 }
6998 "#,
6999 );
7000 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7001 assert_eq!(
7002 cx.read_from_clipboard()
7003 .and_then(|item| item.text().as_deref().map(str::to_string)),
7004 Some(
7005 " for selection in selections.iter() {
7006 let mut start = selection.start;
7007 let mut end = selection.end;
7008 let is_entire_line = selection.is_empty();
7009 if is_entire_line {
7010 start = Point::new(start.row, 0);"
7011 .to_string()
7012 ),
7013 "Regular copying preserves all indentation selected",
7014 );
7015 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7016 assert_eq!(
7017 cx.read_from_clipboard()
7018 .and_then(|item| item.text().as_deref().map(str::to_string)),
7019 Some(
7020 "for selection in selections.iter() {
7021let mut start = selection.start;
7022let mut end = selection.end;
7023let is_entire_line = selection.is_empty();
7024if is_entire_line {
7025 start = Point::new(start.row, 0);"
7026 .to_string()
7027 ),
7028 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
7029 );
7030
7031 cx.set_state(
7032 r#" «ˇ for selection in selections.iter() {
7033 let mut start = selection.start;
7034 let mut end = selection.end;
7035 let is_entire_line = selection.is_empty();
7036 if is_entire_line {
7037 start = Point::new(start.row, 0);»
7038 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7039 }
7040 "#,
7041 );
7042 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7043 assert_eq!(
7044 cx.read_from_clipboard()
7045 .and_then(|item| item.text().as_deref().map(str::to_string)),
7046 Some(
7047 " for selection in selections.iter() {
7048 let mut start = selection.start;
7049 let mut end = selection.end;
7050 let is_entire_line = selection.is_empty();
7051 if is_entire_line {
7052 start = Point::new(start.row, 0);"
7053 .to_string()
7054 ),
7055 "Regular copying for reverse selection works the same",
7056 );
7057 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7058 assert_eq!(
7059 cx.read_from_clipboard()
7060 .and_then(|item| item.text().as_deref().map(str::to_string)),
7061 Some(
7062 "for selection in selections.iter() {
7063let mut start = selection.start;
7064let mut end = selection.end;
7065let is_entire_line = selection.is_empty();
7066if is_entire_line {
7067 start = Point::new(start.row, 0);"
7068 .to_string()
7069 ),
7070 "Copying with stripping for reverse selection works the same"
7071 );
7072
7073 cx.set_state(
7074 r#" for selection «in selections.iter() {
7075 let mut start = selection.start;
7076 let mut end = selection.end;
7077 let is_entire_line = selection.is_empty();
7078 if is_entire_line {
7079 start = Point::new(start.row, 0);ˇ»
7080 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7081 }
7082 "#,
7083 );
7084 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7085 assert_eq!(
7086 cx.read_from_clipboard()
7087 .and_then(|item| item.text().as_deref().map(str::to_string)),
7088 Some(
7089 "in selections.iter() {
7090 let mut start = selection.start;
7091 let mut end = selection.end;
7092 let is_entire_line = selection.is_empty();
7093 if is_entire_line {
7094 start = Point::new(start.row, 0);"
7095 .to_string()
7096 ),
7097 "When selecting past the indent, the copying works as usual",
7098 );
7099 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7100 assert_eq!(
7101 cx.read_from_clipboard()
7102 .and_then(|item| item.text().as_deref().map(str::to_string)),
7103 Some(
7104 "in selections.iter() {
7105 let mut start = selection.start;
7106 let mut end = selection.end;
7107 let is_entire_line = selection.is_empty();
7108 if is_entire_line {
7109 start = Point::new(start.row, 0);"
7110 .to_string()
7111 ),
7112 "When selecting past the indent, nothing is trimmed"
7113 );
7114
7115 cx.set_state(
7116 r#" «for selection in selections.iter() {
7117 let mut start = selection.start;
7118
7119 let mut end = selection.end;
7120 let is_entire_line = selection.is_empty();
7121 if is_entire_line {
7122 start = Point::new(start.row, 0);
7123ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
7124 }
7125 "#,
7126 );
7127 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7128 assert_eq!(
7129 cx.read_from_clipboard()
7130 .and_then(|item| item.text().as_deref().map(str::to_string)),
7131 Some(
7132 "for selection in selections.iter() {
7133let mut start = selection.start;
7134
7135let mut end = selection.end;
7136let is_entire_line = selection.is_empty();
7137if is_entire_line {
7138 start = Point::new(start.row, 0);
7139"
7140 .to_string()
7141 ),
7142 "Copying with stripping should ignore empty lines"
7143 );
7144}
7145
7146#[gpui::test]
7147async fn test_paste_multiline(cx: &mut TestAppContext) {
7148 init_test(cx, |_| {});
7149
7150 let mut cx = EditorTestContext::new(cx).await;
7151 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7152
7153 // Cut an indented block, without the leading whitespace.
7154 cx.set_state(indoc! {"
7155 const a: B = (
7156 c(),
7157 «d(
7158 e,
7159 f
7160 )ˇ»
7161 );
7162 "});
7163 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7164 cx.assert_editor_state(indoc! {"
7165 const a: B = (
7166 c(),
7167 ˇ
7168 );
7169 "});
7170
7171 // Paste it at the same position.
7172 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7173 cx.assert_editor_state(indoc! {"
7174 const a: B = (
7175 c(),
7176 d(
7177 e,
7178 f
7179 )ˇ
7180 );
7181 "});
7182
7183 // Paste it at a line with a lower indent level.
7184 cx.set_state(indoc! {"
7185 ˇ
7186 const a: B = (
7187 c(),
7188 );
7189 "});
7190 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7191 cx.assert_editor_state(indoc! {"
7192 d(
7193 e,
7194 f
7195 )ˇ
7196 const a: B = (
7197 c(),
7198 );
7199 "});
7200
7201 // Cut an indented block, with the leading whitespace.
7202 cx.set_state(indoc! {"
7203 const a: B = (
7204 c(),
7205 « d(
7206 e,
7207 f
7208 )
7209 ˇ»);
7210 "});
7211 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7212 cx.assert_editor_state(indoc! {"
7213 const a: B = (
7214 c(),
7215 ˇ);
7216 "});
7217
7218 // Paste it at the same position.
7219 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7220 cx.assert_editor_state(indoc! {"
7221 const a: B = (
7222 c(),
7223 d(
7224 e,
7225 f
7226 )
7227 ˇ);
7228 "});
7229
7230 // Paste it at a line with a higher indent level.
7231 cx.set_state(indoc! {"
7232 const a: B = (
7233 c(),
7234 d(
7235 e,
7236 fˇ
7237 )
7238 );
7239 "});
7240 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7241 cx.assert_editor_state(indoc! {"
7242 const a: B = (
7243 c(),
7244 d(
7245 e,
7246 f d(
7247 e,
7248 f
7249 )
7250 ˇ
7251 )
7252 );
7253 "});
7254
7255 // Copy an indented block, starting mid-line
7256 cx.set_state(indoc! {"
7257 const a: B = (
7258 c(),
7259 somethin«g(
7260 e,
7261 f
7262 )ˇ»
7263 );
7264 "});
7265 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7266
7267 // Paste it on a line with a lower indent level
7268 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
7269 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7270 cx.assert_editor_state(indoc! {"
7271 const a: B = (
7272 c(),
7273 something(
7274 e,
7275 f
7276 )
7277 );
7278 g(
7279 e,
7280 f
7281 )ˇ"});
7282}
7283
7284#[gpui::test]
7285async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
7286 init_test(cx, |_| {});
7287
7288 cx.write_to_clipboard(ClipboardItem::new_string(
7289 " d(\n e\n );\n".into(),
7290 ));
7291
7292 let mut cx = EditorTestContext::new(cx).await;
7293 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7294
7295 cx.set_state(indoc! {"
7296 fn a() {
7297 b();
7298 if c() {
7299 ˇ
7300 }
7301 }
7302 "});
7303
7304 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7305 cx.assert_editor_state(indoc! {"
7306 fn a() {
7307 b();
7308 if c() {
7309 d(
7310 e
7311 );
7312 ˇ
7313 }
7314 }
7315 "});
7316
7317 cx.set_state(indoc! {"
7318 fn a() {
7319 b();
7320 ˇ
7321 }
7322 "});
7323
7324 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7325 cx.assert_editor_state(indoc! {"
7326 fn a() {
7327 b();
7328 d(
7329 e
7330 );
7331 ˇ
7332 }
7333 "});
7334}
7335
7336#[gpui::test]
7337fn test_select_all(cx: &mut TestAppContext) {
7338 init_test(cx, |_| {});
7339
7340 let editor = cx.add_window(|window, cx| {
7341 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
7342 build_editor(buffer, window, cx)
7343 });
7344 _ = editor.update(cx, |editor, window, cx| {
7345 editor.select_all(&SelectAll, window, cx);
7346 assert_eq!(
7347 editor.selections.display_ranges(cx),
7348 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
7349 );
7350 });
7351}
7352
7353#[gpui::test]
7354fn test_select_line(cx: &mut TestAppContext) {
7355 init_test(cx, |_| {});
7356
7357 let editor = cx.add_window(|window, cx| {
7358 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
7359 build_editor(buffer, window, cx)
7360 });
7361 _ = editor.update(cx, |editor, window, cx| {
7362 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7363 s.select_display_ranges([
7364 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7365 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7366 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7367 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
7368 ])
7369 });
7370 editor.select_line(&SelectLine, window, cx);
7371 assert_eq!(
7372 editor.selections.display_ranges(cx),
7373 vec![
7374 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
7375 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
7376 ]
7377 );
7378 });
7379
7380 _ = editor.update(cx, |editor, window, cx| {
7381 editor.select_line(&SelectLine, window, cx);
7382 assert_eq!(
7383 editor.selections.display_ranges(cx),
7384 vec![
7385 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
7386 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
7387 ]
7388 );
7389 });
7390
7391 _ = editor.update(cx, |editor, window, cx| {
7392 editor.select_line(&SelectLine, window, cx);
7393 assert_eq!(
7394 editor.selections.display_ranges(cx),
7395 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
7396 );
7397 });
7398}
7399
7400#[gpui::test]
7401async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
7402 init_test(cx, |_| {});
7403 let mut cx = EditorTestContext::new(cx).await;
7404
7405 #[track_caller]
7406 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
7407 cx.set_state(initial_state);
7408 cx.update_editor(|e, window, cx| {
7409 e.split_selection_into_lines(&Default::default(), window, cx)
7410 });
7411 cx.assert_editor_state(expected_state);
7412 }
7413
7414 // Selection starts and ends at the middle of lines, left-to-right
7415 test(
7416 &mut cx,
7417 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
7418 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7419 );
7420 // Same thing, right-to-left
7421 test(
7422 &mut cx,
7423 "aa\nb«b\ncc\ndd\neˇ»e\nff",
7424 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7425 );
7426
7427 // Whole buffer, left-to-right, last line *doesn't* end with newline
7428 test(
7429 &mut cx,
7430 "«ˇaa\nbb\ncc\ndd\nee\nff»",
7431 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7432 );
7433 // Same thing, right-to-left
7434 test(
7435 &mut cx,
7436 "«aa\nbb\ncc\ndd\nee\nffˇ»",
7437 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7438 );
7439
7440 // Whole buffer, left-to-right, last line ends with newline
7441 test(
7442 &mut cx,
7443 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
7444 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7445 );
7446 // Same thing, right-to-left
7447 test(
7448 &mut cx,
7449 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
7450 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7451 );
7452
7453 // Starts at the end of a line, ends at the start of another
7454 test(
7455 &mut cx,
7456 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
7457 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
7458 );
7459}
7460
7461#[gpui::test]
7462async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
7463 init_test(cx, |_| {});
7464
7465 let editor = cx.add_window(|window, cx| {
7466 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
7467 build_editor(buffer, window, cx)
7468 });
7469
7470 // setup
7471 _ = editor.update(cx, |editor, window, cx| {
7472 editor.fold_creases(
7473 vec![
7474 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
7475 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
7476 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
7477 ],
7478 true,
7479 window,
7480 cx,
7481 );
7482 assert_eq!(
7483 editor.display_text(cx),
7484 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7485 );
7486 });
7487
7488 _ = editor.update(cx, |editor, window, cx| {
7489 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7490 s.select_display_ranges([
7491 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7492 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7493 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7494 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
7495 ])
7496 });
7497 editor.split_selection_into_lines(&Default::default(), window, cx);
7498 assert_eq!(
7499 editor.display_text(cx),
7500 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7501 );
7502 });
7503 EditorTestContext::for_editor(editor, cx)
7504 .await
7505 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
7506
7507 _ = editor.update(cx, |editor, window, cx| {
7508 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7509 s.select_display_ranges([
7510 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
7511 ])
7512 });
7513 editor.split_selection_into_lines(&Default::default(), window, cx);
7514 assert_eq!(
7515 editor.display_text(cx),
7516 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
7517 );
7518 assert_eq!(
7519 editor.selections.display_ranges(cx),
7520 [
7521 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
7522 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
7523 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
7524 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
7525 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
7526 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
7527 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
7528 ]
7529 );
7530 });
7531 EditorTestContext::for_editor(editor, cx)
7532 .await
7533 .assert_editor_state(
7534 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
7535 );
7536}
7537
7538#[gpui::test]
7539async fn test_add_selection_above_below(cx: &mut TestAppContext) {
7540 init_test(cx, |_| {});
7541
7542 let mut cx = EditorTestContext::new(cx).await;
7543
7544 cx.set_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_above(&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_above(&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 nlmo
7589 "#
7590 ));
7591
7592 cx.update_editor(|editor, window, cx| {
7593 editor.undo_selection(&Default::default(), window, cx);
7594 });
7595
7596 cx.assert_editor_state(indoc!(
7597 r#"abcˇ
7598 defˇghi
7599
7600 jk
7601 nlmo
7602 "#
7603 ));
7604
7605 cx.update_editor(|editor, window, cx| {
7606 editor.redo_selection(&Default::default(), window, cx);
7607 });
7608
7609 cx.assert_editor_state(indoc!(
7610 r#"abc
7611 defˇghi
7612
7613 jk
7614 nlmo
7615 "#
7616 ));
7617
7618 cx.update_editor(|editor, window, cx| {
7619 editor.add_selection_below(&Default::default(), window, cx);
7620 });
7621
7622 cx.assert_editor_state(indoc!(
7623 r#"abc
7624 defˇghi
7625 ˇ
7626 jk
7627 nlmo
7628 "#
7629 ));
7630
7631 cx.update_editor(|editor, window, cx| {
7632 editor.add_selection_below(&Default::default(), window, cx);
7633 });
7634
7635 cx.assert_editor_state(indoc!(
7636 r#"abc
7637 defˇghi
7638 ˇ
7639 jkˇ
7640 nlmo
7641 "#
7642 ));
7643
7644 cx.update_editor(|editor, window, cx| {
7645 editor.add_selection_below(&Default::default(), window, cx);
7646 });
7647
7648 cx.assert_editor_state(indoc!(
7649 r#"abc
7650 defˇghi
7651 ˇ
7652 jkˇ
7653 nlmˇo
7654 "#
7655 ));
7656
7657 cx.update_editor(|editor, window, cx| {
7658 editor.add_selection_below(&Default::default(), window, cx);
7659 });
7660
7661 cx.assert_editor_state(indoc!(
7662 r#"abc
7663 defˇghi
7664 ˇ
7665 jkˇ
7666 nlmˇo
7667 ˇ"#
7668 ));
7669
7670 // change selections
7671 cx.set_state(indoc!(
7672 r#"abc
7673 def«ˇg»hi
7674
7675 jk
7676 nlmo
7677 "#
7678 ));
7679
7680 cx.update_editor(|editor, window, cx| {
7681 editor.add_selection_below(&Default::default(), window, cx);
7682 });
7683
7684 cx.assert_editor_state(indoc!(
7685 r#"abc
7686 def«ˇg»hi
7687
7688 jk
7689 nlm«ˇo»
7690 "#
7691 ));
7692
7693 cx.update_editor(|editor, window, cx| {
7694 editor.add_selection_below(&Default::default(), window, cx);
7695 });
7696
7697 cx.assert_editor_state(indoc!(
7698 r#"abc
7699 def«ˇg»hi
7700
7701 jk
7702 nlm«ˇo»
7703 "#
7704 ));
7705
7706 cx.update_editor(|editor, window, cx| {
7707 editor.add_selection_above(&Default::default(), window, cx);
7708 });
7709
7710 cx.assert_editor_state(indoc!(
7711 r#"abc
7712 def«ˇg»hi
7713
7714 jk
7715 nlmo
7716 "#
7717 ));
7718
7719 cx.update_editor(|editor, window, cx| {
7720 editor.add_selection_above(&Default::default(), window, cx);
7721 });
7722
7723 cx.assert_editor_state(indoc!(
7724 r#"abc
7725 def«ˇg»hi
7726
7727 jk
7728 nlmo
7729 "#
7730 ));
7731
7732 // Change selections again
7733 cx.set_state(indoc!(
7734 r#"a«bc
7735 defgˇ»hi
7736
7737 jk
7738 nlmo
7739 "#
7740 ));
7741
7742 cx.update_editor(|editor, window, cx| {
7743 editor.add_selection_below(&Default::default(), window, cx);
7744 });
7745
7746 cx.assert_editor_state(indoc!(
7747 r#"a«bcˇ»
7748 d«efgˇ»hi
7749
7750 j«kˇ»
7751 nlmo
7752 "#
7753 ));
7754
7755 cx.update_editor(|editor, window, cx| {
7756 editor.add_selection_below(&Default::default(), window, cx);
7757 });
7758 cx.assert_editor_state(indoc!(
7759 r#"a«bcˇ»
7760 d«efgˇ»hi
7761
7762 j«kˇ»
7763 n«lmoˇ»
7764 "#
7765 ));
7766 cx.update_editor(|editor, window, cx| {
7767 editor.add_selection_above(&Default::default(), window, cx);
7768 });
7769
7770 cx.assert_editor_state(indoc!(
7771 r#"a«bcˇ»
7772 d«efgˇ»hi
7773
7774 j«kˇ»
7775 nlmo
7776 "#
7777 ));
7778
7779 // Change selections again
7780 cx.set_state(indoc!(
7781 r#"abc
7782 d«ˇefghi
7783
7784 jk
7785 nlm»o
7786 "#
7787 ));
7788
7789 cx.update_editor(|editor, window, cx| {
7790 editor.add_selection_above(&Default::default(), window, cx);
7791 });
7792
7793 cx.assert_editor_state(indoc!(
7794 r#"a«ˇbc»
7795 d«ˇef»ghi
7796
7797 j«ˇk»
7798 n«ˇlm»o
7799 "#
7800 ));
7801
7802 cx.update_editor(|editor, window, cx| {
7803 editor.add_selection_below(&Default::default(), window, cx);
7804 });
7805
7806 cx.assert_editor_state(indoc!(
7807 r#"abc
7808 d«ˇef»ghi
7809
7810 j«ˇk»
7811 n«ˇlm»o
7812 "#
7813 ));
7814}
7815
7816#[gpui::test]
7817async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
7818 init_test(cx, |_| {});
7819 let mut cx = EditorTestContext::new(cx).await;
7820
7821 cx.set_state(indoc!(
7822 r#"line onˇe
7823 liˇne two
7824 line three
7825 line four"#
7826 ));
7827
7828 cx.update_editor(|editor, window, cx| {
7829 editor.add_selection_below(&Default::default(), window, cx);
7830 });
7831
7832 // test multiple cursors expand in the same direction
7833 cx.assert_editor_state(indoc!(
7834 r#"line onˇe
7835 liˇne twˇo
7836 liˇne three
7837 line four"#
7838 ));
7839
7840 cx.update_editor(|editor, window, cx| {
7841 editor.add_selection_below(&Default::default(), window, cx);
7842 });
7843
7844 cx.update_editor(|editor, window, cx| {
7845 editor.add_selection_below(&Default::default(), window, cx);
7846 });
7847
7848 // test multiple cursors expand below overflow
7849 cx.assert_editor_state(indoc!(
7850 r#"line onˇe
7851 liˇne twˇo
7852 liˇne thˇree
7853 liˇne foˇur"#
7854 ));
7855
7856 cx.update_editor(|editor, window, cx| {
7857 editor.add_selection_above(&Default::default(), window, cx);
7858 });
7859
7860 // test multiple cursors retrieves back correctly
7861 cx.assert_editor_state(indoc!(
7862 r#"line onˇe
7863 liˇne twˇo
7864 liˇne thˇree
7865 line four"#
7866 ));
7867
7868 cx.update_editor(|editor, window, cx| {
7869 editor.add_selection_above(&Default::default(), window, cx);
7870 });
7871
7872 cx.update_editor(|editor, window, cx| {
7873 editor.add_selection_above(&Default::default(), window, cx);
7874 });
7875
7876 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
7877 cx.assert_editor_state(indoc!(
7878 r#"liˇne onˇe
7879 liˇne two
7880 line three
7881 line four"#
7882 ));
7883
7884 cx.update_editor(|editor, window, cx| {
7885 editor.undo_selection(&Default::default(), window, cx);
7886 });
7887
7888 // test undo
7889 cx.assert_editor_state(indoc!(
7890 r#"line onˇe
7891 liˇne twˇo
7892 line three
7893 line four"#
7894 ));
7895
7896 cx.update_editor(|editor, window, cx| {
7897 editor.redo_selection(&Default::default(), window, cx);
7898 });
7899
7900 // test redo
7901 cx.assert_editor_state(indoc!(
7902 r#"liˇne onˇe
7903 liˇne two
7904 line three
7905 line four"#
7906 ));
7907
7908 cx.set_state(indoc!(
7909 r#"abcd
7910 ef«ghˇ»
7911 ijkl
7912 «mˇ»nop"#
7913 ));
7914
7915 cx.update_editor(|editor, window, cx| {
7916 editor.add_selection_above(&Default::default(), window, cx);
7917 });
7918
7919 // test multiple selections expand in the same direction
7920 cx.assert_editor_state(indoc!(
7921 r#"ab«cdˇ»
7922 ef«ghˇ»
7923 «iˇ»jkl
7924 «mˇ»nop"#
7925 ));
7926
7927 cx.update_editor(|editor, window, cx| {
7928 editor.add_selection_above(&Default::default(), window, cx);
7929 });
7930
7931 // test multiple selection upward overflow
7932 cx.assert_editor_state(indoc!(
7933 r#"ab«cdˇ»
7934 «eˇ»f«ghˇ»
7935 «iˇ»jkl
7936 «mˇ»nop"#
7937 ));
7938
7939 cx.update_editor(|editor, window, cx| {
7940 editor.add_selection_below(&Default::default(), window, cx);
7941 });
7942
7943 // test multiple selection retrieves back correctly
7944 cx.assert_editor_state(indoc!(
7945 r#"abcd
7946 ef«ghˇ»
7947 «iˇ»jkl
7948 «mˇ»nop"#
7949 ));
7950
7951 cx.update_editor(|editor, window, cx| {
7952 editor.add_selection_below(&Default::default(), window, cx);
7953 });
7954
7955 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
7956 cx.assert_editor_state(indoc!(
7957 r#"abcd
7958 ef«ghˇ»
7959 ij«klˇ»
7960 «mˇ»nop"#
7961 ));
7962
7963 cx.update_editor(|editor, window, cx| {
7964 editor.undo_selection(&Default::default(), window, cx);
7965 });
7966
7967 // test undo
7968 cx.assert_editor_state(indoc!(
7969 r#"abcd
7970 ef«ghˇ»
7971 «iˇ»jkl
7972 «mˇ»nop"#
7973 ));
7974
7975 cx.update_editor(|editor, window, cx| {
7976 editor.redo_selection(&Default::default(), window, cx);
7977 });
7978
7979 // test redo
7980 cx.assert_editor_state(indoc!(
7981 r#"abcd
7982 ef«ghˇ»
7983 ij«klˇ»
7984 «mˇ»nop"#
7985 ));
7986}
7987
7988#[gpui::test]
7989async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
7990 init_test(cx, |_| {});
7991 let mut cx = EditorTestContext::new(cx).await;
7992
7993 cx.set_state(indoc!(
7994 r#"line onˇe
7995 liˇne two
7996 line three
7997 line four"#
7998 ));
7999
8000 cx.update_editor(|editor, window, cx| {
8001 editor.add_selection_below(&Default::default(), window, cx);
8002 editor.add_selection_below(&Default::default(), window, cx);
8003 editor.add_selection_below(&Default::default(), window, cx);
8004 });
8005
8006 // initial state with two multi cursor groups
8007 cx.assert_editor_state(indoc!(
8008 r#"line onˇe
8009 liˇne twˇo
8010 liˇne thˇree
8011 liˇne foˇur"#
8012 ));
8013
8014 // add single cursor in middle - simulate opt click
8015 cx.update_editor(|editor, window, cx| {
8016 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
8017 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8018 editor.end_selection(window, cx);
8019 });
8020
8021 cx.assert_editor_state(indoc!(
8022 r#"line onˇe
8023 liˇne twˇo
8024 liˇneˇ thˇree
8025 liˇne foˇur"#
8026 ));
8027
8028 cx.update_editor(|editor, window, cx| {
8029 editor.add_selection_above(&Default::default(), window, cx);
8030 });
8031
8032 // test new added selection expands above and existing selection shrinks
8033 cx.assert_editor_state(indoc!(
8034 r#"line onˇe
8035 liˇneˇ twˇo
8036 liˇneˇ thˇree
8037 line four"#
8038 ));
8039
8040 cx.update_editor(|editor, window, cx| {
8041 editor.add_selection_above(&Default::default(), window, cx);
8042 });
8043
8044 // test new added selection expands above and existing selection shrinks
8045 cx.assert_editor_state(indoc!(
8046 r#"lineˇ onˇe
8047 liˇneˇ twˇo
8048 lineˇ three
8049 line four"#
8050 ));
8051
8052 // intial state with two selection groups
8053 cx.set_state(indoc!(
8054 r#"abcd
8055 ef«ghˇ»
8056 ijkl
8057 «mˇ»nop"#
8058 ));
8059
8060 cx.update_editor(|editor, window, cx| {
8061 editor.add_selection_above(&Default::default(), window, cx);
8062 editor.add_selection_above(&Default::default(), window, cx);
8063 });
8064
8065 cx.assert_editor_state(indoc!(
8066 r#"ab«cdˇ»
8067 «eˇ»f«ghˇ»
8068 «iˇ»jkl
8069 «mˇ»nop"#
8070 ));
8071
8072 // add single selection in middle - simulate opt drag
8073 cx.update_editor(|editor, window, cx| {
8074 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
8075 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8076 editor.update_selection(
8077 DisplayPoint::new(DisplayRow(2), 4),
8078 0,
8079 gpui::Point::<f32>::default(),
8080 window,
8081 cx,
8082 );
8083 editor.end_selection(window, cx);
8084 });
8085
8086 cx.assert_editor_state(indoc!(
8087 r#"ab«cdˇ»
8088 «eˇ»f«ghˇ»
8089 «iˇ»jk«lˇ»
8090 «mˇ»nop"#
8091 ));
8092
8093 cx.update_editor(|editor, window, cx| {
8094 editor.add_selection_below(&Default::default(), window, cx);
8095 });
8096
8097 // test new added selection expands below, others shrinks from above
8098 cx.assert_editor_state(indoc!(
8099 r#"abcd
8100 ef«ghˇ»
8101 «iˇ»jk«lˇ»
8102 «mˇ»no«pˇ»"#
8103 ));
8104}
8105
8106#[gpui::test]
8107async fn test_select_next(cx: &mut TestAppContext) {
8108 init_test(cx, |_| {});
8109
8110 let mut cx = EditorTestContext::new(cx).await;
8111 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8112
8113 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8114 .unwrap();
8115 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8116
8117 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8118 .unwrap();
8119 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8120
8121 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8122 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8123
8124 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8125 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8126
8127 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8128 .unwrap();
8129 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8130
8131 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8132 .unwrap();
8133 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8134
8135 // Test selection direction should be preserved
8136 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8137
8138 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8139 .unwrap();
8140 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
8141}
8142
8143#[gpui::test]
8144async fn test_select_all_matches(cx: &mut TestAppContext) {
8145 init_test(cx, |_| {});
8146
8147 let mut cx = EditorTestContext::new(cx).await;
8148
8149 // Test caret-only selections
8150 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8151 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8152 .unwrap();
8153 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8154
8155 // Test left-to-right selections
8156 cx.set_state("abc\n«abcˇ»\nabc");
8157 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8158 .unwrap();
8159 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
8160
8161 // Test right-to-left selections
8162 cx.set_state("abc\n«ˇabc»\nabc");
8163 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8164 .unwrap();
8165 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
8166
8167 // Test selecting whitespace with caret selection
8168 cx.set_state("abc\nˇ abc\nabc");
8169 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8170 .unwrap();
8171 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8172
8173 // Test selecting whitespace with left-to-right selection
8174 cx.set_state("abc\n«ˇ »abc\nabc");
8175 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8176 .unwrap();
8177 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
8178
8179 // Test no matches with right-to-left selection
8180 cx.set_state("abc\n« ˇ»abc\nabc");
8181 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8182 .unwrap();
8183 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8184
8185 // Test with a single word and clip_at_line_ends=true (#29823)
8186 cx.set_state("aˇbc");
8187 cx.update_editor(|e, window, cx| {
8188 e.set_clip_at_line_ends(true, cx);
8189 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8190 e.set_clip_at_line_ends(false, cx);
8191 });
8192 cx.assert_editor_state("«abcˇ»");
8193}
8194
8195#[gpui::test]
8196async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
8197 init_test(cx, |_| {});
8198
8199 let mut cx = EditorTestContext::new(cx).await;
8200
8201 let large_body_1 = "\nd".repeat(200);
8202 let large_body_2 = "\ne".repeat(200);
8203
8204 cx.set_state(&format!(
8205 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
8206 ));
8207 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
8208 let scroll_position = editor.scroll_position(cx);
8209 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
8210 scroll_position
8211 });
8212
8213 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8214 .unwrap();
8215 cx.assert_editor_state(&format!(
8216 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
8217 ));
8218 let scroll_position_after_selection =
8219 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
8220 assert_eq!(
8221 initial_scroll_position, scroll_position_after_selection,
8222 "Scroll position should not change after selecting all matches"
8223 );
8224}
8225
8226#[gpui::test]
8227async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
8228 init_test(cx, |_| {});
8229
8230 let mut cx = EditorLspTestContext::new_rust(
8231 lsp::ServerCapabilities {
8232 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8233 ..Default::default()
8234 },
8235 cx,
8236 )
8237 .await;
8238
8239 cx.set_state(indoc! {"
8240 line 1
8241 line 2
8242 linˇe 3
8243 line 4
8244 line 5
8245 "});
8246
8247 // Make an edit
8248 cx.update_editor(|editor, window, cx| {
8249 editor.handle_input("X", window, cx);
8250 });
8251
8252 // Move cursor to a different position
8253 cx.update_editor(|editor, window, cx| {
8254 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8255 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
8256 });
8257 });
8258
8259 cx.assert_editor_state(indoc! {"
8260 line 1
8261 line 2
8262 linXe 3
8263 line 4
8264 liˇne 5
8265 "});
8266
8267 cx.lsp
8268 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
8269 Ok(Some(vec![lsp::TextEdit::new(
8270 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8271 "PREFIX ".to_string(),
8272 )]))
8273 });
8274
8275 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
8276 .unwrap()
8277 .await
8278 .unwrap();
8279
8280 cx.assert_editor_state(indoc! {"
8281 PREFIX line 1
8282 line 2
8283 linXe 3
8284 line 4
8285 liˇne 5
8286 "});
8287
8288 // Undo formatting
8289 cx.update_editor(|editor, window, cx| {
8290 editor.undo(&Default::default(), window, cx);
8291 });
8292
8293 // Verify cursor moved back to position after edit
8294 cx.assert_editor_state(indoc! {"
8295 line 1
8296 line 2
8297 linXˇe 3
8298 line 4
8299 line 5
8300 "});
8301}
8302
8303#[gpui::test]
8304async fn test_undo_edit_prediction_scrolls_to_edit_pos(cx: &mut TestAppContext) {
8305 init_test(cx, |_| {});
8306
8307 let mut cx = EditorTestContext::new(cx).await;
8308
8309 let provider = cx.new(|_| FakeEditPredictionProvider::default());
8310 cx.update_editor(|editor, window, cx| {
8311 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
8312 });
8313
8314 cx.set_state(indoc! {"
8315 line 1
8316 line 2
8317 linˇe 3
8318 line 4
8319 line 5
8320 line 6
8321 line 7
8322 line 8
8323 line 9
8324 line 10
8325 "});
8326
8327 let snapshot = cx.buffer_snapshot();
8328 let edit_position = snapshot.anchor_after(Point::new(2, 4));
8329
8330 cx.update(|_, cx| {
8331 provider.update(cx, |provider, _| {
8332 provider.set_edit_prediction(Some(edit_prediction::EditPrediction::Local {
8333 id: None,
8334 edits: vec![(edit_position..edit_position, "X".into())],
8335 edit_preview: None,
8336 }))
8337 })
8338 });
8339
8340 cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
8341 cx.update_editor(|editor, window, cx| {
8342 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
8343 });
8344
8345 cx.assert_editor_state(indoc! {"
8346 line 1
8347 line 2
8348 lineXˇ 3
8349 line 4
8350 line 5
8351 line 6
8352 line 7
8353 line 8
8354 line 9
8355 line 10
8356 "});
8357
8358 cx.update_editor(|editor, window, cx| {
8359 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8360 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
8361 });
8362 });
8363
8364 cx.assert_editor_state(indoc! {"
8365 line 1
8366 line 2
8367 lineX 3
8368 line 4
8369 line 5
8370 line 6
8371 line 7
8372 line 8
8373 line 9
8374 liˇne 10
8375 "});
8376
8377 cx.update_editor(|editor, window, cx| {
8378 editor.undo(&Default::default(), window, cx);
8379 });
8380
8381 cx.assert_editor_state(indoc! {"
8382 line 1
8383 line 2
8384 lineˇ 3
8385 line 4
8386 line 5
8387 line 6
8388 line 7
8389 line 8
8390 line 9
8391 line 10
8392 "});
8393}
8394
8395#[gpui::test]
8396async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
8397 init_test(cx, |_| {});
8398
8399 let mut cx = EditorTestContext::new(cx).await;
8400 cx.set_state(
8401 r#"let foo = 2;
8402lˇet foo = 2;
8403let fooˇ = 2;
8404let foo = 2;
8405let foo = ˇ2;"#,
8406 );
8407
8408 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8409 .unwrap();
8410 cx.assert_editor_state(
8411 r#"let foo = 2;
8412«letˇ» foo = 2;
8413let «fooˇ» = 2;
8414let foo = 2;
8415let foo = «2ˇ»;"#,
8416 );
8417
8418 // noop for multiple selections with different contents
8419 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8420 .unwrap();
8421 cx.assert_editor_state(
8422 r#"let foo = 2;
8423«letˇ» foo = 2;
8424let «fooˇ» = 2;
8425let foo = 2;
8426let foo = «2ˇ»;"#,
8427 );
8428
8429 // Test last selection direction should be preserved
8430 cx.set_state(
8431 r#"let foo = 2;
8432let foo = 2;
8433let «fooˇ» = 2;
8434let «ˇfoo» = 2;
8435let foo = 2;"#,
8436 );
8437
8438 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8439 .unwrap();
8440 cx.assert_editor_state(
8441 r#"let foo = 2;
8442let foo = 2;
8443let «fooˇ» = 2;
8444let «ˇfoo» = 2;
8445let «ˇfoo» = 2;"#,
8446 );
8447}
8448
8449#[gpui::test]
8450async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
8451 init_test(cx, |_| {});
8452
8453 let mut cx =
8454 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
8455
8456 cx.assert_editor_state(indoc! {"
8457 ˇbbb
8458 ccc
8459
8460 bbb
8461 ccc
8462 "});
8463 cx.dispatch_action(SelectPrevious::default());
8464 cx.assert_editor_state(indoc! {"
8465 «bbbˇ»
8466 ccc
8467
8468 bbb
8469 ccc
8470 "});
8471 cx.dispatch_action(SelectPrevious::default());
8472 cx.assert_editor_state(indoc! {"
8473 «bbbˇ»
8474 ccc
8475
8476 «bbbˇ»
8477 ccc
8478 "});
8479}
8480
8481#[gpui::test]
8482async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
8483 init_test(cx, |_| {});
8484
8485 let mut cx = EditorTestContext::new(cx).await;
8486 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8487
8488 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8489 .unwrap();
8490 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8491
8492 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8493 .unwrap();
8494 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8495
8496 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8497 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8498
8499 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8500 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8501
8502 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8503 .unwrap();
8504 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
8505
8506 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8507 .unwrap();
8508 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8509}
8510
8511#[gpui::test]
8512async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
8513 init_test(cx, |_| {});
8514
8515 let mut cx = EditorTestContext::new(cx).await;
8516 cx.set_state("aˇ");
8517
8518 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8519 .unwrap();
8520 cx.assert_editor_state("«aˇ»");
8521 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8522 .unwrap();
8523 cx.assert_editor_state("«aˇ»");
8524}
8525
8526#[gpui::test]
8527async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
8528 init_test(cx, |_| {});
8529
8530 let mut cx = EditorTestContext::new(cx).await;
8531 cx.set_state(
8532 r#"let foo = 2;
8533lˇet foo = 2;
8534let fooˇ = 2;
8535let foo = 2;
8536let foo = ˇ2;"#,
8537 );
8538
8539 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8540 .unwrap();
8541 cx.assert_editor_state(
8542 r#"let foo = 2;
8543«letˇ» foo = 2;
8544let «fooˇ» = 2;
8545let foo = 2;
8546let foo = «2ˇ»;"#,
8547 );
8548
8549 // noop for multiple selections with different contents
8550 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8551 .unwrap();
8552 cx.assert_editor_state(
8553 r#"let foo = 2;
8554«letˇ» foo = 2;
8555let «fooˇ» = 2;
8556let foo = 2;
8557let foo = «2ˇ»;"#,
8558 );
8559}
8560
8561#[gpui::test]
8562async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
8563 init_test(cx, |_| {});
8564
8565 let mut cx = EditorTestContext::new(cx).await;
8566 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8567
8568 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8569 .unwrap();
8570 // selection direction is preserved
8571 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8572
8573 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8574 .unwrap();
8575 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8576
8577 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8578 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8579
8580 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8581 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8582
8583 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8584 .unwrap();
8585 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
8586
8587 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8588 .unwrap();
8589 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
8590}
8591
8592#[gpui::test]
8593async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
8594 init_test(cx, |_| {});
8595
8596 let language = Arc::new(Language::new(
8597 LanguageConfig::default(),
8598 Some(tree_sitter_rust::LANGUAGE.into()),
8599 ));
8600
8601 let text = r#"
8602 use mod1::mod2::{mod3, mod4};
8603
8604 fn fn_1(param1: bool, param2: &str) {
8605 let var1 = "text";
8606 }
8607 "#
8608 .unindent();
8609
8610 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8611 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8612 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8613
8614 editor
8615 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8616 .await;
8617
8618 editor.update_in(cx, |editor, window, cx| {
8619 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8620 s.select_display_ranges([
8621 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
8622 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
8623 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
8624 ]);
8625 });
8626 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8627 });
8628 editor.update(cx, |editor, cx| {
8629 assert_text_with_selections(
8630 editor,
8631 indoc! {r#"
8632 use mod1::mod2::{mod3, «mod4ˇ»};
8633
8634 fn fn_1«ˇ(param1: bool, param2: &str)» {
8635 let var1 = "«ˇtext»";
8636 }
8637 "#},
8638 cx,
8639 );
8640 });
8641
8642 editor.update_in(cx, |editor, window, cx| {
8643 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8644 });
8645 editor.update(cx, |editor, cx| {
8646 assert_text_with_selections(
8647 editor,
8648 indoc! {r#"
8649 use mod1::mod2::«{mod3, mod4}ˇ»;
8650
8651 «ˇfn fn_1(param1: bool, param2: &str) {
8652 let var1 = "text";
8653 }»
8654 "#},
8655 cx,
8656 );
8657 });
8658
8659 editor.update_in(cx, |editor, window, cx| {
8660 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8661 });
8662 assert_eq!(
8663 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
8664 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
8665 );
8666
8667 // Trying to expand the selected syntax node one more time has no effect.
8668 editor.update_in(cx, |editor, window, cx| {
8669 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8670 });
8671 assert_eq!(
8672 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
8673 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
8674 );
8675
8676 editor.update_in(cx, |editor, window, cx| {
8677 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8678 });
8679 editor.update(cx, |editor, cx| {
8680 assert_text_with_selections(
8681 editor,
8682 indoc! {r#"
8683 use mod1::mod2::«{mod3, mod4}ˇ»;
8684
8685 «ˇfn fn_1(param1: bool, param2: &str) {
8686 let var1 = "text";
8687 }»
8688 "#},
8689 cx,
8690 );
8691 });
8692
8693 editor.update_in(cx, |editor, window, cx| {
8694 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8695 });
8696 editor.update(cx, |editor, cx| {
8697 assert_text_with_selections(
8698 editor,
8699 indoc! {r#"
8700 use mod1::mod2::{mod3, «mod4ˇ»};
8701
8702 fn fn_1«ˇ(param1: bool, param2: &str)» {
8703 let var1 = "«ˇtext»";
8704 }
8705 "#},
8706 cx,
8707 );
8708 });
8709
8710 editor.update_in(cx, |editor, window, cx| {
8711 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8712 });
8713 editor.update(cx, |editor, cx| {
8714 assert_text_with_selections(
8715 editor,
8716 indoc! {r#"
8717 use mod1::mod2::{mod3, moˇd4};
8718
8719 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
8720 let var1 = "teˇxt";
8721 }
8722 "#},
8723 cx,
8724 );
8725 });
8726
8727 // Trying to shrink the selected syntax node one more time has no effect.
8728 editor.update_in(cx, |editor, window, cx| {
8729 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8730 });
8731 editor.update_in(cx, |editor, _, cx| {
8732 assert_text_with_selections(
8733 editor,
8734 indoc! {r#"
8735 use mod1::mod2::{mod3, moˇd4};
8736
8737 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
8738 let var1 = "teˇxt";
8739 }
8740 "#},
8741 cx,
8742 );
8743 });
8744
8745 // Ensure that we keep expanding the selection if the larger selection starts or ends within
8746 // a fold.
8747 editor.update_in(cx, |editor, window, cx| {
8748 editor.fold_creases(
8749 vec![
8750 Crease::simple(
8751 Point::new(0, 21)..Point::new(0, 24),
8752 FoldPlaceholder::test(),
8753 ),
8754 Crease::simple(
8755 Point::new(3, 20)..Point::new(3, 22),
8756 FoldPlaceholder::test(),
8757 ),
8758 ],
8759 true,
8760 window,
8761 cx,
8762 );
8763 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8764 });
8765 editor.update(cx, |editor, cx| {
8766 assert_text_with_selections(
8767 editor,
8768 indoc! {r#"
8769 use mod1::mod2::«{mod3, mod4}ˇ»;
8770
8771 fn fn_1«ˇ(param1: bool, param2: &str)» {
8772 let var1 = "«ˇtext»";
8773 }
8774 "#},
8775 cx,
8776 );
8777 });
8778}
8779
8780#[gpui::test]
8781async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
8782 init_test(cx, |_| {});
8783
8784 let language = Arc::new(Language::new(
8785 LanguageConfig::default(),
8786 Some(tree_sitter_rust::LANGUAGE.into()),
8787 ));
8788
8789 let text = "let a = 2;";
8790
8791 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8792 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8793 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8794
8795 editor
8796 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8797 .await;
8798
8799 // Test case 1: Cursor at end of word
8800 editor.update_in(cx, |editor, window, cx| {
8801 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8802 s.select_display_ranges([
8803 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
8804 ]);
8805 });
8806 });
8807 editor.update(cx, |editor, cx| {
8808 assert_text_with_selections(editor, "let aˇ = 2;", cx);
8809 });
8810 editor.update_in(cx, |editor, window, cx| {
8811 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8812 });
8813 editor.update(cx, |editor, cx| {
8814 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
8815 });
8816 editor.update_in(cx, |editor, window, cx| {
8817 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8818 });
8819 editor.update(cx, |editor, cx| {
8820 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
8821 });
8822
8823 // Test case 2: Cursor at end of statement
8824 editor.update_in(cx, |editor, window, cx| {
8825 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8826 s.select_display_ranges([
8827 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
8828 ]);
8829 });
8830 });
8831 editor.update(cx, |editor, cx| {
8832 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
8833 });
8834 editor.update_in(cx, |editor, window, cx| {
8835 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8836 });
8837 editor.update(cx, |editor, cx| {
8838 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
8839 });
8840}
8841
8842#[gpui::test]
8843async fn test_select_larger_syntax_node_for_cursor_at_symbol(cx: &mut TestAppContext) {
8844 init_test(cx, |_| {});
8845
8846 let language = Arc::new(Language::new(
8847 LanguageConfig {
8848 name: "JavaScript".into(),
8849 ..Default::default()
8850 },
8851 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
8852 ));
8853
8854 let text = r#"
8855 let a = {
8856 key: "value",
8857 };
8858 "#
8859 .unindent();
8860
8861 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8862 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8863 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8864
8865 editor
8866 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8867 .await;
8868
8869 // Test case 1: Cursor after '{'
8870 editor.update_in(cx, |editor, window, cx| {
8871 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8872 s.select_display_ranges([
8873 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 9)
8874 ]);
8875 });
8876 });
8877 editor.update(cx, |editor, cx| {
8878 assert_text_with_selections(
8879 editor,
8880 indoc! {r#"
8881 let a = {ˇ
8882 key: "value",
8883 };
8884 "#},
8885 cx,
8886 );
8887 });
8888 editor.update_in(cx, |editor, window, cx| {
8889 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8890 });
8891 editor.update(cx, |editor, cx| {
8892 assert_text_with_selections(
8893 editor,
8894 indoc! {r#"
8895 let a = «ˇ{
8896 key: "value",
8897 }»;
8898 "#},
8899 cx,
8900 );
8901 });
8902
8903 // Test case 2: Cursor after ':'
8904 editor.update_in(cx, |editor, window, cx| {
8905 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8906 s.select_display_ranges([
8907 DisplayPoint::new(DisplayRow(1), 8)..DisplayPoint::new(DisplayRow(1), 8)
8908 ]);
8909 });
8910 });
8911 editor.update(cx, |editor, cx| {
8912 assert_text_with_selections(
8913 editor,
8914 indoc! {r#"
8915 let a = {
8916 key:ˇ "value",
8917 };
8918 "#},
8919 cx,
8920 );
8921 });
8922 editor.update_in(cx, |editor, window, cx| {
8923 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8924 });
8925 editor.update(cx, |editor, cx| {
8926 assert_text_with_selections(
8927 editor,
8928 indoc! {r#"
8929 let a = {
8930 «ˇkey: "value"»,
8931 };
8932 "#},
8933 cx,
8934 );
8935 });
8936 editor.update_in(cx, |editor, window, cx| {
8937 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8938 });
8939 editor.update(cx, |editor, cx| {
8940 assert_text_with_selections(
8941 editor,
8942 indoc! {r#"
8943 let a = «ˇ{
8944 key: "value",
8945 }»;
8946 "#},
8947 cx,
8948 );
8949 });
8950
8951 // Test case 3: Cursor after ','
8952 editor.update_in(cx, |editor, window, cx| {
8953 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8954 s.select_display_ranges([
8955 DisplayPoint::new(DisplayRow(1), 17)..DisplayPoint::new(DisplayRow(1), 17)
8956 ]);
8957 });
8958 });
8959 editor.update(cx, |editor, cx| {
8960 assert_text_with_selections(
8961 editor,
8962 indoc! {r#"
8963 let a = {
8964 key: "value",ˇ
8965 };
8966 "#},
8967 cx,
8968 );
8969 });
8970 editor.update_in(cx, |editor, window, cx| {
8971 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8972 });
8973 editor.update(cx, |editor, cx| {
8974 assert_text_with_selections(
8975 editor,
8976 indoc! {r#"
8977 let a = «ˇ{
8978 key: "value",
8979 }»;
8980 "#},
8981 cx,
8982 );
8983 });
8984
8985 // Test case 4: Cursor after ';'
8986 editor.update_in(cx, |editor, window, cx| {
8987 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8988 s.select_display_ranges([
8989 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)
8990 ]);
8991 });
8992 });
8993 editor.update(cx, |editor, cx| {
8994 assert_text_with_selections(
8995 editor,
8996 indoc! {r#"
8997 let a = {
8998 key: "value",
8999 };ˇ
9000 "#},
9001 cx,
9002 );
9003 });
9004 editor.update_in(cx, |editor, window, cx| {
9005 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9006 });
9007 editor.update(cx, |editor, cx| {
9008 assert_text_with_selections(
9009 editor,
9010 indoc! {r#"
9011 «ˇlet a = {
9012 key: "value",
9013 };
9014 »"#},
9015 cx,
9016 );
9017 });
9018}
9019
9020#[gpui::test]
9021async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
9022 init_test(cx, |_| {});
9023
9024 let language = Arc::new(Language::new(
9025 LanguageConfig::default(),
9026 Some(tree_sitter_rust::LANGUAGE.into()),
9027 ));
9028
9029 let text = r#"
9030 use mod1::mod2::{mod3, mod4};
9031
9032 fn fn_1(param1: bool, param2: &str) {
9033 let var1 = "hello world";
9034 }
9035 "#
9036 .unindent();
9037
9038 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9039 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9040 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9041
9042 editor
9043 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9044 .await;
9045
9046 // Test 1: Cursor on a letter of a string word
9047 editor.update_in(cx, |editor, window, cx| {
9048 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9049 s.select_display_ranges([
9050 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
9051 ]);
9052 });
9053 });
9054 editor.update_in(cx, |editor, window, cx| {
9055 assert_text_with_selections(
9056 editor,
9057 indoc! {r#"
9058 use mod1::mod2::{mod3, mod4};
9059
9060 fn fn_1(param1: bool, param2: &str) {
9061 let var1 = "hˇello world";
9062 }
9063 "#},
9064 cx,
9065 );
9066 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9067 assert_text_with_selections(
9068 editor,
9069 indoc! {r#"
9070 use mod1::mod2::{mod3, mod4};
9071
9072 fn fn_1(param1: bool, param2: &str) {
9073 let var1 = "«ˇhello» world";
9074 }
9075 "#},
9076 cx,
9077 );
9078 });
9079
9080 // Test 2: Partial selection within a word
9081 editor.update_in(cx, |editor, window, cx| {
9082 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9083 s.select_display_ranges([
9084 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
9085 ]);
9086 });
9087 });
9088 editor.update_in(cx, |editor, window, cx| {
9089 assert_text_with_selections(
9090 editor,
9091 indoc! {r#"
9092 use mod1::mod2::{mod3, mod4};
9093
9094 fn fn_1(param1: bool, param2: &str) {
9095 let var1 = "h«elˇ»lo world";
9096 }
9097 "#},
9098 cx,
9099 );
9100 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9101 assert_text_with_selections(
9102 editor,
9103 indoc! {r#"
9104 use mod1::mod2::{mod3, mod4};
9105
9106 fn fn_1(param1: bool, param2: &str) {
9107 let var1 = "«ˇhello» world";
9108 }
9109 "#},
9110 cx,
9111 );
9112 });
9113
9114 // Test 3: Complete word already selected
9115 editor.update_in(cx, |editor, window, cx| {
9116 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9117 s.select_display_ranges([
9118 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
9119 ]);
9120 });
9121 });
9122 editor.update_in(cx, |editor, window, cx| {
9123 assert_text_with_selections(
9124 editor,
9125 indoc! {r#"
9126 use mod1::mod2::{mod3, mod4};
9127
9128 fn fn_1(param1: bool, param2: &str) {
9129 let var1 = "«helloˇ» world";
9130 }
9131 "#},
9132 cx,
9133 );
9134 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9135 assert_text_with_selections(
9136 editor,
9137 indoc! {r#"
9138 use mod1::mod2::{mod3, mod4};
9139
9140 fn fn_1(param1: bool, param2: &str) {
9141 let var1 = "«hello worldˇ»";
9142 }
9143 "#},
9144 cx,
9145 );
9146 });
9147
9148 // Test 4: Selection spanning across words
9149 editor.update_in(cx, |editor, window, cx| {
9150 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9151 s.select_display_ranges([
9152 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
9153 ]);
9154 });
9155 });
9156 editor.update_in(cx, |editor, window, cx| {
9157 assert_text_with_selections(
9158 editor,
9159 indoc! {r#"
9160 use mod1::mod2::{mod3, mod4};
9161
9162 fn fn_1(param1: bool, param2: &str) {
9163 let var1 = "hel«lo woˇ»rld";
9164 }
9165 "#},
9166 cx,
9167 );
9168 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9169 assert_text_with_selections(
9170 editor,
9171 indoc! {r#"
9172 use mod1::mod2::{mod3, mod4};
9173
9174 fn fn_1(param1: bool, param2: &str) {
9175 let var1 = "«ˇhello world»";
9176 }
9177 "#},
9178 cx,
9179 );
9180 });
9181
9182 // Test 5: Expansion beyond string
9183 editor.update_in(cx, |editor, window, cx| {
9184 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9185 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9186 assert_text_with_selections(
9187 editor,
9188 indoc! {r#"
9189 use mod1::mod2::{mod3, mod4};
9190
9191 fn fn_1(param1: bool, param2: &str) {
9192 «ˇlet var1 = "hello world";»
9193 }
9194 "#},
9195 cx,
9196 );
9197 });
9198}
9199
9200#[gpui::test]
9201async fn test_unwrap_syntax_nodes(cx: &mut gpui::TestAppContext) {
9202 init_test(cx, |_| {});
9203
9204 let mut cx = EditorTestContext::new(cx).await;
9205
9206 let language = Arc::new(Language::new(
9207 LanguageConfig::default(),
9208 Some(tree_sitter_rust::LANGUAGE.into()),
9209 ));
9210
9211 cx.update_buffer(|buffer, cx| {
9212 buffer.set_language(Some(language), cx);
9213 });
9214
9215 cx.set_state(indoc! { r#"use mod1::{mod2::{«mod3ˇ», mod4}, mod5::{mod6, «mod7ˇ»}};"# });
9216 cx.update_editor(|editor, window, cx| {
9217 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9218 });
9219
9220 cx.assert_editor_state(indoc! { r#"use mod1::{mod2::«mod3ˇ», mod5::«mod7ˇ»};"# });
9221
9222 cx.set_state(indoc! { r#"fn a() {
9223 // what
9224 // a
9225 // ˇlong
9226 // method
9227 // I
9228 // sure
9229 // hope
9230 // it
9231 // works
9232 }"# });
9233
9234 let buffer = cx.update_multibuffer(|multibuffer, _| multibuffer.as_singleton().unwrap());
9235 let multi_buffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
9236 cx.update(|_, cx| {
9237 multi_buffer.update(cx, |multi_buffer, cx| {
9238 multi_buffer.set_excerpts_for_path(
9239 PathKey::for_buffer(&buffer, cx),
9240 buffer,
9241 [Point::new(1, 0)..Point::new(1, 0)],
9242 3,
9243 cx,
9244 );
9245 });
9246 });
9247
9248 let editor2 = cx.new_window_entity(|window, cx| {
9249 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
9250 });
9251
9252 let mut cx = EditorTestContext::for_editor_in(editor2, &mut cx).await;
9253 cx.update_editor(|editor, window, cx| {
9254 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
9255 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)]);
9256 })
9257 });
9258
9259 cx.assert_editor_state(indoc! { "
9260 fn a() {
9261 // what
9262 // a
9263 ˇ // long
9264 // method"});
9265
9266 cx.update_editor(|editor, window, cx| {
9267 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9268 });
9269
9270 // Although we could potentially make the action work when the syntax node
9271 // is half-hidden, it seems a bit dangerous as you can't easily tell what it
9272 // did. Maybe we could also expand the excerpt to contain the range?
9273 cx.assert_editor_state(indoc! { "
9274 fn a() {
9275 // what
9276 // a
9277 ˇ // long
9278 // method"});
9279}
9280
9281#[gpui::test]
9282async fn test_fold_function_bodies(cx: &mut TestAppContext) {
9283 init_test(cx, |_| {});
9284
9285 let base_text = r#"
9286 impl A {
9287 // this is an uncommitted comment
9288
9289 fn b() {
9290 c();
9291 }
9292
9293 // this is another uncommitted comment
9294
9295 fn d() {
9296 // e
9297 // f
9298 }
9299 }
9300
9301 fn g() {
9302 // h
9303 }
9304 "#
9305 .unindent();
9306
9307 let text = r#"
9308 ˇimpl A {
9309
9310 fn b() {
9311 c();
9312 }
9313
9314 fn d() {
9315 // e
9316 // f
9317 }
9318 }
9319
9320 fn g() {
9321 // h
9322 }
9323 "#
9324 .unindent();
9325
9326 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9327 cx.set_state(&text);
9328 cx.set_head_text(&base_text);
9329 cx.update_editor(|editor, window, cx| {
9330 editor.expand_all_diff_hunks(&Default::default(), window, cx);
9331 });
9332
9333 cx.assert_state_with_diff(
9334 "
9335 ˇimpl A {
9336 - // this is an uncommitted comment
9337
9338 fn b() {
9339 c();
9340 }
9341
9342 - // this is another uncommitted comment
9343 -
9344 fn d() {
9345 // e
9346 // f
9347 }
9348 }
9349
9350 fn g() {
9351 // h
9352 }
9353 "
9354 .unindent(),
9355 );
9356
9357 let expected_display_text = "
9358 impl A {
9359 // this is an uncommitted comment
9360
9361 fn b() {
9362 ⋯
9363 }
9364
9365 // this is another uncommitted comment
9366
9367 fn d() {
9368 ⋯
9369 }
9370 }
9371
9372 fn g() {
9373 ⋯
9374 }
9375 "
9376 .unindent();
9377
9378 cx.update_editor(|editor, window, cx| {
9379 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
9380 assert_eq!(editor.display_text(cx), expected_display_text);
9381 });
9382}
9383
9384#[gpui::test]
9385async fn test_autoindent(cx: &mut TestAppContext) {
9386 init_test(cx, |_| {});
9387
9388 let language = Arc::new(
9389 Language::new(
9390 LanguageConfig {
9391 brackets: BracketPairConfig {
9392 pairs: vec![
9393 BracketPair {
9394 start: "{".to_string(),
9395 end: "}".to_string(),
9396 close: false,
9397 surround: false,
9398 newline: true,
9399 },
9400 BracketPair {
9401 start: "(".to_string(),
9402 end: ")".to_string(),
9403 close: false,
9404 surround: false,
9405 newline: true,
9406 },
9407 ],
9408 ..Default::default()
9409 },
9410 ..Default::default()
9411 },
9412 Some(tree_sitter_rust::LANGUAGE.into()),
9413 )
9414 .with_indents_query(
9415 r#"
9416 (_ "(" ")" @end) @indent
9417 (_ "{" "}" @end) @indent
9418 "#,
9419 )
9420 .unwrap(),
9421 );
9422
9423 let text = "fn a() {}";
9424
9425 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9426 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9427 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9428 editor
9429 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9430 .await;
9431
9432 editor.update_in(cx, |editor, window, cx| {
9433 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9434 s.select_ranges([5..5, 8..8, 9..9])
9435 });
9436 editor.newline(&Newline, window, cx);
9437 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
9438 assert_eq!(
9439 editor.selections.ranges(cx),
9440 &[
9441 Point::new(1, 4)..Point::new(1, 4),
9442 Point::new(3, 4)..Point::new(3, 4),
9443 Point::new(5, 0)..Point::new(5, 0)
9444 ]
9445 );
9446 });
9447}
9448
9449#[gpui::test]
9450async fn test_autoindent_disabled(cx: &mut TestAppContext) {
9451 init_test(cx, |settings| settings.defaults.auto_indent = Some(false));
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: false,
9469 surround: false,
9470 newline: true,
9471 },
9472 ],
9473 ..Default::default()
9474 },
9475 ..Default::default()
9476 },
9477 Some(tree_sitter_rust::LANGUAGE.into()),
9478 )
9479 .with_indents_query(
9480 r#"
9481 (_ "(" ")" @end) @indent
9482 (_ "{" "}" @end) @indent
9483 "#,
9484 )
9485 .unwrap(),
9486 );
9487
9488 let text = "fn a() {}";
9489
9490 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9491 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9492 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9493 editor
9494 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9495 .await;
9496
9497 editor.update_in(cx, |editor, window, cx| {
9498 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9499 s.select_ranges([5..5, 8..8, 9..9])
9500 });
9501 editor.newline(&Newline, window, cx);
9502 assert_eq!(
9503 editor.text(cx),
9504 indoc!(
9505 "
9506 fn a(
9507
9508 ) {
9509
9510 }
9511 "
9512 )
9513 );
9514 assert_eq!(
9515 editor.selections.ranges(cx),
9516 &[
9517 Point::new(1, 0)..Point::new(1, 0),
9518 Point::new(3, 0)..Point::new(3, 0),
9519 Point::new(5, 0)..Point::new(5, 0)
9520 ]
9521 );
9522 });
9523}
9524
9525#[gpui::test]
9526async fn test_autoindent_disabled_with_nested_language(cx: &mut TestAppContext) {
9527 init_test(cx, |settings| {
9528 settings.defaults.auto_indent = Some(true);
9529 settings.languages.0.insert(
9530 "python".into(),
9531 LanguageSettingsContent {
9532 auto_indent: Some(false),
9533 ..Default::default()
9534 },
9535 );
9536 });
9537
9538 let mut cx = EditorTestContext::new(cx).await;
9539
9540 let injected_language = Arc::new(
9541 Language::new(
9542 LanguageConfig {
9543 brackets: BracketPairConfig {
9544 pairs: vec![
9545 BracketPair {
9546 start: "{".to_string(),
9547 end: "}".to_string(),
9548 close: false,
9549 surround: false,
9550 newline: true,
9551 },
9552 BracketPair {
9553 start: "(".to_string(),
9554 end: ")".to_string(),
9555 close: true,
9556 surround: false,
9557 newline: true,
9558 },
9559 ],
9560 ..Default::default()
9561 },
9562 name: "python".into(),
9563 ..Default::default()
9564 },
9565 Some(tree_sitter_python::LANGUAGE.into()),
9566 )
9567 .with_indents_query(
9568 r#"
9569 (_ "(" ")" @end) @indent
9570 (_ "{" "}" @end) @indent
9571 "#,
9572 )
9573 .unwrap(),
9574 );
9575
9576 let language = Arc::new(
9577 Language::new(
9578 LanguageConfig {
9579 brackets: BracketPairConfig {
9580 pairs: vec![
9581 BracketPair {
9582 start: "{".to_string(),
9583 end: "}".to_string(),
9584 close: false,
9585 surround: false,
9586 newline: true,
9587 },
9588 BracketPair {
9589 start: "(".to_string(),
9590 end: ")".to_string(),
9591 close: true,
9592 surround: false,
9593 newline: true,
9594 },
9595 ],
9596 ..Default::default()
9597 },
9598 name: LanguageName::new("rust"),
9599 ..Default::default()
9600 },
9601 Some(tree_sitter_rust::LANGUAGE.into()),
9602 )
9603 .with_indents_query(
9604 r#"
9605 (_ "(" ")" @end) @indent
9606 (_ "{" "}" @end) @indent
9607 "#,
9608 )
9609 .unwrap()
9610 .with_injection_query(
9611 r#"
9612 (macro_invocation
9613 macro: (identifier) @_macro_name
9614 (token_tree) @injection.content
9615 (#set! injection.language "python"))
9616 "#,
9617 )
9618 .unwrap(),
9619 );
9620
9621 cx.language_registry().add(injected_language);
9622 cx.language_registry().add(language.clone());
9623
9624 cx.update_buffer(|buffer, cx| {
9625 buffer.set_language(Some(language), cx);
9626 });
9627
9628 cx.set_state(r#"struct A {ˇ}"#);
9629
9630 cx.update_editor(|editor, window, cx| {
9631 editor.newline(&Default::default(), window, cx);
9632 });
9633
9634 cx.assert_editor_state(indoc!(
9635 "struct A {
9636 ˇ
9637 }"
9638 ));
9639
9640 cx.set_state(r#"select_biased!(ˇ)"#);
9641
9642 cx.update_editor(|editor, window, cx| {
9643 editor.newline(&Default::default(), window, cx);
9644 editor.handle_input("def ", window, cx);
9645 editor.handle_input("(", window, cx);
9646 editor.newline(&Default::default(), window, cx);
9647 editor.handle_input("a", window, cx);
9648 });
9649
9650 cx.assert_editor_state(indoc!(
9651 "select_biased!(
9652 def (
9653 aˇ
9654 )
9655 )"
9656 ));
9657}
9658
9659#[gpui::test]
9660async fn test_autoindent_selections(cx: &mut TestAppContext) {
9661 init_test(cx, |_| {});
9662
9663 {
9664 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9665 cx.set_state(indoc! {"
9666 impl A {
9667
9668 fn b() {}
9669
9670 «fn c() {
9671
9672 }ˇ»
9673 }
9674 "});
9675
9676 cx.update_editor(|editor, window, cx| {
9677 editor.autoindent(&Default::default(), window, cx);
9678 });
9679
9680 cx.assert_editor_state(indoc! {"
9681 impl A {
9682
9683 fn b() {}
9684
9685 «fn c() {
9686
9687 }ˇ»
9688 }
9689 "});
9690 }
9691
9692 {
9693 let mut cx = EditorTestContext::new_multibuffer(
9694 cx,
9695 [indoc! { "
9696 impl A {
9697 «
9698 // a
9699 fn b(){}
9700 »
9701 «
9702 }
9703 fn c(){}
9704 »
9705 "}],
9706 );
9707
9708 let buffer = cx.update_editor(|editor, _, cx| {
9709 let buffer = editor.buffer().update(cx, |buffer, _| {
9710 buffer.all_buffers().iter().next().unwrap().clone()
9711 });
9712 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
9713 buffer
9714 });
9715
9716 cx.run_until_parked();
9717 cx.update_editor(|editor, window, cx| {
9718 editor.select_all(&Default::default(), window, cx);
9719 editor.autoindent(&Default::default(), window, cx)
9720 });
9721 cx.run_until_parked();
9722
9723 cx.update(|_, cx| {
9724 assert_eq!(
9725 buffer.read(cx).text(),
9726 indoc! { "
9727 impl A {
9728
9729 // a
9730 fn b(){}
9731
9732
9733 }
9734 fn c(){}
9735
9736 " }
9737 )
9738 });
9739 }
9740}
9741
9742#[gpui::test]
9743async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
9744 init_test(cx, |_| {});
9745
9746 let mut cx = EditorTestContext::new(cx).await;
9747
9748 let language = Arc::new(Language::new(
9749 LanguageConfig {
9750 brackets: BracketPairConfig {
9751 pairs: vec![
9752 BracketPair {
9753 start: "{".to_string(),
9754 end: "}".to_string(),
9755 close: true,
9756 surround: true,
9757 newline: true,
9758 },
9759 BracketPair {
9760 start: "(".to_string(),
9761 end: ")".to_string(),
9762 close: true,
9763 surround: true,
9764 newline: true,
9765 },
9766 BracketPair {
9767 start: "/*".to_string(),
9768 end: " */".to_string(),
9769 close: true,
9770 surround: true,
9771 newline: true,
9772 },
9773 BracketPair {
9774 start: "[".to_string(),
9775 end: "]".to_string(),
9776 close: false,
9777 surround: false,
9778 newline: true,
9779 },
9780 BracketPair {
9781 start: "\"".to_string(),
9782 end: "\"".to_string(),
9783 close: true,
9784 surround: true,
9785 newline: false,
9786 },
9787 BracketPair {
9788 start: "<".to_string(),
9789 end: ">".to_string(),
9790 close: false,
9791 surround: true,
9792 newline: true,
9793 },
9794 ],
9795 ..Default::default()
9796 },
9797 autoclose_before: "})]".to_string(),
9798 ..Default::default()
9799 },
9800 Some(tree_sitter_rust::LANGUAGE.into()),
9801 ));
9802
9803 cx.language_registry().add(language.clone());
9804 cx.update_buffer(|buffer, cx| {
9805 buffer.set_language(Some(language), cx);
9806 });
9807
9808 cx.set_state(
9809 &r#"
9810 🏀ˇ
9811 εˇ
9812 ❤️ˇ
9813 "#
9814 .unindent(),
9815 );
9816
9817 // autoclose multiple nested brackets at multiple cursors
9818 cx.update_editor(|editor, window, cx| {
9819 editor.handle_input("{", window, cx);
9820 editor.handle_input("{", window, cx);
9821 editor.handle_input("{", window, cx);
9822 });
9823 cx.assert_editor_state(
9824 &"
9825 🏀{{{ˇ}}}
9826 ε{{{ˇ}}}
9827 ❤️{{{ˇ}}}
9828 "
9829 .unindent(),
9830 );
9831
9832 // insert a different closing bracket
9833 cx.update_editor(|editor, window, cx| {
9834 editor.handle_input(")", window, cx);
9835 });
9836 cx.assert_editor_state(
9837 &"
9838 🏀{{{)ˇ}}}
9839 ε{{{)ˇ}}}
9840 ❤️{{{)ˇ}}}
9841 "
9842 .unindent(),
9843 );
9844
9845 // skip over the auto-closed brackets when typing a closing bracket
9846 cx.update_editor(|editor, window, cx| {
9847 editor.move_right(&MoveRight, window, cx);
9848 editor.handle_input("}", window, cx);
9849 editor.handle_input("}", window, cx);
9850 editor.handle_input("}", window, cx);
9851 });
9852 cx.assert_editor_state(
9853 &"
9854 🏀{{{)}}}}ˇ
9855 ε{{{)}}}}ˇ
9856 ❤️{{{)}}}}ˇ
9857 "
9858 .unindent(),
9859 );
9860
9861 // autoclose multi-character pairs
9862 cx.set_state(
9863 &"
9864 ˇ
9865 ˇ
9866 "
9867 .unindent(),
9868 );
9869 cx.update_editor(|editor, window, cx| {
9870 editor.handle_input("/", window, cx);
9871 editor.handle_input("*", window, cx);
9872 });
9873 cx.assert_editor_state(
9874 &"
9875 /*ˇ */
9876 /*ˇ */
9877 "
9878 .unindent(),
9879 );
9880
9881 // one cursor autocloses a multi-character pair, one cursor
9882 // does not autoclose.
9883 cx.set_state(
9884 &"
9885 /ˇ
9886 ˇ
9887 "
9888 .unindent(),
9889 );
9890 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
9891 cx.assert_editor_state(
9892 &"
9893 /*ˇ */
9894 *ˇ
9895 "
9896 .unindent(),
9897 );
9898
9899 // Don't autoclose if the next character isn't whitespace and isn't
9900 // listed in the language's "autoclose_before" section.
9901 cx.set_state("ˇa b");
9902 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
9903 cx.assert_editor_state("{ˇa b");
9904
9905 // Don't autoclose if `close` is false for the bracket pair
9906 cx.set_state("ˇ");
9907 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
9908 cx.assert_editor_state("[ˇ");
9909
9910 // Surround with brackets if text is selected
9911 cx.set_state("«aˇ» b");
9912 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
9913 cx.assert_editor_state("{«aˇ»} b");
9914
9915 // Autoclose when not immediately after a word character
9916 cx.set_state("a ˇ");
9917 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
9918 cx.assert_editor_state("a \"ˇ\"");
9919
9920 // Autoclose pair where the start and end characters are the same
9921 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
9922 cx.assert_editor_state("a \"\"ˇ");
9923
9924 // Don't autoclose when immediately after a word character
9925 cx.set_state("aˇ");
9926 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
9927 cx.assert_editor_state("a\"ˇ");
9928
9929 // Do autoclose when after a non-word character
9930 cx.set_state("{ˇ");
9931 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
9932 cx.assert_editor_state("{\"ˇ\"");
9933
9934 // Non identical pairs autoclose regardless of preceding character
9935 cx.set_state("aˇ");
9936 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
9937 cx.assert_editor_state("a{ˇ}");
9938
9939 // Don't autoclose pair if autoclose is disabled
9940 cx.set_state("ˇ");
9941 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
9942 cx.assert_editor_state("<ˇ");
9943
9944 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
9945 cx.set_state("«aˇ» b");
9946 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
9947 cx.assert_editor_state("<«aˇ»> b");
9948}
9949
9950#[gpui::test]
9951async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
9952 init_test(cx, |settings| {
9953 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
9954 });
9955
9956 let mut cx = EditorTestContext::new(cx).await;
9957
9958 let language = Arc::new(Language::new(
9959 LanguageConfig {
9960 brackets: BracketPairConfig {
9961 pairs: vec![
9962 BracketPair {
9963 start: "{".to_string(),
9964 end: "}".to_string(),
9965 close: true,
9966 surround: true,
9967 newline: true,
9968 },
9969 BracketPair {
9970 start: "(".to_string(),
9971 end: ")".to_string(),
9972 close: true,
9973 surround: true,
9974 newline: true,
9975 },
9976 BracketPair {
9977 start: "[".to_string(),
9978 end: "]".to_string(),
9979 close: false,
9980 surround: false,
9981 newline: true,
9982 },
9983 ],
9984 ..Default::default()
9985 },
9986 autoclose_before: "})]".to_string(),
9987 ..Default::default()
9988 },
9989 Some(tree_sitter_rust::LANGUAGE.into()),
9990 ));
9991
9992 cx.language_registry().add(language.clone());
9993 cx.update_buffer(|buffer, cx| {
9994 buffer.set_language(Some(language), cx);
9995 });
9996
9997 cx.set_state(
9998 &"
9999 ˇ
10000 ˇ
10001 ˇ
10002 "
10003 .unindent(),
10004 );
10005
10006 // ensure only matching closing brackets are skipped over
10007 cx.update_editor(|editor, window, cx| {
10008 editor.handle_input("}", window, cx);
10009 editor.move_left(&MoveLeft, window, cx);
10010 editor.handle_input(")", window, cx);
10011 editor.move_left(&MoveLeft, window, cx);
10012 });
10013 cx.assert_editor_state(
10014 &"
10015 ˇ)}
10016 ˇ)}
10017 ˇ)}
10018 "
10019 .unindent(),
10020 );
10021
10022 // skip-over closing brackets at multiple cursors
10023 cx.update_editor(|editor, window, cx| {
10024 editor.handle_input(")", window, cx);
10025 editor.handle_input("}", window, cx);
10026 });
10027 cx.assert_editor_state(
10028 &"
10029 )}ˇ
10030 )}ˇ
10031 )}ˇ
10032 "
10033 .unindent(),
10034 );
10035
10036 // ignore non-close brackets
10037 cx.update_editor(|editor, window, cx| {
10038 editor.handle_input("]", window, cx);
10039 editor.move_left(&MoveLeft, window, cx);
10040 editor.handle_input("]", window, cx);
10041 });
10042 cx.assert_editor_state(
10043 &"
10044 )}]ˇ]
10045 )}]ˇ]
10046 )}]ˇ]
10047 "
10048 .unindent(),
10049 );
10050}
10051
10052#[gpui::test]
10053async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
10054 init_test(cx, |_| {});
10055
10056 let mut cx = EditorTestContext::new(cx).await;
10057
10058 let html_language = Arc::new(
10059 Language::new(
10060 LanguageConfig {
10061 name: "HTML".into(),
10062 brackets: BracketPairConfig {
10063 pairs: vec![
10064 BracketPair {
10065 start: "<".into(),
10066 end: ">".into(),
10067 close: true,
10068 ..Default::default()
10069 },
10070 BracketPair {
10071 start: "{".into(),
10072 end: "}".into(),
10073 close: true,
10074 ..Default::default()
10075 },
10076 BracketPair {
10077 start: "(".into(),
10078 end: ")".into(),
10079 close: true,
10080 ..Default::default()
10081 },
10082 ],
10083 ..Default::default()
10084 },
10085 autoclose_before: "})]>".into(),
10086 ..Default::default()
10087 },
10088 Some(tree_sitter_html::LANGUAGE.into()),
10089 )
10090 .with_injection_query(
10091 r#"
10092 (script_element
10093 (raw_text) @injection.content
10094 (#set! injection.language "javascript"))
10095 "#,
10096 )
10097 .unwrap(),
10098 );
10099
10100 let javascript_language = Arc::new(Language::new(
10101 LanguageConfig {
10102 name: "JavaScript".into(),
10103 brackets: BracketPairConfig {
10104 pairs: vec![
10105 BracketPair {
10106 start: "/*".into(),
10107 end: " */".into(),
10108 close: true,
10109 ..Default::default()
10110 },
10111 BracketPair {
10112 start: "{".into(),
10113 end: "}".into(),
10114 close: true,
10115 ..Default::default()
10116 },
10117 BracketPair {
10118 start: "(".into(),
10119 end: ")".into(),
10120 close: true,
10121 ..Default::default()
10122 },
10123 ],
10124 ..Default::default()
10125 },
10126 autoclose_before: "})]>".into(),
10127 ..Default::default()
10128 },
10129 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
10130 ));
10131
10132 cx.language_registry().add(html_language.clone());
10133 cx.language_registry().add(javascript_language);
10134 cx.executor().run_until_parked();
10135
10136 cx.update_buffer(|buffer, cx| {
10137 buffer.set_language(Some(html_language), cx);
10138 });
10139
10140 cx.set_state(
10141 &r#"
10142 <body>ˇ
10143 <script>
10144 var x = 1;ˇ
10145 </script>
10146 </body>ˇ
10147 "#
10148 .unindent(),
10149 );
10150
10151 // Precondition: different languages are active at different locations.
10152 cx.update_editor(|editor, window, cx| {
10153 let snapshot = editor.snapshot(window, cx);
10154 let cursors = editor.selections.ranges::<usize>(cx);
10155 let languages = cursors
10156 .iter()
10157 .map(|c| snapshot.language_at(c.start).unwrap().name())
10158 .collect::<Vec<_>>();
10159 assert_eq!(
10160 languages,
10161 &["HTML".into(), "JavaScript".into(), "HTML".into()]
10162 );
10163 });
10164
10165 // Angle brackets autoclose in HTML, but not JavaScript.
10166 cx.update_editor(|editor, window, cx| {
10167 editor.handle_input("<", window, cx);
10168 editor.handle_input("a", window, cx);
10169 });
10170 cx.assert_editor_state(
10171 &r#"
10172 <body><aˇ>
10173 <script>
10174 var x = 1;<aˇ
10175 </script>
10176 </body><aˇ>
10177 "#
10178 .unindent(),
10179 );
10180
10181 // Curly braces and parens autoclose in both HTML and JavaScript.
10182 cx.update_editor(|editor, window, cx| {
10183 editor.handle_input(" b=", window, cx);
10184 editor.handle_input("{", window, cx);
10185 editor.handle_input("c", window, cx);
10186 editor.handle_input("(", window, cx);
10187 });
10188 cx.assert_editor_state(
10189 &r#"
10190 <body><a b={c(ˇ)}>
10191 <script>
10192 var x = 1;<a b={c(ˇ)}
10193 </script>
10194 </body><a b={c(ˇ)}>
10195 "#
10196 .unindent(),
10197 );
10198
10199 // Brackets that were already autoclosed are skipped.
10200 cx.update_editor(|editor, window, cx| {
10201 editor.handle_input(")", window, cx);
10202 editor.handle_input("d", window, cx);
10203 editor.handle_input("}", window, cx);
10204 });
10205 cx.assert_editor_state(
10206 &r#"
10207 <body><a b={c()d}ˇ>
10208 <script>
10209 var x = 1;<a b={c()d}ˇ
10210 </script>
10211 </body><a b={c()d}ˇ>
10212 "#
10213 .unindent(),
10214 );
10215 cx.update_editor(|editor, window, cx| {
10216 editor.handle_input(">", window, cx);
10217 });
10218 cx.assert_editor_state(
10219 &r#"
10220 <body><a b={c()d}>ˇ
10221 <script>
10222 var x = 1;<a b={c()d}>ˇ
10223 </script>
10224 </body><a b={c()d}>ˇ
10225 "#
10226 .unindent(),
10227 );
10228
10229 // Reset
10230 cx.set_state(
10231 &r#"
10232 <body>ˇ
10233 <script>
10234 var x = 1;ˇ
10235 </script>
10236 </body>ˇ
10237 "#
10238 .unindent(),
10239 );
10240
10241 cx.update_editor(|editor, window, cx| {
10242 editor.handle_input("<", window, cx);
10243 });
10244 cx.assert_editor_state(
10245 &r#"
10246 <body><ˇ>
10247 <script>
10248 var x = 1;<ˇ
10249 </script>
10250 </body><ˇ>
10251 "#
10252 .unindent(),
10253 );
10254
10255 // When backspacing, the closing angle brackets are removed.
10256 cx.update_editor(|editor, window, cx| {
10257 editor.backspace(&Backspace, window, cx);
10258 });
10259 cx.assert_editor_state(
10260 &r#"
10261 <body>ˇ
10262 <script>
10263 var x = 1;ˇ
10264 </script>
10265 </body>ˇ
10266 "#
10267 .unindent(),
10268 );
10269
10270 // Block comments autoclose in JavaScript, but not HTML.
10271 cx.update_editor(|editor, window, cx| {
10272 editor.handle_input("/", window, cx);
10273 editor.handle_input("*", window, cx);
10274 });
10275 cx.assert_editor_state(
10276 &r#"
10277 <body>/*ˇ
10278 <script>
10279 var x = 1;/*ˇ */
10280 </script>
10281 </body>/*ˇ
10282 "#
10283 .unindent(),
10284 );
10285}
10286
10287#[gpui::test]
10288async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
10289 init_test(cx, |_| {});
10290
10291 let mut cx = EditorTestContext::new(cx).await;
10292
10293 let rust_language = Arc::new(
10294 Language::new(
10295 LanguageConfig {
10296 name: "Rust".into(),
10297 brackets: serde_json::from_value(json!([
10298 { "start": "{", "end": "}", "close": true, "newline": true },
10299 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
10300 ]))
10301 .unwrap(),
10302 autoclose_before: "})]>".into(),
10303 ..Default::default()
10304 },
10305 Some(tree_sitter_rust::LANGUAGE.into()),
10306 )
10307 .with_override_query("(string_literal) @string")
10308 .unwrap(),
10309 );
10310
10311 cx.language_registry().add(rust_language.clone());
10312 cx.update_buffer(|buffer, cx| {
10313 buffer.set_language(Some(rust_language), cx);
10314 });
10315
10316 cx.set_state(
10317 &r#"
10318 let x = ˇ
10319 "#
10320 .unindent(),
10321 );
10322
10323 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
10324 cx.update_editor(|editor, window, cx| {
10325 editor.handle_input("\"", window, cx);
10326 });
10327 cx.assert_editor_state(
10328 &r#"
10329 let x = "ˇ"
10330 "#
10331 .unindent(),
10332 );
10333
10334 // Inserting another quotation mark. The cursor moves across the existing
10335 // automatically-inserted quotation mark.
10336 cx.update_editor(|editor, window, cx| {
10337 editor.handle_input("\"", window, cx);
10338 });
10339 cx.assert_editor_state(
10340 &r#"
10341 let x = ""ˇ
10342 "#
10343 .unindent(),
10344 );
10345
10346 // Reset
10347 cx.set_state(
10348 &r#"
10349 let x = ˇ
10350 "#
10351 .unindent(),
10352 );
10353
10354 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
10355 cx.update_editor(|editor, window, cx| {
10356 editor.handle_input("\"", window, cx);
10357 editor.handle_input(" ", window, cx);
10358 editor.move_left(&Default::default(), window, cx);
10359 editor.handle_input("\\", window, cx);
10360 editor.handle_input("\"", window, cx);
10361 });
10362 cx.assert_editor_state(
10363 &r#"
10364 let x = "\"ˇ "
10365 "#
10366 .unindent(),
10367 );
10368
10369 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
10370 // mark. Nothing is inserted.
10371 cx.update_editor(|editor, window, cx| {
10372 editor.move_right(&Default::default(), window, cx);
10373 editor.handle_input("\"", window, cx);
10374 });
10375 cx.assert_editor_state(
10376 &r#"
10377 let x = "\" "ˇ
10378 "#
10379 .unindent(),
10380 );
10381}
10382
10383#[gpui::test]
10384async fn test_surround_with_pair(cx: &mut TestAppContext) {
10385 init_test(cx, |_| {});
10386
10387 let language = Arc::new(Language::new(
10388 LanguageConfig {
10389 brackets: BracketPairConfig {
10390 pairs: vec![
10391 BracketPair {
10392 start: "{".to_string(),
10393 end: "}".to_string(),
10394 close: true,
10395 surround: true,
10396 newline: true,
10397 },
10398 BracketPair {
10399 start: "/* ".to_string(),
10400 end: "*/".to_string(),
10401 close: true,
10402 surround: true,
10403 ..Default::default()
10404 },
10405 ],
10406 ..Default::default()
10407 },
10408 ..Default::default()
10409 },
10410 Some(tree_sitter_rust::LANGUAGE.into()),
10411 ));
10412
10413 let text = r#"
10414 a
10415 b
10416 c
10417 "#
10418 .unindent();
10419
10420 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10421 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10422 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10423 editor
10424 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10425 .await;
10426
10427 editor.update_in(cx, |editor, window, cx| {
10428 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10429 s.select_display_ranges([
10430 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10431 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10432 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
10433 ])
10434 });
10435
10436 editor.handle_input("{", window, cx);
10437 editor.handle_input("{", window, cx);
10438 editor.handle_input("{", window, cx);
10439 assert_eq!(
10440 editor.text(cx),
10441 "
10442 {{{a}}}
10443 {{{b}}}
10444 {{{c}}}
10445 "
10446 .unindent()
10447 );
10448 assert_eq!(
10449 editor.selections.display_ranges(cx),
10450 [
10451 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
10452 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
10453 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
10454 ]
10455 );
10456
10457 editor.undo(&Undo, window, cx);
10458 editor.undo(&Undo, window, cx);
10459 editor.undo(&Undo, window, cx);
10460 assert_eq!(
10461 editor.text(cx),
10462 "
10463 a
10464 b
10465 c
10466 "
10467 .unindent()
10468 );
10469 assert_eq!(
10470 editor.selections.display_ranges(cx),
10471 [
10472 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10473 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10474 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10475 ]
10476 );
10477
10478 // Ensure inserting the first character of a multi-byte bracket pair
10479 // doesn't surround the selections with the bracket.
10480 editor.handle_input("/", window, cx);
10481 assert_eq!(
10482 editor.text(cx),
10483 "
10484 /
10485 /
10486 /
10487 "
10488 .unindent()
10489 );
10490 assert_eq!(
10491 editor.selections.display_ranges(cx),
10492 [
10493 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10494 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10495 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10496 ]
10497 );
10498
10499 editor.undo(&Undo, window, cx);
10500 assert_eq!(
10501 editor.text(cx),
10502 "
10503 a
10504 b
10505 c
10506 "
10507 .unindent()
10508 );
10509 assert_eq!(
10510 editor.selections.display_ranges(cx),
10511 [
10512 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10513 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10514 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10515 ]
10516 );
10517
10518 // Ensure inserting the last character of a multi-byte bracket pair
10519 // doesn't surround the selections with the bracket.
10520 editor.handle_input("*", window, cx);
10521 assert_eq!(
10522 editor.text(cx),
10523 "
10524 *
10525 *
10526 *
10527 "
10528 .unindent()
10529 );
10530 assert_eq!(
10531 editor.selections.display_ranges(cx),
10532 [
10533 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10534 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10535 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10536 ]
10537 );
10538 });
10539}
10540
10541#[gpui::test]
10542async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
10543 init_test(cx, |_| {});
10544
10545 let language = Arc::new(Language::new(
10546 LanguageConfig {
10547 brackets: BracketPairConfig {
10548 pairs: vec![BracketPair {
10549 start: "{".to_string(),
10550 end: "}".to_string(),
10551 close: true,
10552 surround: true,
10553 newline: true,
10554 }],
10555 ..Default::default()
10556 },
10557 autoclose_before: "}".to_string(),
10558 ..Default::default()
10559 },
10560 Some(tree_sitter_rust::LANGUAGE.into()),
10561 ));
10562
10563 let text = r#"
10564 a
10565 b
10566 c
10567 "#
10568 .unindent();
10569
10570 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10571 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10572 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10573 editor
10574 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10575 .await;
10576
10577 editor.update_in(cx, |editor, window, cx| {
10578 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10579 s.select_ranges([
10580 Point::new(0, 1)..Point::new(0, 1),
10581 Point::new(1, 1)..Point::new(1, 1),
10582 Point::new(2, 1)..Point::new(2, 1),
10583 ])
10584 });
10585
10586 editor.handle_input("{", window, cx);
10587 editor.handle_input("{", window, cx);
10588 editor.handle_input("_", window, cx);
10589 assert_eq!(
10590 editor.text(cx),
10591 "
10592 a{{_}}
10593 b{{_}}
10594 c{{_}}
10595 "
10596 .unindent()
10597 );
10598 assert_eq!(
10599 editor.selections.ranges::<Point>(cx),
10600 [
10601 Point::new(0, 4)..Point::new(0, 4),
10602 Point::new(1, 4)..Point::new(1, 4),
10603 Point::new(2, 4)..Point::new(2, 4)
10604 ]
10605 );
10606
10607 editor.backspace(&Default::default(), window, cx);
10608 editor.backspace(&Default::default(), window, cx);
10609 assert_eq!(
10610 editor.text(cx),
10611 "
10612 a{}
10613 b{}
10614 c{}
10615 "
10616 .unindent()
10617 );
10618 assert_eq!(
10619 editor.selections.ranges::<Point>(cx),
10620 [
10621 Point::new(0, 2)..Point::new(0, 2),
10622 Point::new(1, 2)..Point::new(1, 2),
10623 Point::new(2, 2)..Point::new(2, 2)
10624 ]
10625 );
10626
10627 editor.delete_to_previous_word_start(&Default::default(), window, cx);
10628 assert_eq!(
10629 editor.text(cx),
10630 "
10631 a
10632 b
10633 c
10634 "
10635 .unindent()
10636 );
10637 assert_eq!(
10638 editor.selections.ranges::<Point>(cx),
10639 [
10640 Point::new(0, 1)..Point::new(0, 1),
10641 Point::new(1, 1)..Point::new(1, 1),
10642 Point::new(2, 1)..Point::new(2, 1)
10643 ]
10644 );
10645 });
10646}
10647
10648#[gpui::test]
10649async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
10650 init_test(cx, |settings| {
10651 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
10652 });
10653
10654 let mut cx = EditorTestContext::new(cx).await;
10655
10656 let language = Arc::new(Language::new(
10657 LanguageConfig {
10658 brackets: BracketPairConfig {
10659 pairs: vec![
10660 BracketPair {
10661 start: "{".to_string(),
10662 end: "}".to_string(),
10663 close: true,
10664 surround: true,
10665 newline: true,
10666 },
10667 BracketPair {
10668 start: "(".to_string(),
10669 end: ")".to_string(),
10670 close: true,
10671 surround: true,
10672 newline: true,
10673 },
10674 BracketPair {
10675 start: "[".to_string(),
10676 end: "]".to_string(),
10677 close: false,
10678 surround: true,
10679 newline: true,
10680 },
10681 ],
10682 ..Default::default()
10683 },
10684 autoclose_before: "})]".to_string(),
10685 ..Default::default()
10686 },
10687 Some(tree_sitter_rust::LANGUAGE.into()),
10688 ));
10689
10690 cx.language_registry().add(language.clone());
10691 cx.update_buffer(|buffer, cx| {
10692 buffer.set_language(Some(language), cx);
10693 });
10694
10695 cx.set_state(
10696 &"
10697 {(ˇ)}
10698 [[ˇ]]
10699 {(ˇ)}
10700 "
10701 .unindent(),
10702 );
10703
10704 cx.update_editor(|editor, window, cx| {
10705 editor.backspace(&Default::default(), window, cx);
10706 editor.backspace(&Default::default(), window, cx);
10707 });
10708
10709 cx.assert_editor_state(
10710 &"
10711 ˇ
10712 ˇ]]
10713 ˇ
10714 "
10715 .unindent(),
10716 );
10717
10718 cx.update_editor(|editor, window, cx| {
10719 editor.handle_input("{", window, cx);
10720 editor.handle_input("{", window, cx);
10721 editor.move_right(&MoveRight, window, cx);
10722 editor.move_right(&MoveRight, window, cx);
10723 editor.move_left(&MoveLeft, window, cx);
10724 editor.move_left(&MoveLeft, window, cx);
10725 editor.backspace(&Default::default(), window, cx);
10726 });
10727
10728 cx.assert_editor_state(
10729 &"
10730 {ˇ}
10731 {ˇ}]]
10732 {ˇ}
10733 "
10734 .unindent(),
10735 );
10736
10737 cx.update_editor(|editor, window, cx| {
10738 editor.backspace(&Default::default(), window, cx);
10739 });
10740
10741 cx.assert_editor_state(
10742 &"
10743 ˇ
10744 ˇ]]
10745 ˇ
10746 "
10747 .unindent(),
10748 );
10749}
10750
10751#[gpui::test]
10752async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
10753 init_test(cx, |_| {});
10754
10755 let language = Arc::new(Language::new(
10756 LanguageConfig::default(),
10757 Some(tree_sitter_rust::LANGUAGE.into()),
10758 ));
10759
10760 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
10761 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10762 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10763 editor
10764 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10765 .await;
10766
10767 editor.update_in(cx, |editor, window, cx| {
10768 editor.set_auto_replace_emoji_shortcode(true);
10769
10770 editor.handle_input("Hello ", window, cx);
10771 editor.handle_input(":wave", window, cx);
10772 assert_eq!(editor.text(cx), "Hello :wave".unindent());
10773
10774 editor.handle_input(":", window, cx);
10775 assert_eq!(editor.text(cx), "Hello 👋".unindent());
10776
10777 editor.handle_input(" :smile", window, cx);
10778 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
10779
10780 editor.handle_input(":", window, cx);
10781 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
10782
10783 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
10784 editor.handle_input(":wave", window, cx);
10785 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
10786
10787 editor.handle_input(":", window, cx);
10788 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
10789
10790 editor.handle_input(":1", window, cx);
10791 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
10792
10793 editor.handle_input(":", window, cx);
10794 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
10795
10796 // Ensure shortcode does not get replaced when it is part of a word
10797 editor.handle_input(" Test:wave", window, cx);
10798 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
10799
10800 editor.handle_input(":", window, cx);
10801 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
10802
10803 editor.set_auto_replace_emoji_shortcode(false);
10804
10805 // Ensure shortcode does not get replaced when auto replace is off
10806 editor.handle_input(" :wave", window, cx);
10807 assert_eq!(
10808 editor.text(cx),
10809 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
10810 );
10811
10812 editor.handle_input(":", window, cx);
10813 assert_eq!(
10814 editor.text(cx),
10815 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
10816 );
10817 });
10818}
10819
10820#[gpui::test]
10821async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
10822 init_test(cx, |_| {});
10823
10824 let (text, insertion_ranges) = marked_text_ranges(
10825 indoc! {"
10826 ˇ
10827 "},
10828 false,
10829 );
10830
10831 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
10832 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10833
10834 _ = editor.update_in(cx, |editor, window, cx| {
10835 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
10836
10837 editor
10838 .insert_snippet(&insertion_ranges, snippet, window, cx)
10839 .unwrap();
10840
10841 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
10842 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
10843 assert_eq!(editor.text(cx), expected_text);
10844 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
10845 }
10846
10847 assert(
10848 editor,
10849 cx,
10850 indoc! {"
10851 type «» =•
10852 "},
10853 );
10854
10855 assert!(editor.context_menu_visible(), "There should be a matches");
10856 });
10857}
10858
10859#[gpui::test]
10860async fn test_snippets(cx: &mut TestAppContext) {
10861 init_test(cx, |_| {});
10862
10863 let mut cx = EditorTestContext::new(cx).await;
10864
10865 cx.set_state(indoc! {"
10866 a.ˇ b
10867 a.ˇ b
10868 a.ˇ b
10869 "});
10870
10871 cx.update_editor(|editor, window, cx| {
10872 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
10873 let insertion_ranges = editor
10874 .selections
10875 .all(cx)
10876 .iter()
10877 .map(|s| s.range())
10878 .collect::<Vec<_>>();
10879 editor
10880 .insert_snippet(&insertion_ranges, snippet, window, cx)
10881 .unwrap();
10882 });
10883
10884 cx.assert_editor_state(indoc! {"
10885 a.f(«oneˇ», two, «threeˇ») b
10886 a.f(«oneˇ», two, «threeˇ») b
10887 a.f(«oneˇ», two, «threeˇ») b
10888 "});
10889
10890 // Can't move earlier than the first tab stop
10891 cx.update_editor(|editor, window, cx| {
10892 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
10893 });
10894 cx.assert_editor_state(indoc! {"
10895 a.f(«oneˇ», two, «threeˇ») b
10896 a.f(«oneˇ», two, «threeˇ») b
10897 a.f(«oneˇ», two, «threeˇ») b
10898 "});
10899
10900 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
10901 cx.assert_editor_state(indoc! {"
10902 a.f(one, «twoˇ», three) b
10903 a.f(one, «twoˇ», three) b
10904 a.f(one, «twoˇ», three) b
10905 "});
10906
10907 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
10908 cx.assert_editor_state(indoc! {"
10909 a.f(«oneˇ», two, «threeˇ») b
10910 a.f(«oneˇ», two, «threeˇ») b
10911 a.f(«oneˇ», two, «threeˇ») b
10912 "});
10913
10914 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
10915 cx.assert_editor_state(indoc! {"
10916 a.f(one, «twoˇ», three) b
10917 a.f(one, «twoˇ», three) b
10918 a.f(one, «twoˇ», three) b
10919 "});
10920 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
10921 cx.assert_editor_state(indoc! {"
10922 a.f(one, two, three)ˇ b
10923 a.f(one, two, three)ˇ b
10924 a.f(one, two, three)ˇ b
10925 "});
10926
10927 // As soon as the last tab stop is reached, snippet state is gone
10928 cx.update_editor(|editor, window, cx| {
10929 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
10930 });
10931 cx.assert_editor_state(indoc! {"
10932 a.f(one, two, three)ˇ b
10933 a.f(one, two, three)ˇ b
10934 a.f(one, two, three)ˇ b
10935 "});
10936}
10937
10938#[gpui::test]
10939async fn test_snippet_indentation(cx: &mut TestAppContext) {
10940 init_test(cx, |_| {});
10941
10942 let mut cx = EditorTestContext::new(cx).await;
10943
10944 cx.update_editor(|editor, window, cx| {
10945 let snippet = Snippet::parse(indoc! {"
10946 /*
10947 * Multiline comment with leading indentation
10948 *
10949 * $1
10950 */
10951 $0"})
10952 .unwrap();
10953 let insertion_ranges = editor
10954 .selections
10955 .all(cx)
10956 .iter()
10957 .map(|s| s.range())
10958 .collect::<Vec<_>>();
10959 editor
10960 .insert_snippet(&insertion_ranges, snippet, window, cx)
10961 .unwrap();
10962 });
10963
10964 cx.assert_editor_state(indoc! {"
10965 /*
10966 * Multiline comment with leading indentation
10967 *
10968 * ˇ
10969 */
10970 "});
10971
10972 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
10973 cx.assert_editor_state(indoc! {"
10974 /*
10975 * Multiline comment with leading indentation
10976 *
10977 *•
10978 */
10979 ˇ"});
10980}
10981
10982#[gpui::test]
10983async fn test_document_format_during_save(cx: &mut TestAppContext) {
10984 init_test(cx, |_| {});
10985
10986 let fs = FakeFs::new(cx.executor());
10987 fs.insert_file(path!("/file.rs"), Default::default()).await;
10988
10989 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
10990
10991 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10992 language_registry.add(rust_lang());
10993 let mut fake_servers = language_registry.register_fake_lsp(
10994 "Rust",
10995 FakeLspAdapter {
10996 capabilities: lsp::ServerCapabilities {
10997 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10998 ..Default::default()
10999 },
11000 ..Default::default()
11001 },
11002 );
11003
11004 let buffer = project
11005 .update(cx, |project, cx| {
11006 project.open_local_buffer(path!("/file.rs"), cx)
11007 })
11008 .await
11009 .unwrap();
11010
11011 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11012 let (editor, cx) = cx.add_window_view(|window, cx| {
11013 build_editor_with_project(project.clone(), buffer, window, cx)
11014 });
11015 editor.update_in(cx, |editor, window, cx| {
11016 editor.set_text("one\ntwo\nthree\n", window, cx)
11017 });
11018 assert!(cx.read(|cx| editor.is_dirty(cx)));
11019
11020 cx.executor().start_waiting();
11021 let fake_server = fake_servers.next().await.unwrap();
11022
11023 {
11024 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11025 move |params, _| async move {
11026 assert_eq!(
11027 params.text_document.uri,
11028 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11029 );
11030 assert_eq!(params.options.tab_size, 4);
11031 Ok(Some(vec![lsp::TextEdit::new(
11032 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11033 ", ".to_string(),
11034 )]))
11035 },
11036 );
11037 let save = editor
11038 .update_in(cx, |editor, window, cx| {
11039 editor.save(
11040 SaveOptions {
11041 format: true,
11042 autosave: false,
11043 },
11044 project.clone(),
11045 window,
11046 cx,
11047 )
11048 })
11049 .unwrap();
11050 cx.executor().start_waiting();
11051 save.await;
11052
11053 assert_eq!(
11054 editor.update(cx, |editor, cx| editor.text(cx)),
11055 "one, two\nthree\n"
11056 );
11057 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11058 }
11059
11060 {
11061 editor.update_in(cx, |editor, window, cx| {
11062 editor.set_text("one\ntwo\nthree\n", window, cx)
11063 });
11064 assert!(cx.read(|cx| editor.is_dirty(cx)));
11065
11066 // Ensure we can still save even if formatting hangs.
11067 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11068 move |params, _| async move {
11069 assert_eq!(
11070 params.text_document.uri,
11071 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11072 );
11073 futures::future::pending::<()>().await;
11074 unreachable!()
11075 },
11076 );
11077 let save = editor
11078 .update_in(cx, |editor, window, cx| {
11079 editor.save(
11080 SaveOptions {
11081 format: true,
11082 autosave: false,
11083 },
11084 project.clone(),
11085 window,
11086 cx,
11087 )
11088 })
11089 .unwrap();
11090 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11091 cx.executor().start_waiting();
11092 save.await;
11093 assert_eq!(
11094 editor.update(cx, |editor, cx| editor.text(cx)),
11095 "one\ntwo\nthree\n"
11096 );
11097 }
11098
11099 // Set rust language override and assert overridden tabsize is sent to language server
11100 update_test_language_settings(cx, |settings| {
11101 settings.languages.0.insert(
11102 "Rust".into(),
11103 LanguageSettingsContent {
11104 tab_size: NonZeroU32::new(8),
11105 ..Default::default()
11106 },
11107 );
11108 });
11109
11110 {
11111 editor.update_in(cx, |editor, window, cx| {
11112 editor.set_text("somehting_new\n", window, cx)
11113 });
11114 assert!(cx.read(|cx| editor.is_dirty(cx)));
11115 let _formatting_request_signal = fake_server
11116 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
11117 assert_eq!(
11118 params.text_document.uri,
11119 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11120 );
11121 assert_eq!(params.options.tab_size, 8);
11122 Ok(Some(vec![]))
11123 });
11124 let save = editor
11125 .update_in(cx, |editor, window, cx| {
11126 editor.save(
11127 SaveOptions {
11128 format: true,
11129 autosave: false,
11130 },
11131 project.clone(),
11132 window,
11133 cx,
11134 )
11135 })
11136 .unwrap();
11137 cx.executor().start_waiting();
11138 save.await;
11139 }
11140}
11141
11142#[gpui::test]
11143async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
11144 init_test(cx, |settings| {
11145 settings.defaults.ensure_final_newline_on_save = Some(false);
11146 });
11147
11148 let fs = FakeFs::new(cx.executor());
11149 fs.insert_file(path!("/file.txt"), "foo".into()).await;
11150
11151 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
11152
11153 let buffer = project
11154 .update(cx, |project, cx| {
11155 project.open_local_buffer(path!("/file.txt"), cx)
11156 })
11157 .await
11158 .unwrap();
11159
11160 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11161 let (editor, cx) = cx.add_window_view(|window, cx| {
11162 build_editor_with_project(project.clone(), buffer, window, cx)
11163 });
11164 editor.update_in(cx, |editor, window, cx| {
11165 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
11166 s.select_ranges([0..0])
11167 });
11168 });
11169 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11170
11171 editor.update_in(cx, |editor, window, cx| {
11172 editor.handle_input("\n", window, cx)
11173 });
11174 cx.run_until_parked();
11175 save(&editor, &project, cx).await;
11176 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11177
11178 editor.update_in(cx, |editor, window, cx| {
11179 editor.undo(&Default::default(), window, cx);
11180 });
11181 save(&editor, &project, cx).await;
11182 assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11183
11184 editor.update_in(cx, |editor, window, cx| {
11185 editor.redo(&Default::default(), window, cx);
11186 });
11187 cx.run_until_parked();
11188 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11189
11190 async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
11191 let save = editor
11192 .update_in(cx, |editor, window, cx| {
11193 editor.save(
11194 SaveOptions {
11195 format: true,
11196 autosave: false,
11197 },
11198 project.clone(),
11199 window,
11200 cx,
11201 )
11202 })
11203 .unwrap();
11204 cx.executor().start_waiting();
11205 save.await;
11206 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11207 }
11208}
11209
11210#[gpui::test]
11211async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
11212 init_test(cx, |_| {});
11213
11214 let cols = 4;
11215 let rows = 10;
11216 let sample_text_1 = sample_text(rows, cols, 'a');
11217 assert_eq!(
11218 sample_text_1,
11219 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
11220 );
11221 let sample_text_2 = sample_text(rows, cols, 'l');
11222 assert_eq!(
11223 sample_text_2,
11224 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
11225 );
11226 let sample_text_3 = sample_text(rows, cols, 'v');
11227 assert_eq!(
11228 sample_text_3,
11229 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
11230 );
11231
11232 let fs = FakeFs::new(cx.executor());
11233 fs.insert_tree(
11234 path!("/a"),
11235 json!({
11236 "main.rs": sample_text_1,
11237 "other.rs": sample_text_2,
11238 "lib.rs": sample_text_3,
11239 }),
11240 )
11241 .await;
11242
11243 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11244 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11245 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11246
11247 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11248 language_registry.add(rust_lang());
11249 let mut fake_servers = language_registry.register_fake_lsp(
11250 "Rust",
11251 FakeLspAdapter {
11252 capabilities: lsp::ServerCapabilities {
11253 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11254 ..Default::default()
11255 },
11256 ..Default::default()
11257 },
11258 );
11259
11260 let worktree = project.update(cx, |project, cx| {
11261 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
11262 assert_eq!(worktrees.len(), 1);
11263 worktrees.pop().unwrap()
11264 });
11265 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
11266
11267 let buffer_1 = project
11268 .update(cx, |project, cx| {
11269 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
11270 })
11271 .await
11272 .unwrap();
11273 let buffer_2 = project
11274 .update(cx, |project, cx| {
11275 project.open_buffer((worktree_id, rel_path("other.rs")), cx)
11276 })
11277 .await
11278 .unwrap();
11279 let buffer_3 = project
11280 .update(cx, |project, cx| {
11281 project.open_buffer((worktree_id, rel_path("lib.rs")), cx)
11282 })
11283 .await
11284 .unwrap();
11285
11286 let multi_buffer = cx.new(|cx| {
11287 let mut multi_buffer = MultiBuffer::new(ReadWrite);
11288 multi_buffer.push_excerpts(
11289 buffer_1.clone(),
11290 [
11291 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11292 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11293 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11294 ],
11295 cx,
11296 );
11297 multi_buffer.push_excerpts(
11298 buffer_2.clone(),
11299 [
11300 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11301 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11302 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11303 ],
11304 cx,
11305 );
11306 multi_buffer.push_excerpts(
11307 buffer_3.clone(),
11308 [
11309 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11310 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11311 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11312 ],
11313 cx,
11314 );
11315 multi_buffer
11316 });
11317 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
11318 Editor::new(
11319 EditorMode::full(),
11320 multi_buffer,
11321 Some(project.clone()),
11322 window,
11323 cx,
11324 )
11325 });
11326
11327 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11328 editor.change_selections(
11329 SelectionEffects::scroll(Autoscroll::Next),
11330 window,
11331 cx,
11332 |s| s.select_ranges(Some(1..2)),
11333 );
11334 editor.insert("|one|two|three|", window, cx);
11335 });
11336 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11337 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11338 editor.change_selections(
11339 SelectionEffects::scroll(Autoscroll::Next),
11340 window,
11341 cx,
11342 |s| s.select_ranges(Some(60..70)),
11343 );
11344 editor.insert("|four|five|six|", window, cx);
11345 });
11346 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11347
11348 // First two buffers should be edited, but not the third one.
11349 assert_eq!(
11350 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11351 "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}",
11352 );
11353 buffer_1.update(cx, |buffer, _| {
11354 assert!(buffer.is_dirty());
11355 assert_eq!(
11356 buffer.text(),
11357 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
11358 )
11359 });
11360 buffer_2.update(cx, |buffer, _| {
11361 assert!(buffer.is_dirty());
11362 assert_eq!(
11363 buffer.text(),
11364 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
11365 )
11366 });
11367 buffer_3.update(cx, |buffer, _| {
11368 assert!(!buffer.is_dirty());
11369 assert_eq!(buffer.text(), sample_text_3,)
11370 });
11371 cx.executor().run_until_parked();
11372
11373 cx.executor().start_waiting();
11374 let save = multi_buffer_editor
11375 .update_in(cx, |editor, window, cx| {
11376 editor.save(
11377 SaveOptions {
11378 format: true,
11379 autosave: false,
11380 },
11381 project.clone(),
11382 window,
11383 cx,
11384 )
11385 })
11386 .unwrap();
11387
11388 let fake_server = fake_servers.next().await.unwrap();
11389 fake_server
11390 .server
11391 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
11392 Ok(Some(vec![lsp::TextEdit::new(
11393 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11394 format!("[{} formatted]", params.text_document.uri),
11395 )]))
11396 })
11397 .detach();
11398 save.await;
11399
11400 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
11401 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
11402 assert_eq!(
11403 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11404 uri!(
11405 "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}"
11406 ),
11407 );
11408 buffer_1.update(cx, |buffer, _| {
11409 assert!(!buffer.is_dirty());
11410 assert_eq!(
11411 buffer.text(),
11412 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
11413 )
11414 });
11415 buffer_2.update(cx, |buffer, _| {
11416 assert!(!buffer.is_dirty());
11417 assert_eq!(
11418 buffer.text(),
11419 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
11420 )
11421 });
11422 buffer_3.update(cx, |buffer, _| {
11423 assert!(!buffer.is_dirty());
11424 assert_eq!(buffer.text(), sample_text_3,)
11425 });
11426}
11427
11428#[gpui::test]
11429async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
11430 init_test(cx, |_| {});
11431
11432 let fs = FakeFs::new(cx.executor());
11433 fs.insert_tree(
11434 path!("/dir"),
11435 json!({
11436 "file1.rs": "fn main() { println!(\"hello\"); }",
11437 "file2.rs": "fn test() { println!(\"test\"); }",
11438 "file3.rs": "fn other() { println!(\"other\"); }\n",
11439 }),
11440 )
11441 .await;
11442
11443 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
11444 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11445 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11446
11447 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11448 language_registry.add(rust_lang());
11449
11450 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
11451 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
11452
11453 // Open three buffers
11454 let buffer_1 = project
11455 .update(cx, |project, cx| {
11456 project.open_buffer((worktree_id, rel_path("file1.rs")), cx)
11457 })
11458 .await
11459 .unwrap();
11460 let buffer_2 = project
11461 .update(cx, |project, cx| {
11462 project.open_buffer((worktree_id, rel_path("file2.rs")), cx)
11463 })
11464 .await
11465 .unwrap();
11466 let buffer_3 = project
11467 .update(cx, |project, cx| {
11468 project.open_buffer((worktree_id, rel_path("file3.rs")), cx)
11469 })
11470 .await
11471 .unwrap();
11472
11473 // Create a multi-buffer with all three buffers
11474 let multi_buffer = cx.new(|cx| {
11475 let mut multi_buffer = MultiBuffer::new(ReadWrite);
11476 multi_buffer.push_excerpts(
11477 buffer_1.clone(),
11478 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11479 cx,
11480 );
11481 multi_buffer.push_excerpts(
11482 buffer_2.clone(),
11483 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11484 cx,
11485 );
11486 multi_buffer.push_excerpts(
11487 buffer_3.clone(),
11488 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11489 cx,
11490 );
11491 multi_buffer
11492 });
11493
11494 let editor = cx.new_window_entity(|window, cx| {
11495 Editor::new(
11496 EditorMode::full(),
11497 multi_buffer,
11498 Some(project.clone()),
11499 window,
11500 cx,
11501 )
11502 });
11503
11504 // Edit only the first buffer
11505 editor.update_in(cx, |editor, window, cx| {
11506 editor.change_selections(
11507 SelectionEffects::scroll(Autoscroll::Next),
11508 window,
11509 cx,
11510 |s| s.select_ranges(Some(10..10)),
11511 );
11512 editor.insert("// edited", window, cx);
11513 });
11514
11515 // Verify that only buffer 1 is dirty
11516 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
11517 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11518 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11519
11520 // Get write counts after file creation (files were created with initial content)
11521 // We expect each file to have been written once during creation
11522 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
11523 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
11524 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
11525
11526 // Perform autosave
11527 let save_task = editor.update_in(cx, |editor, window, cx| {
11528 editor.save(
11529 SaveOptions {
11530 format: true,
11531 autosave: true,
11532 },
11533 project.clone(),
11534 window,
11535 cx,
11536 )
11537 });
11538 save_task.await.unwrap();
11539
11540 // Only the dirty buffer should have been saved
11541 assert_eq!(
11542 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
11543 1,
11544 "Buffer 1 was dirty, so it should have been written once during autosave"
11545 );
11546 assert_eq!(
11547 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
11548 0,
11549 "Buffer 2 was clean, so it should not have been written during autosave"
11550 );
11551 assert_eq!(
11552 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
11553 0,
11554 "Buffer 3 was clean, so it should not have been written during autosave"
11555 );
11556
11557 // Verify buffer states after autosave
11558 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11559 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11560 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11561
11562 // Now perform a manual save (format = true)
11563 let save_task = editor.update_in(cx, |editor, window, cx| {
11564 editor.save(
11565 SaveOptions {
11566 format: true,
11567 autosave: false,
11568 },
11569 project.clone(),
11570 window,
11571 cx,
11572 )
11573 });
11574 save_task.await.unwrap();
11575
11576 // During manual save, clean buffers don't get written to disk
11577 // They just get did_save called for language server notifications
11578 assert_eq!(
11579 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
11580 1,
11581 "Buffer 1 should only have been written once total (during autosave, not manual save)"
11582 );
11583 assert_eq!(
11584 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
11585 0,
11586 "Buffer 2 should not have been written at all"
11587 );
11588 assert_eq!(
11589 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
11590 0,
11591 "Buffer 3 should not have been written at all"
11592 );
11593}
11594
11595async fn setup_range_format_test(
11596 cx: &mut TestAppContext,
11597) -> (
11598 Entity<Project>,
11599 Entity<Editor>,
11600 &mut gpui::VisualTestContext,
11601 lsp::FakeLanguageServer,
11602) {
11603 init_test(cx, |_| {});
11604
11605 let fs = FakeFs::new(cx.executor());
11606 fs.insert_file(path!("/file.rs"), Default::default()).await;
11607
11608 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11609
11610 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11611 language_registry.add(rust_lang());
11612 let mut fake_servers = language_registry.register_fake_lsp(
11613 "Rust",
11614 FakeLspAdapter {
11615 capabilities: lsp::ServerCapabilities {
11616 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
11617 ..lsp::ServerCapabilities::default()
11618 },
11619 ..FakeLspAdapter::default()
11620 },
11621 );
11622
11623 let buffer = project
11624 .update(cx, |project, cx| {
11625 project.open_local_buffer(path!("/file.rs"), cx)
11626 })
11627 .await
11628 .unwrap();
11629
11630 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11631 let (editor, cx) = cx.add_window_view(|window, cx| {
11632 build_editor_with_project(project.clone(), buffer, window, cx)
11633 });
11634
11635 cx.executor().start_waiting();
11636 let fake_server = fake_servers.next().await.unwrap();
11637
11638 (project, editor, cx, fake_server)
11639}
11640
11641#[gpui::test]
11642async fn test_range_format_on_save_success(cx: &mut TestAppContext) {
11643 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11644
11645 editor.update_in(cx, |editor, window, cx| {
11646 editor.set_text("one\ntwo\nthree\n", window, cx)
11647 });
11648 assert!(cx.read(|cx| editor.is_dirty(cx)));
11649
11650 let save = editor
11651 .update_in(cx, |editor, window, cx| {
11652 editor.save(
11653 SaveOptions {
11654 format: true,
11655 autosave: false,
11656 },
11657 project.clone(),
11658 window,
11659 cx,
11660 )
11661 })
11662 .unwrap();
11663 fake_server
11664 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
11665 assert_eq!(
11666 params.text_document.uri,
11667 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11668 );
11669 assert_eq!(params.options.tab_size, 4);
11670 Ok(Some(vec![lsp::TextEdit::new(
11671 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11672 ", ".to_string(),
11673 )]))
11674 })
11675 .next()
11676 .await;
11677 cx.executor().start_waiting();
11678 save.await;
11679 assert_eq!(
11680 editor.update(cx, |editor, cx| editor.text(cx)),
11681 "one, two\nthree\n"
11682 );
11683 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11684}
11685
11686#[gpui::test]
11687async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) {
11688 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11689
11690 editor.update_in(cx, |editor, window, cx| {
11691 editor.set_text("one\ntwo\nthree\n", window, cx)
11692 });
11693 assert!(cx.read(|cx| editor.is_dirty(cx)));
11694
11695 // Test that save still works when formatting hangs
11696 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
11697 move |params, _| async move {
11698 assert_eq!(
11699 params.text_document.uri,
11700 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11701 );
11702 futures::future::pending::<()>().await;
11703 unreachable!()
11704 },
11705 );
11706 let save = editor
11707 .update_in(cx, |editor, window, cx| {
11708 editor.save(
11709 SaveOptions {
11710 format: true,
11711 autosave: false,
11712 },
11713 project.clone(),
11714 window,
11715 cx,
11716 )
11717 })
11718 .unwrap();
11719 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11720 cx.executor().start_waiting();
11721 save.await;
11722 assert_eq!(
11723 editor.update(cx, |editor, cx| editor.text(cx)),
11724 "one\ntwo\nthree\n"
11725 );
11726 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11727}
11728
11729#[gpui::test]
11730async fn test_range_format_not_called_for_clean_buffer(cx: &mut TestAppContext) {
11731 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11732
11733 // Buffer starts clean, no formatting should be requested
11734 let save = editor
11735 .update_in(cx, |editor, window, cx| {
11736 editor.save(
11737 SaveOptions {
11738 format: false,
11739 autosave: false,
11740 },
11741 project.clone(),
11742 window,
11743 cx,
11744 )
11745 })
11746 .unwrap();
11747 let _pending_format_request = fake_server
11748 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
11749 panic!("Should not be invoked");
11750 })
11751 .next();
11752 cx.executor().start_waiting();
11753 save.await;
11754 cx.run_until_parked();
11755}
11756
11757#[gpui::test]
11758async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppContext) {
11759 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11760
11761 // Set Rust language override and assert overridden tabsize is sent to language server
11762 update_test_language_settings(cx, |settings| {
11763 settings.languages.0.insert(
11764 "Rust".into(),
11765 LanguageSettingsContent {
11766 tab_size: NonZeroU32::new(8),
11767 ..Default::default()
11768 },
11769 );
11770 });
11771
11772 editor.update_in(cx, |editor, window, cx| {
11773 editor.set_text("something_new\n", window, cx)
11774 });
11775 assert!(cx.read(|cx| editor.is_dirty(cx)));
11776 let save = editor
11777 .update_in(cx, |editor, window, cx| {
11778 editor.save(
11779 SaveOptions {
11780 format: true,
11781 autosave: false,
11782 },
11783 project.clone(),
11784 window,
11785 cx,
11786 )
11787 })
11788 .unwrap();
11789 fake_server
11790 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
11791 assert_eq!(
11792 params.text_document.uri,
11793 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11794 );
11795 assert_eq!(params.options.tab_size, 8);
11796 Ok(Some(Vec::new()))
11797 })
11798 .next()
11799 .await;
11800 save.await;
11801}
11802
11803#[gpui::test]
11804async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
11805 init_test(cx, |settings| {
11806 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
11807 Formatter::LanguageServer { name: None },
11808 )))
11809 });
11810
11811 let fs = FakeFs::new(cx.executor());
11812 fs.insert_file(path!("/file.rs"), Default::default()).await;
11813
11814 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11815
11816 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11817 language_registry.add(Arc::new(Language::new(
11818 LanguageConfig {
11819 name: "Rust".into(),
11820 matcher: LanguageMatcher {
11821 path_suffixes: vec!["rs".to_string()],
11822 ..Default::default()
11823 },
11824 ..LanguageConfig::default()
11825 },
11826 Some(tree_sitter_rust::LANGUAGE.into()),
11827 )));
11828 update_test_language_settings(cx, |settings| {
11829 // Enable Prettier formatting for the same buffer, and ensure
11830 // LSP is called instead of Prettier.
11831 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
11832 });
11833 let mut fake_servers = language_registry.register_fake_lsp(
11834 "Rust",
11835 FakeLspAdapter {
11836 capabilities: lsp::ServerCapabilities {
11837 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11838 ..Default::default()
11839 },
11840 ..Default::default()
11841 },
11842 );
11843
11844 let buffer = project
11845 .update(cx, |project, cx| {
11846 project.open_local_buffer(path!("/file.rs"), cx)
11847 })
11848 .await
11849 .unwrap();
11850
11851 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11852 let (editor, cx) = cx.add_window_view(|window, cx| {
11853 build_editor_with_project(project.clone(), buffer, window, cx)
11854 });
11855 editor.update_in(cx, |editor, window, cx| {
11856 editor.set_text("one\ntwo\nthree\n", window, cx)
11857 });
11858
11859 cx.executor().start_waiting();
11860 let fake_server = fake_servers.next().await.unwrap();
11861
11862 let format = editor
11863 .update_in(cx, |editor, window, cx| {
11864 editor.perform_format(
11865 project.clone(),
11866 FormatTrigger::Manual,
11867 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
11868 window,
11869 cx,
11870 )
11871 })
11872 .unwrap();
11873 fake_server
11874 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
11875 assert_eq!(
11876 params.text_document.uri,
11877 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11878 );
11879 assert_eq!(params.options.tab_size, 4);
11880 Ok(Some(vec![lsp::TextEdit::new(
11881 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11882 ", ".to_string(),
11883 )]))
11884 })
11885 .next()
11886 .await;
11887 cx.executor().start_waiting();
11888 format.await;
11889 assert_eq!(
11890 editor.update(cx, |editor, cx| editor.text(cx)),
11891 "one, two\nthree\n"
11892 );
11893
11894 editor.update_in(cx, |editor, window, cx| {
11895 editor.set_text("one\ntwo\nthree\n", window, cx)
11896 });
11897 // Ensure we don't lock if formatting hangs.
11898 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11899 move |params, _| async move {
11900 assert_eq!(
11901 params.text_document.uri,
11902 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11903 );
11904 futures::future::pending::<()>().await;
11905 unreachable!()
11906 },
11907 );
11908 let format = editor
11909 .update_in(cx, |editor, window, cx| {
11910 editor.perform_format(
11911 project,
11912 FormatTrigger::Manual,
11913 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
11914 window,
11915 cx,
11916 )
11917 })
11918 .unwrap();
11919 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11920 cx.executor().start_waiting();
11921 format.await;
11922 assert_eq!(
11923 editor.update(cx, |editor, cx| editor.text(cx)),
11924 "one\ntwo\nthree\n"
11925 );
11926}
11927
11928#[gpui::test]
11929async fn test_multiple_formatters(cx: &mut TestAppContext) {
11930 init_test(cx, |settings| {
11931 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
11932 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
11933 Formatter::LanguageServer { name: None },
11934 Formatter::CodeAction("code-action-1".into()),
11935 Formatter::CodeAction("code-action-2".into()),
11936 ])))
11937 });
11938
11939 let fs = FakeFs::new(cx.executor());
11940 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
11941 .await;
11942
11943 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11944 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11945 language_registry.add(rust_lang());
11946
11947 let mut fake_servers = language_registry.register_fake_lsp(
11948 "Rust",
11949 FakeLspAdapter {
11950 capabilities: lsp::ServerCapabilities {
11951 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11952 execute_command_provider: Some(lsp::ExecuteCommandOptions {
11953 commands: vec!["the-command-for-code-action-1".into()],
11954 ..Default::default()
11955 }),
11956 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
11957 ..Default::default()
11958 },
11959 ..Default::default()
11960 },
11961 );
11962
11963 let buffer = project
11964 .update(cx, |project, cx| {
11965 project.open_local_buffer(path!("/file.rs"), cx)
11966 })
11967 .await
11968 .unwrap();
11969
11970 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11971 let (editor, cx) = cx.add_window_view(|window, cx| {
11972 build_editor_with_project(project.clone(), buffer, window, cx)
11973 });
11974
11975 cx.executor().start_waiting();
11976
11977 let fake_server = fake_servers.next().await.unwrap();
11978 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11979 move |_params, _| async move {
11980 Ok(Some(vec![lsp::TextEdit::new(
11981 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
11982 "applied-formatting\n".to_string(),
11983 )]))
11984 },
11985 );
11986 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
11987 move |params, _| async move {
11988 let requested_code_actions = params.context.only.expect("Expected code action request");
11989 assert_eq!(requested_code_actions.len(), 1);
11990
11991 let uri = lsp::Uri::from_file_path(path!("/file.rs")).unwrap();
11992 let code_action = match requested_code_actions[0].as_str() {
11993 "code-action-1" => lsp::CodeAction {
11994 kind: Some("code-action-1".into()),
11995 edit: Some(lsp::WorkspaceEdit::new(
11996 [(
11997 uri,
11998 vec![lsp::TextEdit::new(
11999 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12000 "applied-code-action-1-edit\n".to_string(),
12001 )],
12002 )]
12003 .into_iter()
12004 .collect(),
12005 )),
12006 command: Some(lsp::Command {
12007 command: "the-command-for-code-action-1".into(),
12008 ..Default::default()
12009 }),
12010 ..Default::default()
12011 },
12012 "code-action-2" => lsp::CodeAction {
12013 kind: Some("code-action-2".into()),
12014 edit: Some(lsp::WorkspaceEdit::new(
12015 [(
12016 uri,
12017 vec![lsp::TextEdit::new(
12018 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12019 "applied-code-action-2-edit\n".to_string(),
12020 )],
12021 )]
12022 .into_iter()
12023 .collect(),
12024 )),
12025 ..Default::default()
12026 },
12027 req => panic!("Unexpected code action request: {:?}", req),
12028 };
12029 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
12030 code_action,
12031 )]))
12032 },
12033 );
12034
12035 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
12036 move |params, _| async move { Ok(params) }
12037 });
12038
12039 let command_lock = Arc::new(futures::lock::Mutex::new(()));
12040 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
12041 let fake = fake_server.clone();
12042 let lock = command_lock.clone();
12043 move |params, _| {
12044 assert_eq!(params.command, "the-command-for-code-action-1");
12045 let fake = fake.clone();
12046 let lock = lock.clone();
12047 async move {
12048 lock.lock().await;
12049 fake.server
12050 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
12051 label: None,
12052 edit: lsp::WorkspaceEdit {
12053 changes: Some(
12054 [(
12055 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
12056 vec![lsp::TextEdit {
12057 range: lsp::Range::new(
12058 lsp::Position::new(0, 0),
12059 lsp::Position::new(0, 0),
12060 ),
12061 new_text: "applied-code-action-1-command\n".into(),
12062 }],
12063 )]
12064 .into_iter()
12065 .collect(),
12066 ),
12067 ..Default::default()
12068 },
12069 })
12070 .await
12071 .into_response()
12072 .unwrap();
12073 Ok(Some(json!(null)))
12074 }
12075 }
12076 });
12077
12078 cx.executor().start_waiting();
12079 editor
12080 .update_in(cx, |editor, window, cx| {
12081 editor.perform_format(
12082 project.clone(),
12083 FormatTrigger::Manual,
12084 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12085 window,
12086 cx,
12087 )
12088 })
12089 .unwrap()
12090 .await;
12091 editor.update(cx, |editor, cx| {
12092 assert_eq!(
12093 editor.text(cx),
12094 r#"
12095 applied-code-action-2-edit
12096 applied-code-action-1-command
12097 applied-code-action-1-edit
12098 applied-formatting
12099 one
12100 two
12101 three
12102 "#
12103 .unindent()
12104 );
12105 });
12106
12107 editor.update_in(cx, |editor, window, cx| {
12108 editor.undo(&Default::default(), window, cx);
12109 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12110 });
12111
12112 // Perform a manual edit while waiting for an LSP command
12113 // that's being run as part of a formatting code action.
12114 let lock_guard = command_lock.lock().await;
12115 let format = editor
12116 .update_in(cx, |editor, window, cx| {
12117 editor.perform_format(
12118 project.clone(),
12119 FormatTrigger::Manual,
12120 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12121 window,
12122 cx,
12123 )
12124 })
12125 .unwrap();
12126 cx.run_until_parked();
12127 editor.update(cx, |editor, cx| {
12128 assert_eq!(
12129 editor.text(cx),
12130 r#"
12131 applied-code-action-1-edit
12132 applied-formatting
12133 one
12134 two
12135 three
12136 "#
12137 .unindent()
12138 );
12139
12140 editor.buffer.update(cx, |buffer, cx| {
12141 let ix = buffer.len(cx);
12142 buffer.edit([(ix..ix, "edited\n")], None, cx);
12143 });
12144 });
12145
12146 // Allow the LSP command to proceed. Because the buffer was edited,
12147 // the second code action will not be run.
12148 drop(lock_guard);
12149 format.await;
12150 editor.update_in(cx, |editor, window, cx| {
12151 assert_eq!(
12152 editor.text(cx),
12153 r#"
12154 applied-code-action-1-command
12155 applied-code-action-1-edit
12156 applied-formatting
12157 one
12158 two
12159 three
12160 edited
12161 "#
12162 .unindent()
12163 );
12164
12165 // The manual edit is undone first, because it is the last thing the user did
12166 // (even though the command completed afterwards).
12167 editor.undo(&Default::default(), window, cx);
12168 assert_eq!(
12169 editor.text(cx),
12170 r#"
12171 applied-code-action-1-command
12172 applied-code-action-1-edit
12173 applied-formatting
12174 one
12175 two
12176 three
12177 "#
12178 .unindent()
12179 );
12180
12181 // All the formatting (including the command, which completed after the manual edit)
12182 // is undone together.
12183 editor.undo(&Default::default(), window, cx);
12184 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12185 });
12186}
12187
12188#[gpui::test]
12189async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
12190 init_test(cx, |settings| {
12191 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
12192 Formatter::LanguageServer { name: None },
12193 ])))
12194 });
12195
12196 let fs = FakeFs::new(cx.executor());
12197 fs.insert_file(path!("/file.ts"), Default::default()).await;
12198
12199 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12200
12201 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12202 language_registry.add(Arc::new(Language::new(
12203 LanguageConfig {
12204 name: "TypeScript".into(),
12205 matcher: LanguageMatcher {
12206 path_suffixes: vec!["ts".to_string()],
12207 ..Default::default()
12208 },
12209 ..LanguageConfig::default()
12210 },
12211 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
12212 )));
12213 update_test_language_settings(cx, |settings| {
12214 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
12215 });
12216 let mut fake_servers = language_registry.register_fake_lsp(
12217 "TypeScript",
12218 FakeLspAdapter {
12219 capabilities: lsp::ServerCapabilities {
12220 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
12221 ..Default::default()
12222 },
12223 ..Default::default()
12224 },
12225 );
12226
12227 let buffer = project
12228 .update(cx, |project, cx| {
12229 project.open_local_buffer(path!("/file.ts"), cx)
12230 })
12231 .await
12232 .unwrap();
12233
12234 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12235 let (editor, cx) = cx.add_window_view(|window, cx| {
12236 build_editor_with_project(project.clone(), buffer, window, cx)
12237 });
12238 editor.update_in(cx, |editor, window, cx| {
12239 editor.set_text(
12240 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12241 window,
12242 cx,
12243 )
12244 });
12245
12246 cx.executor().start_waiting();
12247 let fake_server = fake_servers.next().await.unwrap();
12248
12249 let format = editor
12250 .update_in(cx, |editor, window, cx| {
12251 editor.perform_code_action_kind(
12252 project.clone(),
12253 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12254 window,
12255 cx,
12256 )
12257 })
12258 .unwrap();
12259 fake_server
12260 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
12261 assert_eq!(
12262 params.text_document.uri,
12263 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12264 );
12265 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
12266 lsp::CodeAction {
12267 title: "Organize Imports".to_string(),
12268 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
12269 edit: Some(lsp::WorkspaceEdit {
12270 changes: Some(
12271 [(
12272 params.text_document.uri.clone(),
12273 vec![lsp::TextEdit::new(
12274 lsp::Range::new(
12275 lsp::Position::new(1, 0),
12276 lsp::Position::new(2, 0),
12277 ),
12278 "".to_string(),
12279 )],
12280 )]
12281 .into_iter()
12282 .collect(),
12283 ),
12284 ..Default::default()
12285 }),
12286 ..Default::default()
12287 },
12288 )]))
12289 })
12290 .next()
12291 .await;
12292 cx.executor().start_waiting();
12293 format.await;
12294 assert_eq!(
12295 editor.update(cx, |editor, cx| editor.text(cx)),
12296 "import { a } from 'module';\n\nconst x = a;\n"
12297 );
12298
12299 editor.update_in(cx, |editor, window, cx| {
12300 editor.set_text(
12301 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12302 window,
12303 cx,
12304 )
12305 });
12306 // Ensure we don't lock if code action hangs.
12307 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
12308 move |params, _| async move {
12309 assert_eq!(
12310 params.text_document.uri,
12311 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12312 );
12313 futures::future::pending::<()>().await;
12314 unreachable!()
12315 },
12316 );
12317 let format = editor
12318 .update_in(cx, |editor, window, cx| {
12319 editor.perform_code_action_kind(
12320 project,
12321 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12322 window,
12323 cx,
12324 )
12325 })
12326 .unwrap();
12327 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
12328 cx.executor().start_waiting();
12329 format.await;
12330 assert_eq!(
12331 editor.update(cx, |editor, cx| editor.text(cx)),
12332 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
12333 );
12334}
12335
12336#[gpui::test]
12337async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
12338 init_test(cx, |_| {});
12339
12340 let mut cx = EditorLspTestContext::new_rust(
12341 lsp::ServerCapabilities {
12342 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12343 ..Default::default()
12344 },
12345 cx,
12346 )
12347 .await;
12348
12349 cx.set_state(indoc! {"
12350 one.twoˇ
12351 "});
12352
12353 // The format request takes a long time. When it completes, it inserts
12354 // a newline and an indent before the `.`
12355 cx.lsp
12356 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
12357 let executor = cx.background_executor().clone();
12358 async move {
12359 executor.timer(Duration::from_millis(100)).await;
12360 Ok(Some(vec![lsp::TextEdit {
12361 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
12362 new_text: "\n ".into(),
12363 }]))
12364 }
12365 });
12366
12367 // Submit a format request.
12368 let format_1 = cx
12369 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12370 .unwrap();
12371 cx.executor().run_until_parked();
12372
12373 // Submit a second format request.
12374 let format_2 = cx
12375 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12376 .unwrap();
12377 cx.executor().run_until_parked();
12378
12379 // Wait for both format requests to complete
12380 cx.executor().advance_clock(Duration::from_millis(200));
12381 cx.executor().start_waiting();
12382 format_1.await.unwrap();
12383 cx.executor().start_waiting();
12384 format_2.await.unwrap();
12385
12386 // The formatting edits only happens once.
12387 cx.assert_editor_state(indoc! {"
12388 one
12389 .twoˇ
12390 "});
12391}
12392
12393#[gpui::test]
12394async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
12395 init_test(cx, |settings| {
12396 settings.defaults.formatter = Some(SelectedFormatter::Auto)
12397 });
12398
12399 let mut cx = EditorLspTestContext::new_rust(
12400 lsp::ServerCapabilities {
12401 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12402 ..Default::default()
12403 },
12404 cx,
12405 )
12406 .await;
12407
12408 // Set up a buffer white some trailing whitespace and no trailing newline.
12409 cx.set_state(
12410 &[
12411 "one ", //
12412 "twoˇ", //
12413 "three ", //
12414 "four", //
12415 ]
12416 .join("\n"),
12417 );
12418
12419 // Record which buffer changes have been sent to the language server
12420 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
12421 cx.lsp
12422 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
12423 let buffer_changes = buffer_changes.clone();
12424 move |params, _| {
12425 buffer_changes.lock().extend(
12426 params
12427 .content_changes
12428 .into_iter()
12429 .map(|e| (e.range.unwrap(), e.text)),
12430 );
12431 }
12432 });
12433
12434 // Handle formatting requests to the language server.
12435 cx.lsp
12436 .set_request_handler::<lsp::request::Formatting, _, _>({
12437 let buffer_changes = buffer_changes.clone();
12438 move |_, _| {
12439 let buffer_changes = buffer_changes.clone();
12440 // Insert blank lines between each line of the buffer.
12441 async move {
12442 // When formatting is requested, trailing whitespace has already been stripped,
12443 // and the trailing newline has already been added.
12444 assert_eq!(
12445 &buffer_changes.lock()[1..],
12446 &[
12447 (
12448 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
12449 "".into()
12450 ),
12451 (
12452 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
12453 "".into()
12454 ),
12455 (
12456 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
12457 "\n".into()
12458 ),
12459 ]
12460 );
12461
12462 Ok(Some(vec![
12463 lsp::TextEdit {
12464 range: lsp::Range::new(
12465 lsp::Position::new(1, 0),
12466 lsp::Position::new(1, 0),
12467 ),
12468 new_text: "\n".into(),
12469 },
12470 lsp::TextEdit {
12471 range: lsp::Range::new(
12472 lsp::Position::new(2, 0),
12473 lsp::Position::new(2, 0),
12474 ),
12475 new_text: "\n".into(),
12476 },
12477 ]))
12478 }
12479 }
12480 });
12481
12482 // Submit a format request.
12483 let format = cx
12484 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12485 .unwrap();
12486
12487 cx.run_until_parked();
12488 // After formatting the buffer, the trailing whitespace is stripped,
12489 // a newline is appended, and the edits provided by the language server
12490 // have been applied.
12491 format.await.unwrap();
12492
12493 cx.assert_editor_state(
12494 &[
12495 "one", //
12496 "", //
12497 "twoˇ", //
12498 "", //
12499 "three", //
12500 "four", //
12501 "", //
12502 ]
12503 .join("\n"),
12504 );
12505
12506 // Undoing the formatting undoes the trailing whitespace removal, the
12507 // trailing newline, and the LSP edits.
12508 cx.update_buffer(|buffer, cx| buffer.undo(cx));
12509 cx.assert_editor_state(
12510 &[
12511 "one ", //
12512 "twoˇ", //
12513 "three ", //
12514 "four", //
12515 ]
12516 .join("\n"),
12517 );
12518}
12519
12520#[gpui::test]
12521async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
12522 cx: &mut TestAppContext,
12523) {
12524 init_test(cx, |_| {});
12525
12526 cx.update(|cx| {
12527 cx.update_global::<SettingsStore, _>(|settings, cx| {
12528 settings.update_user_settings(cx, |settings| {
12529 settings.editor.auto_signature_help = Some(true);
12530 });
12531 });
12532 });
12533
12534 let mut cx = EditorLspTestContext::new_rust(
12535 lsp::ServerCapabilities {
12536 signature_help_provider: Some(lsp::SignatureHelpOptions {
12537 ..Default::default()
12538 }),
12539 ..Default::default()
12540 },
12541 cx,
12542 )
12543 .await;
12544
12545 let language = Language::new(
12546 LanguageConfig {
12547 name: "Rust".into(),
12548 brackets: BracketPairConfig {
12549 pairs: vec![
12550 BracketPair {
12551 start: "{".to_string(),
12552 end: "}".to_string(),
12553 close: true,
12554 surround: true,
12555 newline: true,
12556 },
12557 BracketPair {
12558 start: "(".to_string(),
12559 end: ")".to_string(),
12560 close: true,
12561 surround: true,
12562 newline: true,
12563 },
12564 BracketPair {
12565 start: "/*".to_string(),
12566 end: " */".to_string(),
12567 close: true,
12568 surround: true,
12569 newline: true,
12570 },
12571 BracketPair {
12572 start: "[".to_string(),
12573 end: "]".to_string(),
12574 close: false,
12575 surround: false,
12576 newline: true,
12577 },
12578 BracketPair {
12579 start: "\"".to_string(),
12580 end: "\"".to_string(),
12581 close: true,
12582 surround: true,
12583 newline: false,
12584 },
12585 BracketPair {
12586 start: "<".to_string(),
12587 end: ">".to_string(),
12588 close: false,
12589 surround: true,
12590 newline: true,
12591 },
12592 ],
12593 ..Default::default()
12594 },
12595 autoclose_before: "})]".to_string(),
12596 ..Default::default()
12597 },
12598 Some(tree_sitter_rust::LANGUAGE.into()),
12599 );
12600 let language = Arc::new(language);
12601
12602 cx.language_registry().add(language.clone());
12603 cx.update_buffer(|buffer, cx| {
12604 buffer.set_language(Some(language), cx);
12605 });
12606
12607 cx.set_state(
12608 &r#"
12609 fn main() {
12610 sampleˇ
12611 }
12612 "#
12613 .unindent(),
12614 );
12615
12616 cx.update_editor(|editor, window, cx| {
12617 editor.handle_input("(", window, cx);
12618 });
12619 cx.assert_editor_state(
12620 &"
12621 fn main() {
12622 sample(ˇ)
12623 }
12624 "
12625 .unindent(),
12626 );
12627
12628 let mocked_response = lsp::SignatureHelp {
12629 signatures: vec![lsp::SignatureInformation {
12630 label: "fn sample(param1: u8, param2: u8)".to_string(),
12631 documentation: None,
12632 parameters: Some(vec![
12633 lsp::ParameterInformation {
12634 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12635 documentation: None,
12636 },
12637 lsp::ParameterInformation {
12638 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12639 documentation: None,
12640 },
12641 ]),
12642 active_parameter: None,
12643 }],
12644 active_signature: Some(0),
12645 active_parameter: Some(0),
12646 };
12647 handle_signature_help_request(&mut cx, mocked_response).await;
12648
12649 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12650 .await;
12651
12652 cx.editor(|editor, _, _| {
12653 let signature_help_state = editor.signature_help_state.popover().cloned();
12654 let signature = signature_help_state.unwrap();
12655 assert_eq!(
12656 signature.signatures[signature.current_signature].label,
12657 "fn sample(param1: u8, param2: u8)"
12658 );
12659 });
12660}
12661
12662#[gpui::test]
12663async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
12664 init_test(cx, |_| {});
12665
12666 cx.update(|cx| {
12667 cx.update_global::<SettingsStore, _>(|settings, cx| {
12668 settings.update_user_settings(cx, |settings| {
12669 settings.editor.auto_signature_help = Some(false);
12670 settings.editor.show_signature_help_after_edits = Some(false);
12671 });
12672 });
12673 });
12674
12675 let mut cx = EditorLspTestContext::new_rust(
12676 lsp::ServerCapabilities {
12677 signature_help_provider: Some(lsp::SignatureHelpOptions {
12678 ..Default::default()
12679 }),
12680 ..Default::default()
12681 },
12682 cx,
12683 )
12684 .await;
12685
12686 let language = Language::new(
12687 LanguageConfig {
12688 name: "Rust".into(),
12689 brackets: BracketPairConfig {
12690 pairs: vec![
12691 BracketPair {
12692 start: "{".to_string(),
12693 end: "}".to_string(),
12694 close: true,
12695 surround: true,
12696 newline: true,
12697 },
12698 BracketPair {
12699 start: "(".to_string(),
12700 end: ")".to_string(),
12701 close: true,
12702 surround: true,
12703 newline: true,
12704 },
12705 BracketPair {
12706 start: "/*".to_string(),
12707 end: " */".to_string(),
12708 close: true,
12709 surround: true,
12710 newline: true,
12711 },
12712 BracketPair {
12713 start: "[".to_string(),
12714 end: "]".to_string(),
12715 close: false,
12716 surround: false,
12717 newline: true,
12718 },
12719 BracketPair {
12720 start: "\"".to_string(),
12721 end: "\"".to_string(),
12722 close: true,
12723 surround: true,
12724 newline: false,
12725 },
12726 BracketPair {
12727 start: "<".to_string(),
12728 end: ">".to_string(),
12729 close: false,
12730 surround: true,
12731 newline: true,
12732 },
12733 ],
12734 ..Default::default()
12735 },
12736 autoclose_before: "})]".to_string(),
12737 ..Default::default()
12738 },
12739 Some(tree_sitter_rust::LANGUAGE.into()),
12740 );
12741 let language = Arc::new(language);
12742
12743 cx.language_registry().add(language.clone());
12744 cx.update_buffer(|buffer, cx| {
12745 buffer.set_language(Some(language), cx);
12746 });
12747
12748 // Ensure that signature_help is not called when no signature help is enabled.
12749 cx.set_state(
12750 &r#"
12751 fn main() {
12752 sampleˇ
12753 }
12754 "#
12755 .unindent(),
12756 );
12757 cx.update_editor(|editor, window, cx| {
12758 editor.handle_input("(", window, cx);
12759 });
12760 cx.assert_editor_state(
12761 &"
12762 fn main() {
12763 sample(ˇ)
12764 }
12765 "
12766 .unindent(),
12767 );
12768 cx.editor(|editor, _, _| {
12769 assert!(editor.signature_help_state.task().is_none());
12770 });
12771
12772 let mocked_response = lsp::SignatureHelp {
12773 signatures: vec![lsp::SignatureInformation {
12774 label: "fn sample(param1: u8, param2: u8)".to_string(),
12775 documentation: None,
12776 parameters: Some(vec![
12777 lsp::ParameterInformation {
12778 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12779 documentation: None,
12780 },
12781 lsp::ParameterInformation {
12782 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12783 documentation: None,
12784 },
12785 ]),
12786 active_parameter: None,
12787 }],
12788 active_signature: Some(0),
12789 active_parameter: Some(0),
12790 };
12791
12792 // Ensure that signature_help is called when enabled afte edits
12793 cx.update(|_, cx| {
12794 cx.update_global::<SettingsStore, _>(|settings, cx| {
12795 settings.update_user_settings(cx, |settings| {
12796 settings.editor.auto_signature_help = Some(false);
12797 settings.editor.show_signature_help_after_edits = Some(true);
12798 });
12799 });
12800 });
12801 cx.set_state(
12802 &r#"
12803 fn main() {
12804 sampleˇ
12805 }
12806 "#
12807 .unindent(),
12808 );
12809 cx.update_editor(|editor, window, cx| {
12810 editor.handle_input("(", window, cx);
12811 });
12812 cx.assert_editor_state(
12813 &"
12814 fn main() {
12815 sample(ˇ)
12816 }
12817 "
12818 .unindent(),
12819 );
12820 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12821 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12822 .await;
12823 cx.update_editor(|editor, _, _| {
12824 let signature_help_state = editor.signature_help_state.popover().cloned();
12825 assert!(signature_help_state.is_some());
12826 let signature = signature_help_state.unwrap();
12827 assert_eq!(
12828 signature.signatures[signature.current_signature].label,
12829 "fn sample(param1: u8, param2: u8)"
12830 );
12831 editor.signature_help_state = SignatureHelpState::default();
12832 });
12833
12834 // Ensure that signature_help is called when auto signature help override is enabled
12835 cx.update(|_, cx| {
12836 cx.update_global::<SettingsStore, _>(|settings, cx| {
12837 settings.update_user_settings(cx, |settings| {
12838 settings.editor.auto_signature_help = Some(true);
12839 settings.editor.show_signature_help_after_edits = Some(false);
12840 });
12841 });
12842 });
12843 cx.set_state(
12844 &r#"
12845 fn main() {
12846 sampleˇ
12847 }
12848 "#
12849 .unindent(),
12850 );
12851 cx.update_editor(|editor, window, cx| {
12852 editor.handle_input("(", window, cx);
12853 });
12854 cx.assert_editor_state(
12855 &"
12856 fn main() {
12857 sample(ˇ)
12858 }
12859 "
12860 .unindent(),
12861 );
12862 handle_signature_help_request(&mut cx, mocked_response).await;
12863 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12864 .await;
12865 cx.editor(|editor, _, _| {
12866 let signature_help_state = editor.signature_help_state.popover().cloned();
12867 assert!(signature_help_state.is_some());
12868 let signature = signature_help_state.unwrap();
12869 assert_eq!(
12870 signature.signatures[signature.current_signature].label,
12871 "fn sample(param1: u8, param2: u8)"
12872 );
12873 });
12874}
12875
12876#[gpui::test]
12877async fn test_signature_help(cx: &mut TestAppContext) {
12878 init_test(cx, |_| {});
12879 cx.update(|cx| {
12880 cx.update_global::<SettingsStore, _>(|settings, cx| {
12881 settings.update_user_settings(cx, |settings| {
12882 settings.editor.auto_signature_help = Some(true);
12883 });
12884 });
12885 });
12886
12887 let mut cx = EditorLspTestContext::new_rust(
12888 lsp::ServerCapabilities {
12889 signature_help_provider: Some(lsp::SignatureHelpOptions {
12890 ..Default::default()
12891 }),
12892 ..Default::default()
12893 },
12894 cx,
12895 )
12896 .await;
12897
12898 // A test that directly calls `show_signature_help`
12899 cx.update_editor(|editor, window, cx| {
12900 editor.show_signature_help(&ShowSignatureHelp, window, cx);
12901 });
12902
12903 let mocked_response = lsp::SignatureHelp {
12904 signatures: vec![lsp::SignatureInformation {
12905 label: "fn sample(param1: u8, param2: u8)".to_string(),
12906 documentation: None,
12907 parameters: Some(vec![
12908 lsp::ParameterInformation {
12909 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12910 documentation: None,
12911 },
12912 lsp::ParameterInformation {
12913 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12914 documentation: None,
12915 },
12916 ]),
12917 active_parameter: None,
12918 }],
12919 active_signature: Some(0),
12920 active_parameter: Some(0),
12921 };
12922 handle_signature_help_request(&mut cx, mocked_response).await;
12923
12924 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12925 .await;
12926
12927 cx.editor(|editor, _, _| {
12928 let signature_help_state = editor.signature_help_state.popover().cloned();
12929 assert!(signature_help_state.is_some());
12930 let signature = signature_help_state.unwrap();
12931 assert_eq!(
12932 signature.signatures[signature.current_signature].label,
12933 "fn sample(param1: u8, param2: u8)"
12934 );
12935 });
12936
12937 // When exiting outside from inside the brackets, `signature_help` is closed.
12938 cx.set_state(indoc! {"
12939 fn main() {
12940 sample(ˇ);
12941 }
12942
12943 fn sample(param1: u8, param2: u8) {}
12944 "});
12945
12946 cx.update_editor(|editor, window, cx| {
12947 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12948 s.select_ranges([0..0])
12949 });
12950 });
12951
12952 let mocked_response = lsp::SignatureHelp {
12953 signatures: Vec::new(),
12954 active_signature: None,
12955 active_parameter: None,
12956 };
12957 handle_signature_help_request(&mut cx, mocked_response).await;
12958
12959 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
12960 .await;
12961
12962 cx.editor(|editor, _, _| {
12963 assert!(!editor.signature_help_state.is_shown());
12964 });
12965
12966 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
12967 cx.set_state(indoc! {"
12968 fn main() {
12969 sample(ˇ);
12970 }
12971
12972 fn sample(param1: u8, param2: u8) {}
12973 "});
12974
12975 let mocked_response = lsp::SignatureHelp {
12976 signatures: vec![lsp::SignatureInformation {
12977 label: "fn sample(param1: u8, param2: u8)".to_string(),
12978 documentation: None,
12979 parameters: Some(vec![
12980 lsp::ParameterInformation {
12981 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12982 documentation: None,
12983 },
12984 lsp::ParameterInformation {
12985 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12986 documentation: None,
12987 },
12988 ]),
12989 active_parameter: None,
12990 }],
12991 active_signature: Some(0),
12992 active_parameter: Some(0),
12993 };
12994 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12995 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12996 .await;
12997 cx.editor(|editor, _, _| {
12998 assert!(editor.signature_help_state.is_shown());
12999 });
13000
13001 // Restore the popover with more parameter input
13002 cx.set_state(indoc! {"
13003 fn main() {
13004 sample(param1, param2ˇ);
13005 }
13006
13007 fn sample(param1: u8, param2: u8) {}
13008 "});
13009
13010 let mocked_response = lsp::SignatureHelp {
13011 signatures: vec![lsp::SignatureInformation {
13012 label: "fn sample(param1: u8, param2: u8)".to_string(),
13013 documentation: None,
13014 parameters: Some(vec![
13015 lsp::ParameterInformation {
13016 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13017 documentation: None,
13018 },
13019 lsp::ParameterInformation {
13020 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13021 documentation: None,
13022 },
13023 ]),
13024 active_parameter: None,
13025 }],
13026 active_signature: Some(0),
13027 active_parameter: Some(1),
13028 };
13029 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13030 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13031 .await;
13032
13033 // When selecting a range, the popover is gone.
13034 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
13035 cx.update_editor(|editor, window, cx| {
13036 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13037 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13038 })
13039 });
13040 cx.assert_editor_state(indoc! {"
13041 fn main() {
13042 sample(param1, «ˇparam2»);
13043 }
13044
13045 fn sample(param1: u8, param2: u8) {}
13046 "});
13047 cx.editor(|editor, _, _| {
13048 assert!(!editor.signature_help_state.is_shown());
13049 });
13050
13051 // When unselecting again, the popover is back if within the brackets.
13052 cx.update_editor(|editor, window, cx| {
13053 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13054 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13055 })
13056 });
13057 cx.assert_editor_state(indoc! {"
13058 fn main() {
13059 sample(param1, ˇparam2);
13060 }
13061
13062 fn sample(param1: u8, param2: u8) {}
13063 "});
13064 handle_signature_help_request(&mut cx, mocked_response).await;
13065 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13066 .await;
13067 cx.editor(|editor, _, _| {
13068 assert!(editor.signature_help_state.is_shown());
13069 });
13070
13071 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
13072 cx.update_editor(|editor, window, cx| {
13073 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13074 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
13075 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13076 })
13077 });
13078 cx.assert_editor_state(indoc! {"
13079 fn main() {
13080 sample(param1, ˇparam2);
13081 }
13082
13083 fn sample(param1: u8, param2: u8) {}
13084 "});
13085
13086 let mocked_response = lsp::SignatureHelp {
13087 signatures: vec![lsp::SignatureInformation {
13088 label: "fn sample(param1: u8, param2: u8)".to_string(),
13089 documentation: None,
13090 parameters: Some(vec![
13091 lsp::ParameterInformation {
13092 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13093 documentation: None,
13094 },
13095 lsp::ParameterInformation {
13096 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13097 documentation: None,
13098 },
13099 ]),
13100 active_parameter: None,
13101 }],
13102 active_signature: Some(0),
13103 active_parameter: Some(1),
13104 };
13105 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13106 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13107 .await;
13108 cx.update_editor(|editor, _, cx| {
13109 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
13110 });
13111 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13112 .await;
13113 cx.update_editor(|editor, window, cx| {
13114 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13115 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13116 })
13117 });
13118 cx.assert_editor_state(indoc! {"
13119 fn main() {
13120 sample(param1, «ˇparam2»);
13121 }
13122
13123 fn sample(param1: u8, param2: u8) {}
13124 "});
13125 cx.update_editor(|editor, window, cx| {
13126 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13127 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13128 })
13129 });
13130 cx.assert_editor_state(indoc! {"
13131 fn main() {
13132 sample(param1, ˇparam2);
13133 }
13134
13135 fn sample(param1: u8, param2: u8) {}
13136 "});
13137 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
13138 .await;
13139}
13140
13141#[gpui::test]
13142async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
13143 init_test(cx, |_| {});
13144
13145 let mut cx = EditorLspTestContext::new_rust(
13146 lsp::ServerCapabilities {
13147 signature_help_provider: Some(lsp::SignatureHelpOptions {
13148 ..Default::default()
13149 }),
13150 ..Default::default()
13151 },
13152 cx,
13153 )
13154 .await;
13155
13156 cx.set_state(indoc! {"
13157 fn main() {
13158 overloadedˇ
13159 }
13160 "});
13161
13162 cx.update_editor(|editor, window, cx| {
13163 editor.handle_input("(", window, cx);
13164 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13165 });
13166
13167 // Mock response with 3 signatures
13168 let mocked_response = lsp::SignatureHelp {
13169 signatures: vec![
13170 lsp::SignatureInformation {
13171 label: "fn overloaded(x: i32)".to_string(),
13172 documentation: None,
13173 parameters: Some(vec![lsp::ParameterInformation {
13174 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13175 documentation: None,
13176 }]),
13177 active_parameter: None,
13178 },
13179 lsp::SignatureInformation {
13180 label: "fn overloaded(x: i32, y: i32)".to_string(),
13181 documentation: None,
13182 parameters: Some(vec![
13183 lsp::ParameterInformation {
13184 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13185 documentation: None,
13186 },
13187 lsp::ParameterInformation {
13188 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13189 documentation: None,
13190 },
13191 ]),
13192 active_parameter: None,
13193 },
13194 lsp::SignatureInformation {
13195 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
13196 documentation: None,
13197 parameters: Some(vec![
13198 lsp::ParameterInformation {
13199 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13200 documentation: None,
13201 },
13202 lsp::ParameterInformation {
13203 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13204 documentation: None,
13205 },
13206 lsp::ParameterInformation {
13207 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
13208 documentation: None,
13209 },
13210 ]),
13211 active_parameter: None,
13212 },
13213 ],
13214 active_signature: Some(1),
13215 active_parameter: Some(0),
13216 };
13217 handle_signature_help_request(&mut cx, mocked_response).await;
13218
13219 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13220 .await;
13221
13222 // Verify we have multiple signatures and the right one is selected
13223 cx.editor(|editor, _, _| {
13224 let popover = editor.signature_help_state.popover().cloned().unwrap();
13225 assert_eq!(popover.signatures.len(), 3);
13226 // active_signature was 1, so that should be the current
13227 assert_eq!(popover.current_signature, 1);
13228 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
13229 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
13230 assert_eq!(
13231 popover.signatures[2].label,
13232 "fn overloaded(x: i32, y: i32, z: i32)"
13233 );
13234 });
13235
13236 // Test navigation functionality
13237 cx.update_editor(|editor, window, cx| {
13238 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13239 });
13240
13241 cx.editor(|editor, _, _| {
13242 let popover = editor.signature_help_state.popover().cloned().unwrap();
13243 assert_eq!(popover.current_signature, 2);
13244 });
13245
13246 // Test wrap around
13247 cx.update_editor(|editor, window, cx| {
13248 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13249 });
13250
13251 cx.editor(|editor, _, _| {
13252 let popover = editor.signature_help_state.popover().cloned().unwrap();
13253 assert_eq!(popover.current_signature, 0);
13254 });
13255
13256 // Test previous navigation
13257 cx.update_editor(|editor, window, cx| {
13258 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
13259 });
13260
13261 cx.editor(|editor, _, _| {
13262 let popover = editor.signature_help_state.popover().cloned().unwrap();
13263 assert_eq!(popover.current_signature, 2);
13264 });
13265}
13266
13267#[gpui::test]
13268async fn test_completion_mode(cx: &mut TestAppContext) {
13269 init_test(cx, |_| {});
13270 let mut cx = EditorLspTestContext::new_rust(
13271 lsp::ServerCapabilities {
13272 completion_provider: Some(lsp::CompletionOptions {
13273 resolve_provider: Some(true),
13274 ..Default::default()
13275 }),
13276 ..Default::default()
13277 },
13278 cx,
13279 )
13280 .await;
13281
13282 struct Run {
13283 run_description: &'static str,
13284 initial_state: String,
13285 buffer_marked_text: String,
13286 completion_label: &'static str,
13287 completion_text: &'static str,
13288 expected_with_insert_mode: String,
13289 expected_with_replace_mode: String,
13290 expected_with_replace_subsequence_mode: String,
13291 expected_with_replace_suffix_mode: String,
13292 }
13293
13294 let runs = [
13295 Run {
13296 run_description: "Start of word matches completion text",
13297 initial_state: "before ediˇ after".into(),
13298 buffer_marked_text: "before <edi|> after".into(),
13299 completion_label: "editor",
13300 completion_text: "editor",
13301 expected_with_insert_mode: "before editorˇ after".into(),
13302 expected_with_replace_mode: "before editorˇ after".into(),
13303 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13304 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13305 },
13306 Run {
13307 run_description: "Accept same text at the middle of the word",
13308 initial_state: "before ediˇtor after".into(),
13309 buffer_marked_text: "before <edi|tor> after".into(),
13310 completion_label: "editor",
13311 completion_text: "editor",
13312 expected_with_insert_mode: "before editorˇtor after".into(),
13313 expected_with_replace_mode: "before editorˇ after".into(),
13314 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13315 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13316 },
13317 Run {
13318 run_description: "End of word matches completion text -- cursor at end",
13319 initial_state: "before torˇ after".into(),
13320 buffer_marked_text: "before <tor|> after".into(),
13321 completion_label: "editor",
13322 completion_text: "editor",
13323 expected_with_insert_mode: "before editorˇ after".into(),
13324 expected_with_replace_mode: "before editorˇ after".into(),
13325 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13326 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13327 },
13328 Run {
13329 run_description: "End of word matches completion text -- cursor at start",
13330 initial_state: "before ˇtor after".into(),
13331 buffer_marked_text: "before <|tor> after".into(),
13332 completion_label: "editor",
13333 completion_text: "editor",
13334 expected_with_insert_mode: "before editorˇtor after".into(),
13335 expected_with_replace_mode: "before editorˇ after".into(),
13336 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13337 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13338 },
13339 Run {
13340 run_description: "Prepend text containing whitespace",
13341 initial_state: "pˇfield: bool".into(),
13342 buffer_marked_text: "<p|field>: bool".into(),
13343 completion_label: "pub ",
13344 completion_text: "pub ",
13345 expected_with_insert_mode: "pub ˇfield: bool".into(),
13346 expected_with_replace_mode: "pub ˇ: bool".into(),
13347 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
13348 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
13349 },
13350 Run {
13351 run_description: "Add element to start of list",
13352 initial_state: "[element_ˇelement_2]".into(),
13353 buffer_marked_text: "[<element_|element_2>]".into(),
13354 completion_label: "element_1",
13355 completion_text: "element_1",
13356 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
13357 expected_with_replace_mode: "[element_1ˇ]".into(),
13358 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
13359 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
13360 },
13361 Run {
13362 run_description: "Add element to start of list -- first and second elements are equal",
13363 initial_state: "[elˇelement]".into(),
13364 buffer_marked_text: "[<el|element>]".into(),
13365 completion_label: "element",
13366 completion_text: "element",
13367 expected_with_insert_mode: "[elementˇelement]".into(),
13368 expected_with_replace_mode: "[elementˇ]".into(),
13369 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
13370 expected_with_replace_suffix_mode: "[elementˇ]".into(),
13371 },
13372 Run {
13373 run_description: "Ends with matching suffix",
13374 initial_state: "SubˇError".into(),
13375 buffer_marked_text: "<Sub|Error>".into(),
13376 completion_label: "SubscriptionError",
13377 completion_text: "SubscriptionError",
13378 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
13379 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13380 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13381 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
13382 },
13383 Run {
13384 run_description: "Suffix is a subsequence -- contiguous",
13385 initial_state: "SubˇErr".into(),
13386 buffer_marked_text: "<Sub|Err>".into(),
13387 completion_label: "SubscriptionError",
13388 completion_text: "SubscriptionError",
13389 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
13390 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13391 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13392 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
13393 },
13394 Run {
13395 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
13396 initial_state: "Suˇscrirr".into(),
13397 buffer_marked_text: "<Su|scrirr>".into(),
13398 completion_label: "SubscriptionError",
13399 completion_text: "SubscriptionError",
13400 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
13401 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13402 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13403 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
13404 },
13405 Run {
13406 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
13407 initial_state: "foo(indˇix)".into(),
13408 buffer_marked_text: "foo(<ind|ix>)".into(),
13409 completion_label: "node_index",
13410 completion_text: "node_index",
13411 expected_with_insert_mode: "foo(node_indexˇix)".into(),
13412 expected_with_replace_mode: "foo(node_indexˇ)".into(),
13413 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
13414 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
13415 },
13416 Run {
13417 run_description: "Replace range ends before cursor - should extend to cursor",
13418 initial_state: "before editˇo after".into(),
13419 buffer_marked_text: "before <{ed}>it|o after".into(),
13420 completion_label: "editor",
13421 completion_text: "editor",
13422 expected_with_insert_mode: "before editorˇo after".into(),
13423 expected_with_replace_mode: "before editorˇo after".into(),
13424 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
13425 expected_with_replace_suffix_mode: "before editorˇo after".into(),
13426 },
13427 Run {
13428 run_description: "Uses label for suffix matching",
13429 initial_state: "before ediˇtor after".into(),
13430 buffer_marked_text: "before <edi|tor> after".into(),
13431 completion_label: "editor",
13432 completion_text: "editor()",
13433 expected_with_insert_mode: "before editor()ˇtor after".into(),
13434 expected_with_replace_mode: "before editor()ˇ after".into(),
13435 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
13436 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
13437 },
13438 Run {
13439 run_description: "Case insensitive subsequence and suffix matching",
13440 initial_state: "before EDiˇtoR after".into(),
13441 buffer_marked_text: "before <EDi|toR> after".into(),
13442 completion_label: "editor",
13443 completion_text: "editor",
13444 expected_with_insert_mode: "before editorˇtoR after".into(),
13445 expected_with_replace_mode: "before editorˇ after".into(),
13446 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13447 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13448 },
13449 ];
13450
13451 for run in runs {
13452 let run_variations = [
13453 (LspInsertMode::Insert, run.expected_with_insert_mode),
13454 (LspInsertMode::Replace, run.expected_with_replace_mode),
13455 (
13456 LspInsertMode::ReplaceSubsequence,
13457 run.expected_with_replace_subsequence_mode,
13458 ),
13459 (
13460 LspInsertMode::ReplaceSuffix,
13461 run.expected_with_replace_suffix_mode,
13462 ),
13463 ];
13464
13465 for (lsp_insert_mode, expected_text) in run_variations {
13466 eprintln!(
13467 "run = {:?}, mode = {lsp_insert_mode:.?}",
13468 run.run_description,
13469 );
13470
13471 update_test_language_settings(&mut cx, |settings| {
13472 settings.defaults.completions = Some(CompletionSettingsContent {
13473 lsp_insert_mode: Some(lsp_insert_mode),
13474 words: Some(WordsCompletionMode::Disabled),
13475 words_min_length: Some(0),
13476 ..Default::default()
13477 });
13478 });
13479
13480 cx.set_state(&run.initial_state);
13481 cx.update_editor(|editor, window, cx| {
13482 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13483 });
13484
13485 let counter = Arc::new(AtomicUsize::new(0));
13486 handle_completion_request_with_insert_and_replace(
13487 &mut cx,
13488 &run.buffer_marked_text,
13489 vec![(run.completion_label, run.completion_text)],
13490 counter.clone(),
13491 )
13492 .await;
13493 cx.condition(|editor, _| editor.context_menu_visible())
13494 .await;
13495 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13496
13497 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13498 editor
13499 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13500 .unwrap()
13501 });
13502 cx.assert_editor_state(&expected_text);
13503 handle_resolve_completion_request(&mut cx, None).await;
13504 apply_additional_edits.await.unwrap();
13505 }
13506 }
13507}
13508
13509#[gpui::test]
13510async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
13511 init_test(cx, |_| {});
13512 let mut cx = EditorLspTestContext::new_rust(
13513 lsp::ServerCapabilities {
13514 completion_provider: Some(lsp::CompletionOptions {
13515 resolve_provider: Some(true),
13516 ..Default::default()
13517 }),
13518 ..Default::default()
13519 },
13520 cx,
13521 )
13522 .await;
13523
13524 let initial_state = "SubˇError";
13525 let buffer_marked_text = "<Sub|Error>";
13526 let completion_text = "SubscriptionError";
13527 let expected_with_insert_mode = "SubscriptionErrorˇError";
13528 let expected_with_replace_mode = "SubscriptionErrorˇ";
13529
13530 update_test_language_settings(&mut cx, |settings| {
13531 settings.defaults.completions = Some(CompletionSettingsContent {
13532 words: Some(WordsCompletionMode::Disabled),
13533 words_min_length: Some(0),
13534 // set the opposite here to ensure that the action is overriding the default behavior
13535 lsp_insert_mode: Some(LspInsertMode::Insert),
13536 ..Default::default()
13537 });
13538 });
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
13545 let counter = Arc::new(AtomicUsize::new(0));
13546 handle_completion_request_with_insert_and_replace(
13547 &mut cx,
13548 buffer_marked_text,
13549 vec![(completion_text, completion_text)],
13550 counter.clone(),
13551 )
13552 .await;
13553 cx.condition(|editor, _| editor.context_menu_visible())
13554 .await;
13555 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13556
13557 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13558 editor
13559 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13560 .unwrap()
13561 });
13562 cx.assert_editor_state(expected_with_replace_mode);
13563 handle_resolve_completion_request(&mut cx, None).await;
13564 apply_additional_edits.await.unwrap();
13565
13566 update_test_language_settings(&mut cx, |settings| {
13567 settings.defaults.completions = Some(CompletionSettingsContent {
13568 words: Some(WordsCompletionMode::Disabled),
13569 words_min_length: Some(0),
13570 // set the opposite here to ensure that the action is overriding the default behavior
13571 lsp_insert_mode: Some(LspInsertMode::Replace),
13572 ..Default::default()
13573 });
13574 });
13575
13576 cx.set_state(initial_state);
13577 cx.update_editor(|editor, window, cx| {
13578 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13579 });
13580 handle_completion_request_with_insert_and_replace(
13581 &mut cx,
13582 buffer_marked_text,
13583 vec![(completion_text, completion_text)],
13584 counter.clone(),
13585 )
13586 .await;
13587 cx.condition(|editor, _| editor.context_menu_visible())
13588 .await;
13589 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
13590
13591 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13592 editor
13593 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
13594 .unwrap()
13595 });
13596 cx.assert_editor_state(expected_with_insert_mode);
13597 handle_resolve_completion_request(&mut cx, None).await;
13598 apply_additional_edits.await.unwrap();
13599}
13600
13601#[gpui::test]
13602async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
13603 init_test(cx, |_| {});
13604 let mut cx = EditorLspTestContext::new_rust(
13605 lsp::ServerCapabilities {
13606 completion_provider: Some(lsp::CompletionOptions {
13607 resolve_provider: Some(true),
13608 ..Default::default()
13609 }),
13610 ..Default::default()
13611 },
13612 cx,
13613 )
13614 .await;
13615
13616 // scenario: surrounding text matches completion text
13617 let completion_text = "to_offset";
13618 let initial_state = indoc! {"
13619 1. buf.to_offˇsuffix
13620 2. buf.to_offˇsuf
13621 3. buf.to_offˇfix
13622 4. buf.to_offˇ
13623 5. into_offˇensive
13624 6. ˇsuffix
13625 7. let ˇ //
13626 8. aaˇzz
13627 9. buf.to_off«zzzzzˇ»suffix
13628 10. buf.«ˇzzzzz»suffix
13629 11. to_off«ˇzzzzz»
13630
13631 buf.to_offˇsuffix // newest cursor
13632 "};
13633 let completion_marked_buffer = indoc! {"
13634 1. buf.to_offsuffix
13635 2. buf.to_offsuf
13636 3. buf.to_offfix
13637 4. buf.to_off
13638 5. into_offensive
13639 6. suffix
13640 7. let //
13641 8. aazz
13642 9. buf.to_offzzzzzsuffix
13643 10. buf.zzzzzsuffix
13644 11. to_offzzzzz
13645
13646 buf.<to_off|suffix> // newest cursor
13647 "};
13648 let expected = indoc! {"
13649 1. buf.to_offsetˇ
13650 2. buf.to_offsetˇsuf
13651 3. buf.to_offsetˇfix
13652 4. buf.to_offsetˇ
13653 5. into_offsetˇensive
13654 6. to_offsetˇsuffix
13655 7. let to_offsetˇ //
13656 8. aato_offsetˇzz
13657 9. buf.to_offsetˇ
13658 10. buf.to_offsetˇsuffix
13659 11. to_offsetˇ
13660
13661 buf.to_offsetˇ // newest cursor
13662 "};
13663 cx.set_state(initial_state);
13664 cx.update_editor(|editor, window, cx| {
13665 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13666 });
13667 handle_completion_request_with_insert_and_replace(
13668 &mut cx,
13669 completion_marked_buffer,
13670 vec![(completion_text, completion_text)],
13671 Arc::new(AtomicUsize::new(0)),
13672 )
13673 .await;
13674 cx.condition(|editor, _| editor.context_menu_visible())
13675 .await;
13676 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13677 editor
13678 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13679 .unwrap()
13680 });
13681 cx.assert_editor_state(expected);
13682 handle_resolve_completion_request(&mut cx, None).await;
13683 apply_additional_edits.await.unwrap();
13684
13685 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
13686 let completion_text = "foo_and_bar";
13687 let initial_state = indoc! {"
13688 1. ooanbˇ
13689 2. zooanbˇ
13690 3. ooanbˇz
13691 4. zooanbˇz
13692 5. ooanˇ
13693 6. oanbˇ
13694
13695 ooanbˇ
13696 "};
13697 let completion_marked_buffer = indoc! {"
13698 1. ooanb
13699 2. zooanb
13700 3. ooanbz
13701 4. zooanbz
13702 5. ooan
13703 6. oanb
13704
13705 <ooanb|>
13706 "};
13707 let expected = indoc! {"
13708 1. foo_and_barˇ
13709 2. zfoo_and_barˇ
13710 3. foo_and_barˇz
13711 4. zfoo_and_barˇz
13712 5. ooanfoo_and_barˇ
13713 6. oanbfoo_and_barˇ
13714
13715 foo_and_barˇ
13716 "};
13717 cx.set_state(initial_state);
13718 cx.update_editor(|editor, window, cx| {
13719 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13720 });
13721 handle_completion_request_with_insert_and_replace(
13722 &mut cx,
13723 completion_marked_buffer,
13724 vec![(completion_text, completion_text)],
13725 Arc::new(AtomicUsize::new(0)),
13726 )
13727 .await;
13728 cx.condition(|editor, _| editor.context_menu_visible())
13729 .await;
13730 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13731 editor
13732 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13733 .unwrap()
13734 });
13735 cx.assert_editor_state(expected);
13736 handle_resolve_completion_request(&mut cx, None).await;
13737 apply_additional_edits.await.unwrap();
13738
13739 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
13740 // (expects the same as if it was inserted at the end)
13741 let completion_text = "foo_and_bar";
13742 let initial_state = indoc! {"
13743 1. ooˇanb
13744 2. zooˇanb
13745 3. ooˇanbz
13746 4. zooˇanbz
13747
13748 ooˇanb
13749 "};
13750 let completion_marked_buffer = indoc! {"
13751 1. ooanb
13752 2. zooanb
13753 3. ooanbz
13754 4. zooanbz
13755
13756 <oo|anb>
13757 "};
13758 let expected = indoc! {"
13759 1. foo_and_barˇ
13760 2. zfoo_and_barˇ
13761 3. foo_and_barˇz
13762 4. zfoo_and_barˇz
13763
13764 foo_and_barˇ
13765 "};
13766 cx.set_state(initial_state);
13767 cx.update_editor(|editor, window, cx| {
13768 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13769 });
13770 handle_completion_request_with_insert_and_replace(
13771 &mut cx,
13772 completion_marked_buffer,
13773 vec![(completion_text, completion_text)],
13774 Arc::new(AtomicUsize::new(0)),
13775 )
13776 .await;
13777 cx.condition(|editor, _| editor.context_menu_visible())
13778 .await;
13779 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13780 editor
13781 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13782 .unwrap()
13783 });
13784 cx.assert_editor_state(expected);
13785 handle_resolve_completion_request(&mut cx, None).await;
13786 apply_additional_edits.await.unwrap();
13787}
13788
13789// This used to crash
13790#[gpui::test]
13791async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
13792 init_test(cx, |_| {});
13793
13794 let buffer_text = indoc! {"
13795 fn main() {
13796 10.satu;
13797
13798 //
13799 // separate cursors so they open in different excerpts (manually reproducible)
13800 //
13801
13802 10.satu20;
13803 }
13804 "};
13805 let multibuffer_text_with_selections = indoc! {"
13806 fn main() {
13807 10.satuˇ;
13808
13809 //
13810
13811 //
13812
13813 10.satuˇ20;
13814 }
13815 "};
13816 let expected_multibuffer = indoc! {"
13817 fn main() {
13818 10.saturating_sub()ˇ;
13819
13820 //
13821
13822 //
13823
13824 10.saturating_sub()ˇ;
13825 }
13826 "};
13827
13828 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
13829 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
13830
13831 let fs = FakeFs::new(cx.executor());
13832 fs.insert_tree(
13833 path!("/a"),
13834 json!({
13835 "main.rs": buffer_text,
13836 }),
13837 )
13838 .await;
13839
13840 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13841 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13842 language_registry.add(rust_lang());
13843 let mut fake_servers = language_registry.register_fake_lsp(
13844 "Rust",
13845 FakeLspAdapter {
13846 capabilities: lsp::ServerCapabilities {
13847 completion_provider: Some(lsp::CompletionOptions {
13848 resolve_provider: None,
13849 ..lsp::CompletionOptions::default()
13850 }),
13851 ..lsp::ServerCapabilities::default()
13852 },
13853 ..FakeLspAdapter::default()
13854 },
13855 );
13856 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13857 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13858 let buffer = project
13859 .update(cx, |project, cx| {
13860 project.open_local_buffer(path!("/a/main.rs"), cx)
13861 })
13862 .await
13863 .unwrap();
13864
13865 let multi_buffer = cx.new(|cx| {
13866 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
13867 multi_buffer.push_excerpts(
13868 buffer.clone(),
13869 [ExcerptRange::new(0..first_excerpt_end)],
13870 cx,
13871 );
13872 multi_buffer.push_excerpts(
13873 buffer.clone(),
13874 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
13875 cx,
13876 );
13877 multi_buffer
13878 });
13879
13880 let editor = workspace
13881 .update(cx, |_, window, cx| {
13882 cx.new(|cx| {
13883 Editor::new(
13884 EditorMode::Full {
13885 scale_ui_elements_with_buffer_font_size: false,
13886 show_active_line_background: false,
13887 sized_by_content: false,
13888 },
13889 multi_buffer.clone(),
13890 Some(project.clone()),
13891 window,
13892 cx,
13893 )
13894 })
13895 })
13896 .unwrap();
13897
13898 let pane = workspace
13899 .update(cx, |workspace, _, _| workspace.active_pane().clone())
13900 .unwrap();
13901 pane.update_in(cx, |pane, window, cx| {
13902 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
13903 });
13904
13905 let fake_server = fake_servers.next().await.unwrap();
13906
13907 editor.update_in(cx, |editor, window, cx| {
13908 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13909 s.select_ranges([
13910 Point::new(1, 11)..Point::new(1, 11),
13911 Point::new(7, 11)..Point::new(7, 11),
13912 ])
13913 });
13914
13915 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
13916 });
13917
13918 editor.update_in(cx, |editor, window, cx| {
13919 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13920 });
13921
13922 fake_server
13923 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13924 let completion_item = lsp::CompletionItem {
13925 label: "saturating_sub()".into(),
13926 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
13927 lsp::InsertReplaceEdit {
13928 new_text: "saturating_sub()".to_owned(),
13929 insert: lsp::Range::new(
13930 lsp::Position::new(7, 7),
13931 lsp::Position::new(7, 11),
13932 ),
13933 replace: lsp::Range::new(
13934 lsp::Position::new(7, 7),
13935 lsp::Position::new(7, 13),
13936 ),
13937 },
13938 )),
13939 ..lsp::CompletionItem::default()
13940 };
13941
13942 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
13943 })
13944 .next()
13945 .await
13946 .unwrap();
13947
13948 cx.condition(&editor, |editor, _| editor.context_menu_visible())
13949 .await;
13950
13951 editor
13952 .update_in(cx, |editor, window, cx| {
13953 editor
13954 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13955 .unwrap()
13956 })
13957 .await
13958 .unwrap();
13959
13960 editor.update(cx, |editor, cx| {
13961 assert_text_with_selections(editor, expected_multibuffer, cx);
13962 })
13963}
13964
13965#[gpui::test]
13966async fn test_completion(cx: &mut TestAppContext) {
13967 init_test(cx, |_| {});
13968
13969 let mut cx = EditorLspTestContext::new_rust(
13970 lsp::ServerCapabilities {
13971 completion_provider: Some(lsp::CompletionOptions {
13972 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
13973 resolve_provider: Some(true),
13974 ..Default::default()
13975 }),
13976 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
13977 ..Default::default()
13978 },
13979 cx,
13980 )
13981 .await;
13982 let counter = Arc::new(AtomicUsize::new(0));
13983
13984 cx.set_state(indoc! {"
13985 oneˇ
13986 two
13987 three
13988 "});
13989 cx.simulate_keystroke(".");
13990 handle_completion_request(
13991 indoc! {"
13992 one.|<>
13993 two
13994 three
13995 "},
13996 vec!["first_completion", "second_completion"],
13997 true,
13998 counter.clone(),
13999 &mut cx,
14000 )
14001 .await;
14002 cx.condition(|editor, _| editor.context_menu_visible())
14003 .await;
14004 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14005
14006 let _handler = handle_signature_help_request(
14007 &mut cx,
14008 lsp::SignatureHelp {
14009 signatures: vec![lsp::SignatureInformation {
14010 label: "test signature".to_string(),
14011 documentation: None,
14012 parameters: Some(vec![lsp::ParameterInformation {
14013 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
14014 documentation: None,
14015 }]),
14016 active_parameter: None,
14017 }],
14018 active_signature: None,
14019 active_parameter: None,
14020 },
14021 );
14022 cx.update_editor(|editor, window, cx| {
14023 assert!(
14024 !editor.signature_help_state.is_shown(),
14025 "No signature help was called for"
14026 );
14027 editor.show_signature_help(&ShowSignatureHelp, window, cx);
14028 });
14029 cx.run_until_parked();
14030 cx.update_editor(|editor, _, _| {
14031 assert!(
14032 !editor.signature_help_state.is_shown(),
14033 "No signature help should be shown when completions menu is open"
14034 );
14035 });
14036
14037 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14038 editor.context_menu_next(&Default::default(), window, cx);
14039 editor
14040 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14041 .unwrap()
14042 });
14043 cx.assert_editor_state(indoc! {"
14044 one.second_completionˇ
14045 two
14046 three
14047 "});
14048
14049 handle_resolve_completion_request(
14050 &mut cx,
14051 Some(vec![
14052 (
14053 //This overlaps with the primary completion edit which is
14054 //misbehavior from the LSP spec, test that we filter it out
14055 indoc! {"
14056 one.second_ˇcompletion
14057 two
14058 threeˇ
14059 "},
14060 "overlapping additional edit",
14061 ),
14062 (
14063 indoc! {"
14064 one.second_completion
14065 two
14066 threeˇ
14067 "},
14068 "\nadditional edit",
14069 ),
14070 ]),
14071 )
14072 .await;
14073 apply_additional_edits.await.unwrap();
14074 cx.assert_editor_state(indoc! {"
14075 one.second_completionˇ
14076 two
14077 three
14078 additional edit
14079 "});
14080
14081 cx.set_state(indoc! {"
14082 one.second_completion
14083 twoˇ
14084 threeˇ
14085 additional edit
14086 "});
14087 cx.simulate_keystroke(" ");
14088 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14089 cx.simulate_keystroke("s");
14090 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14091
14092 cx.assert_editor_state(indoc! {"
14093 one.second_completion
14094 two sˇ
14095 three sˇ
14096 additional edit
14097 "});
14098 handle_completion_request(
14099 indoc! {"
14100 one.second_completion
14101 two s
14102 three <s|>
14103 additional edit
14104 "},
14105 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14106 true,
14107 counter.clone(),
14108 &mut cx,
14109 )
14110 .await;
14111 cx.condition(|editor, _| editor.context_menu_visible())
14112 .await;
14113 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14114
14115 cx.simulate_keystroke("i");
14116
14117 handle_completion_request(
14118 indoc! {"
14119 one.second_completion
14120 two si
14121 three <si|>
14122 additional edit
14123 "},
14124 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14125 true,
14126 counter.clone(),
14127 &mut cx,
14128 )
14129 .await;
14130 cx.condition(|editor, _| editor.context_menu_visible())
14131 .await;
14132 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14133
14134 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14135 editor
14136 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14137 .unwrap()
14138 });
14139 cx.assert_editor_state(indoc! {"
14140 one.second_completion
14141 two sixth_completionˇ
14142 three sixth_completionˇ
14143 additional edit
14144 "});
14145
14146 apply_additional_edits.await.unwrap();
14147
14148 update_test_language_settings(&mut cx, |settings| {
14149 settings.defaults.show_completions_on_input = Some(false);
14150 });
14151 cx.set_state("editorˇ");
14152 cx.simulate_keystroke(".");
14153 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14154 cx.simulate_keystrokes("c l o");
14155 cx.assert_editor_state("editor.cloˇ");
14156 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14157 cx.update_editor(|editor, window, cx| {
14158 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14159 });
14160 handle_completion_request(
14161 "editor.<clo|>",
14162 vec!["close", "clobber"],
14163 true,
14164 counter.clone(),
14165 &mut cx,
14166 )
14167 .await;
14168 cx.condition(|editor, _| editor.context_menu_visible())
14169 .await;
14170 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
14171
14172 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14173 editor
14174 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14175 .unwrap()
14176 });
14177 cx.assert_editor_state("editor.clobberˇ");
14178 handle_resolve_completion_request(&mut cx, None).await;
14179 apply_additional_edits.await.unwrap();
14180}
14181
14182#[gpui::test]
14183async fn test_completion_reuse(cx: &mut TestAppContext) {
14184 init_test(cx, |_| {});
14185
14186 let mut cx = EditorLspTestContext::new_rust(
14187 lsp::ServerCapabilities {
14188 completion_provider: Some(lsp::CompletionOptions {
14189 trigger_characters: Some(vec![".".to_string()]),
14190 ..Default::default()
14191 }),
14192 ..Default::default()
14193 },
14194 cx,
14195 )
14196 .await;
14197
14198 let counter = Arc::new(AtomicUsize::new(0));
14199 cx.set_state("objˇ");
14200 cx.simulate_keystroke(".");
14201
14202 // Initial completion request returns complete results
14203 let is_incomplete = false;
14204 handle_completion_request(
14205 "obj.|<>",
14206 vec!["a", "ab", "abc"],
14207 is_incomplete,
14208 counter.clone(),
14209 &mut cx,
14210 )
14211 .await;
14212 cx.run_until_parked();
14213 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14214 cx.assert_editor_state("obj.ˇ");
14215 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14216
14217 // Type "a" - filters existing completions
14218 cx.simulate_keystroke("a");
14219 cx.run_until_parked();
14220 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14221 cx.assert_editor_state("obj.aˇ");
14222 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14223
14224 // Type "b" - filters existing completions
14225 cx.simulate_keystroke("b");
14226 cx.run_until_parked();
14227 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14228 cx.assert_editor_state("obj.abˇ");
14229 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14230
14231 // Type "c" - filters existing completions
14232 cx.simulate_keystroke("c");
14233 cx.run_until_parked();
14234 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14235 cx.assert_editor_state("obj.abcˇ");
14236 check_displayed_completions(vec!["abc"], &mut cx);
14237
14238 // Backspace to delete "c" - filters existing completions
14239 cx.update_editor(|editor, window, cx| {
14240 editor.backspace(&Backspace, window, cx);
14241 });
14242 cx.run_until_parked();
14243 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14244 cx.assert_editor_state("obj.abˇ");
14245 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14246
14247 // Moving cursor to the left dismisses menu.
14248 cx.update_editor(|editor, window, cx| {
14249 editor.move_left(&MoveLeft, window, cx);
14250 });
14251 cx.run_until_parked();
14252 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14253 cx.assert_editor_state("obj.aˇb");
14254 cx.update_editor(|editor, _, _| {
14255 assert_eq!(editor.context_menu_visible(), false);
14256 });
14257
14258 // Type "b" - new request
14259 cx.simulate_keystroke("b");
14260 let is_incomplete = false;
14261 handle_completion_request(
14262 "obj.<ab|>a",
14263 vec!["ab", "abc"],
14264 is_incomplete,
14265 counter.clone(),
14266 &mut cx,
14267 )
14268 .await;
14269 cx.run_until_parked();
14270 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14271 cx.assert_editor_state("obj.abˇb");
14272 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14273
14274 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
14275 cx.update_editor(|editor, window, cx| {
14276 editor.backspace(&Backspace, window, cx);
14277 });
14278 let is_incomplete = false;
14279 handle_completion_request(
14280 "obj.<a|>b",
14281 vec!["a", "ab", "abc"],
14282 is_incomplete,
14283 counter.clone(),
14284 &mut cx,
14285 )
14286 .await;
14287 cx.run_until_parked();
14288 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14289 cx.assert_editor_state("obj.aˇb");
14290 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14291
14292 // Backspace to delete "a" - dismisses menu.
14293 cx.update_editor(|editor, window, cx| {
14294 editor.backspace(&Backspace, window, cx);
14295 });
14296 cx.run_until_parked();
14297 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14298 cx.assert_editor_state("obj.ˇb");
14299 cx.update_editor(|editor, _, _| {
14300 assert_eq!(editor.context_menu_visible(), false);
14301 });
14302}
14303
14304#[gpui::test]
14305async fn test_word_completion(cx: &mut TestAppContext) {
14306 let lsp_fetch_timeout_ms = 10;
14307 init_test(cx, |language_settings| {
14308 language_settings.defaults.completions = Some(CompletionSettingsContent {
14309 words_min_length: Some(0),
14310 lsp_fetch_timeout_ms: Some(10),
14311 lsp_insert_mode: Some(LspInsertMode::Insert),
14312 ..Default::default()
14313 });
14314 });
14315
14316 let mut cx = EditorLspTestContext::new_rust(
14317 lsp::ServerCapabilities {
14318 completion_provider: Some(lsp::CompletionOptions {
14319 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14320 ..lsp::CompletionOptions::default()
14321 }),
14322 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14323 ..lsp::ServerCapabilities::default()
14324 },
14325 cx,
14326 )
14327 .await;
14328
14329 let throttle_completions = Arc::new(AtomicBool::new(false));
14330
14331 let lsp_throttle_completions = throttle_completions.clone();
14332 let _completion_requests_handler =
14333 cx.lsp
14334 .server
14335 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
14336 let lsp_throttle_completions = lsp_throttle_completions.clone();
14337 let cx = cx.clone();
14338 async move {
14339 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
14340 cx.background_executor()
14341 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
14342 .await;
14343 }
14344 Ok(Some(lsp::CompletionResponse::Array(vec![
14345 lsp::CompletionItem {
14346 label: "first".into(),
14347 ..lsp::CompletionItem::default()
14348 },
14349 lsp::CompletionItem {
14350 label: "last".into(),
14351 ..lsp::CompletionItem::default()
14352 },
14353 ])))
14354 }
14355 });
14356
14357 cx.set_state(indoc! {"
14358 oneˇ
14359 two
14360 three
14361 "});
14362 cx.simulate_keystroke(".");
14363 cx.executor().run_until_parked();
14364 cx.condition(|editor, _| editor.context_menu_visible())
14365 .await;
14366 cx.update_editor(|editor, window, cx| {
14367 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14368 {
14369 assert_eq!(
14370 completion_menu_entries(menu),
14371 &["first", "last"],
14372 "When LSP server is fast to reply, no fallback word completions are used"
14373 );
14374 } else {
14375 panic!("expected completion menu to be open");
14376 }
14377 editor.cancel(&Cancel, window, cx);
14378 });
14379 cx.executor().run_until_parked();
14380 cx.condition(|editor, _| !editor.context_menu_visible())
14381 .await;
14382
14383 throttle_completions.store(true, atomic::Ordering::Release);
14384 cx.simulate_keystroke(".");
14385 cx.executor()
14386 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
14387 cx.executor().run_until_parked();
14388 cx.condition(|editor, _| editor.context_menu_visible())
14389 .await;
14390 cx.update_editor(|editor, _, _| {
14391 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14392 {
14393 assert_eq!(completion_menu_entries(menu), &["one", "three", "two"],
14394 "When LSP server is slow, document words can be shown instead, if configured accordingly");
14395 } else {
14396 panic!("expected completion menu to be open");
14397 }
14398 });
14399}
14400
14401#[gpui::test]
14402async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
14403 init_test(cx, |language_settings| {
14404 language_settings.defaults.completions = Some(CompletionSettingsContent {
14405 words: Some(WordsCompletionMode::Enabled),
14406 words_min_length: Some(0),
14407 lsp_insert_mode: Some(LspInsertMode::Insert),
14408 ..Default::default()
14409 });
14410 });
14411
14412 let mut cx = EditorLspTestContext::new_rust(
14413 lsp::ServerCapabilities {
14414 completion_provider: Some(lsp::CompletionOptions {
14415 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14416 ..lsp::CompletionOptions::default()
14417 }),
14418 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14419 ..lsp::ServerCapabilities::default()
14420 },
14421 cx,
14422 )
14423 .await;
14424
14425 let _completion_requests_handler =
14426 cx.lsp
14427 .server
14428 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14429 Ok(Some(lsp::CompletionResponse::Array(vec![
14430 lsp::CompletionItem {
14431 label: "first".into(),
14432 ..lsp::CompletionItem::default()
14433 },
14434 lsp::CompletionItem {
14435 label: "last".into(),
14436 ..lsp::CompletionItem::default()
14437 },
14438 ])))
14439 });
14440
14441 cx.set_state(indoc! {"ˇ
14442 first
14443 last
14444 second
14445 "});
14446 cx.simulate_keystroke(".");
14447 cx.executor().run_until_parked();
14448 cx.condition(|editor, _| editor.context_menu_visible())
14449 .await;
14450 cx.update_editor(|editor, _, _| {
14451 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14452 {
14453 assert_eq!(
14454 completion_menu_entries(menu),
14455 &["first", "last", "second"],
14456 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
14457 );
14458 } else {
14459 panic!("expected completion menu to be open");
14460 }
14461 });
14462}
14463
14464#[gpui::test]
14465async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
14466 init_test(cx, |language_settings| {
14467 language_settings.defaults.completions = Some(CompletionSettingsContent {
14468 words: Some(WordsCompletionMode::Disabled),
14469 words_min_length: Some(0),
14470 lsp_insert_mode: Some(LspInsertMode::Insert),
14471 ..Default::default()
14472 });
14473 });
14474
14475 let mut cx = EditorLspTestContext::new_rust(
14476 lsp::ServerCapabilities {
14477 completion_provider: Some(lsp::CompletionOptions {
14478 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14479 ..lsp::CompletionOptions::default()
14480 }),
14481 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14482 ..lsp::ServerCapabilities::default()
14483 },
14484 cx,
14485 )
14486 .await;
14487
14488 let _completion_requests_handler =
14489 cx.lsp
14490 .server
14491 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14492 panic!("LSP completions should not be queried when dealing with word completions")
14493 });
14494
14495 cx.set_state(indoc! {"ˇ
14496 first
14497 last
14498 second
14499 "});
14500 cx.update_editor(|editor, window, cx| {
14501 editor.show_word_completions(&ShowWordCompletions, window, cx);
14502 });
14503 cx.executor().run_until_parked();
14504 cx.condition(|editor, _| editor.context_menu_visible())
14505 .await;
14506 cx.update_editor(|editor, _, _| {
14507 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14508 {
14509 assert_eq!(
14510 completion_menu_entries(menu),
14511 &["first", "last", "second"],
14512 "`ShowWordCompletions` action should show word completions"
14513 );
14514 } else {
14515 panic!("expected completion menu to be open");
14516 }
14517 });
14518
14519 cx.simulate_keystroke("l");
14520 cx.executor().run_until_parked();
14521 cx.condition(|editor, _| editor.context_menu_visible())
14522 .await;
14523 cx.update_editor(|editor, _, _| {
14524 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14525 {
14526 assert_eq!(
14527 completion_menu_entries(menu),
14528 &["last"],
14529 "After showing word completions, further editing should filter them and not query the LSP"
14530 );
14531 } else {
14532 panic!("expected completion menu to be open");
14533 }
14534 });
14535}
14536
14537#[gpui::test]
14538async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
14539 init_test(cx, |language_settings| {
14540 language_settings.defaults.completions = Some(CompletionSettingsContent {
14541 words_min_length: Some(0),
14542 lsp: Some(false),
14543 lsp_insert_mode: Some(LspInsertMode::Insert),
14544 ..Default::default()
14545 });
14546 });
14547
14548 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14549
14550 cx.set_state(indoc! {"ˇ
14551 0_usize
14552 let
14553 33
14554 4.5f32
14555 "});
14556 cx.update_editor(|editor, window, cx| {
14557 editor.show_completions(&ShowCompletions::default(), window, cx);
14558 });
14559 cx.executor().run_until_parked();
14560 cx.condition(|editor, _| editor.context_menu_visible())
14561 .await;
14562 cx.update_editor(|editor, window, cx| {
14563 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14564 {
14565 assert_eq!(
14566 completion_menu_entries(menu),
14567 &["let"],
14568 "With no digits in the completion query, no digits should be in the word completions"
14569 );
14570 } else {
14571 panic!("expected completion menu to be open");
14572 }
14573 editor.cancel(&Cancel, window, cx);
14574 });
14575
14576 cx.set_state(indoc! {"3ˇ
14577 0_usize
14578 let
14579 3
14580 33.35f32
14581 "});
14582 cx.update_editor(|editor, window, cx| {
14583 editor.show_completions(&ShowCompletions::default(), window, cx);
14584 });
14585 cx.executor().run_until_parked();
14586 cx.condition(|editor, _| editor.context_menu_visible())
14587 .await;
14588 cx.update_editor(|editor, _, _| {
14589 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14590 {
14591 assert_eq!(completion_menu_entries(menu), &["33", "35f32"], "The digit is in the completion query, \
14592 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
14593 } else {
14594 panic!("expected completion menu to be open");
14595 }
14596 });
14597}
14598
14599#[gpui::test]
14600async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) {
14601 init_test(cx, |language_settings| {
14602 language_settings.defaults.completions = Some(CompletionSettingsContent {
14603 words: Some(WordsCompletionMode::Enabled),
14604 words_min_length: Some(3),
14605 lsp_insert_mode: Some(LspInsertMode::Insert),
14606 ..Default::default()
14607 });
14608 });
14609
14610 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14611 cx.set_state(indoc! {"ˇ
14612 wow
14613 wowen
14614 wowser
14615 "});
14616 cx.simulate_keystroke("w");
14617 cx.executor().run_until_parked();
14618 cx.update_editor(|editor, _, _| {
14619 if editor.context_menu.borrow_mut().is_some() {
14620 panic!(
14621 "expected completion menu to be hidden, as words completion threshold is not met"
14622 );
14623 }
14624 });
14625
14626 cx.update_editor(|editor, window, cx| {
14627 editor.show_word_completions(&ShowWordCompletions, window, cx);
14628 });
14629 cx.executor().run_until_parked();
14630 cx.update_editor(|editor, window, cx| {
14631 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14632 {
14633 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");
14634 } else {
14635 panic!("expected completion menu to be open after the word completions are called with an action");
14636 }
14637
14638 editor.cancel(&Cancel, window, cx);
14639 });
14640 cx.update_editor(|editor, _, _| {
14641 if editor.context_menu.borrow_mut().is_some() {
14642 panic!("expected completion menu to be hidden after canceling");
14643 }
14644 });
14645
14646 cx.simulate_keystroke("o");
14647 cx.executor().run_until_parked();
14648 cx.update_editor(|editor, _, _| {
14649 if editor.context_menu.borrow_mut().is_some() {
14650 panic!(
14651 "expected completion menu to be hidden, as words completion threshold is not met still"
14652 );
14653 }
14654 });
14655
14656 cx.simulate_keystroke("w");
14657 cx.executor().run_until_parked();
14658 cx.update_editor(|editor, _, _| {
14659 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14660 {
14661 assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word");
14662 } else {
14663 panic!("expected completion menu to be open after the word completions threshold is met");
14664 }
14665 });
14666}
14667
14668#[gpui::test]
14669async fn test_word_completions_disabled(cx: &mut TestAppContext) {
14670 init_test(cx, |language_settings| {
14671 language_settings.defaults.completions = Some(CompletionSettingsContent {
14672 words: Some(WordsCompletionMode::Enabled),
14673 words_min_length: Some(0),
14674 lsp_insert_mode: Some(LspInsertMode::Insert),
14675 ..Default::default()
14676 });
14677 });
14678
14679 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14680 cx.update_editor(|editor, _, _| {
14681 editor.disable_word_completions();
14682 });
14683 cx.set_state(indoc! {"ˇ
14684 wow
14685 wowen
14686 wowser
14687 "});
14688 cx.simulate_keystroke("w");
14689 cx.executor().run_until_parked();
14690 cx.update_editor(|editor, _, _| {
14691 if editor.context_menu.borrow_mut().is_some() {
14692 panic!(
14693 "expected completion menu to be hidden, as words completion are disabled for this editor"
14694 );
14695 }
14696 });
14697
14698 cx.update_editor(|editor, window, cx| {
14699 editor.show_word_completions(&ShowWordCompletions, window, cx);
14700 });
14701 cx.executor().run_until_parked();
14702 cx.update_editor(|editor, _, _| {
14703 if editor.context_menu.borrow_mut().is_some() {
14704 panic!(
14705 "expected completion menu to be hidden even if called for explicitly, as words completion are disabled for this editor"
14706 );
14707 }
14708 });
14709}
14710
14711fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
14712 let position = || lsp::Position {
14713 line: params.text_document_position.position.line,
14714 character: params.text_document_position.position.character,
14715 };
14716 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14717 range: lsp::Range {
14718 start: position(),
14719 end: position(),
14720 },
14721 new_text: text.to_string(),
14722 }))
14723}
14724
14725#[gpui::test]
14726async fn test_multiline_completion(cx: &mut TestAppContext) {
14727 init_test(cx, |_| {});
14728
14729 let fs = FakeFs::new(cx.executor());
14730 fs.insert_tree(
14731 path!("/a"),
14732 json!({
14733 "main.ts": "a",
14734 }),
14735 )
14736 .await;
14737
14738 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14739 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14740 let typescript_language = Arc::new(Language::new(
14741 LanguageConfig {
14742 name: "TypeScript".into(),
14743 matcher: LanguageMatcher {
14744 path_suffixes: vec!["ts".to_string()],
14745 ..LanguageMatcher::default()
14746 },
14747 line_comments: vec!["// ".into()],
14748 ..LanguageConfig::default()
14749 },
14750 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
14751 ));
14752 language_registry.add(typescript_language.clone());
14753 let mut fake_servers = language_registry.register_fake_lsp(
14754 "TypeScript",
14755 FakeLspAdapter {
14756 capabilities: lsp::ServerCapabilities {
14757 completion_provider: Some(lsp::CompletionOptions {
14758 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14759 ..lsp::CompletionOptions::default()
14760 }),
14761 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14762 ..lsp::ServerCapabilities::default()
14763 },
14764 // Emulate vtsls label generation
14765 label_for_completion: Some(Box::new(|item, _| {
14766 let text = if let Some(description) = item
14767 .label_details
14768 .as_ref()
14769 .and_then(|label_details| label_details.description.as_ref())
14770 {
14771 format!("{} {}", item.label, description)
14772 } else if let Some(detail) = &item.detail {
14773 format!("{} {}", item.label, detail)
14774 } else {
14775 item.label.clone()
14776 };
14777 let len = text.len();
14778 Some(language::CodeLabel {
14779 text,
14780 runs: Vec::new(),
14781 filter_range: 0..len,
14782 })
14783 })),
14784 ..FakeLspAdapter::default()
14785 },
14786 );
14787 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14788 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14789 let worktree_id = workspace
14790 .update(cx, |workspace, _window, cx| {
14791 workspace.project().update(cx, |project, cx| {
14792 project.worktrees(cx).next().unwrap().read(cx).id()
14793 })
14794 })
14795 .unwrap();
14796 let _buffer = project
14797 .update(cx, |project, cx| {
14798 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
14799 })
14800 .await
14801 .unwrap();
14802 let editor = workspace
14803 .update(cx, |workspace, window, cx| {
14804 workspace.open_path((worktree_id, rel_path("main.ts")), None, true, window, cx)
14805 })
14806 .unwrap()
14807 .await
14808 .unwrap()
14809 .downcast::<Editor>()
14810 .unwrap();
14811 let fake_server = fake_servers.next().await.unwrap();
14812
14813 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
14814 let multiline_label_2 = "a\nb\nc\n";
14815 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
14816 let multiline_description = "d\ne\nf\n";
14817 let multiline_detail_2 = "g\nh\ni\n";
14818
14819 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
14820 move |params, _| async move {
14821 Ok(Some(lsp::CompletionResponse::Array(vec![
14822 lsp::CompletionItem {
14823 label: multiline_label.to_string(),
14824 text_edit: gen_text_edit(¶ms, "new_text_1"),
14825 ..lsp::CompletionItem::default()
14826 },
14827 lsp::CompletionItem {
14828 label: "single line label 1".to_string(),
14829 detail: Some(multiline_detail.to_string()),
14830 text_edit: gen_text_edit(¶ms, "new_text_2"),
14831 ..lsp::CompletionItem::default()
14832 },
14833 lsp::CompletionItem {
14834 label: "single line label 2".to_string(),
14835 label_details: Some(lsp::CompletionItemLabelDetails {
14836 description: Some(multiline_description.to_string()),
14837 detail: None,
14838 }),
14839 text_edit: gen_text_edit(¶ms, "new_text_2"),
14840 ..lsp::CompletionItem::default()
14841 },
14842 lsp::CompletionItem {
14843 label: multiline_label_2.to_string(),
14844 detail: Some(multiline_detail_2.to_string()),
14845 text_edit: gen_text_edit(¶ms, "new_text_3"),
14846 ..lsp::CompletionItem::default()
14847 },
14848 lsp::CompletionItem {
14849 label: "Label with many spaces and \t but without newlines".to_string(),
14850 detail: Some(
14851 "Details with many spaces and \t but without newlines".to_string(),
14852 ),
14853 text_edit: gen_text_edit(¶ms, "new_text_4"),
14854 ..lsp::CompletionItem::default()
14855 },
14856 ])))
14857 },
14858 );
14859
14860 editor.update_in(cx, |editor, window, cx| {
14861 cx.focus_self(window);
14862 editor.move_to_end(&MoveToEnd, window, cx);
14863 editor.handle_input(".", window, cx);
14864 });
14865 cx.run_until_parked();
14866 completion_handle.next().await.unwrap();
14867
14868 editor.update(cx, |editor, _| {
14869 assert!(editor.context_menu_visible());
14870 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14871 {
14872 let completion_labels = menu
14873 .completions
14874 .borrow()
14875 .iter()
14876 .map(|c| c.label.text.clone())
14877 .collect::<Vec<_>>();
14878 assert_eq!(
14879 completion_labels,
14880 &[
14881 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
14882 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
14883 "single line label 2 d e f ",
14884 "a b c g h i ",
14885 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
14886 ],
14887 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
14888 );
14889
14890 for completion in menu
14891 .completions
14892 .borrow()
14893 .iter() {
14894 assert_eq!(
14895 completion.label.filter_range,
14896 0..completion.label.text.len(),
14897 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
14898 );
14899 }
14900 } else {
14901 panic!("expected completion menu to be open");
14902 }
14903 });
14904}
14905
14906#[gpui::test]
14907async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
14908 init_test(cx, |_| {});
14909 let mut cx = EditorLspTestContext::new_rust(
14910 lsp::ServerCapabilities {
14911 completion_provider: Some(lsp::CompletionOptions {
14912 trigger_characters: Some(vec![".".to_string()]),
14913 ..Default::default()
14914 }),
14915 ..Default::default()
14916 },
14917 cx,
14918 )
14919 .await;
14920 cx.lsp
14921 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14922 Ok(Some(lsp::CompletionResponse::Array(vec![
14923 lsp::CompletionItem {
14924 label: "first".into(),
14925 ..Default::default()
14926 },
14927 lsp::CompletionItem {
14928 label: "last".into(),
14929 ..Default::default()
14930 },
14931 ])))
14932 });
14933 cx.set_state("variableˇ");
14934 cx.simulate_keystroke(".");
14935 cx.executor().run_until_parked();
14936
14937 cx.update_editor(|editor, _, _| {
14938 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14939 {
14940 assert_eq!(completion_menu_entries(menu), &["first", "last"]);
14941 } else {
14942 panic!("expected completion menu to be open");
14943 }
14944 });
14945
14946 cx.update_editor(|editor, window, cx| {
14947 editor.move_page_down(&MovePageDown::default(), window, cx);
14948 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14949 {
14950 assert!(
14951 menu.selected_item == 1,
14952 "expected PageDown to select the last item from the context menu"
14953 );
14954 } else {
14955 panic!("expected completion menu to stay open after PageDown");
14956 }
14957 });
14958
14959 cx.update_editor(|editor, window, cx| {
14960 editor.move_page_up(&MovePageUp::default(), window, cx);
14961 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14962 {
14963 assert!(
14964 menu.selected_item == 0,
14965 "expected PageUp to select the first item from the context menu"
14966 );
14967 } else {
14968 panic!("expected completion menu to stay open after PageUp");
14969 }
14970 });
14971}
14972
14973#[gpui::test]
14974async fn test_as_is_completions(cx: &mut TestAppContext) {
14975 init_test(cx, |_| {});
14976 let mut cx = EditorLspTestContext::new_rust(
14977 lsp::ServerCapabilities {
14978 completion_provider: Some(lsp::CompletionOptions {
14979 ..Default::default()
14980 }),
14981 ..Default::default()
14982 },
14983 cx,
14984 )
14985 .await;
14986 cx.lsp
14987 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14988 Ok(Some(lsp::CompletionResponse::Array(vec![
14989 lsp::CompletionItem {
14990 label: "unsafe".into(),
14991 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14992 range: lsp::Range {
14993 start: lsp::Position {
14994 line: 1,
14995 character: 2,
14996 },
14997 end: lsp::Position {
14998 line: 1,
14999 character: 3,
15000 },
15001 },
15002 new_text: "unsafe".to_string(),
15003 })),
15004 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
15005 ..Default::default()
15006 },
15007 ])))
15008 });
15009 cx.set_state("fn a() {}\n nˇ");
15010 cx.executor().run_until_parked();
15011 cx.update_editor(|editor, window, cx| {
15012 editor.show_completions(
15013 &ShowCompletions {
15014 trigger: Some("\n".into()),
15015 },
15016 window,
15017 cx,
15018 );
15019 });
15020 cx.executor().run_until_parked();
15021
15022 cx.update_editor(|editor, window, cx| {
15023 editor.confirm_completion(&Default::default(), window, cx)
15024 });
15025 cx.executor().run_until_parked();
15026 cx.assert_editor_state("fn a() {}\n unsafeˇ");
15027}
15028
15029#[gpui::test]
15030async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
15031 init_test(cx, |_| {});
15032 let language =
15033 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
15034 let mut cx = EditorLspTestContext::new(
15035 language,
15036 lsp::ServerCapabilities {
15037 completion_provider: Some(lsp::CompletionOptions {
15038 ..lsp::CompletionOptions::default()
15039 }),
15040 ..lsp::ServerCapabilities::default()
15041 },
15042 cx,
15043 )
15044 .await;
15045
15046 cx.set_state(
15047 "#ifndef BAR_H
15048#define BAR_H
15049
15050#include <stdbool.h>
15051
15052int fn_branch(bool do_branch1, bool do_branch2);
15053
15054#endif // BAR_H
15055ˇ",
15056 );
15057 cx.executor().run_until_parked();
15058 cx.update_editor(|editor, window, cx| {
15059 editor.handle_input("#", window, cx);
15060 });
15061 cx.executor().run_until_parked();
15062 cx.update_editor(|editor, window, cx| {
15063 editor.handle_input("i", window, cx);
15064 });
15065 cx.executor().run_until_parked();
15066 cx.update_editor(|editor, window, cx| {
15067 editor.handle_input("n", window, cx);
15068 });
15069 cx.executor().run_until_parked();
15070 cx.assert_editor_state(
15071 "#ifndef BAR_H
15072#define BAR_H
15073
15074#include <stdbool.h>
15075
15076int fn_branch(bool do_branch1, bool do_branch2);
15077
15078#endif // BAR_H
15079#inˇ",
15080 );
15081
15082 cx.lsp
15083 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15084 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15085 is_incomplete: false,
15086 item_defaults: None,
15087 items: vec![lsp::CompletionItem {
15088 kind: Some(lsp::CompletionItemKind::SNIPPET),
15089 label_details: Some(lsp::CompletionItemLabelDetails {
15090 detail: Some("header".to_string()),
15091 description: None,
15092 }),
15093 label: " include".to_string(),
15094 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15095 range: lsp::Range {
15096 start: lsp::Position {
15097 line: 8,
15098 character: 1,
15099 },
15100 end: lsp::Position {
15101 line: 8,
15102 character: 1,
15103 },
15104 },
15105 new_text: "include \"$0\"".to_string(),
15106 })),
15107 sort_text: Some("40b67681include".to_string()),
15108 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15109 filter_text: Some("include".to_string()),
15110 insert_text: Some("include \"$0\"".to_string()),
15111 ..lsp::CompletionItem::default()
15112 }],
15113 })))
15114 });
15115 cx.update_editor(|editor, window, cx| {
15116 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15117 });
15118 cx.executor().run_until_parked();
15119 cx.update_editor(|editor, window, cx| {
15120 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15121 });
15122 cx.executor().run_until_parked();
15123 cx.assert_editor_state(
15124 "#ifndef BAR_H
15125#define BAR_H
15126
15127#include <stdbool.h>
15128
15129int fn_branch(bool do_branch1, bool do_branch2);
15130
15131#endif // BAR_H
15132#include \"ˇ\"",
15133 );
15134
15135 cx.lsp
15136 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15137 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15138 is_incomplete: true,
15139 item_defaults: None,
15140 items: vec![lsp::CompletionItem {
15141 kind: Some(lsp::CompletionItemKind::FILE),
15142 label: "AGL/".to_string(),
15143 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15144 range: lsp::Range {
15145 start: lsp::Position {
15146 line: 8,
15147 character: 10,
15148 },
15149 end: lsp::Position {
15150 line: 8,
15151 character: 11,
15152 },
15153 },
15154 new_text: "AGL/".to_string(),
15155 })),
15156 sort_text: Some("40b67681AGL/".to_string()),
15157 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
15158 filter_text: Some("AGL/".to_string()),
15159 insert_text: Some("AGL/".to_string()),
15160 ..lsp::CompletionItem::default()
15161 }],
15162 })))
15163 });
15164 cx.update_editor(|editor, window, cx| {
15165 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15166 });
15167 cx.executor().run_until_parked();
15168 cx.update_editor(|editor, window, cx| {
15169 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15170 });
15171 cx.executor().run_until_parked();
15172 cx.assert_editor_state(
15173 r##"#ifndef BAR_H
15174#define BAR_H
15175
15176#include <stdbool.h>
15177
15178int fn_branch(bool do_branch1, bool do_branch2);
15179
15180#endif // BAR_H
15181#include "AGL/ˇ"##,
15182 );
15183
15184 cx.update_editor(|editor, window, cx| {
15185 editor.handle_input("\"", window, cx);
15186 });
15187 cx.executor().run_until_parked();
15188 cx.assert_editor_state(
15189 r##"#ifndef BAR_H
15190#define BAR_H
15191
15192#include <stdbool.h>
15193
15194int fn_branch(bool do_branch1, bool do_branch2);
15195
15196#endif // BAR_H
15197#include "AGL/"ˇ"##,
15198 );
15199}
15200
15201#[gpui::test]
15202async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
15203 init_test(cx, |_| {});
15204
15205 let mut cx = EditorLspTestContext::new_rust(
15206 lsp::ServerCapabilities {
15207 completion_provider: Some(lsp::CompletionOptions {
15208 trigger_characters: Some(vec![".".to_string()]),
15209 resolve_provider: Some(true),
15210 ..Default::default()
15211 }),
15212 ..Default::default()
15213 },
15214 cx,
15215 )
15216 .await;
15217
15218 cx.set_state("fn main() { let a = 2ˇ; }");
15219 cx.simulate_keystroke(".");
15220 let completion_item = lsp::CompletionItem {
15221 label: "Some".into(),
15222 kind: Some(lsp::CompletionItemKind::SNIPPET),
15223 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
15224 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
15225 kind: lsp::MarkupKind::Markdown,
15226 value: "```rust\nSome(2)\n```".to_string(),
15227 })),
15228 deprecated: Some(false),
15229 sort_text: Some("Some".to_string()),
15230 filter_text: Some("Some".to_string()),
15231 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15232 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15233 range: lsp::Range {
15234 start: lsp::Position {
15235 line: 0,
15236 character: 22,
15237 },
15238 end: lsp::Position {
15239 line: 0,
15240 character: 22,
15241 },
15242 },
15243 new_text: "Some(2)".to_string(),
15244 })),
15245 additional_text_edits: Some(vec![lsp::TextEdit {
15246 range: lsp::Range {
15247 start: lsp::Position {
15248 line: 0,
15249 character: 20,
15250 },
15251 end: lsp::Position {
15252 line: 0,
15253 character: 22,
15254 },
15255 },
15256 new_text: "".to_string(),
15257 }]),
15258 ..Default::default()
15259 };
15260
15261 let closure_completion_item = completion_item.clone();
15262 let counter = Arc::new(AtomicUsize::new(0));
15263 let counter_clone = counter.clone();
15264 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15265 let task_completion_item = closure_completion_item.clone();
15266 counter_clone.fetch_add(1, atomic::Ordering::Release);
15267 async move {
15268 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15269 is_incomplete: true,
15270 item_defaults: None,
15271 items: vec![task_completion_item],
15272 })))
15273 }
15274 });
15275
15276 cx.condition(|editor, _| editor.context_menu_visible())
15277 .await;
15278 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
15279 assert!(request.next().await.is_some());
15280 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15281
15282 cx.simulate_keystrokes("S o m");
15283 cx.condition(|editor, _| editor.context_menu_visible())
15284 .await;
15285 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
15286 assert!(request.next().await.is_some());
15287 assert!(request.next().await.is_some());
15288 assert!(request.next().await.is_some());
15289 request.close();
15290 assert!(request.next().await.is_none());
15291 assert_eq!(
15292 counter.load(atomic::Ordering::Acquire),
15293 4,
15294 "With the completions menu open, only one LSP request should happen per input"
15295 );
15296}
15297
15298#[gpui::test]
15299async fn test_toggle_comment(cx: &mut TestAppContext) {
15300 init_test(cx, |_| {});
15301 let mut cx = EditorTestContext::new(cx).await;
15302 let language = Arc::new(Language::new(
15303 LanguageConfig {
15304 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15305 ..Default::default()
15306 },
15307 Some(tree_sitter_rust::LANGUAGE.into()),
15308 ));
15309 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15310
15311 // If multiple selections intersect a line, the line is only toggled once.
15312 cx.set_state(indoc! {"
15313 fn a() {
15314 «//b();
15315 ˇ»// «c();
15316 //ˇ» d();
15317 }
15318 "});
15319
15320 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15321
15322 cx.assert_editor_state(indoc! {"
15323 fn a() {
15324 «b();
15325 c();
15326 ˇ» d();
15327 }
15328 "});
15329
15330 // The comment prefix is inserted at the same column for every line in a
15331 // selection.
15332 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15333
15334 cx.assert_editor_state(indoc! {"
15335 fn a() {
15336 // «b();
15337 // c();
15338 ˇ»// d();
15339 }
15340 "});
15341
15342 // If a selection ends at the beginning of a line, that line is not toggled.
15343 cx.set_selections_state(indoc! {"
15344 fn a() {
15345 // b();
15346 «// c();
15347 ˇ» // d();
15348 }
15349 "});
15350
15351 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15352
15353 cx.assert_editor_state(indoc! {"
15354 fn a() {
15355 // b();
15356 «c();
15357 ˇ» // d();
15358 }
15359 "});
15360
15361 // If a selection span a single line and is empty, the line is toggled.
15362 cx.set_state(indoc! {"
15363 fn a() {
15364 a();
15365 b();
15366 ˇ
15367 }
15368 "});
15369
15370 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15371
15372 cx.assert_editor_state(indoc! {"
15373 fn a() {
15374 a();
15375 b();
15376 //•ˇ
15377 }
15378 "});
15379
15380 // If a selection span multiple lines, empty lines are not toggled.
15381 cx.set_state(indoc! {"
15382 fn a() {
15383 «a();
15384
15385 c();ˇ»
15386 }
15387 "});
15388
15389 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15390
15391 cx.assert_editor_state(indoc! {"
15392 fn a() {
15393 // «a();
15394
15395 // c();ˇ»
15396 }
15397 "});
15398
15399 // If a selection includes multiple comment prefixes, all lines are uncommented.
15400 cx.set_state(indoc! {"
15401 fn a() {
15402 «// a();
15403 /// b();
15404 //! c();ˇ»
15405 }
15406 "});
15407
15408 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15409
15410 cx.assert_editor_state(indoc! {"
15411 fn a() {
15412 «a();
15413 b();
15414 c();ˇ»
15415 }
15416 "});
15417}
15418
15419#[gpui::test]
15420async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
15421 init_test(cx, |_| {});
15422 let mut cx = EditorTestContext::new(cx).await;
15423 let language = Arc::new(Language::new(
15424 LanguageConfig {
15425 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15426 ..Default::default()
15427 },
15428 Some(tree_sitter_rust::LANGUAGE.into()),
15429 ));
15430 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15431
15432 let toggle_comments = &ToggleComments {
15433 advance_downwards: false,
15434 ignore_indent: true,
15435 };
15436
15437 // If multiple selections intersect a line, the line is only toggled once.
15438 cx.set_state(indoc! {"
15439 fn a() {
15440 // «b();
15441 // c();
15442 // ˇ» d();
15443 }
15444 "});
15445
15446 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15447
15448 cx.assert_editor_state(indoc! {"
15449 fn a() {
15450 «b();
15451 c();
15452 ˇ» d();
15453 }
15454 "});
15455
15456 // The comment prefix is inserted at the beginning of each line
15457 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15458
15459 cx.assert_editor_state(indoc! {"
15460 fn a() {
15461 // «b();
15462 // c();
15463 // ˇ» d();
15464 }
15465 "});
15466
15467 // If a selection ends at the beginning of a line, that line is not toggled.
15468 cx.set_selections_state(indoc! {"
15469 fn a() {
15470 // b();
15471 // «c();
15472 ˇ»// d();
15473 }
15474 "});
15475
15476 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15477
15478 cx.assert_editor_state(indoc! {"
15479 fn a() {
15480 // b();
15481 «c();
15482 ˇ»// d();
15483 }
15484 "});
15485
15486 // If a selection span a single line and is empty, the line is toggled.
15487 cx.set_state(indoc! {"
15488 fn a() {
15489 a();
15490 b();
15491 ˇ
15492 }
15493 "});
15494
15495 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15496
15497 cx.assert_editor_state(indoc! {"
15498 fn a() {
15499 a();
15500 b();
15501 //ˇ
15502 }
15503 "});
15504
15505 // If a selection span multiple lines, empty lines are not toggled.
15506 cx.set_state(indoc! {"
15507 fn a() {
15508 «a();
15509
15510 c();ˇ»
15511 }
15512 "});
15513
15514 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15515
15516 cx.assert_editor_state(indoc! {"
15517 fn a() {
15518 // «a();
15519
15520 // c();ˇ»
15521 }
15522 "});
15523
15524 // If a selection includes multiple comment prefixes, all lines are uncommented.
15525 cx.set_state(indoc! {"
15526 fn a() {
15527 // «a();
15528 /// b();
15529 //! c();ˇ»
15530 }
15531 "});
15532
15533 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15534
15535 cx.assert_editor_state(indoc! {"
15536 fn a() {
15537 «a();
15538 b();
15539 c();ˇ»
15540 }
15541 "});
15542}
15543
15544#[gpui::test]
15545async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
15546 init_test(cx, |_| {});
15547
15548 let language = Arc::new(Language::new(
15549 LanguageConfig {
15550 line_comments: vec!["// ".into()],
15551 ..Default::default()
15552 },
15553 Some(tree_sitter_rust::LANGUAGE.into()),
15554 ));
15555
15556 let mut cx = EditorTestContext::new(cx).await;
15557
15558 cx.language_registry().add(language.clone());
15559 cx.update_buffer(|buffer, cx| {
15560 buffer.set_language(Some(language), cx);
15561 });
15562
15563 let toggle_comments = &ToggleComments {
15564 advance_downwards: true,
15565 ignore_indent: false,
15566 };
15567
15568 // Single cursor on one line -> advance
15569 // Cursor moves horizontally 3 characters as well on non-blank line
15570 cx.set_state(indoc!(
15571 "fn a() {
15572 ˇdog();
15573 cat();
15574 }"
15575 ));
15576 cx.update_editor(|editor, window, cx| {
15577 editor.toggle_comments(toggle_comments, window, cx);
15578 });
15579 cx.assert_editor_state(indoc!(
15580 "fn a() {
15581 // dog();
15582 catˇ();
15583 }"
15584 ));
15585
15586 // Single selection on one line -> don't advance
15587 cx.set_state(indoc!(
15588 "fn a() {
15589 «dog()ˇ»;
15590 cat();
15591 }"
15592 ));
15593 cx.update_editor(|editor, window, cx| {
15594 editor.toggle_comments(toggle_comments, window, cx);
15595 });
15596 cx.assert_editor_state(indoc!(
15597 "fn a() {
15598 // «dog()ˇ»;
15599 cat();
15600 }"
15601 ));
15602
15603 // Multiple cursors on one line -> advance
15604 cx.set_state(indoc!(
15605 "fn a() {
15606 ˇdˇog();
15607 cat();
15608 }"
15609 ));
15610 cx.update_editor(|editor, window, cx| {
15611 editor.toggle_comments(toggle_comments, window, cx);
15612 });
15613 cx.assert_editor_state(indoc!(
15614 "fn a() {
15615 // dog();
15616 catˇ(ˇ);
15617 }"
15618 ));
15619
15620 // Multiple cursors on one line, with selection -> don't advance
15621 cx.set_state(indoc!(
15622 "fn a() {
15623 ˇdˇog«()ˇ»;
15624 cat();
15625 }"
15626 ));
15627 cx.update_editor(|editor, window, cx| {
15628 editor.toggle_comments(toggle_comments, window, cx);
15629 });
15630 cx.assert_editor_state(indoc!(
15631 "fn a() {
15632 // ˇdˇog«()ˇ»;
15633 cat();
15634 }"
15635 ));
15636
15637 // Single cursor on one line -> advance
15638 // Cursor moves to column 0 on blank line
15639 cx.set_state(indoc!(
15640 "fn a() {
15641 ˇdog();
15642
15643 cat();
15644 }"
15645 ));
15646 cx.update_editor(|editor, window, cx| {
15647 editor.toggle_comments(toggle_comments, window, cx);
15648 });
15649 cx.assert_editor_state(indoc!(
15650 "fn a() {
15651 // dog();
15652 ˇ
15653 cat();
15654 }"
15655 ));
15656
15657 // Single cursor on one line -> advance
15658 // Cursor starts and ends at column 0
15659 cx.set_state(indoc!(
15660 "fn a() {
15661 ˇ dog();
15662 cat();
15663 }"
15664 ));
15665 cx.update_editor(|editor, window, cx| {
15666 editor.toggle_comments(toggle_comments, window, cx);
15667 });
15668 cx.assert_editor_state(indoc!(
15669 "fn a() {
15670 // dog();
15671 ˇ cat();
15672 }"
15673 ));
15674}
15675
15676#[gpui::test]
15677async fn test_toggle_block_comment(cx: &mut TestAppContext) {
15678 init_test(cx, |_| {});
15679
15680 let mut cx = EditorTestContext::new(cx).await;
15681
15682 let html_language = Arc::new(
15683 Language::new(
15684 LanguageConfig {
15685 name: "HTML".into(),
15686 block_comment: Some(BlockCommentConfig {
15687 start: "<!-- ".into(),
15688 prefix: "".into(),
15689 end: " -->".into(),
15690 tab_size: 0,
15691 }),
15692 ..Default::default()
15693 },
15694 Some(tree_sitter_html::LANGUAGE.into()),
15695 )
15696 .with_injection_query(
15697 r#"
15698 (script_element
15699 (raw_text) @injection.content
15700 (#set! injection.language "javascript"))
15701 "#,
15702 )
15703 .unwrap(),
15704 );
15705
15706 let javascript_language = Arc::new(Language::new(
15707 LanguageConfig {
15708 name: "JavaScript".into(),
15709 line_comments: vec!["// ".into()],
15710 ..Default::default()
15711 },
15712 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
15713 ));
15714
15715 cx.language_registry().add(html_language.clone());
15716 cx.language_registry().add(javascript_language);
15717 cx.update_buffer(|buffer, cx| {
15718 buffer.set_language(Some(html_language), cx);
15719 });
15720
15721 // Toggle comments for empty selections
15722 cx.set_state(
15723 &r#"
15724 <p>A</p>ˇ
15725 <p>B</p>ˇ
15726 <p>C</p>ˇ
15727 "#
15728 .unindent(),
15729 );
15730 cx.update_editor(|editor, window, cx| {
15731 editor.toggle_comments(&ToggleComments::default(), window, cx)
15732 });
15733 cx.assert_editor_state(
15734 &r#"
15735 <!-- <p>A</p>ˇ -->
15736 <!-- <p>B</p>ˇ -->
15737 <!-- <p>C</p>ˇ -->
15738 "#
15739 .unindent(),
15740 );
15741 cx.update_editor(|editor, window, cx| {
15742 editor.toggle_comments(&ToggleComments::default(), window, cx)
15743 });
15744 cx.assert_editor_state(
15745 &r#"
15746 <p>A</p>ˇ
15747 <p>B</p>ˇ
15748 <p>C</p>ˇ
15749 "#
15750 .unindent(),
15751 );
15752
15753 // Toggle comments for mixture of empty and non-empty selections, where
15754 // multiple selections occupy a given line.
15755 cx.set_state(
15756 &r#"
15757 <p>A«</p>
15758 <p>ˇ»B</p>ˇ
15759 <p>C«</p>
15760 <p>ˇ»D</p>ˇ
15761 "#
15762 .unindent(),
15763 );
15764
15765 cx.update_editor(|editor, window, cx| {
15766 editor.toggle_comments(&ToggleComments::default(), window, cx)
15767 });
15768 cx.assert_editor_state(
15769 &r#"
15770 <!-- <p>A«</p>
15771 <p>ˇ»B</p>ˇ -->
15772 <!-- <p>C«</p>
15773 <p>ˇ»D</p>ˇ -->
15774 "#
15775 .unindent(),
15776 );
15777 cx.update_editor(|editor, window, cx| {
15778 editor.toggle_comments(&ToggleComments::default(), window, cx)
15779 });
15780 cx.assert_editor_state(
15781 &r#"
15782 <p>A«</p>
15783 <p>ˇ»B</p>ˇ
15784 <p>C«</p>
15785 <p>ˇ»D</p>ˇ
15786 "#
15787 .unindent(),
15788 );
15789
15790 // Toggle comments when different languages are active for different
15791 // selections.
15792 cx.set_state(
15793 &r#"
15794 ˇ<script>
15795 ˇvar x = new Y();
15796 ˇ</script>
15797 "#
15798 .unindent(),
15799 );
15800 cx.executor().run_until_parked();
15801 cx.update_editor(|editor, window, cx| {
15802 editor.toggle_comments(&ToggleComments::default(), window, cx)
15803 });
15804 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
15805 // Uncommenting and commenting from this position brings in even more wrong artifacts.
15806 cx.assert_editor_state(
15807 &r#"
15808 <!-- ˇ<script> -->
15809 // ˇvar x = new Y();
15810 <!-- ˇ</script> -->
15811 "#
15812 .unindent(),
15813 );
15814}
15815
15816#[gpui::test]
15817fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
15818 init_test(cx, |_| {});
15819
15820 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
15821 let multibuffer = cx.new(|cx| {
15822 let mut multibuffer = MultiBuffer::new(ReadWrite);
15823 multibuffer.push_excerpts(
15824 buffer.clone(),
15825 [
15826 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
15827 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
15828 ],
15829 cx,
15830 );
15831 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
15832 multibuffer
15833 });
15834
15835 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
15836 editor.update_in(cx, |editor, window, cx| {
15837 assert_eq!(editor.text(cx), "aaaa\nbbbb");
15838 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15839 s.select_ranges([
15840 Point::new(0, 0)..Point::new(0, 0),
15841 Point::new(1, 0)..Point::new(1, 0),
15842 ])
15843 });
15844
15845 editor.handle_input("X", window, cx);
15846 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
15847 assert_eq!(
15848 editor.selections.ranges(cx),
15849 [
15850 Point::new(0, 1)..Point::new(0, 1),
15851 Point::new(1, 1)..Point::new(1, 1),
15852 ]
15853 );
15854
15855 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
15856 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15857 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
15858 });
15859 editor.backspace(&Default::default(), window, cx);
15860 assert_eq!(editor.text(cx), "Xa\nbbb");
15861 assert_eq!(
15862 editor.selections.ranges(cx),
15863 [Point::new(1, 0)..Point::new(1, 0)]
15864 );
15865
15866 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15867 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
15868 });
15869 editor.backspace(&Default::default(), window, cx);
15870 assert_eq!(editor.text(cx), "X\nbb");
15871 assert_eq!(
15872 editor.selections.ranges(cx),
15873 [Point::new(0, 1)..Point::new(0, 1)]
15874 );
15875 });
15876}
15877
15878#[gpui::test]
15879fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
15880 init_test(cx, |_| {});
15881
15882 let markers = vec![('[', ']').into(), ('(', ')').into()];
15883 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
15884 indoc! {"
15885 [aaaa
15886 (bbbb]
15887 cccc)",
15888 },
15889 markers.clone(),
15890 );
15891 let excerpt_ranges = markers.into_iter().map(|marker| {
15892 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
15893 ExcerptRange::new(context)
15894 });
15895 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
15896 let multibuffer = cx.new(|cx| {
15897 let mut multibuffer = MultiBuffer::new(ReadWrite);
15898 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
15899 multibuffer
15900 });
15901
15902 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
15903 editor.update_in(cx, |editor, window, cx| {
15904 let (expected_text, selection_ranges) = marked_text_ranges(
15905 indoc! {"
15906 aaaa
15907 bˇbbb
15908 bˇbbˇb
15909 cccc"
15910 },
15911 true,
15912 );
15913 assert_eq!(editor.text(cx), expected_text);
15914 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15915 s.select_ranges(selection_ranges)
15916 });
15917
15918 editor.handle_input("X", window, cx);
15919
15920 let (expected_text, expected_selections) = marked_text_ranges(
15921 indoc! {"
15922 aaaa
15923 bXˇbbXb
15924 bXˇbbXˇb
15925 cccc"
15926 },
15927 false,
15928 );
15929 assert_eq!(editor.text(cx), expected_text);
15930 assert_eq!(editor.selections.ranges(cx), expected_selections);
15931
15932 editor.newline(&Newline, window, cx);
15933 let (expected_text, expected_selections) = marked_text_ranges(
15934 indoc! {"
15935 aaaa
15936 bX
15937 ˇbbX
15938 b
15939 bX
15940 ˇbbX
15941 ˇb
15942 cccc"
15943 },
15944 false,
15945 );
15946 assert_eq!(editor.text(cx), expected_text);
15947 assert_eq!(editor.selections.ranges(cx), expected_selections);
15948 });
15949}
15950
15951#[gpui::test]
15952fn test_refresh_selections(cx: &mut TestAppContext) {
15953 init_test(cx, |_| {});
15954
15955 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
15956 let mut excerpt1_id = None;
15957 let multibuffer = cx.new(|cx| {
15958 let mut multibuffer = MultiBuffer::new(ReadWrite);
15959 excerpt1_id = multibuffer
15960 .push_excerpts(
15961 buffer.clone(),
15962 [
15963 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
15964 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
15965 ],
15966 cx,
15967 )
15968 .into_iter()
15969 .next();
15970 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
15971 multibuffer
15972 });
15973
15974 let editor = cx.add_window(|window, cx| {
15975 let mut editor = build_editor(multibuffer.clone(), window, cx);
15976 let snapshot = editor.snapshot(window, cx);
15977 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15978 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
15979 });
15980 editor.begin_selection(
15981 Point::new(2, 1).to_display_point(&snapshot),
15982 true,
15983 1,
15984 window,
15985 cx,
15986 );
15987 assert_eq!(
15988 editor.selections.ranges(cx),
15989 [
15990 Point::new(1, 3)..Point::new(1, 3),
15991 Point::new(2, 1)..Point::new(2, 1),
15992 ]
15993 );
15994 editor
15995 });
15996
15997 // Refreshing selections is a no-op when excerpts haven't changed.
15998 _ = editor.update(cx, |editor, window, cx| {
15999 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16000 assert_eq!(
16001 editor.selections.ranges(cx),
16002 [
16003 Point::new(1, 3)..Point::new(1, 3),
16004 Point::new(2, 1)..Point::new(2, 1),
16005 ]
16006 );
16007 });
16008
16009 multibuffer.update(cx, |multibuffer, cx| {
16010 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
16011 });
16012 _ = editor.update(cx, |editor, window, cx| {
16013 // Removing an excerpt causes the first selection to become degenerate.
16014 assert_eq!(
16015 editor.selections.ranges(cx),
16016 [
16017 Point::new(0, 0)..Point::new(0, 0),
16018 Point::new(0, 1)..Point::new(0, 1)
16019 ]
16020 );
16021
16022 // Refreshing selections will relocate the first selection to the original buffer
16023 // location.
16024 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16025 assert_eq!(
16026 editor.selections.ranges(cx),
16027 [
16028 Point::new(0, 1)..Point::new(0, 1),
16029 Point::new(0, 3)..Point::new(0, 3)
16030 ]
16031 );
16032 assert!(editor.selections.pending_anchor().is_some());
16033 });
16034}
16035
16036#[gpui::test]
16037fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
16038 init_test(cx, |_| {});
16039
16040 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16041 let mut excerpt1_id = None;
16042 let multibuffer = cx.new(|cx| {
16043 let mut multibuffer = MultiBuffer::new(ReadWrite);
16044 excerpt1_id = multibuffer
16045 .push_excerpts(
16046 buffer.clone(),
16047 [
16048 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
16049 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
16050 ],
16051 cx,
16052 )
16053 .into_iter()
16054 .next();
16055 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
16056 multibuffer
16057 });
16058
16059 let editor = cx.add_window(|window, cx| {
16060 let mut editor = build_editor(multibuffer.clone(), window, cx);
16061 let snapshot = editor.snapshot(window, cx);
16062 editor.begin_selection(
16063 Point::new(1, 3).to_display_point(&snapshot),
16064 false,
16065 1,
16066 window,
16067 cx,
16068 );
16069 assert_eq!(
16070 editor.selections.ranges(cx),
16071 [Point::new(1, 3)..Point::new(1, 3)]
16072 );
16073 editor
16074 });
16075
16076 multibuffer.update(cx, |multibuffer, cx| {
16077 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
16078 });
16079 _ = editor.update(cx, |editor, window, cx| {
16080 assert_eq!(
16081 editor.selections.ranges(cx),
16082 [Point::new(0, 0)..Point::new(0, 0)]
16083 );
16084
16085 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
16086 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16087 assert_eq!(
16088 editor.selections.ranges(cx),
16089 [Point::new(0, 3)..Point::new(0, 3)]
16090 );
16091 assert!(editor.selections.pending_anchor().is_some());
16092 });
16093}
16094
16095#[gpui::test]
16096async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
16097 init_test(cx, |_| {});
16098
16099 let language = Arc::new(
16100 Language::new(
16101 LanguageConfig {
16102 brackets: BracketPairConfig {
16103 pairs: vec![
16104 BracketPair {
16105 start: "{".to_string(),
16106 end: "}".to_string(),
16107 close: true,
16108 surround: true,
16109 newline: true,
16110 },
16111 BracketPair {
16112 start: "/* ".to_string(),
16113 end: " */".to_string(),
16114 close: true,
16115 surround: true,
16116 newline: true,
16117 },
16118 ],
16119 ..Default::default()
16120 },
16121 ..Default::default()
16122 },
16123 Some(tree_sitter_rust::LANGUAGE.into()),
16124 )
16125 .with_indents_query("")
16126 .unwrap(),
16127 );
16128
16129 let text = concat!(
16130 "{ }\n", //
16131 " x\n", //
16132 " /* */\n", //
16133 "x\n", //
16134 "{{} }\n", //
16135 );
16136
16137 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
16138 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
16139 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
16140 editor
16141 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
16142 .await;
16143
16144 editor.update_in(cx, |editor, window, cx| {
16145 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16146 s.select_display_ranges([
16147 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
16148 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
16149 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
16150 ])
16151 });
16152 editor.newline(&Newline, window, cx);
16153
16154 assert_eq!(
16155 editor.buffer().read(cx).read(cx).text(),
16156 concat!(
16157 "{ \n", // Suppress rustfmt
16158 "\n", //
16159 "}\n", //
16160 " x\n", //
16161 " /* \n", //
16162 " \n", //
16163 " */\n", //
16164 "x\n", //
16165 "{{} \n", //
16166 "}\n", //
16167 )
16168 );
16169 });
16170}
16171
16172#[gpui::test]
16173fn test_highlighted_ranges(cx: &mut TestAppContext) {
16174 init_test(cx, |_| {});
16175
16176 let editor = cx.add_window(|window, cx| {
16177 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
16178 build_editor(buffer, window, cx)
16179 });
16180
16181 _ = editor.update(cx, |editor, window, cx| {
16182 struct Type1;
16183 struct Type2;
16184
16185 let buffer = editor.buffer.read(cx).snapshot(cx);
16186
16187 let anchor_range =
16188 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
16189
16190 editor.highlight_background::<Type1>(
16191 &[
16192 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
16193 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
16194 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
16195 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
16196 ],
16197 |_| Hsla::red(),
16198 cx,
16199 );
16200 editor.highlight_background::<Type2>(
16201 &[
16202 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
16203 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
16204 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
16205 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
16206 ],
16207 |_| Hsla::green(),
16208 cx,
16209 );
16210
16211 let snapshot = editor.snapshot(window, cx);
16212 let highlighted_ranges = editor.sorted_background_highlights_in_range(
16213 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
16214 &snapshot,
16215 cx.theme(),
16216 );
16217 assert_eq!(
16218 highlighted_ranges,
16219 &[
16220 (
16221 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
16222 Hsla::green(),
16223 ),
16224 (
16225 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
16226 Hsla::red(),
16227 ),
16228 (
16229 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
16230 Hsla::green(),
16231 ),
16232 (
16233 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16234 Hsla::red(),
16235 ),
16236 ]
16237 );
16238 assert_eq!(
16239 editor.sorted_background_highlights_in_range(
16240 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
16241 &snapshot,
16242 cx.theme(),
16243 ),
16244 &[(
16245 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16246 Hsla::red(),
16247 )]
16248 );
16249 });
16250}
16251
16252#[gpui::test]
16253async fn test_following(cx: &mut TestAppContext) {
16254 init_test(cx, |_| {});
16255
16256 let fs = FakeFs::new(cx.executor());
16257 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16258
16259 let buffer = project.update(cx, |project, cx| {
16260 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, false, cx);
16261 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
16262 });
16263 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
16264 let follower = cx.update(|cx| {
16265 cx.open_window(
16266 WindowOptions {
16267 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
16268 gpui::Point::new(px(0.), px(0.)),
16269 gpui::Point::new(px(10.), px(80.)),
16270 ))),
16271 ..Default::default()
16272 },
16273 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
16274 )
16275 .unwrap()
16276 });
16277
16278 let is_still_following = Rc::new(RefCell::new(true));
16279 let follower_edit_event_count = Rc::new(RefCell::new(0));
16280 let pending_update = Rc::new(RefCell::new(None));
16281 let leader_entity = leader.root(cx).unwrap();
16282 let follower_entity = follower.root(cx).unwrap();
16283 _ = follower.update(cx, {
16284 let update = pending_update.clone();
16285 let is_still_following = is_still_following.clone();
16286 let follower_edit_event_count = follower_edit_event_count.clone();
16287 |_, window, cx| {
16288 cx.subscribe_in(
16289 &leader_entity,
16290 window,
16291 move |_, leader, event, window, cx| {
16292 leader.read(cx).add_event_to_update_proto(
16293 event,
16294 &mut update.borrow_mut(),
16295 window,
16296 cx,
16297 );
16298 },
16299 )
16300 .detach();
16301
16302 cx.subscribe_in(
16303 &follower_entity,
16304 window,
16305 move |_, _, event: &EditorEvent, _window, _cx| {
16306 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
16307 *is_still_following.borrow_mut() = false;
16308 }
16309
16310 if let EditorEvent::BufferEdited = event {
16311 *follower_edit_event_count.borrow_mut() += 1;
16312 }
16313 },
16314 )
16315 .detach();
16316 }
16317 });
16318
16319 // Update the selections only
16320 _ = leader.update(cx, |leader, window, cx| {
16321 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16322 s.select_ranges([1..1])
16323 });
16324 });
16325 follower
16326 .update(cx, |follower, window, cx| {
16327 follower.apply_update_proto(
16328 &project,
16329 pending_update.borrow_mut().take().unwrap(),
16330 window,
16331 cx,
16332 )
16333 })
16334 .unwrap()
16335 .await
16336 .unwrap();
16337 _ = follower.update(cx, |follower, _, cx| {
16338 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
16339 });
16340 assert!(*is_still_following.borrow());
16341 assert_eq!(*follower_edit_event_count.borrow(), 0);
16342
16343 // Update the scroll position only
16344 _ = leader.update(cx, |leader, window, cx| {
16345 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16346 });
16347 follower
16348 .update(cx, |follower, window, cx| {
16349 follower.apply_update_proto(
16350 &project,
16351 pending_update.borrow_mut().take().unwrap(),
16352 window,
16353 cx,
16354 )
16355 })
16356 .unwrap()
16357 .await
16358 .unwrap();
16359 assert_eq!(
16360 follower
16361 .update(cx, |follower, _, cx| follower.scroll_position(cx))
16362 .unwrap(),
16363 gpui::Point::new(1.5, 3.5)
16364 );
16365 assert!(*is_still_following.borrow());
16366 assert_eq!(*follower_edit_event_count.borrow(), 0);
16367
16368 // Update the selections and scroll position. The follower's scroll position is updated
16369 // via autoscroll, not via the leader's exact scroll position.
16370 _ = leader.update(cx, |leader, window, cx| {
16371 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16372 s.select_ranges([0..0])
16373 });
16374 leader.request_autoscroll(Autoscroll::newest(), cx);
16375 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16376 });
16377 follower
16378 .update(cx, |follower, window, cx| {
16379 follower.apply_update_proto(
16380 &project,
16381 pending_update.borrow_mut().take().unwrap(),
16382 window,
16383 cx,
16384 )
16385 })
16386 .unwrap()
16387 .await
16388 .unwrap();
16389 _ = follower.update(cx, |follower, _, cx| {
16390 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
16391 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
16392 });
16393 assert!(*is_still_following.borrow());
16394
16395 // Creating a pending selection that precedes another selection
16396 _ = leader.update(cx, |leader, window, cx| {
16397 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16398 s.select_ranges([1..1])
16399 });
16400 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
16401 });
16402 follower
16403 .update(cx, |follower, window, cx| {
16404 follower.apply_update_proto(
16405 &project,
16406 pending_update.borrow_mut().take().unwrap(),
16407 window,
16408 cx,
16409 )
16410 })
16411 .unwrap()
16412 .await
16413 .unwrap();
16414 _ = follower.update(cx, |follower, _, cx| {
16415 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
16416 });
16417 assert!(*is_still_following.borrow());
16418
16419 // Extend the pending selection so that it surrounds another selection
16420 _ = leader.update(cx, |leader, window, cx| {
16421 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
16422 });
16423 follower
16424 .update(cx, |follower, window, cx| {
16425 follower.apply_update_proto(
16426 &project,
16427 pending_update.borrow_mut().take().unwrap(),
16428 window,
16429 cx,
16430 )
16431 })
16432 .unwrap()
16433 .await
16434 .unwrap();
16435 _ = follower.update(cx, |follower, _, cx| {
16436 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
16437 });
16438
16439 // Scrolling locally breaks the follow
16440 _ = follower.update(cx, |follower, window, cx| {
16441 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
16442 follower.set_scroll_anchor(
16443 ScrollAnchor {
16444 anchor: top_anchor,
16445 offset: gpui::Point::new(0.0, 0.5),
16446 },
16447 window,
16448 cx,
16449 );
16450 });
16451 assert!(!(*is_still_following.borrow()));
16452}
16453
16454#[gpui::test]
16455async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
16456 init_test(cx, |_| {});
16457
16458 let fs = FakeFs::new(cx.executor());
16459 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16460 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16461 let pane = workspace
16462 .update(cx, |workspace, _, _| workspace.active_pane().clone())
16463 .unwrap();
16464
16465 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16466
16467 let leader = pane.update_in(cx, |_, window, cx| {
16468 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
16469 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
16470 });
16471
16472 // Start following the editor when it has no excerpts.
16473 let mut state_message =
16474 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16475 let workspace_entity = workspace.root(cx).unwrap();
16476 let follower_1 = cx
16477 .update_window(*workspace.deref(), |_, window, cx| {
16478 Editor::from_state_proto(
16479 workspace_entity,
16480 ViewId {
16481 creator: CollaboratorId::PeerId(PeerId::default()),
16482 id: 0,
16483 },
16484 &mut state_message,
16485 window,
16486 cx,
16487 )
16488 })
16489 .unwrap()
16490 .unwrap()
16491 .await
16492 .unwrap();
16493
16494 let update_message = Rc::new(RefCell::new(None));
16495 follower_1.update_in(cx, {
16496 let update = update_message.clone();
16497 |_, window, cx| {
16498 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
16499 leader.read(cx).add_event_to_update_proto(
16500 event,
16501 &mut update.borrow_mut(),
16502 window,
16503 cx,
16504 );
16505 })
16506 .detach();
16507 }
16508 });
16509
16510 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
16511 (
16512 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, false, cx),
16513 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, false, cx),
16514 )
16515 });
16516
16517 // Insert some excerpts.
16518 leader.update(cx, |leader, cx| {
16519 leader.buffer.update(cx, |multibuffer, cx| {
16520 multibuffer.set_excerpts_for_path(
16521 PathKey::with_sort_prefix(1, rel_path("b.txt").into_arc()),
16522 buffer_1.clone(),
16523 vec![
16524 Point::row_range(0..3),
16525 Point::row_range(1..6),
16526 Point::row_range(12..15),
16527 ],
16528 0,
16529 cx,
16530 );
16531 multibuffer.set_excerpts_for_path(
16532 PathKey::with_sort_prefix(1, rel_path("a.txt").into_arc()),
16533 buffer_2.clone(),
16534 vec![Point::row_range(0..6), Point::row_range(8..12)],
16535 0,
16536 cx,
16537 );
16538 });
16539 });
16540
16541 // Apply the update of adding the excerpts.
16542 follower_1
16543 .update_in(cx, |follower, window, cx| {
16544 follower.apply_update_proto(
16545 &project,
16546 update_message.borrow().clone().unwrap(),
16547 window,
16548 cx,
16549 )
16550 })
16551 .await
16552 .unwrap();
16553 assert_eq!(
16554 follower_1.update(cx, |editor, cx| editor.text(cx)),
16555 leader.update(cx, |editor, cx| editor.text(cx))
16556 );
16557 update_message.borrow_mut().take();
16558
16559 // Start following separately after it already has excerpts.
16560 let mut state_message =
16561 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16562 let workspace_entity = workspace.root(cx).unwrap();
16563 let follower_2 = cx
16564 .update_window(*workspace.deref(), |_, window, cx| {
16565 Editor::from_state_proto(
16566 workspace_entity,
16567 ViewId {
16568 creator: CollaboratorId::PeerId(PeerId::default()),
16569 id: 0,
16570 },
16571 &mut state_message,
16572 window,
16573 cx,
16574 )
16575 })
16576 .unwrap()
16577 .unwrap()
16578 .await
16579 .unwrap();
16580 assert_eq!(
16581 follower_2.update(cx, |editor, cx| editor.text(cx)),
16582 leader.update(cx, |editor, cx| editor.text(cx))
16583 );
16584
16585 // Remove some excerpts.
16586 leader.update(cx, |leader, cx| {
16587 leader.buffer.update(cx, |multibuffer, cx| {
16588 let excerpt_ids = multibuffer.excerpt_ids();
16589 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
16590 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
16591 });
16592 });
16593
16594 // Apply the update of removing the excerpts.
16595 follower_1
16596 .update_in(cx, |follower, window, cx| {
16597 follower.apply_update_proto(
16598 &project,
16599 update_message.borrow().clone().unwrap(),
16600 window,
16601 cx,
16602 )
16603 })
16604 .await
16605 .unwrap();
16606 follower_2
16607 .update_in(cx, |follower, window, cx| {
16608 follower.apply_update_proto(
16609 &project,
16610 update_message.borrow().clone().unwrap(),
16611 window,
16612 cx,
16613 )
16614 })
16615 .await
16616 .unwrap();
16617 update_message.borrow_mut().take();
16618 assert_eq!(
16619 follower_1.update(cx, |editor, cx| editor.text(cx)),
16620 leader.update(cx, |editor, cx| editor.text(cx))
16621 );
16622}
16623
16624#[gpui::test]
16625async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16626 init_test(cx, |_| {});
16627
16628 let mut cx = EditorTestContext::new(cx).await;
16629 let lsp_store =
16630 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
16631
16632 cx.set_state(indoc! {"
16633 ˇfn func(abc def: i32) -> u32 {
16634 }
16635 "});
16636
16637 cx.update(|_, cx| {
16638 lsp_store.update(cx, |lsp_store, cx| {
16639 lsp_store
16640 .update_diagnostics(
16641 LanguageServerId(0),
16642 lsp::PublishDiagnosticsParams {
16643 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
16644 version: None,
16645 diagnostics: vec![
16646 lsp::Diagnostic {
16647 range: lsp::Range::new(
16648 lsp::Position::new(0, 11),
16649 lsp::Position::new(0, 12),
16650 ),
16651 severity: Some(lsp::DiagnosticSeverity::ERROR),
16652 ..Default::default()
16653 },
16654 lsp::Diagnostic {
16655 range: lsp::Range::new(
16656 lsp::Position::new(0, 12),
16657 lsp::Position::new(0, 15),
16658 ),
16659 severity: Some(lsp::DiagnosticSeverity::ERROR),
16660 ..Default::default()
16661 },
16662 lsp::Diagnostic {
16663 range: lsp::Range::new(
16664 lsp::Position::new(0, 25),
16665 lsp::Position::new(0, 28),
16666 ),
16667 severity: Some(lsp::DiagnosticSeverity::ERROR),
16668 ..Default::default()
16669 },
16670 ],
16671 },
16672 None,
16673 DiagnosticSourceKind::Pushed,
16674 &[],
16675 cx,
16676 )
16677 .unwrap()
16678 });
16679 });
16680
16681 executor.run_until_parked();
16682
16683 cx.update_editor(|editor, window, cx| {
16684 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16685 });
16686
16687 cx.assert_editor_state(indoc! {"
16688 fn func(abc def: i32) -> ˇu32 {
16689 }
16690 "});
16691
16692 cx.update_editor(|editor, window, cx| {
16693 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16694 });
16695
16696 cx.assert_editor_state(indoc! {"
16697 fn func(abc ˇdef: i32) -> u32 {
16698 }
16699 "});
16700
16701 cx.update_editor(|editor, window, cx| {
16702 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16703 });
16704
16705 cx.assert_editor_state(indoc! {"
16706 fn func(abcˇ def: i32) -> u32 {
16707 }
16708 "});
16709
16710 cx.update_editor(|editor, window, cx| {
16711 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16712 });
16713
16714 cx.assert_editor_state(indoc! {"
16715 fn func(abc def: i32) -> ˇu32 {
16716 }
16717 "});
16718}
16719
16720#[gpui::test]
16721async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16722 init_test(cx, |_| {});
16723
16724 let mut cx = EditorTestContext::new(cx).await;
16725
16726 let diff_base = r#"
16727 use some::mod;
16728
16729 const A: u32 = 42;
16730
16731 fn main() {
16732 println!("hello");
16733
16734 println!("world");
16735 }
16736 "#
16737 .unindent();
16738
16739 // Edits are modified, removed, modified, added
16740 cx.set_state(
16741 &r#"
16742 use some::modified;
16743
16744 ˇ
16745 fn main() {
16746 println!("hello there");
16747
16748 println!("around the");
16749 println!("world");
16750 }
16751 "#
16752 .unindent(),
16753 );
16754
16755 cx.set_head_text(&diff_base);
16756 executor.run_until_parked();
16757
16758 cx.update_editor(|editor, window, cx| {
16759 //Wrap around the bottom of the buffer
16760 for _ in 0..3 {
16761 editor.go_to_next_hunk(&GoToHunk, window, cx);
16762 }
16763 });
16764
16765 cx.assert_editor_state(
16766 &r#"
16767 ˇuse some::modified;
16768
16769
16770 fn main() {
16771 println!("hello there");
16772
16773 println!("around the");
16774 println!("world");
16775 }
16776 "#
16777 .unindent(),
16778 );
16779
16780 cx.update_editor(|editor, window, cx| {
16781 //Wrap around the top of the buffer
16782 for _ in 0..2 {
16783 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16784 }
16785 });
16786
16787 cx.assert_editor_state(
16788 &r#"
16789 use some::modified;
16790
16791
16792 fn main() {
16793 ˇ println!("hello there");
16794
16795 println!("around the");
16796 println!("world");
16797 }
16798 "#
16799 .unindent(),
16800 );
16801
16802 cx.update_editor(|editor, window, cx| {
16803 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16804 });
16805
16806 cx.assert_editor_state(
16807 &r#"
16808 use some::modified;
16809
16810 ˇ
16811 fn main() {
16812 println!("hello there");
16813
16814 println!("around the");
16815 println!("world");
16816 }
16817 "#
16818 .unindent(),
16819 );
16820
16821 cx.update_editor(|editor, window, cx| {
16822 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16823 });
16824
16825 cx.assert_editor_state(
16826 &r#"
16827 ˇuse some::modified;
16828
16829
16830 fn main() {
16831 println!("hello there");
16832
16833 println!("around the");
16834 println!("world");
16835 }
16836 "#
16837 .unindent(),
16838 );
16839
16840 cx.update_editor(|editor, window, cx| {
16841 for _ in 0..2 {
16842 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16843 }
16844 });
16845
16846 cx.assert_editor_state(
16847 &r#"
16848 use some::modified;
16849
16850
16851 fn main() {
16852 ˇ println!("hello there");
16853
16854 println!("around the");
16855 println!("world");
16856 }
16857 "#
16858 .unindent(),
16859 );
16860
16861 cx.update_editor(|editor, window, cx| {
16862 editor.fold(&Fold, window, cx);
16863 });
16864
16865 cx.update_editor(|editor, window, cx| {
16866 editor.go_to_next_hunk(&GoToHunk, window, cx);
16867 });
16868
16869 cx.assert_editor_state(
16870 &r#"
16871 ˇuse some::modified;
16872
16873
16874 fn main() {
16875 println!("hello there");
16876
16877 println!("around the");
16878 println!("world");
16879 }
16880 "#
16881 .unindent(),
16882 );
16883}
16884
16885#[test]
16886fn test_split_words() {
16887 fn split(text: &str) -> Vec<&str> {
16888 split_words(text).collect()
16889 }
16890
16891 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
16892 assert_eq!(split("hello_world"), &["hello_", "world"]);
16893 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
16894 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
16895 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
16896 assert_eq!(split("helloworld"), &["helloworld"]);
16897
16898 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
16899}
16900
16901#[gpui::test]
16902async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
16903 init_test(cx, |_| {});
16904
16905 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
16906 let mut assert = |before, after| {
16907 let _state_context = cx.set_state(before);
16908 cx.run_until_parked();
16909 cx.update_editor(|editor, window, cx| {
16910 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
16911 });
16912 cx.run_until_parked();
16913 cx.assert_editor_state(after);
16914 };
16915
16916 // Outside bracket jumps to outside of matching bracket
16917 assert("console.logˇ(var);", "console.log(var)ˇ;");
16918 assert("console.log(var)ˇ;", "console.logˇ(var);");
16919
16920 // Inside bracket jumps to inside of matching bracket
16921 assert("console.log(ˇvar);", "console.log(varˇ);");
16922 assert("console.log(varˇ);", "console.log(ˇvar);");
16923
16924 // When outside a bracket and inside, favor jumping to the inside bracket
16925 assert(
16926 "console.log('foo', [1, 2, 3]ˇ);",
16927 "console.log(ˇ'foo', [1, 2, 3]);",
16928 );
16929 assert(
16930 "console.log(ˇ'foo', [1, 2, 3]);",
16931 "console.log('foo', [1, 2, 3]ˇ);",
16932 );
16933
16934 // Bias forward if two options are equally likely
16935 assert(
16936 "let result = curried_fun()ˇ();",
16937 "let result = curried_fun()()ˇ;",
16938 );
16939
16940 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
16941 assert(
16942 indoc! {"
16943 function test() {
16944 console.log('test')ˇ
16945 }"},
16946 indoc! {"
16947 function test() {
16948 console.logˇ('test')
16949 }"},
16950 );
16951}
16952
16953#[gpui::test]
16954async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
16955 init_test(cx, |_| {});
16956
16957 let fs = FakeFs::new(cx.executor());
16958 fs.insert_tree(
16959 path!("/a"),
16960 json!({
16961 "main.rs": "fn main() { let a = 5; }",
16962 "other.rs": "// Test file",
16963 }),
16964 )
16965 .await;
16966 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
16967
16968 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
16969 language_registry.add(Arc::new(Language::new(
16970 LanguageConfig {
16971 name: "Rust".into(),
16972 matcher: LanguageMatcher {
16973 path_suffixes: vec!["rs".to_string()],
16974 ..Default::default()
16975 },
16976 brackets: BracketPairConfig {
16977 pairs: vec![BracketPair {
16978 start: "{".to_string(),
16979 end: "}".to_string(),
16980 close: true,
16981 surround: true,
16982 newline: true,
16983 }],
16984 disabled_scopes_by_bracket_ix: Vec::new(),
16985 },
16986 ..Default::default()
16987 },
16988 Some(tree_sitter_rust::LANGUAGE.into()),
16989 )));
16990 let mut fake_servers = language_registry.register_fake_lsp(
16991 "Rust",
16992 FakeLspAdapter {
16993 capabilities: lsp::ServerCapabilities {
16994 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
16995 first_trigger_character: "{".to_string(),
16996 more_trigger_character: None,
16997 }),
16998 ..Default::default()
16999 },
17000 ..Default::default()
17001 },
17002 );
17003
17004 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17005
17006 let cx = &mut VisualTestContext::from_window(*workspace, cx);
17007
17008 let worktree_id = workspace
17009 .update(cx, |workspace, _, cx| {
17010 workspace.project().update(cx, |project, cx| {
17011 project.worktrees(cx).next().unwrap().read(cx).id()
17012 })
17013 })
17014 .unwrap();
17015
17016 let buffer = project
17017 .update(cx, |project, cx| {
17018 project.open_local_buffer(path!("/a/main.rs"), cx)
17019 })
17020 .await
17021 .unwrap();
17022 let editor_handle = workspace
17023 .update(cx, |workspace, window, cx| {
17024 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
17025 })
17026 .unwrap()
17027 .await
17028 .unwrap()
17029 .downcast::<Editor>()
17030 .unwrap();
17031
17032 cx.executor().start_waiting();
17033 let fake_server = fake_servers.next().await.unwrap();
17034
17035 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
17036 |params, _| async move {
17037 assert_eq!(
17038 params.text_document_position.text_document.uri,
17039 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
17040 );
17041 assert_eq!(
17042 params.text_document_position.position,
17043 lsp::Position::new(0, 21),
17044 );
17045
17046 Ok(Some(vec![lsp::TextEdit {
17047 new_text: "]".to_string(),
17048 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17049 }]))
17050 },
17051 );
17052
17053 editor_handle.update_in(cx, |editor, window, cx| {
17054 window.focus(&editor.focus_handle(cx));
17055 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17056 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
17057 });
17058 editor.handle_input("{", window, cx);
17059 });
17060
17061 cx.executor().run_until_parked();
17062
17063 buffer.update(cx, |buffer, _| {
17064 assert_eq!(
17065 buffer.text(),
17066 "fn main() { let a = {5}; }",
17067 "No extra braces from on type formatting should appear in the buffer"
17068 )
17069 });
17070}
17071
17072#[gpui::test(iterations = 20, seeds(31))]
17073async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
17074 init_test(cx, |_| {});
17075
17076 let mut cx = EditorLspTestContext::new_rust(
17077 lsp::ServerCapabilities {
17078 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
17079 first_trigger_character: ".".to_string(),
17080 more_trigger_character: None,
17081 }),
17082 ..Default::default()
17083 },
17084 cx,
17085 )
17086 .await;
17087
17088 cx.update_buffer(|buffer, _| {
17089 // This causes autoindent to be async.
17090 buffer.set_sync_parse_timeout(Duration::ZERO)
17091 });
17092
17093 cx.set_state("fn c() {\n d()ˇ\n}\n");
17094 cx.simulate_keystroke("\n");
17095 cx.run_until_parked();
17096
17097 let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap());
17098 let mut request =
17099 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
17100 let buffer_cloned = buffer_cloned.clone();
17101 async move {
17102 buffer_cloned.update(&mut cx, |buffer, _| {
17103 assert_eq!(
17104 buffer.text(),
17105 "fn c() {\n d()\n .\n}\n",
17106 "OnTypeFormatting should triggered after autoindent applied"
17107 )
17108 })?;
17109
17110 Ok(Some(vec![]))
17111 }
17112 });
17113
17114 cx.simulate_keystroke(".");
17115 cx.run_until_parked();
17116
17117 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
17118 assert!(request.next().await.is_some());
17119 request.close();
17120 assert!(request.next().await.is_none());
17121}
17122
17123#[gpui::test]
17124async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
17125 init_test(cx, |_| {});
17126
17127 let fs = FakeFs::new(cx.executor());
17128 fs.insert_tree(
17129 path!("/a"),
17130 json!({
17131 "main.rs": "fn main() { let a = 5; }",
17132 "other.rs": "// Test file",
17133 }),
17134 )
17135 .await;
17136
17137 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17138
17139 let server_restarts = Arc::new(AtomicUsize::new(0));
17140 let closure_restarts = Arc::clone(&server_restarts);
17141 let language_server_name = "test language server";
17142 let language_name: LanguageName = "Rust".into();
17143
17144 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17145 language_registry.add(Arc::new(Language::new(
17146 LanguageConfig {
17147 name: language_name.clone(),
17148 matcher: LanguageMatcher {
17149 path_suffixes: vec!["rs".to_string()],
17150 ..Default::default()
17151 },
17152 ..Default::default()
17153 },
17154 Some(tree_sitter_rust::LANGUAGE.into()),
17155 )));
17156 let mut fake_servers = language_registry.register_fake_lsp(
17157 "Rust",
17158 FakeLspAdapter {
17159 name: language_server_name,
17160 initialization_options: Some(json!({
17161 "testOptionValue": true
17162 })),
17163 initializer: Some(Box::new(move |fake_server| {
17164 let task_restarts = Arc::clone(&closure_restarts);
17165 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
17166 task_restarts.fetch_add(1, atomic::Ordering::Release);
17167 futures::future::ready(Ok(()))
17168 });
17169 })),
17170 ..Default::default()
17171 },
17172 );
17173
17174 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17175 let _buffer = project
17176 .update(cx, |project, cx| {
17177 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
17178 })
17179 .await
17180 .unwrap();
17181 let _fake_server = fake_servers.next().await.unwrap();
17182 update_test_language_settings(cx, |language_settings| {
17183 language_settings.languages.0.insert(
17184 language_name.clone().0,
17185 LanguageSettingsContent {
17186 tab_size: NonZeroU32::new(8),
17187 ..Default::default()
17188 },
17189 );
17190 });
17191 cx.executor().run_until_parked();
17192 assert_eq!(
17193 server_restarts.load(atomic::Ordering::Acquire),
17194 0,
17195 "Should not restart LSP server on an unrelated change"
17196 );
17197
17198 update_test_project_settings(cx, |project_settings| {
17199 project_settings.lsp.insert(
17200 "Some other server name".into(),
17201 LspSettings {
17202 binary: None,
17203 settings: None,
17204 initialization_options: Some(json!({
17205 "some other init value": false
17206 })),
17207 enable_lsp_tasks: false,
17208 fetch: None,
17209 },
17210 );
17211 });
17212 cx.executor().run_until_parked();
17213 assert_eq!(
17214 server_restarts.load(atomic::Ordering::Acquire),
17215 0,
17216 "Should not restart LSP server on an unrelated LSP settings change"
17217 );
17218
17219 update_test_project_settings(cx, |project_settings| {
17220 project_settings.lsp.insert(
17221 language_server_name.into(),
17222 LspSettings {
17223 binary: None,
17224 settings: None,
17225 initialization_options: Some(json!({
17226 "anotherInitValue": false
17227 })),
17228 enable_lsp_tasks: false,
17229 fetch: None,
17230 },
17231 );
17232 });
17233 cx.executor().run_until_parked();
17234 assert_eq!(
17235 server_restarts.load(atomic::Ordering::Acquire),
17236 1,
17237 "Should restart LSP server on a related LSP settings change"
17238 );
17239
17240 update_test_project_settings(cx, |project_settings| {
17241 project_settings.lsp.insert(
17242 language_server_name.into(),
17243 LspSettings {
17244 binary: None,
17245 settings: None,
17246 initialization_options: Some(json!({
17247 "anotherInitValue": false
17248 })),
17249 enable_lsp_tasks: false,
17250 fetch: None,
17251 },
17252 );
17253 });
17254 cx.executor().run_until_parked();
17255 assert_eq!(
17256 server_restarts.load(atomic::Ordering::Acquire),
17257 1,
17258 "Should not restart LSP server on a related LSP settings change that is the same"
17259 );
17260
17261 update_test_project_settings(cx, |project_settings| {
17262 project_settings.lsp.insert(
17263 language_server_name.into(),
17264 LspSettings {
17265 binary: None,
17266 settings: None,
17267 initialization_options: None,
17268 enable_lsp_tasks: false,
17269 fetch: None,
17270 },
17271 );
17272 });
17273 cx.executor().run_until_parked();
17274 assert_eq!(
17275 server_restarts.load(atomic::Ordering::Acquire),
17276 2,
17277 "Should restart LSP server on another related LSP settings change"
17278 );
17279}
17280
17281#[gpui::test]
17282async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
17283 init_test(cx, |_| {});
17284
17285 let mut cx = EditorLspTestContext::new_rust(
17286 lsp::ServerCapabilities {
17287 completion_provider: Some(lsp::CompletionOptions {
17288 trigger_characters: Some(vec![".".to_string()]),
17289 resolve_provider: Some(true),
17290 ..Default::default()
17291 }),
17292 ..Default::default()
17293 },
17294 cx,
17295 )
17296 .await;
17297
17298 cx.set_state("fn main() { let a = 2ˇ; }");
17299 cx.simulate_keystroke(".");
17300 let completion_item = lsp::CompletionItem {
17301 label: "some".into(),
17302 kind: Some(lsp::CompletionItemKind::SNIPPET),
17303 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
17304 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
17305 kind: lsp::MarkupKind::Markdown,
17306 value: "```rust\nSome(2)\n```".to_string(),
17307 })),
17308 deprecated: Some(false),
17309 sort_text: Some("fffffff2".to_string()),
17310 filter_text: Some("some".to_string()),
17311 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
17312 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17313 range: lsp::Range {
17314 start: lsp::Position {
17315 line: 0,
17316 character: 22,
17317 },
17318 end: lsp::Position {
17319 line: 0,
17320 character: 22,
17321 },
17322 },
17323 new_text: "Some(2)".to_string(),
17324 })),
17325 additional_text_edits: Some(vec![lsp::TextEdit {
17326 range: lsp::Range {
17327 start: lsp::Position {
17328 line: 0,
17329 character: 20,
17330 },
17331 end: lsp::Position {
17332 line: 0,
17333 character: 22,
17334 },
17335 },
17336 new_text: "".to_string(),
17337 }]),
17338 ..Default::default()
17339 };
17340
17341 let closure_completion_item = completion_item.clone();
17342 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17343 let task_completion_item = closure_completion_item.clone();
17344 async move {
17345 Ok(Some(lsp::CompletionResponse::Array(vec![
17346 task_completion_item,
17347 ])))
17348 }
17349 });
17350
17351 request.next().await;
17352
17353 cx.condition(|editor, _| editor.context_menu_visible())
17354 .await;
17355 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
17356 editor
17357 .confirm_completion(&ConfirmCompletion::default(), window, cx)
17358 .unwrap()
17359 });
17360 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
17361
17362 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
17363 let task_completion_item = completion_item.clone();
17364 async move { Ok(task_completion_item) }
17365 })
17366 .next()
17367 .await
17368 .unwrap();
17369 apply_additional_edits.await.unwrap();
17370 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
17371}
17372
17373#[gpui::test]
17374async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
17375 init_test(cx, |_| {});
17376
17377 let mut cx = EditorLspTestContext::new_rust(
17378 lsp::ServerCapabilities {
17379 completion_provider: Some(lsp::CompletionOptions {
17380 trigger_characters: Some(vec![".".to_string()]),
17381 resolve_provider: Some(true),
17382 ..Default::default()
17383 }),
17384 ..Default::default()
17385 },
17386 cx,
17387 )
17388 .await;
17389
17390 cx.set_state("fn main() { let a = 2ˇ; }");
17391 cx.simulate_keystroke(".");
17392
17393 let item1 = lsp::CompletionItem {
17394 label: "method id()".to_string(),
17395 filter_text: Some("id".to_string()),
17396 detail: None,
17397 documentation: None,
17398 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17399 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17400 new_text: ".id".to_string(),
17401 })),
17402 ..lsp::CompletionItem::default()
17403 };
17404
17405 let item2 = lsp::CompletionItem {
17406 label: "other".to_string(),
17407 filter_text: Some("other".to_string()),
17408 detail: None,
17409 documentation: None,
17410 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17411 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17412 new_text: ".other".to_string(),
17413 })),
17414 ..lsp::CompletionItem::default()
17415 };
17416
17417 let item1 = item1.clone();
17418 cx.set_request_handler::<lsp::request::Completion, _, _>({
17419 let item1 = item1.clone();
17420 move |_, _, _| {
17421 let item1 = item1.clone();
17422 let item2 = item2.clone();
17423 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
17424 }
17425 })
17426 .next()
17427 .await;
17428
17429 cx.condition(|editor, _| editor.context_menu_visible())
17430 .await;
17431 cx.update_editor(|editor, _, _| {
17432 let context_menu = editor.context_menu.borrow_mut();
17433 let context_menu = context_menu
17434 .as_ref()
17435 .expect("Should have the context menu deployed");
17436 match context_menu {
17437 CodeContextMenu::Completions(completions_menu) => {
17438 let completions = completions_menu.completions.borrow_mut();
17439 assert_eq!(
17440 completions
17441 .iter()
17442 .map(|completion| &completion.label.text)
17443 .collect::<Vec<_>>(),
17444 vec!["method id()", "other"]
17445 )
17446 }
17447 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17448 }
17449 });
17450
17451 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
17452 let item1 = item1.clone();
17453 move |_, item_to_resolve, _| {
17454 let item1 = item1.clone();
17455 async move {
17456 if item1 == item_to_resolve {
17457 Ok(lsp::CompletionItem {
17458 label: "method id()".to_string(),
17459 filter_text: Some("id".to_string()),
17460 detail: Some("Now resolved!".to_string()),
17461 documentation: Some(lsp::Documentation::String("Docs".to_string())),
17462 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17463 range: lsp::Range::new(
17464 lsp::Position::new(0, 22),
17465 lsp::Position::new(0, 22),
17466 ),
17467 new_text: ".id".to_string(),
17468 })),
17469 ..lsp::CompletionItem::default()
17470 })
17471 } else {
17472 Ok(item_to_resolve)
17473 }
17474 }
17475 }
17476 })
17477 .next()
17478 .await
17479 .unwrap();
17480 cx.run_until_parked();
17481
17482 cx.update_editor(|editor, window, cx| {
17483 editor.context_menu_next(&Default::default(), window, cx);
17484 });
17485
17486 cx.update_editor(|editor, _, _| {
17487 let context_menu = editor.context_menu.borrow_mut();
17488 let context_menu = context_menu
17489 .as_ref()
17490 .expect("Should have the context menu deployed");
17491 match context_menu {
17492 CodeContextMenu::Completions(completions_menu) => {
17493 let completions = completions_menu.completions.borrow_mut();
17494 assert_eq!(
17495 completions
17496 .iter()
17497 .map(|completion| &completion.label.text)
17498 .collect::<Vec<_>>(),
17499 vec!["method id() Now resolved!", "other"],
17500 "Should update first completion label, but not second as the filter text did not match."
17501 );
17502 }
17503 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17504 }
17505 });
17506}
17507
17508#[gpui::test]
17509async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
17510 init_test(cx, |_| {});
17511 let mut cx = EditorLspTestContext::new_rust(
17512 lsp::ServerCapabilities {
17513 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
17514 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
17515 completion_provider: Some(lsp::CompletionOptions {
17516 resolve_provider: Some(true),
17517 ..Default::default()
17518 }),
17519 ..Default::default()
17520 },
17521 cx,
17522 )
17523 .await;
17524 cx.set_state(indoc! {"
17525 struct TestStruct {
17526 field: i32
17527 }
17528
17529 fn mainˇ() {
17530 let unused_var = 42;
17531 let test_struct = TestStruct { field: 42 };
17532 }
17533 "});
17534 let symbol_range = cx.lsp_range(indoc! {"
17535 struct TestStruct {
17536 field: i32
17537 }
17538
17539 «fn main»() {
17540 let unused_var = 42;
17541 let test_struct = TestStruct { field: 42 };
17542 }
17543 "});
17544 let mut hover_requests =
17545 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
17546 Ok(Some(lsp::Hover {
17547 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
17548 kind: lsp::MarkupKind::Markdown,
17549 value: "Function documentation".to_string(),
17550 }),
17551 range: Some(symbol_range),
17552 }))
17553 });
17554
17555 // Case 1: Test that code action menu hide hover popover
17556 cx.dispatch_action(Hover);
17557 hover_requests.next().await;
17558 cx.condition(|editor, _| editor.hover_state.visible()).await;
17559 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
17560 move |_, _, _| async move {
17561 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
17562 lsp::CodeAction {
17563 title: "Remove unused variable".to_string(),
17564 kind: Some(CodeActionKind::QUICKFIX),
17565 edit: Some(lsp::WorkspaceEdit {
17566 changes: Some(
17567 [(
17568 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
17569 vec![lsp::TextEdit {
17570 range: lsp::Range::new(
17571 lsp::Position::new(5, 4),
17572 lsp::Position::new(5, 27),
17573 ),
17574 new_text: "".to_string(),
17575 }],
17576 )]
17577 .into_iter()
17578 .collect(),
17579 ),
17580 ..Default::default()
17581 }),
17582 ..Default::default()
17583 },
17584 )]))
17585 },
17586 );
17587 cx.update_editor(|editor, window, cx| {
17588 editor.toggle_code_actions(
17589 &ToggleCodeActions {
17590 deployed_from: None,
17591 quick_launch: false,
17592 },
17593 window,
17594 cx,
17595 );
17596 });
17597 code_action_requests.next().await;
17598 cx.run_until_parked();
17599 cx.condition(|editor, _| editor.context_menu_visible())
17600 .await;
17601 cx.update_editor(|editor, _, _| {
17602 assert!(
17603 !editor.hover_state.visible(),
17604 "Hover popover should be hidden when code action menu is shown"
17605 );
17606 // Hide code actions
17607 editor.context_menu.take();
17608 });
17609
17610 // Case 2: Test that code completions hide hover popover
17611 cx.dispatch_action(Hover);
17612 hover_requests.next().await;
17613 cx.condition(|editor, _| editor.hover_state.visible()).await;
17614 let counter = Arc::new(AtomicUsize::new(0));
17615 let mut completion_requests =
17616 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17617 let counter = counter.clone();
17618 async move {
17619 counter.fetch_add(1, atomic::Ordering::Release);
17620 Ok(Some(lsp::CompletionResponse::Array(vec![
17621 lsp::CompletionItem {
17622 label: "main".into(),
17623 kind: Some(lsp::CompletionItemKind::FUNCTION),
17624 detail: Some("() -> ()".to_string()),
17625 ..Default::default()
17626 },
17627 lsp::CompletionItem {
17628 label: "TestStruct".into(),
17629 kind: Some(lsp::CompletionItemKind::STRUCT),
17630 detail: Some("struct TestStruct".to_string()),
17631 ..Default::default()
17632 },
17633 ])))
17634 }
17635 });
17636 cx.update_editor(|editor, window, cx| {
17637 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
17638 });
17639 completion_requests.next().await;
17640 cx.condition(|editor, _| editor.context_menu_visible())
17641 .await;
17642 cx.update_editor(|editor, _, _| {
17643 assert!(
17644 !editor.hover_state.visible(),
17645 "Hover popover should be hidden when completion menu is shown"
17646 );
17647 });
17648}
17649
17650#[gpui::test]
17651async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
17652 init_test(cx, |_| {});
17653
17654 let mut cx = EditorLspTestContext::new_rust(
17655 lsp::ServerCapabilities {
17656 completion_provider: Some(lsp::CompletionOptions {
17657 trigger_characters: Some(vec![".".to_string()]),
17658 resolve_provider: Some(true),
17659 ..Default::default()
17660 }),
17661 ..Default::default()
17662 },
17663 cx,
17664 )
17665 .await;
17666
17667 cx.set_state("fn main() { let a = 2ˇ; }");
17668 cx.simulate_keystroke(".");
17669
17670 let unresolved_item_1 = lsp::CompletionItem {
17671 label: "id".to_string(),
17672 filter_text: Some("id".to_string()),
17673 detail: None,
17674 documentation: None,
17675 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17676 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17677 new_text: ".id".to_string(),
17678 })),
17679 ..lsp::CompletionItem::default()
17680 };
17681 let resolved_item_1 = lsp::CompletionItem {
17682 additional_text_edits: Some(vec![lsp::TextEdit {
17683 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
17684 new_text: "!!".to_string(),
17685 }]),
17686 ..unresolved_item_1.clone()
17687 };
17688 let unresolved_item_2 = lsp::CompletionItem {
17689 label: "other".to_string(),
17690 filter_text: Some("other".to_string()),
17691 detail: None,
17692 documentation: None,
17693 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17694 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17695 new_text: ".other".to_string(),
17696 })),
17697 ..lsp::CompletionItem::default()
17698 };
17699 let resolved_item_2 = lsp::CompletionItem {
17700 additional_text_edits: Some(vec![lsp::TextEdit {
17701 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
17702 new_text: "??".to_string(),
17703 }]),
17704 ..unresolved_item_2.clone()
17705 };
17706
17707 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
17708 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
17709 cx.lsp
17710 .server
17711 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
17712 let unresolved_item_1 = unresolved_item_1.clone();
17713 let resolved_item_1 = resolved_item_1.clone();
17714 let unresolved_item_2 = unresolved_item_2.clone();
17715 let resolved_item_2 = resolved_item_2.clone();
17716 let resolve_requests_1 = resolve_requests_1.clone();
17717 let resolve_requests_2 = resolve_requests_2.clone();
17718 move |unresolved_request, _| {
17719 let unresolved_item_1 = unresolved_item_1.clone();
17720 let resolved_item_1 = resolved_item_1.clone();
17721 let unresolved_item_2 = unresolved_item_2.clone();
17722 let resolved_item_2 = resolved_item_2.clone();
17723 let resolve_requests_1 = resolve_requests_1.clone();
17724 let resolve_requests_2 = resolve_requests_2.clone();
17725 async move {
17726 if unresolved_request == unresolved_item_1 {
17727 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
17728 Ok(resolved_item_1.clone())
17729 } else if unresolved_request == unresolved_item_2 {
17730 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
17731 Ok(resolved_item_2.clone())
17732 } else {
17733 panic!("Unexpected completion item {unresolved_request:?}")
17734 }
17735 }
17736 }
17737 })
17738 .detach();
17739
17740 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17741 let unresolved_item_1 = unresolved_item_1.clone();
17742 let unresolved_item_2 = unresolved_item_2.clone();
17743 async move {
17744 Ok(Some(lsp::CompletionResponse::Array(vec![
17745 unresolved_item_1,
17746 unresolved_item_2,
17747 ])))
17748 }
17749 })
17750 .next()
17751 .await;
17752
17753 cx.condition(|editor, _| editor.context_menu_visible())
17754 .await;
17755 cx.update_editor(|editor, _, _| {
17756 let context_menu = editor.context_menu.borrow_mut();
17757 let context_menu = context_menu
17758 .as_ref()
17759 .expect("Should have the context menu deployed");
17760 match context_menu {
17761 CodeContextMenu::Completions(completions_menu) => {
17762 let completions = completions_menu.completions.borrow_mut();
17763 assert_eq!(
17764 completions
17765 .iter()
17766 .map(|completion| &completion.label.text)
17767 .collect::<Vec<_>>(),
17768 vec!["id", "other"]
17769 )
17770 }
17771 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17772 }
17773 });
17774 cx.run_until_parked();
17775
17776 cx.update_editor(|editor, window, cx| {
17777 editor.context_menu_next(&ContextMenuNext, window, cx);
17778 });
17779 cx.run_until_parked();
17780 cx.update_editor(|editor, window, cx| {
17781 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
17782 });
17783 cx.run_until_parked();
17784 cx.update_editor(|editor, window, cx| {
17785 editor.context_menu_next(&ContextMenuNext, window, cx);
17786 });
17787 cx.run_until_parked();
17788 cx.update_editor(|editor, window, cx| {
17789 editor
17790 .compose_completion(&ComposeCompletion::default(), window, cx)
17791 .expect("No task returned")
17792 })
17793 .await
17794 .expect("Completion failed");
17795 cx.run_until_parked();
17796
17797 cx.update_editor(|editor, _, cx| {
17798 assert_eq!(
17799 resolve_requests_1.load(atomic::Ordering::Acquire),
17800 1,
17801 "Should always resolve once despite multiple selections"
17802 );
17803 assert_eq!(
17804 resolve_requests_2.load(atomic::Ordering::Acquire),
17805 1,
17806 "Should always resolve once after multiple selections and applying the completion"
17807 );
17808 assert_eq!(
17809 editor.text(cx),
17810 "fn main() { let a = ??.other; }",
17811 "Should use resolved data when applying the completion"
17812 );
17813 });
17814}
17815
17816#[gpui::test]
17817async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
17818 init_test(cx, |_| {});
17819
17820 let item_0 = lsp::CompletionItem {
17821 label: "abs".into(),
17822 insert_text: Some("abs".into()),
17823 data: Some(json!({ "very": "special"})),
17824 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
17825 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
17826 lsp::InsertReplaceEdit {
17827 new_text: "abs".to_string(),
17828 insert: lsp::Range::default(),
17829 replace: lsp::Range::default(),
17830 },
17831 )),
17832 ..lsp::CompletionItem::default()
17833 };
17834 let items = iter::once(item_0.clone())
17835 .chain((11..51).map(|i| lsp::CompletionItem {
17836 label: format!("item_{}", i),
17837 insert_text: Some(format!("item_{}", i)),
17838 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
17839 ..lsp::CompletionItem::default()
17840 }))
17841 .collect::<Vec<_>>();
17842
17843 let default_commit_characters = vec!["?".to_string()];
17844 let default_data = json!({ "default": "data"});
17845 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
17846 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
17847 let default_edit_range = lsp::Range {
17848 start: lsp::Position {
17849 line: 0,
17850 character: 5,
17851 },
17852 end: lsp::Position {
17853 line: 0,
17854 character: 5,
17855 },
17856 };
17857
17858 let mut cx = EditorLspTestContext::new_rust(
17859 lsp::ServerCapabilities {
17860 completion_provider: Some(lsp::CompletionOptions {
17861 trigger_characters: Some(vec![".".to_string()]),
17862 resolve_provider: Some(true),
17863 ..Default::default()
17864 }),
17865 ..Default::default()
17866 },
17867 cx,
17868 )
17869 .await;
17870
17871 cx.set_state("fn main() { let a = 2ˇ; }");
17872 cx.simulate_keystroke(".");
17873
17874 let completion_data = default_data.clone();
17875 let completion_characters = default_commit_characters.clone();
17876 let completion_items = items.clone();
17877 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17878 let default_data = completion_data.clone();
17879 let default_commit_characters = completion_characters.clone();
17880 let items = completion_items.clone();
17881 async move {
17882 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
17883 items,
17884 item_defaults: Some(lsp::CompletionListItemDefaults {
17885 data: Some(default_data.clone()),
17886 commit_characters: Some(default_commit_characters.clone()),
17887 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
17888 default_edit_range,
17889 )),
17890 insert_text_format: Some(default_insert_text_format),
17891 insert_text_mode: Some(default_insert_text_mode),
17892 }),
17893 ..lsp::CompletionList::default()
17894 })))
17895 }
17896 })
17897 .next()
17898 .await;
17899
17900 let resolved_items = Arc::new(Mutex::new(Vec::new()));
17901 cx.lsp
17902 .server
17903 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
17904 let closure_resolved_items = resolved_items.clone();
17905 move |item_to_resolve, _| {
17906 let closure_resolved_items = closure_resolved_items.clone();
17907 async move {
17908 closure_resolved_items.lock().push(item_to_resolve.clone());
17909 Ok(item_to_resolve)
17910 }
17911 }
17912 })
17913 .detach();
17914
17915 cx.condition(|editor, _| editor.context_menu_visible())
17916 .await;
17917 cx.run_until_parked();
17918 cx.update_editor(|editor, _, _| {
17919 let menu = editor.context_menu.borrow_mut();
17920 match menu.as_ref().expect("should have the completions menu") {
17921 CodeContextMenu::Completions(completions_menu) => {
17922 assert_eq!(
17923 completions_menu
17924 .entries
17925 .borrow()
17926 .iter()
17927 .map(|mat| mat.string.clone())
17928 .collect::<Vec<String>>(),
17929 items
17930 .iter()
17931 .map(|completion| completion.label.clone())
17932 .collect::<Vec<String>>()
17933 );
17934 }
17935 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
17936 }
17937 });
17938 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
17939 // with 4 from the end.
17940 assert_eq!(
17941 *resolved_items.lock(),
17942 [&items[0..16], &items[items.len() - 4..items.len()]]
17943 .concat()
17944 .iter()
17945 .cloned()
17946 .map(|mut item| {
17947 if item.data.is_none() {
17948 item.data = Some(default_data.clone());
17949 }
17950 item
17951 })
17952 .collect::<Vec<lsp::CompletionItem>>(),
17953 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
17954 );
17955 resolved_items.lock().clear();
17956
17957 cx.update_editor(|editor, window, cx| {
17958 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
17959 });
17960 cx.run_until_parked();
17961 // Completions that have already been resolved are skipped.
17962 assert_eq!(
17963 *resolved_items.lock(),
17964 items[items.len() - 17..items.len() - 4]
17965 .iter()
17966 .cloned()
17967 .map(|mut item| {
17968 if item.data.is_none() {
17969 item.data = Some(default_data.clone());
17970 }
17971 item
17972 })
17973 .collect::<Vec<lsp::CompletionItem>>()
17974 );
17975 resolved_items.lock().clear();
17976}
17977
17978#[gpui::test]
17979async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
17980 init_test(cx, |_| {});
17981
17982 let mut cx = EditorLspTestContext::new(
17983 Language::new(
17984 LanguageConfig {
17985 matcher: LanguageMatcher {
17986 path_suffixes: vec!["jsx".into()],
17987 ..Default::default()
17988 },
17989 overrides: [(
17990 "element".into(),
17991 LanguageConfigOverride {
17992 completion_query_characters: Override::Set(['-'].into_iter().collect()),
17993 ..Default::default()
17994 },
17995 )]
17996 .into_iter()
17997 .collect(),
17998 ..Default::default()
17999 },
18000 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
18001 )
18002 .with_override_query("(jsx_self_closing_element) @element")
18003 .unwrap(),
18004 lsp::ServerCapabilities {
18005 completion_provider: Some(lsp::CompletionOptions {
18006 trigger_characters: Some(vec![":".to_string()]),
18007 ..Default::default()
18008 }),
18009 ..Default::default()
18010 },
18011 cx,
18012 )
18013 .await;
18014
18015 cx.lsp
18016 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
18017 Ok(Some(lsp::CompletionResponse::Array(vec![
18018 lsp::CompletionItem {
18019 label: "bg-blue".into(),
18020 ..Default::default()
18021 },
18022 lsp::CompletionItem {
18023 label: "bg-red".into(),
18024 ..Default::default()
18025 },
18026 lsp::CompletionItem {
18027 label: "bg-yellow".into(),
18028 ..Default::default()
18029 },
18030 ])))
18031 });
18032
18033 cx.set_state(r#"<p class="bgˇ" />"#);
18034
18035 // Trigger completion when typing a dash, because the dash is an extra
18036 // word character in the 'element' scope, which contains the cursor.
18037 cx.simulate_keystroke("-");
18038 cx.executor().run_until_parked();
18039 cx.update_editor(|editor, _, _| {
18040 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18041 {
18042 assert_eq!(
18043 completion_menu_entries(menu),
18044 &["bg-blue", "bg-red", "bg-yellow"]
18045 );
18046 } else {
18047 panic!("expected completion menu to be open");
18048 }
18049 });
18050
18051 cx.simulate_keystroke("l");
18052 cx.executor().run_until_parked();
18053 cx.update_editor(|editor, _, _| {
18054 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18055 {
18056 assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]);
18057 } else {
18058 panic!("expected completion menu to be open");
18059 }
18060 });
18061
18062 // When filtering completions, consider the character after the '-' to
18063 // be the start of a subword.
18064 cx.set_state(r#"<p class="yelˇ" />"#);
18065 cx.simulate_keystroke("l");
18066 cx.executor().run_until_parked();
18067 cx.update_editor(|editor, _, _| {
18068 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18069 {
18070 assert_eq!(completion_menu_entries(menu), &["bg-yellow"]);
18071 } else {
18072 panic!("expected completion menu to be open");
18073 }
18074 });
18075}
18076
18077fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
18078 let entries = menu.entries.borrow();
18079 entries.iter().map(|mat| mat.string.clone()).collect()
18080}
18081
18082#[gpui::test]
18083async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
18084 init_test(cx, |settings| {
18085 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
18086 Formatter::Prettier,
18087 )))
18088 });
18089
18090 let fs = FakeFs::new(cx.executor());
18091 fs.insert_file(path!("/file.ts"), Default::default()).await;
18092
18093 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
18094 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18095
18096 language_registry.add(Arc::new(Language::new(
18097 LanguageConfig {
18098 name: "TypeScript".into(),
18099 matcher: LanguageMatcher {
18100 path_suffixes: vec!["ts".to_string()],
18101 ..Default::default()
18102 },
18103 ..Default::default()
18104 },
18105 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
18106 )));
18107 update_test_language_settings(cx, |settings| {
18108 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
18109 });
18110
18111 let test_plugin = "test_plugin";
18112 let _ = language_registry.register_fake_lsp(
18113 "TypeScript",
18114 FakeLspAdapter {
18115 prettier_plugins: vec![test_plugin],
18116 ..Default::default()
18117 },
18118 );
18119
18120 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
18121 let buffer = project
18122 .update(cx, |project, cx| {
18123 project.open_local_buffer(path!("/file.ts"), cx)
18124 })
18125 .await
18126 .unwrap();
18127
18128 let buffer_text = "one\ntwo\nthree\n";
18129 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
18130 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
18131 editor.update_in(cx, |editor, window, cx| {
18132 editor.set_text(buffer_text, window, cx)
18133 });
18134
18135 editor
18136 .update_in(cx, |editor, window, cx| {
18137 editor.perform_format(
18138 project.clone(),
18139 FormatTrigger::Manual,
18140 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18141 window,
18142 cx,
18143 )
18144 })
18145 .unwrap()
18146 .await;
18147 assert_eq!(
18148 editor.update(cx, |editor, cx| editor.text(cx)),
18149 buffer_text.to_string() + prettier_format_suffix,
18150 "Test prettier formatting was not applied to the original buffer text",
18151 );
18152
18153 update_test_language_settings(cx, |settings| {
18154 settings.defaults.formatter = Some(SelectedFormatter::Auto)
18155 });
18156 let format = editor.update_in(cx, |editor, window, cx| {
18157 editor.perform_format(
18158 project.clone(),
18159 FormatTrigger::Manual,
18160 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18161 window,
18162 cx,
18163 )
18164 });
18165 format.await.unwrap();
18166 assert_eq!(
18167 editor.update(cx, |editor, cx| editor.text(cx)),
18168 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
18169 "Autoformatting (via test prettier) was not applied to the original buffer text",
18170 );
18171}
18172
18173#[gpui::test]
18174async fn test_addition_reverts(cx: &mut TestAppContext) {
18175 init_test(cx, |_| {});
18176 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18177 let base_text = indoc! {r#"
18178 struct Row;
18179 struct Row1;
18180 struct Row2;
18181
18182 struct Row4;
18183 struct Row5;
18184 struct Row6;
18185
18186 struct Row8;
18187 struct Row9;
18188 struct Row10;"#};
18189
18190 // When addition hunks are not adjacent to carets, no hunk revert is performed
18191 assert_hunk_revert(
18192 indoc! {r#"struct Row;
18193 struct Row1;
18194 struct Row1.1;
18195 struct Row1.2;
18196 struct Row2;ˇ
18197
18198 struct Row4;
18199 struct Row5;
18200 struct Row6;
18201
18202 struct Row8;
18203 ˇstruct Row9;
18204 struct Row9.1;
18205 struct Row9.2;
18206 struct Row9.3;
18207 struct Row10;"#},
18208 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18209 indoc! {r#"struct Row;
18210 struct Row1;
18211 struct Row1.1;
18212 struct Row1.2;
18213 struct Row2;ˇ
18214
18215 struct Row4;
18216 struct Row5;
18217 struct Row6;
18218
18219 struct Row8;
18220 ˇstruct Row9;
18221 struct Row9.1;
18222 struct Row9.2;
18223 struct Row9.3;
18224 struct Row10;"#},
18225 base_text,
18226 &mut cx,
18227 );
18228 // Same for selections
18229 assert_hunk_revert(
18230 indoc! {r#"struct Row;
18231 struct Row1;
18232 struct Row2;
18233 struct Row2.1;
18234 struct Row2.2;
18235 «ˇ
18236 struct Row4;
18237 struct» Row5;
18238 «struct Row6;
18239 ˇ»
18240 struct Row9.1;
18241 struct Row9.2;
18242 struct Row9.3;
18243 struct Row8;
18244 struct Row9;
18245 struct Row10;"#},
18246 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18247 indoc! {r#"struct Row;
18248 struct Row1;
18249 struct Row2;
18250 struct Row2.1;
18251 struct Row2.2;
18252 «ˇ
18253 struct Row4;
18254 struct» Row5;
18255 «struct Row6;
18256 ˇ»
18257 struct Row9.1;
18258 struct Row9.2;
18259 struct Row9.3;
18260 struct Row8;
18261 struct Row9;
18262 struct Row10;"#},
18263 base_text,
18264 &mut cx,
18265 );
18266
18267 // When carets and selections intersect the addition hunks, those are reverted.
18268 // Adjacent carets got merged.
18269 assert_hunk_revert(
18270 indoc! {r#"struct Row;
18271 ˇ// something on the top
18272 struct Row1;
18273 struct Row2;
18274 struct Roˇw3.1;
18275 struct Row2.2;
18276 struct Row2.3;ˇ
18277
18278 struct Row4;
18279 struct ˇRow5.1;
18280 struct Row5.2;
18281 struct «Rowˇ»5.3;
18282 struct Row5;
18283 struct Row6;
18284 ˇ
18285 struct Row9.1;
18286 struct «Rowˇ»9.2;
18287 struct «ˇRow»9.3;
18288 struct Row8;
18289 struct Row9;
18290 «ˇ// something on bottom»
18291 struct Row10;"#},
18292 vec![
18293 DiffHunkStatusKind::Added,
18294 DiffHunkStatusKind::Added,
18295 DiffHunkStatusKind::Added,
18296 DiffHunkStatusKind::Added,
18297 DiffHunkStatusKind::Added,
18298 ],
18299 indoc! {r#"struct Row;
18300 ˇstruct Row1;
18301 struct Row2;
18302 ˇ
18303 struct Row4;
18304 ˇstruct Row5;
18305 struct Row6;
18306 ˇ
18307 ˇstruct Row8;
18308 struct Row9;
18309 ˇstruct Row10;"#},
18310 base_text,
18311 &mut cx,
18312 );
18313}
18314
18315#[gpui::test]
18316async fn test_modification_reverts(cx: &mut TestAppContext) {
18317 init_test(cx, |_| {});
18318 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18319 let base_text = indoc! {r#"
18320 struct Row;
18321 struct Row1;
18322 struct Row2;
18323
18324 struct Row4;
18325 struct Row5;
18326 struct Row6;
18327
18328 struct Row8;
18329 struct Row9;
18330 struct Row10;"#};
18331
18332 // Modification hunks behave the same as the addition ones.
18333 assert_hunk_revert(
18334 indoc! {r#"struct Row;
18335 struct Row1;
18336 struct Row33;
18337 ˇ
18338 struct Row4;
18339 struct Row5;
18340 struct Row6;
18341 ˇ
18342 struct Row99;
18343 struct Row9;
18344 struct Row10;"#},
18345 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18346 indoc! {r#"struct Row;
18347 struct Row1;
18348 struct Row33;
18349 ˇ
18350 struct Row4;
18351 struct Row5;
18352 struct Row6;
18353 ˇ
18354 struct Row99;
18355 struct Row9;
18356 struct Row10;"#},
18357 base_text,
18358 &mut cx,
18359 );
18360 assert_hunk_revert(
18361 indoc! {r#"struct Row;
18362 struct Row1;
18363 struct Row33;
18364 «ˇ
18365 struct Row4;
18366 struct» Row5;
18367 «struct Row6;
18368 ˇ»
18369 struct Row99;
18370 struct Row9;
18371 struct Row10;"#},
18372 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18373 indoc! {r#"struct Row;
18374 struct Row1;
18375 struct Row33;
18376 «ˇ
18377 struct Row4;
18378 struct» Row5;
18379 «struct Row6;
18380 ˇ»
18381 struct Row99;
18382 struct Row9;
18383 struct Row10;"#},
18384 base_text,
18385 &mut cx,
18386 );
18387
18388 assert_hunk_revert(
18389 indoc! {r#"ˇstruct Row1.1;
18390 struct Row1;
18391 «ˇstr»uct Row22;
18392
18393 struct ˇRow44;
18394 struct Row5;
18395 struct «Rˇ»ow66;ˇ
18396
18397 «struˇ»ct Row88;
18398 struct Row9;
18399 struct Row1011;ˇ"#},
18400 vec![
18401 DiffHunkStatusKind::Modified,
18402 DiffHunkStatusKind::Modified,
18403 DiffHunkStatusKind::Modified,
18404 DiffHunkStatusKind::Modified,
18405 DiffHunkStatusKind::Modified,
18406 DiffHunkStatusKind::Modified,
18407 ],
18408 indoc! {r#"struct Row;
18409 ˇstruct Row1;
18410 struct Row2;
18411 ˇ
18412 struct Row4;
18413 ˇstruct Row5;
18414 struct Row6;
18415 ˇ
18416 struct Row8;
18417 ˇstruct Row9;
18418 struct Row10;ˇ"#},
18419 base_text,
18420 &mut cx,
18421 );
18422}
18423
18424#[gpui::test]
18425async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
18426 init_test(cx, |_| {});
18427 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18428 let base_text = indoc! {r#"
18429 one
18430
18431 two
18432 three
18433 "#};
18434
18435 cx.set_head_text(base_text);
18436 cx.set_state("\nˇ\n");
18437 cx.executor().run_until_parked();
18438 cx.update_editor(|editor, _window, cx| {
18439 editor.expand_selected_diff_hunks(cx);
18440 });
18441 cx.executor().run_until_parked();
18442 cx.update_editor(|editor, window, cx| {
18443 editor.backspace(&Default::default(), window, cx);
18444 });
18445 cx.run_until_parked();
18446 cx.assert_state_with_diff(
18447 indoc! {r#"
18448
18449 - two
18450 - threeˇ
18451 +
18452 "#}
18453 .to_string(),
18454 );
18455}
18456
18457#[gpui::test]
18458async fn test_deletion_reverts(cx: &mut TestAppContext) {
18459 init_test(cx, |_| {});
18460 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18461 let base_text = indoc! {r#"struct Row;
18462struct Row1;
18463struct Row2;
18464
18465struct Row4;
18466struct Row5;
18467struct Row6;
18468
18469struct Row8;
18470struct Row9;
18471struct Row10;"#};
18472
18473 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
18474 assert_hunk_revert(
18475 indoc! {r#"struct Row;
18476 struct Row2;
18477
18478 ˇstruct Row4;
18479 struct Row5;
18480 struct Row6;
18481 ˇ
18482 struct Row8;
18483 struct Row10;"#},
18484 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18485 indoc! {r#"struct Row;
18486 struct Row2;
18487
18488 ˇstruct Row4;
18489 struct Row5;
18490 struct Row6;
18491 ˇ
18492 struct Row8;
18493 struct Row10;"#},
18494 base_text,
18495 &mut cx,
18496 );
18497 assert_hunk_revert(
18498 indoc! {r#"struct Row;
18499 struct Row2;
18500
18501 «ˇstruct Row4;
18502 struct» Row5;
18503 «struct Row6;
18504 ˇ»
18505 struct Row8;
18506 struct Row10;"#},
18507 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18508 indoc! {r#"struct Row;
18509 struct Row2;
18510
18511 «ˇstruct Row4;
18512 struct» Row5;
18513 «struct Row6;
18514 ˇ»
18515 struct Row8;
18516 struct Row10;"#},
18517 base_text,
18518 &mut cx,
18519 );
18520
18521 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
18522 assert_hunk_revert(
18523 indoc! {r#"struct Row;
18524 ˇstruct Row2;
18525
18526 struct Row4;
18527 struct Row5;
18528 struct Row6;
18529
18530 struct Row8;ˇ
18531 struct Row10;"#},
18532 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18533 indoc! {r#"struct Row;
18534 struct Row1;
18535 ˇstruct Row2;
18536
18537 struct Row4;
18538 struct Row5;
18539 struct Row6;
18540
18541 struct Row8;ˇ
18542 struct Row9;
18543 struct Row10;"#},
18544 base_text,
18545 &mut cx,
18546 );
18547 assert_hunk_revert(
18548 indoc! {r#"struct Row;
18549 struct Row2«ˇ;
18550 struct Row4;
18551 struct» Row5;
18552 «struct Row6;
18553
18554 struct Row8;ˇ»
18555 struct Row10;"#},
18556 vec![
18557 DiffHunkStatusKind::Deleted,
18558 DiffHunkStatusKind::Deleted,
18559 DiffHunkStatusKind::Deleted,
18560 ],
18561 indoc! {r#"struct Row;
18562 struct Row1;
18563 struct Row2«ˇ;
18564
18565 struct Row4;
18566 struct» Row5;
18567 «struct Row6;
18568
18569 struct Row8;ˇ»
18570 struct Row9;
18571 struct Row10;"#},
18572 base_text,
18573 &mut cx,
18574 );
18575}
18576
18577#[gpui::test]
18578async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
18579 init_test(cx, |_| {});
18580
18581 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
18582 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
18583 let base_text_3 =
18584 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
18585
18586 let text_1 = edit_first_char_of_every_line(base_text_1);
18587 let text_2 = edit_first_char_of_every_line(base_text_2);
18588 let text_3 = edit_first_char_of_every_line(base_text_3);
18589
18590 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
18591 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
18592 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
18593
18594 let multibuffer = cx.new(|cx| {
18595 let mut multibuffer = MultiBuffer::new(ReadWrite);
18596 multibuffer.push_excerpts(
18597 buffer_1.clone(),
18598 [
18599 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18600 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18601 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18602 ],
18603 cx,
18604 );
18605 multibuffer.push_excerpts(
18606 buffer_2.clone(),
18607 [
18608 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18609 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18610 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18611 ],
18612 cx,
18613 );
18614 multibuffer.push_excerpts(
18615 buffer_3.clone(),
18616 [
18617 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18618 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18619 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18620 ],
18621 cx,
18622 );
18623 multibuffer
18624 });
18625
18626 let fs = FakeFs::new(cx.executor());
18627 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
18628 let (editor, cx) = cx
18629 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
18630 editor.update_in(cx, |editor, _window, cx| {
18631 for (buffer, diff_base) in [
18632 (buffer_1.clone(), base_text_1),
18633 (buffer_2.clone(), base_text_2),
18634 (buffer_3.clone(), base_text_3),
18635 ] {
18636 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
18637 editor
18638 .buffer
18639 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
18640 }
18641 });
18642 cx.executor().run_until_parked();
18643
18644 editor.update_in(cx, |editor, window, cx| {
18645 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}");
18646 editor.select_all(&SelectAll, window, cx);
18647 editor.git_restore(&Default::default(), window, cx);
18648 });
18649 cx.executor().run_until_parked();
18650
18651 // When all ranges are selected, all buffer hunks are reverted.
18652 editor.update(cx, |editor, cx| {
18653 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");
18654 });
18655 buffer_1.update(cx, |buffer, _| {
18656 assert_eq!(buffer.text(), base_text_1);
18657 });
18658 buffer_2.update(cx, |buffer, _| {
18659 assert_eq!(buffer.text(), base_text_2);
18660 });
18661 buffer_3.update(cx, |buffer, _| {
18662 assert_eq!(buffer.text(), base_text_3);
18663 });
18664
18665 editor.update_in(cx, |editor, window, cx| {
18666 editor.undo(&Default::default(), window, cx);
18667 });
18668
18669 editor.update_in(cx, |editor, window, cx| {
18670 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18671 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
18672 });
18673 editor.git_restore(&Default::default(), window, cx);
18674 });
18675
18676 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
18677 // but not affect buffer_2 and its related excerpts.
18678 editor.update(cx, |editor, cx| {
18679 assert_eq!(
18680 editor.text(cx),
18681 "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}"
18682 );
18683 });
18684 buffer_1.update(cx, |buffer, _| {
18685 assert_eq!(buffer.text(), base_text_1);
18686 });
18687 buffer_2.update(cx, |buffer, _| {
18688 assert_eq!(
18689 buffer.text(),
18690 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
18691 );
18692 });
18693 buffer_3.update(cx, |buffer, _| {
18694 assert_eq!(
18695 buffer.text(),
18696 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
18697 );
18698 });
18699
18700 fn edit_first_char_of_every_line(text: &str) -> String {
18701 text.split('\n')
18702 .map(|line| format!("X{}", &line[1..]))
18703 .collect::<Vec<_>>()
18704 .join("\n")
18705 }
18706}
18707
18708#[gpui::test]
18709async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
18710 init_test(cx, |_| {});
18711
18712 let cols = 4;
18713 let rows = 10;
18714 let sample_text_1 = sample_text(rows, cols, 'a');
18715 assert_eq!(
18716 sample_text_1,
18717 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
18718 );
18719 let sample_text_2 = sample_text(rows, cols, 'l');
18720 assert_eq!(
18721 sample_text_2,
18722 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
18723 );
18724 let sample_text_3 = sample_text(rows, cols, 'v');
18725 assert_eq!(
18726 sample_text_3,
18727 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
18728 );
18729
18730 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
18731 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
18732 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
18733
18734 let multi_buffer = cx.new(|cx| {
18735 let mut multibuffer = MultiBuffer::new(ReadWrite);
18736 multibuffer.push_excerpts(
18737 buffer_1.clone(),
18738 [
18739 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18740 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18741 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18742 ],
18743 cx,
18744 );
18745 multibuffer.push_excerpts(
18746 buffer_2.clone(),
18747 [
18748 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18749 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18750 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18751 ],
18752 cx,
18753 );
18754 multibuffer.push_excerpts(
18755 buffer_3.clone(),
18756 [
18757 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18758 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18759 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18760 ],
18761 cx,
18762 );
18763 multibuffer
18764 });
18765
18766 let fs = FakeFs::new(cx.executor());
18767 fs.insert_tree(
18768 "/a",
18769 json!({
18770 "main.rs": sample_text_1,
18771 "other.rs": sample_text_2,
18772 "lib.rs": sample_text_3,
18773 }),
18774 )
18775 .await;
18776 let project = Project::test(fs, ["/a".as_ref()], cx).await;
18777 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18778 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18779 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18780 Editor::new(
18781 EditorMode::full(),
18782 multi_buffer,
18783 Some(project.clone()),
18784 window,
18785 cx,
18786 )
18787 });
18788 let multibuffer_item_id = workspace
18789 .update(cx, |workspace, window, cx| {
18790 assert!(
18791 workspace.active_item(cx).is_none(),
18792 "active item should be None before the first item is added"
18793 );
18794 workspace.add_item_to_active_pane(
18795 Box::new(multi_buffer_editor.clone()),
18796 None,
18797 true,
18798 window,
18799 cx,
18800 );
18801 let active_item = workspace
18802 .active_item(cx)
18803 .expect("should have an active item after adding the multi buffer");
18804 assert_eq!(
18805 active_item.buffer_kind(cx),
18806 ItemBufferKind::Multibuffer,
18807 "A multi buffer was expected to active after adding"
18808 );
18809 active_item.item_id()
18810 })
18811 .unwrap();
18812 cx.executor().run_until_parked();
18813
18814 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18815 editor.change_selections(
18816 SelectionEffects::scroll(Autoscroll::Next),
18817 window,
18818 cx,
18819 |s| s.select_ranges(Some(1..2)),
18820 );
18821 editor.open_excerpts(&OpenExcerpts, window, cx);
18822 });
18823 cx.executor().run_until_parked();
18824 let first_item_id = workspace
18825 .update(cx, |workspace, window, cx| {
18826 let active_item = workspace
18827 .active_item(cx)
18828 .expect("should have an active item after navigating into the 1st buffer");
18829 let first_item_id = active_item.item_id();
18830 assert_ne!(
18831 first_item_id, multibuffer_item_id,
18832 "Should navigate into the 1st buffer and activate it"
18833 );
18834 assert_eq!(
18835 active_item.buffer_kind(cx),
18836 ItemBufferKind::Singleton,
18837 "New active item should be a singleton buffer"
18838 );
18839 assert_eq!(
18840 active_item
18841 .act_as::<Editor>(cx)
18842 .expect("should have navigated into an editor for the 1st buffer")
18843 .read(cx)
18844 .text(cx),
18845 sample_text_1
18846 );
18847
18848 workspace
18849 .go_back(workspace.active_pane().downgrade(), window, cx)
18850 .detach_and_log_err(cx);
18851
18852 first_item_id
18853 })
18854 .unwrap();
18855 cx.executor().run_until_parked();
18856 workspace
18857 .update(cx, |workspace, _, cx| {
18858 let active_item = workspace
18859 .active_item(cx)
18860 .expect("should have an active item after navigating back");
18861 assert_eq!(
18862 active_item.item_id(),
18863 multibuffer_item_id,
18864 "Should navigate back to the multi buffer"
18865 );
18866 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
18867 })
18868 .unwrap();
18869
18870 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18871 editor.change_selections(
18872 SelectionEffects::scroll(Autoscroll::Next),
18873 window,
18874 cx,
18875 |s| s.select_ranges(Some(39..40)),
18876 );
18877 editor.open_excerpts(&OpenExcerpts, window, cx);
18878 });
18879 cx.executor().run_until_parked();
18880 let second_item_id = workspace
18881 .update(cx, |workspace, window, cx| {
18882 let active_item = workspace
18883 .active_item(cx)
18884 .expect("should have an active item after navigating into the 2nd buffer");
18885 let second_item_id = active_item.item_id();
18886 assert_ne!(
18887 second_item_id, multibuffer_item_id,
18888 "Should navigate away from the multibuffer"
18889 );
18890 assert_ne!(
18891 second_item_id, first_item_id,
18892 "Should navigate into the 2nd buffer and activate it"
18893 );
18894 assert_eq!(
18895 active_item.buffer_kind(cx),
18896 ItemBufferKind::Singleton,
18897 "New active item should be a singleton buffer"
18898 );
18899 assert_eq!(
18900 active_item
18901 .act_as::<Editor>(cx)
18902 .expect("should have navigated into an editor")
18903 .read(cx)
18904 .text(cx),
18905 sample_text_2
18906 );
18907
18908 workspace
18909 .go_back(workspace.active_pane().downgrade(), window, cx)
18910 .detach_and_log_err(cx);
18911
18912 second_item_id
18913 })
18914 .unwrap();
18915 cx.executor().run_until_parked();
18916 workspace
18917 .update(cx, |workspace, _, cx| {
18918 let active_item = workspace
18919 .active_item(cx)
18920 .expect("should have an active item after navigating back from the 2nd buffer");
18921 assert_eq!(
18922 active_item.item_id(),
18923 multibuffer_item_id,
18924 "Should navigate back from the 2nd buffer to the multi buffer"
18925 );
18926 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
18927 })
18928 .unwrap();
18929
18930 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18931 editor.change_selections(
18932 SelectionEffects::scroll(Autoscroll::Next),
18933 window,
18934 cx,
18935 |s| s.select_ranges(Some(70..70)),
18936 );
18937 editor.open_excerpts(&OpenExcerpts, window, cx);
18938 });
18939 cx.executor().run_until_parked();
18940 workspace
18941 .update(cx, |workspace, window, cx| {
18942 let active_item = workspace
18943 .active_item(cx)
18944 .expect("should have an active item after navigating into the 3rd buffer");
18945 let third_item_id = active_item.item_id();
18946 assert_ne!(
18947 third_item_id, multibuffer_item_id,
18948 "Should navigate into the 3rd buffer and activate it"
18949 );
18950 assert_ne!(third_item_id, first_item_id);
18951 assert_ne!(third_item_id, second_item_id);
18952 assert_eq!(
18953 active_item.buffer_kind(cx),
18954 ItemBufferKind::Singleton,
18955 "New active item should be a singleton buffer"
18956 );
18957 assert_eq!(
18958 active_item
18959 .act_as::<Editor>(cx)
18960 .expect("should have navigated into an editor")
18961 .read(cx)
18962 .text(cx),
18963 sample_text_3
18964 );
18965
18966 workspace
18967 .go_back(workspace.active_pane().downgrade(), window, cx)
18968 .detach_and_log_err(cx);
18969 })
18970 .unwrap();
18971 cx.executor().run_until_parked();
18972 workspace
18973 .update(cx, |workspace, _, cx| {
18974 let active_item = workspace
18975 .active_item(cx)
18976 .expect("should have an active item after navigating back from the 3rd buffer");
18977 assert_eq!(
18978 active_item.item_id(),
18979 multibuffer_item_id,
18980 "Should navigate back from the 3rd buffer to the multi buffer"
18981 );
18982 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
18983 })
18984 .unwrap();
18985}
18986
18987#[gpui::test]
18988async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
18989 init_test(cx, |_| {});
18990
18991 let mut cx = EditorTestContext::new(cx).await;
18992
18993 let diff_base = r#"
18994 use some::mod;
18995
18996 const A: u32 = 42;
18997
18998 fn main() {
18999 println!("hello");
19000
19001 println!("world");
19002 }
19003 "#
19004 .unindent();
19005
19006 cx.set_state(
19007 &r#"
19008 use some::modified;
19009
19010 ˇ
19011 fn main() {
19012 println!("hello there");
19013
19014 println!("around the");
19015 println!("world");
19016 }
19017 "#
19018 .unindent(),
19019 );
19020
19021 cx.set_head_text(&diff_base);
19022 executor.run_until_parked();
19023
19024 cx.update_editor(|editor, window, cx| {
19025 editor.go_to_next_hunk(&GoToHunk, window, cx);
19026 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19027 });
19028 executor.run_until_parked();
19029 cx.assert_state_with_diff(
19030 r#"
19031 use some::modified;
19032
19033
19034 fn main() {
19035 - println!("hello");
19036 + ˇ println!("hello there");
19037
19038 println!("around the");
19039 println!("world");
19040 }
19041 "#
19042 .unindent(),
19043 );
19044
19045 cx.update_editor(|editor, window, cx| {
19046 for _ in 0..2 {
19047 editor.go_to_next_hunk(&GoToHunk, window, cx);
19048 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19049 }
19050 });
19051 executor.run_until_parked();
19052 cx.assert_state_with_diff(
19053 r#"
19054 - use some::mod;
19055 + ˇuse some::modified;
19056
19057
19058 fn main() {
19059 - println!("hello");
19060 + println!("hello there");
19061
19062 + println!("around the");
19063 println!("world");
19064 }
19065 "#
19066 .unindent(),
19067 );
19068
19069 cx.update_editor(|editor, window, cx| {
19070 editor.go_to_next_hunk(&GoToHunk, window, cx);
19071 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19072 });
19073 executor.run_until_parked();
19074 cx.assert_state_with_diff(
19075 r#"
19076 - use some::mod;
19077 + use some::modified;
19078
19079 - const A: u32 = 42;
19080 ˇ
19081 fn main() {
19082 - println!("hello");
19083 + println!("hello there");
19084
19085 + println!("around the");
19086 println!("world");
19087 }
19088 "#
19089 .unindent(),
19090 );
19091
19092 cx.update_editor(|editor, window, cx| {
19093 editor.cancel(&Cancel, window, cx);
19094 });
19095
19096 cx.assert_state_with_diff(
19097 r#"
19098 use some::modified;
19099
19100 ˇ
19101 fn main() {
19102 println!("hello there");
19103
19104 println!("around the");
19105 println!("world");
19106 }
19107 "#
19108 .unindent(),
19109 );
19110}
19111
19112#[gpui::test]
19113async fn test_diff_base_change_with_expanded_diff_hunks(
19114 executor: BackgroundExecutor,
19115 cx: &mut TestAppContext,
19116) {
19117 init_test(cx, |_| {});
19118
19119 let mut cx = EditorTestContext::new(cx).await;
19120
19121 let diff_base = r#"
19122 use some::mod1;
19123 use some::mod2;
19124
19125 const A: u32 = 42;
19126 const B: u32 = 42;
19127 const C: u32 = 42;
19128
19129 fn main() {
19130 println!("hello");
19131
19132 println!("world");
19133 }
19134 "#
19135 .unindent();
19136
19137 cx.set_state(
19138 &r#"
19139 use some::mod2;
19140
19141 const A: u32 = 42;
19142 const C: u32 = 42;
19143
19144 fn main(ˇ) {
19145 //println!("hello");
19146
19147 println!("world");
19148 //
19149 //
19150 }
19151 "#
19152 .unindent(),
19153 );
19154
19155 cx.set_head_text(&diff_base);
19156 executor.run_until_parked();
19157
19158 cx.update_editor(|editor, window, cx| {
19159 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19160 });
19161 executor.run_until_parked();
19162 cx.assert_state_with_diff(
19163 r#"
19164 - use some::mod1;
19165 use some::mod2;
19166
19167 const A: u32 = 42;
19168 - const B: u32 = 42;
19169 const C: u32 = 42;
19170
19171 fn main(ˇ) {
19172 - println!("hello");
19173 + //println!("hello");
19174
19175 println!("world");
19176 + //
19177 + //
19178 }
19179 "#
19180 .unindent(),
19181 );
19182
19183 cx.set_head_text("new diff base!");
19184 executor.run_until_parked();
19185 cx.assert_state_with_diff(
19186 r#"
19187 - new diff base!
19188 + use some::mod2;
19189 +
19190 + const A: u32 = 42;
19191 + const C: u32 = 42;
19192 +
19193 + fn main(ˇ) {
19194 + //println!("hello");
19195 +
19196 + println!("world");
19197 + //
19198 + //
19199 + }
19200 "#
19201 .unindent(),
19202 );
19203}
19204
19205#[gpui::test]
19206async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
19207 init_test(cx, |_| {});
19208
19209 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19210 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19211 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19212 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19213 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
19214 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
19215
19216 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
19217 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
19218 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
19219
19220 let multi_buffer = cx.new(|cx| {
19221 let mut multibuffer = MultiBuffer::new(ReadWrite);
19222 multibuffer.push_excerpts(
19223 buffer_1.clone(),
19224 [
19225 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19226 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19227 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19228 ],
19229 cx,
19230 );
19231 multibuffer.push_excerpts(
19232 buffer_2.clone(),
19233 [
19234 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19235 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19236 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19237 ],
19238 cx,
19239 );
19240 multibuffer.push_excerpts(
19241 buffer_3.clone(),
19242 [
19243 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19244 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19245 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19246 ],
19247 cx,
19248 );
19249 multibuffer
19250 });
19251
19252 let editor =
19253 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19254 editor
19255 .update(cx, |editor, _window, cx| {
19256 for (buffer, diff_base) in [
19257 (buffer_1.clone(), file_1_old),
19258 (buffer_2.clone(), file_2_old),
19259 (buffer_3.clone(), file_3_old),
19260 ] {
19261 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
19262 editor
19263 .buffer
19264 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
19265 }
19266 })
19267 .unwrap();
19268
19269 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19270 cx.run_until_parked();
19271
19272 cx.assert_editor_state(
19273 &"
19274 ˇaaa
19275 ccc
19276 ddd
19277
19278 ggg
19279 hhh
19280
19281
19282 lll
19283 mmm
19284 NNN
19285
19286 qqq
19287 rrr
19288
19289 uuu
19290 111
19291 222
19292 333
19293
19294 666
19295 777
19296
19297 000
19298 !!!"
19299 .unindent(),
19300 );
19301
19302 cx.update_editor(|editor, window, cx| {
19303 editor.select_all(&SelectAll, window, cx);
19304 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19305 });
19306 cx.executor().run_until_parked();
19307
19308 cx.assert_state_with_diff(
19309 "
19310 «aaa
19311 - bbb
19312 ccc
19313 ddd
19314
19315 ggg
19316 hhh
19317
19318
19319 lll
19320 mmm
19321 - nnn
19322 + NNN
19323
19324 qqq
19325 rrr
19326
19327 uuu
19328 111
19329 222
19330 333
19331
19332 + 666
19333 777
19334
19335 000
19336 !!!ˇ»"
19337 .unindent(),
19338 );
19339}
19340
19341#[gpui::test]
19342async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
19343 init_test(cx, |_| {});
19344
19345 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
19346 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
19347
19348 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
19349 let multi_buffer = cx.new(|cx| {
19350 let mut multibuffer = MultiBuffer::new(ReadWrite);
19351 multibuffer.push_excerpts(
19352 buffer.clone(),
19353 [
19354 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
19355 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
19356 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
19357 ],
19358 cx,
19359 );
19360 multibuffer
19361 });
19362
19363 let editor =
19364 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19365 editor
19366 .update(cx, |editor, _window, cx| {
19367 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
19368 editor
19369 .buffer
19370 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
19371 })
19372 .unwrap();
19373
19374 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19375 cx.run_until_parked();
19376
19377 cx.update_editor(|editor, window, cx| {
19378 editor.expand_all_diff_hunks(&Default::default(), window, cx)
19379 });
19380 cx.executor().run_until_parked();
19381
19382 // When the start of a hunk coincides with the start of its excerpt,
19383 // the hunk is expanded. When the start of a hunk is earlier than
19384 // the start of its excerpt, the hunk is not expanded.
19385 cx.assert_state_with_diff(
19386 "
19387 ˇaaa
19388 - bbb
19389 + BBB
19390
19391 - ddd
19392 - eee
19393 + DDD
19394 + EEE
19395 fff
19396
19397 iii
19398 "
19399 .unindent(),
19400 );
19401}
19402
19403#[gpui::test]
19404async fn test_edits_around_expanded_insertion_hunks(
19405 executor: BackgroundExecutor,
19406 cx: &mut TestAppContext,
19407) {
19408 init_test(cx, |_| {});
19409
19410 let mut cx = EditorTestContext::new(cx).await;
19411
19412 let diff_base = r#"
19413 use some::mod1;
19414 use some::mod2;
19415
19416 const A: u32 = 42;
19417
19418 fn main() {
19419 println!("hello");
19420
19421 println!("world");
19422 }
19423 "#
19424 .unindent();
19425 executor.run_until_parked();
19426 cx.set_state(
19427 &r#"
19428 use some::mod1;
19429 use some::mod2;
19430
19431 const A: u32 = 42;
19432 const B: u32 = 42;
19433 const C: u32 = 42;
19434 ˇ
19435
19436 fn main() {
19437 println!("hello");
19438
19439 println!("world");
19440 }
19441 "#
19442 .unindent(),
19443 );
19444
19445 cx.set_head_text(&diff_base);
19446 executor.run_until_parked();
19447
19448 cx.update_editor(|editor, window, cx| {
19449 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19450 });
19451 executor.run_until_parked();
19452
19453 cx.assert_state_with_diff(
19454 r#"
19455 use some::mod1;
19456 use some::mod2;
19457
19458 const A: u32 = 42;
19459 + const B: u32 = 42;
19460 + const C: u32 = 42;
19461 + ˇ
19462
19463 fn main() {
19464 println!("hello");
19465
19466 println!("world");
19467 }
19468 "#
19469 .unindent(),
19470 );
19471
19472 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
19473 executor.run_until_parked();
19474
19475 cx.assert_state_with_diff(
19476 r#"
19477 use some::mod1;
19478 use some::mod2;
19479
19480 const A: u32 = 42;
19481 + const B: u32 = 42;
19482 + const C: u32 = 42;
19483 + const D: u32 = 42;
19484 + ˇ
19485
19486 fn main() {
19487 println!("hello");
19488
19489 println!("world");
19490 }
19491 "#
19492 .unindent(),
19493 );
19494
19495 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
19496 executor.run_until_parked();
19497
19498 cx.assert_state_with_diff(
19499 r#"
19500 use some::mod1;
19501 use some::mod2;
19502
19503 const A: u32 = 42;
19504 + const B: u32 = 42;
19505 + const C: u32 = 42;
19506 + const D: u32 = 42;
19507 + const E: u32 = 42;
19508 + ˇ
19509
19510 fn main() {
19511 println!("hello");
19512
19513 println!("world");
19514 }
19515 "#
19516 .unindent(),
19517 );
19518
19519 cx.update_editor(|editor, window, cx| {
19520 editor.delete_line(&DeleteLine, window, cx);
19521 });
19522 executor.run_until_parked();
19523
19524 cx.assert_state_with_diff(
19525 r#"
19526 use some::mod1;
19527 use some::mod2;
19528
19529 const A: u32 = 42;
19530 + const B: u32 = 42;
19531 + const C: u32 = 42;
19532 + const D: u32 = 42;
19533 + const E: u32 = 42;
19534 ˇ
19535 fn main() {
19536 println!("hello");
19537
19538 println!("world");
19539 }
19540 "#
19541 .unindent(),
19542 );
19543
19544 cx.update_editor(|editor, window, cx| {
19545 editor.move_up(&MoveUp, window, cx);
19546 editor.delete_line(&DeleteLine, window, cx);
19547 editor.move_up(&MoveUp, window, cx);
19548 editor.delete_line(&DeleteLine, window, cx);
19549 editor.move_up(&MoveUp, window, cx);
19550 editor.delete_line(&DeleteLine, window, cx);
19551 });
19552 executor.run_until_parked();
19553 cx.assert_state_with_diff(
19554 r#"
19555 use some::mod1;
19556 use some::mod2;
19557
19558 const A: u32 = 42;
19559 + const B: u32 = 42;
19560 ˇ
19561 fn main() {
19562 println!("hello");
19563
19564 println!("world");
19565 }
19566 "#
19567 .unindent(),
19568 );
19569
19570 cx.update_editor(|editor, window, cx| {
19571 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
19572 editor.delete_line(&DeleteLine, window, cx);
19573 });
19574 executor.run_until_parked();
19575 cx.assert_state_with_diff(
19576 r#"
19577 ˇ
19578 fn main() {
19579 println!("hello");
19580
19581 println!("world");
19582 }
19583 "#
19584 .unindent(),
19585 );
19586}
19587
19588#[gpui::test]
19589async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
19590 init_test(cx, |_| {});
19591
19592 let mut cx = EditorTestContext::new(cx).await;
19593 cx.set_head_text(indoc! { "
19594 one
19595 two
19596 three
19597 four
19598 five
19599 "
19600 });
19601 cx.set_state(indoc! { "
19602 one
19603 ˇthree
19604 five
19605 "});
19606 cx.run_until_parked();
19607 cx.update_editor(|editor, window, cx| {
19608 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19609 });
19610 cx.assert_state_with_diff(
19611 indoc! { "
19612 one
19613 - two
19614 ˇthree
19615 - four
19616 five
19617 "}
19618 .to_string(),
19619 );
19620 cx.update_editor(|editor, window, cx| {
19621 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19622 });
19623
19624 cx.assert_state_with_diff(
19625 indoc! { "
19626 one
19627 ˇthree
19628 five
19629 "}
19630 .to_string(),
19631 );
19632
19633 cx.set_state(indoc! { "
19634 one
19635 ˇTWO
19636 three
19637 four
19638 five
19639 "});
19640 cx.run_until_parked();
19641 cx.update_editor(|editor, window, cx| {
19642 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19643 });
19644
19645 cx.assert_state_with_diff(
19646 indoc! { "
19647 one
19648 - two
19649 + ˇTWO
19650 three
19651 four
19652 five
19653 "}
19654 .to_string(),
19655 );
19656 cx.update_editor(|editor, window, cx| {
19657 editor.move_up(&Default::default(), window, cx);
19658 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19659 });
19660 cx.assert_state_with_diff(
19661 indoc! { "
19662 one
19663 ˇTWO
19664 three
19665 four
19666 five
19667 "}
19668 .to_string(),
19669 );
19670}
19671
19672#[gpui::test]
19673async fn test_edits_around_expanded_deletion_hunks(
19674 executor: BackgroundExecutor,
19675 cx: &mut TestAppContext,
19676) {
19677 init_test(cx, |_| {});
19678
19679 let mut cx = EditorTestContext::new(cx).await;
19680
19681 let diff_base = r#"
19682 use some::mod1;
19683 use some::mod2;
19684
19685 const A: u32 = 42;
19686 const B: u32 = 42;
19687 const C: u32 = 42;
19688
19689
19690 fn main() {
19691 println!("hello");
19692
19693 println!("world");
19694 }
19695 "#
19696 .unindent();
19697 executor.run_until_parked();
19698 cx.set_state(
19699 &r#"
19700 use some::mod1;
19701 use some::mod2;
19702
19703 ˇconst B: u32 = 42;
19704 const C: u32 = 42;
19705
19706
19707 fn main() {
19708 println!("hello");
19709
19710 println!("world");
19711 }
19712 "#
19713 .unindent(),
19714 );
19715
19716 cx.set_head_text(&diff_base);
19717 executor.run_until_parked();
19718
19719 cx.update_editor(|editor, window, cx| {
19720 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19721 });
19722 executor.run_until_parked();
19723
19724 cx.assert_state_with_diff(
19725 r#"
19726 use some::mod1;
19727 use some::mod2;
19728
19729 - const A: u32 = 42;
19730 ˇconst B: u32 = 42;
19731 const C: u32 = 42;
19732
19733
19734 fn main() {
19735 println!("hello");
19736
19737 println!("world");
19738 }
19739 "#
19740 .unindent(),
19741 );
19742
19743 cx.update_editor(|editor, window, cx| {
19744 editor.delete_line(&DeleteLine, window, cx);
19745 });
19746 executor.run_until_parked();
19747 cx.assert_state_with_diff(
19748 r#"
19749 use some::mod1;
19750 use some::mod2;
19751
19752 - const A: u32 = 42;
19753 - const B: u32 = 42;
19754 ˇconst C: u32 = 42;
19755
19756
19757 fn main() {
19758 println!("hello");
19759
19760 println!("world");
19761 }
19762 "#
19763 .unindent(),
19764 );
19765
19766 cx.update_editor(|editor, window, cx| {
19767 editor.delete_line(&DeleteLine, window, cx);
19768 });
19769 executor.run_until_parked();
19770 cx.assert_state_with_diff(
19771 r#"
19772 use some::mod1;
19773 use some::mod2;
19774
19775 - const A: u32 = 42;
19776 - const B: u32 = 42;
19777 - const C: u32 = 42;
19778 ˇ
19779
19780 fn main() {
19781 println!("hello");
19782
19783 println!("world");
19784 }
19785 "#
19786 .unindent(),
19787 );
19788
19789 cx.update_editor(|editor, window, cx| {
19790 editor.handle_input("replacement", window, cx);
19791 });
19792 executor.run_until_parked();
19793 cx.assert_state_with_diff(
19794 r#"
19795 use some::mod1;
19796 use some::mod2;
19797
19798 - const A: u32 = 42;
19799 - const B: u32 = 42;
19800 - const C: u32 = 42;
19801 -
19802 + replacementˇ
19803
19804 fn main() {
19805 println!("hello");
19806
19807 println!("world");
19808 }
19809 "#
19810 .unindent(),
19811 );
19812}
19813
19814#[gpui::test]
19815async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19816 init_test(cx, |_| {});
19817
19818 let mut cx = EditorTestContext::new(cx).await;
19819
19820 let base_text = r#"
19821 one
19822 two
19823 three
19824 four
19825 five
19826 "#
19827 .unindent();
19828 executor.run_until_parked();
19829 cx.set_state(
19830 &r#"
19831 one
19832 two
19833 fˇour
19834 five
19835 "#
19836 .unindent(),
19837 );
19838
19839 cx.set_head_text(&base_text);
19840 executor.run_until_parked();
19841
19842 cx.update_editor(|editor, window, cx| {
19843 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19844 });
19845 executor.run_until_parked();
19846
19847 cx.assert_state_with_diff(
19848 r#"
19849 one
19850 two
19851 - three
19852 fˇour
19853 five
19854 "#
19855 .unindent(),
19856 );
19857
19858 cx.update_editor(|editor, window, cx| {
19859 editor.backspace(&Backspace, window, cx);
19860 editor.backspace(&Backspace, window, cx);
19861 });
19862 executor.run_until_parked();
19863 cx.assert_state_with_diff(
19864 r#"
19865 one
19866 two
19867 - threeˇ
19868 - four
19869 + our
19870 five
19871 "#
19872 .unindent(),
19873 );
19874}
19875
19876#[gpui::test]
19877async fn test_edit_after_expanded_modification_hunk(
19878 executor: BackgroundExecutor,
19879 cx: &mut TestAppContext,
19880) {
19881 init_test(cx, |_| {});
19882
19883 let mut cx = EditorTestContext::new(cx).await;
19884
19885 let diff_base = r#"
19886 use some::mod1;
19887 use some::mod2;
19888
19889 const A: u32 = 42;
19890 const B: u32 = 42;
19891 const C: u32 = 42;
19892 const D: u32 = 42;
19893
19894
19895 fn main() {
19896 println!("hello");
19897
19898 println!("world");
19899 }"#
19900 .unindent();
19901
19902 cx.set_state(
19903 &r#"
19904 use some::mod1;
19905 use some::mod2;
19906
19907 const A: u32 = 42;
19908 const B: u32 = 42;
19909 const C: u32 = 43ˇ
19910 const D: u32 = 42;
19911
19912
19913 fn main() {
19914 println!("hello");
19915
19916 println!("world");
19917 }"#
19918 .unindent(),
19919 );
19920
19921 cx.set_head_text(&diff_base);
19922 executor.run_until_parked();
19923 cx.update_editor(|editor, window, cx| {
19924 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19925 });
19926 executor.run_until_parked();
19927
19928 cx.assert_state_with_diff(
19929 r#"
19930 use some::mod1;
19931 use some::mod2;
19932
19933 const A: u32 = 42;
19934 const B: u32 = 42;
19935 - const C: u32 = 42;
19936 + const C: u32 = 43ˇ
19937 const D: u32 = 42;
19938
19939
19940 fn main() {
19941 println!("hello");
19942
19943 println!("world");
19944 }"#
19945 .unindent(),
19946 );
19947
19948 cx.update_editor(|editor, window, cx| {
19949 editor.handle_input("\nnew_line\n", window, cx);
19950 });
19951 executor.run_until_parked();
19952
19953 cx.assert_state_with_diff(
19954 r#"
19955 use some::mod1;
19956 use some::mod2;
19957
19958 const A: u32 = 42;
19959 const B: u32 = 42;
19960 - const C: u32 = 42;
19961 + const C: u32 = 43
19962 + new_line
19963 + ˇ
19964 const D: u32 = 42;
19965
19966
19967 fn main() {
19968 println!("hello");
19969
19970 println!("world");
19971 }"#
19972 .unindent(),
19973 );
19974}
19975
19976#[gpui::test]
19977async fn test_stage_and_unstage_added_file_hunk(
19978 executor: BackgroundExecutor,
19979 cx: &mut TestAppContext,
19980) {
19981 init_test(cx, |_| {});
19982
19983 let mut cx = EditorTestContext::new(cx).await;
19984 cx.update_editor(|editor, _, cx| {
19985 editor.set_expand_all_diff_hunks(cx);
19986 });
19987
19988 let working_copy = r#"
19989 ˇfn main() {
19990 println!("hello, world!");
19991 }
19992 "#
19993 .unindent();
19994
19995 cx.set_state(&working_copy);
19996 executor.run_until_parked();
19997
19998 cx.assert_state_with_diff(
19999 r#"
20000 + ˇfn main() {
20001 + println!("hello, world!");
20002 + }
20003 "#
20004 .unindent(),
20005 );
20006 cx.assert_index_text(None);
20007
20008 cx.update_editor(|editor, window, cx| {
20009 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20010 });
20011 executor.run_until_parked();
20012 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
20013 cx.assert_state_with_diff(
20014 r#"
20015 + ˇfn main() {
20016 + println!("hello, world!");
20017 + }
20018 "#
20019 .unindent(),
20020 );
20021
20022 cx.update_editor(|editor, window, cx| {
20023 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20024 });
20025 executor.run_until_parked();
20026 cx.assert_index_text(None);
20027}
20028
20029async fn setup_indent_guides_editor(
20030 text: &str,
20031 cx: &mut TestAppContext,
20032) -> (BufferId, EditorTestContext) {
20033 init_test(cx, |_| {});
20034
20035 let mut cx = EditorTestContext::new(cx).await;
20036
20037 let buffer_id = cx.update_editor(|editor, window, cx| {
20038 editor.set_text(text, window, cx);
20039 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
20040
20041 buffer_ids[0]
20042 });
20043
20044 (buffer_id, cx)
20045}
20046
20047fn assert_indent_guides(
20048 range: Range<u32>,
20049 expected: Vec<IndentGuide>,
20050 active_indices: Option<Vec<usize>>,
20051 cx: &mut EditorTestContext,
20052) {
20053 let indent_guides = cx.update_editor(|editor, window, cx| {
20054 let snapshot = editor.snapshot(window, cx).display_snapshot;
20055 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
20056 editor,
20057 MultiBufferRow(range.start)..MultiBufferRow(range.end),
20058 true,
20059 &snapshot,
20060 cx,
20061 );
20062
20063 indent_guides.sort_by(|a, b| {
20064 a.depth.cmp(&b.depth).then(
20065 a.start_row
20066 .cmp(&b.start_row)
20067 .then(a.end_row.cmp(&b.end_row)),
20068 )
20069 });
20070 indent_guides
20071 });
20072
20073 if let Some(expected) = active_indices {
20074 let active_indices = cx.update_editor(|editor, window, cx| {
20075 let snapshot = editor.snapshot(window, cx).display_snapshot;
20076 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
20077 });
20078
20079 assert_eq!(
20080 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
20081 expected,
20082 "Active indent guide indices do not match"
20083 );
20084 }
20085
20086 assert_eq!(indent_guides, expected, "Indent guides do not match");
20087}
20088
20089fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
20090 IndentGuide {
20091 buffer_id,
20092 start_row: MultiBufferRow(start_row),
20093 end_row: MultiBufferRow(end_row),
20094 depth,
20095 tab_size: 4,
20096 settings: IndentGuideSettings {
20097 enabled: true,
20098 line_width: 1,
20099 active_line_width: 1,
20100 coloring: IndentGuideColoring::default(),
20101 background_coloring: IndentGuideBackgroundColoring::default(),
20102 },
20103 }
20104}
20105
20106#[gpui::test]
20107async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
20108 let (buffer_id, mut cx) = setup_indent_guides_editor(
20109 &"
20110 fn main() {
20111 let a = 1;
20112 }"
20113 .unindent(),
20114 cx,
20115 )
20116 .await;
20117
20118 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20119}
20120
20121#[gpui::test]
20122async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
20123 let (buffer_id, mut cx) = setup_indent_guides_editor(
20124 &"
20125 fn main() {
20126 let a = 1;
20127 let b = 2;
20128 }"
20129 .unindent(),
20130 cx,
20131 )
20132 .await;
20133
20134 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
20135}
20136
20137#[gpui::test]
20138async fn test_indent_guide_nested(cx: &mut TestAppContext) {
20139 let (buffer_id, mut cx) = setup_indent_guides_editor(
20140 &"
20141 fn main() {
20142 let a = 1;
20143 if a == 3 {
20144 let b = 2;
20145 } else {
20146 let c = 3;
20147 }
20148 }"
20149 .unindent(),
20150 cx,
20151 )
20152 .await;
20153
20154 assert_indent_guides(
20155 0..8,
20156 vec![
20157 indent_guide(buffer_id, 1, 6, 0),
20158 indent_guide(buffer_id, 3, 3, 1),
20159 indent_guide(buffer_id, 5, 5, 1),
20160 ],
20161 None,
20162 &mut cx,
20163 );
20164}
20165
20166#[gpui::test]
20167async fn test_indent_guide_tab(cx: &mut TestAppContext) {
20168 let (buffer_id, mut cx) = setup_indent_guides_editor(
20169 &"
20170 fn main() {
20171 let a = 1;
20172 let b = 2;
20173 let c = 3;
20174 }"
20175 .unindent(),
20176 cx,
20177 )
20178 .await;
20179
20180 assert_indent_guides(
20181 0..5,
20182 vec![
20183 indent_guide(buffer_id, 1, 3, 0),
20184 indent_guide(buffer_id, 2, 2, 1),
20185 ],
20186 None,
20187 &mut cx,
20188 );
20189}
20190
20191#[gpui::test]
20192async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
20193 let (buffer_id, mut cx) = setup_indent_guides_editor(
20194 &"
20195 fn main() {
20196 let a = 1;
20197
20198 let c = 3;
20199 }"
20200 .unindent(),
20201 cx,
20202 )
20203 .await;
20204
20205 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
20206}
20207
20208#[gpui::test]
20209async fn test_indent_guide_complex(cx: &mut TestAppContext) {
20210 let (buffer_id, mut cx) = setup_indent_guides_editor(
20211 &"
20212 fn main() {
20213 let a = 1;
20214
20215 let c = 3;
20216
20217 if a == 3 {
20218 let b = 2;
20219 } else {
20220 let c = 3;
20221 }
20222 }"
20223 .unindent(),
20224 cx,
20225 )
20226 .await;
20227
20228 assert_indent_guides(
20229 0..11,
20230 vec![
20231 indent_guide(buffer_id, 1, 9, 0),
20232 indent_guide(buffer_id, 6, 6, 1),
20233 indent_guide(buffer_id, 8, 8, 1),
20234 ],
20235 None,
20236 &mut cx,
20237 );
20238}
20239
20240#[gpui::test]
20241async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
20242 let (buffer_id, mut cx) = setup_indent_guides_editor(
20243 &"
20244 fn main() {
20245 let a = 1;
20246
20247 let c = 3;
20248
20249 if a == 3 {
20250 let b = 2;
20251 } else {
20252 let c = 3;
20253 }
20254 }"
20255 .unindent(),
20256 cx,
20257 )
20258 .await;
20259
20260 assert_indent_guides(
20261 1..11,
20262 vec![
20263 indent_guide(buffer_id, 1, 9, 0),
20264 indent_guide(buffer_id, 6, 6, 1),
20265 indent_guide(buffer_id, 8, 8, 1),
20266 ],
20267 None,
20268 &mut cx,
20269 );
20270}
20271
20272#[gpui::test]
20273async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
20274 let (buffer_id, mut cx) = setup_indent_guides_editor(
20275 &"
20276 fn main() {
20277 let a = 1;
20278
20279 let c = 3;
20280
20281 if a == 3 {
20282 let b = 2;
20283 } else {
20284 let c = 3;
20285 }
20286 }"
20287 .unindent(),
20288 cx,
20289 )
20290 .await;
20291
20292 assert_indent_guides(
20293 1..10,
20294 vec![
20295 indent_guide(buffer_id, 1, 9, 0),
20296 indent_guide(buffer_id, 6, 6, 1),
20297 indent_guide(buffer_id, 8, 8, 1),
20298 ],
20299 None,
20300 &mut cx,
20301 );
20302}
20303
20304#[gpui::test]
20305async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
20306 let (buffer_id, mut cx) = setup_indent_guides_editor(
20307 &"
20308 fn main() {
20309 if a {
20310 b(
20311 c,
20312 d,
20313 )
20314 } else {
20315 e(
20316 f
20317 )
20318 }
20319 }"
20320 .unindent(),
20321 cx,
20322 )
20323 .await;
20324
20325 assert_indent_guides(
20326 0..11,
20327 vec![
20328 indent_guide(buffer_id, 1, 10, 0),
20329 indent_guide(buffer_id, 2, 5, 1),
20330 indent_guide(buffer_id, 7, 9, 1),
20331 indent_guide(buffer_id, 3, 4, 2),
20332 indent_guide(buffer_id, 8, 8, 2),
20333 ],
20334 None,
20335 &mut cx,
20336 );
20337
20338 cx.update_editor(|editor, window, cx| {
20339 editor.fold_at(MultiBufferRow(2), window, cx);
20340 assert_eq!(
20341 editor.display_text(cx),
20342 "
20343 fn main() {
20344 if a {
20345 b(⋯
20346 )
20347 } else {
20348 e(
20349 f
20350 )
20351 }
20352 }"
20353 .unindent()
20354 );
20355 });
20356
20357 assert_indent_guides(
20358 0..11,
20359 vec![
20360 indent_guide(buffer_id, 1, 10, 0),
20361 indent_guide(buffer_id, 2, 5, 1),
20362 indent_guide(buffer_id, 7, 9, 1),
20363 indent_guide(buffer_id, 8, 8, 2),
20364 ],
20365 None,
20366 &mut cx,
20367 );
20368}
20369
20370#[gpui::test]
20371async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
20372 let (buffer_id, mut cx) = setup_indent_guides_editor(
20373 &"
20374 block1
20375 block2
20376 block3
20377 block4
20378 block2
20379 block1
20380 block1"
20381 .unindent(),
20382 cx,
20383 )
20384 .await;
20385
20386 assert_indent_guides(
20387 1..10,
20388 vec![
20389 indent_guide(buffer_id, 1, 4, 0),
20390 indent_guide(buffer_id, 2, 3, 1),
20391 indent_guide(buffer_id, 3, 3, 2),
20392 ],
20393 None,
20394 &mut cx,
20395 );
20396}
20397
20398#[gpui::test]
20399async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
20400 let (buffer_id, mut cx) = setup_indent_guides_editor(
20401 &"
20402 block1
20403 block2
20404 block3
20405
20406 block1
20407 block1"
20408 .unindent(),
20409 cx,
20410 )
20411 .await;
20412
20413 assert_indent_guides(
20414 0..6,
20415 vec![
20416 indent_guide(buffer_id, 1, 2, 0),
20417 indent_guide(buffer_id, 2, 2, 1),
20418 ],
20419 None,
20420 &mut cx,
20421 );
20422}
20423
20424#[gpui::test]
20425async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
20426 let (buffer_id, mut cx) = setup_indent_guides_editor(
20427 &"
20428 function component() {
20429 \treturn (
20430 \t\t\t
20431 \t\t<div>
20432 \t\t\t<abc></abc>
20433 \t\t</div>
20434 \t)
20435 }"
20436 .unindent(),
20437 cx,
20438 )
20439 .await;
20440
20441 assert_indent_guides(
20442 0..8,
20443 vec![
20444 indent_guide(buffer_id, 1, 6, 0),
20445 indent_guide(buffer_id, 2, 5, 1),
20446 indent_guide(buffer_id, 4, 4, 2),
20447 ],
20448 None,
20449 &mut cx,
20450 );
20451}
20452
20453#[gpui::test]
20454async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
20455 let (buffer_id, mut cx) = setup_indent_guides_editor(
20456 &"
20457 function component() {
20458 \treturn (
20459 \t
20460 \t\t<div>
20461 \t\t\t<abc></abc>
20462 \t\t</div>
20463 \t)
20464 }"
20465 .unindent(),
20466 cx,
20467 )
20468 .await;
20469
20470 assert_indent_guides(
20471 0..8,
20472 vec![
20473 indent_guide(buffer_id, 1, 6, 0),
20474 indent_guide(buffer_id, 2, 5, 1),
20475 indent_guide(buffer_id, 4, 4, 2),
20476 ],
20477 None,
20478 &mut cx,
20479 );
20480}
20481
20482#[gpui::test]
20483async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
20484 let (buffer_id, mut cx) = setup_indent_guides_editor(
20485 &"
20486 block1
20487
20488
20489
20490 block2
20491 "
20492 .unindent(),
20493 cx,
20494 )
20495 .await;
20496
20497 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20498}
20499
20500#[gpui::test]
20501async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
20502 let (buffer_id, mut cx) = setup_indent_guides_editor(
20503 &"
20504 def a:
20505 \tb = 3
20506 \tif True:
20507 \t\tc = 4
20508 \t\td = 5
20509 \tprint(b)
20510 "
20511 .unindent(),
20512 cx,
20513 )
20514 .await;
20515
20516 assert_indent_guides(
20517 0..6,
20518 vec![
20519 indent_guide(buffer_id, 1, 5, 0),
20520 indent_guide(buffer_id, 3, 4, 1),
20521 ],
20522 None,
20523 &mut cx,
20524 );
20525}
20526
20527#[gpui::test]
20528async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
20529 let (buffer_id, mut cx) = setup_indent_guides_editor(
20530 &"
20531 fn main() {
20532 let a = 1;
20533 }"
20534 .unindent(),
20535 cx,
20536 )
20537 .await;
20538
20539 cx.update_editor(|editor, window, cx| {
20540 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20541 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20542 });
20543 });
20544
20545 assert_indent_guides(
20546 0..3,
20547 vec![indent_guide(buffer_id, 1, 1, 0)],
20548 Some(vec![0]),
20549 &mut cx,
20550 );
20551}
20552
20553#[gpui::test]
20554async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
20555 let (buffer_id, mut cx) = setup_indent_guides_editor(
20556 &"
20557 fn main() {
20558 if 1 == 2 {
20559 let a = 1;
20560 }
20561 }"
20562 .unindent(),
20563 cx,
20564 )
20565 .await;
20566
20567 cx.update_editor(|editor, window, cx| {
20568 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20569 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20570 });
20571 });
20572
20573 assert_indent_guides(
20574 0..4,
20575 vec![
20576 indent_guide(buffer_id, 1, 3, 0),
20577 indent_guide(buffer_id, 2, 2, 1),
20578 ],
20579 Some(vec![1]),
20580 &mut cx,
20581 );
20582
20583 cx.update_editor(|editor, window, cx| {
20584 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20585 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20586 });
20587 });
20588
20589 assert_indent_guides(
20590 0..4,
20591 vec![
20592 indent_guide(buffer_id, 1, 3, 0),
20593 indent_guide(buffer_id, 2, 2, 1),
20594 ],
20595 Some(vec![1]),
20596 &mut cx,
20597 );
20598
20599 cx.update_editor(|editor, window, cx| {
20600 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20601 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
20602 });
20603 });
20604
20605 assert_indent_guides(
20606 0..4,
20607 vec![
20608 indent_guide(buffer_id, 1, 3, 0),
20609 indent_guide(buffer_id, 2, 2, 1),
20610 ],
20611 Some(vec![0]),
20612 &mut cx,
20613 );
20614}
20615
20616#[gpui::test]
20617async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
20618 let (buffer_id, mut cx) = setup_indent_guides_editor(
20619 &"
20620 fn main() {
20621 let a = 1;
20622
20623 let b = 2;
20624 }"
20625 .unindent(),
20626 cx,
20627 )
20628 .await;
20629
20630 cx.update_editor(|editor, window, cx| {
20631 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20632 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20633 });
20634 });
20635
20636 assert_indent_guides(
20637 0..5,
20638 vec![indent_guide(buffer_id, 1, 3, 0)],
20639 Some(vec![0]),
20640 &mut cx,
20641 );
20642}
20643
20644#[gpui::test]
20645async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
20646 let (buffer_id, mut cx) = setup_indent_guides_editor(
20647 &"
20648 def m:
20649 a = 1
20650 pass"
20651 .unindent(),
20652 cx,
20653 )
20654 .await;
20655
20656 cx.update_editor(|editor, window, cx| {
20657 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20658 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20659 });
20660 });
20661
20662 assert_indent_guides(
20663 0..3,
20664 vec![indent_guide(buffer_id, 1, 2, 0)],
20665 Some(vec![0]),
20666 &mut cx,
20667 );
20668}
20669
20670#[gpui::test]
20671async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
20672 init_test(cx, |_| {});
20673 let mut cx = EditorTestContext::new(cx).await;
20674 let text = indoc! {
20675 "
20676 impl A {
20677 fn b() {
20678 0;
20679 3;
20680 5;
20681 6;
20682 7;
20683 }
20684 }
20685 "
20686 };
20687 let base_text = indoc! {
20688 "
20689 impl A {
20690 fn b() {
20691 0;
20692 1;
20693 2;
20694 3;
20695 4;
20696 }
20697 fn c() {
20698 5;
20699 6;
20700 7;
20701 }
20702 }
20703 "
20704 };
20705
20706 cx.update_editor(|editor, window, cx| {
20707 editor.set_text(text, window, cx);
20708
20709 editor.buffer().update(cx, |multibuffer, cx| {
20710 let buffer = multibuffer.as_singleton().unwrap();
20711 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
20712
20713 multibuffer.set_all_diff_hunks_expanded(cx);
20714 multibuffer.add_diff(diff, cx);
20715
20716 buffer.read(cx).remote_id()
20717 })
20718 });
20719 cx.run_until_parked();
20720
20721 cx.assert_state_with_diff(
20722 indoc! { "
20723 impl A {
20724 fn b() {
20725 0;
20726 - 1;
20727 - 2;
20728 3;
20729 - 4;
20730 - }
20731 - fn c() {
20732 5;
20733 6;
20734 7;
20735 }
20736 }
20737 ˇ"
20738 }
20739 .to_string(),
20740 );
20741
20742 let mut actual_guides = cx.update_editor(|editor, window, cx| {
20743 editor
20744 .snapshot(window, cx)
20745 .buffer_snapshot()
20746 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
20747 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
20748 .collect::<Vec<_>>()
20749 });
20750 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
20751 assert_eq!(
20752 actual_guides,
20753 vec![
20754 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
20755 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
20756 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
20757 ]
20758 );
20759}
20760
20761#[gpui::test]
20762async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
20763 init_test(cx, |_| {});
20764 let mut cx = EditorTestContext::new(cx).await;
20765
20766 let diff_base = r#"
20767 a
20768 b
20769 c
20770 "#
20771 .unindent();
20772
20773 cx.set_state(
20774 &r#"
20775 ˇA
20776 b
20777 C
20778 "#
20779 .unindent(),
20780 );
20781 cx.set_head_text(&diff_base);
20782 cx.update_editor(|editor, window, cx| {
20783 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20784 });
20785 executor.run_until_parked();
20786
20787 let both_hunks_expanded = r#"
20788 - a
20789 + ˇA
20790 b
20791 - c
20792 + C
20793 "#
20794 .unindent();
20795
20796 cx.assert_state_with_diff(both_hunks_expanded.clone());
20797
20798 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20799 let snapshot = editor.snapshot(window, cx);
20800 let hunks = editor
20801 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
20802 .collect::<Vec<_>>();
20803 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20804 let buffer_id = hunks[0].buffer_id;
20805 hunks
20806 .into_iter()
20807 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20808 .collect::<Vec<_>>()
20809 });
20810 assert_eq!(hunk_ranges.len(), 2);
20811
20812 cx.update_editor(|editor, _, cx| {
20813 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20814 });
20815 executor.run_until_parked();
20816
20817 let second_hunk_expanded = r#"
20818 ˇA
20819 b
20820 - c
20821 + C
20822 "#
20823 .unindent();
20824
20825 cx.assert_state_with_diff(second_hunk_expanded);
20826
20827 cx.update_editor(|editor, _, cx| {
20828 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20829 });
20830 executor.run_until_parked();
20831
20832 cx.assert_state_with_diff(both_hunks_expanded.clone());
20833
20834 cx.update_editor(|editor, _, cx| {
20835 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20836 });
20837 executor.run_until_parked();
20838
20839 let first_hunk_expanded = r#"
20840 - a
20841 + ˇA
20842 b
20843 C
20844 "#
20845 .unindent();
20846
20847 cx.assert_state_with_diff(first_hunk_expanded);
20848
20849 cx.update_editor(|editor, _, cx| {
20850 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20851 });
20852 executor.run_until_parked();
20853
20854 cx.assert_state_with_diff(both_hunks_expanded);
20855
20856 cx.set_state(
20857 &r#"
20858 ˇA
20859 b
20860 "#
20861 .unindent(),
20862 );
20863 cx.run_until_parked();
20864
20865 // TODO this cursor position seems bad
20866 cx.assert_state_with_diff(
20867 r#"
20868 - ˇa
20869 + A
20870 b
20871 "#
20872 .unindent(),
20873 );
20874
20875 cx.update_editor(|editor, window, cx| {
20876 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20877 });
20878
20879 cx.assert_state_with_diff(
20880 r#"
20881 - ˇa
20882 + A
20883 b
20884 - c
20885 "#
20886 .unindent(),
20887 );
20888
20889 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20890 let snapshot = editor.snapshot(window, cx);
20891 let hunks = editor
20892 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
20893 .collect::<Vec<_>>();
20894 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20895 let buffer_id = hunks[0].buffer_id;
20896 hunks
20897 .into_iter()
20898 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20899 .collect::<Vec<_>>()
20900 });
20901 assert_eq!(hunk_ranges.len(), 2);
20902
20903 cx.update_editor(|editor, _, cx| {
20904 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20905 });
20906 executor.run_until_parked();
20907
20908 cx.assert_state_with_diff(
20909 r#"
20910 - ˇa
20911 + A
20912 b
20913 "#
20914 .unindent(),
20915 );
20916}
20917
20918#[gpui::test]
20919async fn test_toggle_deletion_hunk_at_start_of_file(
20920 executor: BackgroundExecutor,
20921 cx: &mut TestAppContext,
20922) {
20923 init_test(cx, |_| {});
20924 let mut cx = EditorTestContext::new(cx).await;
20925
20926 let diff_base = r#"
20927 a
20928 b
20929 c
20930 "#
20931 .unindent();
20932
20933 cx.set_state(
20934 &r#"
20935 ˇb
20936 c
20937 "#
20938 .unindent(),
20939 );
20940 cx.set_head_text(&diff_base);
20941 cx.update_editor(|editor, window, cx| {
20942 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20943 });
20944 executor.run_until_parked();
20945
20946 let hunk_expanded = r#"
20947 - a
20948 ˇb
20949 c
20950 "#
20951 .unindent();
20952
20953 cx.assert_state_with_diff(hunk_expanded.clone());
20954
20955 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20956 let snapshot = editor.snapshot(window, cx);
20957 let hunks = editor
20958 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
20959 .collect::<Vec<_>>();
20960 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20961 let buffer_id = hunks[0].buffer_id;
20962 hunks
20963 .into_iter()
20964 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20965 .collect::<Vec<_>>()
20966 });
20967 assert_eq!(hunk_ranges.len(), 1);
20968
20969 cx.update_editor(|editor, _, cx| {
20970 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20971 });
20972 executor.run_until_parked();
20973
20974 let hunk_collapsed = r#"
20975 ˇb
20976 c
20977 "#
20978 .unindent();
20979
20980 cx.assert_state_with_diff(hunk_collapsed);
20981
20982 cx.update_editor(|editor, _, cx| {
20983 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20984 });
20985 executor.run_until_parked();
20986
20987 cx.assert_state_with_diff(hunk_expanded);
20988}
20989
20990#[gpui::test]
20991async fn test_display_diff_hunks(cx: &mut TestAppContext) {
20992 init_test(cx, |_| {});
20993
20994 let fs = FakeFs::new(cx.executor());
20995 fs.insert_tree(
20996 path!("/test"),
20997 json!({
20998 ".git": {},
20999 "file-1": "ONE\n",
21000 "file-2": "TWO\n",
21001 "file-3": "THREE\n",
21002 }),
21003 )
21004 .await;
21005
21006 fs.set_head_for_repo(
21007 path!("/test/.git").as_ref(),
21008 &[
21009 ("file-1", "one\n".into()),
21010 ("file-2", "two\n".into()),
21011 ("file-3", "three\n".into()),
21012 ],
21013 "deadbeef",
21014 );
21015
21016 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
21017 let mut buffers = vec![];
21018 for i in 1..=3 {
21019 let buffer = project
21020 .update(cx, |project, cx| {
21021 let path = format!(path!("/test/file-{}"), i);
21022 project.open_local_buffer(path, cx)
21023 })
21024 .await
21025 .unwrap();
21026 buffers.push(buffer);
21027 }
21028
21029 let multibuffer = cx.new(|cx| {
21030 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
21031 multibuffer.set_all_diff_hunks_expanded(cx);
21032 for buffer in &buffers {
21033 let snapshot = buffer.read(cx).snapshot();
21034 multibuffer.set_excerpts_for_path(
21035 PathKey::with_sort_prefix(0, buffer.read(cx).file().unwrap().path().clone()),
21036 buffer.clone(),
21037 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
21038 2,
21039 cx,
21040 );
21041 }
21042 multibuffer
21043 });
21044
21045 let editor = cx.add_window(|window, cx| {
21046 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
21047 });
21048 cx.run_until_parked();
21049
21050 let snapshot = editor
21051 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21052 .unwrap();
21053 let hunks = snapshot
21054 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
21055 .map(|hunk| match hunk {
21056 DisplayDiffHunk::Unfolded {
21057 display_row_range, ..
21058 } => display_row_range,
21059 DisplayDiffHunk::Folded { .. } => unreachable!(),
21060 })
21061 .collect::<Vec<_>>();
21062 assert_eq!(
21063 hunks,
21064 [
21065 DisplayRow(2)..DisplayRow(4),
21066 DisplayRow(7)..DisplayRow(9),
21067 DisplayRow(12)..DisplayRow(14),
21068 ]
21069 );
21070}
21071
21072#[gpui::test]
21073async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
21074 init_test(cx, |_| {});
21075
21076 let mut cx = EditorTestContext::new(cx).await;
21077 cx.set_head_text(indoc! { "
21078 one
21079 two
21080 three
21081 four
21082 five
21083 "
21084 });
21085 cx.set_index_text(indoc! { "
21086 one
21087 two
21088 three
21089 four
21090 five
21091 "
21092 });
21093 cx.set_state(indoc! {"
21094 one
21095 TWO
21096 ˇTHREE
21097 FOUR
21098 five
21099 "});
21100 cx.run_until_parked();
21101 cx.update_editor(|editor, window, cx| {
21102 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21103 });
21104 cx.run_until_parked();
21105 cx.assert_index_text(Some(indoc! {"
21106 one
21107 TWO
21108 THREE
21109 FOUR
21110 five
21111 "}));
21112 cx.set_state(indoc! { "
21113 one
21114 TWO
21115 ˇTHREE-HUNDRED
21116 FOUR
21117 five
21118 "});
21119 cx.run_until_parked();
21120 cx.update_editor(|editor, window, cx| {
21121 let snapshot = editor.snapshot(window, cx);
21122 let hunks = editor
21123 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21124 .collect::<Vec<_>>();
21125 assert_eq!(hunks.len(), 1);
21126 assert_eq!(
21127 hunks[0].status(),
21128 DiffHunkStatus {
21129 kind: DiffHunkStatusKind::Modified,
21130 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
21131 }
21132 );
21133
21134 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21135 });
21136 cx.run_until_parked();
21137 cx.assert_index_text(Some(indoc! {"
21138 one
21139 TWO
21140 THREE-HUNDRED
21141 FOUR
21142 five
21143 "}));
21144}
21145
21146#[gpui::test]
21147fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
21148 init_test(cx, |_| {});
21149
21150 let editor = cx.add_window(|window, cx| {
21151 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
21152 build_editor(buffer, window, cx)
21153 });
21154
21155 let render_args = Arc::new(Mutex::new(None));
21156 let snapshot = editor
21157 .update(cx, |editor, window, cx| {
21158 let snapshot = editor.buffer().read(cx).snapshot(cx);
21159 let range =
21160 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
21161
21162 struct RenderArgs {
21163 row: MultiBufferRow,
21164 folded: bool,
21165 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
21166 }
21167
21168 let crease = Crease::inline(
21169 range,
21170 FoldPlaceholder::test(),
21171 {
21172 let toggle_callback = render_args.clone();
21173 move |row, folded, callback, _window, _cx| {
21174 *toggle_callback.lock() = Some(RenderArgs {
21175 row,
21176 folded,
21177 callback,
21178 });
21179 div()
21180 }
21181 },
21182 |_row, _folded, _window, _cx| div(),
21183 );
21184
21185 editor.insert_creases(Some(crease), cx);
21186 let snapshot = editor.snapshot(window, cx);
21187 let _div =
21188 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
21189 snapshot
21190 })
21191 .unwrap();
21192
21193 let render_args = render_args.lock().take().unwrap();
21194 assert_eq!(render_args.row, MultiBufferRow(1));
21195 assert!(!render_args.folded);
21196 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21197
21198 cx.update_window(*editor, |_, window, cx| {
21199 (render_args.callback)(true, window, cx)
21200 })
21201 .unwrap();
21202 let snapshot = editor
21203 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21204 .unwrap();
21205 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
21206
21207 cx.update_window(*editor, |_, window, cx| {
21208 (render_args.callback)(false, window, cx)
21209 })
21210 .unwrap();
21211 let snapshot = editor
21212 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21213 .unwrap();
21214 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21215}
21216
21217#[gpui::test]
21218async fn test_input_text(cx: &mut TestAppContext) {
21219 init_test(cx, |_| {});
21220 let mut cx = EditorTestContext::new(cx).await;
21221
21222 cx.set_state(
21223 &r#"ˇone
21224 two
21225
21226 three
21227 fourˇ
21228 five
21229
21230 siˇx"#
21231 .unindent(),
21232 );
21233
21234 cx.dispatch_action(HandleInput(String::new()));
21235 cx.assert_editor_state(
21236 &r#"ˇone
21237 two
21238
21239 three
21240 fourˇ
21241 five
21242
21243 siˇx"#
21244 .unindent(),
21245 );
21246
21247 cx.dispatch_action(HandleInput("AAAA".to_string()));
21248 cx.assert_editor_state(
21249 &r#"AAAAˇone
21250 two
21251
21252 three
21253 fourAAAAˇ
21254 five
21255
21256 siAAAAˇx"#
21257 .unindent(),
21258 );
21259}
21260
21261#[gpui::test]
21262async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
21263 init_test(cx, |_| {});
21264
21265 let mut cx = EditorTestContext::new(cx).await;
21266 cx.set_state(
21267 r#"let foo = 1;
21268let foo = 2;
21269let foo = 3;
21270let fooˇ = 4;
21271let foo = 5;
21272let foo = 6;
21273let foo = 7;
21274let foo = 8;
21275let foo = 9;
21276let foo = 10;
21277let foo = 11;
21278let foo = 12;
21279let foo = 13;
21280let foo = 14;
21281let foo = 15;"#,
21282 );
21283
21284 cx.update_editor(|e, window, cx| {
21285 assert_eq!(
21286 e.next_scroll_position,
21287 NextScrollCursorCenterTopBottom::Center,
21288 "Default next scroll direction is center",
21289 );
21290
21291 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21292 assert_eq!(
21293 e.next_scroll_position,
21294 NextScrollCursorCenterTopBottom::Top,
21295 "After center, next scroll direction should be top",
21296 );
21297
21298 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21299 assert_eq!(
21300 e.next_scroll_position,
21301 NextScrollCursorCenterTopBottom::Bottom,
21302 "After top, next scroll direction should be bottom",
21303 );
21304
21305 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21306 assert_eq!(
21307 e.next_scroll_position,
21308 NextScrollCursorCenterTopBottom::Center,
21309 "After bottom, scrolling should start over",
21310 );
21311
21312 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21313 assert_eq!(
21314 e.next_scroll_position,
21315 NextScrollCursorCenterTopBottom::Top,
21316 "Scrolling continues if retriggered fast enough"
21317 );
21318 });
21319
21320 cx.executor()
21321 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
21322 cx.executor().run_until_parked();
21323 cx.update_editor(|e, _, _| {
21324 assert_eq!(
21325 e.next_scroll_position,
21326 NextScrollCursorCenterTopBottom::Center,
21327 "If scrolling is not triggered fast enough, it should reset"
21328 );
21329 });
21330}
21331
21332#[gpui::test]
21333async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
21334 init_test(cx, |_| {});
21335 let mut cx = EditorLspTestContext::new_rust(
21336 lsp::ServerCapabilities {
21337 definition_provider: Some(lsp::OneOf::Left(true)),
21338 references_provider: Some(lsp::OneOf::Left(true)),
21339 ..lsp::ServerCapabilities::default()
21340 },
21341 cx,
21342 )
21343 .await;
21344
21345 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
21346 let go_to_definition = cx
21347 .lsp
21348 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21349 move |params, _| async move {
21350 if empty_go_to_definition {
21351 Ok(None)
21352 } else {
21353 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
21354 uri: params.text_document_position_params.text_document.uri,
21355 range: lsp::Range::new(
21356 lsp::Position::new(4, 3),
21357 lsp::Position::new(4, 6),
21358 ),
21359 })))
21360 }
21361 },
21362 );
21363 let references = cx
21364 .lsp
21365 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21366 Ok(Some(vec![lsp::Location {
21367 uri: params.text_document_position.text_document.uri,
21368 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
21369 }]))
21370 });
21371 (go_to_definition, references)
21372 };
21373
21374 cx.set_state(
21375 &r#"fn one() {
21376 let mut a = ˇtwo();
21377 }
21378
21379 fn two() {}"#
21380 .unindent(),
21381 );
21382 set_up_lsp_handlers(false, &mut cx);
21383 let navigated = cx
21384 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21385 .await
21386 .expect("Failed to navigate to definition");
21387 assert_eq!(
21388 navigated,
21389 Navigated::Yes,
21390 "Should have navigated to definition from the GetDefinition response"
21391 );
21392 cx.assert_editor_state(
21393 &r#"fn one() {
21394 let mut a = two();
21395 }
21396
21397 fn «twoˇ»() {}"#
21398 .unindent(),
21399 );
21400
21401 let editors = cx.update_workspace(|workspace, _, cx| {
21402 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21403 });
21404 cx.update_editor(|_, _, test_editor_cx| {
21405 assert_eq!(
21406 editors.len(),
21407 1,
21408 "Initially, only one, test, editor should be open in the workspace"
21409 );
21410 assert_eq!(
21411 test_editor_cx.entity(),
21412 editors.last().expect("Asserted len is 1").clone()
21413 );
21414 });
21415
21416 set_up_lsp_handlers(true, &mut cx);
21417 let navigated = cx
21418 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21419 .await
21420 .expect("Failed to navigate to lookup references");
21421 assert_eq!(
21422 navigated,
21423 Navigated::Yes,
21424 "Should have navigated to references as a fallback after empty GoToDefinition response"
21425 );
21426 // We should not change the selections in the existing file,
21427 // if opening another milti buffer with the references
21428 cx.assert_editor_state(
21429 &r#"fn one() {
21430 let mut a = two();
21431 }
21432
21433 fn «twoˇ»() {}"#
21434 .unindent(),
21435 );
21436 let editors = cx.update_workspace(|workspace, _, cx| {
21437 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21438 });
21439 cx.update_editor(|_, _, test_editor_cx| {
21440 assert_eq!(
21441 editors.len(),
21442 2,
21443 "After falling back to references search, we open a new editor with the results"
21444 );
21445 let references_fallback_text = editors
21446 .into_iter()
21447 .find(|new_editor| *new_editor != test_editor_cx.entity())
21448 .expect("Should have one non-test editor now")
21449 .read(test_editor_cx)
21450 .text(test_editor_cx);
21451 assert_eq!(
21452 references_fallback_text, "fn one() {\n let mut a = two();\n}",
21453 "Should use the range from the references response and not the GoToDefinition one"
21454 );
21455 });
21456}
21457
21458#[gpui::test]
21459async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
21460 init_test(cx, |_| {});
21461 cx.update(|cx| {
21462 let mut editor_settings = EditorSettings::get_global(cx).clone();
21463 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
21464 EditorSettings::override_global(editor_settings, cx);
21465 });
21466 let mut cx = EditorLspTestContext::new_rust(
21467 lsp::ServerCapabilities {
21468 definition_provider: Some(lsp::OneOf::Left(true)),
21469 references_provider: Some(lsp::OneOf::Left(true)),
21470 ..lsp::ServerCapabilities::default()
21471 },
21472 cx,
21473 )
21474 .await;
21475 let original_state = r#"fn one() {
21476 let mut a = ˇtwo();
21477 }
21478
21479 fn two() {}"#
21480 .unindent();
21481 cx.set_state(&original_state);
21482
21483 let mut go_to_definition = cx
21484 .lsp
21485 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21486 move |_, _| async move { Ok(None) },
21487 );
21488 let _references = cx
21489 .lsp
21490 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
21491 panic!("Should not call for references with no go to definition fallback")
21492 });
21493
21494 let navigated = cx
21495 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21496 .await
21497 .expect("Failed to navigate to lookup references");
21498 go_to_definition
21499 .next()
21500 .await
21501 .expect("Should have called the go_to_definition handler");
21502
21503 assert_eq!(
21504 navigated,
21505 Navigated::No,
21506 "Should have navigated to references as a fallback after empty GoToDefinition response"
21507 );
21508 cx.assert_editor_state(&original_state);
21509 let editors = cx.update_workspace(|workspace, _, cx| {
21510 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21511 });
21512 cx.update_editor(|_, _, _| {
21513 assert_eq!(
21514 editors.len(),
21515 1,
21516 "After unsuccessful fallback, no other editor should have been opened"
21517 );
21518 });
21519}
21520
21521#[gpui::test]
21522async fn test_find_all_references_editor_reuse(cx: &mut TestAppContext) {
21523 init_test(cx, |_| {});
21524 let mut cx = EditorLspTestContext::new_rust(
21525 lsp::ServerCapabilities {
21526 references_provider: Some(lsp::OneOf::Left(true)),
21527 ..lsp::ServerCapabilities::default()
21528 },
21529 cx,
21530 )
21531 .await;
21532
21533 cx.set_state(
21534 &r#"
21535 fn one() {
21536 let mut a = two();
21537 }
21538
21539 fn ˇtwo() {}"#
21540 .unindent(),
21541 );
21542 cx.lsp
21543 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21544 Ok(Some(vec![
21545 lsp::Location {
21546 uri: params.text_document_position.text_document.uri.clone(),
21547 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
21548 },
21549 lsp::Location {
21550 uri: params.text_document_position.text_document.uri,
21551 range: lsp::Range::new(lsp::Position::new(4, 4), lsp::Position::new(4, 7)),
21552 },
21553 ]))
21554 });
21555 let navigated = cx
21556 .update_editor(|editor, window, cx| {
21557 editor.find_all_references(&FindAllReferences, window, cx)
21558 })
21559 .unwrap()
21560 .await
21561 .expect("Failed to navigate to references");
21562 assert_eq!(
21563 navigated,
21564 Navigated::Yes,
21565 "Should have navigated to references from the FindAllReferences response"
21566 );
21567 cx.assert_editor_state(
21568 &r#"fn one() {
21569 let mut a = two();
21570 }
21571
21572 fn ˇtwo() {}"#
21573 .unindent(),
21574 );
21575
21576 let editors = cx.update_workspace(|workspace, _, cx| {
21577 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21578 });
21579 cx.update_editor(|_, _, _| {
21580 assert_eq!(editors.len(), 2, "We should have opened a new multibuffer");
21581 });
21582
21583 cx.set_state(
21584 &r#"fn one() {
21585 let mut a = ˇtwo();
21586 }
21587
21588 fn two() {}"#
21589 .unindent(),
21590 );
21591 let navigated = cx
21592 .update_editor(|editor, window, cx| {
21593 editor.find_all_references(&FindAllReferences, window, cx)
21594 })
21595 .unwrap()
21596 .await
21597 .expect("Failed to navigate to references");
21598 assert_eq!(
21599 navigated,
21600 Navigated::Yes,
21601 "Should have navigated to references from the FindAllReferences response"
21602 );
21603 cx.assert_editor_state(
21604 &r#"fn one() {
21605 let mut a = ˇtwo();
21606 }
21607
21608 fn two() {}"#
21609 .unindent(),
21610 );
21611 let editors = cx.update_workspace(|workspace, _, cx| {
21612 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21613 });
21614 cx.update_editor(|_, _, _| {
21615 assert_eq!(
21616 editors.len(),
21617 2,
21618 "should have re-used the previous multibuffer"
21619 );
21620 });
21621
21622 cx.set_state(
21623 &r#"fn one() {
21624 let mut a = ˇtwo();
21625 }
21626 fn three() {}
21627 fn two() {}"#
21628 .unindent(),
21629 );
21630 cx.lsp
21631 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21632 Ok(Some(vec![
21633 lsp::Location {
21634 uri: params.text_document_position.text_document.uri.clone(),
21635 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
21636 },
21637 lsp::Location {
21638 uri: params.text_document_position.text_document.uri,
21639 range: lsp::Range::new(lsp::Position::new(5, 4), lsp::Position::new(5, 7)),
21640 },
21641 ]))
21642 });
21643 let navigated = cx
21644 .update_editor(|editor, window, cx| {
21645 editor.find_all_references(&FindAllReferences, window, cx)
21646 })
21647 .unwrap()
21648 .await
21649 .expect("Failed to navigate to references");
21650 assert_eq!(
21651 navigated,
21652 Navigated::Yes,
21653 "Should have navigated to references from the FindAllReferences response"
21654 );
21655 cx.assert_editor_state(
21656 &r#"fn one() {
21657 let mut a = ˇtwo();
21658 }
21659 fn three() {}
21660 fn two() {}"#
21661 .unindent(),
21662 );
21663 let editors = cx.update_workspace(|workspace, _, cx| {
21664 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21665 });
21666 cx.update_editor(|_, _, _| {
21667 assert_eq!(
21668 editors.len(),
21669 3,
21670 "should have used a new multibuffer as offsets changed"
21671 );
21672 });
21673}
21674#[gpui::test]
21675async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
21676 init_test(cx, |_| {});
21677
21678 let language = Arc::new(Language::new(
21679 LanguageConfig::default(),
21680 Some(tree_sitter_rust::LANGUAGE.into()),
21681 ));
21682
21683 let text = r#"
21684 #[cfg(test)]
21685 mod tests() {
21686 #[test]
21687 fn runnable_1() {
21688 let a = 1;
21689 }
21690
21691 #[test]
21692 fn runnable_2() {
21693 let a = 1;
21694 let b = 2;
21695 }
21696 }
21697 "#
21698 .unindent();
21699
21700 let fs = FakeFs::new(cx.executor());
21701 fs.insert_file("/file.rs", Default::default()).await;
21702
21703 let project = Project::test(fs, ["/a".as_ref()], cx).await;
21704 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21705 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21706 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
21707 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
21708
21709 let editor = cx.new_window_entity(|window, cx| {
21710 Editor::new(
21711 EditorMode::full(),
21712 multi_buffer,
21713 Some(project.clone()),
21714 window,
21715 cx,
21716 )
21717 });
21718
21719 editor.update_in(cx, |editor, window, cx| {
21720 let snapshot = editor.buffer().read(cx).snapshot(cx);
21721 editor.tasks.insert(
21722 (buffer.read(cx).remote_id(), 3),
21723 RunnableTasks {
21724 templates: vec![],
21725 offset: snapshot.anchor_before(43),
21726 column: 0,
21727 extra_variables: HashMap::default(),
21728 context_range: BufferOffset(43)..BufferOffset(85),
21729 },
21730 );
21731 editor.tasks.insert(
21732 (buffer.read(cx).remote_id(), 8),
21733 RunnableTasks {
21734 templates: vec![],
21735 offset: snapshot.anchor_before(86),
21736 column: 0,
21737 extra_variables: HashMap::default(),
21738 context_range: BufferOffset(86)..BufferOffset(191),
21739 },
21740 );
21741
21742 // Test finding task when cursor is inside function body
21743 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21744 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
21745 });
21746 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
21747 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
21748
21749 // Test finding task when cursor is on function name
21750 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21751 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
21752 });
21753 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
21754 assert_eq!(row, 8, "Should find task when cursor is on function name");
21755 });
21756}
21757
21758#[gpui::test]
21759async fn test_folding_buffers(cx: &mut TestAppContext) {
21760 init_test(cx, |_| {});
21761
21762 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
21763 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
21764 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
21765
21766 let fs = FakeFs::new(cx.executor());
21767 fs.insert_tree(
21768 path!("/a"),
21769 json!({
21770 "first.rs": sample_text_1,
21771 "second.rs": sample_text_2,
21772 "third.rs": sample_text_3,
21773 }),
21774 )
21775 .await;
21776 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21777 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21778 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21779 let worktree = project.update(cx, |project, cx| {
21780 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
21781 assert_eq!(worktrees.len(), 1);
21782 worktrees.pop().unwrap()
21783 });
21784 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
21785
21786 let buffer_1 = project
21787 .update(cx, |project, cx| {
21788 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
21789 })
21790 .await
21791 .unwrap();
21792 let buffer_2 = project
21793 .update(cx, |project, cx| {
21794 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
21795 })
21796 .await
21797 .unwrap();
21798 let buffer_3 = project
21799 .update(cx, |project, cx| {
21800 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
21801 })
21802 .await
21803 .unwrap();
21804
21805 let multi_buffer = cx.new(|cx| {
21806 let mut multi_buffer = MultiBuffer::new(ReadWrite);
21807 multi_buffer.push_excerpts(
21808 buffer_1.clone(),
21809 [
21810 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21811 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21812 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21813 ],
21814 cx,
21815 );
21816 multi_buffer.push_excerpts(
21817 buffer_2.clone(),
21818 [
21819 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21820 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21821 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21822 ],
21823 cx,
21824 );
21825 multi_buffer.push_excerpts(
21826 buffer_3.clone(),
21827 [
21828 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21829 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21830 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21831 ],
21832 cx,
21833 );
21834 multi_buffer
21835 });
21836 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
21837 Editor::new(
21838 EditorMode::full(),
21839 multi_buffer.clone(),
21840 Some(project.clone()),
21841 window,
21842 cx,
21843 )
21844 });
21845
21846 assert_eq!(
21847 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21848 "\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",
21849 );
21850
21851 multi_buffer_editor.update(cx, |editor, cx| {
21852 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
21853 });
21854 assert_eq!(
21855 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21856 "\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",
21857 "After folding the first buffer, its text should not be displayed"
21858 );
21859
21860 multi_buffer_editor.update(cx, |editor, cx| {
21861 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
21862 });
21863 assert_eq!(
21864 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21865 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
21866 "After folding the second buffer, its text should not be displayed"
21867 );
21868
21869 multi_buffer_editor.update(cx, |editor, cx| {
21870 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
21871 });
21872 assert_eq!(
21873 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21874 "\n\n\n\n\n",
21875 "After folding the third buffer, its text should not be displayed"
21876 );
21877
21878 // Emulate selection inside the fold logic, that should work
21879 multi_buffer_editor.update_in(cx, |editor, window, cx| {
21880 editor
21881 .snapshot(window, cx)
21882 .next_line_boundary(Point::new(0, 4));
21883 });
21884
21885 multi_buffer_editor.update(cx, |editor, cx| {
21886 editor.unfold_buffer(buffer_2.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\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
21891 "After unfolding the second buffer, its text should be displayed"
21892 );
21893
21894 // Typing inside of buffer 1 causes that buffer to be unfolded.
21895 multi_buffer_editor.update_in(cx, |editor, window, cx| {
21896 assert_eq!(
21897 multi_buffer
21898 .read(cx)
21899 .snapshot(cx)
21900 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
21901 .collect::<String>(),
21902 "bbbb"
21903 );
21904 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
21905 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
21906 });
21907 editor.handle_input("B", window, cx);
21908 });
21909
21910 assert_eq!(
21911 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21912 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
21913 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
21914 );
21915
21916 multi_buffer_editor.update(cx, |editor, cx| {
21917 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
21918 });
21919 assert_eq!(
21920 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21921 "\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",
21922 "After unfolding the all buffers, all original text should be displayed"
21923 );
21924}
21925
21926#[gpui::test]
21927async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
21928 init_test(cx, |_| {});
21929
21930 let sample_text_1 = "1111\n2222\n3333".to_string();
21931 let sample_text_2 = "4444\n5555\n6666".to_string();
21932 let sample_text_3 = "7777\n8888\n9999".to_string();
21933
21934 let fs = FakeFs::new(cx.executor());
21935 fs.insert_tree(
21936 path!("/a"),
21937 json!({
21938 "first.rs": sample_text_1,
21939 "second.rs": sample_text_2,
21940 "third.rs": sample_text_3,
21941 }),
21942 )
21943 .await;
21944 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21945 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21946 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21947 let worktree = project.update(cx, |project, cx| {
21948 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
21949 assert_eq!(worktrees.len(), 1);
21950 worktrees.pop().unwrap()
21951 });
21952 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
21953
21954 let buffer_1 = project
21955 .update(cx, |project, cx| {
21956 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
21957 })
21958 .await
21959 .unwrap();
21960 let buffer_2 = project
21961 .update(cx, |project, cx| {
21962 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
21963 })
21964 .await
21965 .unwrap();
21966 let buffer_3 = project
21967 .update(cx, |project, cx| {
21968 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
21969 })
21970 .await
21971 .unwrap();
21972
21973 let multi_buffer = cx.new(|cx| {
21974 let mut multi_buffer = MultiBuffer::new(ReadWrite);
21975 multi_buffer.push_excerpts(
21976 buffer_1.clone(),
21977 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
21978 cx,
21979 );
21980 multi_buffer.push_excerpts(
21981 buffer_2.clone(),
21982 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
21983 cx,
21984 );
21985 multi_buffer.push_excerpts(
21986 buffer_3.clone(),
21987 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
21988 cx,
21989 );
21990 multi_buffer
21991 });
21992
21993 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
21994 Editor::new(
21995 EditorMode::full(),
21996 multi_buffer,
21997 Some(project.clone()),
21998 window,
21999 cx,
22000 )
22001 });
22002
22003 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
22004 assert_eq!(
22005 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22006 full_text,
22007 );
22008
22009 multi_buffer_editor.update(cx, |editor, cx| {
22010 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
22011 });
22012 assert_eq!(
22013 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22014 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
22015 "After folding the first buffer, its text should not be displayed"
22016 );
22017
22018 multi_buffer_editor.update(cx, |editor, cx| {
22019 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
22020 });
22021
22022 assert_eq!(
22023 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22024 "\n\n\n\n\n\n7777\n8888\n9999",
22025 "After folding the second buffer, its text should not be displayed"
22026 );
22027
22028 multi_buffer_editor.update(cx, |editor, cx| {
22029 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
22030 });
22031 assert_eq!(
22032 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22033 "\n\n\n\n\n",
22034 "After folding the third buffer, its text should not be displayed"
22035 );
22036
22037 multi_buffer_editor.update(cx, |editor, cx| {
22038 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
22039 });
22040 assert_eq!(
22041 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22042 "\n\n\n\n4444\n5555\n6666\n\n",
22043 "After unfolding the second buffer, its text should be displayed"
22044 );
22045
22046 multi_buffer_editor.update(cx, |editor, cx| {
22047 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
22048 });
22049 assert_eq!(
22050 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22051 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
22052 "After unfolding the first buffer, its text should be displayed"
22053 );
22054
22055 multi_buffer_editor.update(cx, |editor, cx| {
22056 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
22057 });
22058 assert_eq!(
22059 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22060 full_text,
22061 "After unfolding all buffers, all original text should be displayed"
22062 );
22063}
22064
22065#[gpui::test]
22066async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
22067 init_test(cx, |_| {});
22068
22069 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
22070
22071 let fs = FakeFs::new(cx.executor());
22072 fs.insert_tree(
22073 path!("/a"),
22074 json!({
22075 "main.rs": sample_text,
22076 }),
22077 )
22078 .await;
22079 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22080 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22081 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22082 let worktree = project.update(cx, |project, cx| {
22083 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22084 assert_eq!(worktrees.len(), 1);
22085 worktrees.pop().unwrap()
22086 });
22087 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22088
22089 let buffer_1 = project
22090 .update(cx, |project, cx| {
22091 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22092 })
22093 .await
22094 .unwrap();
22095
22096 let multi_buffer = cx.new(|cx| {
22097 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22098 multi_buffer.push_excerpts(
22099 buffer_1.clone(),
22100 [ExcerptRange::new(
22101 Point::new(0, 0)
22102 ..Point::new(
22103 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
22104 0,
22105 ),
22106 )],
22107 cx,
22108 );
22109 multi_buffer
22110 });
22111 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22112 Editor::new(
22113 EditorMode::full(),
22114 multi_buffer,
22115 Some(project.clone()),
22116 window,
22117 cx,
22118 )
22119 });
22120
22121 let selection_range = Point::new(1, 0)..Point::new(2, 0);
22122 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22123 enum TestHighlight {}
22124 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
22125 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
22126 editor.highlight_text::<TestHighlight>(
22127 vec![highlight_range.clone()],
22128 HighlightStyle::color(Hsla::green()),
22129 cx,
22130 );
22131 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22132 s.select_ranges(Some(highlight_range))
22133 });
22134 });
22135
22136 let full_text = format!("\n\n{sample_text}");
22137 assert_eq!(
22138 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22139 full_text,
22140 );
22141}
22142
22143#[gpui::test]
22144async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
22145 init_test(cx, |_| {});
22146 cx.update(|cx| {
22147 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
22148 "keymaps/default-linux.json",
22149 cx,
22150 )
22151 .unwrap();
22152 cx.bind_keys(default_key_bindings);
22153 });
22154
22155 let (editor, cx) = cx.add_window_view(|window, cx| {
22156 let multi_buffer = MultiBuffer::build_multi(
22157 [
22158 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
22159 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
22160 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
22161 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
22162 ],
22163 cx,
22164 );
22165 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
22166
22167 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
22168 // fold all but the second buffer, so that we test navigating between two
22169 // adjacent folded buffers, as well as folded buffers at the start and
22170 // end the multibuffer
22171 editor.fold_buffer(buffer_ids[0], cx);
22172 editor.fold_buffer(buffer_ids[2], cx);
22173 editor.fold_buffer(buffer_ids[3], cx);
22174
22175 editor
22176 });
22177 cx.simulate_resize(size(px(1000.), px(1000.)));
22178
22179 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
22180 cx.assert_excerpts_with_selections(indoc! {"
22181 [EXCERPT]
22182 ˇ[FOLDED]
22183 [EXCERPT]
22184 a1
22185 b1
22186 [EXCERPT]
22187 [FOLDED]
22188 [EXCERPT]
22189 [FOLDED]
22190 "
22191 });
22192 cx.simulate_keystroke("down");
22193 cx.assert_excerpts_with_selections(indoc! {"
22194 [EXCERPT]
22195 [FOLDED]
22196 [EXCERPT]
22197 ˇa1
22198 b1
22199 [EXCERPT]
22200 [FOLDED]
22201 [EXCERPT]
22202 [FOLDED]
22203 "
22204 });
22205 cx.simulate_keystroke("down");
22206 cx.assert_excerpts_with_selections(indoc! {"
22207 [EXCERPT]
22208 [FOLDED]
22209 [EXCERPT]
22210 a1
22211 ˇb1
22212 [EXCERPT]
22213 [FOLDED]
22214 [EXCERPT]
22215 [FOLDED]
22216 "
22217 });
22218 cx.simulate_keystroke("down");
22219 cx.assert_excerpts_with_selections(indoc! {"
22220 [EXCERPT]
22221 [FOLDED]
22222 [EXCERPT]
22223 a1
22224 b1
22225 ˇ[EXCERPT]
22226 [FOLDED]
22227 [EXCERPT]
22228 [FOLDED]
22229 "
22230 });
22231 cx.simulate_keystroke("down");
22232 cx.assert_excerpts_with_selections(indoc! {"
22233 [EXCERPT]
22234 [FOLDED]
22235 [EXCERPT]
22236 a1
22237 b1
22238 [EXCERPT]
22239 ˇ[FOLDED]
22240 [EXCERPT]
22241 [FOLDED]
22242 "
22243 });
22244 for _ in 0..5 {
22245 cx.simulate_keystroke("down");
22246 cx.assert_excerpts_with_selections(indoc! {"
22247 [EXCERPT]
22248 [FOLDED]
22249 [EXCERPT]
22250 a1
22251 b1
22252 [EXCERPT]
22253 [FOLDED]
22254 [EXCERPT]
22255 ˇ[FOLDED]
22256 "
22257 });
22258 }
22259
22260 cx.simulate_keystroke("up");
22261 cx.assert_excerpts_with_selections(indoc! {"
22262 [EXCERPT]
22263 [FOLDED]
22264 [EXCERPT]
22265 a1
22266 b1
22267 [EXCERPT]
22268 ˇ[FOLDED]
22269 [EXCERPT]
22270 [FOLDED]
22271 "
22272 });
22273 cx.simulate_keystroke("up");
22274 cx.assert_excerpts_with_selections(indoc! {"
22275 [EXCERPT]
22276 [FOLDED]
22277 [EXCERPT]
22278 a1
22279 b1
22280 ˇ[EXCERPT]
22281 [FOLDED]
22282 [EXCERPT]
22283 [FOLDED]
22284 "
22285 });
22286 cx.simulate_keystroke("up");
22287 cx.assert_excerpts_with_selections(indoc! {"
22288 [EXCERPT]
22289 [FOLDED]
22290 [EXCERPT]
22291 a1
22292 ˇb1
22293 [EXCERPT]
22294 [FOLDED]
22295 [EXCERPT]
22296 [FOLDED]
22297 "
22298 });
22299 cx.simulate_keystroke("up");
22300 cx.assert_excerpts_with_selections(indoc! {"
22301 [EXCERPT]
22302 [FOLDED]
22303 [EXCERPT]
22304 ˇa1
22305 b1
22306 [EXCERPT]
22307 [FOLDED]
22308 [EXCERPT]
22309 [FOLDED]
22310 "
22311 });
22312 for _ in 0..5 {
22313 cx.simulate_keystroke("up");
22314 cx.assert_excerpts_with_selections(indoc! {"
22315 [EXCERPT]
22316 ˇ[FOLDED]
22317 [EXCERPT]
22318 a1
22319 b1
22320 [EXCERPT]
22321 [FOLDED]
22322 [EXCERPT]
22323 [FOLDED]
22324 "
22325 });
22326 }
22327}
22328
22329#[gpui::test]
22330async fn test_edit_prediction_text(cx: &mut TestAppContext) {
22331 init_test(cx, |_| {});
22332
22333 // Simple insertion
22334 assert_highlighted_edits(
22335 "Hello, world!",
22336 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
22337 true,
22338 cx,
22339 |highlighted_edits, cx| {
22340 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
22341 assert_eq!(highlighted_edits.highlights.len(), 1);
22342 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
22343 assert_eq!(
22344 highlighted_edits.highlights[0].1.background_color,
22345 Some(cx.theme().status().created_background)
22346 );
22347 },
22348 )
22349 .await;
22350
22351 // Replacement
22352 assert_highlighted_edits(
22353 "This is a test.",
22354 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
22355 false,
22356 cx,
22357 |highlighted_edits, cx| {
22358 assert_eq!(highlighted_edits.text, "That is a test.");
22359 assert_eq!(highlighted_edits.highlights.len(), 1);
22360 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
22361 assert_eq!(
22362 highlighted_edits.highlights[0].1.background_color,
22363 Some(cx.theme().status().created_background)
22364 );
22365 },
22366 )
22367 .await;
22368
22369 // Multiple edits
22370 assert_highlighted_edits(
22371 "Hello, world!",
22372 vec![
22373 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
22374 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
22375 ],
22376 false,
22377 cx,
22378 |highlighted_edits, cx| {
22379 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
22380 assert_eq!(highlighted_edits.highlights.len(), 2);
22381 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
22382 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
22383 assert_eq!(
22384 highlighted_edits.highlights[0].1.background_color,
22385 Some(cx.theme().status().created_background)
22386 );
22387 assert_eq!(
22388 highlighted_edits.highlights[1].1.background_color,
22389 Some(cx.theme().status().created_background)
22390 );
22391 },
22392 )
22393 .await;
22394
22395 // Multiple lines with edits
22396 assert_highlighted_edits(
22397 "First line\nSecond line\nThird line\nFourth line",
22398 vec![
22399 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
22400 (
22401 Point::new(2, 0)..Point::new(2, 10),
22402 "New third line".to_string(),
22403 ),
22404 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
22405 ],
22406 false,
22407 cx,
22408 |highlighted_edits, cx| {
22409 assert_eq!(
22410 highlighted_edits.text,
22411 "Second modified\nNew third line\nFourth updated line"
22412 );
22413 assert_eq!(highlighted_edits.highlights.len(), 3);
22414 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
22415 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
22416 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
22417 for highlight in &highlighted_edits.highlights {
22418 assert_eq!(
22419 highlight.1.background_color,
22420 Some(cx.theme().status().created_background)
22421 );
22422 }
22423 },
22424 )
22425 .await;
22426}
22427
22428#[gpui::test]
22429async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
22430 init_test(cx, |_| {});
22431
22432 // Deletion
22433 assert_highlighted_edits(
22434 "Hello, world!",
22435 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
22436 true,
22437 cx,
22438 |highlighted_edits, cx| {
22439 assert_eq!(highlighted_edits.text, "Hello, world!");
22440 assert_eq!(highlighted_edits.highlights.len(), 1);
22441 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
22442 assert_eq!(
22443 highlighted_edits.highlights[0].1.background_color,
22444 Some(cx.theme().status().deleted_background)
22445 );
22446 },
22447 )
22448 .await;
22449
22450 // Insertion
22451 assert_highlighted_edits(
22452 "Hello, world!",
22453 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
22454 true,
22455 cx,
22456 |highlighted_edits, cx| {
22457 assert_eq!(highlighted_edits.highlights.len(), 1);
22458 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
22459 assert_eq!(
22460 highlighted_edits.highlights[0].1.background_color,
22461 Some(cx.theme().status().created_background)
22462 );
22463 },
22464 )
22465 .await;
22466}
22467
22468async fn assert_highlighted_edits(
22469 text: &str,
22470 edits: Vec<(Range<Point>, String)>,
22471 include_deletions: bool,
22472 cx: &mut TestAppContext,
22473 assertion_fn: impl Fn(HighlightedText, &App),
22474) {
22475 let window = cx.add_window(|window, cx| {
22476 let buffer = MultiBuffer::build_simple(text, cx);
22477 Editor::new(EditorMode::full(), buffer, None, window, cx)
22478 });
22479 let cx = &mut VisualTestContext::from_window(*window, cx);
22480
22481 let (buffer, snapshot) = window
22482 .update(cx, |editor, _window, cx| {
22483 (
22484 editor.buffer().clone(),
22485 editor.buffer().read(cx).snapshot(cx),
22486 )
22487 })
22488 .unwrap();
22489
22490 let edits = edits
22491 .into_iter()
22492 .map(|(range, edit)| {
22493 (
22494 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
22495 edit,
22496 )
22497 })
22498 .collect::<Vec<_>>();
22499
22500 let text_anchor_edits = edits
22501 .clone()
22502 .into_iter()
22503 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
22504 .collect::<Vec<_>>();
22505
22506 let edit_preview = window
22507 .update(cx, |_, _window, cx| {
22508 buffer
22509 .read(cx)
22510 .as_singleton()
22511 .unwrap()
22512 .read(cx)
22513 .preview_edits(text_anchor_edits.into(), cx)
22514 })
22515 .unwrap()
22516 .await;
22517
22518 cx.update(|_window, cx| {
22519 let highlighted_edits = edit_prediction_edit_text(
22520 snapshot.as_singleton().unwrap().2,
22521 &edits,
22522 &edit_preview,
22523 include_deletions,
22524 cx,
22525 );
22526 assertion_fn(highlighted_edits, cx)
22527 });
22528}
22529
22530#[track_caller]
22531fn assert_breakpoint(
22532 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
22533 path: &Arc<Path>,
22534 expected: Vec<(u32, Breakpoint)>,
22535) {
22536 if expected.is_empty() {
22537 assert!(!breakpoints.contains_key(path), "{}", path.display());
22538 } else {
22539 let mut breakpoint = breakpoints
22540 .get(path)
22541 .unwrap()
22542 .iter()
22543 .map(|breakpoint| {
22544 (
22545 breakpoint.row,
22546 Breakpoint {
22547 message: breakpoint.message.clone(),
22548 state: breakpoint.state,
22549 condition: breakpoint.condition.clone(),
22550 hit_condition: breakpoint.hit_condition.clone(),
22551 },
22552 )
22553 })
22554 .collect::<Vec<_>>();
22555
22556 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
22557
22558 assert_eq!(expected, breakpoint);
22559 }
22560}
22561
22562fn add_log_breakpoint_at_cursor(
22563 editor: &mut Editor,
22564 log_message: &str,
22565 window: &mut Window,
22566 cx: &mut Context<Editor>,
22567) {
22568 let (anchor, bp) = editor
22569 .breakpoints_at_cursors(window, cx)
22570 .first()
22571 .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
22572 .unwrap_or_else(|| {
22573 let cursor_position: Point = editor.selections.newest(cx).head();
22574
22575 let breakpoint_position = editor
22576 .snapshot(window, cx)
22577 .display_snapshot
22578 .buffer_snapshot()
22579 .anchor_before(Point::new(cursor_position.row, 0));
22580
22581 (breakpoint_position, Breakpoint::new_log(log_message))
22582 });
22583
22584 editor.edit_breakpoint_at_anchor(
22585 anchor,
22586 bp,
22587 BreakpointEditAction::EditLogMessage(log_message.into()),
22588 cx,
22589 );
22590}
22591
22592#[gpui::test]
22593async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
22594 init_test(cx, |_| {});
22595
22596 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22597 let fs = FakeFs::new(cx.executor());
22598 fs.insert_tree(
22599 path!("/a"),
22600 json!({
22601 "main.rs": sample_text,
22602 }),
22603 )
22604 .await;
22605 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22606 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22607 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22608
22609 let fs = FakeFs::new(cx.executor());
22610 fs.insert_tree(
22611 path!("/a"),
22612 json!({
22613 "main.rs": sample_text,
22614 }),
22615 )
22616 .await;
22617 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22618 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22619 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22620 let worktree_id = workspace
22621 .update(cx, |workspace, _window, cx| {
22622 workspace.project().update(cx, |project, cx| {
22623 project.worktrees(cx).next().unwrap().read(cx).id()
22624 })
22625 })
22626 .unwrap();
22627
22628 let buffer = project
22629 .update(cx, |project, cx| {
22630 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22631 })
22632 .await
22633 .unwrap();
22634
22635 let (editor, cx) = cx.add_window_view(|window, cx| {
22636 Editor::new(
22637 EditorMode::full(),
22638 MultiBuffer::build_from_buffer(buffer, cx),
22639 Some(project.clone()),
22640 window,
22641 cx,
22642 )
22643 });
22644
22645 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22646 let abs_path = project.read_with(cx, |project, cx| {
22647 project
22648 .absolute_path(&project_path, cx)
22649 .map(Arc::from)
22650 .unwrap()
22651 });
22652
22653 // assert we can add breakpoint on the first line
22654 editor.update_in(cx, |editor, window, cx| {
22655 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22656 editor.move_to_end(&MoveToEnd, window, cx);
22657 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22658 });
22659
22660 let breakpoints = editor.update(cx, |editor, cx| {
22661 editor
22662 .breakpoint_store()
22663 .as_ref()
22664 .unwrap()
22665 .read(cx)
22666 .all_source_breakpoints(cx)
22667 });
22668
22669 assert_eq!(1, breakpoints.len());
22670 assert_breakpoint(
22671 &breakpoints,
22672 &abs_path,
22673 vec![
22674 (0, Breakpoint::new_standard()),
22675 (3, Breakpoint::new_standard()),
22676 ],
22677 );
22678
22679 editor.update_in(cx, |editor, window, cx| {
22680 editor.move_to_beginning(&MoveToBeginning, window, cx);
22681 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22682 });
22683
22684 let breakpoints = editor.update(cx, |editor, cx| {
22685 editor
22686 .breakpoint_store()
22687 .as_ref()
22688 .unwrap()
22689 .read(cx)
22690 .all_source_breakpoints(cx)
22691 });
22692
22693 assert_eq!(1, breakpoints.len());
22694 assert_breakpoint(
22695 &breakpoints,
22696 &abs_path,
22697 vec![(3, Breakpoint::new_standard())],
22698 );
22699
22700 editor.update_in(cx, |editor, window, cx| {
22701 editor.move_to_end(&MoveToEnd, window, cx);
22702 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22703 });
22704
22705 let breakpoints = editor.update(cx, |editor, cx| {
22706 editor
22707 .breakpoint_store()
22708 .as_ref()
22709 .unwrap()
22710 .read(cx)
22711 .all_source_breakpoints(cx)
22712 });
22713
22714 assert_eq!(0, breakpoints.len());
22715 assert_breakpoint(&breakpoints, &abs_path, vec![]);
22716}
22717
22718#[gpui::test]
22719async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
22720 init_test(cx, |_| {});
22721
22722 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22723
22724 let fs = FakeFs::new(cx.executor());
22725 fs.insert_tree(
22726 path!("/a"),
22727 json!({
22728 "main.rs": sample_text,
22729 }),
22730 )
22731 .await;
22732 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22733 let (workspace, cx) =
22734 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
22735
22736 let worktree_id = workspace.update(cx, |workspace, cx| {
22737 workspace.project().update(cx, |project, cx| {
22738 project.worktrees(cx).next().unwrap().read(cx).id()
22739 })
22740 });
22741
22742 let buffer = project
22743 .update(cx, |project, cx| {
22744 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22745 })
22746 .await
22747 .unwrap();
22748
22749 let (editor, cx) = cx.add_window_view(|window, cx| {
22750 Editor::new(
22751 EditorMode::full(),
22752 MultiBuffer::build_from_buffer(buffer, cx),
22753 Some(project.clone()),
22754 window,
22755 cx,
22756 )
22757 });
22758
22759 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22760 let abs_path = project.read_with(cx, |project, cx| {
22761 project
22762 .absolute_path(&project_path, cx)
22763 .map(Arc::from)
22764 .unwrap()
22765 });
22766
22767 editor.update_in(cx, |editor, window, cx| {
22768 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
22769 });
22770
22771 let breakpoints = editor.update(cx, |editor, cx| {
22772 editor
22773 .breakpoint_store()
22774 .as_ref()
22775 .unwrap()
22776 .read(cx)
22777 .all_source_breakpoints(cx)
22778 });
22779
22780 assert_breakpoint(
22781 &breakpoints,
22782 &abs_path,
22783 vec![(0, Breakpoint::new_log("hello world"))],
22784 );
22785
22786 // Removing a log message from a log breakpoint should remove it
22787 editor.update_in(cx, |editor, window, cx| {
22788 add_log_breakpoint_at_cursor(editor, "", window, cx);
22789 });
22790
22791 let breakpoints = editor.update(cx, |editor, cx| {
22792 editor
22793 .breakpoint_store()
22794 .as_ref()
22795 .unwrap()
22796 .read(cx)
22797 .all_source_breakpoints(cx)
22798 });
22799
22800 assert_breakpoint(&breakpoints, &abs_path, vec![]);
22801
22802 editor.update_in(cx, |editor, window, cx| {
22803 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22804 editor.move_to_end(&MoveToEnd, window, cx);
22805 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22806 // Not adding a log message to a standard breakpoint shouldn't remove it
22807 add_log_breakpoint_at_cursor(editor, "", window, cx);
22808 });
22809
22810 let breakpoints = editor.update(cx, |editor, cx| {
22811 editor
22812 .breakpoint_store()
22813 .as_ref()
22814 .unwrap()
22815 .read(cx)
22816 .all_source_breakpoints(cx)
22817 });
22818
22819 assert_breakpoint(
22820 &breakpoints,
22821 &abs_path,
22822 vec![
22823 (0, Breakpoint::new_standard()),
22824 (3, Breakpoint::new_standard()),
22825 ],
22826 );
22827
22828 editor.update_in(cx, |editor, window, cx| {
22829 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
22830 });
22831
22832 let breakpoints = editor.update(cx, |editor, cx| {
22833 editor
22834 .breakpoint_store()
22835 .as_ref()
22836 .unwrap()
22837 .read(cx)
22838 .all_source_breakpoints(cx)
22839 });
22840
22841 assert_breakpoint(
22842 &breakpoints,
22843 &abs_path,
22844 vec![
22845 (0, Breakpoint::new_standard()),
22846 (3, Breakpoint::new_log("hello world")),
22847 ],
22848 );
22849
22850 editor.update_in(cx, |editor, window, cx| {
22851 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
22852 });
22853
22854 let breakpoints = editor.update(cx, |editor, cx| {
22855 editor
22856 .breakpoint_store()
22857 .as_ref()
22858 .unwrap()
22859 .read(cx)
22860 .all_source_breakpoints(cx)
22861 });
22862
22863 assert_breakpoint(
22864 &breakpoints,
22865 &abs_path,
22866 vec![
22867 (0, Breakpoint::new_standard()),
22868 (3, Breakpoint::new_log("hello Earth!!")),
22869 ],
22870 );
22871}
22872
22873/// This also tests that Editor::breakpoint_at_cursor_head is working properly
22874/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
22875/// or when breakpoints were placed out of order. This tests for a regression too
22876#[gpui::test]
22877async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
22878 init_test(cx, |_| {});
22879
22880 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22881 let fs = FakeFs::new(cx.executor());
22882 fs.insert_tree(
22883 path!("/a"),
22884 json!({
22885 "main.rs": sample_text,
22886 }),
22887 )
22888 .await;
22889 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22890 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22891 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22892
22893 let fs = FakeFs::new(cx.executor());
22894 fs.insert_tree(
22895 path!("/a"),
22896 json!({
22897 "main.rs": sample_text,
22898 }),
22899 )
22900 .await;
22901 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22902 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22903 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22904 let worktree_id = workspace
22905 .update(cx, |workspace, _window, cx| {
22906 workspace.project().update(cx, |project, cx| {
22907 project.worktrees(cx).next().unwrap().read(cx).id()
22908 })
22909 })
22910 .unwrap();
22911
22912 let buffer = project
22913 .update(cx, |project, cx| {
22914 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22915 })
22916 .await
22917 .unwrap();
22918
22919 let (editor, cx) = cx.add_window_view(|window, cx| {
22920 Editor::new(
22921 EditorMode::full(),
22922 MultiBuffer::build_from_buffer(buffer, cx),
22923 Some(project.clone()),
22924 window,
22925 cx,
22926 )
22927 });
22928
22929 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22930 let abs_path = project.read_with(cx, |project, cx| {
22931 project
22932 .absolute_path(&project_path, cx)
22933 .map(Arc::from)
22934 .unwrap()
22935 });
22936
22937 // assert we can add breakpoint on the first line
22938 editor.update_in(cx, |editor, window, cx| {
22939 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22940 editor.move_to_end(&MoveToEnd, window, cx);
22941 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22942 editor.move_up(&MoveUp, window, cx);
22943 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22944 });
22945
22946 let breakpoints = editor.update(cx, |editor, cx| {
22947 editor
22948 .breakpoint_store()
22949 .as_ref()
22950 .unwrap()
22951 .read(cx)
22952 .all_source_breakpoints(cx)
22953 });
22954
22955 assert_eq!(1, breakpoints.len());
22956 assert_breakpoint(
22957 &breakpoints,
22958 &abs_path,
22959 vec![
22960 (0, Breakpoint::new_standard()),
22961 (2, Breakpoint::new_standard()),
22962 (3, Breakpoint::new_standard()),
22963 ],
22964 );
22965
22966 editor.update_in(cx, |editor, window, cx| {
22967 editor.move_to_beginning(&MoveToBeginning, window, cx);
22968 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22969 editor.move_to_end(&MoveToEnd, window, cx);
22970 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22971 // Disabling a breakpoint that doesn't exist should do nothing
22972 editor.move_up(&MoveUp, window, cx);
22973 editor.move_up(&MoveUp, window, cx);
22974 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22975 });
22976
22977 let breakpoints = editor.update(cx, |editor, cx| {
22978 editor
22979 .breakpoint_store()
22980 .as_ref()
22981 .unwrap()
22982 .read(cx)
22983 .all_source_breakpoints(cx)
22984 });
22985
22986 let disable_breakpoint = {
22987 let mut bp = Breakpoint::new_standard();
22988 bp.state = BreakpointState::Disabled;
22989 bp
22990 };
22991
22992 assert_eq!(1, breakpoints.len());
22993 assert_breakpoint(
22994 &breakpoints,
22995 &abs_path,
22996 vec![
22997 (0, disable_breakpoint.clone()),
22998 (2, Breakpoint::new_standard()),
22999 (3, disable_breakpoint.clone()),
23000 ],
23001 );
23002
23003 editor.update_in(cx, |editor, window, cx| {
23004 editor.move_to_beginning(&MoveToBeginning, window, cx);
23005 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
23006 editor.move_to_end(&MoveToEnd, window, cx);
23007 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
23008 editor.move_up(&MoveUp, window, cx);
23009 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23010 });
23011
23012 let breakpoints = editor.update(cx, |editor, cx| {
23013 editor
23014 .breakpoint_store()
23015 .as_ref()
23016 .unwrap()
23017 .read(cx)
23018 .all_source_breakpoints(cx)
23019 });
23020
23021 assert_eq!(1, breakpoints.len());
23022 assert_breakpoint(
23023 &breakpoints,
23024 &abs_path,
23025 vec![
23026 (0, Breakpoint::new_standard()),
23027 (2, disable_breakpoint),
23028 (3, Breakpoint::new_standard()),
23029 ],
23030 );
23031}
23032
23033#[gpui::test]
23034async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
23035 init_test(cx, |_| {});
23036 let capabilities = lsp::ServerCapabilities {
23037 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
23038 prepare_provider: Some(true),
23039 work_done_progress_options: Default::default(),
23040 })),
23041 ..Default::default()
23042 };
23043 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
23044
23045 cx.set_state(indoc! {"
23046 struct Fˇoo {}
23047 "});
23048
23049 cx.update_editor(|editor, _, cx| {
23050 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
23051 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
23052 editor.highlight_background::<DocumentHighlightRead>(
23053 &[highlight_range],
23054 |theme| theme.colors().editor_document_highlight_read_background,
23055 cx,
23056 );
23057 });
23058
23059 let mut prepare_rename_handler = cx
23060 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
23061 move |_, _, _| async move {
23062 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
23063 start: lsp::Position {
23064 line: 0,
23065 character: 7,
23066 },
23067 end: lsp::Position {
23068 line: 0,
23069 character: 10,
23070 },
23071 })))
23072 },
23073 );
23074 let prepare_rename_task = cx
23075 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23076 .expect("Prepare rename was not started");
23077 prepare_rename_handler.next().await.unwrap();
23078 prepare_rename_task.await.expect("Prepare rename failed");
23079
23080 let mut rename_handler =
23081 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23082 let edit = lsp::TextEdit {
23083 range: lsp::Range {
23084 start: lsp::Position {
23085 line: 0,
23086 character: 7,
23087 },
23088 end: lsp::Position {
23089 line: 0,
23090 character: 10,
23091 },
23092 },
23093 new_text: "FooRenamed".to_string(),
23094 };
23095 Ok(Some(lsp::WorkspaceEdit::new(
23096 // Specify the same edit twice
23097 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
23098 )))
23099 });
23100 let rename_task = cx
23101 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23102 .expect("Confirm rename was not started");
23103 rename_handler.next().await.unwrap();
23104 rename_task.await.expect("Confirm rename failed");
23105 cx.run_until_parked();
23106
23107 // Despite two edits, only one is actually applied as those are identical
23108 cx.assert_editor_state(indoc! {"
23109 struct FooRenamedˇ {}
23110 "});
23111}
23112
23113#[gpui::test]
23114async fn test_rename_without_prepare(cx: &mut TestAppContext) {
23115 init_test(cx, |_| {});
23116 // These capabilities indicate that the server does not support prepare rename.
23117 let capabilities = lsp::ServerCapabilities {
23118 rename_provider: Some(lsp::OneOf::Left(true)),
23119 ..Default::default()
23120 };
23121 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
23122
23123 cx.set_state(indoc! {"
23124 struct Fˇoo {}
23125 "});
23126
23127 cx.update_editor(|editor, _window, cx| {
23128 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
23129 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
23130 editor.highlight_background::<DocumentHighlightRead>(
23131 &[highlight_range],
23132 |theme| theme.colors().editor_document_highlight_read_background,
23133 cx,
23134 );
23135 });
23136
23137 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23138 .expect("Prepare rename was not started")
23139 .await
23140 .expect("Prepare rename failed");
23141
23142 let mut rename_handler =
23143 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23144 let edit = lsp::TextEdit {
23145 range: lsp::Range {
23146 start: lsp::Position {
23147 line: 0,
23148 character: 7,
23149 },
23150 end: lsp::Position {
23151 line: 0,
23152 character: 10,
23153 },
23154 },
23155 new_text: "FooRenamed".to_string(),
23156 };
23157 Ok(Some(lsp::WorkspaceEdit::new(
23158 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
23159 )))
23160 });
23161 let rename_task = cx
23162 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23163 .expect("Confirm rename was not started");
23164 rename_handler.next().await.unwrap();
23165 rename_task.await.expect("Confirm rename failed");
23166 cx.run_until_parked();
23167
23168 // Correct range is renamed, as `surrounding_word` is used to find it.
23169 cx.assert_editor_state(indoc! {"
23170 struct FooRenamedˇ {}
23171 "});
23172}
23173
23174#[gpui::test]
23175async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
23176 init_test(cx, |_| {});
23177 let mut cx = EditorTestContext::new(cx).await;
23178
23179 let language = Arc::new(
23180 Language::new(
23181 LanguageConfig::default(),
23182 Some(tree_sitter_html::LANGUAGE.into()),
23183 )
23184 .with_brackets_query(
23185 r#"
23186 ("<" @open "/>" @close)
23187 ("</" @open ">" @close)
23188 ("<" @open ">" @close)
23189 ("\"" @open "\"" @close)
23190 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
23191 "#,
23192 )
23193 .unwrap(),
23194 );
23195 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23196
23197 cx.set_state(indoc! {"
23198 <span>ˇ</span>
23199 "});
23200 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23201 cx.assert_editor_state(indoc! {"
23202 <span>
23203 ˇ
23204 </span>
23205 "});
23206
23207 cx.set_state(indoc! {"
23208 <span><span></span>ˇ</span>
23209 "});
23210 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23211 cx.assert_editor_state(indoc! {"
23212 <span><span></span>
23213 ˇ</span>
23214 "});
23215
23216 cx.set_state(indoc! {"
23217 <span>ˇ
23218 </span>
23219 "});
23220 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23221 cx.assert_editor_state(indoc! {"
23222 <span>
23223 ˇ
23224 </span>
23225 "});
23226}
23227
23228#[gpui::test(iterations = 10)]
23229async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
23230 init_test(cx, |_| {});
23231
23232 let fs = FakeFs::new(cx.executor());
23233 fs.insert_tree(
23234 path!("/dir"),
23235 json!({
23236 "a.ts": "a",
23237 }),
23238 )
23239 .await;
23240
23241 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
23242 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23243 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23244
23245 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23246 language_registry.add(Arc::new(Language::new(
23247 LanguageConfig {
23248 name: "TypeScript".into(),
23249 matcher: LanguageMatcher {
23250 path_suffixes: vec!["ts".to_string()],
23251 ..Default::default()
23252 },
23253 ..Default::default()
23254 },
23255 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
23256 )));
23257 let mut fake_language_servers = language_registry.register_fake_lsp(
23258 "TypeScript",
23259 FakeLspAdapter {
23260 capabilities: lsp::ServerCapabilities {
23261 code_lens_provider: Some(lsp::CodeLensOptions {
23262 resolve_provider: Some(true),
23263 }),
23264 execute_command_provider: Some(lsp::ExecuteCommandOptions {
23265 commands: vec!["_the/command".to_string()],
23266 ..lsp::ExecuteCommandOptions::default()
23267 }),
23268 ..lsp::ServerCapabilities::default()
23269 },
23270 ..FakeLspAdapter::default()
23271 },
23272 );
23273
23274 let editor = workspace
23275 .update(cx, |workspace, window, cx| {
23276 workspace.open_abs_path(
23277 PathBuf::from(path!("/dir/a.ts")),
23278 OpenOptions::default(),
23279 window,
23280 cx,
23281 )
23282 })
23283 .unwrap()
23284 .await
23285 .unwrap()
23286 .downcast::<Editor>()
23287 .unwrap();
23288 cx.executor().run_until_parked();
23289
23290 let fake_server = fake_language_servers.next().await.unwrap();
23291
23292 let buffer = editor.update(cx, |editor, cx| {
23293 editor
23294 .buffer()
23295 .read(cx)
23296 .as_singleton()
23297 .expect("have opened a single file by path")
23298 });
23299
23300 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
23301 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
23302 drop(buffer_snapshot);
23303 let actions = cx
23304 .update_window(*workspace, |_, window, cx| {
23305 project.code_actions(&buffer, anchor..anchor, window, cx)
23306 })
23307 .unwrap();
23308
23309 fake_server
23310 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23311 Ok(Some(vec![
23312 lsp::CodeLens {
23313 range: lsp::Range::default(),
23314 command: Some(lsp::Command {
23315 title: "Code lens command".to_owned(),
23316 command: "_the/command".to_owned(),
23317 arguments: None,
23318 }),
23319 data: None,
23320 },
23321 lsp::CodeLens {
23322 range: lsp::Range::default(),
23323 command: Some(lsp::Command {
23324 title: "Command not in capabilities".to_owned(),
23325 command: "not in capabilities".to_owned(),
23326 arguments: None,
23327 }),
23328 data: None,
23329 },
23330 lsp::CodeLens {
23331 range: lsp::Range {
23332 start: lsp::Position {
23333 line: 1,
23334 character: 1,
23335 },
23336 end: lsp::Position {
23337 line: 1,
23338 character: 1,
23339 },
23340 },
23341 command: Some(lsp::Command {
23342 title: "Command not in range".to_owned(),
23343 command: "_the/command".to_owned(),
23344 arguments: None,
23345 }),
23346 data: None,
23347 },
23348 ]))
23349 })
23350 .next()
23351 .await;
23352
23353 let actions = actions.await.unwrap();
23354 assert_eq!(
23355 actions.len(),
23356 1,
23357 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
23358 );
23359 let action = actions[0].clone();
23360 let apply = project.update(cx, |project, cx| {
23361 project.apply_code_action(buffer.clone(), action, true, cx)
23362 });
23363
23364 // Resolving the code action does not populate its edits. In absence of
23365 // edits, we must execute the given command.
23366 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
23367 |mut lens, _| async move {
23368 let lens_command = lens.command.as_mut().expect("should have a command");
23369 assert_eq!(lens_command.title, "Code lens command");
23370 lens_command.arguments = Some(vec![json!("the-argument")]);
23371 Ok(lens)
23372 },
23373 );
23374
23375 // While executing the command, the language server sends the editor
23376 // a `workspaceEdit` request.
23377 fake_server
23378 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
23379 let fake = fake_server.clone();
23380 move |params, _| {
23381 assert_eq!(params.command, "_the/command");
23382 let fake = fake.clone();
23383 async move {
23384 fake.server
23385 .request::<lsp::request::ApplyWorkspaceEdit>(
23386 lsp::ApplyWorkspaceEditParams {
23387 label: None,
23388 edit: lsp::WorkspaceEdit {
23389 changes: Some(
23390 [(
23391 lsp::Uri::from_file_path(path!("/dir/a.ts")).unwrap(),
23392 vec![lsp::TextEdit {
23393 range: lsp::Range::new(
23394 lsp::Position::new(0, 0),
23395 lsp::Position::new(0, 0),
23396 ),
23397 new_text: "X".into(),
23398 }],
23399 )]
23400 .into_iter()
23401 .collect(),
23402 ),
23403 ..lsp::WorkspaceEdit::default()
23404 },
23405 },
23406 )
23407 .await
23408 .into_response()
23409 .unwrap();
23410 Ok(Some(json!(null)))
23411 }
23412 }
23413 })
23414 .next()
23415 .await;
23416
23417 // Applying the code lens command returns a project transaction containing the edits
23418 // sent by the language server in its `workspaceEdit` request.
23419 let transaction = apply.await.unwrap();
23420 assert!(transaction.0.contains_key(&buffer));
23421 buffer.update(cx, |buffer, cx| {
23422 assert_eq!(buffer.text(), "Xa");
23423 buffer.undo(cx);
23424 assert_eq!(buffer.text(), "a");
23425 });
23426
23427 let actions_after_edits = cx
23428 .update_window(*workspace, |_, window, cx| {
23429 project.code_actions(&buffer, anchor..anchor, window, cx)
23430 })
23431 .unwrap()
23432 .await
23433 .unwrap();
23434 assert_eq!(
23435 actions, actions_after_edits,
23436 "For the same selection, same code lens actions should be returned"
23437 );
23438
23439 let _responses =
23440 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23441 panic!("No more code lens requests are expected");
23442 });
23443 editor.update_in(cx, |editor, window, cx| {
23444 editor.select_all(&SelectAll, window, cx);
23445 });
23446 cx.executor().run_until_parked();
23447 let new_actions = cx
23448 .update_window(*workspace, |_, window, cx| {
23449 project.code_actions(&buffer, anchor..anchor, window, cx)
23450 })
23451 .unwrap()
23452 .await
23453 .unwrap();
23454 assert_eq!(
23455 actions, new_actions,
23456 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
23457 );
23458}
23459
23460#[gpui::test]
23461async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
23462 init_test(cx, |_| {});
23463
23464 let fs = FakeFs::new(cx.executor());
23465 let main_text = r#"fn main() {
23466println!("1");
23467println!("2");
23468println!("3");
23469println!("4");
23470println!("5");
23471}"#;
23472 let lib_text = "mod foo {}";
23473 fs.insert_tree(
23474 path!("/a"),
23475 json!({
23476 "lib.rs": lib_text,
23477 "main.rs": main_text,
23478 }),
23479 )
23480 .await;
23481
23482 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23483 let (workspace, cx) =
23484 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23485 let worktree_id = workspace.update(cx, |workspace, cx| {
23486 workspace.project().update(cx, |project, cx| {
23487 project.worktrees(cx).next().unwrap().read(cx).id()
23488 })
23489 });
23490
23491 let expected_ranges = vec![
23492 Point::new(0, 0)..Point::new(0, 0),
23493 Point::new(1, 0)..Point::new(1, 1),
23494 Point::new(2, 0)..Point::new(2, 2),
23495 Point::new(3, 0)..Point::new(3, 3),
23496 ];
23497
23498 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23499 let editor_1 = workspace
23500 .update_in(cx, |workspace, window, cx| {
23501 workspace.open_path(
23502 (worktree_id, rel_path("main.rs")),
23503 Some(pane_1.downgrade()),
23504 true,
23505 window,
23506 cx,
23507 )
23508 })
23509 .unwrap()
23510 .await
23511 .downcast::<Editor>()
23512 .unwrap();
23513 pane_1.update(cx, |pane, cx| {
23514 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23515 open_editor.update(cx, |editor, cx| {
23516 assert_eq!(
23517 editor.display_text(cx),
23518 main_text,
23519 "Original main.rs text on initial open",
23520 );
23521 assert_eq!(
23522 editor
23523 .selections
23524 .all::<Point>(cx)
23525 .into_iter()
23526 .map(|s| s.range())
23527 .collect::<Vec<_>>(),
23528 vec![Point::zero()..Point::zero()],
23529 "Default selections on initial open",
23530 );
23531 })
23532 });
23533 editor_1.update_in(cx, |editor, window, cx| {
23534 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23535 s.select_ranges(expected_ranges.clone());
23536 });
23537 });
23538
23539 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
23540 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
23541 });
23542 let editor_2 = workspace
23543 .update_in(cx, |workspace, window, cx| {
23544 workspace.open_path(
23545 (worktree_id, rel_path("main.rs")),
23546 Some(pane_2.downgrade()),
23547 true,
23548 window,
23549 cx,
23550 )
23551 })
23552 .unwrap()
23553 .await
23554 .downcast::<Editor>()
23555 .unwrap();
23556 pane_2.update(cx, |pane, cx| {
23557 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23558 open_editor.update(cx, |editor, cx| {
23559 assert_eq!(
23560 editor.display_text(cx),
23561 main_text,
23562 "Original main.rs text on initial open in another panel",
23563 );
23564 assert_eq!(
23565 editor
23566 .selections
23567 .all::<Point>(cx)
23568 .into_iter()
23569 .map(|s| s.range())
23570 .collect::<Vec<_>>(),
23571 vec![Point::zero()..Point::zero()],
23572 "Default selections on initial open in another panel",
23573 );
23574 })
23575 });
23576
23577 editor_2.update_in(cx, |editor, window, cx| {
23578 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
23579 });
23580
23581 let _other_editor_1 = workspace
23582 .update_in(cx, |workspace, window, cx| {
23583 workspace.open_path(
23584 (worktree_id, rel_path("lib.rs")),
23585 Some(pane_1.downgrade()),
23586 true,
23587 window,
23588 cx,
23589 )
23590 })
23591 .unwrap()
23592 .await
23593 .downcast::<Editor>()
23594 .unwrap();
23595 pane_1
23596 .update_in(cx, |pane, window, cx| {
23597 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23598 })
23599 .await
23600 .unwrap();
23601 drop(editor_1);
23602 pane_1.update(cx, |pane, cx| {
23603 pane.active_item()
23604 .unwrap()
23605 .downcast::<Editor>()
23606 .unwrap()
23607 .update(cx, |editor, cx| {
23608 assert_eq!(
23609 editor.display_text(cx),
23610 lib_text,
23611 "Other file should be open and active",
23612 );
23613 });
23614 assert_eq!(pane.items().count(), 1, "No other editors should be open");
23615 });
23616
23617 let _other_editor_2 = workspace
23618 .update_in(cx, |workspace, window, cx| {
23619 workspace.open_path(
23620 (worktree_id, rel_path("lib.rs")),
23621 Some(pane_2.downgrade()),
23622 true,
23623 window,
23624 cx,
23625 )
23626 })
23627 .unwrap()
23628 .await
23629 .downcast::<Editor>()
23630 .unwrap();
23631 pane_2
23632 .update_in(cx, |pane, window, cx| {
23633 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23634 })
23635 .await
23636 .unwrap();
23637 drop(editor_2);
23638 pane_2.update(cx, |pane, cx| {
23639 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23640 open_editor.update(cx, |editor, cx| {
23641 assert_eq!(
23642 editor.display_text(cx),
23643 lib_text,
23644 "Other file should be open and active in another panel too",
23645 );
23646 });
23647 assert_eq!(
23648 pane.items().count(),
23649 1,
23650 "No other editors should be open in another pane",
23651 );
23652 });
23653
23654 let _editor_1_reopened = workspace
23655 .update_in(cx, |workspace, window, cx| {
23656 workspace.open_path(
23657 (worktree_id, rel_path("main.rs")),
23658 Some(pane_1.downgrade()),
23659 true,
23660 window,
23661 cx,
23662 )
23663 })
23664 .unwrap()
23665 .await
23666 .downcast::<Editor>()
23667 .unwrap();
23668 let _editor_2_reopened = workspace
23669 .update_in(cx, |workspace, window, cx| {
23670 workspace.open_path(
23671 (worktree_id, rel_path("main.rs")),
23672 Some(pane_2.downgrade()),
23673 true,
23674 window,
23675 cx,
23676 )
23677 })
23678 .unwrap()
23679 .await
23680 .downcast::<Editor>()
23681 .unwrap();
23682 pane_1.update(cx, |pane, cx| {
23683 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23684 open_editor.update(cx, |editor, cx| {
23685 assert_eq!(
23686 editor.display_text(cx),
23687 main_text,
23688 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
23689 );
23690 assert_eq!(
23691 editor
23692 .selections
23693 .all::<Point>(cx)
23694 .into_iter()
23695 .map(|s| s.range())
23696 .collect::<Vec<_>>(),
23697 expected_ranges,
23698 "Previous editor in the 1st panel had selections and should get them restored on reopen",
23699 );
23700 })
23701 });
23702 pane_2.update(cx, |pane, cx| {
23703 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23704 open_editor.update(cx, |editor, cx| {
23705 assert_eq!(
23706 editor.display_text(cx),
23707 r#"fn main() {
23708⋯rintln!("1");
23709⋯intln!("2");
23710⋯ntln!("3");
23711println!("4");
23712println!("5");
23713}"#,
23714 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
23715 );
23716 assert_eq!(
23717 editor
23718 .selections
23719 .all::<Point>(cx)
23720 .into_iter()
23721 .map(|s| s.range())
23722 .collect::<Vec<_>>(),
23723 vec![Point::zero()..Point::zero()],
23724 "Previous editor in the 2nd pane had no selections changed hence should restore none",
23725 );
23726 })
23727 });
23728}
23729
23730#[gpui::test]
23731async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
23732 init_test(cx, |_| {});
23733
23734 let fs = FakeFs::new(cx.executor());
23735 let main_text = r#"fn main() {
23736println!("1");
23737println!("2");
23738println!("3");
23739println!("4");
23740println!("5");
23741}"#;
23742 let lib_text = "mod foo {}";
23743 fs.insert_tree(
23744 path!("/a"),
23745 json!({
23746 "lib.rs": lib_text,
23747 "main.rs": main_text,
23748 }),
23749 )
23750 .await;
23751
23752 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23753 let (workspace, cx) =
23754 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23755 let worktree_id = workspace.update(cx, |workspace, cx| {
23756 workspace.project().update(cx, |project, cx| {
23757 project.worktrees(cx).next().unwrap().read(cx).id()
23758 })
23759 });
23760
23761 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23762 let editor = workspace
23763 .update_in(cx, |workspace, window, cx| {
23764 workspace.open_path(
23765 (worktree_id, rel_path("main.rs")),
23766 Some(pane.downgrade()),
23767 true,
23768 window,
23769 cx,
23770 )
23771 })
23772 .unwrap()
23773 .await
23774 .downcast::<Editor>()
23775 .unwrap();
23776 pane.update(cx, |pane, cx| {
23777 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23778 open_editor.update(cx, |editor, cx| {
23779 assert_eq!(
23780 editor.display_text(cx),
23781 main_text,
23782 "Original main.rs text on initial open",
23783 );
23784 })
23785 });
23786 editor.update_in(cx, |editor, window, cx| {
23787 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
23788 });
23789
23790 cx.update_global(|store: &mut SettingsStore, cx| {
23791 store.update_user_settings(cx, |s| {
23792 s.workspace.restore_on_file_reopen = Some(false);
23793 });
23794 });
23795 editor.update_in(cx, |editor, window, cx| {
23796 editor.fold_ranges(
23797 vec![
23798 Point::new(1, 0)..Point::new(1, 1),
23799 Point::new(2, 0)..Point::new(2, 2),
23800 Point::new(3, 0)..Point::new(3, 3),
23801 ],
23802 false,
23803 window,
23804 cx,
23805 );
23806 });
23807 pane.update_in(cx, |pane, window, cx| {
23808 pane.close_all_items(&CloseAllItems::default(), window, cx)
23809 })
23810 .await
23811 .unwrap();
23812 pane.update(cx, |pane, _| {
23813 assert!(pane.active_item().is_none());
23814 });
23815 cx.update_global(|store: &mut SettingsStore, cx| {
23816 store.update_user_settings(cx, |s| {
23817 s.workspace.restore_on_file_reopen = Some(true);
23818 });
23819 });
23820
23821 let _editor_reopened = workspace
23822 .update_in(cx, |workspace, window, cx| {
23823 workspace.open_path(
23824 (worktree_id, rel_path("main.rs")),
23825 Some(pane.downgrade()),
23826 true,
23827 window,
23828 cx,
23829 )
23830 })
23831 .unwrap()
23832 .await
23833 .downcast::<Editor>()
23834 .unwrap();
23835 pane.update(cx, |pane, cx| {
23836 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23837 open_editor.update(cx, |editor, cx| {
23838 assert_eq!(
23839 editor.display_text(cx),
23840 main_text,
23841 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
23842 );
23843 })
23844 });
23845}
23846
23847#[gpui::test]
23848async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
23849 struct EmptyModalView {
23850 focus_handle: gpui::FocusHandle,
23851 }
23852 impl EventEmitter<DismissEvent> for EmptyModalView {}
23853 impl Render for EmptyModalView {
23854 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
23855 div()
23856 }
23857 }
23858 impl Focusable for EmptyModalView {
23859 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
23860 self.focus_handle.clone()
23861 }
23862 }
23863 impl workspace::ModalView for EmptyModalView {}
23864 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
23865 EmptyModalView {
23866 focus_handle: cx.focus_handle(),
23867 }
23868 }
23869
23870 init_test(cx, |_| {});
23871
23872 let fs = FakeFs::new(cx.executor());
23873 let project = Project::test(fs, [], cx).await;
23874 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23875 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
23876 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23877 let editor = cx.new_window_entity(|window, cx| {
23878 Editor::new(
23879 EditorMode::full(),
23880 buffer,
23881 Some(project.clone()),
23882 window,
23883 cx,
23884 )
23885 });
23886 workspace
23887 .update(cx, |workspace, window, cx| {
23888 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
23889 })
23890 .unwrap();
23891 editor.update_in(cx, |editor, window, cx| {
23892 editor.open_context_menu(&OpenContextMenu, window, cx);
23893 assert!(editor.mouse_context_menu.is_some());
23894 });
23895 workspace
23896 .update(cx, |workspace, window, cx| {
23897 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
23898 })
23899 .unwrap();
23900 cx.read(|cx| {
23901 assert!(editor.read(cx).mouse_context_menu.is_none());
23902 });
23903}
23904
23905fn set_linked_edit_ranges(
23906 opening: (Point, Point),
23907 closing: (Point, Point),
23908 editor: &mut Editor,
23909 cx: &mut Context<Editor>,
23910) {
23911 let Some((buffer, _)) = editor
23912 .buffer
23913 .read(cx)
23914 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
23915 else {
23916 panic!("Failed to get buffer for selection position");
23917 };
23918 let buffer = buffer.read(cx);
23919 let buffer_id = buffer.remote_id();
23920 let opening_range = buffer.anchor_before(opening.0)..buffer.anchor_after(opening.1);
23921 let closing_range = buffer.anchor_before(closing.0)..buffer.anchor_after(closing.1);
23922 let mut linked_ranges = HashMap::default();
23923 linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
23924 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
23925}
23926
23927#[gpui::test]
23928async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
23929 init_test(cx, |_| {});
23930
23931 let fs = FakeFs::new(cx.executor());
23932 fs.insert_file(path!("/file.html"), Default::default())
23933 .await;
23934
23935 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
23936
23937 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23938 let html_language = Arc::new(Language::new(
23939 LanguageConfig {
23940 name: "HTML".into(),
23941 matcher: LanguageMatcher {
23942 path_suffixes: vec!["html".to_string()],
23943 ..LanguageMatcher::default()
23944 },
23945 brackets: BracketPairConfig {
23946 pairs: vec![BracketPair {
23947 start: "<".into(),
23948 end: ">".into(),
23949 close: true,
23950 ..Default::default()
23951 }],
23952 ..Default::default()
23953 },
23954 ..Default::default()
23955 },
23956 Some(tree_sitter_html::LANGUAGE.into()),
23957 ));
23958 language_registry.add(html_language);
23959 let mut fake_servers = language_registry.register_fake_lsp(
23960 "HTML",
23961 FakeLspAdapter {
23962 capabilities: lsp::ServerCapabilities {
23963 completion_provider: Some(lsp::CompletionOptions {
23964 resolve_provider: Some(true),
23965 ..Default::default()
23966 }),
23967 ..Default::default()
23968 },
23969 ..Default::default()
23970 },
23971 );
23972
23973 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23974 let cx = &mut VisualTestContext::from_window(*workspace, cx);
23975
23976 let worktree_id = workspace
23977 .update(cx, |workspace, _window, cx| {
23978 workspace.project().update(cx, |project, cx| {
23979 project.worktrees(cx).next().unwrap().read(cx).id()
23980 })
23981 })
23982 .unwrap();
23983 project
23984 .update(cx, |project, cx| {
23985 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
23986 })
23987 .await
23988 .unwrap();
23989 let editor = workspace
23990 .update(cx, |workspace, window, cx| {
23991 workspace.open_path((worktree_id, rel_path("file.html")), None, true, window, cx)
23992 })
23993 .unwrap()
23994 .await
23995 .unwrap()
23996 .downcast::<Editor>()
23997 .unwrap();
23998
23999 let fake_server = fake_servers.next().await.unwrap();
24000 editor.update_in(cx, |editor, window, cx| {
24001 editor.set_text("<ad></ad>", window, cx);
24002 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
24003 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
24004 });
24005 set_linked_edit_ranges(
24006 (Point::new(0, 1), Point::new(0, 3)),
24007 (Point::new(0, 6), Point::new(0, 8)),
24008 editor,
24009 cx,
24010 );
24011 });
24012 let mut completion_handle =
24013 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
24014 Ok(Some(lsp::CompletionResponse::Array(vec![
24015 lsp::CompletionItem {
24016 label: "head".to_string(),
24017 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
24018 lsp::InsertReplaceEdit {
24019 new_text: "head".to_string(),
24020 insert: lsp::Range::new(
24021 lsp::Position::new(0, 1),
24022 lsp::Position::new(0, 3),
24023 ),
24024 replace: lsp::Range::new(
24025 lsp::Position::new(0, 1),
24026 lsp::Position::new(0, 3),
24027 ),
24028 },
24029 )),
24030 ..Default::default()
24031 },
24032 ])))
24033 });
24034 editor.update_in(cx, |editor, window, cx| {
24035 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
24036 });
24037 cx.run_until_parked();
24038 completion_handle.next().await.unwrap();
24039 editor.update(cx, |editor, _| {
24040 assert!(
24041 editor.context_menu_visible(),
24042 "Completion menu should be visible"
24043 );
24044 });
24045 editor.update_in(cx, |editor, window, cx| {
24046 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
24047 });
24048 cx.executor().run_until_parked();
24049 editor.update(cx, |editor, cx| {
24050 assert_eq!(editor.text(cx), "<head></head>");
24051 });
24052}
24053
24054#[gpui::test]
24055async fn test_linked_edits_on_typing_punctuation(cx: &mut TestAppContext) {
24056 init_test(cx, |_| {});
24057
24058 let mut cx = EditorTestContext::new(cx).await;
24059 let language = Arc::new(Language::new(
24060 LanguageConfig {
24061 name: "TSX".into(),
24062 matcher: LanguageMatcher {
24063 path_suffixes: vec!["tsx".to_string()],
24064 ..LanguageMatcher::default()
24065 },
24066 brackets: BracketPairConfig {
24067 pairs: vec![BracketPair {
24068 start: "<".into(),
24069 end: ">".into(),
24070 close: true,
24071 ..Default::default()
24072 }],
24073 ..Default::default()
24074 },
24075 linked_edit_characters: HashSet::from_iter(['.']),
24076 ..Default::default()
24077 },
24078 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
24079 ));
24080 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24081
24082 // Test typing > does not extend linked pair
24083 cx.set_state("<divˇ<div></div>");
24084 cx.update_editor(|editor, _, cx| {
24085 set_linked_edit_ranges(
24086 (Point::new(0, 1), Point::new(0, 4)),
24087 (Point::new(0, 11), Point::new(0, 14)),
24088 editor,
24089 cx,
24090 );
24091 });
24092 cx.update_editor(|editor, window, cx| {
24093 editor.handle_input(">", window, cx);
24094 });
24095 cx.assert_editor_state("<div>ˇ<div></div>");
24096
24097 // Test typing . do extend linked pair
24098 cx.set_state("<Animatedˇ></Animated>");
24099 cx.update_editor(|editor, _, cx| {
24100 set_linked_edit_ranges(
24101 (Point::new(0, 1), Point::new(0, 9)),
24102 (Point::new(0, 12), Point::new(0, 20)),
24103 editor,
24104 cx,
24105 );
24106 });
24107 cx.update_editor(|editor, window, cx| {
24108 editor.handle_input(".", window, cx);
24109 });
24110 cx.assert_editor_state("<Animated.ˇ></Animated.>");
24111 cx.update_editor(|editor, _, cx| {
24112 set_linked_edit_ranges(
24113 (Point::new(0, 1), Point::new(0, 10)),
24114 (Point::new(0, 13), Point::new(0, 21)),
24115 editor,
24116 cx,
24117 );
24118 });
24119 cx.update_editor(|editor, window, cx| {
24120 editor.handle_input("V", window, cx);
24121 });
24122 cx.assert_editor_state("<Animated.Vˇ></Animated.V>");
24123}
24124
24125#[gpui::test]
24126async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
24127 init_test(cx, |_| {});
24128
24129 let fs = FakeFs::new(cx.executor());
24130 fs.insert_tree(
24131 path!("/root"),
24132 json!({
24133 "a": {
24134 "main.rs": "fn main() {}",
24135 },
24136 "foo": {
24137 "bar": {
24138 "external_file.rs": "pub mod external {}",
24139 }
24140 }
24141 }),
24142 )
24143 .await;
24144
24145 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
24146 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24147 language_registry.add(rust_lang());
24148 let _fake_servers = language_registry.register_fake_lsp(
24149 "Rust",
24150 FakeLspAdapter {
24151 ..FakeLspAdapter::default()
24152 },
24153 );
24154 let (workspace, cx) =
24155 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24156 let worktree_id = workspace.update(cx, |workspace, cx| {
24157 workspace.project().update(cx, |project, cx| {
24158 project.worktrees(cx).next().unwrap().read(cx).id()
24159 })
24160 });
24161
24162 let assert_language_servers_count =
24163 |expected: usize, context: &str, cx: &mut VisualTestContext| {
24164 project.update(cx, |project, cx| {
24165 let current = project
24166 .lsp_store()
24167 .read(cx)
24168 .as_local()
24169 .unwrap()
24170 .language_servers
24171 .len();
24172 assert_eq!(expected, current, "{context}");
24173 });
24174 };
24175
24176 assert_language_servers_count(
24177 0,
24178 "No servers should be running before any file is open",
24179 cx,
24180 );
24181 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
24182 let main_editor = workspace
24183 .update_in(cx, |workspace, window, cx| {
24184 workspace.open_path(
24185 (worktree_id, rel_path("main.rs")),
24186 Some(pane.downgrade()),
24187 true,
24188 window,
24189 cx,
24190 )
24191 })
24192 .unwrap()
24193 .await
24194 .downcast::<Editor>()
24195 .unwrap();
24196 pane.update(cx, |pane, cx| {
24197 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24198 open_editor.update(cx, |editor, cx| {
24199 assert_eq!(
24200 editor.display_text(cx),
24201 "fn main() {}",
24202 "Original main.rs text on initial open",
24203 );
24204 });
24205 assert_eq!(open_editor, main_editor);
24206 });
24207 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
24208
24209 let external_editor = workspace
24210 .update_in(cx, |workspace, window, cx| {
24211 workspace.open_abs_path(
24212 PathBuf::from("/root/foo/bar/external_file.rs"),
24213 OpenOptions::default(),
24214 window,
24215 cx,
24216 )
24217 })
24218 .await
24219 .expect("opening external file")
24220 .downcast::<Editor>()
24221 .expect("downcasted external file's open element to editor");
24222 pane.update(cx, |pane, cx| {
24223 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24224 open_editor.update(cx, |editor, cx| {
24225 assert_eq!(
24226 editor.display_text(cx),
24227 "pub mod external {}",
24228 "External file is open now",
24229 );
24230 });
24231 assert_eq!(open_editor, external_editor);
24232 });
24233 assert_language_servers_count(
24234 1,
24235 "Second, external, *.rs file should join the existing server",
24236 cx,
24237 );
24238
24239 pane.update_in(cx, |pane, window, cx| {
24240 pane.close_active_item(&CloseActiveItem::default(), window, cx)
24241 })
24242 .await
24243 .unwrap();
24244 pane.update_in(cx, |pane, window, cx| {
24245 pane.navigate_backward(&Default::default(), window, cx);
24246 });
24247 cx.run_until_parked();
24248 pane.update(cx, |pane, cx| {
24249 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24250 open_editor.update(cx, |editor, cx| {
24251 assert_eq!(
24252 editor.display_text(cx),
24253 "pub mod external {}",
24254 "External file is open now",
24255 );
24256 });
24257 });
24258 assert_language_servers_count(
24259 1,
24260 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
24261 cx,
24262 );
24263
24264 cx.update(|_, cx| {
24265 workspace::reload(cx);
24266 });
24267 assert_language_servers_count(
24268 1,
24269 "After reloading the worktree with local and external files opened, only one project should be started",
24270 cx,
24271 );
24272}
24273
24274#[gpui::test]
24275async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
24276 init_test(cx, |_| {});
24277
24278 let mut cx = EditorTestContext::new(cx).await;
24279 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24280 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24281
24282 // test cursor move to start of each line on tab
24283 // for `if`, `elif`, `else`, `while`, `with` and `for`
24284 cx.set_state(indoc! {"
24285 def main():
24286 ˇ for item in items:
24287 ˇ while item.active:
24288 ˇ if item.value > 10:
24289 ˇ continue
24290 ˇ elif item.value < 0:
24291 ˇ break
24292 ˇ else:
24293 ˇ with item.context() as ctx:
24294 ˇ yield count
24295 ˇ else:
24296 ˇ log('while else')
24297 ˇ else:
24298 ˇ log('for else')
24299 "});
24300 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24301 cx.assert_editor_state(indoc! {"
24302 def main():
24303 ˇfor item in items:
24304 ˇwhile item.active:
24305 ˇif item.value > 10:
24306 ˇcontinue
24307 ˇelif item.value < 0:
24308 ˇbreak
24309 ˇelse:
24310 ˇwith item.context() as ctx:
24311 ˇyield count
24312 ˇelse:
24313 ˇlog('while else')
24314 ˇelse:
24315 ˇlog('for else')
24316 "});
24317 // test relative indent is preserved when tab
24318 // for `if`, `elif`, `else`, `while`, `with` and `for`
24319 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24320 cx.assert_editor_state(indoc! {"
24321 def main():
24322 ˇfor item in items:
24323 ˇwhile item.active:
24324 ˇif item.value > 10:
24325 ˇcontinue
24326 ˇelif item.value < 0:
24327 ˇbreak
24328 ˇelse:
24329 ˇwith item.context() as ctx:
24330 ˇyield count
24331 ˇelse:
24332 ˇlog('while else')
24333 ˇelse:
24334 ˇlog('for else')
24335 "});
24336
24337 // test cursor move to start of each line on tab
24338 // for `try`, `except`, `else`, `finally`, `match` and `def`
24339 cx.set_state(indoc! {"
24340 def main():
24341 ˇ try:
24342 ˇ fetch()
24343 ˇ except ValueError:
24344 ˇ handle_error()
24345 ˇ else:
24346 ˇ match value:
24347 ˇ case _:
24348 ˇ finally:
24349 ˇ def status():
24350 ˇ return 0
24351 "});
24352 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24353 cx.assert_editor_state(indoc! {"
24354 def main():
24355 ˇtry:
24356 ˇfetch()
24357 ˇexcept ValueError:
24358 ˇhandle_error()
24359 ˇelse:
24360 ˇmatch value:
24361 ˇcase _:
24362 ˇfinally:
24363 ˇdef status():
24364 ˇreturn 0
24365 "});
24366 // test relative indent is preserved when tab
24367 // for `try`, `except`, `else`, `finally`, `match` and `def`
24368 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24369 cx.assert_editor_state(indoc! {"
24370 def main():
24371 ˇtry:
24372 ˇfetch()
24373 ˇexcept ValueError:
24374 ˇhandle_error()
24375 ˇelse:
24376 ˇmatch value:
24377 ˇcase _:
24378 ˇfinally:
24379 ˇdef status():
24380 ˇreturn 0
24381 "});
24382}
24383
24384#[gpui::test]
24385async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
24386 init_test(cx, |_| {});
24387
24388 let mut cx = EditorTestContext::new(cx).await;
24389 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24390 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24391
24392 // test `else` auto outdents when typed inside `if` block
24393 cx.set_state(indoc! {"
24394 def main():
24395 if i == 2:
24396 return
24397 ˇ
24398 "});
24399 cx.update_editor(|editor, window, cx| {
24400 editor.handle_input("else:", window, cx);
24401 });
24402 cx.assert_editor_state(indoc! {"
24403 def main():
24404 if i == 2:
24405 return
24406 else:ˇ
24407 "});
24408
24409 // test `except` auto outdents when typed inside `try` block
24410 cx.set_state(indoc! {"
24411 def main():
24412 try:
24413 i = 2
24414 ˇ
24415 "});
24416 cx.update_editor(|editor, window, cx| {
24417 editor.handle_input("except:", window, cx);
24418 });
24419 cx.assert_editor_state(indoc! {"
24420 def main():
24421 try:
24422 i = 2
24423 except:ˇ
24424 "});
24425
24426 // test `else` auto outdents when typed inside `except` block
24427 cx.set_state(indoc! {"
24428 def main():
24429 try:
24430 i = 2
24431 except:
24432 j = 2
24433 ˇ
24434 "});
24435 cx.update_editor(|editor, window, cx| {
24436 editor.handle_input("else:", window, cx);
24437 });
24438 cx.assert_editor_state(indoc! {"
24439 def main():
24440 try:
24441 i = 2
24442 except:
24443 j = 2
24444 else:ˇ
24445 "});
24446
24447 // test `finally` auto outdents when typed inside `else` block
24448 cx.set_state(indoc! {"
24449 def main():
24450 try:
24451 i = 2
24452 except:
24453 j = 2
24454 else:
24455 k = 2
24456 ˇ
24457 "});
24458 cx.update_editor(|editor, window, cx| {
24459 editor.handle_input("finally:", window, cx);
24460 });
24461 cx.assert_editor_state(indoc! {"
24462 def main():
24463 try:
24464 i = 2
24465 except:
24466 j = 2
24467 else:
24468 k = 2
24469 finally:ˇ
24470 "});
24471
24472 // test `else` does not outdents when typed inside `except` block right after for block
24473 cx.set_state(indoc! {"
24474 def main():
24475 try:
24476 i = 2
24477 except:
24478 for i in range(n):
24479 pass
24480 ˇ
24481 "});
24482 cx.update_editor(|editor, window, cx| {
24483 editor.handle_input("else:", window, cx);
24484 });
24485 cx.assert_editor_state(indoc! {"
24486 def main():
24487 try:
24488 i = 2
24489 except:
24490 for i in range(n):
24491 pass
24492 else:ˇ
24493 "});
24494
24495 // test `finally` auto outdents when typed inside `else` block right after for block
24496 cx.set_state(indoc! {"
24497 def main():
24498 try:
24499 i = 2
24500 except:
24501 j = 2
24502 else:
24503 for i in range(n):
24504 pass
24505 ˇ
24506 "});
24507 cx.update_editor(|editor, window, cx| {
24508 editor.handle_input("finally:", window, cx);
24509 });
24510 cx.assert_editor_state(indoc! {"
24511 def main():
24512 try:
24513 i = 2
24514 except:
24515 j = 2
24516 else:
24517 for i in range(n):
24518 pass
24519 finally:ˇ
24520 "});
24521
24522 // test `except` outdents to inner "try" block
24523 cx.set_state(indoc! {"
24524 def main():
24525 try:
24526 i = 2
24527 if i == 2:
24528 try:
24529 i = 3
24530 ˇ
24531 "});
24532 cx.update_editor(|editor, window, cx| {
24533 editor.handle_input("except:", window, cx);
24534 });
24535 cx.assert_editor_state(indoc! {"
24536 def main():
24537 try:
24538 i = 2
24539 if i == 2:
24540 try:
24541 i = 3
24542 except:ˇ
24543 "});
24544
24545 // test `except` outdents to outer "try" block
24546 cx.set_state(indoc! {"
24547 def main():
24548 try:
24549 i = 2
24550 if i == 2:
24551 try:
24552 i = 3
24553 ˇ
24554 "});
24555 cx.update_editor(|editor, window, cx| {
24556 editor.handle_input("except:", window, cx);
24557 });
24558 cx.assert_editor_state(indoc! {"
24559 def main():
24560 try:
24561 i = 2
24562 if i == 2:
24563 try:
24564 i = 3
24565 except:ˇ
24566 "});
24567
24568 // test `else` stays at correct indent when typed after `for` block
24569 cx.set_state(indoc! {"
24570 def main():
24571 for i in range(10):
24572 if i == 3:
24573 break
24574 ˇ
24575 "});
24576 cx.update_editor(|editor, window, cx| {
24577 editor.handle_input("else:", window, cx);
24578 });
24579 cx.assert_editor_state(indoc! {"
24580 def main():
24581 for i in range(10):
24582 if i == 3:
24583 break
24584 else:ˇ
24585 "});
24586
24587 // test does not outdent on typing after line with square brackets
24588 cx.set_state(indoc! {"
24589 def f() -> list[str]:
24590 ˇ
24591 "});
24592 cx.update_editor(|editor, window, cx| {
24593 editor.handle_input("a", window, cx);
24594 });
24595 cx.assert_editor_state(indoc! {"
24596 def f() -> list[str]:
24597 aˇ
24598 "});
24599
24600 // test does not outdent on typing : after case keyword
24601 cx.set_state(indoc! {"
24602 match 1:
24603 caseˇ
24604 "});
24605 cx.update_editor(|editor, window, cx| {
24606 editor.handle_input(":", window, cx);
24607 });
24608 cx.assert_editor_state(indoc! {"
24609 match 1:
24610 case:ˇ
24611 "});
24612}
24613
24614#[gpui::test]
24615async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
24616 init_test(cx, |_| {});
24617 update_test_language_settings(cx, |settings| {
24618 settings.defaults.extend_comment_on_newline = Some(false);
24619 });
24620 let mut cx = EditorTestContext::new(cx).await;
24621 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24622 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24623
24624 // test correct indent after newline on comment
24625 cx.set_state(indoc! {"
24626 # COMMENT:ˇ
24627 "});
24628 cx.update_editor(|editor, window, cx| {
24629 editor.newline(&Newline, window, cx);
24630 });
24631 cx.assert_editor_state(indoc! {"
24632 # COMMENT:
24633 ˇ
24634 "});
24635
24636 // test correct indent after newline in brackets
24637 cx.set_state(indoc! {"
24638 {ˇ}
24639 "});
24640 cx.update_editor(|editor, window, cx| {
24641 editor.newline(&Newline, window, cx);
24642 });
24643 cx.run_until_parked();
24644 cx.assert_editor_state(indoc! {"
24645 {
24646 ˇ
24647 }
24648 "});
24649
24650 cx.set_state(indoc! {"
24651 (ˇ)
24652 "});
24653 cx.update_editor(|editor, window, cx| {
24654 editor.newline(&Newline, window, cx);
24655 });
24656 cx.run_until_parked();
24657 cx.assert_editor_state(indoc! {"
24658 (
24659 ˇ
24660 )
24661 "});
24662
24663 // do not indent after empty lists or dictionaries
24664 cx.set_state(indoc! {"
24665 a = []ˇ
24666 "});
24667 cx.update_editor(|editor, window, cx| {
24668 editor.newline(&Newline, window, cx);
24669 });
24670 cx.run_until_parked();
24671 cx.assert_editor_state(indoc! {"
24672 a = []
24673 ˇ
24674 "});
24675}
24676
24677#[gpui::test]
24678async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
24679 init_test(cx, |_| {});
24680
24681 let mut cx = EditorTestContext::new(cx).await;
24682 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24683 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24684
24685 // test cursor move to start of each line on tab
24686 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
24687 cx.set_state(indoc! {"
24688 function main() {
24689 ˇ for item in $items; do
24690 ˇ while [ -n \"$item\" ]; do
24691 ˇ if [ \"$value\" -gt 10 ]; then
24692 ˇ continue
24693 ˇ elif [ \"$value\" -lt 0 ]; then
24694 ˇ break
24695 ˇ else
24696 ˇ echo \"$item\"
24697 ˇ fi
24698 ˇ done
24699 ˇ done
24700 ˇ}
24701 "});
24702 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24703 cx.assert_editor_state(indoc! {"
24704 function main() {
24705 ˇfor item in $items; do
24706 ˇwhile [ -n \"$item\" ]; do
24707 ˇif [ \"$value\" -gt 10 ]; then
24708 ˇcontinue
24709 ˇelif [ \"$value\" -lt 0 ]; then
24710 ˇbreak
24711 ˇelse
24712 ˇecho \"$item\"
24713 ˇfi
24714 ˇdone
24715 ˇdone
24716 ˇ}
24717 "});
24718 // test relative indent is preserved when tab
24719 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24720 cx.assert_editor_state(indoc! {"
24721 function main() {
24722 ˇfor item in $items; do
24723 ˇwhile [ -n \"$item\" ]; do
24724 ˇif [ \"$value\" -gt 10 ]; then
24725 ˇcontinue
24726 ˇelif [ \"$value\" -lt 0 ]; then
24727 ˇbreak
24728 ˇelse
24729 ˇecho \"$item\"
24730 ˇfi
24731 ˇdone
24732 ˇdone
24733 ˇ}
24734 "});
24735
24736 // test cursor move to start of each line on tab
24737 // for `case` statement with patterns
24738 cx.set_state(indoc! {"
24739 function handle() {
24740 ˇ case \"$1\" in
24741 ˇ start)
24742 ˇ echo \"a\"
24743 ˇ ;;
24744 ˇ stop)
24745 ˇ echo \"b\"
24746 ˇ ;;
24747 ˇ *)
24748 ˇ echo \"c\"
24749 ˇ ;;
24750 ˇ esac
24751 ˇ}
24752 "});
24753 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24754 cx.assert_editor_state(indoc! {"
24755 function handle() {
24756 ˇcase \"$1\" in
24757 ˇstart)
24758 ˇecho \"a\"
24759 ˇ;;
24760 ˇstop)
24761 ˇecho \"b\"
24762 ˇ;;
24763 ˇ*)
24764 ˇecho \"c\"
24765 ˇ;;
24766 ˇesac
24767 ˇ}
24768 "});
24769}
24770
24771#[gpui::test]
24772async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
24773 init_test(cx, |_| {});
24774
24775 let mut cx = EditorTestContext::new(cx).await;
24776 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24777 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24778
24779 // test indents on comment insert
24780 cx.set_state(indoc! {"
24781 function main() {
24782 ˇ for item in $items; do
24783 ˇ while [ -n \"$item\" ]; do
24784 ˇ if [ \"$value\" -gt 10 ]; then
24785 ˇ continue
24786 ˇ elif [ \"$value\" -lt 0 ]; then
24787 ˇ break
24788 ˇ else
24789 ˇ echo \"$item\"
24790 ˇ fi
24791 ˇ done
24792 ˇ done
24793 ˇ}
24794 "});
24795 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
24796 cx.assert_editor_state(indoc! {"
24797 function main() {
24798 #ˇ for item in $items; do
24799 #ˇ while [ -n \"$item\" ]; do
24800 #ˇ if [ \"$value\" -gt 10 ]; then
24801 #ˇ continue
24802 #ˇ elif [ \"$value\" -lt 0 ]; then
24803 #ˇ break
24804 #ˇ else
24805 #ˇ echo \"$item\"
24806 #ˇ fi
24807 #ˇ done
24808 #ˇ done
24809 #ˇ}
24810 "});
24811}
24812
24813#[gpui::test]
24814async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
24815 init_test(cx, |_| {});
24816
24817 let mut cx = EditorTestContext::new(cx).await;
24818 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24819 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24820
24821 // test `else` auto outdents when typed inside `if` block
24822 cx.set_state(indoc! {"
24823 if [ \"$1\" = \"test\" ]; then
24824 echo \"foo bar\"
24825 ˇ
24826 "});
24827 cx.update_editor(|editor, window, cx| {
24828 editor.handle_input("else", window, cx);
24829 });
24830 cx.assert_editor_state(indoc! {"
24831 if [ \"$1\" = \"test\" ]; then
24832 echo \"foo bar\"
24833 elseˇ
24834 "});
24835
24836 // test `elif` auto outdents when typed inside `if` block
24837 cx.set_state(indoc! {"
24838 if [ \"$1\" = \"test\" ]; then
24839 echo \"foo bar\"
24840 ˇ
24841 "});
24842 cx.update_editor(|editor, window, cx| {
24843 editor.handle_input("elif", window, cx);
24844 });
24845 cx.assert_editor_state(indoc! {"
24846 if [ \"$1\" = \"test\" ]; then
24847 echo \"foo bar\"
24848 elifˇ
24849 "});
24850
24851 // test `fi` auto outdents when typed inside `else` block
24852 cx.set_state(indoc! {"
24853 if [ \"$1\" = \"test\" ]; then
24854 echo \"foo bar\"
24855 else
24856 echo \"bar baz\"
24857 ˇ
24858 "});
24859 cx.update_editor(|editor, window, cx| {
24860 editor.handle_input("fi", window, cx);
24861 });
24862 cx.assert_editor_state(indoc! {"
24863 if [ \"$1\" = \"test\" ]; then
24864 echo \"foo bar\"
24865 else
24866 echo \"bar baz\"
24867 fiˇ
24868 "});
24869
24870 // test `done` auto outdents when typed inside `while` block
24871 cx.set_state(indoc! {"
24872 while read line; do
24873 echo \"$line\"
24874 ˇ
24875 "});
24876 cx.update_editor(|editor, window, cx| {
24877 editor.handle_input("done", window, cx);
24878 });
24879 cx.assert_editor_state(indoc! {"
24880 while read line; do
24881 echo \"$line\"
24882 doneˇ
24883 "});
24884
24885 // test `done` auto outdents when typed inside `for` block
24886 cx.set_state(indoc! {"
24887 for file in *.txt; do
24888 cat \"$file\"
24889 ˇ
24890 "});
24891 cx.update_editor(|editor, window, cx| {
24892 editor.handle_input("done", window, cx);
24893 });
24894 cx.assert_editor_state(indoc! {"
24895 for file in *.txt; do
24896 cat \"$file\"
24897 doneˇ
24898 "});
24899
24900 // test `esac` auto outdents when typed inside `case` block
24901 cx.set_state(indoc! {"
24902 case \"$1\" in
24903 start)
24904 echo \"foo bar\"
24905 ;;
24906 stop)
24907 echo \"bar baz\"
24908 ;;
24909 ˇ
24910 "});
24911 cx.update_editor(|editor, window, cx| {
24912 editor.handle_input("esac", window, cx);
24913 });
24914 cx.assert_editor_state(indoc! {"
24915 case \"$1\" in
24916 start)
24917 echo \"foo bar\"
24918 ;;
24919 stop)
24920 echo \"bar baz\"
24921 ;;
24922 esacˇ
24923 "});
24924
24925 // test `*)` auto outdents when typed inside `case` block
24926 cx.set_state(indoc! {"
24927 case \"$1\" in
24928 start)
24929 echo \"foo bar\"
24930 ;;
24931 ˇ
24932 "});
24933 cx.update_editor(|editor, window, cx| {
24934 editor.handle_input("*)", window, cx);
24935 });
24936 cx.assert_editor_state(indoc! {"
24937 case \"$1\" in
24938 start)
24939 echo \"foo bar\"
24940 ;;
24941 *)ˇ
24942 "});
24943
24944 // test `fi` outdents to correct level with nested if blocks
24945 cx.set_state(indoc! {"
24946 if [ \"$1\" = \"test\" ]; then
24947 echo \"outer if\"
24948 if [ \"$2\" = \"debug\" ]; then
24949 echo \"inner if\"
24950 ˇ
24951 "});
24952 cx.update_editor(|editor, window, cx| {
24953 editor.handle_input("fi", window, cx);
24954 });
24955 cx.assert_editor_state(indoc! {"
24956 if [ \"$1\" = \"test\" ]; then
24957 echo \"outer if\"
24958 if [ \"$2\" = \"debug\" ]; then
24959 echo \"inner if\"
24960 fiˇ
24961 "});
24962}
24963
24964#[gpui::test]
24965async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
24966 init_test(cx, |_| {});
24967 update_test_language_settings(cx, |settings| {
24968 settings.defaults.extend_comment_on_newline = Some(false);
24969 });
24970 let mut cx = EditorTestContext::new(cx).await;
24971 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24972 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24973
24974 // test correct indent after newline on comment
24975 cx.set_state(indoc! {"
24976 # COMMENT:ˇ
24977 "});
24978 cx.update_editor(|editor, window, cx| {
24979 editor.newline(&Newline, window, cx);
24980 });
24981 cx.assert_editor_state(indoc! {"
24982 # COMMENT:
24983 ˇ
24984 "});
24985
24986 // test correct indent after newline after `then`
24987 cx.set_state(indoc! {"
24988
24989 if [ \"$1\" = \"test\" ]; thenˇ
24990 "});
24991 cx.update_editor(|editor, window, cx| {
24992 editor.newline(&Newline, window, cx);
24993 });
24994 cx.run_until_parked();
24995 cx.assert_editor_state(indoc! {"
24996
24997 if [ \"$1\" = \"test\" ]; then
24998 ˇ
24999 "});
25000
25001 // test correct indent after newline after `else`
25002 cx.set_state(indoc! {"
25003 if [ \"$1\" = \"test\" ]; then
25004 elseˇ
25005 "});
25006 cx.update_editor(|editor, window, cx| {
25007 editor.newline(&Newline, window, cx);
25008 });
25009 cx.run_until_parked();
25010 cx.assert_editor_state(indoc! {"
25011 if [ \"$1\" = \"test\" ]; then
25012 else
25013 ˇ
25014 "});
25015
25016 // test correct indent after newline after `elif`
25017 cx.set_state(indoc! {"
25018 if [ \"$1\" = \"test\" ]; then
25019 elifˇ
25020 "});
25021 cx.update_editor(|editor, window, cx| {
25022 editor.newline(&Newline, window, cx);
25023 });
25024 cx.run_until_parked();
25025 cx.assert_editor_state(indoc! {"
25026 if [ \"$1\" = \"test\" ]; then
25027 elif
25028 ˇ
25029 "});
25030
25031 // test correct indent after newline after `do`
25032 cx.set_state(indoc! {"
25033 for file in *.txt; doˇ
25034 "});
25035 cx.update_editor(|editor, window, cx| {
25036 editor.newline(&Newline, window, cx);
25037 });
25038 cx.run_until_parked();
25039 cx.assert_editor_state(indoc! {"
25040 for file in *.txt; do
25041 ˇ
25042 "});
25043
25044 // test correct indent after newline after case pattern
25045 cx.set_state(indoc! {"
25046 case \"$1\" in
25047 start)ˇ
25048 "});
25049 cx.update_editor(|editor, window, cx| {
25050 editor.newline(&Newline, window, cx);
25051 });
25052 cx.run_until_parked();
25053 cx.assert_editor_state(indoc! {"
25054 case \"$1\" in
25055 start)
25056 ˇ
25057 "});
25058
25059 // test correct indent after newline after case pattern
25060 cx.set_state(indoc! {"
25061 case \"$1\" in
25062 start)
25063 ;;
25064 *)ˇ
25065 "});
25066 cx.update_editor(|editor, window, cx| {
25067 editor.newline(&Newline, window, cx);
25068 });
25069 cx.run_until_parked();
25070 cx.assert_editor_state(indoc! {"
25071 case \"$1\" in
25072 start)
25073 ;;
25074 *)
25075 ˇ
25076 "});
25077
25078 // test correct indent after newline after function opening brace
25079 cx.set_state(indoc! {"
25080 function test() {ˇ}
25081 "});
25082 cx.update_editor(|editor, window, cx| {
25083 editor.newline(&Newline, window, cx);
25084 });
25085 cx.run_until_parked();
25086 cx.assert_editor_state(indoc! {"
25087 function test() {
25088 ˇ
25089 }
25090 "});
25091
25092 // test no extra indent after semicolon on same line
25093 cx.set_state(indoc! {"
25094 echo \"test\";ˇ
25095 "});
25096 cx.update_editor(|editor, window, cx| {
25097 editor.newline(&Newline, window, cx);
25098 });
25099 cx.run_until_parked();
25100 cx.assert_editor_state(indoc! {"
25101 echo \"test\";
25102 ˇ
25103 "});
25104}
25105
25106fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
25107 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
25108 point..point
25109}
25110
25111#[track_caller]
25112fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
25113 let (text, ranges) = marked_text_ranges(marked_text, true);
25114 assert_eq!(editor.text(cx), text);
25115 assert_eq!(
25116 editor.selections.ranges(cx),
25117 ranges,
25118 "Assert selections are {}",
25119 marked_text
25120 );
25121}
25122
25123pub fn handle_signature_help_request(
25124 cx: &mut EditorLspTestContext,
25125 mocked_response: lsp::SignatureHelp,
25126) -> impl Future<Output = ()> + use<> {
25127 let mut request =
25128 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
25129 let mocked_response = mocked_response.clone();
25130 async move { Ok(Some(mocked_response)) }
25131 });
25132
25133 async move {
25134 request.next().await;
25135 }
25136}
25137
25138#[track_caller]
25139pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
25140 cx.update_editor(|editor, _, _| {
25141 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
25142 let entries = menu.entries.borrow();
25143 let entries = entries
25144 .iter()
25145 .map(|entry| entry.string.as_str())
25146 .collect::<Vec<_>>();
25147 assert_eq!(entries, expected);
25148 } else {
25149 panic!("Expected completions menu");
25150 }
25151 });
25152}
25153
25154/// Handle completion request passing a marked string specifying where the completion
25155/// should be triggered from using '|' character, what range should be replaced, and what completions
25156/// should be returned using '<' and '>' to delimit the range.
25157///
25158/// Also see `handle_completion_request_with_insert_and_replace`.
25159#[track_caller]
25160pub fn handle_completion_request(
25161 marked_string: &str,
25162 completions: Vec<&'static str>,
25163 is_incomplete: bool,
25164 counter: Arc<AtomicUsize>,
25165 cx: &mut EditorLspTestContext,
25166) -> impl Future<Output = ()> {
25167 let complete_from_marker: TextRangeMarker = '|'.into();
25168 let replace_range_marker: TextRangeMarker = ('<', '>').into();
25169 let (_, mut marked_ranges) = marked_text_ranges_by(
25170 marked_string,
25171 vec![complete_from_marker.clone(), replace_range_marker.clone()],
25172 );
25173
25174 let complete_from_position =
25175 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
25176 let replace_range =
25177 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
25178
25179 let mut request =
25180 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
25181 let completions = completions.clone();
25182 counter.fetch_add(1, atomic::Ordering::Release);
25183 async move {
25184 assert_eq!(params.text_document_position.text_document.uri, url.clone());
25185 assert_eq!(
25186 params.text_document_position.position,
25187 complete_from_position
25188 );
25189 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
25190 is_incomplete,
25191 item_defaults: None,
25192 items: completions
25193 .iter()
25194 .map(|completion_text| lsp::CompletionItem {
25195 label: completion_text.to_string(),
25196 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
25197 range: replace_range,
25198 new_text: completion_text.to_string(),
25199 })),
25200 ..Default::default()
25201 })
25202 .collect(),
25203 })))
25204 }
25205 });
25206
25207 async move {
25208 request.next().await;
25209 }
25210}
25211
25212/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
25213/// given instead, which also contains an `insert` range.
25214///
25215/// This function uses markers to define ranges:
25216/// - `|` marks the cursor position
25217/// - `<>` marks the replace range
25218/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
25219pub fn handle_completion_request_with_insert_and_replace(
25220 cx: &mut EditorLspTestContext,
25221 marked_string: &str,
25222 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
25223 counter: Arc<AtomicUsize>,
25224) -> impl Future<Output = ()> {
25225 let complete_from_marker: TextRangeMarker = '|'.into();
25226 let replace_range_marker: TextRangeMarker = ('<', '>').into();
25227 let insert_range_marker: TextRangeMarker = ('{', '}').into();
25228
25229 let (_, mut marked_ranges) = marked_text_ranges_by(
25230 marked_string,
25231 vec![
25232 complete_from_marker.clone(),
25233 replace_range_marker.clone(),
25234 insert_range_marker.clone(),
25235 ],
25236 );
25237
25238 let complete_from_position =
25239 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
25240 let replace_range =
25241 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
25242
25243 let insert_range = match marked_ranges.remove(&insert_range_marker) {
25244 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
25245 _ => lsp::Range {
25246 start: replace_range.start,
25247 end: complete_from_position,
25248 },
25249 };
25250
25251 let mut request =
25252 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
25253 let completions = completions.clone();
25254 counter.fetch_add(1, atomic::Ordering::Release);
25255 async move {
25256 assert_eq!(params.text_document_position.text_document.uri, url.clone());
25257 assert_eq!(
25258 params.text_document_position.position, complete_from_position,
25259 "marker `|` position doesn't match",
25260 );
25261 Ok(Some(lsp::CompletionResponse::Array(
25262 completions
25263 .iter()
25264 .map(|(label, new_text)| lsp::CompletionItem {
25265 label: label.to_string(),
25266 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
25267 lsp::InsertReplaceEdit {
25268 insert: insert_range,
25269 replace: replace_range,
25270 new_text: new_text.to_string(),
25271 },
25272 )),
25273 ..Default::default()
25274 })
25275 .collect(),
25276 )))
25277 }
25278 });
25279
25280 async move {
25281 request.next().await;
25282 }
25283}
25284
25285fn handle_resolve_completion_request(
25286 cx: &mut EditorLspTestContext,
25287 edits: Option<Vec<(&'static str, &'static str)>>,
25288) -> impl Future<Output = ()> {
25289 let edits = edits.map(|edits| {
25290 edits
25291 .iter()
25292 .map(|(marked_string, new_text)| {
25293 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
25294 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
25295 lsp::TextEdit::new(replace_range, new_text.to_string())
25296 })
25297 .collect::<Vec<_>>()
25298 });
25299
25300 let mut request =
25301 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
25302 let edits = edits.clone();
25303 async move {
25304 Ok(lsp::CompletionItem {
25305 additional_text_edits: edits,
25306 ..Default::default()
25307 })
25308 }
25309 });
25310
25311 async move {
25312 request.next().await;
25313 }
25314}
25315
25316pub(crate) fn update_test_language_settings(
25317 cx: &mut TestAppContext,
25318 f: impl Fn(&mut AllLanguageSettingsContent),
25319) {
25320 cx.update(|cx| {
25321 SettingsStore::update_global(cx, |store, cx| {
25322 store.update_user_settings(cx, |settings| f(&mut settings.project.all_languages));
25323 });
25324 });
25325}
25326
25327pub(crate) fn update_test_project_settings(
25328 cx: &mut TestAppContext,
25329 f: impl Fn(&mut ProjectSettingsContent),
25330) {
25331 cx.update(|cx| {
25332 SettingsStore::update_global(cx, |store, cx| {
25333 store.update_user_settings(cx, |settings| f(&mut settings.project));
25334 });
25335 });
25336}
25337
25338pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
25339 cx.update(|cx| {
25340 assets::Assets.load_test_fonts(cx);
25341 let store = SettingsStore::test(cx);
25342 cx.set_global(store);
25343 theme::init(theme::LoadThemes::JustBase, cx);
25344 release_channel::init(SemanticVersion::default(), cx);
25345 client::init_settings(cx);
25346 language::init(cx);
25347 Project::init_settings(cx);
25348 workspace::init_settings(cx);
25349 crate::init(cx);
25350 });
25351 zlog::init_test();
25352 update_test_language_settings(cx, f);
25353}
25354
25355#[track_caller]
25356fn assert_hunk_revert(
25357 not_reverted_text_with_selections: &str,
25358 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
25359 expected_reverted_text_with_selections: &str,
25360 base_text: &str,
25361 cx: &mut EditorLspTestContext,
25362) {
25363 cx.set_state(not_reverted_text_with_selections);
25364 cx.set_head_text(base_text);
25365 cx.executor().run_until_parked();
25366
25367 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
25368 let snapshot = editor.snapshot(window, cx);
25369 let reverted_hunk_statuses = snapshot
25370 .buffer_snapshot()
25371 .diff_hunks_in_range(0..snapshot.buffer_snapshot().len())
25372 .map(|hunk| hunk.status().kind)
25373 .collect::<Vec<_>>();
25374
25375 editor.git_restore(&Default::default(), window, cx);
25376 reverted_hunk_statuses
25377 });
25378 cx.executor().run_until_parked();
25379 cx.assert_editor_state(expected_reverted_text_with_selections);
25380 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
25381}
25382
25383#[gpui::test(iterations = 10)]
25384async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
25385 init_test(cx, |_| {});
25386
25387 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
25388 let counter = diagnostic_requests.clone();
25389
25390 let fs = FakeFs::new(cx.executor());
25391 fs.insert_tree(
25392 path!("/a"),
25393 json!({
25394 "first.rs": "fn main() { let a = 5; }",
25395 "second.rs": "// Test file",
25396 }),
25397 )
25398 .await;
25399
25400 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25401 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25402 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25403
25404 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25405 language_registry.add(rust_lang());
25406 let mut fake_servers = language_registry.register_fake_lsp(
25407 "Rust",
25408 FakeLspAdapter {
25409 capabilities: lsp::ServerCapabilities {
25410 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
25411 lsp::DiagnosticOptions {
25412 identifier: None,
25413 inter_file_dependencies: true,
25414 workspace_diagnostics: true,
25415 work_done_progress_options: Default::default(),
25416 },
25417 )),
25418 ..Default::default()
25419 },
25420 ..Default::default()
25421 },
25422 );
25423
25424 let editor = workspace
25425 .update(cx, |workspace, window, cx| {
25426 workspace.open_abs_path(
25427 PathBuf::from(path!("/a/first.rs")),
25428 OpenOptions::default(),
25429 window,
25430 cx,
25431 )
25432 })
25433 .unwrap()
25434 .await
25435 .unwrap()
25436 .downcast::<Editor>()
25437 .unwrap();
25438 let fake_server = fake_servers.next().await.unwrap();
25439 let server_id = fake_server.server.server_id();
25440 let mut first_request = fake_server
25441 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
25442 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
25443 let result_id = Some(new_result_id.to_string());
25444 assert_eq!(
25445 params.text_document.uri,
25446 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25447 );
25448 async move {
25449 Ok(lsp::DocumentDiagnosticReportResult::Report(
25450 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
25451 related_documents: None,
25452 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
25453 items: Vec::new(),
25454 result_id,
25455 },
25456 }),
25457 ))
25458 }
25459 });
25460
25461 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
25462 project.update(cx, |project, cx| {
25463 let buffer_id = editor
25464 .read(cx)
25465 .buffer()
25466 .read(cx)
25467 .as_singleton()
25468 .expect("created a singleton buffer")
25469 .read(cx)
25470 .remote_id();
25471 let buffer_result_id = project
25472 .lsp_store()
25473 .read(cx)
25474 .result_id(server_id, buffer_id, cx);
25475 assert_eq!(expected, buffer_result_id);
25476 });
25477 };
25478
25479 ensure_result_id(None, cx);
25480 cx.executor().advance_clock(Duration::from_millis(60));
25481 cx.executor().run_until_parked();
25482 assert_eq!(
25483 diagnostic_requests.load(atomic::Ordering::Acquire),
25484 1,
25485 "Opening file should trigger diagnostic request"
25486 );
25487 first_request
25488 .next()
25489 .await
25490 .expect("should have sent the first diagnostics pull request");
25491 ensure_result_id(Some("1".to_string()), cx);
25492
25493 // Editing should trigger diagnostics
25494 editor.update_in(cx, |editor, window, cx| {
25495 editor.handle_input("2", window, cx)
25496 });
25497 cx.executor().advance_clock(Duration::from_millis(60));
25498 cx.executor().run_until_parked();
25499 assert_eq!(
25500 diagnostic_requests.load(atomic::Ordering::Acquire),
25501 2,
25502 "Editing should trigger diagnostic request"
25503 );
25504 ensure_result_id(Some("2".to_string()), cx);
25505
25506 // Moving cursor should not trigger diagnostic request
25507 editor.update_in(cx, |editor, window, cx| {
25508 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25509 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
25510 });
25511 });
25512 cx.executor().advance_clock(Duration::from_millis(60));
25513 cx.executor().run_until_parked();
25514 assert_eq!(
25515 diagnostic_requests.load(atomic::Ordering::Acquire),
25516 2,
25517 "Cursor movement should not trigger diagnostic request"
25518 );
25519 ensure_result_id(Some("2".to_string()), cx);
25520 // Multiple rapid edits should be debounced
25521 for _ in 0..5 {
25522 editor.update_in(cx, |editor, window, cx| {
25523 editor.handle_input("x", window, cx)
25524 });
25525 }
25526 cx.executor().advance_clock(Duration::from_millis(60));
25527 cx.executor().run_until_parked();
25528
25529 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
25530 assert!(
25531 final_requests <= 4,
25532 "Multiple rapid edits should be debounced (got {final_requests} requests)",
25533 );
25534 ensure_result_id(Some(final_requests.to_string()), cx);
25535}
25536
25537#[gpui::test]
25538async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
25539 // Regression test for issue #11671
25540 // Previously, adding a cursor after moving multiple cursors would reset
25541 // the cursor count instead of adding to the existing cursors.
25542 init_test(cx, |_| {});
25543 let mut cx = EditorTestContext::new(cx).await;
25544
25545 // Create a simple buffer with cursor at start
25546 cx.set_state(indoc! {"
25547 ˇaaaa
25548 bbbb
25549 cccc
25550 dddd
25551 eeee
25552 ffff
25553 gggg
25554 hhhh"});
25555
25556 // Add 2 cursors below (so we have 3 total)
25557 cx.update_editor(|editor, window, cx| {
25558 editor.add_selection_below(&Default::default(), window, cx);
25559 editor.add_selection_below(&Default::default(), window, cx);
25560 });
25561
25562 // Verify we have 3 cursors
25563 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
25564 assert_eq!(
25565 initial_count, 3,
25566 "Should have 3 cursors after adding 2 below"
25567 );
25568
25569 // Move down one line
25570 cx.update_editor(|editor, window, cx| {
25571 editor.move_down(&MoveDown, window, cx);
25572 });
25573
25574 // Add another cursor below
25575 cx.update_editor(|editor, window, cx| {
25576 editor.add_selection_below(&Default::default(), window, cx);
25577 });
25578
25579 // Should now have 4 cursors (3 original + 1 new)
25580 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
25581 assert_eq!(
25582 final_count, 4,
25583 "Should have 4 cursors after moving and adding another"
25584 );
25585}
25586
25587#[gpui::test(iterations = 10)]
25588async fn test_document_colors(cx: &mut TestAppContext) {
25589 let expected_color = Rgba {
25590 r: 0.33,
25591 g: 0.33,
25592 b: 0.33,
25593 a: 0.33,
25594 };
25595
25596 init_test(cx, |_| {});
25597
25598 let fs = FakeFs::new(cx.executor());
25599 fs.insert_tree(
25600 path!("/a"),
25601 json!({
25602 "first.rs": "fn main() { let a = 5; }",
25603 }),
25604 )
25605 .await;
25606
25607 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25608 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25609 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25610
25611 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25612 language_registry.add(rust_lang());
25613 let mut fake_servers = language_registry.register_fake_lsp(
25614 "Rust",
25615 FakeLspAdapter {
25616 capabilities: lsp::ServerCapabilities {
25617 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
25618 ..lsp::ServerCapabilities::default()
25619 },
25620 name: "rust-analyzer",
25621 ..FakeLspAdapter::default()
25622 },
25623 );
25624 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
25625 "Rust",
25626 FakeLspAdapter {
25627 capabilities: lsp::ServerCapabilities {
25628 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
25629 ..lsp::ServerCapabilities::default()
25630 },
25631 name: "not-rust-analyzer",
25632 ..FakeLspAdapter::default()
25633 },
25634 );
25635
25636 let editor = workspace
25637 .update(cx, |workspace, window, cx| {
25638 workspace.open_abs_path(
25639 PathBuf::from(path!("/a/first.rs")),
25640 OpenOptions::default(),
25641 window,
25642 cx,
25643 )
25644 })
25645 .unwrap()
25646 .await
25647 .unwrap()
25648 .downcast::<Editor>()
25649 .unwrap();
25650 let fake_language_server = fake_servers.next().await.unwrap();
25651 let fake_language_server_without_capabilities =
25652 fake_servers_without_capabilities.next().await.unwrap();
25653 let requests_made = Arc::new(AtomicUsize::new(0));
25654 let closure_requests_made = Arc::clone(&requests_made);
25655 let mut color_request_handle = fake_language_server
25656 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
25657 let requests_made = Arc::clone(&closure_requests_made);
25658 async move {
25659 assert_eq!(
25660 params.text_document.uri,
25661 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25662 );
25663 requests_made.fetch_add(1, atomic::Ordering::Release);
25664 Ok(vec![
25665 lsp::ColorInformation {
25666 range: lsp::Range {
25667 start: lsp::Position {
25668 line: 0,
25669 character: 0,
25670 },
25671 end: lsp::Position {
25672 line: 0,
25673 character: 1,
25674 },
25675 },
25676 color: lsp::Color {
25677 red: 0.33,
25678 green: 0.33,
25679 blue: 0.33,
25680 alpha: 0.33,
25681 },
25682 },
25683 lsp::ColorInformation {
25684 range: lsp::Range {
25685 start: lsp::Position {
25686 line: 0,
25687 character: 0,
25688 },
25689 end: lsp::Position {
25690 line: 0,
25691 character: 1,
25692 },
25693 },
25694 color: lsp::Color {
25695 red: 0.33,
25696 green: 0.33,
25697 blue: 0.33,
25698 alpha: 0.33,
25699 },
25700 },
25701 ])
25702 }
25703 });
25704
25705 let _handle = fake_language_server_without_capabilities
25706 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
25707 panic!("Should not be called");
25708 });
25709 cx.executor().advance_clock(Duration::from_millis(100));
25710 color_request_handle.next().await.unwrap();
25711 cx.run_until_parked();
25712 assert_eq!(
25713 1,
25714 requests_made.load(atomic::Ordering::Acquire),
25715 "Should query for colors once per editor open"
25716 );
25717 editor.update_in(cx, |editor, _, cx| {
25718 assert_eq!(
25719 vec![expected_color],
25720 extract_color_inlays(editor, cx),
25721 "Should have an initial inlay"
25722 );
25723 });
25724
25725 // opening another file in a split should not influence the LSP query counter
25726 workspace
25727 .update(cx, |workspace, window, cx| {
25728 assert_eq!(
25729 workspace.panes().len(),
25730 1,
25731 "Should have one pane with one editor"
25732 );
25733 workspace.move_item_to_pane_in_direction(
25734 &MoveItemToPaneInDirection {
25735 direction: SplitDirection::Right,
25736 focus: false,
25737 clone: true,
25738 },
25739 window,
25740 cx,
25741 );
25742 })
25743 .unwrap();
25744 cx.run_until_parked();
25745 workspace
25746 .update(cx, |workspace, _, cx| {
25747 let panes = workspace.panes();
25748 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
25749 for pane in panes {
25750 let editor = pane
25751 .read(cx)
25752 .active_item()
25753 .and_then(|item| item.downcast::<Editor>())
25754 .expect("Should have opened an editor in each split");
25755 let editor_file = editor
25756 .read(cx)
25757 .buffer()
25758 .read(cx)
25759 .as_singleton()
25760 .expect("test deals with singleton buffers")
25761 .read(cx)
25762 .file()
25763 .expect("test buffese should have a file")
25764 .path();
25765 assert_eq!(
25766 editor_file.as_ref(),
25767 rel_path("first.rs"),
25768 "Both editors should be opened for the same file"
25769 )
25770 }
25771 })
25772 .unwrap();
25773
25774 cx.executor().advance_clock(Duration::from_millis(500));
25775 let save = editor.update_in(cx, |editor, window, cx| {
25776 editor.move_to_end(&MoveToEnd, window, cx);
25777 editor.handle_input("dirty", window, cx);
25778 editor.save(
25779 SaveOptions {
25780 format: true,
25781 autosave: true,
25782 },
25783 project.clone(),
25784 window,
25785 cx,
25786 )
25787 });
25788 save.await.unwrap();
25789
25790 color_request_handle.next().await.unwrap();
25791 cx.run_until_parked();
25792 assert_eq!(
25793 3,
25794 requests_made.load(atomic::Ordering::Acquire),
25795 "Should query for colors once per save and once per formatting after save"
25796 );
25797
25798 drop(editor);
25799 let close = workspace
25800 .update(cx, |workspace, window, cx| {
25801 workspace.active_pane().update(cx, |pane, cx| {
25802 pane.close_active_item(&CloseActiveItem::default(), window, cx)
25803 })
25804 })
25805 .unwrap();
25806 close.await.unwrap();
25807 let close = workspace
25808 .update(cx, |workspace, window, cx| {
25809 workspace.active_pane().update(cx, |pane, cx| {
25810 pane.close_active_item(&CloseActiveItem::default(), window, cx)
25811 })
25812 })
25813 .unwrap();
25814 close.await.unwrap();
25815 assert_eq!(
25816 3,
25817 requests_made.load(atomic::Ordering::Acquire),
25818 "After saving and closing all editors, no extra requests should be made"
25819 );
25820 workspace
25821 .update(cx, |workspace, _, cx| {
25822 assert!(
25823 workspace.active_item(cx).is_none(),
25824 "Should close all editors"
25825 )
25826 })
25827 .unwrap();
25828
25829 workspace
25830 .update(cx, |workspace, window, cx| {
25831 workspace.active_pane().update(cx, |pane, cx| {
25832 pane.navigate_backward(&workspace::GoBack, window, cx);
25833 })
25834 })
25835 .unwrap();
25836 cx.executor().advance_clock(Duration::from_millis(100));
25837 cx.run_until_parked();
25838 let editor = workspace
25839 .update(cx, |workspace, _, cx| {
25840 workspace
25841 .active_item(cx)
25842 .expect("Should have reopened the editor again after navigating back")
25843 .downcast::<Editor>()
25844 .expect("Should be an editor")
25845 })
25846 .unwrap();
25847 color_request_handle.next().await.unwrap();
25848 assert_eq!(
25849 3,
25850 requests_made.load(atomic::Ordering::Acquire),
25851 "Cache should be reused on buffer close and reopen"
25852 );
25853 editor.update(cx, |editor, cx| {
25854 assert_eq!(
25855 vec![expected_color],
25856 extract_color_inlays(editor, cx),
25857 "Should have an initial inlay"
25858 );
25859 });
25860
25861 drop(color_request_handle);
25862 let closure_requests_made = Arc::clone(&requests_made);
25863 let mut empty_color_request_handle = fake_language_server
25864 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
25865 let requests_made = Arc::clone(&closure_requests_made);
25866 async move {
25867 assert_eq!(
25868 params.text_document.uri,
25869 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25870 );
25871 requests_made.fetch_add(1, atomic::Ordering::Release);
25872 Ok(Vec::new())
25873 }
25874 });
25875 let save = editor.update_in(cx, |editor, window, cx| {
25876 editor.move_to_end(&MoveToEnd, window, cx);
25877 editor.handle_input("dirty_again", window, cx);
25878 editor.save(
25879 SaveOptions {
25880 format: false,
25881 autosave: true,
25882 },
25883 project.clone(),
25884 window,
25885 cx,
25886 )
25887 });
25888 save.await.unwrap();
25889
25890 empty_color_request_handle.next().await.unwrap();
25891 cx.run_until_parked();
25892 assert_eq!(
25893 4,
25894 requests_made.load(atomic::Ordering::Acquire),
25895 "Should query for colors once per save only, as formatting was not requested"
25896 );
25897 editor.update(cx, |editor, cx| {
25898 assert_eq!(
25899 Vec::<Rgba>::new(),
25900 extract_color_inlays(editor, cx),
25901 "Should clear all colors when the server returns an empty response"
25902 );
25903 });
25904}
25905
25906#[gpui::test]
25907async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
25908 init_test(cx, |_| {});
25909 let (editor, cx) = cx.add_window_view(Editor::single_line);
25910 editor.update_in(cx, |editor, window, cx| {
25911 editor.set_text("oops\n\nwow\n", window, cx)
25912 });
25913 cx.run_until_parked();
25914 editor.update(cx, |editor, cx| {
25915 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
25916 });
25917 editor.update(cx, |editor, cx| editor.edit([(3..5, "")], cx));
25918 cx.run_until_parked();
25919 editor.update(cx, |editor, cx| {
25920 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
25921 });
25922}
25923
25924#[gpui::test]
25925async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
25926 init_test(cx, |_| {});
25927
25928 cx.update(|cx| {
25929 register_project_item::<Editor>(cx);
25930 });
25931
25932 let fs = FakeFs::new(cx.executor());
25933 fs.insert_tree("/root1", json!({})).await;
25934 fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
25935 .await;
25936
25937 let project = Project::test(fs, ["/root1".as_ref()], cx).await;
25938 let (workspace, cx) =
25939 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
25940
25941 let worktree_id = project.update(cx, |project, cx| {
25942 project.worktrees(cx).next().unwrap().read(cx).id()
25943 });
25944
25945 let handle = workspace
25946 .update_in(cx, |workspace, window, cx| {
25947 let project_path = (worktree_id, rel_path("one.pdf"));
25948 workspace.open_path(project_path, None, true, window, cx)
25949 })
25950 .await
25951 .unwrap();
25952
25953 assert_eq!(
25954 handle.to_any().entity_type(),
25955 TypeId::of::<InvalidBufferView>()
25956 );
25957}
25958
25959#[gpui::test]
25960async fn test_select_next_prev_syntax_node(cx: &mut TestAppContext) {
25961 init_test(cx, |_| {});
25962
25963 let language = Arc::new(Language::new(
25964 LanguageConfig::default(),
25965 Some(tree_sitter_rust::LANGUAGE.into()),
25966 ));
25967
25968 // Test hierarchical sibling navigation
25969 let text = r#"
25970 fn outer() {
25971 if condition {
25972 let a = 1;
25973 }
25974 let b = 2;
25975 }
25976
25977 fn another() {
25978 let c = 3;
25979 }
25980 "#;
25981
25982 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
25983 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
25984 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
25985
25986 // Wait for parsing to complete
25987 editor
25988 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
25989 .await;
25990
25991 editor.update_in(cx, |editor, window, cx| {
25992 // Start by selecting "let a = 1;" inside the if block
25993 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25994 s.select_display_ranges([
25995 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 26)
25996 ]);
25997 });
25998
25999 let initial_selection = editor.selections.display_ranges(cx);
26000 assert_eq!(initial_selection.len(), 1, "Should have one selection");
26001
26002 // Test select next sibling - should move up levels to find the next sibling
26003 // Since "let a = 1;" has no siblings in the if block, it should move up
26004 // to find "let b = 2;" which is a sibling of the if block
26005 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
26006 let next_selection = editor.selections.display_ranges(cx);
26007
26008 // Should have a selection and it should be different from the initial
26009 assert_eq!(
26010 next_selection.len(),
26011 1,
26012 "Should have one selection after next"
26013 );
26014 assert_ne!(
26015 next_selection[0], initial_selection[0],
26016 "Next sibling selection should be different"
26017 );
26018
26019 // Test hierarchical navigation by going to the end of the current function
26020 // and trying to navigate to the next function
26021 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26022 s.select_display_ranges([
26023 DisplayPoint::new(DisplayRow(5), 12)..DisplayPoint::new(DisplayRow(5), 22)
26024 ]);
26025 });
26026
26027 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
26028 let function_next_selection = editor.selections.display_ranges(cx);
26029
26030 // Should move to the next function
26031 assert_eq!(
26032 function_next_selection.len(),
26033 1,
26034 "Should have one selection after function next"
26035 );
26036
26037 // Test select previous sibling navigation
26038 editor.select_prev_syntax_node(&SelectPreviousSyntaxNode, window, cx);
26039 let prev_selection = editor.selections.display_ranges(cx);
26040
26041 // Should have a selection and it should be different
26042 assert_eq!(
26043 prev_selection.len(),
26044 1,
26045 "Should have one selection after prev"
26046 );
26047 assert_ne!(
26048 prev_selection[0], function_next_selection[0],
26049 "Previous sibling selection should be different from next"
26050 );
26051 });
26052}
26053
26054#[gpui::test]
26055async fn test_next_prev_document_highlight(cx: &mut TestAppContext) {
26056 init_test(cx, |_| {});
26057
26058 let mut cx = EditorTestContext::new(cx).await;
26059 cx.set_state(
26060 "let ˇvariable = 42;
26061let another = variable + 1;
26062let result = variable * 2;",
26063 );
26064
26065 // Set up document highlights manually (simulating LSP response)
26066 cx.update_editor(|editor, _window, cx| {
26067 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
26068
26069 // Create highlights for "variable" occurrences
26070 let highlight_ranges = [
26071 Point::new(0, 4)..Point::new(0, 12), // First "variable"
26072 Point::new(1, 14)..Point::new(1, 22), // Second "variable"
26073 Point::new(2, 13)..Point::new(2, 21), // Third "variable"
26074 ];
26075
26076 let anchor_ranges: Vec<_> = highlight_ranges
26077 .iter()
26078 .map(|range| range.clone().to_anchors(&buffer_snapshot))
26079 .collect();
26080
26081 editor.highlight_background::<DocumentHighlightRead>(
26082 &anchor_ranges,
26083 |theme| theme.colors().editor_document_highlight_read_background,
26084 cx,
26085 );
26086 });
26087
26088 // Go to next highlight - should move to second "variable"
26089 cx.update_editor(|editor, window, cx| {
26090 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26091 });
26092 cx.assert_editor_state(
26093 "let variable = 42;
26094let another = ˇvariable + 1;
26095let result = variable * 2;",
26096 );
26097
26098 // Go to next highlight - should move to third "variable"
26099 cx.update_editor(|editor, window, cx| {
26100 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26101 });
26102 cx.assert_editor_state(
26103 "let variable = 42;
26104let another = variable + 1;
26105let result = ˇvariable * 2;",
26106 );
26107
26108 // Go to next highlight - should stay at third "variable" (no wrap-around)
26109 cx.update_editor(|editor, window, cx| {
26110 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26111 });
26112 cx.assert_editor_state(
26113 "let variable = 42;
26114let another = variable + 1;
26115let result = ˇvariable * 2;",
26116 );
26117
26118 // Now test going backwards from third position
26119 cx.update_editor(|editor, window, cx| {
26120 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26121 });
26122 cx.assert_editor_state(
26123 "let variable = 42;
26124let another = ˇvariable + 1;
26125let result = variable * 2;",
26126 );
26127
26128 // Go to previous highlight - should move to first "variable"
26129 cx.update_editor(|editor, window, cx| {
26130 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26131 });
26132 cx.assert_editor_state(
26133 "let ˇvariable = 42;
26134let another = variable + 1;
26135let result = variable * 2;",
26136 );
26137
26138 // Go to previous highlight - should stay on first "variable"
26139 cx.update_editor(|editor, window, cx| {
26140 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26141 });
26142 cx.assert_editor_state(
26143 "let ˇvariable = 42;
26144let another = variable + 1;
26145let result = variable * 2;",
26146 );
26147}
26148
26149#[gpui::test]
26150async fn test_paste_url_from_other_app_creates_markdown_link_over_selected_text(
26151 cx: &mut gpui::TestAppContext,
26152) {
26153 init_test(cx, |_| {});
26154
26155 let url = "https://zed.dev";
26156
26157 let markdown_language = Arc::new(Language::new(
26158 LanguageConfig {
26159 name: "Markdown".into(),
26160 ..LanguageConfig::default()
26161 },
26162 None,
26163 ));
26164
26165 let mut cx = EditorTestContext::new(cx).await;
26166 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26167 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)");
26168
26169 cx.update_editor(|editor, window, cx| {
26170 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26171 editor.paste(&Paste, window, cx);
26172 });
26173
26174 cx.assert_editor_state(&format!(
26175 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)"
26176 ));
26177}
26178
26179#[gpui::test]
26180async fn test_paste_url_from_zed_copy_creates_markdown_link_over_selected_text(
26181 cx: &mut gpui::TestAppContext,
26182) {
26183 init_test(cx, |_| {});
26184
26185 let url = "https://zed.dev";
26186
26187 let markdown_language = Arc::new(Language::new(
26188 LanguageConfig {
26189 name: "Markdown".into(),
26190 ..LanguageConfig::default()
26191 },
26192 None,
26193 ));
26194
26195 let mut cx = EditorTestContext::new(cx).await;
26196 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26197 cx.set_state(&format!(
26198 "Hello, editor.\nZed is great (see this link: )\n«{url}ˇ»"
26199 ));
26200
26201 cx.update_editor(|editor, window, cx| {
26202 editor.copy(&Copy, window, cx);
26203 });
26204
26205 cx.set_state(&format!(
26206 "Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)\n{url}"
26207 ));
26208
26209 cx.update_editor(|editor, window, cx| {
26210 editor.paste(&Paste, window, cx);
26211 });
26212
26213 cx.assert_editor_state(&format!(
26214 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)\n{url}"
26215 ));
26216}
26217
26218#[gpui::test]
26219async fn test_paste_url_from_other_app_replaces_existing_url_without_creating_markdown_link(
26220 cx: &mut gpui::TestAppContext,
26221) {
26222 init_test(cx, |_| {});
26223
26224 let url = "https://zed.dev";
26225
26226 let markdown_language = Arc::new(Language::new(
26227 LanguageConfig {
26228 name: "Markdown".into(),
26229 ..LanguageConfig::default()
26230 },
26231 None,
26232 ));
26233
26234 let mut cx = EditorTestContext::new(cx).await;
26235 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26236 cx.set_state("Please visit zed's homepage: «https://www.apple.comˇ»");
26237
26238 cx.update_editor(|editor, window, cx| {
26239 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26240 editor.paste(&Paste, window, cx);
26241 });
26242
26243 cx.assert_editor_state(&format!("Please visit zed's homepage: {url}ˇ"));
26244}
26245
26246#[gpui::test]
26247async fn test_paste_plain_text_from_other_app_replaces_selection_without_creating_markdown_link(
26248 cx: &mut gpui::TestAppContext,
26249) {
26250 init_test(cx, |_| {});
26251
26252 let text = "Awesome";
26253
26254 let markdown_language = Arc::new(Language::new(
26255 LanguageConfig {
26256 name: "Markdown".into(),
26257 ..LanguageConfig::default()
26258 },
26259 None,
26260 ));
26261
26262 let mut cx = EditorTestContext::new(cx).await;
26263 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26264 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat»");
26265
26266 cx.update_editor(|editor, window, cx| {
26267 cx.write_to_clipboard(ClipboardItem::new_string(text.to_string()));
26268 editor.paste(&Paste, window, cx);
26269 });
26270
26271 cx.assert_editor_state(&format!("Hello, {text}ˇ.\nZed is {text}ˇ"));
26272}
26273
26274#[gpui::test]
26275async fn test_paste_url_from_other_app_without_creating_markdown_link_in_non_markdown_language(
26276 cx: &mut gpui::TestAppContext,
26277) {
26278 init_test(cx, |_| {});
26279
26280 let url = "https://zed.dev";
26281
26282 let markdown_language = Arc::new(Language::new(
26283 LanguageConfig {
26284 name: "Rust".into(),
26285 ..LanguageConfig::default()
26286 },
26287 None,
26288 ));
26289
26290 let mut cx = EditorTestContext::new(cx).await;
26291 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26292 cx.set_state("// Hello, «editorˇ».\n// Zed is «ˇgreat» (see this link: ˇ)");
26293
26294 cx.update_editor(|editor, window, cx| {
26295 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26296 editor.paste(&Paste, window, cx);
26297 });
26298
26299 cx.assert_editor_state(&format!(
26300 "// Hello, {url}ˇ.\n// Zed is {url}ˇ (see this link: {url}ˇ)"
26301 ));
26302}
26303
26304#[gpui::test]
26305async fn test_paste_url_from_other_app_creates_markdown_link_selectively_in_multi_buffer(
26306 cx: &mut TestAppContext,
26307) {
26308 init_test(cx, |_| {});
26309
26310 let url = "https://zed.dev";
26311
26312 let markdown_language = Arc::new(Language::new(
26313 LanguageConfig {
26314 name: "Markdown".into(),
26315 ..LanguageConfig::default()
26316 },
26317 None,
26318 ));
26319
26320 let (editor, cx) = cx.add_window_view(|window, cx| {
26321 let multi_buffer = MultiBuffer::build_multi(
26322 [
26323 ("this will embed -> link", vec![Point::row_range(0..1)]),
26324 ("this will replace -> link", vec![Point::row_range(0..1)]),
26325 ],
26326 cx,
26327 );
26328 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
26329 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26330 s.select_ranges(vec![
26331 Point::new(0, 19)..Point::new(0, 23),
26332 Point::new(1, 21)..Point::new(1, 25),
26333 ])
26334 });
26335 let first_buffer_id = multi_buffer
26336 .read(cx)
26337 .excerpt_buffer_ids()
26338 .into_iter()
26339 .next()
26340 .unwrap();
26341 let first_buffer = multi_buffer.read(cx).buffer(first_buffer_id).unwrap();
26342 first_buffer.update(cx, |buffer, cx| {
26343 buffer.set_language(Some(markdown_language.clone()), cx);
26344 });
26345
26346 editor
26347 });
26348 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
26349
26350 cx.update_editor(|editor, window, cx| {
26351 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26352 editor.paste(&Paste, window, cx);
26353 });
26354
26355 cx.assert_editor_state(&format!(
26356 "this will embed -> [link]({url})ˇ\nthis will replace -> {url}ˇ"
26357 ));
26358}
26359
26360#[gpui::test]
26361async fn test_race_in_multibuffer_save(cx: &mut TestAppContext) {
26362 init_test(cx, |_| {});
26363
26364 let fs = FakeFs::new(cx.executor());
26365 fs.insert_tree(
26366 path!("/project"),
26367 json!({
26368 "first.rs": "# First Document\nSome content here.",
26369 "second.rs": "Plain text content for second file.",
26370 }),
26371 )
26372 .await;
26373
26374 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
26375 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
26376 let cx = &mut VisualTestContext::from_window(*workspace, cx);
26377
26378 let language = rust_lang();
26379 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
26380 language_registry.add(language.clone());
26381 let mut fake_servers = language_registry.register_fake_lsp(
26382 "Rust",
26383 FakeLspAdapter {
26384 ..FakeLspAdapter::default()
26385 },
26386 );
26387
26388 let buffer1 = project
26389 .update(cx, |project, cx| {
26390 project.open_local_buffer(PathBuf::from(path!("/project/first.rs")), cx)
26391 })
26392 .await
26393 .unwrap();
26394 let buffer2 = project
26395 .update(cx, |project, cx| {
26396 project.open_local_buffer(PathBuf::from(path!("/project/second.rs")), cx)
26397 })
26398 .await
26399 .unwrap();
26400
26401 let multi_buffer = cx.new(|cx| {
26402 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
26403 multi_buffer.set_excerpts_for_path(
26404 PathKey::for_buffer(&buffer1, cx),
26405 buffer1.clone(),
26406 [Point::zero()..buffer1.read(cx).max_point()],
26407 3,
26408 cx,
26409 );
26410 multi_buffer.set_excerpts_for_path(
26411 PathKey::for_buffer(&buffer2, cx),
26412 buffer2.clone(),
26413 [Point::zero()..buffer1.read(cx).max_point()],
26414 3,
26415 cx,
26416 );
26417 multi_buffer
26418 });
26419
26420 let (editor, cx) = cx.add_window_view(|window, cx| {
26421 Editor::new(
26422 EditorMode::full(),
26423 multi_buffer,
26424 Some(project.clone()),
26425 window,
26426 cx,
26427 )
26428 });
26429
26430 let fake_language_server = fake_servers.next().await.unwrap();
26431
26432 buffer1.update(cx, |buffer, cx| buffer.edit([(0..0, "hello!")], None, cx));
26433
26434 let save = editor.update_in(cx, |editor, window, cx| {
26435 assert!(editor.is_dirty(cx));
26436
26437 editor.save(
26438 SaveOptions {
26439 format: true,
26440 autosave: true,
26441 },
26442 project,
26443 window,
26444 cx,
26445 )
26446 });
26447 let (start_edit_tx, start_edit_rx) = oneshot::channel();
26448 let (done_edit_tx, done_edit_rx) = oneshot::channel();
26449 let mut done_edit_rx = Some(done_edit_rx);
26450 let mut start_edit_tx = Some(start_edit_tx);
26451
26452 fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| {
26453 start_edit_tx.take().unwrap().send(()).unwrap();
26454 let done_edit_rx = done_edit_rx.take().unwrap();
26455 async move {
26456 done_edit_rx.await.unwrap();
26457 Ok(None)
26458 }
26459 });
26460
26461 start_edit_rx.await.unwrap();
26462 buffer2
26463 .update(cx, |buffer, cx| buffer.edit([(0..0, "world!")], None, cx))
26464 .unwrap();
26465
26466 done_edit_tx.send(()).unwrap();
26467
26468 save.await.unwrap();
26469 cx.update(|_, cx| assert!(editor.is_dirty(cx)));
26470}
26471
26472#[track_caller]
26473fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
26474 editor
26475 .all_inlays(cx)
26476 .into_iter()
26477 .filter_map(|inlay| inlay.get_color())
26478 .map(Rgba::from)
26479 .collect()
26480}
26481
26482#[gpui::test]
26483fn test_duplicate_line_up_on_last_line_without_newline(cx: &mut TestAppContext) {
26484 init_test(cx, |_| {});
26485
26486 let editor = cx.add_window(|window, cx| {
26487 let buffer = MultiBuffer::build_simple("line1\nline2", cx);
26488 build_editor(buffer, window, cx)
26489 });
26490
26491 editor
26492 .update(cx, |editor, window, cx| {
26493 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26494 s.select_display_ranges([
26495 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
26496 ])
26497 });
26498
26499 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
26500
26501 assert_eq!(
26502 editor.display_text(cx),
26503 "line1\nline2\nline2",
26504 "Duplicating last line upward should create duplicate above, not on same line"
26505 );
26506
26507 assert_eq!(
26508 editor.selections.display_ranges(cx),
26509 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)],
26510 "Selection should remain on the original line"
26511 );
26512 })
26513 .unwrap();
26514}
26515
26516#[gpui::test]
26517async fn test_copy_line_without_trailing_newline(cx: &mut TestAppContext) {
26518 init_test(cx, |_| {});
26519
26520 let mut cx = EditorTestContext::new(cx).await;
26521
26522 cx.set_state("line1\nline2ˇ");
26523
26524 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
26525
26526 let clipboard_text = cx
26527 .read_from_clipboard()
26528 .and_then(|item| item.text().as_deref().map(str::to_string));
26529
26530 assert_eq!(
26531 clipboard_text,
26532 Some("line2\n".to_string()),
26533 "Copying a line without trailing newline should include a newline"
26534 );
26535
26536 cx.set_state("line1\nˇ");
26537
26538 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
26539
26540 cx.assert_editor_state("line1\nline2\nˇ");
26541}