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 // Submit a format request.
12420 let format = cx
12421 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12422 .unwrap();
12423
12424 // Record which buffer changes have been sent to the language server
12425 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
12426 cx.lsp
12427 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
12428 let buffer_changes = buffer_changes.clone();
12429 move |params, _| {
12430 buffer_changes.lock().extend(
12431 params
12432 .content_changes
12433 .into_iter()
12434 .map(|e| (e.range.unwrap(), e.text)),
12435 );
12436 }
12437 });
12438
12439 // Handle formatting requests to the language server.
12440 cx.lsp
12441 .set_request_handler::<lsp::request::Formatting, _, _>({
12442 let buffer_changes = buffer_changes.clone();
12443 move |_, _| {
12444 // When formatting is requested, trailing whitespace has already been stripped,
12445 // and the trailing newline has already been added.
12446 assert_eq!(
12447 &buffer_changes.lock()[1..],
12448 &[
12449 (
12450 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
12451 "".into()
12452 ),
12453 (
12454 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
12455 "".into()
12456 ),
12457 (
12458 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
12459 "\n".into()
12460 ),
12461 ]
12462 );
12463
12464 // Insert blank lines between each line of the buffer.
12465 async move {
12466 Ok(Some(vec![
12467 lsp::TextEdit {
12468 range: lsp::Range::new(
12469 lsp::Position::new(1, 0),
12470 lsp::Position::new(1, 0),
12471 ),
12472 new_text: "\n".into(),
12473 },
12474 lsp::TextEdit {
12475 range: lsp::Range::new(
12476 lsp::Position::new(2, 0),
12477 lsp::Position::new(2, 0),
12478 ),
12479 new_text: "\n".into(),
12480 },
12481 ]))
12482 }
12483 }
12484 });
12485
12486 // After formatting the buffer, the trailing whitespace is stripped,
12487 // a newline is appended, and the edits provided by the language server
12488 // have been applied.
12489 format.await.unwrap();
12490 cx.assert_editor_state(
12491 &[
12492 "one", //
12493 "", //
12494 "twoˇ", //
12495 "", //
12496 "three", //
12497 "four", //
12498 "", //
12499 ]
12500 .join("\n"),
12501 );
12502
12503 // Undoing the formatting undoes the trailing whitespace removal, the
12504 // trailing newline, and the LSP edits.
12505 cx.update_buffer(|buffer, cx| buffer.undo(cx));
12506 cx.assert_editor_state(
12507 &[
12508 "one ", //
12509 "twoˇ", //
12510 "three ", //
12511 "four", //
12512 ]
12513 .join("\n"),
12514 );
12515}
12516
12517#[gpui::test]
12518async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
12519 cx: &mut TestAppContext,
12520) {
12521 init_test(cx, |_| {});
12522
12523 cx.update(|cx| {
12524 cx.update_global::<SettingsStore, _>(|settings, cx| {
12525 settings.update_user_settings(cx, |settings| {
12526 settings.editor.auto_signature_help = Some(true);
12527 });
12528 });
12529 });
12530
12531 let mut cx = EditorLspTestContext::new_rust(
12532 lsp::ServerCapabilities {
12533 signature_help_provider: Some(lsp::SignatureHelpOptions {
12534 ..Default::default()
12535 }),
12536 ..Default::default()
12537 },
12538 cx,
12539 )
12540 .await;
12541
12542 let language = Language::new(
12543 LanguageConfig {
12544 name: "Rust".into(),
12545 brackets: BracketPairConfig {
12546 pairs: vec![
12547 BracketPair {
12548 start: "{".to_string(),
12549 end: "}".to_string(),
12550 close: true,
12551 surround: true,
12552 newline: true,
12553 },
12554 BracketPair {
12555 start: "(".to_string(),
12556 end: ")".to_string(),
12557 close: true,
12558 surround: true,
12559 newline: true,
12560 },
12561 BracketPair {
12562 start: "/*".to_string(),
12563 end: " */".to_string(),
12564 close: true,
12565 surround: true,
12566 newline: true,
12567 },
12568 BracketPair {
12569 start: "[".to_string(),
12570 end: "]".to_string(),
12571 close: false,
12572 surround: false,
12573 newline: true,
12574 },
12575 BracketPair {
12576 start: "\"".to_string(),
12577 end: "\"".to_string(),
12578 close: true,
12579 surround: true,
12580 newline: false,
12581 },
12582 BracketPair {
12583 start: "<".to_string(),
12584 end: ">".to_string(),
12585 close: false,
12586 surround: true,
12587 newline: true,
12588 },
12589 ],
12590 ..Default::default()
12591 },
12592 autoclose_before: "})]".to_string(),
12593 ..Default::default()
12594 },
12595 Some(tree_sitter_rust::LANGUAGE.into()),
12596 );
12597 let language = Arc::new(language);
12598
12599 cx.language_registry().add(language.clone());
12600 cx.update_buffer(|buffer, cx| {
12601 buffer.set_language(Some(language), cx);
12602 });
12603
12604 cx.set_state(
12605 &r#"
12606 fn main() {
12607 sampleˇ
12608 }
12609 "#
12610 .unindent(),
12611 );
12612
12613 cx.update_editor(|editor, window, cx| {
12614 editor.handle_input("(", window, cx);
12615 });
12616 cx.assert_editor_state(
12617 &"
12618 fn main() {
12619 sample(ˇ)
12620 }
12621 "
12622 .unindent(),
12623 );
12624
12625 let mocked_response = lsp::SignatureHelp {
12626 signatures: vec![lsp::SignatureInformation {
12627 label: "fn sample(param1: u8, param2: u8)".to_string(),
12628 documentation: None,
12629 parameters: Some(vec![
12630 lsp::ParameterInformation {
12631 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12632 documentation: None,
12633 },
12634 lsp::ParameterInformation {
12635 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12636 documentation: None,
12637 },
12638 ]),
12639 active_parameter: None,
12640 }],
12641 active_signature: Some(0),
12642 active_parameter: Some(0),
12643 };
12644 handle_signature_help_request(&mut cx, mocked_response).await;
12645
12646 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12647 .await;
12648
12649 cx.editor(|editor, _, _| {
12650 let signature_help_state = editor.signature_help_state.popover().cloned();
12651 let signature = signature_help_state.unwrap();
12652 assert_eq!(
12653 signature.signatures[signature.current_signature].label,
12654 "fn sample(param1: u8, param2: u8)"
12655 );
12656 });
12657}
12658
12659#[gpui::test]
12660async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
12661 init_test(cx, |_| {});
12662
12663 cx.update(|cx| {
12664 cx.update_global::<SettingsStore, _>(|settings, cx| {
12665 settings.update_user_settings(cx, |settings| {
12666 settings.editor.auto_signature_help = Some(false);
12667 settings.editor.show_signature_help_after_edits = Some(false);
12668 });
12669 });
12670 });
12671
12672 let mut cx = EditorLspTestContext::new_rust(
12673 lsp::ServerCapabilities {
12674 signature_help_provider: Some(lsp::SignatureHelpOptions {
12675 ..Default::default()
12676 }),
12677 ..Default::default()
12678 },
12679 cx,
12680 )
12681 .await;
12682
12683 let language = Language::new(
12684 LanguageConfig {
12685 name: "Rust".into(),
12686 brackets: BracketPairConfig {
12687 pairs: vec![
12688 BracketPair {
12689 start: "{".to_string(),
12690 end: "}".to_string(),
12691 close: true,
12692 surround: true,
12693 newline: true,
12694 },
12695 BracketPair {
12696 start: "(".to_string(),
12697 end: ")".to_string(),
12698 close: true,
12699 surround: true,
12700 newline: true,
12701 },
12702 BracketPair {
12703 start: "/*".to_string(),
12704 end: " */".to_string(),
12705 close: true,
12706 surround: true,
12707 newline: true,
12708 },
12709 BracketPair {
12710 start: "[".to_string(),
12711 end: "]".to_string(),
12712 close: false,
12713 surround: false,
12714 newline: true,
12715 },
12716 BracketPair {
12717 start: "\"".to_string(),
12718 end: "\"".to_string(),
12719 close: true,
12720 surround: true,
12721 newline: false,
12722 },
12723 BracketPair {
12724 start: "<".to_string(),
12725 end: ">".to_string(),
12726 close: false,
12727 surround: true,
12728 newline: true,
12729 },
12730 ],
12731 ..Default::default()
12732 },
12733 autoclose_before: "})]".to_string(),
12734 ..Default::default()
12735 },
12736 Some(tree_sitter_rust::LANGUAGE.into()),
12737 );
12738 let language = Arc::new(language);
12739
12740 cx.language_registry().add(language.clone());
12741 cx.update_buffer(|buffer, cx| {
12742 buffer.set_language(Some(language), cx);
12743 });
12744
12745 // Ensure that signature_help is not called when no signature help is enabled.
12746 cx.set_state(
12747 &r#"
12748 fn main() {
12749 sampleˇ
12750 }
12751 "#
12752 .unindent(),
12753 );
12754 cx.update_editor(|editor, window, cx| {
12755 editor.handle_input("(", window, cx);
12756 });
12757 cx.assert_editor_state(
12758 &"
12759 fn main() {
12760 sample(ˇ)
12761 }
12762 "
12763 .unindent(),
12764 );
12765 cx.editor(|editor, _, _| {
12766 assert!(editor.signature_help_state.task().is_none());
12767 });
12768
12769 let mocked_response = lsp::SignatureHelp {
12770 signatures: vec![lsp::SignatureInformation {
12771 label: "fn sample(param1: u8, param2: u8)".to_string(),
12772 documentation: None,
12773 parameters: Some(vec![
12774 lsp::ParameterInformation {
12775 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12776 documentation: None,
12777 },
12778 lsp::ParameterInformation {
12779 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12780 documentation: None,
12781 },
12782 ]),
12783 active_parameter: None,
12784 }],
12785 active_signature: Some(0),
12786 active_parameter: Some(0),
12787 };
12788
12789 // Ensure that signature_help is called when enabled afte edits
12790 cx.update(|_, cx| {
12791 cx.update_global::<SettingsStore, _>(|settings, cx| {
12792 settings.update_user_settings(cx, |settings| {
12793 settings.editor.auto_signature_help = Some(false);
12794 settings.editor.show_signature_help_after_edits = Some(true);
12795 });
12796 });
12797 });
12798 cx.set_state(
12799 &r#"
12800 fn main() {
12801 sampleˇ
12802 }
12803 "#
12804 .unindent(),
12805 );
12806 cx.update_editor(|editor, window, cx| {
12807 editor.handle_input("(", window, cx);
12808 });
12809 cx.assert_editor_state(
12810 &"
12811 fn main() {
12812 sample(ˇ)
12813 }
12814 "
12815 .unindent(),
12816 );
12817 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12818 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12819 .await;
12820 cx.update_editor(|editor, _, _| {
12821 let signature_help_state = editor.signature_help_state.popover().cloned();
12822 assert!(signature_help_state.is_some());
12823 let signature = signature_help_state.unwrap();
12824 assert_eq!(
12825 signature.signatures[signature.current_signature].label,
12826 "fn sample(param1: u8, param2: u8)"
12827 );
12828 editor.signature_help_state = SignatureHelpState::default();
12829 });
12830
12831 // Ensure that signature_help is called when auto signature help override is enabled
12832 cx.update(|_, cx| {
12833 cx.update_global::<SettingsStore, _>(|settings, cx| {
12834 settings.update_user_settings(cx, |settings| {
12835 settings.editor.auto_signature_help = Some(true);
12836 settings.editor.show_signature_help_after_edits = Some(false);
12837 });
12838 });
12839 });
12840 cx.set_state(
12841 &r#"
12842 fn main() {
12843 sampleˇ
12844 }
12845 "#
12846 .unindent(),
12847 );
12848 cx.update_editor(|editor, window, cx| {
12849 editor.handle_input("(", window, cx);
12850 });
12851 cx.assert_editor_state(
12852 &"
12853 fn main() {
12854 sample(ˇ)
12855 }
12856 "
12857 .unindent(),
12858 );
12859 handle_signature_help_request(&mut cx, mocked_response).await;
12860 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12861 .await;
12862 cx.editor(|editor, _, _| {
12863 let signature_help_state = editor.signature_help_state.popover().cloned();
12864 assert!(signature_help_state.is_some());
12865 let signature = signature_help_state.unwrap();
12866 assert_eq!(
12867 signature.signatures[signature.current_signature].label,
12868 "fn sample(param1: u8, param2: u8)"
12869 );
12870 });
12871}
12872
12873#[gpui::test]
12874async fn test_signature_help(cx: &mut TestAppContext) {
12875 init_test(cx, |_| {});
12876 cx.update(|cx| {
12877 cx.update_global::<SettingsStore, _>(|settings, cx| {
12878 settings.update_user_settings(cx, |settings| {
12879 settings.editor.auto_signature_help = Some(true);
12880 });
12881 });
12882 });
12883
12884 let mut cx = EditorLspTestContext::new_rust(
12885 lsp::ServerCapabilities {
12886 signature_help_provider: Some(lsp::SignatureHelpOptions {
12887 ..Default::default()
12888 }),
12889 ..Default::default()
12890 },
12891 cx,
12892 )
12893 .await;
12894
12895 // A test that directly calls `show_signature_help`
12896 cx.update_editor(|editor, window, cx| {
12897 editor.show_signature_help(&ShowSignatureHelp, window, cx);
12898 });
12899
12900 let mocked_response = lsp::SignatureHelp {
12901 signatures: vec![lsp::SignatureInformation {
12902 label: "fn sample(param1: u8, param2: u8)".to_string(),
12903 documentation: None,
12904 parameters: Some(vec![
12905 lsp::ParameterInformation {
12906 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12907 documentation: None,
12908 },
12909 lsp::ParameterInformation {
12910 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12911 documentation: None,
12912 },
12913 ]),
12914 active_parameter: None,
12915 }],
12916 active_signature: Some(0),
12917 active_parameter: Some(0),
12918 };
12919 handle_signature_help_request(&mut cx, mocked_response).await;
12920
12921 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12922 .await;
12923
12924 cx.editor(|editor, _, _| {
12925 let signature_help_state = editor.signature_help_state.popover().cloned();
12926 assert!(signature_help_state.is_some());
12927 let signature = signature_help_state.unwrap();
12928 assert_eq!(
12929 signature.signatures[signature.current_signature].label,
12930 "fn sample(param1: u8, param2: u8)"
12931 );
12932 });
12933
12934 // When exiting outside from inside the brackets, `signature_help` is closed.
12935 cx.set_state(indoc! {"
12936 fn main() {
12937 sample(ˇ);
12938 }
12939
12940 fn sample(param1: u8, param2: u8) {}
12941 "});
12942
12943 cx.update_editor(|editor, window, cx| {
12944 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12945 s.select_ranges([0..0])
12946 });
12947 });
12948
12949 let mocked_response = lsp::SignatureHelp {
12950 signatures: Vec::new(),
12951 active_signature: None,
12952 active_parameter: None,
12953 };
12954 handle_signature_help_request(&mut cx, mocked_response).await;
12955
12956 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
12957 .await;
12958
12959 cx.editor(|editor, _, _| {
12960 assert!(!editor.signature_help_state.is_shown());
12961 });
12962
12963 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
12964 cx.set_state(indoc! {"
12965 fn main() {
12966 sample(ˇ);
12967 }
12968
12969 fn sample(param1: u8, param2: u8) {}
12970 "});
12971
12972 let mocked_response = lsp::SignatureHelp {
12973 signatures: vec![lsp::SignatureInformation {
12974 label: "fn sample(param1: u8, param2: u8)".to_string(),
12975 documentation: None,
12976 parameters: Some(vec![
12977 lsp::ParameterInformation {
12978 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12979 documentation: None,
12980 },
12981 lsp::ParameterInformation {
12982 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12983 documentation: None,
12984 },
12985 ]),
12986 active_parameter: None,
12987 }],
12988 active_signature: Some(0),
12989 active_parameter: Some(0),
12990 };
12991 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12992 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12993 .await;
12994 cx.editor(|editor, _, _| {
12995 assert!(editor.signature_help_state.is_shown());
12996 });
12997
12998 // Restore the popover with more parameter input
12999 cx.set_state(indoc! {"
13000 fn main() {
13001 sample(param1, param2ˇ);
13002 }
13003
13004 fn sample(param1: u8, param2: u8) {}
13005 "});
13006
13007 let mocked_response = lsp::SignatureHelp {
13008 signatures: vec![lsp::SignatureInformation {
13009 label: "fn sample(param1: u8, param2: u8)".to_string(),
13010 documentation: None,
13011 parameters: Some(vec![
13012 lsp::ParameterInformation {
13013 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13014 documentation: None,
13015 },
13016 lsp::ParameterInformation {
13017 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13018 documentation: None,
13019 },
13020 ]),
13021 active_parameter: None,
13022 }],
13023 active_signature: Some(0),
13024 active_parameter: Some(1),
13025 };
13026 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13027 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13028 .await;
13029
13030 // When selecting a range, the popover is gone.
13031 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
13032 cx.update_editor(|editor, window, cx| {
13033 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13034 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13035 })
13036 });
13037 cx.assert_editor_state(indoc! {"
13038 fn main() {
13039 sample(param1, «ˇparam2»);
13040 }
13041
13042 fn sample(param1: u8, param2: u8) {}
13043 "});
13044 cx.editor(|editor, _, _| {
13045 assert!(!editor.signature_help_state.is_shown());
13046 });
13047
13048 // When unselecting again, the popover is back if within the brackets.
13049 cx.update_editor(|editor, window, cx| {
13050 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13051 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13052 })
13053 });
13054 cx.assert_editor_state(indoc! {"
13055 fn main() {
13056 sample(param1, ˇparam2);
13057 }
13058
13059 fn sample(param1: u8, param2: u8) {}
13060 "});
13061 handle_signature_help_request(&mut cx, mocked_response).await;
13062 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13063 .await;
13064 cx.editor(|editor, _, _| {
13065 assert!(editor.signature_help_state.is_shown());
13066 });
13067
13068 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
13069 cx.update_editor(|editor, window, cx| {
13070 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13071 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
13072 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13073 })
13074 });
13075 cx.assert_editor_state(indoc! {"
13076 fn main() {
13077 sample(param1, ˇparam2);
13078 }
13079
13080 fn sample(param1: u8, param2: u8) {}
13081 "});
13082
13083 let mocked_response = lsp::SignatureHelp {
13084 signatures: vec![lsp::SignatureInformation {
13085 label: "fn sample(param1: u8, param2: u8)".to_string(),
13086 documentation: None,
13087 parameters: Some(vec![
13088 lsp::ParameterInformation {
13089 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13090 documentation: None,
13091 },
13092 lsp::ParameterInformation {
13093 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13094 documentation: None,
13095 },
13096 ]),
13097 active_parameter: None,
13098 }],
13099 active_signature: Some(0),
13100 active_parameter: Some(1),
13101 };
13102 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13103 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13104 .await;
13105 cx.update_editor(|editor, _, cx| {
13106 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
13107 });
13108 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13109 .await;
13110 cx.update_editor(|editor, window, cx| {
13111 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13112 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13113 })
13114 });
13115 cx.assert_editor_state(indoc! {"
13116 fn main() {
13117 sample(param1, «ˇparam2»);
13118 }
13119
13120 fn sample(param1: u8, param2: u8) {}
13121 "});
13122 cx.update_editor(|editor, window, cx| {
13123 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13124 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13125 })
13126 });
13127 cx.assert_editor_state(indoc! {"
13128 fn main() {
13129 sample(param1, ˇparam2);
13130 }
13131
13132 fn sample(param1: u8, param2: u8) {}
13133 "});
13134 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
13135 .await;
13136}
13137
13138#[gpui::test]
13139async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
13140 init_test(cx, |_| {});
13141
13142 let mut cx = EditorLspTestContext::new_rust(
13143 lsp::ServerCapabilities {
13144 signature_help_provider: Some(lsp::SignatureHelpOptions {
13145 ..Default::default()
13146 }),
13147 ..Default::default()
13148 },
13149 cx,
13150 )
13151 .await;
13152
13153 cx.set_state(indoc! {"
13154 fn main() {
13155 overloadedˇ
13156 }
13157 "});
13158
13159 cx.update_editor(|editor, window, cx| {
13160 editor.handle_input("(", window, cx);
13161 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13162 });
13163
13164 // Mock response with 3 signatures
13165 let mocked_response = lsp::SignatureHelp {
13166 signatures: vec![
13167 lsp::SignatureInformation {
13168 label: "fn overloaded(x: i32)".to_string(),
13169 documentation: None,
13170 parameters: Some(vec![lsp::ParameterInformation {
13171 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13172 documentation: None,
13173 }]),
13174 active_parameter: None,
13175 },
13176 lsp::SignatureInformation {
13177 label: "fn overloaded(x: i32, y: i32)".to_string(),
13178 documentation: None,
13179 parameters: Some(vec![
13180 lsp::ParameterInformation {
13181 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13182 documentation: None,
13183 },
13184 lsp::ParameterInformation {
13185 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13186 documentation: None,
13187 },
13188 ]),
13189 active_parameter: None,
13190 },
13191 lsp::SignatureInformation {
13192 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
13193 documentation: None,
13194 parameters: Some(vec![
13195 lsp::ParameterInformation {
13196 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13197 documentation: None,
13198 },
13199 lsp::ParameterInformation {
13200 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13201 documentation: None,
13202 },
13203 lsp::ParameterInformation {
13204 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
13205 documentation: None,
13206 },
13207 ]),
13208 active_parameter: None,
13209 },
13210 ],
13211 active_signature: Some(1),
13212 active_parameter: Some(0),
13213 };
13214 handle_signature_help_request(&mut cx, mocked_response).await;
13215
13216 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13217 .await;
13218
13219 // Verify we have multiple signatures and the right one is selected
13220 cx.editor(|editor, _, _| {
13221 let popover = editor.signature_help_state.popover().cloned().unwrap();
13222 assert_eq!(popover.signatures.len(), 3);
13223 // active_signature was 1, so that should be the current
13224 assert_eq!(popover.current_signature, 1);
13225 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
13226 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
13227 assert_eq!(
13228 popover.signatures[2].label,
13229 "fn overloaded(x: i32, y: i32, z: i32)"
13230 );
13231 });
13232
13233 // Test navigation functionality
13234 cx.update_editor(|editor, window, cx| {
13235 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13236 });
13237
13238 cx.editor(|editor, _, _| {
13239 let popover = editor.signature_help_state.popover().cloned().unwrap();
13240 assert_eq!(popover.current_signature, 2);
13241 });
13242
13243 // Test wrap around
13244 cx.update_editor(|editor, window, cx| {
13245 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13246 });
13247
13248 cx.editor(|editor, _, _| {
13249 let popover = editor.signature_help_state.popover().cloned().unwrap();
13250 assert_eq!(popover.current_signature, 0);
13251 });
13252
13253 // Test previous navigation
13254 cx.update_editor(|editor, window, cx| {
13255 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
13256 });
13257
13258 cx.editor(|editor, _, _| {
13259 let popover = editor.signature_help_state.popover().cloned().unwrap();
13260 assert_eq!(popover.current_signature, 2);
13261 });
13262}
13263
13264#[gpui::test]
13265async fn test_completion_mode(cx: &mut TestAppContext) {
13266 init_test(cx, |_| {});
13267 let mut cx = EditorLspTestContext::new_rust(
13268 lsp::ServerCapabilities {
13269 completion_provider: Some(lsp::CompletionOptions {
13270 resolve_provider: Some(true),
13271 ..Default::default()
13272 }),
13273 ..Default::default()
13274 },
13275 cx,
13276 )
13277 .await;
13278
13279 struct Run {
13280 run_description: &'static str,
13281 initial_state: String,
13282 buffer_marked_text: String,
13283 completion_label: &'static str,
13284 completion_text: &'static str,
13285 expected_with_insert_mode: String,
13286 expected_with_replace_mode: String,
13287 expected_with_replace_subsequence_mode: String,
13288 expected_with_replace_suffix_mode: String,
13289 }
13290
13291 let runs = [
13292 Run {
13293 run_description: "Start of word matches completion text",
13294 initial_state: "before ediˇ after".into(),
13295 buffer_marked_text: "before <edi|> after".into(),
13296 completion_label: "editor",
13297 completion_text: "editor",
13298 expected_with_insert_mode: "before editorˇ after".into(),
13299 expected_with_replace_mode: "before editorˇ after".into(),
13300 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13301 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13302 },
13303 Run {
13304 run_description: "Accept same text at the middle of the word",
13305 initial_state: "before ediˇtor after".into(),
13306 buffer_marked_text: "before <edi|tor> after".into(),
13307 completion_label: "editor",
13308 completion_text: "editor",
13309 expected_with_insert_mode: "before editorˇtor after".into(),
13310 expected_with_replace_mode: "before editorˇ after".into(),
13311 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13312 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13313 },
13314 Run {
13315 run_description: "End of word matches completion text -- cursor at end",
13316 initial_state: "before torˇ after".into(),
13317 buffer_marked_text: "before <tor|> after".into(),
13318 completion_label: "editor",
13319 completion_text: "editor",
13320 expected_with_insert_mode: "before editorˇ after".into(),
13321 expected_with_replace_mode: "before editorˇ after".into(),
13322 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13323 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13324 },
13325 Run {
13326 run_description: "End of word matches completion text -- cursor at start",
13327 initial_state: "before ˇtor after".into(),
13328 buffer_marked_text: "before <|tor> after".into(),
13329 completion_label: "editor",
13330 completion_text: "editor",
13331 expected_with_insert_mode: "before editorˇtor after".into(),
13332 expected_with_replace_mode: "before editorˇ after".into(),
13333 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13334 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13335 },
13336 Run {
13337 run_description: "Prepend text containing whitespace",
13338 initial_state: "pˇfield: bool".into(),
13339 buffer_marked_text: "<p|field>: bool".into(),
13340 completion_label: "pub ",
13341 completion_text: "pub ",
13342 expected_with_insert_mode: "pub ˇfield: bool".into(),
13343 expected_with_replace_mode: "pub ˇ: bool".into(),
13344 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
13345 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
13346 },
13347 Run {
13348 run_description: "Add element to start of list",
13349 initial_state: "[element_ˇelement_2]".into(),
13350 buffer_marked_text: "[<element_|element_2>]".into(),
13351 completion_label: "element_1",
13352 completion_text: "element_1",
13353 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
13354 expected_with_replace_mode: "[element_1ˇ]".into(),
13355 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
13356 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
13357 },
13358 Run {
13359 run_description: "Add element to start of list -- first and second elements are equal",
13360 initial_state: "[elˇelement]".into(),
13361 buffer_marked_text: "[<el|element>]".into(),
13362 completion_label: "element",
13363 completion_text: "element",
13364 expected_with_insert_mode: "[elementˇelement]".into(),
13365 expected_with_replace_mode: "[elementˇ]".into(),
13366 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
13367 expected_with_replace_suffix_mode: "[elementˇ]".into(),
13368 },
13369 Run {
13370 run_description: "Ends with matching suffix",
13371 initial_state: "SubˇError".into(),
13372 buffer_marked_text: "<Sub|Error>".into(),
13373 completion_label: "SubscriptionError",
13374 completion_text: "SubscriptionError",
13375 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
13376 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13377 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13378 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
13379 },
13380 Run {
13381 run_description: "Suffix is a subsequence -- contiguous",
13382 initial_state: "SubˇErr".into(),
13383 buffer_marked_text: "<Sub|Err>".into(),
13384 completion_label: "SubscriptionError",
13385 completion_text: "SubscriptionError",
13386 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
13387 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13388 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13389 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
13390 },
13391 Run {
13392 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
13393 initial_state: "Suˇscrirr".into(),
13394 buffer_marked_text: "<Su|scrirr>".into(),
13395 completion_label: "SubscriptionError",
13396 completion_text: "SubscriptionError",
13397 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
13398 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13399 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13400 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
13401 },
13402 Run {
13403 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
13404 initial_state: "foo(indˇix)".into(),
13405 buffer_marked_text: "foo(<ind|ix>)".into(),
13406 completion_label: "node_index",
13407 completion_text: "node_index",
13408 expected_with_insert_mode: "foo(node_indexˇix)".into(),
13409 expected_with_replace_mode: "foo(node_indexˇ)".into(),
13410 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
13411 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
13412 },
13413 Run {
13414 run_description: "Replace range ends before cursor - should extend to cursor",
13415 initial_state: "before editˇo after".into(),
13416 buffer_marked_text: "before <{ed}>it|o after".into(),
13417 completion_label: "editor",
13418 completion_text: "editor",
13419 expected_with_insert_mode: "before editorˇo after".into(),
13420 expected_with_replace_mode: "before editorˇo after".into(),
13421 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
13422 expected_with_replace_suffix_mode: "before editorˇo after".into(),
13423 },
13424 Run {
13425 run_description: "Uses label for suffix matching",
13426 initial_state: "before ediˇtor after".into(),
13427 buffer_marked_text: "before <edi|tor> after".into(),
13428 completion_label: "editor",
13429 completion_text: "editor()",
13430 expected_with_insert_mode: "before editor()ˇtor after".into(),
13431 expected_with_replace_mode: "before editor()ˇ after".into(),
13432 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
13433 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
13434 },
13435 Run {
13436 run_description: "Case insensitive subsequence and suffix matching",
13437 initial_state: "before EDiˇtoR after".into(),
13438 buffer_marked_text: "before <EDi|toR> after".into(),
13439 completion_label: "editor",
13440 completion_text: "editor",
13441 expected_with_insert_mode: "before editorˇtoR after".into(),
13442 expected_with_replace_mode: "before editorˇ after".into(),
13443 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13444 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13445 },
13446 ];
13447
13448 for run in runs {
13449 let run_variations = [
13450 (LspInsertMode::Insert, run.expected_with_insert_mode),
13451 (LspInsertMode::Replace, run.expected_with_replace_mode),
13452 (
13453 LspInsertMode::ReplaceSubsequence,
13454 run.expected_with_replace_subsequence_mode,
13455 ),
13456 (
13457 LspInsertMode::ReplaceSuffix,
13458 run.expected_with_replace_suffix_mode,
13459 ),
13460 ];
13461
13462 for (lsp_insert_mode, expected_text) in run_variations {
13463 eprintln!(
13464 "run = {:?}, mode = {lsp_insert_mode:.?}",
13465 run.run_description,
13466 );
13467
13468 update_test_language_settings(&mut cx, |settings| {
13469 settings.defaults.completions = Some(CompletionSettingsContent {
13470 lsp_insert_mode: Some(lsp_insert_mode),
13471 words: Some(WordsCompletionMode::Disabled),
13472 words_min_length: Some(0),
13473 ..Default::default()
13474 });
13475 });
13476
13477 cx.set_state(&run.initial_state);
13478 cx.update_editor(|editor, window, cx| {
13479 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13480 });
13481
13482 let counter = Arc::new(AtomicUsize::new(0));
13483 handle_completion_request_with_insert_and_replace(
13484 &mut cx,
13485 &run.buffer_marked_text,
13486 vec![(run.completion_label, run.completion_text)],
13487 counter.clone(),
13488 )
13489 .await;
13490 cx.condition(|editor, _| editor.context_menu_visible())
13491 .await;
13492 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13493
13494 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13495 editor
13496 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13497 .unwrap()
13498 });
13499 cx.assert_editor_state(&expected_text);
13500 handle_resolve_completion_request(&mut cx, None).await;
13501 apply_additional_edits.await.unwrap();
13502 }
13503 }
13504}
13505
13506#[gpui::test]
13507async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
13508 init_test(cx, |_| {});
13509 let mut cx = EditorLspTestContext::new_rust(
13510 lsp::ServerCapabilities {
13511 completion_provider: Some(lsp::CompletionOptions {
13512 resolve_provider: Some(true),
13513 ..Default::default()
13514 }),
13515 ..Default::default()
13516 },
13517 cx,
13518 )
13519 .await;
13520
13521 let initial_state = "SubˇError";
13522 let buffer_marked_text = "<Sub|Error>";
13523 let completion_text = "SubscriptionError";
13524 let expected_with_insert_mode = "SubscriptionErrorˇError";
13525 let expected_with_replace_mode = "SubscriptionErrorˇ";
13526
13527 update_test_language_settings(&mut cx, |settings| {
13528 settings.defaults.completions = Some(CompletionSettingsContent {
13529 words: Some(WordsCompletionMode::Disabled),
13530 words_min_length: Some(0),
13531 // set the opposite here to ensure that the action is overriding the default behavior
13532 lsp_insert_mode: Some(LspInsertMode::Insert),
13533 ..Default::default()
13534 });
13535 });
13536
13537 cx.set_state(initial_state);
13538 cx.update_editor(|editor, window, cx| {
13539 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13540 });
13541
13542 let counter = Arc::new(AtomicUsize::new(0));
13543 handle_completion_request_with_insert_and_replace(
13544 &mut cx,
13545 buffer_marked_text,
13546 vec![(completion_text, completion_text)],
13547 counter.clone(),
13548 )
13549 .await;
13550 cx.condition(|editor, _| editor.context_menu_visible())
13551 .await;
13552 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13553
13554 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13555 editor
13556 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13557 .unwrap()
13558 });
13559 cx.assert_editor_state(expected_with_replace_mode);
13560 handle_resolve_completion_request(&mut cx, None).await;
13561 apply_additional_edits.await.unwrap();
13562
13563 update_test_language_settings(&mut cx, |settings| {
13564 settings.defaults.completions = Some(CompletionSettingsContent {
13565 words: Some(WordsCompletionMode::Disabled),
13566 words_min_length: Some(0),
13567 // set the opposite here to ensure that the action is overriding the default behavior
13568 lsp_insert_mode: Some(LspInsertMode::Replace),
13569 ..Default::default()
13570 });
13571 });
13572
13573 cx.set_state(initial_state);
13574 cx.update_editor(|editor, window, cx| {
13575 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13576 });
13577 handle_completion_request_with_insert_and_replace(
13578 &mut cx,
13579 buffer_marked_text,
13580 vec![(completion_text, completion_text)],
13581 counter.clone(),
13582 )
13583 .await;
13584 cx.condition(|editor, _| editor.context_menu_visible())
13585 .await;
13586 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
13587
13588 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13589 editor
13590 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
13591 .unwrap()
13592 });
13593 cx.assert_editor_state(expected_with_insert_mode);
13594 handle_resolve_completion_request(&mut cx, None).await;
13595 apply_additional_edits.await.unwrap();
13596}
13597
13598#[gpui::test]
13599async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
13600 init_test(cx, |_| {});
13601 let mut cx = EditorLspTestContext::new_rust(
13602 lsp::ServerCapabilities {
13603 completion_provider: Some(lsp::CompletionOptions {
13604 resolve_provider: Some(true),
13605 ..Default::default()
13606 }),
13607 ..Default::default()
13608 },
13609 cx,
13610 )
13611 .await;
13612
13613 // scenario: surrounding text matches completion text
13614 let completion_text = "to_offset";
13615 let initial_state = indoc! {"
13616 1. buf.to_offˇsuffix
13617 2. buf.to_offˇsuf
13618 3. buf.to_offˇfix
13619 4. buf.to_offˇ
13620 5. into_offˇensive
13621 6. ˇsuffix
13622 7. let ˇ //
13623 8. aaˇzz
13624 9. buf.to_off«zzzzzˇ»suffix
13625 10. buf.«ˇzzzzz»suffix
13626 11. to_off«ˇzzzzz»
13627
13628 buf.to_offˇsuffix // newest cursor
13629 "};
13630 let completion_marked_buffer = indoc! {"
13631 1. buf.to_offsuffix
13632 2. buf.to_offsuf
13633 3. buf.to_offfix
13634 4. buf.to_off
13635 5. into_offensive
13636 6. suffix
13637 7. let //
13638 8. aazz
13639 9. buf.to_offzzzzzsuffix
13640 10. buf.zzzzzsuffix
13641 11. to_offzzzzz
13642
13643 buf.<to_off|suffix> // newest cursor
13644 "};
13645 let expected = indoc! {"
13646 1. buf.to_offsetˇ
13647 2. buf.to_offsetˇsuf
13648 3. buf.to_offsetˇfix
13649 4. buf.to_offsetˇ
13650 5. into_offsetˇensive
13651 6. to_offsetˇsuffix
13652 7. let to_offsetˇ //
13653 8. aato_offsetˇzz
13654 9. buf.to_offsetˇ
13655 10. buf.to_offsetˇsuffix
13656 11. to_offsetˇ
13657
13658 buf.to_offsetˇ // newest cursor
13659 "};
13660 cx.set_state(initial_state);
13661 cx.update_editor(|editor, window, cx| {
13662 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13663 });
13664 handle_completion_request_with_insert_and_replace(
13665 &mut cx,
13666 completion_marked_buffer,
13667 vec![(completion_text, completion_text)],
13668 Arc::new(AtomicUsize::new(0)),
13669 )
13670 .await;
13671 cx.condition(|editor, _| editor.context_menu_visible())
13672 .await;
13673 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13674 editor
13675 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13676 .unwrap()
13677 });
13678 cx.assert_editor_state(expected);
13679 handle_resolve_completion_request(&mut cx, None).await;
13680 apply_additional_edits.await.unwrap();
13681
13682 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
13683 let completion_text = "foo_and_bar";
13684 let initial_state = indoc! {"
13685 1. ooanbˇ
13686 2. zooanbˇ
13687 3. ooanbˇz
13688 4. zooanbˇz
13689 5. ooanˇ
13690 6. oanbˇ
13691
13692 ooanbˇ
13693 "};
13694 let completion_marked_buffer = indoc! {"
13695 1. ooanb
13696 2. zooanb
13697 3. ooanbz
13698 4. zooanbz
13699 5. ooan
13700 6. oanb
13701
13702 <ooanb|>
13703 "};
13704 let expected = indoc! {"
13705 1. foo_and_barˇ
13706 2. zfoo_and_barˇ
13707 3. foo_and_barˇz
13708 4. zfoo_and_barˇz
13709 5. ooanfoo_and_barˇ
13710 6. oanbfoo_and_barˇ
13711
13712 foo_and_barˇ
13713 "};
13714 cx.set_state(initial_state);
13715 cx.update_editor(|editor, window, cx| {
13716 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13717 });
13718 handle_completion_request_with_insert_and_replace(
13719 &mut cx,
13720 completion_marked_buffer,
13721 vec![(completion_text, completion_text)],
13722 Arc::new(AtomicUsize::new(0)),
13723 )
13724 .await;
13725 cx.condition(|editor, _| editor.context_menu_visible())
13726 .await;
13727 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13728 editor
13729 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13730 .unwrap()
13731 });
13732 cx.assert_editor_state(expected);
13733 handle_resolve_completion_request(&mut cx, None).await;
13734 apply_additional_edits.await.unwrap();
13735
13736 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
13737 // (expects the same as if it was inserted at the end)
13738 let completion_text = "foo_and_bar";
13739 let initial_state = indoc! {"
13740 1. ooˇanb
13741 2. zooˇanb
13742 3. ooˇanbz
13743 4. zooˇanbz
13744
13745 ooˇanb
13746 "};
13747 let completion_marked_buffer = indoc! {"
13748 1. ooanb
13749 2. zooanb
13750 3. ooanbz
13751 4. zooanbz
13752
13753 <oo|anb>
13754 "};
13755 let expected = indoc! {"
13756 1. foo_and_barˇ
13757 2. zfoo_and_barˇ
13758 3. foo_and_barˇz
13759 4. zfoo_and_barˇz
13760
13761 foo_and_barˇ
13762 "};
13763 cx.set_state(initial_state);
13764 cx.update_editor(|editor, window, cx| {
13765 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13766 });
13767 handle_completion_request_with_insert_and_replace(
13768 &mut cx,
13769 completion_marked_buffer,
13770 vec![(completion_text, completion_text)],
13771 Arc::new(AtomicUsize::new(0)),
13772 )
13773 .await;
13774 cx.condition(|editor, _| editor.context_menu_visible())
13775 .await;
13776 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13777 editor
13778 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13779 .unwrap()
13780 });
13781 cx.assert_editor_state(expected);
13782 handle_resolve_completion_request(&mut cx, None).await;
13783 apply_additional_edits.await.unwrap();
13784}
13785
13786// This used to crash
13787#[gpui::test]
13788async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
13789 init_test(cx, |_| {});
13790
13791 let buffer_text = indoc! {"
13792 fn main() {
13793 10.satu;
13794
13795 //
13796 // separate cursors so they open in different excerpts (manually reproducible)
13797 //
13798
13799 10.satu20;
13800 }
13801 "};
13802 let multibuffer_text_with_selections = indoc! {"
13803 fn main() {
13804 10.satuˇ;
13805
13806 //
13807
13808 //
13809
13810 10.satuˇ20;
13811 }
13812 "};
13813 let expected_multibuffer = indoc! {"
13814 fn main() {
13815 10.saturating_sub()ˇ;
13816
13817 //
13818
13819 //
13820
13821 10.saturating_sub()ˇ;
13822 }
13823 "};
13824
13825 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
13826 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
13827
13828 let fs = FakeFs::new(cx.executor());
13829 fs.insert_tree(
13830 path!("/a"),
13831 json!({
13832 "main.rs": buffer_text,
13833 }),
13834 )
13835 .await;
13836
13837 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13838 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13839 language_registry.add(rust_lang());
13840 let mut fake_servers = language_registry.register_fake_lsp(
13841 "Rust",
13842 FakeLspAdapter {
13843 capabilities: lsp::ServerCapabilities {
13844 completion_provider: Some(lsp::CompletionOptions {
13845 resolve_provider: None,
13846 ..lsp::CompletionOptions::default()
13847 }),
13848 ..lsp::ServerCapabilities::default()
13849 },
13850 ..FakeLspAdapter::default()
13851 },
13852 );
13853 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13854 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13855 let buffer = project
13856 .update(cx, |project, cx| {
13857 project.open_local_buffer(path!("/a/main.rs"), cx)
13858 })
13859 .await
13860 .unwrap();
13861
13862 let multi_buffer = cx.new(|cx| {
13863 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
13864 multi_buffer.push_excerpts(
13865 buffer.clone(),
13866 [ExcerptRange::new(0..first_excerpt_end)],
13867 cx,
13868 );
13869 multi_buffer.push_excerpts(
13870 buffer.clone(),
13871 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
13872 cx,
13873 );
13874 multi_buffer
13875 });
13876
13877 let editor = workspace
13878 .update(cx, |_, window, cx| {
13879 cx.new(|cx| {
13880 Editor::new(
13881 EditorMode::Full {
13882 scale_ui_elements_with_buffer_font_size: false,
13883 show_active_line_background: false,
13884 sized_by_content: false,
13885 },
13886 multi_buffer.clone(),
13887 Some(project.clone()),
13888 window,
13889 cx,
13890 )
13891 })
13892 })
13893 .unwrap();
13894
13895 let pane = workspace
13896 .update(cx, |workspace, _, _| workspace.active_pane().clone())
13897 .unwrap();
13898 pane.update_in(cx, |pane, window, cx| {
13899 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
13900 });
13901
13902 let fake_server = fake_servers.next().await.unwrap();
13903
13904 editor.update_in(cx, |editor, window, cx| {
13905 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13906 s.select_ranges([
13907 Point::new(1, 11)..Point::new(1, 11),
13908 Point::new(7, 11)..Point::new(7, 11),
13909 ])
13910 });
13911
13912 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
13913 });
13914
13915 editor.update_in(cx, |editor, window, cx| {
13916 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13917 });
13918
13919 fake_server
13920 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13921 let completion_item = lsp::CompletionItem {
13922 label: "saturating_sub()".into(),
13923 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
13924 lsp::InsertReplaceEdit {
13925 new_text: "saturating_sub()".to_owned(),
13926 insert: lsp::Range::new(
13927 lsp::Position::new(7, 7),
13928 lsp::Position::new(7, 11),
13929 ),
13930 replace: lsp::Range::new(
13931 lsp::Position::new(7, 7),
13932 lsp::Position::new(7, 13),
13933 ),
13934 },
13935 )),
13936 ..lsp::CompletionItem::default()
13937 };
13938
13939 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
13940 })
13941 .next()
13942 .await
13943 .unwrap();
13944
13945 cx.condition(&editor, |editor, _| editor.context_menu_visible())
13946 .await;
13947
13948 editor
13949 .update_in(cx, |editor, window, cx| {
13950 editor
13951 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13952 .unwrap()
13953 })
13954 .await
13955 .unwrap();
13956
13957 editor.update(cx, |editor, cx| {
13958 assert_text_with_selections(editor, expected_multibuffer, cx);
13959 })
13960}
13961
13962#[gpui::test]
13963async fn test_completion(cx: &mut TestAppContext) {
13964 init_test(cx, |_| {});
13965
13966 let mut cx = EditorLspTestContext::new_rust(
13967 lsp::ServerCapabilities {
13968 completion_provider: Some(lsp::CompletionOptions {
13969 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
13970 resolve_provider: Some(true),
13971 ..Default::default()
13972 }),
13973 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
13974 ..Default::default()
13975 },
13976 cx,
13977 )
13978 .await;
13979 let counter = Arc::new(AtomicUsize::new(0));
13980
13981 cx.set_state(indoc! {"
13982 oneˇ
13983 two
13984 three
13985 "});
13986 cx.simulate_keystroke(".");
13987 handle_completion_request(
13988 indoc! {"
13989 one.|<>
13990 two
13991 three
13992 "},
13993 vec!["first_completion", "second_completion"],
13994 true,
13995 counter.clone(),
13996 &mut cx,
13997 )
13998 .await;
13999 cx.condition(|editor, _| editor.context_menu_visible())
14000 .await;
14001 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14002
14003 let _handler = handle_signature_help_request(
14004 &mut cx,
14005 lsp::SignatureHelp {
14006 signatures: vec![lsp::SignatureInformation {
14007 label: "test signature".to_string(),
14008 documentation: None,
14009 parameters: Some(vec![lsp::ParameterInformation {
14010 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
14011 documentation: None,
14012 }]),
14013 active_parameter: None,
14014 }],
14015 active_signature: None,
14016 active_parameter: None,
14017 },
14018 );
14019 cx.update_editor(|editor, window, cx| {
14020 assert!(
14021 !editor.signature_help_state.is_shown(),
14022 "No signature help was called for"
14023 );
14024 editor.show_signature_help(&ShowSignatureHelp, window, cx);
14025 });
14026 cx.run_until_parked();
14027 cx.update_editor(|editor, _, _| {
14028 assert!(
14029 !editor.signature_help_state.is_shown(),
14030 "No signature help should be shown when completions menu is open"
14031 );
14032 });
14033
14034 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14035 editor.context_menu_next(&Default::default(), window, cx);
14036 editor
14037 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14038 .unwrap()
14039 });
14040 cx.assert_editor_state(indoc! {"
14041 one.second_completionˇ
14042 two
14043 three
14044 "});
14045
14046 handle_resolve_completion_request(
14047 &mut cx,
14048 Some(vec![
14049 (
14050 //This overlaps with the primary completion edit which is
14051 //misbehavior from the LSP spec, test that we filter it out
14052 indoc! {"
14053 one.second_ˇcompletion
14054 two
14055 threeˇ
14056 "},
14057 "overlapping additional edit",
14058 ),
14059 (
14060 indoc! {"
14061 one.second_completion
14062 two
14063 threeˇ
14064 "},
14065 "\nadditional edit",
14066 ),
14067 ]),
14068 )
14069 .await;
14070 apply_additional_edits.await.unwrap();
14071 cx.assert_editor_state(indoc! {"
14072 one.second_completionˇ
14073 two
14074 three
14075 additional edit
14076 "});
14077
14078 cx.set_state(indoc! {"
14079 one.second_completion
14080 twoˇ
14081 threeˇ
14082 additional edit
14083 "});
14084 cx.simulate_keystroke(" ");
14085 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14086 cx.simulate_keystroke("s");
14087 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14088
14089 cx.assert_editor_state(indoc! {"
14090 one.second_completion
14091 two sˇ
14092 three sˇ
14093 additional edit
14094 "});
14095 handle_completion_request(
14096 indoc! {"
14097 one.second_completion
14098 two s
14099 three <s|>
14100 additional edit
14101 "},
14102 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14103 true,
14104 counter.clone(),
14105 &mut cx,
14106 )
14107 .await;
14108 cx.condition(|editor, _| editor.context_menu_visible())
14109 .await;
14110 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14111
14112 cx.simulate_keystroke("i");
14113
14114 handle_completion_request(
14115 indoc! {"
14116 one.second_completion
14117 two si
14118 three <si|>
14119 additional edit
14120 "},
14121 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14122 true,
14123 counter.clone(),
14124 &mut cx,
14125 )
14126 .await;
14127 cx.condition(|editor, _| editor.context_menu_visible())
14128 .await;
14129 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14130
14131 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14132 editor
14133 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14134 .unwrap()
14135 });
14136 cx.assert_editor_state(indoc! {"
14137 one.second_completion
14138 two sixth_completionˇ
14139 three sixth_completionˇ
14140 additional edit
14141 "});
14142
14143 apply_additional_edits.await.unwrap();
14144
14145 update_test_language_settings(&mut cx, |settings| {
14146 settings.defaults.show_completions_on_input = Some(false);
14147 });
14148 cx.set_state("editorˇ");
14149 cx.simulate_keystroke(".");
14150 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14151 cx.simulate_keystrokes("c l o");
14152 cx.assert_editor_state("editor.cloˇ");
14153 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14154 cx.update_editor(|editor, window, cx| {
14155 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14156 });
14157 handle_completion_request(
14158 "editor.<clo|>",
14159 vec!["close", "clobber"],
14160 true,
14161 counter.clone(),
14162 &mut cx,
14163 )
14164 .await;
14165 cx.condition(|editor, _| editor.context_menu_visible())
14166 .await;
14167 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
14168
14169 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14170 editor
14171 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14172 .unwrap()
14173 });
14174 cx.assert_editor_state("editor.clobberˇ");
14175 handle_resolve_completion_request(&mut cx, None).await;
14176 apply_additional_edits.await.unwrap();
14177}
14178
14179#[gpui::test]
14180async fn test_completion_reuse(cx: &mut TestAppContext) {
14181 init_test(cx, |_| {});
14182
14183 let mut cx = EditorLspTestContext::new_rust(
14184 lsp::ServerCapabilities {
14185 completion_provider: Some(lsp::CompletionOptions {
14186 trigger_characters: Some(vec![".".to_string()]),
14187 ..Default::default()
14188 }),
14189 ..Default::default()
14190 },
14191 cx,
14192 )
14193 .await;
14194
14195 let counter = Arc::new(AtomicUsize::new(0));
14196 cx.set_state("objˇ");
14197 cx.simulate_keystroke(".");
14198
14199 // Initial completion request returns complete results
14200 let is_incomplete = false;
14201 handle_completion_request(
14202 "obj.|<>",
14203 vec!["a", "ab", "abc"],
14204 is_incomplete,
14205 counter.clone(),
14206 &mut cx,
14207 )
14208 .await;
14209 cx.run_until_parked();
14210 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14211 cx.assert_editor_state("obj.ˇ");
14212 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14213
14214 // Type "a" - filters existing completions
14215 cx.simulate_keystroke("a");
14216 cx.run_until_parked();
14217 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14218 cx.assert_editor_state("obj.aˇ");
14219 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14220
14221 // Type "b" - filters existing completions
14222 cx.simulate_keystroke("b");
14223 cx.run_until_parked();
14224 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14225 cx.assert_editor_state("obj.abˇ");
14226 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14227
14228 // Type "c" - filters existing completions
14229 cx.simulate_keystroke("c");
14230 cx.run_until_parked();
14231 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14232 cx.assert_editor_state("obj.abcˇ");
14233 check_displayed_completions(vec!["abc"], &mut cx);
14234
14235 // Backspace to delete "c" - filters existing completions
14236 cx.update_editor(|editor, window, cx| {
14237 editor.backspace(&Backspace, window, cx);
14238 });
14239 cx.run_until_parked();
14240 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14241 cx.assert_editor_state("obj.abˇ");
14242 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14243
14244 // Moving cursor to the left dismisses menu.
14245 cx.update_editor(|editor, window, cx| {
14246 editor.move_left(&MoveLeft, window, cx);
14247 });
14248 cx.run_until_parked();
14249 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14250 cx.assert_editor_state("obj.aˇb");
14251 cx.update_editor(|editor, _, _| {
14252 assert_eq!(editor.context_menu_visible(), false);
14253 });
14254
14255 // Type "b" - new request
14256 cx.simulate_keystroke("b");
14257 let is_incomplete = false;
14258 handle_completion_request(
14259 "obj.<ab|>a",
14260 vec!["ab", "abc"],
14261 is_incomplete,
14262 counter.clone(),
14263 &mut cx,
14264 )
14265 .await;
14266 cx.run_until_parked();
14267 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14268 cx.assert_editor_state("obj.abˇb");
14269 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14270
14271 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
14272 cx.update_editor(|editor, window, cx| {
14273 editor.backspace(&Backspace, window, cx);
14274 });
14275 let is_incomplete = false;
14276 handle_completion_request(
14277 "obj.<a|>b",
14278 vec!["a", "ab", "abc"],
14279 is_incomplete,
14280 counter.clone(),
14281 &mut cx,
14282 )
14283 .await;
14284 cx.run_until_parked();
14285 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14286 cx.assert_editor_state("obj.aˇb");
14287 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14288
14289 // Backspace to delete "a" - dismisses menu.
14290 cx.update_editor(|editor, window, cx| {
14291 editor.backspace(&Backspace, window, cx);
14292 });
14293 cx.run_until_parked();
14294 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14295 cx.assert_editor_state("obj.ˇb");
14296 cx.update_editor(|editor, _, _| {
14297 assert_eq!(editor.context_menu_visible(), false);
14298 });
14299}
14300
14301#[gpui::test]
14302async fn test_word_completion(cx: &mut TestAppContext) {
14303 let lsp_fetch_timeout_ms = 10;
14304 init_test(cx, |language_settings| {
14305 language_settings.defaults.completions = Some(CompletionSettingsContent {
14306 words_min_length: Some(0),
14307 lsp_fetch_timeout_ms: Some(10),
14308 lsp_insert_mode: Some(LspInsertMode::Insert),
14309 ..Default::default()
14310 });
14311 });
14312
14313 let mut cx = EditorLspTestContext::new_rust(
14314 lsp::ServerCapabilities {
14315 completion_provider: Some(lsp::CompletionOptions {
14316 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14317 ..lsp::CompletionOptions::default()
14318 }),
14319 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14320 ..lsp::ServerCapabilities::default()
14321 },
14322 cx,
14323 )
14324 .await;
14325
14326 let throttle_completions = Arc::new(AtomicBool::new(false));
14327
14328 let lsp_throttle_completions = throttle_completions.clone();
14329 let _completion_requests_handler =
14330 cx.lsp
14331 .server
14332 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
14333 let lsp_throttle_completions = lsp_throttle_completions.clone();
14334 let cx = cx.clone();
14335 async move {
14336 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
14337 cx.background_executor()
14338 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
14339 .await;
14340 }
14341 Ok(Some(lsp::CompletionResponse::Array(vec![
14342 lsp::CompletionItem {
14343 label: "first".into(),
14344 ..lsp::CompletionItem::default()
14345 },
14346 lsp::CompletionItem {
14347 label: "last".into(),
14348 ..lsp::CompletionItem::default()
14349 },
14350 ])))
14351 }
14352 });
14353
14354 cx.set_state(indoc! {"
14355 oneˇ
14356 two
14357 three
14358 "});
14359 cx.simulate_keystroke(".");
14360 cx.executor().run_until_parked();
14361 cx.condition(|editor, _| editor.context_menu_visible())
14362 .await;
14363 cx.update_editor(|editor, window, cx| {
14364 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14365 {
14366 assert_eq!(
14367 completion_menu_entries(menu),
14368 &["first", "last"],
14369 "When LSP server is fast to reply, no fallback word completions are used"
14370 );
14371 } else {
14372 panic!("expected completion menu to be open");
14373 }
14374 editor.cancel(&Cancel, window, cx);
14375 });
14376 cx.executor().run_until_parked();
14377 cx.condition(|editor, _| !editor.context_menu_visible())
14378 .await;
14379
14380 throttle_completions.store(true, atomic::Ordering::Release);
14381 cx.simulate_keystroke(".");
14382 cx.executor()
14383 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
14384 cx.executor().run_until_parked();
14385 cx.condition(|editor, _| editor.context_menu_visible())
14386 .await;
14387 cx.update_editor(|editor, _, _| {
14388 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14389 {
14390 assert_eq!(completion_menu_entries(menu), &["one", "three", "two"],
14391 "When LSP server is slow, document words can be shown instead, if configured accordingly");
14392 } else {
14393 panic!("expected completion menu to be open");
14394 }
14395 });
14396}
14397
14398#[gpui::test]
14399async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
14400 init_test(cx, |language_settings| {
14401 language_settings.defaults.completions = Some(CompletionSettingsContent {
14402 words: Some(WordsCompletionMode::Enabled),
14403 words_min_length: Some(0),
14404 lsp_insert_mode: Some(LspInsertMode::Insert),
14405 ..Default::default()
14406 });
14407 });
14408
14409 let mut cx = EditorLspTestContext::new_rust(
14410 lsp::ServerCapabilities {
14411 completion_provider: Some(lsp::CompletionOptions {
14412 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14413 ..lsp::CompletionOptions::default()
14414 }),
14415 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14416 ..lsp::ServerCapabilities::default()
14417 },
14418 cx,
14419 )
14420 .await;
14421
14422 let _completion_requests_handler =
14423 cx.lsp
14424 .server
14425 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14426 Ok(Some(lsp::CompletionResponse::Array(vec![
14427 lsp::CompletionItem {
14428 label: "first".into(),
14429 ..lsp::CompletionItem::default()
14430 },
14431 lsp::CompletionItem {
14432 label: "last".into(),
14433 ..lsp::CompletionItem::default()
14434 },
14435 ])))
14436 });
14437
14438 cx.set_state(indoc! {"ˇ
14439 first
14440 last
14441 second
14442 "});
14443 cx.simulate_keystroke(".");
14444 cx.executor().run_until_parked();
14445 cx.condition(|editor, _| editor.context_menu_visible())
14446 .await;
14447 cx.update_editor(|editor, _, _| {
14448 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14449 {
14450 assert_eq!(
14451 completion_menu_entries(menu),
14452 &["first", "last", "second"],
14453 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
14454 );
14455 } else {
14456 panic!("expected completion menu to be open");
14457 }
14458 });
14459}
14460
14461#[gpui::test]
14462async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
14463 init_test(cx, |language_settings| {
14464 language_settings.defaults.completions = Some(CompletionSettingsContent {
14465 words: Some(WordsCompletionMode::Disabled),
14466 words_min_length: Some(0),
14467 lsp_insert_mode: Some(LspInsertMode::Insert),
14468 ..Default::default()
14469 });
14470 });
14471
14472 let mut cx = EditorLspTestContext::new_rust(
14473 lsp::ServerCapabilities {
14474 completion_provider: Some(lsp::CompletionOptions {
14475 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14476 ..lsp::CompletionOptions::default()
14477 }),
14478 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14479 ..lsp::ServerCapabilities::default()
14480 },
14481 cx,
14482 )
14483 .await;
14484
14485 let _completion_requests_handler =
14486 cx.lsp
14487 .server
14488 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14489 panic!("LSP completions should not be queried when dealing with word completions")
14490 });
14491
14492 cx.set_state(indoc! {"ˇ
14493 first
14494 last
14495 second
14496 "});
14497 cx.update_editor(|editor, window, cx| {
14498 editor.show_word_completions(&ShowWordCompletions, window, cx);
14499 });
14500 cx.executor().run_until_parked();
14501 cx.condition(|editor, _| editor.context_menu_visible())
14502 .await;
14503 cx.update_editor(|editor, _, _| {
14504 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14505 {
14506 assert_eq!(
14507 completion_menu_entries(menu),
14508 &["first", "last", "second"],
14509 "`ShowWordCompletions` action should show word completions"
14510 );
14511 } else {
14512 panic!("expected completion menu to be open");
14513 }
14514 });
14515
14516 cx.simulate_keystroke("l");
14517 cx.executor().run_until_parked();
14518 cx.condition(|editor, _| editor.context_menu_visible())
14519 .await;
14520 cx.update_editor(|editor, _, _| {
14521 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14522 {
14523 assert_eq!(
14524 completion_menu_entries(menu),
14525 &["last"],
14526 "After showing word completions, further editing should filter them and not query the LSP"
14527 );
14528 } else {
14529 panic!("expected completion menu to be open");
14530 }
14531 });
14532}
14533
14534#[gpui::test]
14535async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
14536 init_test(cx, |language_settings| {
14537 language_settings.defaults.completions = Some(CompletionSettingsContent {
14538 words_min_length: Some(0),
14539 lsp: Some(false),
14540 lsp_insert_mode: Some(LspInsertMode::Insert),
14541 ..Default::default()
14542 });
14543 });
14544
14545 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14546
14547 cx.set_state(indoc! {"ˇ
14548 0_usize
14549 let
14550 33
14551 4.5f32
14552 "});
14553 cx.update_editor(|editor, window, cx| {
14554 editor.show_completions(&ShowCompletions::default(), window, cx);
14555 });
14556 cx.executor().run_until_parked();
14557 cx.condition(|editor, _| editor.context_menu_visible())
14558 .await;
14559 cx.update_editor(|editor, window, cx| {
14560 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14561 {
14562 assert_eq!(
14563 completion_menu_entries(menu),
14564 &["let"],
14565 "With no digits in the completion query, no digits should be in the word completions"
14566 );
14567 } else {
14568 panic!("expected completion menu to be open");
14569 }
14570 editor.cancel(&Cancel, window, cx);
14571 });
14572
14573 cx.set_state(indoc! {"3ˇ
14574 0_usize
14575 let
14576 3
14577 33.35f32
14578 "});
14579 cx.update_editor(|editor, window, cx| {
14580 editor.show_completions(&ShowCompletions::default(), window, cx);
14581 });
14582 cx.executor().run_until_parked();
14583 cx.condition(|editor, _| editor.context_menu_visible())
14584 .await;
14585 cx.update_editor(|editor, _, _| {
14586 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14587 {
14588 assert_eq!(completion_menu_entries(menu), &["33", "35f32"], "The digit is in the completion query, \
14589 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
14590 } else {
14591 panic!("expected completion menu to be open");
14592 }
14593 });
14594}
14595
14596#[gpui::test]
14597async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) {
14598 init_test(cx, |language_settings| {
14599 language_settings.defaults.completions = Some(CompletionSettingsContent {
14600 words: Some(WordsCompletionMode::Enabled),
14601 words_min_length: Some(3),
14602 lsp_insert_mode: Some(LspInsertMode::Insert),
14603 ..Default::default()
14604 });
14605 });
14606
14607 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14608 cx.set_state(indoc! {"ˇ
14609 wow
14610 wowen
14611 wowser
14612 "});
14613 cx.simulate_keystroke("w");
14614 cx.executor().run_until_parked();
14615 cx.update_editor(|editor, _, _| {
14616 if editor.context_menu.borrow_mut().is_some() {
14617 panic!(
14618 "expected completion menu to be hidden, as words completion threshold is not met"
14619 );
14620 }
14621 });
14622
14623 cx.update_editor(|editor, window, cx| {
14624 editor.show_word_completions(&ShowWordCompletions, window, cx);
14625 });
14626 cx.executor().run_until_parked();
14627 cx.update_editor(|editor, window, cx| {
14628 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14629 {
14630 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");
14631 } else {
14632 panic!("expected completion menu to be open after the word completions are called with an action");
14633 }
14634
14635 editor.cancel(&Cancel, window, cx);
14636 });
14637 cx.update_editor(|editor, _, _| {
14638 if editor.context_menu.borrow_mut().is_some() {
14639 panic!("expected completion menu to be hidden after canceling");
14640 }
14641 });
14642
14643 cx.simulate_keystroke("o");
14644 cx.executor().run_until_parked();
14645 cx.update_editor(|editor, _, _| {
14646 if editor.context_menu.borrow_mut().is_some() {
14647 panic!(
14648 "expected completion menu to be hidden, as words completion threshold is not met still"
14649 );
14650 }
14651 });
14652
14653 cx.simulate_keystroke("w");
14654 cx.executor().run_until_parked();
14655 cx.update_editor(|editor, _, _| {
14656 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14657 {
14658 assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word");
14659 } else {
14660 panic!("expected completion menu to be open after the word completions threshold is met");
14661 }
14662 });
14663}
14664
14665#[gpui::test]
14666async fn test_word_completions_disabled(cx: &mut TestAppContext) {
14667 init_test(cx, |language_settings| {
14668 language_settings.defaults.completions = Some(CompletionSettingsContent {
14669 words: Some(WordsCompletionMode::Enabled),
14670 words_min_length: Some(0),
14671 lsp_insert_mode: Some(LspInsertMode::Insert),
14672 ..Default::default()
14673 });
14674 });
14675
14676 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14677 cx.update_editor(|editor, _, _| {
14678 editor.disable_word_completions();
14679 });
14680 cx.set_state(indoc! {"ˇ
14681 wow
14682 wowen
14683 wowser
14684 "});
14685 cx.simulate_keystroke("w");
14686 cx.executor().run_until_parked();
14687 cx.update_editor(|editor, _, _| {
14688 if editor.context_menu.borrow_mut().is_some() {
14689 panic!(
14690 "expected completion menu to be hidden, as words completion are disabled for this editor"
14691 );
14692 }
14693 });
14694
14695 cx.update_editor(|editor, window, cx| {
14696 editor.show_word_completions(&ShowWordCompletions, window, cx);
14697 });
14698 cx.executor().run_until_parked();
14699 cx.update_editor(|editor, _, _| {
14700 if editor.context_menu.borrow_mut().is_some() {
14701 panic!(
14702 "expected completion menu to be hidden even if called for explicitly, as words completion are disabled for this editor"
14703 );
14704 }
14705 });
14706}
14707
14708fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
14709 let position = || lsp::Position {
14710 line: params.text_document_position.position.line,
14711 character: params.text_document_position.position.character,
14712 };
14713 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14714 range: lsp::Range {
14715 start: position(),
14716 end: position(),
14717 },
14718 new_text: text.to_string(),
14719 }))
14720}
14721
14722#[gpui::test]
14723async fn test_multiline_completion(cx: &mut TestAppContext) {
14724 init_test(cx, |_| {});
14725
14726 let fs = FakeFs::new(cx.executor());
14727 fs.insert_tree(
14728 path!("/a"),
14729 json!({
14730 "main.ts": "a",
14731 }),
14732 )
14733 .await;
14734
14735 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14736 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14737 let typescript_language = Arc::new(Language::new(
14738 LanguageConfig {
14739 name: "TypeScript".into(),
14740 matcher: LanguageMatcher {
14741 path_suffixes: vec!["ts".to_string()],
14742 ..LanguageMatcher::default()
14743 },
14744 line_comments: vec!["// ".into()],
14745 ..LanguageConfig::default()
14746 },
14747 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
14748 ));
14749 language_registry.add(typescript_language.clone());
14750 let mut fake_servers = language_registry.register_fake_lsp(
14751 "TypeScript",
14752 FakeLspAdapter {
14753 capabilities: lsp::ServerCapabilities {
14754 completion_provider: Some(lsp::CompletionOptions {
14755 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14756 ..lsp::CompletionOptions::default()
14757 }),
14758 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14759 ..lsp::ServerCapabilities::default()
14760 },
14761 // Emulate vtsls label generation
14762 label_for_completion: Some(Box::new(|item, _| {
14763 let text = if let Some(description) = item
14764 .label_details
14765 .as_ref()
14766 .and_then(|label_details| label_details.description.as_ref())
14767 {
14768 format!("{} {}", item.label, description)
14769 } else if let Some(detail) = &item.detail {
14770 format!("{} {}", item.label, detail)
14771 } else {
14772 item.label.clone()
14773 };
14774 let len = text.len();
14775 Some(language::CodeLabel {
14776 text,
14777 runs: Vec::new(),
14778 filter_range: 0..len,
14779 })
14780 })),
14781 ..FakeLspAdapter::default()
14782 },
14783 );
14784 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14785 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14786 let worktree_id = workspace
14787 .update(cx, |workspace, _window, cx| {
14788 workspace.project().update(cx, |project, cx| {
14789 project.worktrees(cx).next().unwrap().read(cx).id()
14790 })
14791 })
14792 .unwrap();
14793 let _buffer = project
14794 .update(cx, |project, cx| {
14795 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
14796 })
14797 .await
14798 .unwrap();
14799 let editor = workspace
14800 .update(cx, |workspace, window, cx| {
14801 workspace.open_path((worktree_id, rel_path("main.ts")), None, true, window, cx)
14802 })
14803 .unwrap()
14804 .await
14805 .unwrap()
14806 .downcast::<Editor>()
14807 .unwrap();
14808 let fake_server = fake_servers.next().await.unwrap();
14809
14810 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
14811 let multiline_label_2 = "a\nb\nc\n";
14812 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
14813 let multiline_description = "d\ne\nf\n";
14814 let multiline_detail_2 = "g\nh\ni\n";
14815
14816 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
14817 move |params, _| async move {
14818 Ok(Some(lsp::CompletionResponse::Array(vec![
14819 lsp::CompletionItem {
14820 label: multiline_label.to_string(),
14821 text_edit: gen_text_edit(¶ms, "new_text_1"),
14822 ..lsp::CompletionItem::default()
14823 },
14824 lsp::CompletionItem {
14825 label: "single line label 1".to_string(),
14826 detail: Some(multiline_detail.to_string()),
14827 text_edit: gen_text_edit(¶ms, "new_text_2"),
14828 ..lsp::CompletionItem::default()
14829 },
14830 lsp::CompletionItem {
14831 label: "single line label 2".to_string(),
14832 label_details: Some(lsp::CompletionItemLabelDetails {
14833 description: Some(multiline_description.to_string()),
14834 detail: None,
14835 }),
14836 text_edit: gen_text_edit(¶ms, "new_text_2"),
14837 ..lsp::CompletionItem::default()
14838 },
14839 lsp::CompletionItem {
14840 label: multiline_label_2.to_string(),
14841 detail: Some(multiline_detail_2.to_string()),
14842 text_edit: gen_text_edit(¶ms, "new_text_3"),
14843 ..lsp::CompletionItem::default()
14844 },
14845 lsp::CompletionItem {
14846 label: "Label with many spaces and \t but without newlines".to_string(),
14847 detail: Some(
14848 "Details with many spaces and \t but without newlines".to_string(),
14849 ),
14850 text_edit: gen_text_edit(¶ms, "new_text_4"),
14851 ..lsp::CompletionItem::default()
14852 },
14853 ])))
14854 },
14855 );
14856
14857 editor.update_in(cx, |editor, window, cx| {
14858 cx.focus_self(window);
14859 editor.move_to_end(&MoveToEnd, window, cx);
14860 editor.handle_input(".", window, cx);
14861 });
14862 cx.run_until_parked();
14863 completion_handle.next().await.unwrap();
14864
14865 editor.update(cx, |editor, _| {
14866 assert!(editor.context_menu_visible());
14867 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14868 {
14869 let completion_labels = menu
14870 .completions
14871 .borrow()
14872 .iter()
14873 .map(|c| c.label.text.clone())
14874 .collect::<Vec<_>>();
14875 assert_eq!(
14876 completion_labels,
14877 &[
14878 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
14879 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
14880 "single line label 2 d e f ",
14881 "a b c g h i ",
14882 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
14883 ],
14884 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
14885 );
14886
14887 for completion in menu
14888 .completions
14889 .borrow()
14890 .iter() {
14891 assert_eq!(
14892 completion.label.filter_range,
14893 0..completion.label.text.len(),
14894 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
14895 );
14896 }
14897 } else {
14898 panic!("expected completion menu to be open");
14899 }
14900 });
14901}
14902
14903#[gpui::test]
14904async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
14905 init_test(cx, |_| {});
14906 let mut cx = EditorLspTestContext::new_rust(
14907 lsp::ServerCapabilities {
14908 completion_provider: Some(lsp::CompletionOptions {
14909 trigger_characters: Some(vec![".".to_string()]),
14910 ..Default::default()
14911 }),
14912 ..Default::default()
14913 },
14914 cx,
14915 )
14916 .await;
14917 cx.lsp
14918 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14919 Ok(Some(lsp::CompletionResponse::Array(vec![
14920 lsp::CompletionItem {
14921 label: "first".into(),
14922 ..Default::default()
14923 },
14924 lsp::CompletionItem {
14925 label: "last".into(),
14926 ..Default::default()
14927 },
14928 ])))
14929 });
14930 cx.set_state("variableˇ");
14931 cx.simulate_keystroke(".");
14932 cx.executor().run_until_parked();
14933
14934 cx.update_editor(|editor, _, _| {
14935 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14936 {
14937 assert_eq!(completion_menu_entries(menu), &["first", "last"]);
14938 } else {
14939 panic!("expected completion menu to be open");
14940 }
14941 });
14942
14943 cx.update_editor(|editor, window, cx| {
14944 editor.move_page_down(&MovePageDown::default(), window, cx);
14945 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14946 {
14947 assert!(
14948 menu.selected_item == 1,
14949 "expected PageDown to select the last item from the context menu"
14950 );
14951 } else {
14952 panic!("expected completion menu to stay open after PageDown");
14953 }
14954 });
14955
14956 cx.update_editor(|editor, window, cx| {
14957 editor.move_page_up(&MovePageUp::default(), window, cx);
14958 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14959 {
14960 assert!(
14961 menu.selected_item == 0,
14962 "expected PageUp to select the first item from the context menu"
14963 );
14964 } else {
14965 panic!("expected completion menu to stay open after PageUp");
14966 }
14967 });
14968}
14969
14970#[gpui::test]
14971async fn test_as_is_completions(cx: &mut TestAppContext) {
14972 init_test(cx, |_| {});
14973 let mut cx = EditorLspTestContext::new_rust(
14974 lsp::ServerCapabilities {
14975 completion_provider: Some(lsp::CompletionOptions {
14976 ..Default::default()
14977 }),
14978 ..Default::default()
14979 },
14980 cx,
14981 )
14982 .await;
14983 cx.lsp
14984 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14985 Ok(Some(lsp::CompletionResponse::Array(vec![
14986 lsp::CompletionItem {
14987 label: "unsafe".into(),
14988 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14989 range: lsp::Range {
14990 start: lsp::Position {
14991 line: 1,
14992 character: 2,
14993 },
14994 end: lsp::Position {
14995 line: 1,
14996 character: 3,
14997 },
14998 },
14999 new_text: "unsafe".to_string(),
15000 })),
15001 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
15002 ..Default::default()
15003 },
15004 ])))
15005 });
15006 cx.set_state("fn a() {}\n nˇ");
15007 cx.executor().run_until_parked();
15008 cx.update_editor(|editor, window, cx| {
15009 editor.show_completions(
15010 &ShowCompletions {
15011 trigger: Some("\n".into()),
15012 },
15013 window,
15014 cx,
15015 );
15016 });
15017 cx.executor().run_until_parked();
15018
15019 cx.update_editor(|editor, window, cx| {
15020 editor.confirm_completion(&Default::default(), window, cx)
15021 });
15022 cx.executor().run_until_parked();
15023 cx.assert_editor_state("fn a() {}\n unsafeˇ");
15024}
15025
15026#[gpui::test]
15027async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
15028 init_test(cx, |_| {});
15029 let language =
15030 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
15031 let mut cx = EditorLspTestContext::new(
15032 language,
15033 lsp::ServerCapabilities {
15034 completion_provider: Some(lsp::CompletionOptions {
15035 ..lsp::CompletionOptions::default()
15036 }),
15037 ..lsp::ServerCapabilities::default()
15038 },
15039 cx,
15040 )
15041 .await;
15042
15043 cx.set_state(
15044 "#ifndef BAR_H
15045#define BAR_H
15046
15047#include <stdbool.h>
15048
15049int fn_branch(bool do_branch1, bool do_branch2);
15050
15051#endif // BAR_H
15052ˇ",
15053 );
15054 cx.executor().run_until_parked();
15055 cx.update_editor(|editor, window, cx| {
15056 editor.handle_input("#", window, cx);
15057 });
15058 cx.executor().run_until_parked();
15059 cx.update_editor(|editor, window, cx| {
15060 editor.handle_input("i", window, cx);
15061 });
15062 cx.executor().run_until_parked();
15063 cx.update_editor(|editor, window, cx| {
15064 editor.handle_input("n", window, cx);
15065 });
15066 cx.executor().run_until_parked();
15067 cx.assert_editor_state(
15068 "#ifndef BAR_H
15069#define BAR_H
15070
15071#include <stdbool.h>
15072
15073int fn_branch(bool do_branch1, bool do_branch2);
15074
15075#endif // BAR_H
15076#inˇ",
15077 );
15078
15079 cx.lsp
15080 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15081 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15082 is_incomplete: false,
15083 item_defaults: None,
15084 items: vec![lsp::CompletionItem {
15085 kind: Some(lsp::CompletionItemKind::SNIPPET),
15086 label_details: Some(lsp::CompletionItemLabelDetails {
15087 detail: Some("header".to_string()),
15088 description: None,
15089 }),
15090 label: " include".to_string(),
15091 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15092 range: lsp::Range {
15093 start: lsp::Position {
15094 line: 8,
15095 character: 1,
15096 },
15097 end: lsp::Position {
15098 line: 8,
15099 character: 1,
15100 },
15101 },
15102 new_text: "include \"$0\"".to_string(),
15103 })),
15104 sort_text: Some("40b67681include".to_string()),
15105 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15106 filter_text: Some("include".to_string()),
15107 insert_text: Some("include \"$0\"".to_string()),
15108 ..lsp::CompletionItem::default()
15109 }],
15110 })))
15111 });
15112 cx.update_editor(|editor, window, cx| {
15113 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15114 });
15115 cx.executor().run_until_parked();
15116 cx.update_editor(|editor, window, cx| {
15117 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15118 });
15119 cx.executor().run_until_parked();
15120 cx.assert_editor_state(
15121 "#ifndef BAR_H
15122#define BAR_H
15123
15124#include <stdbool.h>
15125
15126int fn_branch(bool do_branch1, bool do_branch2);
15127
15128#endif // BAR_H
15129#include \"ˇ\"",
15130 );
15131
15132 cx.lsp
15133 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15134 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15135 is_incomplete: true,
15136 item_defaults: None,
15137 items: vec![lsp::CompletionItem {
15138 kind: Some(lsp::CompletionItemKind::FILE),
15139 label: "AGL/".to_string(),
15140 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15141 range: lsp::Range {
15142 start: lsp::Position {
15143 line: 8,
15144 character: 10,
15145 },
15146 end: lsp::Position {
15147 line: 8,
15148 character: 11,
15149 },
15150 },
15151 new_text: "AGL/".to_string(),
15152 })),
15153 sort_text: Some("40b67681AGL/".to_string()),
15154 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
15155 filter_text: Some("AGL/".to_string()),
15156 insert_text: Some("AGL/".to_string()),
15157 ..lsp::CompletionItem::default()
15158 }],
15159 })))
15160 });
15161 cx.update_editor(|editor, window, cx| {
15162 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15163 });
15164 cx.executor().run_until_parked();
15165 cx.update_editor(|editor, window, cx| {
15166 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15167 });
15168 cx.executor().run_until_parked();
15169 cx.assert_editor_state(
15170 r##"#ifndef BAR_H
15171#define BAR_H
15172
15173#include <stdbool.h>
15174
15175int fn_branch(bool do_branch1, bool do_branch2);
15176
15177#endif // BAR_H
15178#include "AGL/ˇ"##,
15179 );
15180
15181 cx.update_editor(|editor, window, cx| {
15182 editor.handle_input("\"", window, cx);
15183 });
15184 cx.executor().run_until_parked();
15185 cx.assert_editor_state(
15186 r##"#ifndef BAR_H
15187#define BAR_H
15188
15189#include <stdbool.h>
15190
15191int fn_branch(bool do_branch1, bool do_branch2);
15192
15193#endif // BAR_H
15194#include "AGL/"ˇ"##,
15195 );
15196}
15197
15198#[gpui::test]
15199async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
15200 init_test(cx, |_| {});
15201
15202 let mut cx = EditorLspTestContext::new_rust(
15203 lsp::ServerCapabilities {
15204 completion_provider: Some(lsp::CompletionOptions {
15205 trigger_characters: Some(vec![".".to_string()]),
15206 resolve_provider: Some(true),
15207 ..Default::default()
15208 }),
15209 ..Default::default()
15210 },
15211 cx,
15212 )
15213 .await;
15214
15215 cx.set_state("fn main() { let a = 2ˇ; }");
15216 cx.simulate_keystroke(".");
15217 let completion_item = lsp::CompletionItem {
15218 label: "Some".into(),
15219 kind: Some(lsp::CompletionItemKind::SNIPPET),
15220 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
15221 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
15222 kind: lsp::MarkupKind::Markdown,
15223 value: "```rust\nSome(2)\n```".to_string(),
15224 })),
15225 deprecated: Some(false),
15226 sort_text: Some("Some".to_string()),
15227 filter_text: Some("Some".to_string()),
15228 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15229 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15230 range: lsp::Range {
15231 start: lsp::Position {
15232 line: 0,
15233 character: 22,
15234 },
15235 end: lsp::Position {
15236 line: 0,
15237 character: 22,
15238 },
15239 },
15240 new_text: "Some(2)".to_string(),
15241 })),
15242 additional_text_edits: Some(vec![lsp::TextEdit {
15243 range: lsp::Range {
15244 start: lsp::Position {
15245 line: 0,
15246 character: 20,
15247 },
15248 end: lsp::Position {
15249 line: 0,
15250 character: 22,
15251 },
15252 },
15253 new_text: "".to_string(),
15254 }]),
15255 ..Default::default()
15256 };
15257
15258 let closure_completion_item = completion_item.clone();
15259 let counter = Arc::new(AtomicUsize::new(0));
15260 let counter_clone = counter.clone();
15261 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15262 let task_completion_item = closure_completion_item.clone();
15263 counter_clone.fetch_add(1, atomic::Ordering::Release);
15264 async move {
15265 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15266 is_incomplete: true,
15267 item_defaults: None,
15268 items: vec![task_completion_item],
15269 })))
15270 }
15271 });
15272
15273 cx.condition(|editor, _| editor.context_menu_visible())
15274 .await;
15275 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
15276 assert!(request.next().await.is_some());
15277 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15278
15279 cx.simulate_keystrokes("S o m");
15280 cx.condition(|editor, _| editor.context_menu_visible())
15281 .await;
15282 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
15283 assert!(request.next().await.is_some());
15284 assert!(request.next().await.is_some());
15285 assert!(request.next().await.is_some());
15286 request.close();
15287 assert!(request.next().await.is_none());
15288 assert_eq!(
15289 counter.load(atomic::Ordering::Acquire),
15290 4,
15291 "With the completions menu open, only one LSP request should happen per input"
15292 );
15293}
15294
15295#[gpui::test]
15296async fn test_toggle_comment(cx: &mut TestAppContext) {
15297 init_test(cx, |_| {});
15298 let mut cx = EditorTestContext::new(cx).await;
15299 let language = Arc::new(Language::new(
15300 LanguageConfig {
15301 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15302 ..Default::default()
15303 },
15304 Some(tree_sitter_rust::LANGUAGE.into()),
15305 ));
15306 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15307
15308 // If multiple selections intersect a line, the line is only toggled once.
15309 cx.set_state(indoc! {"
15310 fn a() {
15311 «//b();
15312 ˇ»// «c();
15313 //ˇ» d();
15314 }
15315 "});
15316
15317 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15318
15319 cx.assert_editor_state(indoc! {"
15320 fn a() {
15321 «b();
15322 c();
15323 ˇ» d();
15324 }
15325 "});
15326
15327 // The comment prefix is inserted at the same column for every line in a
15328 // selection.
15329 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15330
15331 cx.assert_editor_state(indoc! {"
15332 fn a() {
15333 // «b();
15334 // c();
15335 ˇ»// d();
15336 }
15337 "});
15338
15339 // If a selection ends at the beginning of a line, that line is not toggled.
15340 cx.set_selections_state(indoc! {"
15341 fn a() {
15342 // b();
15343 «// c();
15344 ˇ» // d();
15345 }
15346 "});
15347
15348 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15349
15350 cx.assert_editor_state(indoc! {"
15351 fn a() {
15352 // b();
15353 «c();
15354 ˇ» // d();
15355 }
15356 "});
15357
15358 // If a selection span a single line and is empty, the line is toggled.
15359 cx.set_state(indoc! {"
15360 fn a() {
15361 a();
15362 b();
15363 ˇ
15364 }
15365 "});
15366
15367 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15368
15369 cx.assert_editor_state(indoc! {"
15370 fn a() {
15371 a();
15372 b();
15373 //•ˇ
15374 }
15375 "});
15376
15377 // If a selection span multiple lines, empty lines are not toggled.
15378 cx.set_state(indoc! {"
15379 fn a() {
15380 «a();
15381
15382 c();ˇ»
15383 }
15384 "});
15385
15386 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15387
15388 cx.assert_editor_state(indoc! {"
15389 fn a() {
15390 // «a();
15391
15392 // c();ˇ»
15393 }
15394 "});
15395
15396 // If a selection includes multiple comment prefixes, all lines are uncommented.
15397 cx.set_state(indoc! {"
15398 fn a() {
15399 «// a();
15400 /// b();
15401 //! c();ˇ»
15402 }
15403 "});
15404
15405 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15406
15407 cx.assert_editor_state(indoc! {"
15408 fn a() {
15409 «a();
15410 b();
15411 c();ˇ»
15412 }
15413 "});
15414}
15415
15416#[gpui::test]
15417async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
15418 init_test(cx, |_| {});
15419 let mut cx = EditorTestContext::new(cx).await;
15420 let language = Arc::new(Language::new(
15421 LanguageConfig {
15422 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15423 ..Default::default()
15424 },
15425 Some(tree_sitter_rust::LANGUAGE.into()),
15426 ));
15427 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15428
15429 let toggle_comments = &ToggleComments {
15430 advance_downwards: false,
15431 ignore_indent: true,
15432 };
15433
15434 // If multiple selections intersect a line, the line is only toggled once.
15435 cx.set_state(indoc! {"
15436 fn a() {
15437 // «b();
15438 // c();
15439 // ˇ» d();
15440 }
15441 "});
15442
15443 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15444
15445 cx.assert_editor_state(indoc! {"
15446 fn a() {
15447 «b();
15448 c();
15449 ˇ» d();
15450 }
15451 "});
15452
15453 // The comment prefix is inserted at the beginning of each line
15454 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15455
15456 cx.assert_editor_state(indoc! {"
15457 fn a() {
15458 // «b();
15459 // c();
15460 // ˇ» d();
15461 }
15462 "});
15463
15464 // If a selection ends at the beginning of a line, that line is not toggled.
15465 cx.set_selections_state(indoc! {"
15466 fn a() {
15467 // b();
15468 // «c();
15469 ˇ»// d();
15470 }
15471 "});
15472
15473 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15474
15475 cx.assert_editor_state(indoc! {"
15476 fn a() {
15477 // b();
15478 «c();
15479 ˇ»// d();
15480 }
15481 "});
15482
15483 // If a selection span a single line and is empty, the line is toggled.
15484 cx.set_state(indoc! {"
15485 fn a() {
15486 a();
15487 b();
15488 ˇ
15489 }
15490 "});
15491
15492 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15493
15494 cx.assert_editor_state(indoc! {"
15495 fn a() {
15496 a();
15497 b();
15498 //ˇ
15499 }
15500 "});
15501
15502 // If a selection span multiple lines, empty lines are not toggled.
15503 cx.set_state(indoc! {"
15504 fn a() {
15505 «a();
15506
15507 c();ˇ»
15508 }
15509 "});
15510
15511 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15512
15513 cx.assert_editor_state(indoc! {"
15514 fn a() {
15515 // «a();
15516
15517 // c();ˇ»
15518 }
15519 "});
15520
15521 // If a selection includes multiple comment prefixes, all lines are uncommented.
15522 cx.set_state(indoc! {"
15523 fn a() {
15524 // «a();
15525 /// b();
15526 //! c();ˇ»
15527 }
15528 "});
15529
15530 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15531
15532 cx.assert_editor_state(indoc! {"
15533 fn a() {
15534 «a();
15535 b();
15536 c();ˇ»
15537 }
15538 "});
15539}
15540
15541#[gpui::test]
15542async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
15543 init_test(cx, |_| {});
15544
15545 let language = Arc::new(Language::new(
15546 LanguageConfig {
15547 line_comments: vec!["// ".into()],
15548 ..Default::default()
15549 },
15550 Some(tree_sitter_rust::LANGUAGE.into()),
15551 ));
15552
15553 let mut cx = EditorTestContext::new(cx).await;
15554
15555 cx.language_registry().add(language.clone());
15556 cx.update_buffer(|buffer, cx| {
15557 buffer.set_language(Some(language), cx);
15558 });
15559
15560 let toggle_comments = &ToggleComments {
15561 advance_downwards: true,
15562 ignore_indent: false,
15563 };
15564
15565 // Single cursor on one line -> advance
15566 // Cursor moves horizontally 3 characters as well on non-blank line
15567 cx.set_state(indoc!(
15568 "fn a() {
15569 ˇdog();
15570 cat();
15571 }"
15572 ));
15573 cx.update_editor(|editor, window, cx| {
15574 editor.toggle_comments(toggle_comments, window, cx);
15575 });
15576 cx.assert_editor_state(indoc!(
15577 "fn a() {
15578 // dog();
15579 catˇ();
15580 }"
15581 ));
15582
15583 // Single selection on one line -> don't advance
15584 cx.set_state(indoc!(
15585 "fn a() {
15586 «dog()ˇ»;
15587 cat();
15588 }"
15589 ));
15590 cx.update_editor(|editor, window, cx| {
15591 editor.toggle_comments(toggle_comments, window, cx);
15592 });
15593 cx.assert_editor_state(indoc!(
15594 "fn a() {
15595 // «dog()ˇ»;
15596 cat();
15597 }"
15598 ));
15599
15600 // Multiple cursors on one line -> advance
15601 cx.set_state(indoc!(
15602 "fn a() {
15603 ˇdˇog();
15604 cat();
15605 }"
15606 ));
15607 cx.update_editor(|editor, window, cx| {
15608 editor.toggle_comments(toggle_comments, window, cx);
15609 });
15610 cx.assert_editor_state(indoc!(
15611 "fn a() {
15612 // dog();
15613 catˇ(ˇ);
15614 }"
15615 ));
15616
15617 // Multiple cursors on one line, with selection -> don't advance
15618 cx.set_state(indoc!(
15619 "fn a() {
15620 ˇdˇog«()ˇ»;
15621 cat();
15622 }"
15623 ));
15624 cx.update_editor(|editor, window, cx| {
15625 editor.toggle_comments(toggle_comments, window, cx);
15626 });
15627 cx.assert_editor_state(indoc!(
15628 "fn a() {
15629 // ˇdˇog«()ˇ»;
15630 cat();
15631 }"
15632 ));
15633
15634 // Single cursor on one line -> advance
15635 // Cursor moves to column 0 on blank line
15636 cx.set_state(indoc!(
15637 "fn a() {
15638 ˇdog();
15639
15640 cat();
15641 }"
15642 ));
15643 cx.update_editor(|editor, window, cx| {
15644 editor.toggle_comments(toggle_comments, window, cx);
15645 });
15646 cx.assert_editor_state(indoc!(
15647 "fn a() {
15648 // dog();
15649 ˇ
15650 cat();
15651 }"
15652 ));
15653
15654 // Single cursor on one line -> advance
15655 // Cursor starts and ends at column 0
15656 cx.set_state(indoc!(
15657 "fn a() {
15658 ˇ dog();
15659 cat();
15660 }"
15661 ));
15662 cx.update_editor(|editor, window, cx| {
15663 editor.toggle_comments(toggle_comments, window, cx);
15664 });
15665 cx.assert_editor_state(indoc!(
15666 "fn a() {
15667 // dog();
15668 ˇ cat();
15669 }"
15670 ));
15671}
15672
15673#[gpui::test]
15674async fn test_toggle_block_comment(cx: &mut TestAppContext) {
15675 init_test(cx, |_| {});
15676
15677 let mut cx = EditorTestContext::new(cx).await;
15678
15679 let html_language = Arc::new(
15680 Language::new(
15681 LanguageConfig {
15682 name: "HTML".into(),
15683 block_comment: Some(BlockCommentConfig {
15684 start: "<!-- ".into(),
15685 prefix: "".into(),
15686 end: " -->".into(),
15687 tab_size: 0,
15688 }),
15689 ..Default::default()
15690 },
15691 Some(tree_sitter_html::LANGUAGE.into()),
15692 )
15693 .with_injection_query(
15694 r#"
15695 (script_element
15696 (raw_text) @injection.content
15697 (#set! injection.language "javascript"))
15698 "#,
15699 )
15700 .unwrap(),
15701 );
15702
15703 let javascript_language = Arc::new(Language::new(
15704 LanguageConfig {
15705 name: "JavaScript".into(),
15706 line_comments: vec!["// ".into()],
15707 ..Default::default()
15708 },
15709 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
15710 ));
15711
15712 cx.language_registry().add(html_language.clone());
15713 cx.language_registry().add(javascript_language);
15714 cx.update_buffer(|buffer, cx| {
15715 buffer.set_language(Some(html_language), cx);
15716 });
15717
15718 // Toggle comments for empty selections
15719 cx.set_state(
15720 &r#"
15721 <p>A</p>ˇ
15722 <p>B</p>ˇ
15723 <p>C</p>ˇ
15724 "#
15725 .unindent(),
15726 );
15727 cx.update_editor(|editor, window, cx| {
15728 editor.toggle_comments(&ToggleComments::default(), window, cx)
15729 });
15730 cx.assert_editor_state(
15731 &r#"
15732 <!-- <p>A</p>ˇ -->
15733 <!-- <p>B</p>ˇ -->
15734 <!-- <p>C</p>ˇ -->
15735 "#
15736 .unindent(),
15737 );
15738 cx.update_editor(|editor, window, cx| {
15739 editor.toggle_comments(&ToggleComments::default(), window, cx)
15740 });
15741 cx.assert_editor_state(
15742 &r#"
15743 <p>A</p>ˇ
15744 <p>B</p>ˇ
15745 <p>C</p>ˇ
15746 "#
15747 .unindent(),
15748 );
15749
15750 // Toggle comments for mixture of empty and non-empty selections, where
15751 // multiple selections occupy a given line.
15752 cx.set_state(
15753 &r#"
15754 <p>A«</p>
15755 <p>ˇ»B</p>ˇ
15756 <p>C«</p>
15757 <p>ˇ»D</p>ˇ
15758 "#
15759 .unindent(),
15760 );
15761
15762 cx.update_editor(|editor, window, cx| {
15763 editor.toggle_comments(&ToggleComments::default(), window, cx)
15764 });
15765 cx.assert_editor_state(
15766 &r#"
15767 <!-- <p>A«</p>
15768 <p>ˇ»B</p>ˇ -->
15769 <!-- <p>C«</p>
15770 <p>ˇ»D</p>ˇ -->
15771 "#
15772 .unindent(),
15773 );
15774 cx.update_editor(|editor, window, cx| {
15775 editor.toggle_comments(&ToggleComments::default(), window, cx)
15776 });
15777 cx.assert_editor_state(
15778 &r#"
15779 <p>A«</p>
15780 <p>ˇ»B</p>ˇ
15781 <p>C«</p>
15782 <p>ˇ»D</p>ˇ
15783 "#
15784 .unindent(),
15785 );
15786
15787 // Toggle comments when different languages are active for different
15788 // selections.
15789 cx.set_state(
15790 &r#"
15791 ˇ<script>
15792 ˇvar x = new Y();
15793 ˇ</script>
15794 "#
15795 .unindent(),
15796 );
15797 cx.executor().run_until_parked();
15798 cx.update_editor(|editor, window, cx| {
15799 editor.toggle_comments(&ToggleComments::default(), window, cx)
15800 });
15801 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
15802 // Uncommenting and commenting from this position brings in even more wrong artifacts.
15803 cx.assert_editor_state(
15804 &r#"
15805 <!-- ˇ<script> -->
15806 // ˇvar x = new Y();
15807 <!-- ˇ</script> -->
15808 "#
15809 .unindent(),
15810 );
15811}
15812
15813#[gpui::test]
15814fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
15815 init_test(cx, |_| {});
15816
15817 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
15818 let multibuffer = cx.new(|cx| {
15819 let mut multibuffer = MultiBuffer::new(ReadWrite);
15820 multibuffer.push_excerpts(
15821 buffer.clone(),
15822 [
15823 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
15824 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
15825 ],
15826 cx,
15827 );
15828 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
15829 multibuffer
15830 });
15831
15832 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
15833 editor.update_in(cx, |editor, window, cx| {
15834 assert_eq!(editor.text(cx), "aaaa\nbbbb");
15835 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15836 s.select_ranges([
15837 Point::new(0, 0)..Point::new(0, 0),
15838 Point::new(1, 0)..Point::new(1, 0),
15839 ])
15840 });
15841
15842 editor.handle_input("X", window, cx);
15843 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
15844 assert_eq!(
15845 editor.selections.ranges(cx),
15846 [
15847 Point::new(0, 1)..Point::new(0, 1),
15848 Point::new(1, 1)..Point::new(1, 1),
15849 ]
15850 );
15851
15852 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
15853 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15854 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
15855 });
15856 editor.backspace(&Default::default(), window, cx);
15857 assert_eq!(editor.text(cx), "Xa\nbbb");
15858 assert_eq!(
15859 editor.selections.ranges(cx),
15860 [Point::new(1, 0)..Point::new(1, 0)]
15861 );
15862
15863 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15864 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
15865 });
15866 editor.backspace(&Default::default(), window, cx);
15867 assert_eq!(editor.text(cx), "X\nbb");
15868 assert_eq!(
15869 editor.selections.ranges(cx),
15870 [Point::new(0, 1)..Point::new(0, 1)]
15871 );
15872 });
15873}
15874
15875#[gpui::test]
15876fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
15877 init_test(cx, |_| {});
15878
15879 let markers = vec![('[', ']').into(), ('(', ')').into()];
15880 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
15881 indoc! {"
15882 [aaaa
15883 (bbbb]
15884 cccc)",
15885 },
15886 markers.clone(),
15887 );
15888 let excerpt_ranges = markers.into_iter().map(|marker| {
15889 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
15890 ExcerptRange::new(context)
15891 });
15892 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
15893 let multibuffer = cx.new(|cx| {
15894 let mut multibuffer = MultiBuffer::new(ReadWrite);
15895 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
15896 multibuffer
15897 });
15898
15899 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
15900 editor.update_in(cx, |editor, window, cx| {
15901 let (expected_text, selection_ranges) = marked_text_ranges(
15902 indoc! {"
15903 aaaa
15904 bˇbbb
15905 bˇbbˇb
15906 cccc"
15907 },
15908 true,
15909 );
15910 assert_eq!(editor.text(cx), expected_text);
15911 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15912 s.select_ranges(selection_ranges)
15913 });
15914
15915 editor.handle_input("X", window, cx);
15916
15917 let (expected_text, expected_selections) = marked_text_ranges(
15918 indoc! {"
15919 aaaa
15920 bXˇbbXb
15921 bXˇbbXˇb
15922 cccc"
15923 },
15924 false,
15925 );
15926 assert_eq!(editor.text(cx), expected_text);
15927 assert_eq!(editor.selections.ranges(cx), expected_selections);
15928
15929 editor.newline(&Newline, window, cx);
15930 let (expected_text, expected_selections) = marked_text_ranges(
15931 indoc! {"
15932 aaaa
15933 bX
15934 ˇbbX
15935 b
15936 bX
15937 ˇbbX
15938 ˇb
15939 cccc"
15940 },
15941 false,
15942 );
15943 assert_eq!(editor.text(cx), expected_text);
15944 assert_eq!(editor.selections.ranges(cx), expected_selections);
15945 });
15946}
15947
15948#[gpui::test]
15949fn test_refresh_selections(cx: &mut TestAppContext) {
15950 init_test(cx, |_| {});
15951
15952 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
15953 let mut excerpt1_id = None;
15954 let multibuffer = cx.new(|cx| {
15955 let mut multibuffer = MultiBuffer::new(ReadWrite);
15956 excerpt1_id = multibuffer
15957 .push_excerpts(
15958 buffer.clone(),
15959 [
15960 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
15961 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
15962 ],
15963 cx,
15964 )
15965 .into_iter()
15966 .next();
15967 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
15968 multibuffer
15969 });
15970
15971 let editor = cx.add_window(|window, cx| {
15972 let mut editor = build_editor(multibuffer.clone(), window, cx);
15973 let snapshot = editor.snapshot(window, cx);
15974 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15975 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
15976 });
15977 editor.begin_selection(
15978 Point::new(2, 1).to_display_point(&snapshot),
15979 true,
15980 1,
15981 window,
15982 cx,
15983 );
15984 assert_eq!(
15985 editor.selections.ranges(cx),
15986 [
15987 Point::new(1, 3)..Point::new(1, 3),
15988 Point::new(2, 1)..Point::new(2, 1),
15989 ]
15990 );
15991 editor
15992 });
15993
15994 // Refreshing selections is a no-op when excerpts haven't changed.
15995 _ = editor.update(cx, |editor, window, cx| {
15996 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
15997 assert_eq!(
15998 editor.selections.ranges(cx),
15999 [
16000 Point::new(1, 3)..Point::new(1, 3),
16001 Point::new(2, 1)..Point::new(2, 1),
16002 ]
16003 );
16004 });
16005
16006 multibuffer.update(cx, |multibuffer, cx| {
16007 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
16008 });
16009 _ = editor.update(cx, |editor, window, cx| {
16010 // Removing an excerpt causes the first selection to become degenerate.
16011 assert_eq!(
16012 editor.selections.ranges(cx),
16013 [
16014 Point::new(0, 0)..Point::new(0, 0),
16015 Point::new(0, 1)..Point::new(0, 1)
16016 ]
16017 );
16018
16019 // Refreshing selections will relocate the first selection to the original buffer
16020 // location.
16021 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16022 assert_eq!(
16023 editor.selections.ranges(cx),
16024 [
16025 Point::new(0, 1)..Point::new(0, 1),
16026 Point::new(0, 3)..Point::new(0, 3)
16027 ]
16028 );
16029 assert!(editor.selections.pending_anchor().is_some());
16030 });
16031}
16032
16033#[gpui::test]
16034fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
16035 init_test(cx, |_| {});
16036
16037 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16038 let mut excerpt1_id = None;
16039 let multibuffer = cx.new(|cx| {
16040 let mut multibuffer = MultiBuffer::new(ReadWrite);
16041 excerpt1_id = multibuffer
16042 .push_excerpts(
16043 buffer.clone(),
16044 [
16045 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
16046 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
16047 ],
16048 cx,
16049 )
16050 .into_iter()
16051 .next();
16052 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
16053 multibuffer
16054 });
16055
16056 let editor = cx.add_window(|window, cx| {
16057 let mut editor = build_editor(multibuffer.clone(), window, cx);
16058 let snapshot = editor.snapshot(window, cx);
16059 editor.begin_selection(
16060 Point::new(1, 3).to_display_point(&snapshot),
16061 false,
16062 1,
16063 window,
16064 cx,
16065 );
16066 assert_eq!(
16067 editor.selections.ranges(cx),
16068 [Point::new(1, 3)..Point::new(1, 3)]
16069 );
16070 editor
16071 });
16072
16073 multibuffer.update(cx, |multibuffer, cx| {
16074 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
16075 });
16076 _ = editor.update(cx, |editor, window, cx| {
16077 assert_eq!(
16078 editor.selections.ranges(cx),
16079 [Point::new(0, 0)..Point::new(0, 0)]
16080 );
16081
16082 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
16083 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16084 assert_eq!(
16085 editor.selections.ranges(cx),
16086 [Point::new(0, 3)..Point::new(0, 3)]
16087 );
16088 assert!(editor.selections.pending_anchor().is_some());
16089 });
16090}
16091
16092#[gpui::test]
16093async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
16094 init_test(cx, |_| {});
16095
16096 let language = Arc::new(
16097 Language::new(
16098 LanguageConfig {
16099 brackets: BracketPairConfig {
16100 pairs: vec![
16101 BracketPair {
16102 start: "{".to_string(),
16103 end: "}".to_string(),
16104 close: true,
16105 surround: true,
16106 newline: true,
16107 },
16108 BracketPair {
16109 start: "/* ".to_string(),
16110 end: " */".to_string(),
16111 close: true,
16112 surround: true,
16113 newline: true,
16114 },
16115 ],
16116 ..Default::default()
16117 },
16118 ..Default::default()
16119 },
16120 Some(tree_sitter_rust::LANGUAGE.into()),
16121 )
16122 .with_indents_query("")
16123 .unwrap(),
16124 );
16125
16126 let text = concat!(
16127 "{ }\n", //
16128 " x\n", //
16129 " /* */\n", //
16130 "x\n", //
16131 "{{} }\n", //
16132 );
16133
16134 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
16135 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
16136 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
16137 editor
16138 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
16139 .await;
16140
16141 editor.update_in(cx, |editor, window, cx| {
16142 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16143 s.select_display_ranges([
16144 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
16145 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
16146 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
16147 ])
16148 });
16149 editor.newline(&Newline, window, cx);
16150
16151 assert_eq!(
16152 editor.buffer().read(cx).read(cx).text(),
16153 concat!(
16154 "{ \n", // Suppress rustfmt
16155 "\n", //
16156 "}\n", //
16157 " x\n", //
16158 " /* \n", //
16159 " \n", //
16160 " */\n", //
16161 "x\n", //
16162 "{{} \n", //
16163 "}\n", //
16164 )
16165 );
16166 });
16167}
16168
16169#[gpui::test]
16170fn test_highlighted_ranges(cx: &mut TestAppContext) {
16171 init_test(cx, |_| {});
16172
16173 let editor = cx.add_window(|window, cx| {
16174 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
16175 build_editor(buffer, window, cx)
16176 });
16177
16178 _ = editor.update(cx, |editor, window, cx| {
16179 struct Type1;
16180 struct Type2;
16181
16182 let buffer = editor.buffer.read(cx).snapshot(cx);
16183
16184 let anchor_range =
16185 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
16186
16187 editor.highlight_background::<Type1>(
16188 &[
16189 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
16190 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
16191 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
16192 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
16193 ],
16194 |_| Hsla::red(),
16195 cx,
16196 );
16197 editor.highlight_background::<Type2>(
16198 &[
16199 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
16200 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
16201 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
16202 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
16203 ],
16204 |_| Hsla::green(),
16205 cx,
16206 );
16207
16208 let snapshot = editor.snapshot(window, cx);
16209 let highlighted_ranges = editor.sorted_background_highlights_in_range(
16210 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
16211 &snapshot,
16212 cx.theme(),
16213 );
16214 assert_eq!(
16215 highlighted_ranges,
16216 &[
16217 (
16218 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
16219 Hsla::green(),
16220 ),
16221 (
16222 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
16223 Hsla::red(),
16224 ),
16225 (
16226 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
16227 Hsla::green(),
16228 ),
16229 (
16230 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16231 Hsla::red(),
16232 ),
16233 ]
16234 );
16235 assert_eq!(
16236 editor.sorted_background_highlights_in_range(
16237 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
16238 &snapshot,
16239 cx.theme(),
16240 ),
16241 &[(
16242 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16243 Hsla::red(),
16244 )]
16245 );
16246 });
16247}
16248
16249#[gpui::test]
16250async fn test_following(cx: &mut TestAppContext) {
16251 init_test(cx, |_| {});
16252
16253 let fs = FakeFs::new(cx.executor());
16254 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16255
16256 let buffer = project.update(cx, |project, cx| {
16257 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, false, cx);
16258 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
16259 });
16260 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
16261 let follower = cx.update(|cx| {
16262 cx.open_window(
16263 WindowOptions {
16264 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
16265 gpui::Point::new(px(0.), px(0.)),
16266 gpui::Point::new(px(10.), px(80.)),
16267 ))),
16268 ..Default::default()
16269 },
16270 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
16271 )
16272 .unwrap()
16273 });
16274
16275 let is_still_following = Rc::new(RefCell::new(true));
16276 let follower_edit_event_count = Rc::new(RefCell::new(0));
16277 let pending_update = Rc::new(RefCell::new(None));
16278 let leader_entity = leader.root(cx).unwrap();
16279 let follower_entity = follower.root(cx).unwrap();
16280 _ = follower.update(cx, {
16281 let update = pending_update.clone();
16282 let is_still_following = is_still_following.clone();
16283 let follower_edit_event_count = follower_edit_event_count.clone();
16284 |_, window, cx| {
16285 cx.subscribe_in(
16286 &leader_entity,
16287 window,
16288 move |_, leader, event, window, cx| {
16289 leader.read(cx).add_event_to_update_proto(
16290 event,
16291 &mut update.borrow_mut(),
16292 window,
16293 cx,
16294 );
16295 },
16296 )
16297 .detach();
16298
16299 cx.subscribe_in(
16300 &follower_entity,
16301 window,
16302 move |_, _, event: &EditorEvent, _window, _cx| {
16303 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
16304 *is_still_following.borrow_mut() = false;
16305 }
16306
16307 if let EditorEvent::BufferEdited = event {
16308 *follower_edit_event_count.borrow_mut() += 1;
16309 }
16310 },
16311 )
16312 .detach();
16313 }
16314 });
16315
16316 // Update the selections only
16317 _ = leader.update(cx, |leader, window, cx| {
16318 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16319 s.select_ranges([1..1])
16320 });
16321 });
16322 follower
16323 .update(cx, |follower, window, cx| {
16324 follower.apply_update_proto(
16325 &project,
16326 pending_update.borrow_mut().take().unwrap(),
16327 window,
16328 cx,
16329 )
16330 })
16331 .unwrap()
16332 .await
16333 .unwrap();
16334 _ = follower.update(cx, |follower, _, cx| {
16335 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
16336 });
16337 assert!(*is_still_following.borrow());
16338 assert_eq!(*follower_edit_event_count.borrow(), 0);
16339
16340 // Update the scroll position only
16341 _ = leader.update(cx, |leader, window, cx| {
16342 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16343 });
16344 follower
16345 .update(cx, |follower, window, cx| {
16346 follower.apply_update_proto(
16347 &project,
16348 pending_update.borrow_mut().take().unwrap(),
16349 window,
16350 cx,
16351 )
16352 })
16353 .unwrap()
16354 .await
16355 .unwrap();
16356 assert_eq!(
16357 follower
16358 .update(cx, |follower, _, cx| follower.scroll_position(cx))
16359 .unwrap(),
16360 gpui::Point::new(1.5, 3.5)
16361 );
16362 assert!(*is_still_following.borrow());
16363 assert_eq!(*follower_edit_event_count.borrow(), 0);
16364
16365 // Update the selections and scroll position. The follower's scroll position is updated
16366 // via autoscroll, not via the leader's exact scroll position.
16367 _ = leader.update(cx, |leader, window, cx| {
16368 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16369 s.select_ranges([0..0])
16370 });
16371 leader.request_autoscroll(Autoscroll::newest(), cx);
16372 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16373 });
16374 follower
16375 .update(cx, |follower, window, cx| {
16376 follower.apply_update_proto(
16377 &project,
16378 pending_update.borrow_mut().take().unwrap(),
16379 window,
16380 cx,
16381 )
16382 })
16383 .unwrap()
16384 .await
16385 .unwrap();
16386 _ = follower.update(cx, |follower, _, cx| {
16387 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
16388 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
16389 });
16390 assert!(*is_still_following.borrow());
16391
16392 // Creating a pending selection that precedes another selection
16393 _ = leader.update(cx, |leader, window, cx| {
16394 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16395 s.select_ranges([1..1])
16396 });
16397 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
16398 });
16399 follower
16400 .update(cx, |follower, window, cx| {
16401 follower.apply_update_proto(
16402 &project,
16403 pending_update.borrow_mut().take().unwrap(),
16404 window,
16405 cx,
16406 )
16407 })
16408 .unwrap()
16409 .await
16410 .unwrap();
16411 _ = follower.update(cx, |follower, _, cx| {
16412 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
16413 });
16414 assert!(*is_still_following.borrow());
16415
16416 // Extend the pending selection so that it surrounds another selection
16417 _ = leader.update(cx, |leader, window, cx| {
16418 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
16419 });
16420 follower
16421 .update(cx, |follower, window, cx| {
16422 follower.apply_update_proto(
16423 &project,
16424 pending_update.borrow_mut().take().unwrap(),
16425 window,
16426 cx,
16427 )
16428 })
16429 .unwrap()
16430 .await
16431 .unwrap();
16432 _ = follower.update(cx, |follower, _, cx| {
16433 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
16434 });
16435
16436 // Scrolling locally breaks the follow
16437 _ = follower.update(cx, |follower, window, cx| {
16438 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
16439 follower.set_scroll_anchor(
16440 ScrollAnchor {
16441 anchor: top_anchor,
16442 offset: gpui::Point::new(0.0, 0.5),
16443 },
16444 window,
16445 cx,
16446 );
16447 });
16448 assert!(!(*is_still_following.borrow()));
16449}
16450
16451#[gpui::test]
16452async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
16453 init_test(cx, |_| {});
16454
16455 let fs = FakeFs::new(cx.executor());
16456 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16457 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16458 let pane = workspace
16459 .update(cx, |workspace, _, _| workspace.active_pane().clone())
16460 .unwrap();
16461
16462 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16463
16464 let leader = pane.update_in(cx, |_, window, cx| {
16465 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
16466 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
16467 });
16468
16469 // Start following the editor when it has no excerpts.
16470 let mut state_message =
16471 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16472 let workspace_entity = workspace.root(cx).unwrap();
16473 let follower_1 = cx
16474 .update_window(*workspace.deref(), |_, window, cx| {
16475 Editor::from_state_proto(
16476 workspace_entity,
16477 ViewId {
16478 creator: CollaboratorId::PeerId(PeerId::default()),
16479 id: 0,
16480 },
16481 &mut state_message,
16482 window,
16483 cx,
16484 )
16485 })
16486 .unwrap()
16487 .unwrap()
16488 .await
16489 .unwrap();
16490
16491 let update_message = Rc::new(RefCell::new(None));
16492 follower_1.update_in(cx, {
16493 let update = update_message.clone();
16494 |_, window, cx| {
16495 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
16496 leader.read(cx).add_event_to_update_proto(
16497 event,
16498 &mut update.borrow_mut(),
16499 window,
16500 cx,
16501 );
16502 })
16503 .detach();
16504 }
16505 });
16506
16507 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
16508 (
16509 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, false, cx),
16510 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, false, cx),
16511 )
16512 });
16513
16514 // Insert some excerpts.
16515 leader.update(cx, |leader, cx| {
16516 leader.buffer.update(cx, |multibuffer, cx| {
16517 multibuffer.set_excerpts_for_path(
16518 PathKey::namespaced(1, rel_path("b.txt").into_arc()),
16519 buffer_1.clone(),
16520 vec![
16521 Point::row_range(0..3),
16522 Point::row_range(1..6),
16523 Point::row_range(12..15),
16524 ],
16525 0,
16526 cx,
16527 );
16528 multibuffer.set_excerpts_for_path(
16529 PathKey::namespaced(1, rel_path("a.txt").into_arc()),
16530 buffer_2.clone(),
16531 vec![Point::row_range(0..6), Point::row_range(8..12)],
16532 0,
16533 cx,
16534 );
16535 });
16536 });
16537
16538 // Apply the update of adding the excerpts.
16539 follower_1
16540 .update_in(cx, |follower, window, cx| {
16541 follower.apply_update_proto(
16542 &project,
16543 update_message.borrow().clone().unwrap(),
16544 window,
16545 cx,
16546 )
16547 })
16548 .await
16549 .unwrap();
16550 assert_eq!(
16551 follower_1.update(cx, |editor, cx| editor.text(cx)),
16552 leader.update(cx, |editor, cx| editor.text(cx))
16553 );
16554 update_message.borrow_mut().take();
16555
16556 // Start following separately after it already has excerpts.
16557 let mut state_message =
16558 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16559 let workspace_entity = workspace.root(cx).unwrap();
16560 let follower_2 = cx
16561 .update_window(*workspace.deref(), |_, window, cx| {
16562 Editor::from_state_proto(
16563 workspace_entity,
16564 ViewId {
16565 creator: CollaboratorId::PeerId(PeerId::default()),
16566 id: 0,
16567 },
16568 &mut state_message,
16569 window,
16570 cx,
16571 )
16572 })
16573 .unwrap()
16574 .unwrap()
16575 .await
16576 .unwrap();
16577 assert_eq!(
16578 follower_2.update(cx, |editor, cx| editor.text(cx)),
16579 leader.update(cx, |editor, cx| editor.text(cx))
16580 );
16581
16582 // Remove some excerpts.
16583 leader.update(cx, |leader, cx| {
16584 leader.buffer.update(cx, |multibuffer, cx| {
16585 let excerpt_ids = multibuffer.excerpt_ids();
16586 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
16587 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
16588 });
16589 });
16590
16591 // Apply the update of removing the excerpts.
16592 follower_1
16593 .update_in(cx, |follower, window, cx| {
16594 follower.apply_update_proto(
16595 &project,
16596 update_message.borrow().clone().unwrap(),
16597 window,
16598 cx,
16599 )
16600 })
16601 .await
16602 .unwrap();
16603 follower_2
16604 .update_in(cx, |follower, window, cx| {
16605 follower.apply_update_proto(
16606 &project,
16607 update_message.borrow().clone().unwrap(),
16608 window,
16609 cx,
16610 )
16611 })
16612 .await
16613 .unwrap();
16614 update_message.borrow_mut().take();
16615 assert_eq!(
16616 follower_1.update(cx, |editor, cx| editor.text(cx)),
16617 leader.update(cx, |editor, cx| editor.text(cx))
16618 );
16619}
16620
16621#[gpui::test]
16622async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16623 init_test(cx, |_| {});
16624
16625 let mut cx = EditorTestContext::new(cx).await;
16626 let lsp_store =
16627 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
16628
16629 cx.set_state(indoc! {"
16630 ˇfn func(abc def: i32) -> u32 {
16631 }
16632 "});
16633
16634 cx.update(|_, cx| {
16635 lsp_store.update(cx, |lsp_store, cx| {
16636 lsp_store
16637 .update_diagnostics(
16638 LanguageServerId(0),
16639 lsp::PublishDiagnosticsParams {
16640 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
16641 version: None,
16642 diagnostics: vec![
16643 lsp::Diagnostic {
16644 range: lsp::Range::new(
16645 lsp::Position::new(0, 11),
16646 lsp::Position::new(0, 12),
16647 ),
16648 severity: Some(lsp::DiagnosticSeverity::ERROR),
16649 ..Default::default()
16650 },
16651 lsp::Diagnostic {
16652 range: lsp::Range::new(
16653 lsp::Position::new(0, 12),
16654 lsp::Position::new(0, 15),
16655 ),
16656 severity: Some(lsp::DiagnosticSeverity::ERROR),
16657 ..Default::default()
16658 },
16659 lsp::Diagnostic {
16660 range: lsp::Range::new(
16661 lsp::Position::new(0, 25),
16662 lsp::Position::new(0, 28),
16663 ),
16664 severity: Some(lsp::DiagnosticSeverity::ERROR),
16665 ..Default::default()
16666 },
16667 ],
16668 },
16669 None,
16670 DiagnosticSourceKind::Pushed,
16671 &[],
16672 cx,
16673 )
16674 .unwrap()
16675 });
16676 });
16677
16678 executor.run_until_parked();
16679
16680 cx.update_editor(|editor, window, cx| {
16681 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16682 });
16683
16684 cx.assert_editor_state(indoc! {"
16685 fn func(abc def: i32) -> ˇu32 {
16686 }
16687 "});
16688
16689 cx.update_editor(|editor, window, cx| {
16690 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16691 });
16692
16693 cx.assert_editor_state(indoc! {"
16694 fn func(abc ˇdef: i32) -> u32 {
16695 }
16696 "});
16697
16698 cx.update_editor(|editor, window, cx| {
16699 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16700 });
16701
16702 cx.assert_editor_state(indoc! {"
16703 fn func(abcˇ def: i32) -> u32 {
16704 }
16705 "});
16706
16707 cx.update_editor(|editor, window, cx| {
16708 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16709 });
16710
16711 cx.assert_editor_state(indoc! {"
16712 fn func(abc def: i32) -> ˇu32 {
16713 }
16714 "});
16715}
16716
16717#[gpui::test]
16718async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16719 init_test(cx, |_| {});
16720
16721 let mut cx = EditorTestContext::new(cx).await;
16722
16723 let diff_base = r#"
16724 use some::mod;
16725
16726 const A: u32 = 42;
16727
16728 fn main() {
16729 println!("hello");
16730
16731 println!("world");
16732 }
16733 "#
16734 .unindent();
16735
16736 // Edits are modified, removed, modified, added
16737 cx.set_state(
16738 &r#"
16739 use some::modified;
16740
16741 ˇ
16742 fn main() {
16743 println!("hello there");
16744
16745 println!("around the");
16746 println!("world");
16747 }
16748 "#
16749 .unindent(),
16750 );
16751
16752 cx.set_head_text(&diff_base);
16753 executor.run_until_parked();
16754
16755 cx.update_editor(|editor, window, cx| {
16756 //Wrap around the bottom of the buffer
16757 for _ in 0..3 {
16758 editor.go_to_next_hunk(&GoToHunk, window, cx);
16759 }
16760 });
16761
16762 cx.assert_editor_state(
16763 &r#"
16764 ˇuse some::modified;
16765
16766
16767 fn main() {
16768 println!("hello there");
16769
16770 println!("around the");
16771 println!("world");
16772 }
16773 "#
16774 .unindent(),
16775 );
16776
16777 cx.update_editor(|editor, window, cx| {
16778 //Wrap around the top of the buffer
16779 for _ in 0..2 {
16780 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16781 }
16782 });
16783
16784 cx.assert_editor_state(
16785 &r#"
16786 use some::modified;
16787
16788
16789 fn main() {
16790 ˇ println!("hello there");
16791
16792 println!("around the");
16793 println!("world");
16794 }
16795 "#
16796 .unindent(),
16797 );
16798
16799 cx.update_editor(|editor, window, cx| {
16800 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16801 });
16802
16803 cx.assert_editor_state(
16804 &r#"
16805 use some::modified;
16806
16807 ˇ
16808 fn main() {
16809 println!("hello there");
16810
16811 println!("around the");
16812 println!("world");
16813 }
16814 "#
16815 .unindent(),
16816 );
16817
16818 cx.update_editor(|editor, window, cx| {
16819 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16820 });
16821
16822 cx.assert_editor_state(
16823 &r#"
16824 ˇuse some::modified;
16825
16826
16827 fn main() {
16828 println!("hello there");
16829
16830 println!("around the");
16831 println!("world");
16832 }
16833 "#
16834 .unindent(),
16835 );
16836
16837 cx.update_editor(|editor, window, cx| {
16838 for _ in 0..2 {
16839 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16840 }
16841 });
16842
16843 cx.assert_editor_state(
16844 &r#"
16845 use some::modified;
16846
16847
16848 fn main() {
16849 ˇ println!("hello there");
16850
16851 println!("around the");
16852 println!("world");
16853 }
16854 "#
16855 .unindent(),
16856 );
16857
16858 cx.update_editor(|editor, window, cx| {
16859 editor.fold(&Fold, window, cx);
16860 });
16861
16862 cx.update_editor(|editor, window, cx| {
16863 editor.go_to_next_hunk(&GoToHunk, window, cx);
16864 });
16865
16866 cx.assert_editor_state(
16867 &r#"
16868 ˇuse some::modified;
16869
16870
16871 fn main() {
16872 println!("hello there");
16873
16874 println!("around the");
16875 println!("world");
16876 }
16877 "#
16878 .unindent(),
16879 );
16880}
16881
16882#[test]
16883fn test_split_words() {
16884 fn split(text: &str) -> Vec<&str> {
16885 split_words(text).collect()
16886 }
16887
16888 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
16889 assert_eq!(split("hello_world"), &["hello_", "world"]);
16890 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
16891 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
16892 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
16893 assert_eq!(split("helloworld"), &["helloworld"]);
16894
16895 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
16896}
16897
16898#[gpui::test]
16899async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
16900 init_test(cx, |_| {});
16901
16902 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
16903 let mut assert = |before, after| {
16904 let _state_context = cx.set_state(before);
16905 cx.run_until_parked();
16906 cx.update_editor(|editor, window, cx| {
16907 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
16908 });
16909 cx.run_until_parked();
16910 cx.assert_editor_state(after);
16911 };
16912
16913 // Outside bracket jumps to outside of matching bracket
16914 assert("console.logˇ(var);", "console.log(var)ˇ;");
16915 assert("console.log(var)ˇ;", "console.logˇ(var);");
16916
16917 // Inside bracket jumps to inside of matching bracket
16918 assert("console.log(ˇvar);", "console.log(varˇ);");
16919 assert("console.log(varˇ);", "console.log(ˇvar);");
16920
16921 // When outside a bracket and inside, favor jumping to the inside bracket
16922 assert(
16923 "console.log('foo', [1, 2, 3]ˇ);",
16924 "console.log(ˇ'foo', [1, 2, 3]);",
16925 );
16926 assert(
16927 "console.log(ˇ'foo', [1, 2, 3]);",
16928 "console.log('foo', [1, 2, 3]ˇ);",
16929 );
16930
16931 // Bias forward if two options are equally likely
16932 assert(
16933 "let result = curried_fun()ˇ();",
16934 "let result = curried_fun()()ˇ;",
16935 );
16936
16937 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
16938 assert(
16939 indoc! {"
16940 function test() {
16941 console.log('test')ˇ
16942 }"},
16943 indoc! {"
16944 function test() {
16945 console.logˇ('test')
16946 }"},
16947 );
16948}
16949
16950#[gpui::test]
16951async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
16952 init_test(cx, |_| {});
16953
16954 let fs = FakeFs::new(cx.executor());
16955 fs.insert_tree(
16956 path!("/a"),
16957 json!({
16958 "main.rs": "fn main() { let a = 5; }",
16959 "other.rs": "// Test file",
16960 }),
16961 )
16962 .await;
16963 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
16964
16965 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
16966 language_registry.add(Arc::new(Language::new(
16967 LanguageConfig {
16968 name: "Rust".into(),
16969 matcher: LanguageMatcher {
16970 path_suffixes: vec!["rs".to_string()],
16971 ..Default::default()
16972 },
16973 brackets: BracketPairConfig {
16974 pairs: vec![BracketPair {
16975 start: "{".to_string(),
16976 end: "}".to_string(),
16977 close: true,
16978 surround: true,
16979 newline: true,
16980 }],
16981 disabled_scopes_by_bracket_ix: Vec::new(),
16982 },
16983 ..Default::default()
16984 },
16985 Some(tree_sitter_rust::LANGUAGE.into()),
16986 )));
16987 let mut fake_servers = language_registry.register_fake_lsp(
16988 "Rust",
16989 FakeLspAdapter {
16990 capabilities: lsp::ServerCapabilities {
16991 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
16992 first_trigger_character: "{".to_string(),
16993 more_trigger_character: None,
16994 }),
16995 ..Default::default()
16996 },
16997 ..Default::default()
16998 },
16999 );
17000
17001 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17002
17003 let cx = &mut VisualTestContext::from_window(*workspace, cx);
17004
17005 let worktree_id = workspace
17006 .update(cx, |workspace, _, cx| {
17007 workspace.project().update(cx, |project, cx| {
17008 project.worktrees(cx).next().unwrap().read(cx).id()
17009 })
17010 })
17011 .unwrap();
17012
17013 let buffer = project
17014 .update(cx, |project, cx| {
17015 project.open_local_buffer(path!("/a/main.rs"), cx)
17016 })
17017 .await
17018 .unwrap();
17019 let editor_handle = workspace
17020 .update(cx, |workspace, window, cx| {
17021 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
17022 })
17023 .unwrap()
17024 .await
17025 .unwrap()
17026 .downcast::<Editor>()
17027 .unwrap();
17028
17029 cx.executor().start_waiting();
17030 let fake_server = fake_servers.next().await.unwrap();
17031
17032 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
17033 |params, _| async move {
17034 assert_eq!(
17035 params.text_document_position.text_document.uri,
17036 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
17037 );
17038 assert_eq!(
17039 params.text_document_position.position,
17040 lsp::Position::new(0, 21),
17041 );
17042
17043 Ok(Some(vec![lsp::TextEdit {
17044 new_text: "]".to_string(),
17045 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17046 }]))
17047 },
17048 );
17049
17050 editor_handle.update_in(cx, |editor, window, cx| {
17051 window.focus(&editor.focus_handle(cx));
17052 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17053 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
17054 });
17055 editor.handle_input("{", window, cx);
17056 });
17057
17058 cx.executor().run_until_parked();
17059
17060 buffer.update(cx, |buffer, _| {
17061 assert_eq!(
17062 buffer.text(),
17063 "fn main() { let a = {5}; }",
17064 "No extra braces from on type formatting should appear in the buffer"
17065 )
17066 });
17067}
17068
17069#[gpui::test(iterations = 20, seeds(31))]
17070async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
17071 init_test(cx, |_| {});
17072
17073 let mut cx = EditorLspTestContext::new_rust(
17074 lsp::ServerCapabilities {
17075 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
17076 first_trigger_character: ".".to_string(),
17077 more_trigger_character: None,
17078 }),
17079 ..Default::default()
17080 },
17081 cx,
17082 )
17083 .await;
17084
17085 cx.update_buffer(|buffer, _| {
17086 // This causes autoindent to be async.
17087 buffer.set_sync_parse_timeout(Duration::ZERO)
17088 });
17089
17090 cx.set_state("fn c() {\n d()ˇ\n}\n");
17091 cx.simulate_keystroke("\n");
17092 cx.run_until_parked();
17093
17094 let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap());
17095 let mut request =
17096 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
17097 let buffer_cloned = buffer_cloned.clone();
17098 async move {
17099 buffer_cloned.update(&mut cx, |buffer, _| {
17100 assert_eq!(
17101 buffer.text(),
17102 "fn c() {\n d()\n .\n}\n",
17103 "OnTypeFormatting should triggered after autoindent applied"
17104 )
17105 })?;
17106
17107 Ok(Some(vec![]))
17108 }
17109 });
17110
17111 cx.simulate_keystroke(".");
17112 cx.run_until_parked();
17113
17114 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
17115 assert!(request.next().await.is_some());
17116 request.close();
17117 assert!(request.next().await.is_none());
17118}
17119
17120#[gpui::test]
17121async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
17122 init_test(cx, |_| {});
17123
17124 let fs = FakeFs::new(cx.executor());
17125 fs.insert_tree(
17126 path!("/a"),
17127 json!({
17128 "main.rs": "fn main() { let a = 5; }",
17129 "other.rs": "// Test file",
17130 }),
17131 )
17132 .await;
17133
17134 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17135
17136 let server_restarts = Arc::new(AtomicUsize::new(0));
17137 let closure_restarts = Arc::clone(&server_restarts);
17138 let language_server_name = "test language server";
17139 let language_name: LanguageName = "Rust".into();
17140
17141 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17142 language_registry.add(Arc::new(Language::new(
17143 LanguageConfig {
17144 name: language_name.clone(),
17145 matcher: LanguageMatcher {
17146 path_suffixes: vec!["rs".to_string()],
17147 ..Default::default()
17148 },
17149 ..Default::default()
17150 },
17151 Some(tree_sitter_rust::LANGUAGE.into()),
17152 )));
17153 let mut fake_servers = language_registry.register_fake_lsp(
17154 "Rust",
17155 FakeLspAdapter {
17156 name: language_server_name,
17157 initialization_options: Some(json!({
17158 "testOptionValue": true
17159 })),
17160 initializer: Some(Box::new(move |fake_server| {
17161 let task_restarts = Arc::clone(&closure_restarts);
17162 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
17163 task_restarts.fetch_add(1, atomic::Ordering::Release);
17164 futures::future::ready(Ok(()))
17165 });
17166 })),
17167 ..Default::default()
17168 },
17169 );
17170
17171 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17172 let _buffer = project
17173 .update(cx, |project, cx| {
17174 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
17175 })
17176 .await
17177 .unwrap();
17178 let _fake_server = fake_servers.next().await.unwrap();
17179 update_test_language_settings(cx, |language_settings| {
17180 language_settings.languages.0.insert(
17181 language_name.clone().0,
17182 LanguageSettingsContent {
17183 tab_size: NonZeroU32::new(8),
17184 ..Default::default()
17185 },
17186 );
17187 });
17188 cx.executor().run_until_parked();
17189 assert_eq!(
17190 server_restarts.load(atomic::Ordering::Acquire),
17191 0,
17192 "Should not restart LSP server on an unrelated change"
17193 );
17194
17195 update_test_project_settings(cx, |project_settings| {
17196 project_settings.lsp.insert(
17197 "Some other server name".into(),
17198 LspSettings {
17199 binary: None,
17200 settings: None,
17201 initialization_options: Some(json!({
17202 "some other init value": false
17203 })),
17204 enable_lsp_tasks: false,
17205 fetch: None,
17206 },
17207 );
17208 });
17209 cx.executor().run_until_parked();
17210 assert_eq!(
17211 server_restarts.load(atomic::Ordering::Acquire),
17212 0,
17213 "Should not restart LSP server on an unrelated LSP settings change"
17214 );
17215
17216 update_test_project_settings(cx, |project_settings| {
17217 project_settings.lsp.insert(
17218 language_server_name.into(),
17219 LspSettings {
17220 binary: None,
17221 settings: None,
17222 initialization_options: Some(json!({
17223 "anotherInitValue": false
17224 })),
17225 enable_lsp_tasks: false,
17226 fetch: None,
17227 },
17228 );
17229 });
17230 cx.executor().run_until_parked();
17231 assert_eq!(
17232 server_restarts.load(atomic::Ordering::Acquire),
17233 1,
17234 "Should restart LSP server on a related LSP settings change"
17235 );
17236
17237 update_test_project_settings(cx, |project_settings| {
17238 project_settings.lsp.insert(
17239 language_server_name.into(),
17240 LspSettings {
17241 binary: None,
17242 settings: None,
17243 initialization_options: Some(json!({
17244 "anotherInitValue": false
17245 })),
17246 enable_lsp_tasks: false,
17247 fetch: None,
17248 },
17249 );
17250 });
17251 cx.executor().run_until_parked();
17252 assert_eq!(
17253 server_restarts.load(atomic::Ordering::Acquire),
17254 1,
17255 "Should not restart LSP server on a related LSP settings change that is the same"
17256 );
17257
17258 update_test_project_settings(cx, |project_settings| {
17259 project_settings.lsp.insert(
17260 language_server_name.into(),
17261 LspSettings {
17262 binary: None,
17263 settings: None,
17264 initialization_options: None,
17265 enable_lsp_tasks: false,
17266 fetch: None,
17267 },
17268 );
17269 });
17270 cx.executor().run_until_parked();
17271 assert_eq!(
17272 server_restarts.load(atomic::Ordering::Acquire),
17273 2,
17274 "Should restart LSP server on another related LSP settings change"
17275 );
17276}
17277
17278#[gpui::test]
17279async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
17280 init_test(cx, |_| {});
17281
17282 let mut cx = EditorLspTestContext::new_rust(
17283 lsp::ServerCapabilities {
17284 completion_provider: Some(lsp::CompletionOptions {
17285 trigger_characters: Some(vec![".".to_string()]),
17286 resolve_provider: Some(true),
17287 ..Default::default()
17288 }),
17289 ..Default::default()
17290 },
17291 cx,
17292 )
17293 .await;
17294
17295 cx.set_state("fn main() { let a = 2ˇ; }");
17296 cx.simulate_keystroke(".");
17297 let completion_item = lsp::CompletionItem {
17298 label: "some".into(),
17299 kind: Some(lsp::CompletionItemKind::SNIPPET),
17300 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
17301 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
17302 kind: lsp::MarkupKind::Markdown,
17303 value: "```rust\nSome(2)\n```".to_string(),
17304 })),
17305 deprecated: Some(false),
17306 sort_text: Some("fffffff2".to_string()),
17307 filter_text: Some("some".to_string()),
17308 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
17309 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17310 range: lsp::Range {
17311 start: lsp::Position {
17312 line: 0,
17313 character: 22,
17314 },
17315 end: lsp::Position {
17316 line: 0,
17317 character: 22,
17318 },
17319 },
17320 new_text: "Some(2)".to_string(),
17321 })),
17322 additional_text_edits: Some(vec![lsp::TextEdit {
17323 range: lsp::Range {
17324 start: lsp::Position {
17325 line: 0,
17326 character: 20,
17327 },
17328 end: lsp::Position {
17329 line: 0,
17330 character: 22,
17331 },
17332 },
17333 new_text: "".to_string(),
17334 }]),
17335 ..Default::default()
17336 };
17337
17338 let closure_completion_item = completion_item.clone();
17339 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17340 let task_completion_item = closure_completion_item.clone();
17341 async move {
17342 Ok(Some(lsp::CompletionResponse::Array(vec![
17343 task_completion_item,
17344 ])))
17345 }
17346 });
17347
17348 request.next().await;
17349
17350 cx.condition(|editor, _| editor.context_menu_visible())
17351 .await;
17352 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
17353 editor
17354 .confirm_completion(&ConfirmCompletion::default(), window, cx)
17355 .unwrap()
17356 });
17357 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
17358
17359 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
17360 let task_completion_item = completion_item.clone();
17361 async move { Ok(task_completion_item) }
17362 })
17363 .next()
17364 .await
17365 .unwrap();
17366 apply_additional_edits.await.unwrap();
17367 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
17368}
17369
17370#[gpui::test]
17371async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
17372 init_test(cx, |_| {});
17373
17374 let mut cx = EditorLspTestContext::new_rust(
17375 lsp::ServerCapabilities {
17376 completion_provider: Some(lsp::CompletionOptions {
17377 trigger_characters: Some(vec![".".to_string()]),
17378 resolve_provider: Some(true),
17379 ..Default::default()
17380 }),
17381 ..Default::default()
17382 },
17383 cx,
17384 )
17385 .await;
17386
17387 cx.set_state("fn main() { let a = 2ˇ; }");
17388 cx.simulate_keystroke(".");
17389
17390 let item1 = lsp::CompletionItem {
17391 label: "method id()".to_string(),
17392 filter_text: Some("id".to_string()),
17393 detail: None,
17394 documentation: None,
17395 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17396 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17397 new_text: ".id".to_string(),
17398 })),
17399 ..lsp::CompletionItem::default()
17400 };
17401
17402 let item2 = lsp::CompletionItem {
17403 label: "other".to_string(),
17404 filter_text: Some("other".to_string()),
17405 detail: None,
17406 documentation: None,
17407 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17408 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17409 new_text: ".other".to_string(),
17410 })),
17411 ..lsp::CompletionItem::default()
17412 };
17413
17414 let item1 = item1.clone();
17415 cx.set_request_handler::<lsp::request::Completion, _, _>({
17416 let item1 = item1.clone();
17417 move |_, _, _| {
17418 let item1 = item1.clone();
17419 let item2 = item2.clone();
17420 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
17421 }
17422 })
17423 .next()
17424 .await;
17425
17426 cx.condition(|editor, _| editor.context_menu_visible())
17427 .await;
17428 cx.update_editor(|editor, _, _| {
17429 let context_menu = editor.context_menu.borrow_mut();
17430 let context_menu = context_menu
17431 .as_ref()
17432 .expect("Should have the context menu deployed");
17433 match context_menu {
17434 CodeContextMenu::Completions(completions_menu) => {
17435 let completions = completions_menu.completions.borrow_mut();
17436 assert_eq!(
17437 completions
17438 .iter()
17439 .map(|completion| &completion.label.text)
17440 .collect::<Vec<_>>(),
17441 vec!["method id()", "other"]
17442 )
17443 }
17444 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17445 }
17446 });
17447
17448 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
17449 let item1 = item1.clone();
17450 move |_, item_to_resolve, _| {
17451 let item1 = item1.clone();
17452 async move {
17453 if item1 == item_to_resolve {
17454 Ok(lsp::CompletionItem {
17455 label: "method id()".to_string(),
17456 filter_text: Some("id".to_string()),
17457 detail: Some("Now resolved!".to_string()),
17458 documentation: Some(lsp::Documentation::String("Docs".to_string())),
17459 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17460 range: lsp::Range::new(
17461 lsp::Position::new(0, 22),
17462 lsp::Position::new(0, 22),
17463 ),
17464 new_text: ".id".to_string(),
17465 })),
17466 ..lsp::CompletionItem::default()
17467 })
17468 } else {
17469 Ok(item_to_resolve)
17470 }
17471 }
17472 }
17473 })
17474 .next()
17475 .await
17476 .unwrap();
17477 cx.run_until_parked();
17478
17479 cx.update_editor(|editor, window, cx| {
17480 editor.context_menu_next(&Default::default(), window, cx);
17481 });
17482
17483 cx.update_editor(|editor, _, _| {
17484 let context_menu = editor.context_menu.borrow_mut();
17485 let context_menu = context_menu
17486 .as_ref()
17487 .expect("Should have the context menu deployed");
17488 match context_menu {
17489 CodeContextMenu::Completions(completions_menu) => {
17490 let completions = completions_menu.completions.borrow_mut();
17491 assert_eq!(
17492 completions
17493 .iter()
17494 .map(|completion| &completion.label.text)
17495 .collect::<Vec<_>>(),
17496 vec!["method id() Now resolved!", "other"],
17497 "Should update first completion label, but not second as the filter text did not match."
17498 );
17499 }
17500 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17501 }
17502 });
17503}
17504
17505#[gpui::test]
17506async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
17507 init_test(cx, |_| {});
17508 let mut cx = EditorLspTestContext::new_rust(
17509 lsp::ServerCapabilities {
17510 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
17511 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
17512 completion_provider: Some(lsp::CompletionOptions {
17513 resolve_provider: Some(true),
17514 ..Default::default()
17515 }),
17516 ..Default::default()
17517 },
17518 cx,
17519 )
17520 .await;
17521 cx.set_state(indoc! {"
17522 struct TestStruct {
17523 field: i32
17524 }
17525
17526 fn mainˇ() {
17527 let unused_var = 42;
17528 let test_struct = TestStruct { field: 42 };
17529 }
17530 "});
17531 let symbol_range = cx.lsp_range(indoc! {"
17532 struct TestStruct {
17533 field: i32
17534 }
17535
17536 «fn main»() {
17537 let unused_var = 42;
17538 let test_struct = TestStruct { field: 42 };
17539 }
17540 "});
17541 let mut hover_requests =
17542 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
17543 Ok(Some(lsp::Hover {
17544 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
17545 kind: lsp::MarkupKind::Markdown,
17546 value: "Function documentation".to_string(),
17547 }),
17548 range: Some(symbol_range),
17549 }))
17550 });
17551
17552 // Case 1: Test that code action menu hide hover popover
17553 cx.dispatch_action(Hover);
17554 hover_requests.next().await;
17555 cx.condition(|editor, _| editor.hover_state.visible()).await;
17556 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
17557 move |_, _, _| async move {
17558 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
17559 lsp::CodeAction {
17560 title: "Remove unused variable".to_string(),
17561 kind: Some(CodeActionKind::QUICKFIX),
17562 edit: Some(lsp::WorkspaceEdit {
17563 changes: Some(
17564 [(
17565 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
17566 vec![lsp::TextEdit {
17567 range: lsp::Range::new(
17568 lsp::Position::new(5, 4),
17569 lsp::Position::new(5, 27),
17570 ),
17571 new_text: "".to_string(),
17572 }],
17573 )]
17574 .into_iter()
17575 .collect(),
17576 ),
17577 ..Default::default()
17578 }),
17579 ..Default::default()
17580 },
17581 )]))
17582 },
17583 );
17584 cx.update_editor(|editor, window, cx| {
17585 editor.toggle_code_actions(
17586 &ToggleCodeActions {
17587 deployed_from: None,
17588 quick_launch: false,
17589 },
17590 window,
17591 cx,
17592 );
17593 });
17594 code_action_requests.next().await;
17595 cx.run_until_parked();
17596 cx.condition(|editor, _| editor.context_menu_visible())
17597 .await;
17598 cx.update_editor(|editor, _, _| {
17599 assert!(
17600 !editor.hover_state.visible(),
17601 "Hover popover should be hidden when code action menu is shown"
17602 );
17603 // Hide code actions
17604 editor.context_menu.take();
17605 });
17606
17607 // Case 2: Test that code completions hide hover popover
17608 cx.dispatch_action(Hover);
17609 hover_requests.next().await;
17610 cx.condition(|editor, _| editor.hover_state.visible()).await;
17611 let counter = Arc::new(AtomicUsize::new(0));
17612 let mut completion_requests =
17613 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17614 let counter = counter.clone();
17615 async move {
17616 counter.fetch_add(1, atomic::Ordering::Release);
17617 Ok(Some(lsp::CompletionResponse::Array(vec![
17618 lsp::CompletionItem {
17619 label: "main".into(),
17620 kind: Some(lsp::CompletionItemKind::FUNCTION),
17621 detail: Some("() -> ()".to_string()),
17622 ..Default::default()
17623 },
17624 lsp::CompletionItem {
17625 label: "TestStruct".into(),
17626 kind: Some(lsp::CompletionItemKind::STRUCT),
17627 detail: Some("struct TestStruct".to_string()),
17628 ..Default::default()
17629 },
17630 ])))
17631 }
17632 });
17633 cx.update_editor(|editor, window, cx| {
17634 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
17635 });
17636 completion_requests.next().await;
17637 cx.condition(|editor, _| editor.context_menu_visible())
17638 .await;
17639 cx.update_editor(|editor, _, _| {
17640 assert!(
17641 !editor.hover_state.visible(),
17642 "Hover popover should be hidden when completion menu is shown"
17643 );
17644 });
17645}
17646
17647#[gpui::test]
17648async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
17649 init_test(cx, |_| {});
17650
17651 let mut cx = EditorLspTestContext::new_rust(
17652 lsp::ServerCapabilities {
17653 completion_provider: Some(lsp::CompletionOptions {
17654 trigger_characters: Some(vec![".".to_string()]),
17655 resolve_provider: Some(true),
17656 ..Default::default()
17657 }),
17658 ..Default::default()
17659 },
17660 cx,
17661 )
17662 .await;
17663
17664 cx.set_state("fn main() { let a = 2ˇ; }");
17665 cx.simulate_keystroke(".");
17666
17667 let unresolved_item_1 = lsp::CompletionItem {
17668 label: "id".to_string(),
17669 filter_text: Some("id".to_string()),
17670 detail: None,
17671 documentation: None,
17672 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17673 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17674 new_text: ".id".to_string(),
17675 })),
17676 ..lsp::CompletionItem::default()
17677 };
17678 let resolved_item_1 = lsp::CompletionItem {
17679 additional_text_edits: Some(vec![lsp::TextEdit {
17680 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
17681 new_text: "!!".to_string(),
17682 }]),
17683 ..unresolved_item_1.clone()
17684 };
17685 let unresolved_item_2 = lsp::CompletionItem {
17686 label: "other".to_string(),
17687 filter_text: Some("other".to_string()),
17688 detail: None,
17689 documentation: None,
17690 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17691 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17692 new_text: ".other".to_string(),
17693 })),
17694 ..lsp::CompletionItem::default()
17695 };
17696 let resolved_item_2 = lsp::CompletionItem {
17697 additional_text_edits: Some(vec![lsp::TextEdit {
17698 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
17699 new_text: "??".to_string(),
17700 }]),
17701 ..unresolved_item_2.clone()
17702 };
17703
17704 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
17705 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
17706 cx.lsp
17707 .server
17708 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
17709 let unresolved_item_1 = unresolved_item_1.clone();
17710 let resolved_item_1 = resolved_item_1.clone();
17711 let unresolved_item_2 = unresolved_item_2.clone();
17712 let resolved_item_2 = resolved_item_2.clone();
17713 let resolve_requests_1 = resolve_requests_1.clone();
17714 let resolve_requests_2 = resolve_requests_2.clone();
17715 move |unresolved_request, _| {
17716 let unresolved_item_1 = unresolved_item_1.clone();
17717 let resolved_item_1 = resolved_item_1.clone();
17718 let unresolved_item_2 = unresolved_item_2.clone();
17719 let resolved_item_2 = resolved_item_2.clone();
17720 let resolve_requests_1 = resolve_requests_1.clone();
17721 let resolve_requests_2 = resolve_requests_2.clone();
17722 async move {
17723 if unresolved_request == unresolved_item_1 {
17724 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
17725 Ok(resolved_item_1.clone())
17726 } else if unresolved_request == unresolved_item_2 {
17727 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
17728 Ok(resolved_item_2.clone())
17729 } else {
17730 panic!("Unexpected completion item {unresolved_request:?}")
17731 }
17732 }
17733 }
17734 })
17735 .detach();
17736
17737 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17738 let unresolved_item_1 = unresolved_item_1.clone();
17739 let unresolved_item_2 = unresolved_item_2.clone();
17740 async move {
17741 Ok(Some(lsp::CompletionResponse::Array(vec![
17742 unresolved_item_1,
17743 unresolved_item_2,
17744 ])))
17745 }
17746 })
17747 .next()
17748 .await;
17749
17750 cx.condition(|editor, _| editor.context_menu_visible())
17751 .await;
17752 cx.update_editor(|editor, _, _| {
17753 let context_menu = editor.context_menu.borrow_mut();
17754 let context_menu = context_menu
17755 .as_ref()
17756 .expect("Should have the context menu deployed");
17757 match context_menu {
17758 CodeContextMenu::Completions(completions_menu) => {
17759 let completions = completions_menu.completions.borrow_mut();
17760 assert_eq!(
17761 completions
17762 .iter()
17763 .map(|completion| &completion.label.text)
17764 .collect::<Vec<_>>(),
17765 vec!["id", "other"]
17766 )
17767 }
17768 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17769 }
17770 });
17771 cx.run_until_parked();
17772
17773 cx.update_editor(|editor, window, cx| {
17774 editor.context_menu_next(&ContextMenuNext, window, cx);
17775 });
17776 cx.run_until_parked();
17777 cx.update_editor(|editor, window, cx| {
17778 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
17779 });
17780 cx.run_until_parked();
17781 cx.update_editor(|editor, window, cx| {
17782 editor.context_menu_next(&ContextMenuNext, window, cx);
17783 });
17784 cx.run_until_parked();
17785 cx.update_editor(|editor, window, cx| {
17786 editor
17787 .compose_completion(&ComposeCompletion::default(), window, cx)
17788 .expect("No task returned")
17789 })
17790 .await
17791 .expect("Completion failed");
17792 cx.run_until_parked();
17793
17794 cx.update_editor(|editor, _, cx| {
17795 assert_eq!(
17796 resolve_requests_1.load(atomic::Ordering::Acquire),
17797 1,
17798 "Should always resolve once despite multiple selections"
17799 );
17800 assert_eq!(
17801 resolve_requests_2.load(atomic::Ordering::Acquire),
17802 1,
17803 "Should always resolve once after multiple selections and applying the completion"
17804 );
17805 assert_eq!(
17806 editor.text(cx),
17807 "fn main() { let a = ??.other; }",
17808 "Should use resolved data when applying the completion"
17809 );
17810 });
17811}
17812
17813#[gpui::test]
17814async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
17815 init_test(cx, |_| {});
17816
17817 let item_0 = lsp::CompletionItem {
17818 label: "abs".into(),
17819 insert_text: Some("abs".into()),
17820 data: Some(json!({ "very": "special"})),
17821 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
17822 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
17823 lsp::InsertReplaceEdit {
17824 new_text: "abs".to_string(),
17825 insert: lsp::Range::default(),
17826 replace: lsp::Range::default(),
17827 },
17828 )),
17829 ..lsp::CompletionItem::default()
17830 };
17831 let items = iter::once(item_0.clone())
17832 .chain((11..51).map(|i| lsp::CompletionItem {
17833 label: format!("item_{}", i),
17834 insert_text: Some(format!("item_{}", i)),
17835 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
17836 ..lsp::CompletionItem::default()
17837 }))
17838 .collect::<Vec<_>>();
17839
17840 let default_commit_characters = vec!["?".to_string()];
17841 let default_data = json!({ "default": "data"});
17842 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
17843 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
17844 let default_edit_range = lsp::Range {
17845 start: lsp::Position {
17846 line: 0,
17847 character: 5,
17848 },
17849 end: lsp::Position {
17850 line: 0,
17851 character: 5,
17852 },
17853 };
17854
17855 let mut cx = EditorLspTestContext::new_rust(
17856 lsp::ServerCapabilities {
17857 completion_provider: Some(lsp::CompletionOptions {
17858 trigger_characters: Some(vec![".".to_string()]),
17859 resolve_provider: Some(true),
17860 ..Default::default()
17861 }),
17862 ..Default::default()
17863 },
17864 cx,
17865 )
17866 .await;
17867
17868 cx.set_state("fn main() { let a = 2ˇ; }");
17869 cx.simulate_keystroke(".");
17870
17871 let completion_data = default_data.clone();
17872 let completion_characters = default_commit_characters.clone();
17873 let completion_items = items.clone();
17874 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17875 let default_data = completion_data.clone();
17876 let default_commit_characters = completion_characters.clone();
17877 let items = completion_items.clone();
17878 async move {
17879 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
17880 items,
17881 item_defaults: Some(lsp::CompletionListItemDefaults {
17882 data: Some(default_data.clone()),
17883 commit_characters: Some(default_commit_characters.clone()),
17884 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
17885 default_edit_range,
17886 )),
17887 insert_text_format: Some(default_insert_text_format),
17888 insert_text_mode: Some(default_insert_text_mode),
17889 }),
17890 ..lsp::CompletionList::default()
17891 })))
17892 }
17893 })
17894 .next()
17895 .await;
17896
17897 let resolved_items = Arc::new(Mutex::new(Vec::new()));
17898 cx.lsp
17899 .server
17900 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
17901 let closure_resolved_items = resolved_items.clone();
17902 move |item_to_resolve, _| {
17903 let closure_resolved_items = closure_resolved_items.clone();
17904 async move {
17905 closure_resolved_items.lock().push(item_to_resolve.clone());
17906 Ok(item_to_resolve)
17907 }
17908 }
17909 })
17910 .detach();
17911
17912 cx.condition(|editor, _| editor.context_menu_visible())
17913 .await;
17914 cx.run_until_parked();
17915 cx.update_editor(|editor, _, _| {
17916 let menu = editor.context_menu.borrow_mut();
17917 match menu.as_ref().expect("should have the completions menu") {
17918 CodeContextMenu::Completions(completions_menu) => {
17919 assert_eq!(
17920 completions_menu
17921 .entries
17922 .borrow()
17923 .iter()
17924 .map(|mat| mat.string.clone())
17925 .collect::<Vec<String>>(),
17926 items
17927 .iter()
17928 .map(|completion| completion.label.clone())
17929 .collect::<Vec<String>>()
17930 );
17931 }
17932 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
17933 }
17934 });
17935 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
17936 // with 4 from the end.
17937 assert_eq!(
17938 *resolved_items.lock(),
17939 [&items[0..16], &items[items.len() - 4..items.len()]]
17940 .concat()
17941 .iter()
17942 .cloned()
17943 .map(|mut item| {
17944 if item.data.is_none() {
17945 item.data = Some(default_data.clone());
17946 }
17947 item
17948 })
17949 .collect::<Vec<lsp::CompletionItem>>(),
17950 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
17951 );
17952 resolved_items.lock().clear();
17953
17954 cx.update_editor(|editor, window, cx| {
17955 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
17956 });
17957 cx.run_until_parked();
17958 // Completions that have already been resolved are skipped.
17959 assert_eq!(
17960 *resolved_items.lock(),
17961 items[items.len() - 17..items.len() - 4]
17962 .iter()
17963 .cloned()
17964 .map(|mut item| {
17965 if item.data.is_none() {
17966 item.data = Some(default_data.clone());
17967 }
17968 item
17969 })
17970 .collect::<Vec<lsp::CompletionItem>>()
17971 );
17972 resolved_items.lock().clear();
17973}
17974
17975#[gpui::test]
17976async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
17977 init_test(cx, |_| {});
17978
17979 let mut cx = EditorLspTestContext::new(
17980 Language::new(
17981 LanguageConfig {
17982 matcher: LanguageMatcher {
17983 path_suffixes: vec!["jsx".into()],
17984 ..Default::default()
17985 },
17986 overrides: [(
17987 "element".into(),
17988 LanguageConfigOverride {
17989 completion_query_characters: Override::Set(['-'].into_iter().collect()),
17990 ..Default::default()
17991 },
17992 )]
17993 .into_iter()
17994 .collect(),
17995 ..Default::default()
17996 },
17997 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
17998 )
17999 .with_override_query("(jsx_self_closing_element) @element")
18000 .unwrap(),
18001 lsp::ServerCapabilities {
18002 completion_provider: Some(lsp::CompletionOptions {
18003 trigger_characters: Some(vec![":".to_string()]),
18004 ..Default::default()
18005 }),
18006 ..Default::default()
18007 },
18008 cx,
18009 )
18010 .await;
18011
18012 cx.lsp
18013 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
18014 Ok(Some(lsp::CompletionResponse::Array(vec![
18015 lsp::CompletionItem {
18016 label: "bg-blue".into(),
18017 ..Default::default()
18018 },
18019 lsp::CompletionItem {
18020 label: "bg-red".into(),
18021 ..Default::default()
18022 },
18023 lsp::CompletionItem {
18024 label: "bg-yellow".into(),
18025 ..Default::default()
18026 },
18027 ])))
18028 });
18029
18030 cx.set_state(r#"<p class="bgˇ" />"#);
18031
18032 // Trigger completion when typing a dash, because the dash is an extra
18033 // word character in the 'element' scope, which contains the cursor.
18034 cx.simulate_keystroke("-");
18035 cx.executor().run_until_parked();
18036 cx.update_editor(|editor, _, _| {
18037 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18038 {
18039 assert_eq!(
18040 completion_menu_entries(menu),
18041 &["bg-blue", "bg-red", "bg-yellow"]
18042 );
18043 } else {
18044 panic!("expected completion menu to be open");
18045 }
18046 });
18047
18048 cx.simulate_keystroke("l");
18049 cx.executor().run_until_parked();
18050 cx.update_editor(|editor, _, _| {
18051 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18052 {
18053 assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]);
18054 } else {
18055 panic!("expected completion menu to be open");
18056 }
18057 });
18058
18059 // When filtering completions, consider the character after the '-' to
18060 // be the start of a subword.
18061 cx.set_state(r#"<p class="yelˇ" />"#);
18062 cx.simulate_keystroke("l");
18063 cx.executor().run_until_parked();
18064 cx.update_editor(|editor, _, _| {
18065 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18066 {
18067 assert_eq!(completion_menu_entries(menu), &["bg-yellow"]);
18068 } else {
18069 panic!("expected completion menu to be open");
18070 }
18071 });
18072}
18073
18074fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
18075 let entries = menu.entries.borrow();
18076 entries.iter().map(|mat| mat.string.clone()).collect()
18077}
18078
18079#[gpui::test]
18080async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
18081 init_test(cx, |settings| {
18082 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
18083 Formatter::Prettier,
18084 )))
18085 });
18086
18087 let fs = FakeFs::new(cx.executor());
18088 fs.insert_file(path!("/file.ts"), Default::default()).await;
18089
18090 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
18091 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18092
18093 language_registry.add(Arc::new(Language::new(
18094 LanguageConfig {
18095 name: "TypeScript".into(),
18096 matcher: LanguageMatcher {
18097 path_suffixes: vec!["ts".to_string()],
18098 ..Default::default()
18099 },
18100 ..Default::default()
18101 },
18102 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
18103 )));
18104 update_test_language_settings(cx, |settings| {
18105 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
18106 });
18107
18108 let test_plugin = "test_plugin";
18109 let _ = language_registry.register_fake_lsp(
18110 "TypeScript",
18111 FakeLspAdapter {
18112 prettier_plugins: vec![test_plugin],
18113 ..Default::default()
18114 },
18115 );
18116
18117 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
18118 let buffer = project
18119 .update(cx, |project, cx| {
18120 project.open_local_buffer(path!("/file.ts"), cx)
18121 })
18122 .await
18123 .unwrap();
18124
18125 let buffer_text = "one\ntwo\nthree\n";
18126 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
18127 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
18128 editor.update_in(cx, |editor, window, cx| {
18129 editor.set_text(buffer_text, window, cx)
18130 });
18131
18132 editor
18133 .update_in(cx, |editor, window, cx| {
18134 editor.perform_format(
18135 project.clone(),
18136 FormatTrigger::Manual,
18137 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18138 window,
18139 cx,
18140 )
18141 })
18142 .unwrap()
18143 .await;
18144 assert_eq!(
18145 editor.update(cx, |editor, cx| editor.text(cx)),
18146 buffer_text.to_string() + prettier_format_suffix,
18147 "Test prettier formatting was not applied to the original buffer text",
18148 );
18149
18150 update_test_language_settings(cx, |settings| {
18151 settings.defaults.formatter = Some(SelectedFormatter::Auto)
18152 });
18153 let format = editor.update_in(cx, |editor, window, cx| {
18154 editor.perform_format(
18155 project.clone(),
18156 FormatTrigger::Manual,
18157 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18158 window,
18159 cx,
18160 )
18161 });
18162 format.await.unwrap();
18163 assert_eq!(
18164 editor.update(cx, |editor, cx| editor.text(cx)),
18165 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
18166 "Autoformatting (via test prettier) was not applied to the original buffer text",
18167 );
18168}
18169
18170#[gpui::test]
18171async fn test_addition_reverts(cx: &mut TestAppContext) {
18172 init_test(cx, |_| {});
18173 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18174 let base_text = indoc! {r#"
18175 struct Row;
18176 struct Row1;
18177 struct Row2;
18178
18179 struct Row4;
18180 struct Row5;
18181 struct Row6;
18182
18183 struct Row8;
18184 struct Row9;
18185 struct Row10;"#};
18186
18187 // When addition hunks are not adjacent to carets, no hunk revert is performed
18188 assert_hunk_revert(
18189 indoc! {r#"struct Row;
18190 struct Row1;
18191 struct Row1.1;
18192 struct Row1.2;
18193 struct Row2;ˇ
18194
18195 struct Row4;
18196 struct Row5;
18197 struct Row6;
18198
18199 struct Row8;
18200 ˇstruct Row9;
18201 struct Row9.1;
18202 struct Row9.2;
18203 struct Row9.3;
18204 struct Row10;"#},
18205 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18206 indoc! {r#"struct Row;
18207 struct Row1;
18208 struct Row1.1;
18209 struct Row1.2;
18210 struct Row2;ˇ
18211
18212 struct Row4;
18213 struct Row5;
18214 struct Row6;
18215
18216 struct Row8;
18217 ˇstruct Row9;
18218 struct Row9.1;
18219 struct Row9.2;
18220 struct Row9.3;
18221 struct Row10;"#},
18222 base_text,
18223 &mut cx,
18224 );
18225 // Same for selections
18226 assert_hunk_revert(
18227 indoc! {r#"struct Row;
18228 struct Row1;
18229 struct Row2;
18230 struct Row2.1;
18231 struct Row2.2;
18232 «ˇ
18233 struct Row4;
18234 struct» Row5;
18235 «struct Row6;
18236 ˇ»
18237 struct Row9.1;
18238 struct Row9.2;
18239 struct Row9.3;
18240 struct Row8;
18241 struct Row9;
18242 struct Row10;"#},
18243 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18244 indoc! {r#"struct Row;
18245 struct Row1;
18246 struct Row2;
18247 struct Row2.1;
18248 struct Row2.2;
18249 «ˇ
18250 struct Row4;
18251 struct» Row5;
18252 «struct Row6;
18253 ˇ»
18254 struct Row9.1;
18255 struct Row9.2;
18256 struct Row9.3;
18257 struct Row8;
18258 struct Row9;
18259 struct Row10;"#},
18260 base_text,
18261 &mut cx,
18262 );
18263
18264 // When carets and selections intersect the addition hunks, those are reverted.
18265 // Adjacent carets got merged.
18266 assert_hunk_revert(
18267 indoc! {r#"struct Row;
18268 ˇ// something on the top
18269 struct Row1;
18270 struct Row2;
18271 struct Roˇw3.1;
18272 struct Row2.2;
18273 struct Row2.3;ˇ
18274
18275 struct Row4;
18276 struct ˇRow5.1;
18277 struct Row5.2;
18278 struct «Rowˇ»5.3;
18279 struct Row5;
18280 struct Row6;
18281 ˇ
18282 struct Row9.1;
18283 struct «Rowˇ»9.2;
18284 struct «ˇRow»9.3;
18285 struct Row8;
18286 struct Row9;
18287 «ˇ// something on bottom»
18288 struct Row10;"#},
18289 vec![
18290 DiffHunkStatusKind::Added,
18291 DiffHunkStatusKind::Added,
18292 DiffHunkStatusKind::Added,
18293 DiffHunkStatusKind::Added,
18294 DiffHunkStatusKind::Added,
18295 ],
18296 indoc! {r#"struct Row;
18297 ˇstruct Row1;
18298 struct Row2;
18299 ˇ
18300 struct Row4;
18301 ˇstruct Row5;
18302 struct Row6;
18303 ˇ
18304 ˇstruct Row8;
18305 struct Row9;
18306 ˇstruct Row10;"#},
18307 base_text,
18308 &mut cx,
18309 );
18310}
18311
18312#[gpui::test]
18313async fn test_modification_reverts(cx: &mut TestAppContext) {
18314 init_test(cx, |_| {});
18315 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18316 let base_text = indoc! {r#"
18317 struct Row;
18318 struct Row1;
18319 struct Row2;
18320
18321 struct Row4;
18322 struct Row5;
18323 struct Row6;
18324
18325 struct Row8;
18326 struct Row9;
18327 struct Row10;"#};
18328
18329 // Modification hunks behave the same as the addition ones.
18330 assert_hunk_revert(
18331 indoc! {r#"struct Row;
18332 struct Row1;
18333 struct Row33;
18334 ˇ
18335 struct Row4;
18336 struct Row5;
18337 struct Row6;
18338 ˇ
18339 struct Row99;
18340 struct Row9;
18341 struct Row10;"#},
18342 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18343 indoc! {r#"struct Row;
18344 struct Row1;
18345 struct Row33;
18346 ˇ
18347 struct Row4;
18348 struct Row5;
18349 struct Row6;
18350 ˇ
18351 struct Row99;
18352 struct Row9;
18353 struct Row10;"#},
18354 base_text,
18355 &mut cx,
18356 );
18357 assert_hunk_revert(
18358 indoc! {r#"struct Row;
18359 struct Row1;
18360 struct Row33;
18361 «ˇ
18362 struct Row4;
18363 struct» Row5;
18364 «struct Row6;
18365 ˇ»
18366 struct Row99;
18367 struct Row9;
18368 struct Row10;"#},
18369 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18370 indoc! {r#"struct Row;
18371 struct Row1;
18372 struct Row33;
18373 «ˇ
18374 struct Row4;
18375 struct» Row5;
18376 «struct Row6;
18377 ˇ»
18378 struct Row99;
18379 struct Row9;
18380 struct Row10;"#},
18381 base_text,
18382 &mut cx,
18383 );
18384
18385 assert_hunk_revert(
18386 indoc! {r#"ˇstruct Row1.1;
18387 struct Row1;
18388 «ˇstr»uct Row22;
18389
18390 struct ˇRow44;
18391 struct Row5;
18392 struct «Rˇ»ow66;ˇ
18393
18394 «struˇ»ct Row88;
18395 struct Row9;
18396 struct Row1011;ˇ"#},
18397 vec![
18398 DiffHunkStatusKind::Modified,
18399 DiffHunkStatusKind::Modified,
18400 DiffHunkStatusKind::Modified,
18401 DiffHunkStatusKind::Modified,
18402 DiffHunkStatusKind::Modified,
18403 DiffHunkStatusKind::Modified,
18404 ],
18405 indoc! {r#"struct Row;
18406 ˇstruct Row1;
18407 struct Row2;
18408 ˇ
18409 struct Row4;
18410 ˇstruct Row5;
18411 struct Row6;
18412 ˇ
18413 struct Row8;
18414 ˇstruct Row9;
18415 struct Row10;ˇ"#},
18416 base_text,
18417 &mut cx,
18418 );
18419}
18420
18421#[gpui::test]
18422async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
18423 init_test(cx, |_| {});
18424 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18425 let base_text = indoc! {r#"
18426 one
18427
18428 two
18429 three
18430 "#};
18431
18432 cx.set_head_text(base_text);
18433 cx.set_state("\nˇ\n");
18434 cx.executor().run_until_parked();
18435 cx.update_editor(|editor, _window, cx| {
18436 editor.expand_selected_diff_hunks(cx);
18437 });
18438 cx.executor().run_until_parked();
18439 cx.update_editor(|editor, window, cx| {
18440 editor.backspace(&Default::default(), window, cx);
18441 });
18442 cx.run_until_parked();
18443 cx.assert_state_with_diff(
18444 indoc! {r#"
18445
18446 - two
18447 - threeˇ
18448 +
18449 "#}
18450 .to_string(),
18451 );
18452}
18453
18454#[gpui::test]
18455async fn test_deletion_reverts(cx: &mut TestAppContext) {
18456 init_test(cx, |_| {});
18457 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18458 let base_text = indoc! {r#"struct Row;
18459struct Row1;
18460struct Row2;
18461
18462struct Row4;
18463struct Row5;
18464struct Row6;
18465
18466struct Row8;
18467struct Row9;
18468struct Row10;"#};
18469
18470 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
18471 assert_hunk_revert(
18472 indoc! {r#"struct Row;
18473 struct Row2;
18474
18475 ˇstruct Row4;
18476 struct Row5;
18477 struct Row6;
18478 ˇ
18479 struct Row8;
18480 struct Row10;"#},
18481 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18482 indoc! {r#"struct Row;
18483 struct Row2;
18484
18485 ˇstruct Row4;
18486 struct Row5;
18487 struct Row6;
18488 ˇ
18489 struct Row8;
18490 struct Row10;"#},
18491 base_text,
18492 &mut cx,
18493 );
18494 assert_hunk_revert(
18495 indoc! {r#"struct Row;
18496 struct Row2;
18497
18498 «ˇstruct Row4;
18499 struct» Row5;
18500 «struct Row6;
18501 ˇ»
18502 struct Row8;
18503 struct Row10;"#},
18504 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18505 indoc! {r#"struct Row;
18506 struct Row2;
18507
18508 «ˇstruct Row4;
18509 struct» Row5;
18510 «struct Row6;
18511 ˇ»
18512 struct Row8;
18513 struct Row10;"#},
18514 base_text,
18515 &mut cx,
18516 );
18517
18518 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
18519 assert_hunk_revert(
18520 indoc! {r#"struct Row;
18521 ˇstruct Row2;
18522
18523 struct Row4;
18524 struct Row5;
18525 struct Row6;
18526
18527 struct Row8;ˇ
18528 struct Row10;"#},
18529 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18530 indoc! {r#"struct Row;
18531 struct Row1;
18532 ˇstruct Row2;
18533
18534 struct Row4;
18535 struct Row5;
18536 struct Row6;
18537
18538 struct Row8;ˇ
18539 struct Row9;
18540 struct Row10;"#},
18541 base_text,
18542 &mut cx,
18543 );
18544 assert_hunk_revert(
18545 indoc! {r#"struct Row;
18546 struct Row2«ˇ;
18547 struct Row4;
18548 struct» Row5;
18549 «struct Row6;
18550
18551 struct Row8;ˇ»
18552 struct Row10;"#},
18553 vec![
18554 DiffHunkStatusKind::Deleted,
18555 DiffHunkStatusKind::Deleted,
18556 DiffHunkStatusKind::Deleted,
18557 ],
18558 indoc! {r#"struct Row;
18559 struct Row1;
18560 struct Row2«ˇ;
18561
18562 struct Row4;
18563 struct» Row5;
18564 «struct Row6;
18565
18566 struct Row8;ˇ»
18567 struct Row9;
18568 struct Row10;"#},
18569 base_text,
18570 &mut cx,
18571 );
18572}
18573
18574#[gpui::test]
18575async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
18576 init_test(cx, |_| {});
18577
18578 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
18579 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
18580 let base_text_3 =
18581 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
18582
18583 let text_1 = edit_first_char_of_every_line(base_text_1);
18584 let text_2 = edit_first_char_of_every_line(base_text_2);
18585 let text_3 = edit_first_char_of_every_line(base_text_3);
18586
18587 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
18588 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
18589 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
18590
18591 let multibuffer = cx.new(|cx| {
18592 let mut multibuffer = MultiBuffer::new(ReadWrite);
18593 multibuffer.push_excerpts(
18594 buffer_1.clone(),
18595 [
18596 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18597 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18598 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18599 ],
18600 cx,
18601 );
18602 multibuffer.push_excerpts(
18603 buffer_2.clone(),
18604 [
18605 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18606 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18607 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18608 ],
18609 cx,
18610 );
18611 multibuffer.push_excerpts(
18612 buffer_3.clone(),
18613 [
18614 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18615 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18616 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18617 ],
18618 cx,
18619 );
18620 multibuffer
18621 });
18622
18623 let fs = FakeFs::new(cx.executor());
18624 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
18625 let (editor, cx) = cx
18626 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
18627 editor.update_in(cx, |editor, _window, cx| {
18628 for (buffer, diff_base) in [
18629 (buffer_1.clone(), base_text_1),
18630 (buffer_2.clone(), base_text_2),
18631 (buffer_3.clone(), base_text_3),
18632 ] {
18633 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
18634 editor
18635 .buffer
18636 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
18637 }
18638 });
18639 cx.executor().run_until_parked();
18640
18641 editor.update_in(cx, |editor, window, cx| {
18642 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}");
18643 editor.select_all(&SelectAll, window, cx);
18644 editor.git_restore(&Default::default(), window, cx);
18645 });
18646 cx.executor().run_until_parked();
18647
18648 // When all ranges are selected, all buffer hunks are reverted.
18649 editor.update(cx, |editor, cx| {
18650 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");
18651 });
18652 buffer_1.update(cx, |buffer, _| {
18653 assert_eq!(buffer.text(), base_text_1);
18654 });
18655 buffer_2.update(cx, |buffer, _| {
18656 assert_eq!(buffer.text(), base_text_2);
18657 });
18658 buffer_3.update(cx, |buffer, _| {
18659 assert_eq!(buffer.text(), base_text_3);
18660 });
18661
18662 editor.update_in(cx, |editor, window, cx| {
18663 editor.undo(&Default::default(), window, cx);
18664 });
18665
18666 editor.update_in(cx, |editor, window, cx| {
18667 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18668 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
18669 });
18670 editor.git_restore(&Default::default(), window, cx);
18671 });
18672
18673 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
18674 // but not affect buffer_2 and its related excerpts.
18675 editor.update(cx, |editor, cx| {
18676 assert_eq!(
18677 editor.text(cx),
18678 "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}"
18679 );
18680 });
18681 buffer_1.update(cx, |buffer, _| {
18682 assert_eq!(buffer.text(), base_text_1);
18683 });
18684 buffer_2.update(cx, |buffer, _| {
18685 assert_eq!(
18686 buffer.text(),
18687 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
18688 );
18689 });
18690 buffer_3.update(cx, |buffer, _| {
18691 assert_eq!(
18692 buffer.text(),
18693 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
18694 );
18695 });
18696
18697 fn edit_first_char_of_every_line(text: &str) -> String {
18698 text.split('\n')
18699 .map(|line| format!("X{}", &line[1..]))
18700 .collect::<Vec<_>>()
18701 .join("\n")
18702 }
18703}
18704
18705#[gpui::test]
18706async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
18707 init_test(cx, |_| {});
18708
18709 let cols = 4;
18710 let rows = 10;
18711 let sample_text_1 = sample_text(rows, cols, 'a');
18712 assert_eq!(
18713 sample_text_1,
18714 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
18715 );
18716 let sample_text_2 = sample_text(rows, cols, 'l');
18717 assert_eq!(
18718 sample_text_2,
18719 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
18720 );
18721 let sample_text_3 = sample_text(rows, cols, 'v');
18722 assert_eq!(
18723 sample_text_3,
18724 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
18725 );
18726
18727 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
18728 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
18729 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
18730
18731 let multi_buffer = cx.new(|cx| {
18732 let mut multibuffer = MultiBuffer::new(ReadWrite);
18733 multibuffer.push_excerpts(
18734 buffer_1.clone(),
18735 [
18736 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18737 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18738 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18739 ],
18740 cx,
18741 );
18742 multibuffer.push_excerpts(
18743 buffer_2.clone(),
18744 [
18745 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18746 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18747 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18748 ],
18749 cx,
18750 );
18751 multibuffer.push_excerpts(
18752 buffer_3.clone(),
18753 [
18754 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18755 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18756 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18757 ],
18758 cx,
18759 );
18760 multibuffer
18761 });
18762
18763 let fs = FakeFs::new(cx.executor());
18764 fs.insert_tree(
18765 "/a",
18766 json!({
18767 "main.rs": sample_text_1,
18768 "other.rs": sample_text_2,
18769 "lib.rs": sample_text_3,
18770 }),
18771 )
18772 .await;
18773 let project = Project::test(fs, ["/a".as_ref()], cx).await;
18774 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18775 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18776 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18777 Editor::new(
18778 EditorMode::full(),
18779 multi_buffer,
18780 Some(project.clone()),
18781 window,
18782 cx,
18783 )
18784 });
18785 let multibuffer_item_id = workspace
18786 .update(cx, |workspace, window, cx| {
18787 assert!(
18788 workspace.active_item(cx).is_none(),
18789 "active item should be None before the first item is added"
18790 );
18791 workspace.add_item_to_active_pane(
18792 Box::new(multi_buffer_editor.clone()),
18793 None,
18794 true,
18795 window,
18796 cx,
18797 );
18798 let active_item = workspace
18799 .active_item(cx)
18800 .expect("should have an active item after adding the multi buffer");
18801 assert_eq!(
18802 active_item.buffer_kind(cx),
18803 ItemBufferKind::Multibuffer,
18804 "A multi buffer was expected to active after adding"
18805 );
18806 active_item.item_id()
18807 })
18808 .unwrap();
18809 cx.executor().run_until_parked();
18810
18811 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18812 editor.change_selections(
18813 SelectionEffects::scroll(Autoscroll::Next),
18814 window,
18815 cx,
18816 |s| s.select_ranges(Some(1..2)),
18817 );
18818 editor.open_excerpts(&OpenExcerpts, window, cx);
18819 });
18820 cx.executor().run_until_parked();
18821 let first_item_id = workspace
18822 .update(cx, |workspace, window, cx| {
18823 let active_item = workspace
18824 .active_item(cx)
18825 .expect("should have an active item after navigating into the 1st buffer");
18826 let first_item_id = active_item.item_id();
18827 assert_ne!(
18828 first_item_id, multibuffer_item_id,
18829 "Should navigate into the 1st buffer and activate it"
18830 );
18831 assert_eq!(
18832 active_item.buffer_kind(cx),
18833 ItemBufferKind::Singleton,
18834 "New active item should be a singleton buffer"
18835 );
18836 assert_eq!(
18837 active_item
18838 .act_as::<Editor>(cx)
18839 .expect("should have navigated into an editor for the 1st buffer")
18840 .read(cx)
18841 .text(cx),
18842 sample_text_1
18843 );
18844
18845 workspace
18846 .go_back(workspace.active_pane().downgrade(), window, cx)
18847 .detach_and_log_err(cx);
18848
18849 first_item_id
18850 })
18851 .unwrap();
18852 cx.executor().run_until_parked();
18853 workspace
18854 .update(cx, |workspace, _, cx| {
18855 let active_item = workspace
18856 .active_item(cx)
18857 .expect("should have an active item after navigating back");
18858 assert_eq!(
18859 active_item.item_id(),
18860 multibuffer_item_id,
18861 "Should navigate back to the multi buffer"
18862 );
18863 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
18864 })
18865 .unwrap();
18866
18867 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18868 editor.change_selections(
18869 SelectionEffects::scroll(Autoscroll::Next),
18870 window,
18871 cx,
18872 |s| s.select_ranges(Some(39..40)),
18873 );
18874 editor.open_excerpts(&OpenExcerpts, window, cx);
18875 });
18876 cx.executor().run_until_parked();
18877 let second_item_id = workspace
18878 .update(cx, |workspace, window, cx| {
18879 let active_item = workspace
18880 .active_item(cx)
18881 .expect("should have an active item after navigating into the 2nd buffer");
18882 let second_item_id = active_item.item_id();
18883 assert_ne!(
18884 second_item_id, multibuffer_item_id,
18885 "Should navigate away from the multibuffer"
18886 );
18887 assert_ne!(
18888 second_item_id, first_item_id,
18889 "Should navigate into the 2nd buffer and activate it"
18890 );
18891 assert_eq!(
18892 active_item.buffer_kind(cx),
18893 ItemBufferKind::Singleton,
18894 "New active item should be a singleton buffer"
18895 );
18896 assert_eq!(
18897 active_item
18898 .act_as::<Editor>(cx)
18899 .expect("should have navigated into an editor")
18900 .read(cx)
18901 .text(cx),
18902 sample_text_2
18903 );
18904
18905 workspace
18906 .go_back(workspace.active_pane().downgrade(), window, cx)
18907 .detach_and_log_err(cx);
18908
18909 second_item_id
18910 })
18911 .unwrap();
18912 cx.executor().run_until_parked();
18913 workspace
18914 .update(cx, |workspace, _, cx| {
18915 let active_item = workspace
18916 .active_item(cx)
18917 .expect("should have an active item after navigating back from the 2nd buffer");
18918 assert_eq!(
18919 active_item.item_id(),
18920 multibuffer_item_id,
18921 "Should navigate back from the 2nd buffer to the multi buffer"
18922 );
18923 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
18924 })
18925 .unwrap();
18926
18927 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18928 editor.change_selections(
18929 SelectionEffects::scroll(Autoscroll::Next),
18930 window,
18931 cx,
18932 |s| s.select_ranges(Some(70..70)),
18933 );
18934 editor.open_excerpts(&OpenExcerpts, window, cx);
18935 });
18936 cx.executor().run_until_parked();
18937 workspace
18938 .update(cx, |workspace, window, cx| {
18939 let active_item = workspace
18940 .active_item(cx)
18941 .expect("should have an active item after navigating into the 3rd buffer");
18942 let third_item_id = active_item.item_id();
18943 assert_ne!(
18944 third_item_id, multibuffer_item_id,
18945 "Should navigate into the 3rd buffer and activate it"
18946 );
18947 assert_ne!(third_item_id, first_item_id);
18948 assert_ne!(third_item_id, second_item_id);
18949 assert_eq!(
18950 active_item.buffer_kind(cx),
18951 ItemBufferKind::Singleton,
18952 "New active item should be a singleton buffer"
18953 );
18954 assert_eq!(
18955 active_item
18956 .act_as::<Editor>(cx)
18957 .expect("should have navigated into an editor")
18958 .read(cx)
18959 .text(cx),
18960 sample_text_3
18961 );
18962
18963 workspace
18964 .go_back(workspace.active_pane().downgrade(), window, cx)
18965 .detach_and_log_err(cx);
18966 })
18967 .unwrap();
18968 cx.executor().run_until_parked();
18969 workspace
18970 .update(cx, |workspace, _, cx| {
18971 let active_item = workspace
18972 .active_item(cx)
18973 .expect("should have an active item after navigating back from the 3rd buffer");
18974 assert_eq!(
18975 active_item.item_id(),
18976 multibuffer_item_id,
18977 "Should navigate back from the 3rd buffer to the multi buffer"
18978 );
18979 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
18980 })
18981 .unwrap();
18982}
18983
18984#[gpui::test]
18985async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
18986 init_test(cx, |_| {});
18987
18988 let mut cx = EditorTestContext::new(cx).await;
18989
18990 let diff_base = r#"
18991 use some::mod;
18992
18993 const A: u32 = 42;
18994
18995 fn main() {
18996 println!("hello");
18997
18998 println!("world");
18999 }
19000 "#
19001 .unindent();
19002
19003 cx.set_state(
19004 &r#"
19005 use some::modified;
19006
19007 ˇ
19008 fn main() {
19009 println!("hello there");
19010
19011 println!("around the");
19012 println!("world");
19013 }
19014 "#
19015 .unindent(),
19016 );
19017
19018 cx.set_head_text(&diff_base);
19019 executor.run_until_parked();
19020
19021 cx.update_editor(|editor, window, cx| {
19022 editor.go_to_next_hunk(&GoToHunk, window, cx);
19023 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19024 });
19025 executor.run_until_parked();
19026 cx.assert_state_with_diff(
19027 r#"
19028 use some::modified;
19029
19030
19031 fn main() {
19032 - println!("hello");
19033 + ˇ println!("hello there");
19034
19035 println!("around the");
19036 println!("world");
19037 }
19038 "#
19039 .unindent(),
19040 );
19041
19042 cx.update_editor(|editor, window, cx| {
19043 for _ in 0..2 {
19044 editor.go_to_next_hunk(&GoToHunk, window, cx);
19045 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19046 }
19047 });
19048 executor.run_until_parked();
19049 cx.assert_state_with_diff(
19050 r#"
19051 - use some::mod;
19052 + ˇuse some::modified;
19053
19054
19055 fn main() {
19056 - println!("hello");
19057 + println!("hello there");
19058
19059 + println!("around the");
19060 println!("world");
19061 }
19062 "#
19063 .unindent(),
19064 );
19065
19066 cx.update_editor(|editor, window, cx| {
19067 editor.go_to_next_hunk(&GoToHunk, window, cx);
19068 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19069 });
19070 executor.run_until_parked();
19071 cx.assert_state_with_diff(
19072 r#"
19073 - use some::mod;
19074 + use some::modified;
19075
19076 - const A: u32 = 42;
19077 ˇ
19078 fn main() {
19079 - println!("hello");
19080 + println!("hello there");
19081
19082 + println!("around the");
19083 println!("world");
19084 }
19085 "#
19086 .unindent(),
19087 );
19088
19089 cx.update_editor(|editor, window, cx| {
19090 editor.cancel(&Cancel, window, cx);
19091 });
19092
19093 cx.assert_state_with_diff(
19094 r#"
19095 use some::modified;
19096
19097 ˇ
19098 fn main() {
19099 println!("hello there");
19100
19101 println!("around the");
19102 println!("world");
19103 }
19104 "#
19105 .unindent(),
19106 );
19107}
19108
19109#[gpui::test]
19110async fn test_diff_base_change_with_expanded_diff_hunks(
19111 executor: BackgroundExecutor,
19112 cx: &mut TestAppContext,
19113) {
19114 init_test(cx, |_| {});
19115
19116 let mut cx = EditorTestContext::new(cx).await;
19117
19118 let diff_base = r#"
19119 use some::mod1;
19120 use some::mod2;
19121
19122 const A: u32 = 42;
19123 const B: u32 = 42;
19124 const C: u32 = 42;
19125
19126 fn main() {
19127 println!("hello");
19128
19129 println!("world");
19130 }
19131 "#
19132 .unindent();
19133
19134 cx.set_state(
19135 &r#"
19136 use some::mod2;
19137
19138 const A: u32 = 42;
19139 const C: u32 = 42;
19140
19141 fn main(ˇ) {
19142 //println!("hello");
19143
19144 println!("world");
19145 //
19146 //
19147 }
19148 "#
19149 .unindent(),
19150 );
19151
19152 cx.set_head_text(&diff_base);
19153 executor.run_until_parked();
19154
19155 cx.update_editor(|editor, window, cx| {
19156 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19157 });
19158 executor.run_until_parked();
19159 cx.assert_state_with_diff(
19160 r#"
19161 - use some::mod1;
19162 use some::mod2;
19163
19164 const A: u32 = 42;
19165 - const B: u32 = 42;
19166 const C: u32 = 42;
19167
19168 fn main(ˇ) {
19169 - println!("hello");
19170 + //println!("hello");
19171
19172 println!("world");
19173 + //
19174 + //
19175 }
19176 "#
19177 .unindent(),
19178 );
19179
19180 cx.set_head_text("new diff base!");
19181 executor.run_until_parked();
19182 cx.assert_state_with_diff(
19183 r#"
19184 - new diff base!
19185 + use some::mod2;
19186 +
19187 + const A: u32 = 42;
19188 + const C: u32 = 42;
19189 +
19190 + fn main(ˇ) {
19191 + //println!("hello");
19192 +
19193 + println!("world");
19194 + //
19195 + //
19196 + }
19197 "#
19198 .unindent(),
19199 );
19200}
19201
19202#[gpui::test]
19203async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
19204 init_test(cx, |_| {});
19205
19206 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19207 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19208 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19209 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19210 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
19211 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
19212
19213 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
19214 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
19215 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
19216
19217 let multi_buffer = cx.new(|cx| {
19218 let mut multibuffer = MultiBuffer::new(ReadWrite);
19219 multibuffer.push_excerpts(
19220 buffer_1.clone(),
19221 [
19222 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19223 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19224 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19225 ],
19226 cx,
19227 );
19228 multibuffer.push_excerpts(
19229 buffer_2.clone(),
19230 [
19231 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19232 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19233 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19234 ],
19235 cx,
19236 );
19237 multibuffer.push_excerpts(
19238 buffer_3.clone(),
19239 [
19240 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19241 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19242 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19243 ],
19244 cx,
19245 );
19246 multibuffer
19247 });
19248
19249 let editor =
19250 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19251 editor
19252 .update(cx, |editor, _window, cx| {
19253 for (buffer, diff_base) in [
19254 (buffer_1.clone(), file_1_old),
19255 (buffer_2.clone(), file_2_old),
19256 (buffer_3.clone(), file_3_old),
19257 ] {
19258 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
19259 editor
19260 .buffer
19261 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
19262 }
19263 })
19264 .unwrap();
19265
19266 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19267 cx.run_until_parked();
19268
19269 cx.assert_editor_state(
19270 &"
19271 ˇaaa
19272 ccc
19273 ddd
19274
19275 ggg
19276 hhh
19277
19278
19279 lll
19280 mmm
19281 NNN
19282
19283 qqq
19284 rrr
19285
19286 uuu
19287 111
19288 222
19289 333
19290
19291 666
19292 777
19293
19294 000
19295 !!!"
19296 .unindent(),
19297 );
19298
19299 cx.update_editor(|editor, window, cx| {
19300 editor.select_all(&SelectAll, window, cx);
19301 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19302 });
19303 cx.executor().run_until_parked();
19304
19305 cx.assert_state_with_diff(
19306 "
19307 «aaa
19308 - bbb
19309 ccc
19310 ddd
19311
19312 ggg
19313 hhh
19314
19315
19316 lll
19317 mmm
19318 - nnn
19319 + NNN
19320
19321 qqq
19322 rrr
19323
19324 uuu
19325 111
19326 222
19327 333
19328
19329 + 666
19330 777
19331
19332 000
19333 !!!ˇ»"
19334 .unindent(),
19335 );
19336}
19337
19338#[gpui::test]
19339async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
19340 init_test(cx, |_| {});
19341
19342 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
19343 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
19344
19345 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
19346 let multi_buffer = cx.new(|cx| {
19347 let mut multibuffer = MultiBuffer::new(ReadWrite);
19348 multibuffer.push_excerpts(
19349 buffer.clone(),
19350 [
19351 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
19352 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
19353 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
19354 ],
19355 cx,
19356 );
19357 multibuffer
19358 });
19359
19360 let editor =
19361 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19362 editor
19363 .update(cx, |editor, _window, cx| {
19364 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
19365 editor
19366 .buffer
19367 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
19368 })
19369 .unwrap();
19370
19371 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19372 cx.run_until_parked();
19373
19374 cx.update_editor(|editor, window, cx| {
19375 editor.expand_all_diff_hunks(&Default::default(), window, cx)
19376 });
19377 cx.executor().run_until_parked();
19378
19379 // When the start of a hunk coincides with the start of its excerpt,
19380 // the hunk is expanded. When the start of a hunk is earlier than
19381 // the start of its excerpt, the hunk is not expanded.
19382 cx.assert_state_with_diff(
19383 "
19384 ˇaaa
19385 - bbb
19386 + BBB
19387
19388 - ddd
19389 - eee
19390 + DDD
19391 + EEE
19392 fff
19393
19394 iii
19395 "
19396 .unindent(),
19397 );
19398}
19399
19400#[gpui::test]
19401async fn test_edits_around_expanded_insertion_hunks(
19402 executor: BackgroundExecutor,
19403 cx: &mut TestAppContext,
19404) {
19405 init_test(cx, |_| {});
19406
19407 let mut cx = EditorTestContext::new(cx).await;
19408
19409 let diff_base = r#"
19410 use some::mod1;
19411 use some::mod2;
19412
19413 const A: u32 = 42;
19414
19415 fn main() {
19416 println!("hello");
19417
19418 println!("world");
19419 }
19420 "#
19421 .unindent();
19422 executor.run_until_parked();
19423 cx.set_state(
19424 &r#"
19425 use some::mod1;
19426 use some::mod2;
19427
19428 const A: u32 = 42;
19429 const B: u32 = 42;
19430 const C: u32 = 42;
19431 ˇ
19432
19433 fn main() {
19434 println!("hello");
19435
19436 println!("world");
19437 }
19438 "#
19439 .unindent(),
19440 );
19441
19442 cx.set_head_text(&diff_base);
19443 executor.run_until_parked();
19444
19445 cx.update_editor(|editor, window, cx| {
19446 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19447 });
19448 executor.run_until_parked();
19449
19450 cx.assert_state_with_diff(
19451 r#"
19452 use some::mod1;
19453 use some::mod2;
19454
19455 const A: u32 = 42;
19456 + const B: u32 = 42;
19457 + const C: u32 = 42;
19458 + ˇ
19459
19460 fn main() {
19461 println!("hello");
19462
19463 println!("world");
19464 }
19465 "#
19466 .unindent(),
19467 );
19468
19469 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
19470 executor.run_until_parked();
19471
19472 cx.assert_state_with_diff(
19473 r#"
19474 use some::mod1;
19475 use some::mod2;
19476
19477 const A: u32 = 42;
19478 + const B: u32 = 42;
19479 + const C: u32 = 42;
19480 + const D: u32 = 42;
19481 + ˇ
19482
19483 fn main() {
19484 println!("hello");
19485
19486 println!("world");
19487 }
19488 "#
19489 .unindent(),
19490 );
19491
19492 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
19493 executor.run_until_parked();
19494
19495 cx.assert_state_with_diff(
19496 r#"
19497 use some::mod1;
19498 use some::mod2;
19499
19500 const A: u32 = 42;
19501 + const B: u32 = 42;
19502 + const C: u32 = 42;
19503 + const D: u32 = 42;
19504 + const E: u32 = 42;
19505 + ˇ
19506
19507 fn main() {
19508 println!("hello");
19509
19510 println!("world");
19511 }
19512 "#
19513 .unindent(),
19514 );
19515
19516 cx.update_editor(|editor, window, cx| {
19517 editor.delete_line(&DeleteLine, window, cx);
19518 });
19519 executor.run_until_parked();
19520
19521 cx.assert_state_with_diff(
19522 r#"
19523 use some::mod1;
19524 use some::mod2;
19525
19526 const A: u32 = 42;
19527 + const B: u32 = 42;
19528 + const C: u32 = 42;
19529 + const D: u32 = 42;
19530 + const E: u32 = 42;
19531 ˇ
19532 fn main() {
19533 println!("hello");
19534
19535 println!("world");
19536 }
19537 "#
19538 .unindent(),
19539 );
19540
19541 cx.update_editor(|editor, window, cx| {
19542 editor.move_up(&MoveUp, window, cx);
19543 editor.delete_line(&DeleteLine, window, cx);
19544 editor.move_up(&MoveUp, window, cx);
19545 editor.delete_line(&DeleteLine, window, cx);
19546 editor.move_up(&MoveUp, window, cx);
19547 editor.delete_line(&DeleteLine, window, cx);
19548 });
19549 executor.run_until_parked();
19550 cx.assert_state_with_diff(
19551 r#"
19552 use some::mod1;
19553 use some::mod2;
19554
19555 const A: u32 = 42;
19556 + const B: u32 = 42;
19557 ˇ
19558 fn main() {
19559 println!("hello");
19560
19561 println!("world");
19562 }
19563 "#
19564 .unindent(),
19565 );
19566
19567 cx.update_editor(|editor, window, cx| {
19568 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
19569 editor.delete_line(&DeleteLine, window, cx);
19570 });
19571 executor.run_until_parked();
19572 cx.assert_state_with_diff(
19573 r#"
19574 ˇ
19575 fn main() {
19576 println!("hello");
19577
19578 println!("world");
19579 }
19580 "#
19581 .unindent(),
19582 );
19583}
19584
19585#[gpui::test]
19586async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
19587 init_test(cx, |_| {});
19588
19589 let mut cx = EditorTestContext::new(cx).await;
19590 cx.set_head_text(indoc! { "
19591 one
19592 two
19593 three
19594 four
19595 five
19596 "
19597 });
19598 cx.set_state(indoc! { "
19599 one
19600 ˇthree
19601 five
19602 "});
19603 cx.run_until_parked();
19604 cx.update_editor(|editor, window, cx| {
19605 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19606 });
19607 cx.assert_state_with_diff(
19608 indoc! { "
19609 one
19610 - two
19611 ˇthree
19612 - four
19613 five
19614 "}
19615 .to_string(),
19616 );
19617 cx.update_editor(|editor, window, cx| {
19618 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19619 });
19620
19621 cx.assert_state_with_diff(
19622 indoc! { "
19623 one
19624 ˇthree
19625 five
19626 "}
19627 .to_string(),
19628 );
19629
19630 cx.set_state(indoc! { "
19631 one
19632 ˇTWO
19633 three
19634 four
19635 five
19636 "});
19637 cx.run_until_parked();
19638 cx.update_editor(|editor, window, cx| {
19639 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19640 });
19641
19642 cx.assert_state_with_diff(
19643 indoc! { "
19644 one
19645 - two
19646 + ˇTWO
19647 three
19648 four
19649 five
19650 "}
19651 .to_string(),
19652 );
19653 cx.update_editor(|editor, window, cx| {
19654 editor.move_up(&Default::default(), window, cx);
19655 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19656 });
19657 cx.assert_state_with_diff(
19658 indoc! { "
19659 one
19660 ˇTWO
19661 three
19662 four
19663 five
19664 "}
19665 .to_string(),
19666 );
19667}
19668
19669#[gpui::test]
19670async fn test_edits_around_expanded_deletion_hunks(
19671 executor: BackgroundExecutor,
19672 cx: &mut TestAppContext,
19673) {
19674 init_test(cx, |_| {});
19675
19676 let mut cx = EditorTestContext::new(cx).await;
19677
19678 let diff_base = r#"
19679 use some::mod1;
19680 use some::mod2;
19681
19682 const A: u32 = 42;
19683 const B: u32 = 42;
19684 const C: u32 = 42;
19685
19686
19687 fn main() {
19688 println!("hello");
19689
19690 println!("world");
19691 }
19692 "#
19693 .unindent();
19694 executor.run_until_parked();
19695 cx.set_state(
19696 &r#"
19697 use some::mod1;
19698 use some::mod2;
19699
19700 ˇconst B: u32 = 42;
19701 const C: u32 = 42;
19702
19703
19704 fn main() {
19705 println!("hello");
19706
19707 println!("world");
19708 }
19709 "#
19710 .unindent(),
19711 );
19712
19713 cx.set_head_text(&diff_base);
19714 executor.run_until_parked();
19715
19716 cx.update_editor(|editor, window, cx| {
19717 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19718 });
19719 executor.run_until_parked();
19720
19721 cx.assert_state_with_diff(
19722 r#"
19723 use some::mod1;
19724 use some::mod2;
19725
19726 - const A: u32 = 42;
19727 ˇconst B: u32 = 42;
19728 const C: u32 = 42;
19729
19730
19731 fn main() {
19732 println!("hello");
19733
19734 println!("world");
19735 }
19736 "#
19737 .unindent(),
19738 );
19739
19740 cx.update_editor(|editor, window, cx| {
19741 editor.delete_line(&DeleteLine, window, cx);
19742 });
19743 executor.run_until_parked();
19744 cx.assert_state_with_diff(
19745 r#"
19746 use some::mod1;
19747 use some::mod2;
19748
19749 - const A: u32 = 42;
19750 - const B: u32 = 42;
19751 ˇconst C: u32 = 42;
19752
19753
19754 fn main() {
19755 println!("hello");
19756
19757 println!("world");
19758 }
19759 "#
19760 .unindent(),
19761 );
19762
19763 cx.update_editor(|editor, window, cx| {
19764 editor.delete_line(&DeleteLine, window, cx);
19765 });
19766 executor.run_until_parked();
19767 cx.assert_state_with_diff(
19768 r#"
19769 use some::mod1;
19770 use some::mod2;
19771
19772 - const A: u32 = 42;
19773 - const B: u32 = 42;
19774 - const C: u32 = 42;
19775 ˇ
19776
19777 fn main() {
19778 println!("hello");
19779
19780 println!("world");
19781 }
19782 "#
19783 .unindent(),
19784 );
19785
19786 cx.update_editor(|editor, window, cx| {
19787 editor.handle_input("replacement", window, cx);
19788 });
19789 executor.run_until_parked();
19790 cx.assert_state_with_diff(
19791 r#"
19792 use some::mod1;
19793 use some::mod2;
19794
19795 - const A: u32 = 42;
19796 - const B: u32 = 42;
19797 - const C: u32 = 42;
19798 -
19799 + replacementˇ
19800
19801 fn main() {
19802 println!("hello");
19803
19804 println!("world");
19805 }
19806 "#
19807 .unindent(),
19808 );
19809}
19810
19811#[gpui::test]
19812async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19813 init_test(cx, |_| {});
19814
19815 let mut cx = EditorTestContext::new(cx).await;
19816
19817 let base_text = r#"
19818 one
19819 two
19820 three
19821 four
19822 five
19823 "#
19824 .unindent();
19825 executor.run_until_parked();
19826 cx.set_state(
19827 &r#"
19828 one
19829 two
19830 fˇour
19831 five
19832 "#
19833 .unindent(),
19834 );
19835
19836 cx.set_head_text(&base_text);
19837 executor.run_until_parked();
19838
19839 cx.update_editor(|editor, window, cx| {
19840 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19841 });
19842 executor.run_until_parked();
19843
19844 cx.assert_state_with_diff(
19845 r#"
19846 one
19847 two
19848 - three
19849 fˇour
19850 five
19851 "#
19852 .unindent(),
19853 );
19854
19855 cx.update_editor(|editor, window, cx| {
19856 editor.backspace(&Backspace, window, cx);
19857 editor.backspace(&Backspace, window, cx);
19858 });
19859 executor.run_until_parked();
19860 cx.assert_state_with_diff(
19861 r#"
19862 one
19863 two
19864 - threeˇ
19865 - four
19866 + our
19867 five
19868 "#
19869 .unindent(),
19870 );
19871}
19872
19873#[gpui::test]
19874async fn test_edit_after_expanded_modification_hunk(
19875 executor: BackgroundExecutor,
19876 cx: &mut TestAppContext,
19877) {
19878 init_test(cx, |_| {});
19879
19880 let mut cx = EditorTestContext::new(cx).await;
19881
19882 let diff_base = r#"
19883 use some::mod1;
19884 use some::mod2;
19885
19886 const A: u32 = 42;
19887 const B: u32 = 42;
19888 const C: u32 = 42;
19889 const D: u32 = 42;
19890
19891
19892 fn main() {
19893 println!("hello");
19894
19895 println!("world");
19896 }"#
19897 .unindent();
19898
19899 cx.set_state(
19900 &r#"
19901 use some::mod1;
19902 use some::mod2;
19903
19904 const A: u32 = 42;
19905 const B: u32 = 42;
19906 const C: u32 = 43ˇ
19907 const D: u32 = 42;
19908
19909
19910 fn main() {
19911 println!("hello");
19912
19913 println!("world");
19914 }"#
19915 .unindent(),
19916 );
19917
19918 cx.set_head_text(&diff_base);
19919 executor.run_until_parked();
19920 cx.update_editor(|editor, window, cx| {
19921 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19922 });
19923 executor.run_until_parked();
19924
19925 cx.assert_state_with_diff(
19926 r#"
19927 use some::mod1;
19928 use some::mod2;
19929
19930 const A: u32 = 42;
19931 const B: u32 = 42;
19932 - const C: u32 = 42;
19933 + const C: u32 = 43ˇ
19934 const D: u32 = 42;
19935
19936
19937 fn main() {
19938 println!("hello");
19939
19940 println!("world");
19941 }"#
19942 .unindent(),
19943 );
19944
19945 cx.update_editor(|editor, window, cx| {
19946 editor.handle_input("\nnew_line\n", window, cx);
19947 });
19948 executor.run_until_parked();
19949
19950 cx.assert_state_with_diff(
19951 r#"
19952 use some::mod1;
19953 use some::mod2;
19954
19955 const A: u32 = 42;
19956 const B: u32 = 42;
19957 - const C: u32 = 42;
19958 + const C: u32 = 43
19959 + new_line
19960 + ˇ
19961 const D: u32 = 42;
19962
19963
19964 fn main() {
19965 println!("hello");
19966
19967 println!("world");
19968 }"#
19969 .unindent(),
19970 );
19971}
19972
19973#[gpui::test]
19974async fn test_stage_and_unstage_added_file_hunk(
19975 executor: BackgroundExecutor,
19976 cx: &mut TestAppContext,
19977) {
19978 init_test(cx, |_| {});
19979
19980 let mut cx = EditorTestContext::new(cx).await;
19981 cx.update_editor(|editor, _, cx| {
19982 editor.set_expand_all_diff_hunks(cx);
19983 });
19984
19985 let working_copy = r#"
19986 ˇfn main() {
19987 println!("hello, world!");
19988 }
19989 "#
19990 .unindent();
19991
19992 cx.set_state(&working_copy);
19993 executor.run_until_parked();
19994
19995 cx.assert_state_with_diff(
19996 r#"
19997 + ˇfn main() {
19998 + println!("hello, world!");
19999 + }
20000 "#
20001 .unindent(),
20002 );
20003 cx.assert_index_text(None);
20004
20005 cx.update_editor(|editor, window, cx| {
20006 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20007 });
20008 executor.run_until_parked();
20009 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
20010 cx.assert_state_with_diff(
20011 r#"
20012 + ˇfn main() {
20013 + println!("hello, world!");
20014 + }
20015 "#
20016 .unindent(),
20017 );
20018
20019 cx.update_editor(|editor, window, cx| {
20020 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20021 });
20022 executor.run_until_parked();
20023 cx.assert_index_text(None);
20024}
20025
20026async fn setup_indent_guides_editor(
20027 text: &str,
20028 cx: &mut TestAppContext,
20029) -> (BufferId, EditorTestContext) {
20030 init_test(cx, |_| {});
20031
20032 let mut cx = EditorTestContext::new(cx).await;
20033
20034 let buffer_id = cx.update_editor(|editor, window, cx| {
20035 editor.set_text(text, window, cx);
20036 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
20037
20038 buffer_ids[0]
20039 });
20040
20041 (buffer_id, cx)
20042}
20043
20044fn assert_indent_guides(
20045 range: Range<u32>,
20046 expected: Vec<IndentGuide>,
20047 active_indices: Option<Vec<usize>>,
20048 cx: &mut EditorTestContext,
20049) {
20050 let indent_guides = cx.update_editor(|editor, window, cx| {
20051 let snapshot = editor.snapshot(window, cx).display_snapshot;
20052 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
20053 editor,
20054 MultiBufferRow(range.start)..MultiBufferRow(range.end),
20055 true,
20056 &snapshot,
20057 cx,
20058 );
20059
20060 indent_guides.sort_by(|a, b| {
20061 a.depth.cmp(&b.depth).then(
20062 a.start_row
20063 .cmp(&b.start_row)
20064 .then(a.end_row.cmp(&b.end_row)),
20065 )
20066 });
20067 indent_guides
20068 });
20069
20070 if let Some(expected) = active_indices {
20071 let active_indices = cx.update_editor(|editor, window, cx| {
20072 let snapshot = editor.snapshot(window, cx).display_snapshot;
20073 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
20074 });
20075
20076 assert_eq!(
20077 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
20078 expected,
20079 "Active indent guide indices do not match"
20080 );
20081 }
20082
20083 assert_eq!(indent_guides, expected, "Indent guides do not match");
20084}
20085
20086fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
20087 IndentGuide {
20088 buffer_id,
20089 start_row: MultiBufferRow(start_row),
20090 end_row: MultiBufferRow(end_row),
20091 depth,
20092 tab_size: 4,
20093 settings: IndentGuideSettings {
20094 enabled: true,
20095 line_width: 1,
20096 active_line_width: 1,
20097 coloring: IndentGuideColoring::default(),
20098 background_coloring: IndentGuideBackgroundColoring::default(),
20099 },
20100 }
20101}
20102
20103#[gpui::test]
20104async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
20105 let (buffer_id, mut cx) = setup_indent_guides_editor(
20106 &"
20107 fn main() {
20108 let a = 1;
20109 }"
20110 .unindent(),
20111 cx,
20112 )
20113 .await;
20114
20115 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20116}
20117
20118#[gpui::test]
20119async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
20120 let (buffer_id, mut cx) = setup_indent_guides_editor(
20121 &"
20122 fn main() {
20123 let a = 1;
20124 let b = 2;
20125 }"
20126 .unindent(),
20127 cx,
20128 )
20129 .await;
20130
20131 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
20132}
20133
20134#[gpui::test]
20135async fn test_indent_guide_nested(cx: &mut TestAppContext) {
20136 let (buffer_id, mut cx) = setup_indent_guides_editor(
20137 &"
20138 fn main() {
20139 let a = 1;
20140 if a == 3 {
20141 let b = 2;
20142 } else {
20143 let c = 3;
20144 }
20145 }"
20146 .unindent(),
20147 cx,
20148 )
20149 .await;
20150
20151 assert_indent_guides(
20152 0..8,
20153 vec![
20154 indent_guide(buffer_id, 1, 6, 0),
20155 indent_guide(buffer_id, 3, 3, 1),
20156 indent_guide(buffer_id, 5, 5, 1),
20157 ],
20158 None,
20159 &mut cx,
20160 );
20161}
20162
20163#[gpui::test]
20164async fn test_indent_guide_tab(cx: &mut TestAppContext) {
20165 let (buffer_id, mut cx) = setup_indent_guides_editor(
20166 &"
20167 fn main() {
20168 let a = 1;
20169 let b = 2;
20170 let c = 3;
20171 }"
20172 .unindent(),
20173 cx,
20174 )
20175 .await;
20176
20177 assert_indent_guides(
20178 0..5,
20179 vec![
20180 indent_guide(buffer_id, 1, 3, 0),
20181 indent_guide(buffer_id, 2, 2, 1),
20182 ],
20183 None,
20184 &mut cx,
20185 );
20186}
20187
20188#[gpui::test]
20189async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
20190 let (buffer_id, mut cx) = setup_indent_guides_editor(
20191 &"
20192 fn main() {
20193 let a = 1;
20194
20195 let c = 3;
20196 }"
20197 .unindent(),
20198 cx,
20199 )
20200 .await;
20201
20202 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
20203}
20204
20205#[gpui::test]
20206async fn test_indent_guide_complex(cx: &mut TestAppContext) {
20207 let (buffer_id, mut cx) = setup_indent_guides_editor(
20208 &"
20209 fn main() {
20210 let a = 1;
20211
20212 let c = 3;
20213
20214 if a == 3 {
20215 let b = 2;
20216 } else {
20217 let c = 3;
20218 }
20219 }"
20220 .unindent(),
20221 cx,
20222 )
20223 .await;
20224
20225 assert_indent_guides(
20226 0..11,
20227 vec![
20228 indent_guide(buffer_id, 1, 9, 0),
20229 indent_guide(buffer_id, 6, 6, 1),
20230 indent_guide(buffer_id, 8, 8, 1),
20231 ],
20232 None,
20233 &mut cx,
20234 );
20235}
20236
20237#[gpui::test]
20238async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
20239 let (buffer_id, mut cx) = setup_indent_guides_editor(
20240 &"
20241 fn main() {
20242 let a = 1;
20243
20244 let c = 3;
20245
20246 if a == 3 {
20247 let b = 2;
20248 } else {
20249 let c = 3;
20250 }
20251 }"
20252 .unindent(),
20253 cx,
20254 )
20255 .await;
20256
20257 assert_indent_guides(
20258 1..11,
20259 vec![
20260 indent_guide(buffer_id, 1, 9, 0),
20261 indent_guide(buffer_id, 6, 6, 1),
20262 indent_guide(buffer_id, 8, 8, 1),
20263 ],
20264 None,
20265 &mut cx,
20266 );
20267}
20268
20269#[gpui::test]
20270async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
20271 let (buffer_id, mut cx) = setup_indent_guides_editor(
20272 &"
20273 fn main() {
20274 let a = 1;
20275
20276 let c = 3;
20277
20278 if a == 3 {
20279 let b = 2;
20280 } else {
20281 let c = 3;
20282 }
20283 }"
20284 .unindent(),
20285 cx,
20286 )
20287 .await;
20288
20289 assert_indent_guides(
20290 1..10,
20291 vec![
20292 indent_guide(buffer_id, 1, 9, 0),
20293 indent_guide(buffer_id, 6, 6, 1),
20294 indent_guide(buffer_id, 8, 8, 1),
20295 ],
20296 None,
20297 &mut cx,
20298 );
20299}
20300
20301#[gpui::test]
20302async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
20303 let (buffer_id, mut cx) = setup_indent_guides_editor(
20304 &"
20305 fn main() {
20306 if a {
20307 b(
20308 c,
20309 d,
20310 )
20311 } else {
20312 e(
20313 f
20314 )
20315 }
20316 }"
20317 .unindent(),
20318 cx,
20319 )
20320 .await;
20321
20322 assert_indent_guides(
20323 0..11,
20324 vec![
20325 indent_guide(buffer_id, 1, 10, 0),
20326 indent_guide(buffer_id, 2, 5, 1),
20327 indent_guide(buffer_id, 7, 9, 1),
20328 indent_guide(buffer_id, 3, 4, 2),
20329 indent_guide(buffer_id, 8, 8, 2),
20330 ],
20331 None,
20332 &mut cx,
20333 );
20334
20335 cx.update_editor(|editor, window, cx| {
20336 editor.fold_at(MultiBufferRow(2), window, cx);
20337 assert_eq!(
20338 editor.display_text(cx),
20339 "
20340 fn main() {
20341 if a {
20342 b(⋯
20343 )
20344 } else {
20345 e(
20346 f
20347 )
20348 }
20349 }"
20350 .unindent()
20351 );
20352 });
20353
20354 assert_indent_guides(
20355 0..11,
20356 vec![
20357 indent_guide(buffer_id, 1, 10, 0),
20358 indent_guide(buffer_id, 2, 5, 1),
20359 indent_guide(buffer_id, 7, 9, 1),
20360 indent_guide(buffer_id, 8, 8, 2),
20361 ],
20362 None,
20363 &mut cx,
20364 );
20365}
20366
20367#[gpui::test]
20368async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
20369 let (buffer_id, mut cx) = setup_indent_guides_editor(
20370 &"
20371 block1
20372 block2
20373 block3
20374 block4
20375 block2
20376 block1
20377 block1"
20378 .unindent(),
20379 cx,
20380 )
20381 .await;
20382
20383 assert_indent_guides(
20384 1..10,
20385 vec![
20386 indent_guide(buffer_id, 1, 4, 0),
20387 indent_guide(buffer_id, 2, 3, 1),
20388 indent_guide(buffer_id, 3, 3, 2),
20389 ],
20390 None,
20391 &mut cx,
20392 );
20393}
20394
20395#[gpui::test]
20396async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
20397 let (buffer_id, mut cx) = setup_indent_guides_editor(
20398 &"
20399 block1
20400 block2
20401 block3
20402
20403 block1
20404 block1"
20405 .unindent(),
20406 cx,
20407 )
20408 .await;
20409
20410 assert_indent_guides(
20411 0..6,
20412 vec![
20413 indent_guide(buffer_id, 1, 2, 0),
20414 indent_guide(buffer_id, 2, 2, 1),
20415 ],
20416 None,
20417 &mut cx,
20418 );
20419}
20420
20421#[gpui::test]
20422async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
20423 let (buffer_id, mut cx) = setup_indent_guides_editor(
20424 &"
20425 function component() {
20426 \treturn (
20427 \t\t\t
20428 \t\t<div>
20429 \t\t\t<abc></abc>
20430 \t\t</div>
20431 \t)
20432 }"
20433 .unindent(),
20434 cx,
20435 )
20436 .await;
20437
20438 assert_indent_guides(
20439 0..8,
20440 vec![
20441 indent_guide(buffer_id, 1, 6, 0),
20442 indent_guide(buffer_id, 2, 5, 1),
20443 indent_guide(buffer_id, 4, 4, 2),
20444 ],
20445 None,
20446 &mut cx,
20447 );
20448}
20449
20450#[gpui::test]
20451async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
20452 let (buffer_id, mut cx) = setup_indent_guides_editor(
20453 &"
20454 function component() {
20455 \treturn (
20456 \t
20457 \t\t<div>
20458 \t\t\t<abc></abc>
20459 \t\t</div>
20460 \t)
20461 }"
20462 .unindent(),
20463 cx,
20464 )
20465 .await;
20466
20467 assert_indent_guides(
20468 0..8,
20469 vec![
20470 indent_guide(buffer_id, 1, 6, 0),
20471 indent_guide(buffer_id, 2, 5, 1),
20472 indent_guide(buffer_id, 4, 4, 2),
20473 ],
20474 None,
20475 &mut cx,
20476 );
20477}
20478
20479#[gpui::test]
20480async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
20481 let (buffer_id, mut cx) = setup_indent_guides_editor(
20482 &"
20483 block1
20484
20485
20486
20487 block2
20488 "
20489 .unindent(),
20490 cx,
20491 )
20492 .await;
20493
20494 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20495}
20496
20497#[gpui::test]
20498async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
20499 let (buffer_id, mut cx) = setup_indent_guides_editor(
20500 &"
20501 def a:
20502 \tb = 3
20503 \tif True:
20504 \t\tc = 4
20505 \t\td = 5
20506 \tprint(b)
20507 "
20508 .unindent(),
20509 cx,
20510 )
20511 .await;
20512
20513 assert_indent_guides(
20514 0..6,
20515 vec![
20516 indent_guide(buffer_id, 1, 5, 0),
20517 indent_guide(buffer_id, 3, 4, 1),
20518 ],
20519 None,
20520 &mut cx,
20521 );
20522}
20523
20524#[gpui::test]
20525async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
20526 let (buffer_id, mut cx) = setup_indent_guides_editor(
20527 &"
20528 fn main() {
20529 let a = 1;
20530 }"
20531 .unindent(),
20532 cx,
20533 )
20534 .await;
20535
20536 cx.update_editor(|editor, window, cx| {
20537 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20538 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20539 });
20540 });
20541
20542 assert_indent_guides(
20543 0..3,
20544 vec![indent_guide(buffer_id, 1, 1, 0)],
20545 Some(vec![0]),
20546 &mut cx,
20547 );
20548}
20549
20550#[gpui::test]
20551async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
20552 let (buffer_id, mut cx) = setup_indent_guides_editor(
20553 &"
20554 fn main() {
20555 if 1 == 2 {
20556 let a = 1;
20557 }
20558 }"
20559 .unindent(),
20560 cx,
20561 )
20562 .await;
20563
20564 cx.update_editor(|editor, window, cx| {
20565 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20566 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20567 });
20568 });
20569
20570 assert_indent_guides(
20571 0..4,
20572 vec![
20573 indent_guide(buffer_id, 1, 3, 0),
20574 indent_guide(buffer_id, 2, 2, 1),
20575 ],
20576 Some(vec![1]),
20577 &mut cx,
20578 );
20579
20580 cx.update_editor(|editor, window, cx| {
20581 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20582 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20583 });
20584 });
20585
20586 assert_indent_guides(
20587 0..4,
20588 vec![
20589 indent_guide(buffer_id, 1, 3, 0),
20590 indent_guide(buffer_id, 2, 2, 1),
20591 ],
20592 Some(vec![1]),
20593 &mut cx,
20594 );
20595
20596 cx.update_editor(|editor, window, cx| {
20597 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20598 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
20599 });
20600 });
20601
20602 assert_indent_guides(
20603 0..4,
20604 vec![
20605 indent_guide(buffer_id, 1, 3, 0),
20606 indent_guide(buffer_id, 2, 2, 1),
20607 ],
20608 Some(vec![0]),
20609 &mut cx,
20610 );
20611}
20612
20613#[gpui::test]
20614async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
20615 let (buffer_id, mut cx) = setup_indent_guides_editor(
20616 &"
20617 fn main() {
20618 let a = 1;
20619
20620 let b = 2;
20621 }"
20622 .unindent(),
20623 cx,
20624 )
20625 .await;
20626
20627 cx.update_editor(|editor, window, cx| {
20628 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20629 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20630 });
20631 });
20632
20633 assert_indent_guides(
20634 0..5,
20635 vec![indent_guide(buffer_id, 1, 3, 0)],
20636 Some(vec![0]),
20637 &mut cx,
20638 );
20639}
20640
20641#[gpui::test]
20642async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
20643 let (buffer_id, mut cx) = setup_indent_guides_editor(
20644 &"
20645 def m:
20646 a = 1
20647 pass"
20648 .unindent(),
20649 cx,
20650 )
20651 .await;
20652
20653 cx.update_editor(|editor, window, cx| {
20654 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20655 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20656 });
20657 });
20658
20659 assert_indent_guides(
20660 0..3,
20661 vec![indent_guide(buffer_id, 1, 2, 0)],
20662 Some(vec![0]),
20663 &mut cx,
20664 );
20665}
20666
20667#[gpui::test]
20668async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
20669 init_test(cx, |_| {});
20670 let mut cx = EditorTestContext::new(cx).await;
20671 let text = indoc! {
20672 "
20673 impl A {
20674 fn b() {
20675 0;
20676 3;
20677 5;
20678 6;
20679 7;
20680 }
20681 }
20682 "
20683 };
20684 let base_text = indoc! {
20685 "
20686 impl A {
20687 fn b() {
20688 0;
20689 1;
20690 2;
20691 3;
20692 4;
20693 }
20694 fn c() {
20695 5;
20696 6;
20697 7;
20698 }
20699 }
20700 "
20701 };
20702
20703 cx.update_editor(|editor, window, cx| {
20704 editor.set_text(text, window, cx);
20705
20706 editor.buffer().update(cx, |multibuffer, cx| {
20707 let buffer = multibuffer.as_singleton().unwrap();
20708 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
20709
20710 multibuffer.set_all_diff_hunks_expanded(cx);
20711 multibuffer.add_diff(diff, cx);
20712
20713 buffer.read(cx).remote_id()
20714 })
20715 });
20716 cx.run_until_parked();
20717
20718 cx.assert_state_with_diff(
20719 indoc! { "
20720 impl A {
20721 fn b() {
20722 0;
20723 - 1;
20724 - 2;
20725 3;
20726 - 4;
20727 - }
20728 - fn c() {
20729 5;
20730 6;
20731 7;
20732 }
20733 }
20734 ˇ"
20735 }
20736 .to_string(),
20737 );
20738
20739 let mut actual_guides = cx.update_editor(|editor, window, cx| {
20740 editor
20741 .snapshot(window, cx)
20742 .buffer_snapshot()
20743 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
20744 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
20745 .collect::<Vec<_>>()
20746 });
20747 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
20748 assert_eq!(
20749 actual_guides,
20750 vec![
20751 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
20752 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
20753 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
20754 ]
20755 );
20756}
20757
20758#[gpui::test]
20759async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
20760 init_test(cx, |_| {});
20761 let mut cx = EditorTestContext::new(cx).await;
20762
20763 let diff_base = r#"
20764 a
20765 b
20766 c
20767 "#
20768 .unindent();
20769
20770 cx.set_state(
20771 &r#"
20772 ˇA
20773 b
20774 C
20775 "#
20776 .unindent(),
20777 );
20778 cx.set_head_text(&diff_base);
20779 cx.update_editor(|editor, window, cx| {
20780 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20781 });
20782 executor.run_until_parked();
20783
20784 let both_hunks_expanded = r#"
20785 - a
20786 + ˇA
20787 b
20788 - c
20789 + C
20790 "#
20791 .unindent();
20792
20793 cx.assert_state_with_diff(both_hunks_expanded.clone());
20794
20795 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20796 let snapshot = editor.snapshot(window, cx);
20797 let hunks = editor
20798 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
20799 .collect::<Vec<_>>();
20800 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20801 let buffer_id = hunks[0].buffer_id;
20802 hunks
20803 .into_iter()
20804 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20805 .collect::<Vec<_>>()
20806 });
20807 assert_eq!(hunk_ranges.len(), 2);
20808
20809 cx.update_editor(|editor, _, cx| {
20810 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20811 });
20812 executor.run_until_parked();
20813
20814 let second_hunk_expanded = r#"
20815 ˇA
20816 b
20817 - c
20818 + C
20819 "#
20820 .unindent();
20821
20822 cx.assert_state_with_diff(second_hunk_expanded);
20823
20824 cx.update_editor(|editor, _, cx| {
20825 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20826 });
20827 executor.run_until_parked();
20828
20829 cx.assert_state_with_diff(both_hunks_expanded.clone());
20830
20831 cx.update_editor(|editor, _, cx| {
20832 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20833 });
20834 executor.run_until_parked();
20835
20836 let first_hunk_expanded = r#"
20837 - a
20838 + ˇA
20839 b
20840 C
20841 "#
20842 .unindent();
20843
20844 cx.assert_state_with_diff(first_hunk_expanded);
20845
20846 cx.update_editor(|editor, _, cx| {
20847 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20848 });
20849 executor.run_until_parked();
20850
20851 cx.assert_state_with_diff(both_hunks_expanded);
20852
20853 cx.set_state(
20854 &r#"
20855 ˇA
20856 b
20857 "#
20858 .unindent(),
20859 );
20860 cx.run_until_parked();
20861
20862 // TODO this cursor position seems bad
20863 cx.assert_state_with_diff(
20864 r#"
20865 - ˇa
20866 + A
20867 b
20868 "#
20869 .unindent(),
20870 );
20871
20872 cx.update_editor(|editor, window, cx| {
20873 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20874 });
20875
20876 cx.assert_state_with_diff(
20877 r#"
20878 - ˇa
20879 + A
20880 b
20881 - c
20882 "#
20883 .unindent(),
20884 );
20885
20886 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20887 let snapshot = editor.snapshot(window, cx);
20888 let hunks = editor
20889 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
20890 .collect::<Vec<_>>();
20891 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20892 let buffer_id = hunks[0].buffer_id;
20893 hunks
20894 .into_iter()
20895 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20896 .collect::<Vec<_>>()
20897 });
20898 assert_eq!(hunk_ranges.len(), 2);
20899
20900 cx.update_editor(|editor, _, cx| {
20901 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20902 });
20903 executor.run_until_parked();
20904
20905 cx.assert_state_with_diff(
20906 r#"
20907 - ˇa
20908 + A
20909 b
20910 "#
20911 .unindent(),
20912 );
20913}
20914
20915#[gpui::test]
20916async fn test_toggle_deletion_hunk_at_start_of_file(
20917 executor: BackgroundExecutor,
20918 cx: &mut TestAppContext,
20919) {
20920 init_test(cx, |_| {});
20921 let mut cx = EditorTestContext::new(cx).await;
20922
20923 let diff_base = r#"
20924 a
20925 b
20926 c
20927 "#
20928 .unindent();
20929
20930 cx.set_state(
20931 &r#"
20932 ˇb
20933 c
20934 "#
20935 .unindent(),
20936 );
20937 cx.set_head_text(&diff_base);
20938 cx.update_editor(|editor, window, cx| {
20939 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20940 });
20941 executor.run_until_parked();
20942
20943 let hunk_expanded = r#"
20944 - a
20945 ˇb
20946 c
20947 "#
20948 .unindent();
20949
20950 cx.assert_state_with_diff(hunk_expanded.clone());
20951
20952 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20953 let snapshot = editor.snapshot(window, cx);
20954 let hunks = editor
20955 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
20956 .collect::<Vec<_>>();
20957 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20958 let buffer_id = hunks[0].buffer_id;
20959 hunks
20960 .into_iter()
20961 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20962 .collect::<Vec<_>>()
20963 });
20964 assert_eq!(hunk_ranges.len(), 1);
20965
20966 cx.update_editor(|editor, _, cx| {
20967 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20968 });
20969 executor.run_until_parked();
20970
20971 let hunk_collapsed = r#"
20972 ˇb
20973 c
20974 "#
20975 .unindent();
20976
20977 cx.assert_state_with_diff(hunk_collapsed);
20978
20979 cx.update_editor(|editor, _, cx| {
20980 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20981 });
20982 executor.run_until_parked();
20983
20984 cx.assert_state_with_diff(hunk_expanded);
20985}
20986
20987#[gpui::test]
20988async fn test_display_diff_hunks(cx: &mut TestAppContext) {
20989 init_test(cx, |_| {});
20990
20991 let fs = FakeFs::new(cx.executor());
20992 fs.insert_tree(
20993 path!("/test"),
20994 json!({
20995 ".git": {},
20996 "file-1": "ONE\n",
20997 "file-2": "TWO\n",
20998 "file-3": "THREE\n",
20999 }),
21000 )
21001 .await;
21002
21003 fs.set_head_for_repo(
21004 path!("/test/.git").as_ref(),
21005 &[
21006 ("file-1", "one\n".into()),
21007 ("file-2", "two\n".into()),
21008 ("file-3", "three\n".into()),
21009 ],
21010 "deadbeef",
21011 );
21012
21013 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
21014 let mut buffers = vec![];
21015 for i in 1..=3 {
21016 let buffer = project
21017 .update(cx, |project, cx| {
21018 let path = format!(path!("/test/file-{}"), i);
21019 project.open_local_buffer(path, cx)
21020 })
21021 .await
21022 .unwrap();
21023 buffers.push(buffer);
21024 }
21025
21026 let multibuffer = cx.new(|cx| {
21027 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
21028 multibuffer.set_all_diff_hunks_expanded(cx);
21029 for buffer in &buffers {
21030 let snapshot = buffer.read(cx).snapshot();
21031 multibuffer.set_excerpts_for_path(
21032 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
21033 buffer.clone(),
21034 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
21035 2,
21036 cx,
21037 );
21038 }
21039 multibuffer
21040 });
21041
21042 let editor = cx.add_window(|window, cx| {
21043 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
21044 });
21045 cx.run_until_parked();
21046
21047 let snapshot = editor
21048 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21049 .unwrap();
21050 let hunks = snapshot
21051 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
21052 .map(|hunk| match hunk {
21053 DisplayDiffHunk::Unfolded {
21054 display_row_range, ..
21055 } => display_row_range,
21056 DisplayDiffHunk::Folded { .. } => unreachable!(),
21057 })
21058 .collect::<Vec<_>>();
21059 assert_eq!(
21060 hunks,
21061 [
21062 DisplayRow(2)..DisplayRow(4),
21063 DisplayRow(7)..DisplayRow(9),
21064 DisplayRow(12)..DisplayRow(14),
21065 ]
21066 );
21067}
21068
21069#[gpui::test]
21070async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
21071 init_test(cx, |_| {});
21072
21073 let mut cx = EditorTestContext::new(cx).await;
21074 cx.set_head_text(indoc! { "
21075 one
21076 two
21077 three
21078 four
21079 five
21080 "
21081 });
21082 cx.set_index_text(indoc! { "
21083 one
21084 two
21085 three
21086 four
21087 five
21088 "
21089 });
21090 cx.set_state(indoc! {"
21091 one
21092 TWO
21093 ˇTHREE
21094 FOUR
21095 five
21096 "});
21097 cx.run_until_parked();
21098 cx.update_editor(|editor, window, cx| {
21099 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21100 });
21101 cx.run_until_parked();
21102 cx.assert_index_text(Some(indoc! {"
21103 one
21104 TWO
21105 THREE
21106 FOUR
21107 five
21108 "}));
21109 cx.set_state(indoc! { "
21110 one
21111 TWO
21112 ˇTHREE-HUNDRED
21113 FOUR
21114 five
21115 "});
21116 cx.run_until_parked();
21117 cx.update_editor(|editor, window, cx| {
21118 let snapshot = editor.snapshot(window, cx);
21119 let hunks = editor
21120 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21121 .collect::<Vec<_>>();
21122 assert_eq!(hunks.len(), 1);
21123 assert_eq!(
21124 hunks[0].status(),
21125 DiffHunkStatus {
21126 kind: DiffHunkStatusKind::Modified,
21127 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
21128 }
21129 );
21130
21131 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21132 });
21133 cx.run_until_parked();
21134 cx.assert_index_text(Some(indoc! {"
21135 one
21136 TWO
21137 THREE-HUNDRED
21138 FOUR
21139 five
21140 "}));
21141}
21142
21143#[gpui::test]
21144fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
21145 init_test(cx, |_| {});
21146
21147 let editor = cx.add_window(|window, cx| {
21148 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
21149 build_editor(buffer, window, cx)
21150 });
21151
21152 let render_args = Arc::new(Mutex::new(None));
21153 let snapshot = editor
21154 .update(cx, |editor, window, cx| {
21155 let snapshot = editor.buffer().read(cx).snapshot(cx);
21156 let range =
21157 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
21158
21159 struct RenderArgs {
21160 row: MultiBufferRow,
21161 folded: bool,
21162 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
21163 }
21164
21165 let crease = Crease::inline(
21166 range,
21167 FoldPlaceholder::test(),
21168 {
21169 let toggle_callback = render_args.clone();
21170 move |row, folded, callback, _window, _cx| {
21171 *toggle_callback.lock() = Some(RenderArgs {
21172 row,
21173 folded,
21174 callback,
21175 });
21176 div()
21177 }
21178 },
21179 |_row, _folded, _window, _cx| div(),
21180 );
21181
21182 editor.insert_creases(Some(crease), cx);
21183 let snapshot = editor.snapshot(window, cx);
21184 let _div =
21185 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
21186 snapshot
21187 })
21188 .unwrap();
21189
21190 let render_args = render_args.lock().take().unwrap();
21191 assert_eq!(render_args.row, MultiBufferRow(1));
21192 assert!(!render_args.folded);
21193 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21194
21195 cx.update_window(*editor, |_, window, cx| {
21196 (render_args.callback)(true, window, cx)
21197 })
21198 .unwrap();
21199 let snapshot = editor
21200 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21201 .unwrap();
21202 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
21203
21204 cx.update_window(*editor, |_, window, cx| {
21205 (render_args.callback)(false, window, cx)
21206 })
21207 .unwrap();
21208 let snapshot = editor
21209 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21210 .unwrap();
21211 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21212}
21213
21214#[gpui::test]
21215async fn test_input_text(cx: &mut TestAppContext) {
21216 init_test(cx, |_| {});
21217 let mut cx = EditorTestContext::new(cx).await;
21218
21219 cx.set_state(
21220 &r#"ˇone
21221 two
21222
21223 three
21224 fourˇ
21225 five
21226
21227 siˇx"#
21228 .unindent(),
21229 );
21230
21231 cx.dispatch_action(HandleInput(String::new()));
21232 cx.assert_editor_state(
21233 &r#"ˇone
21234 two
21235
21236 three
21237 fourˇ
21238 five
21239
21240 siˇx"#
21241 .unindent(),
21242 );
21243
21244 cx.dispatch_action(HandleInput("AAAA".to_string()));
21245 cx.assert_editor_state(
21246 &r#"AAAAˇone
21247 two
21248
21249 three
21250 fourAAAAˇ
21251 five
21252
21253 siAAAAˇx"#
21254 .unindent(),
21255 );
21256}
21257
21258#[gpui::test]
21259async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
21260 init_test(cx, |_| {});
21261
21262 let mut cx = EditorTestContext::new(cx).await;
21263 cx.set_state(
21264 r#"let foo = 1;
21265let foo = 2;
21266let foo = 3;
21267let fooˇ = 4;
21268let foo = 5;
21269let foo = 6;
21270let foo = 7;
21271let foo = 8;
21272let foo = 9;
21273let foo = 10;
21274let foo = 11;
21275let foo = 12;
21276let foo = 13;
21277let foo = 14;
21278let foo = 15;"#,
21279 );
21280
21281 cx.update_editor(|e, window, cx| {
21282 assert_eq!(
21283 e.next_scroll_position,
21284 NextScrollCursorCenterTopBottom::Center,
21285 "Default next scroll direction is center",
21286 );
21287
21288 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21289 assert_eq!(
21290 e.next_scroll_position,
21291 NextScrollCursorCenterTopBottom::Top,
21292 "After center, next scroll direction should be top",
21293 );
21294
21295 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21296 assert_eq!(
21297 e.next_scroll_position,
21298 NextScrollCursorCenterTopBottom::Bottom,
21299 "After top, next scroll direction should be bottom",
21300 );
21301
21302 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21303 assert_eq!(
21304 e.next_scroll_position,
21305 NextScrollCursorCenterTopBottom::Center,
21306 "After bottom, scrolling should start over",
21307 );
21308
21309 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21310 assert_eq!(
21311 e.next_scroll_position,
21312 NextScrollCursorCenterTopBottom::Top,
21313 "Scrolling continues if retriggered fast enough"
21314 );
21315 });
21316
21317 cx.executor()
21318 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
21319 cx.executor().run_until_parked();
21320 cx.update_editor(|e, _, _| {
21321 assert_eq!(
21322 e.next_scroll_position,
21323 NextScrollCursorCenterTopBottom::Center,
21324 "If scrolling is not triggered fast enough, it should reset"
21325 );
21326 });
21327}
21328
21329#[gpui::test]
21330async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
21331 init_test(cx, |_| {});
21332 let mut cx = EditorLspTestContext::new_rust(
21333 lsp::ServerCapabilities {
21334 definition_provider: Some(lsp::OneOf::Left(true)),
21335 references_provider: Some(lsp::OneOf::Left(true)),
21336 ..lsp::ServerCapabilities::default()
21337 },
21338 cx,
21339 )
21340 .await;
21341
21342 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
21343 let go_to_definition = cx
21344 .lsp
21345 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21346 move |params, _| async move {
21347 if empty_go_to_definition {
21348 Ok(None)
21349 } else {
21350 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
21351 uri: params.text_document_position_params.text_document.uri,
21352 range: lsp::Range::new(
21353 lsp::Position::new(4, 3),
21354 lsp::Position::new(4, 6),
21355 ),
21356 })))
21357 }
21358 },
21359 );
21360 let references = cx
21361 .lsp
21362 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21363 Ok(Some(vec![lsp::Location {
21364 uri: params.text_document_position.text_document.uri,
21365 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
21366 }]))
21367 });
21368 (go_to_definition, references)
21369 };
21370
21371 cx.set_state(
21372 &r#"fn one() {
21373 let mut a = ˇtwo();
21374 }
21375
21376 fn two() {}"#
21377 .unindent(),
21378 );
21379 set_up_lsp_handlers(false, &mut cx);
21380 let navigated = cx
21381 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21382 .await
21383 .expect("Failed to navigate to definition");
21384 assert_eq!(
21385 navigated,
21386 Navigated::Yes,
21387 "Should have navigated to definition from the GetDefinition response"
21388 );
21389 cx.assert_editor_state(
21390 &r#"fn one() {
21391 let mut a = two();
21392 }
21393
21394 fn «twoˇ»() {}"#
21395 .unindent(),
21396 );
21397
21398 let editors = cx.update_workspace(|workspace, _, cx| {
21399 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21400 });
21401 cx.update_editor(|_, _, test_editor_cx| {
21402 assert_eq!(
21403 editors.len(),
21404 1,
21405 "Initially, only one, test, editor should be open in the workspace"
21406 );
21407 assert_eq!(
21408 test_editor_cx.entity(),
21409 editors.last().expect("Asserted len is 1").clone()
21410 );
21411 });
21412
21413 set_up_lsp_handlers(true, &mut cx);
21414 let navigated = cx
21415 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21416 .await
21417 .expect("Failed to navigate to lookup references");
21418 assert_eq!(
21419 navigated,
21420 Navigated::Yes,
21421 "Should have navigated to references as a fallback after empty GoToDefinition response"
21422 );
21423 // We should not change the selections in the existing file,
21424 // if opening another milti buffer with the references
21425 cx.assert_editor_state(
21426 &r#"fn one() {
21427 let mut a = two();
21428 }
21429
21430 fn «twoˇ»() {}"#
21431 .unindent(),
21432 );
21433 let editors = cx.update_workspace(|workspace, _, cx| {
21434 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21435 });
21436 cx.update_editor(|_, _, test_editor_cx| {
21437 assert_eq!(
21438 editors.len(),
21439 2,
21440 "After falling back to references search, we open a new editor with the results"
21441 );
21442 let references_fallback_text = editors
21443 .into_iter()
21444 .find(|new_editor| *new_editor != test_editor_cx.entity())
21445 .expect("Should have one non-test editor now")
21446 .read(test_editor_cx)
21447 .text(test_editor_cx);
21448 assert_eq!(
21449 references_fallback_text, "fn one() {\n let mut a = two();\n}",
21450 "Should use the range from the references response and not the GoToDefinition one"
21451 );
21452 });
21453}
21454
21455#[gpui::test]
21456async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
21457 init_test(cx, |_| {});
21458 cx.update(|cx| {
21459 let mut editor_settings = EditorSettings::get_global(cx).clone();
21460 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
21461 EditorSettings::override_global(editor_settings, cx);
21462 });
21463 let mut cx = EditorLspTestContext::new_rust(
21464 lsp::ServerCapabilities {
21465 definition_provider: Some(lsp::OneOf::Left(true)),
21466 references_provider: Some(lsp::OneOf::Left(true)),
21467 ..lsp::ServerCapabilities::default()
21468 },
21469 cx,
21470 )
21471 .await;
21472 let original_state = r#"fn one() {
21473 let mut a = ˇtwo();
21474 }
21475
21476 fn two() {}"#
21477 .unindent();
21478 cx.set_state(&original_state);
21479
21480 let mut go_to_definition = cx
21481 .lsp
21482 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21483 move |_, _| async move { Ok(None) },
21484 );
21485 let _references = cx
21486 .lsp
21487 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
21488 panic!("Should not call for references with no go to definition fallback")
21489 });
21490
21491 let navigated = cx
21492 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21493 .await
21494 .expect("Failed to navigate to lookup references");
21495 go_to_definition
21496 .next()
21497 .await
21498 .expect("Should have called the go_to_definition handler");
21499
21500 assert_eq!(
21501 navigated,
21502 Navigated::No,
21503 "Should have navigated to references as a fallback after empty GoToDefinition response"
21504 );
21505 cx.assert_editor_state(&original_state);
21506 let editors = cx.update_workspace(|workspace, _, cx| {
21507 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21508 });
21509 cx.update_editor(|_, _, _| {
21510 assert_eq!(
21511 editors.len(),
21512 1,
21513 "After unsuccessful fallback, no other editor should have been opened"
21514 );
21515 });
21516}
21517
21518#[gpui::test]
21519async fn test_find_all_references_editor_reuse(cx: &mut TestAppContext) {
21520 init_test(cx, |_| {});
21521 let mut cx = EditorLspTestContext::new_rust(
21522 lsp::ServerCapabilities {
21523 references_provider: Some(lsp::OneOf::Left(true)),
21524 ..lsp::ServerCapabilities::default()
21525 },
21526 cx,
21527 )
21528 .await;
21529
21530 cx.set_state(
21531 &r#"
21532 fn one() {
21533 let mut a = two();
21534 }
21535
21536 fn ˇtwo() {}"#
21537 .unindent(),
21538 );
21539 cx.lsp
21540 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21541 Ok(Some(vec![
21542 lsp::Location {
21543 uri: params.text_document_position.text_document.uri.clone(),
21544 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
21545 },
21546 lsp::Location {
21547 uri: params.text_document_position.text_document.uri,
21548 range: lsp::Range::new(lsp::Position::new(4, 4), lsp::Position::new(4, 7)),
21549 },
21550 ]))
21551 });
21552 let navigated = cx
21553 .update_editor(|editor, window, cx| {
21554 editor.find_all_references(&FindAllReferences, window, cx)
21555 })
21556 .unwrap()
21557 .await
21558 .expect("Failed to navigate to references");
21559 assert_eq!(
21560 navigated,
21561 Navigated::Yes,
21562 "Should have navigated to references from the FindAllReferences response"
21563 );
21564 cx.assert_editor_state(
21565 &r#"fn one() {
21566 let mut a = two();
21567 }
21568
21569 fn ˇtwo() {}"#
21570 .unindent(),
21571 );
21572
21573 let editors = cx.update_workspace(|workspace, _, cx| {
21574 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21575 });
21576 cx.update_editor(|_, _, _| {
21577 assert_eq!(editors.len(), 2, "We should have opened a new multibuffer");
21578 });
21579
21580 cx.set_state(
21581 &r#"fn one() {
21582 let mut a = ˇtwo();
21583 }
21584
21585 fn two() {}"#
21586 .unindent(),
21587 );
21588 let navigated = cx
21589 .update_editor(|editor, window, cx| {
21590 editor.find_all_references(&FindAllReferences, window, cx)
21591 })
21592 .unwrap()
21593 .await
21594 .expect("Failed to navigate to references");
21595 assert_eq!(
21596 navigated,
21597 Navigated::Yes,
21598 "Should have navigated to references from the FindAllReferences response"
21599 );
21600 cx.assert_editor_state(
21601 &r#"fn one() {
21602 let mut a = ˇtwo();
21603 }
21604
21605 fn two() {}"#
21606 .unindent(),
21607 );
21608 let editors = cx.update_workspace(|workspace, _, cx| {
21609 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21610 });
21611 cx.update_editor(|_, _, _| {
21612 assert_eq!(
21613 editors.len(),
21614 2,
21615 "should have re-used the previous multibuffer"
21616 );
21617 });
21618
21619 cx.set_state(
21620 &r#"fn one() {
21621 let mut a = ˇtwo();
21622 }
21623 fn three() {}
21624 fn two() {}"#
21625 .unindent(),
21626 );
21627 cx.lsp
21628 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21629 Ok(Some(vec![
21630 lsp::Location {
21631 uri: params.text_document_position.text_document.uri.clone(),
21632 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
21633 },
21634 lsp::Location {
21635 uri: params.text_document_position.text_document.uri,
21636 range: lsp::Range::new(lsp::Position::new(5, 4), lsp::Position::new(5, 7)),
21637 },
21638 ]))
21639 });
21640 let navigated = cx
21641 .update_editor(|editor, window, cx| {
21642 editor.find_all_references(&FindAllReferences, window, cx)
21643 })
21644 .unwrap()
21645 .await
21646 .expect("Failed to navigate to references");
21647 assert_eq!(
21648 navigated,
21649 Navigated::Yes,
21650 "Should have navigated to references from the FindAllReferences response"
21651 );
21652 cx.assert_editor_state(
21653 &r#"fn one() {
21654 let mut a = ˇtwo();
21655 }
21656 fn three() {}
21657 fn two() {}"#
21658 .unindent(),
21659 );
21660 let editors = cx.update_workspace(|workspace, _, cx| {
21661 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21662 });
21663 cx.update_editor(|_, _, _| {
21664 assert_eq!(
21665 editors.len(),
21666 3,
21667 "should have used a new multibuffer as offsets changed"
21668 );
21669 });
21670}
21671#[gpui::test]
21672async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
21673 init_test(cx, |_| {});
21674
21675 let language = Arc::new(Language::new(
21676 LanguageConfig::default(),
21677 Some(tree_sitter_rust::LANGUAGE.into()),
21678 ));
21679
21680 let text = r#"
21681 #[cfg(test)]
21682 mod tests() {
21683 #[test]
21684 fn runnable_1() {
21685 let a = 1;
21686 }
21687
21688 #[test]
21689 fn runnable_2() {
21690 let a = 1;
21691 let b = 2;
21692 }
21693 }
21694 "#
21695 .unindent();
21696
21697 let fs = FakeFs::new(cx.executor());
21698 fs.insert_file("/file.rs", Default::default()).await;
21699
21700 let project = Project::test(fs, ["/a".as_ref()], cx).await;
21701 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21702 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21703 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
21704 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
21705
21706 let editor = cx.new_window_entity(|window, cx| {
21707 Editor::new(
21708 EditorMode::full(),
21709 multi_buffer,
21710 Some(project.clone()),
21711 window,
21712 cx,
21713 )
21714 });
21715
21716 editor.update_in(cx, |editor, window, cx| {
21717 let snapshot = editor.buffer().read(cx).snapshot(cx);
21718 editor.tasks.insert(
21719 (buffer.read(cx).remote_id(), 3),
21720 RunnableTasks {
21721 templates: vec![],
21722 offset: snapshot.anchor_before(43),
21723 column: 0,
21724 extra_variables: HashMap::default(),
21725 context_range: BufferOffset(43)..BufferOffset(85),
21726 },
21727 );
21728 editor.tasks.insert(
21729 (buffer.read(cx).remote_id(), 8),
21730 RunnableTasks {
21731 templates: vec![],
21732 offset: snapshot.anchor_before(86),
21733 column: 0,
21734 extra_variables: HashMap::default(),
21735 context_range: BufferOffset(86)..BufferOffset(191),
21736 },
21737 );
21738
21739 // Test finding task when cursor is inside function body
21740 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21741 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
21742 });
21743 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
21744 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
21745
21746 // Test finding task when cursor is on function name
21747 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21748 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
21749 });
21750 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
21751 assert_eq!(row, 8, "Should find task when cursor is on function name");
21752 });
21753}
21754
21755#[gpui::test]
21756async fn test_folding_buffers(cx: &mut TestAppContext) {
21757 init_test(cx, |_| {});
21758
21759 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
21760 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
21761 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
21762
21763 let fs = FakeFs::new(cx.executor());
21764 fs.insert_tree(
21765 path!("/a"),
21766 json!({
21767 "first.rs": sample_text_1,
21768 "second.rs": sample_text_2,
21769 "third.rs": sample_text_3,
21770 }),
21771 )
21772 .await;
21773 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21774 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21775 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21776 let worktree = project.update(cx, |project, cx| {
21777 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
21778 assert_eq!(worktrees.len(), 1);
21779 worktrees.pop().unwrap()
21780 });
21781 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
21782
21783 let buffer_1 = project
21784 .update(cx, |project, cx| {
21785 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
21786 })
21787 .await
21788 .unwrap();
21789 let buffer_2 = project
21790 .update(cx, |project, cx| {
21791 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
21792 })
21793 .await
21794 .unwrap();
21795 let buffer_3 = project
21796 .update(cx, |project, cx| {
21797 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
21798 })
21799 .await
21800 .unwrap();
21801
21802 let multi_buffer = cx.new(|cx| {
21803 let mut multi_buffer = MultiBuffer::new(ReadWrite);
21804 multi_buffer.push_excerpts(
21805 buffer_1.clone(),
21806 [
21807 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21808 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21809 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21810 ],
21811 cx,
21812 );
21813 multi_buffer.push_excerpts(
21814 buffer_2.clone(),
21815 [
21816 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21817 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21818 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21819 ],
21820 cx,
21821 );
21822 multi_buffer.push_excerpts(
21823 buffer_3.clone(),
21824 [
21825 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21826 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21827 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21828 ],
21829 cx,
21830 );
21831 multi_buffer
21832 });
21833 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
21834 Editor::new(
21835 EditorMode::full(),
21836 multi_buffer.clone(),
21837 Some(project.clone()),
21838 window,
21839 cx,
21840 )
21841 });
21842
21843 assert_eq!(
21844 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21845 "\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",
21846 );
21847
21848 multi_buffer_editor.update(cx, |editor, cx| {
21849 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
21850 });
21851 assert_eq!(
21852 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21853 "\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",
21854 "After folding the first buffer, its text should not be displayed"
21855 );
21856
21857 multi_buffer_editor.update(cx, |editor, cx| {
21858 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
21859 });
21860 assert_eq!(
21861 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21862 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
21863 "After folding the second buffer, its text should not be displayed"
21864 );
21865
21866 multi_buffer_editor.update(cx, |editor, cx| {
21867 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
21868 });
21869 assert_eq!(
21870 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21871 "\n\n\n\n\n",
21872 "After folding the third buffer, its text should not be displayed"
21873 );
21874
21875 // Emulate selection inside the fold logic, that should work
21876 multi_buffer_editor.update_in(cx, |editor, window, cx| {
21877 editor
21878 .snapshot(window, cx)
21879 .next_line_boundary(Point::new(0, 4));
21880 });
21881
21882 multi_buffer_editor.update(cx, |editor, cx| {
21883 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
21884 });
21885 assert_eq!(
21886 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21887 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
21888 "After unfolding the second buffer, its text should be displayed"
21889 );
21890
21891 // Typing inside of buffer 1 causes that buffer to be unfolded.
21892 multi_buffer_editor.update_in(cx, |editor, window, cx| {
21893 assert_eq!(
21894 multi_buffer
21895 .read(cx)
21896 .snapshot(cx)
21897 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
21898 .collect::<String>(),
21899 "bbbb"
21900 );
21901 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
21902 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
21903 });
21904 editor.handle_input("B", window, cx);
21905 });
21906
21907 assert_eq!(
21908 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21909 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
21910 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
21911 );
21912
21913 multi_buffer_editor.update(cx, |editor, cx| {
21914 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
21915 });
21916 assert_eq!(
21917 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21918 "\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",
21919 "After unfolding the all buffers, all original text should be displayed"
21920 );
21921}
21922
21923#[gpui::test]
21924async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
21925 init_test(cx, |_| {});
21926
21927 let sample_text_1 = "1111\n2222\n3333".to_string();
21928 let sample_text_2 = "4444\n5555\n6666".to_string();
21929 let sample_text_3 = "7777\n8888\n9999".to_string();
21930
21931 let fs = FakeFs::new(cx.executor());
21932 fs.insert_tree(
21933 path!("/a"),
21934 json!({
21935 "first.rs": sample_text_1,
21936 "second.rs": sample_text_2,
21937 "third.rs": sample_text_3,
21938 }),
21939 )
21940 .await;
21941 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21942 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21943 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21944 let worktree = project.update(cx, |project, cx| {
21945 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
21946 assert_eq!(worktrees.len(), 1);
21947 worktrees.pop().unwrap()
21948 });
21949 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
21950
21951 let buffer_1 = project
21952 .update(cx, |project, cx| {
21953 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
21954 })
21955 .await
21956 .unwrap();
21957 let buffer_2 = project
21958 .update(cx, |project, cx| {
21959 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
21960 })
21961 .await
21962 .unwrap();
21963 let buffer_3 = project
21964 .update(cx, |project, cx| {
21965 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
21966 })
21967 .await
21968 .unwrap();
21969
21970 let multi_buffer = cx.new(|cx| {
21971 let mut multi_buffer = MultiBuffer::new(ReadWrite);
21972 multi_buffer.push_excerpts(
21973 buffer_1.clone(),
21974 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
21975 cx,
21976 );
21977 multi_buffer.push_excerpts(
21978 buffer_2.clone(),
21979 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
21980 cx,
21981 );
21982 multi_buffer.push_excerpts(
21983 buffer_3.clone(),
21984 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
21985 cx,
21986 );
21987 multi_buffer
21988 });
21989
21990 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
21991 Editor::new(
21992 EditorMode::full(),
21993 multi_buffer,
21994 Some(project.clone()),
21995 window,
21996 cx,
21997 )
21998 });
21999
22000 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
22001 assert_eq!(
22002 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22003 full_text,
22004 );
22005
22006 multi_buffer_editor.update(cx, |editor, cx| {
22007 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
22008 });
22009 assert_eq!(
22010 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22011 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
22012 "After folding the first buffer, its text should not be displayed"
22013 );
22014
22015 multi_buffer_editor.update(cx, |editor, cx| {
22016 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
22017 });
22018
22019 assert_eq!(
22020 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22021 "\n\n\n\n\n\n7777\n8888\n9999",
22022 "After folding the second buffer, its text should not be displayed"
22023 );
22024
22025 multi_buffer_editor.update(cx, |editor, cx| {
22026 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
22027 });
22028 assert_eq!(
22029 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22030 "\n\n\n\n\n",
22031 "After folding the third buffer, its text should not be displayed"
22032 );
22033
22034 multi_buffer_editor.update(cx, |editor, cx| {
22035 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
22036 });
22037 assert_eq!(
22038 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22039 "\n\n\n\n4444\n5555\n6666\n\n",
22040 "After unfolding the second buffer, its text should be displayed"
22041 );
22042
22043 multi_buffer_editor.update(cx, |editor, cx| {
22044 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
22045 });
22046 assert_eq!(
22047 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22048 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
22049 "After unfolding the first buffer, its text should be displayed"
22050 );
22051
22052 multi_buffer_editor.update(cx, |editor, cx| {
22053 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
22054 });
22055 assert_eq!(
22056 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22057 full_text,
22058 "After unfolding all buffers, all original text should be displayed"
22059 );
22060}
22061
22062#[gpui::test]
22063async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
22064 init_test(cx, |_| {});
22065
22066 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
22067
22068 let fs = FakeFs::new(cx.executor());
22069 fs.insert_tree(
22070 path!("/a"),
22071 json!({
22072 "main.rs": sample_text,
22073 }),
22074 )
22075 .await;
22076 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22077 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22078 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22079 let worktree = project.update(cx, |project, cx| {
22080 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22081 assert_eq!(worktrees.len(), 1);
22082 worktrees.pop().unwrap()
22083 });
22084 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22085
22086 let buffer_1 = project
22087 .update(cx, |project, cx| {
22088 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22089 })
22090 .await
22091 .unwrap();
22092
22093 let multi_buffer = cx.new(|cx| {
22094 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22095 multi_buffer.push_excerpts(
22096 buffer_1.clone(),
22097 [ExcerptRange::new(
22098 Point::new(0, 0)
22099 ..Point::new(
22100 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
22101 0,
22102 ),
22103 )],
22104 cx,
22105 );
22106 multi_buffer
22107 });
22108 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22109 Editor::new(
22110 EditorMode::full(),
22111 multi_buffer,
22112 Some(project.clone()),
22113 window,
22114 cx,
22115 )
22116 });
22117
22118 let selection_range = Point::new(1, 0)..Point::new(2, 0);
22119 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22120 enum TestHighlight {}
22121 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
22122 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
22123 editor.highlight_text::<TestHighlight>(
22124 vec![highlight_range.clone()],
22125 HighlightStyle::color(Hsla::green()),
22126 cx,
22127 );
22128 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22129 s.select_ranges(Some(highlight_range))
22130 });
22131 });
22132
22133 let full_text = format!("\n\n{sample_text}");
22134 assert_eq!(
22135 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22136 full_text,
22137 );
22138}
22139
22140#[gpui::test]
22141async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
22142 init_test(cx, |_| {});
22143 cx.update(|cx| {
22144 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
22145 "keymaps/default-linux.json",
22146 cx,
22147 )
22148 .unwrap();
22149 cx.bind_keys(default_key_bindings);
22150 });
22151
22152 let (editor, cx) = cx.add_window_view(|window, cx| {
22153 let multi_buffer = MultiBuffer::build_multi(
22154 [
22155 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
22156 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
22157 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
22158 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
22159 ],
22160 cx,
22161 );
22162 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
22163
22164 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
22165 // fold all but the second buffer, so that we test navigating between two
22166 // adjacent folded buffers, as well as folded buffers at the start and
22167 // end the multibuffer
22168 editor.fold_buffer(buffer_ids[0], cx);
22169 editor.fold_buffer(buffer_ids[2], cx);
22170 editor.fold_buffer(buffer_ids[3], cx);
22171
22172 editor
22173 });
22174 cx.simulate_resize(size(px(1000.), px(1000.)));
22175
22176 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
22177 cx.assert_excerpts_with_selections(indoc! {"
22178 [EXCERPT]
22179 ˇ[FOLDED]
22180 [EXCERPT]
22181 a1
22182 b1
22183 [EXCERPT]
22184 [FOLDED]
22185 [EXCERPT]
22186 [FOLDED]
22187 "
22188 });
22189 cx.simulate_keystroke("down");
22190 cx.assert_excerpts_with_selections(indoc! {"
22191 [EXCERPT]
22192 [FOLDED]
22193 [EXCERPT]
22194 ˇa1
22195 b1
22196 [EXCERPT]
22197 [FOLDED]
22198 [EXCERPT]
22199 [FOLDED]
22200 "
22201 });
22202 cx.simulate_keystroke("down");
22203 cx.assert_excerpts_with_selections(indoc! {"
22204 [EXCERPT]
22205 [FOLDED]
22206 [EXCERPT]
22207 a1
22208 ˇb1
22209 [EXCERPT]
22210 [FOLDED]
22211 [EXCERPT]
22212 [FOLDED]
22213 "
22214 });
22215 cx.simulate_keystroke("down");
22216 cx.assert_excerpts_with_selections(indoc! {"
22217 [EXCERPT]
22218 [FOLDED]
22219 [EXCERPT]
22220 a1
22221 b1
22222 ˇ[EXCERPT]
22223 [FOLDED]
22224 [EXCERPT]
22225 [FOLDED]
22226 "
22227 });
22228 cx.simulate_keystroke("down");
22229 cx.assert_excerpts_with_selections(indoc! {"
22230 [EXCERPT]
22231 [FOLDED]
22232 [EXCERPT]
22233 a1
22234 b1
22235 [EXCERPT]
22236 ˇ[FOLDED]
22237 [EXCERPT]
22238 [FOLDED]
22239 "
22240 });
22241 for _ in 0..5 {
22242 cx.simulate_keystroke("down");
22243 cx.assert_excerpts_with_selections(indoc! {"
22244 [EXCERPT]
22245 [FOLDED]
22246 [EXCERPT]
22247 a1
22248 b1
22249 [EXCERPT]
22250 [FOLDED]
22251 [EXCERPT]
22252 ˇ[FOLDED]
22253 "
22254 });
22255 }
22256
22257 cx.simulate_keystroke("up");
22258 cx.assert_excerpts_with_selections(indoc! {"
22259 [EXCERPT]
22260 [FOLDED]
22261 [EXCERPT]
22262 a1
22263 b1
22264 [EXCERPT]
22265 ˇ[FOLDED]
22266 [EXCERPT]
22267 [FOLDED]
22268 "
22269 });
22270 cx.simulate_keystroke("up");
22271 cx.assert_excerpts_with_selections(indoc! {"
22272 [EXCERPT]
22273 [FOLDED]
22274 [EXCERPT]
22275 a1
22276 b1
22277 ˇ[EXCERPT]
22278 [FOLDED]
22279 [EXCERPT]
22280 [FOLDED]
22281 "
22282 });
22283 cx.simulate_keystroke("up");
22284 cx.assert_excerpts_with_selections(indoc! {"
22285 [EXCERPT]
22286 [FOLDED]
22287 [EXCERPT]
22288 a1
22289 ˇb1
22290 [EXCERPT]
22291 [FOLDED]
22292 [EXCERPT]
22293 [FOLDED]
22294 "
22295 });
22296 cx.simulate_keystroke("up");
22297 cx.assert_excerpts_with_selections(indoc! {"
22298 [EXCERPT]
22299 [FOLDED]
22300 [EXCERPT]
22301 ˇa1
22302 b1
22303 [EXCERPT]
22304 [FOLDED]
22305 [EXCERPT]
22306 [FOLDED]
22307 "
22308 });
22309 for _ in 0..5 {
22310 cx.simulate_keystroke("up");
22311 cx.assert_excerpts_with_selections(indoc! {"
22312 [EXCERPT]
22313 ˇ[FOLDED]
22314 [EXCERPT]
22315 a1
22316 b1
22317 [EXCERPT]
22318 [FOLDED]
22319 [EXCERPT]
22320 [FOLDED]
22321 "
22322 });
22323 }
22324}
22325
22326#[gpui::test]
22327async fn test_edit_prediction_text(cx: &mut TestAppContext) {
22328 init_test(cx, |_| {});
22329
22330 // Simple insertion
22331 assert_highlighted_edits(
22332 "Hello, world!",
22333 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
22334 true,
22335 cx,
22336 |highlighted_edits, cx| {
22337 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
22338 assert_eq!(highlighted_edits.highlights.len(), 1);
22339 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
22340 assert_eq!(
22341 highlighted_edits.highlights[0].1.background_color,
22342 Some(cx.theme().status().created_background)
22343 );
22344 },
22345 )
22346 .await;
22347
22348 // Replacement
22349 assert_highlighted_edits(
22350 "This is a test.",
22351 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
22352 false,
22353 cx,
22354 |highlighted_edits, cx| {
22355 assert_eq!(highlighted_edits.text, "That is a test.");
22356 assert_eq!(highlighted_edits.highlights.len(), 1);
22357 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
22358 assert_eq!(
22359 highlighted_edits.highlights[0].1.background_color,
22360 Some(cx.theme().status().created_background)
22361 );
22362 },
22363 )
22364 .await;
22365
22366 // Multiple edits
22367 assert_highlighted_edits(
22368 "Hello, world!",
22369 vec![
22370 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
22371 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
22372 ],
22373 false,
22374 cx,
22375 |highlighted_edits, cx| {
22376 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
22377 assert_eq!(highlighted_edits.highlights.len(), 2);
22378 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
22379 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
22380 assert_eq!(
22381 highlighted_edits.highlights[0].1.background_color,
22382 Some(cx.theme().status().created_background)
22383 );
22384 assert_eq!(
22385 highlighted_edits.highlights[1].1.background_color,
22386 Some(cx.theme().status().created_background)
22387 );
22388 },
22389 )
22390 .await;
22391
22392 // Multiple lines with edits
22393 assert_highlighted_edits(
22394 "First line\nSecond line\nThird line\nFourth line",
22395 vec![
22396 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
22397 (
22398 Point::new(2, 0)..Point::new(2, 10),
22399 "New third line".to_string(),
22400 ),
22401 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
22402 ],
22403 false,
22404 cx,
22405 |highlighted_edits, cx| {
22406 assert_eq!(
22407 highlighted_edits.text,
22408 "Second modified\nNew third line\nFourth updated line"
22409 );
22410 assert_eq!(highlighted_edits.highlights.len(), 3);
22411 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
22412 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
22413 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
22414 for highlight in &highlighted_edits.highlights {
22415 assert_eq!(
22416 highlight.1.background_color,
22417 Some(cx.theme().status().created_background)
22418 );
22419 }
22420 },
22421 )
22422 .await;
22423}
22424
22425#[gpui::test]
22426async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
22427 init_test(cx, |_| {});
22428
22429 // Deletion
22430 assert_highlighted_edits(
22431 "Hello, world!",
22432 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
22433 true,
22434 cx,
22435 |highlighted_edits, cx| {
22436 assert_eq!(highlighted_edits.text, "Hello, world!");
22437 assert_eq!(highlighted_edits.highlights.len(), 1);
22438 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
22439 assert_eq!(
22440 highlighted_edits.highlights[0].1.background_color,
22441 Some(cx.theme().status().deleted_background)
22442 );
22443 },
22444 )
22445 .await;
22446
22447 // Insertion
22448 assert_highlighted_edits(
22449 "Hello, world!",
22450 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
22451 true,
22452 cx,
22453 |highlighted_edits, cx| {
22454 assert_eq!(highlighted_edits.highlights.len(), 1);
22455 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
22456 assert_eq!(
22457 highlighted_edits.highlights[0].1.background_color,
22458 Some(cx.theme().status().created_background)
22459 );
22460 },
22461 )
22462 .await;
22463}
22464
22465async fn assert_highlighted_edits(
22466 text: &str,
22467 edits: Vec<(Range<Point>, String)>,
22468 include_deletions: bool,
22469 cx: &mut TestAppContext,
22470 assertion_fn: impl Fn(HighlightedText, &App),
22471) {
22472 let window = cx.add_window(|window, cx| {
22473 let buffer = MultiBuffer::build_simple(text, cx);
22474 Editor::new(EditorMode::full(), buffer, None, window, cx)
22475 });
22476 let cx = &mut VisualTestContext::from_window(*window, cx);
22477
22478 let (buffer, snapshot) = window
22479 .update(cx, |editor, _window, cx| {
22480 (
22481 editor.buffer().clone(),
22482 editor.buffer().read(cx).snapshot(cx),
22483 )
22484 })
22485 .unwrap();
22486
22487 let edits = edits
22488 .into_iter()
22489 .map(|(range, edit)| {
22490 (
22491 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
22492 edit,
22493 )
22494 })
22495 .collect::<Vec<_>>();
22496
22497 let text_anchor_edits = edits
22498 .clone()
22499 .into_iter()
22500 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
22501 .collect::<Vec<_>>();
22502
22503 let edit_preview = window
22504 .update(cx, |_, _window, cx| {
22505 buffer
22506 .read(cx)
22507 .as_singleton()
22508 .unwrap()
22509 .read(cx)
22510 .preview_edits(text_anchor_edits.into(), cx)
22511 })
22512 .unwrap()
22513 .await;
22514
22515 cx.update(|_window, cx| {
22516 let highlighted_edits = edit_prediction_edit_text(
22517 snapshot.as_singleton().unwrap().2,
22518 &edits,
22519 &edit_preview,
22520 include_deletions,
22521 cx,
22522 );
22523 assertion_fn(highlighted_edits, cx)
22524 });
22525}
22526
22527#[track_caller]
22528fn assert_breakpoint(
22529 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
22530 path: &Arc<Path>,
22531 expected: Vec<(u32, Breakpoint)>,
22532) {
22533 if expected.is_empty() {
22534 assert!(!breakpoints.contains_key(path), "{}", path.display());
22535 } else {
22536 let mut breakpoint = breakpoints
22537 .get(path)
22538 .unwrap()
22539 .iter()
22540 .map(|breakpoint| {
22541 (
22542 breakpoint.row,
22543 Breakpoint {
22544 message: breakpoint.message.clone(),
22545 state: breakpoint.state,
22546 condition: breakpoint.condition.clone(),
22547 hit_condition: breakpoint.hit_condition.clone(),
22548 },
22549 )
22550 })
22551 .collect::<Vec<_>>();
22552
22553 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
22554
22555 assert_eq!(expected, breakpoint);
22556 }
22557}
22558
22559fn add_log_breakpoint_at_cursor(
22560 editor: &mut Editor,
22561 log_message: &str,
22562 window: &mut Window,
22563 cx: &mut Context<Editor>,
22564) {
22565 let (anchor, bp) = editor
22566 .breakpoints_at_cursors(window, cx)
22567 .first()
22568 .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
22569 .unwrap_or_else(|| {
22570 let cursor_position: Point = editor.selections.newest(cx).head();
22571
22572 let breakpoint_position = editor
22573 .snapshot(window, cx)
22574 .display_snapshot
22575 .buffer_snapshot()
22576 .anchor_before(Point::new(cursor_position.row, 0));
22577
22578 (breakpoint_position, Breakpoint::new_log(log_message))
22579 });
22580
22581 editor.edit_breakpoint_at_anchor(
22582 anchor,
22583 bp,
22584 BreakpointEditAction::EditLogMessage(log_message.into()),
22585 cx,
22586 );
22587}
22588
22589#[gpui::test]
22590async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
22591 init_test(cx, |_| {});
22592
22593 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22594 let fs = FakeFs::new(cx.executor());
22595 fs.insert_tree(
22596 path!("/a"),
22597 json!({
22598 "main.rs": sample_text,
22599 }),
22600 )
22601 .await;
22602 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22603 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22604 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22605
22606 let fs = FakeFs::new(cx.executor());
22607 fs.insert_tree(
22608 path!("/a"),
22609 json!({
22610 "main.rs": sample_text,
22611 }),
22612 )
22613 .await;
22614 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22615 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22616 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22617 let worktree_id = workspace
22618 .update(cx, |workspace, _window, cx| {
22619 workspace.project().update(cx, |project, cx| {
22620 project.worktrees(cx).next().unwrap().read(cx).id()
22621 })
22622 })
22623 .unwrap();
22624
22625 let buffer = project
22626 .update(cx, |project, cx| {
22627 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22628 })
22629 .await
22630 .unwrap();
22631
22632 let (editor, cx) = cx.add_window_view(|window, cx| {
22633 Editor::new(
22634 EditorMode::full(),
22635 MultiBuffer::build_from_buffer(buffer, cx),
22636 Some(project.clone()),
22637 window,
22638 cx,
22639 )
22640 });
22641
22642 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22643 let abs_path = project.read_with(cx, |project, cx| {
22644 project
22645 .absolute_path(&project_path, cx)
22646 .map(Arc::from)
22647 .unwrap()
22648 });
22649
22650 // assert we can add breakpoint on the first line
22651 editor.update_in(cx, |editor, window, cx| {
22652 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22653 editor.move_to_end(&MoveToEnd, window, cx);
22654 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22655 });
22656
22657 let breakpoints = editor.update(cx, |editor, cx| {
22658 editor
22659 .breakpoint_store()
22660 .as_ref()
22661 .unwrap()
22662 .read(cx)
22663 .all_source_breakpoints(cx)
22664 });
22665
22666 assert_eq!(1, breakpoints.len());
22667 assert_breakpoint(
22668 &breakpoints,
22669 &abs_path,
22670 vec![
22671 (0, Breakpoint::new_standard()),
22672 (3, Breakpoint::new_standard()),
22673 ],
22674 );
22675
22676 editor.update_in(cx, |editor, window, cx| {
22677 editor.move_to_beginning(&MoveToBeginning, window, cx);
22678 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22679 });
22680
22681 let breakpoints = editor.update(cx, |editor, cx| {
22682 editor
22683 .breakpoint_store()
22684 .as_ref()
22685 .unwrap()
22686 .read(cx)
22687 .all_source_breakpoints(cx)
22688 });
22689
22690 assert_eq!(1, breakpoints.len());
22691 assert_breakpoint(
22692 &breakpoints,
22693 &abs_path,
22694 vec![(3, Breakpoint::new_standard())],
22695 );
22696
22697 editor.update_in(cx, |editor, window, cx| {
22698 editor.move_to_end(&MoveToEnd, window, cx);
22699 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22700 });
22701
22702 let breakpoints = editor.update(cx, |editor, cx| {
22703 editor
22704 .breakpoint_store()
22705 .as_ref()
22706 .unwrap()
22707 .read(cx)
22708 .all_source_breakpoints(cx)
22709 });
22710
22711 assert_eq!(0, breakpoints.len());
22712 assert_breakpoint(&breakpoints, &abs_path, vec![]);
22713}
22714
22715#[gpui::test]
22716async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
22717 init_test(cx, |_| {});
22718
22719 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22720
22721 let fs = FakeFs::new(cx.executor());
22722 fs.insert_tree(
22723 path!("/a"),
22724 json!({
22725 "main.rs": sample_text,
22726 }),
22727 )
22728 .await;
22729 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22730 let (workspace, cx) =
22731 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
22732
22733 let worktree_id = workspace.update(cx, |workspace, cx| {
22734 workspace.project().update(cx, |project, cx| {
22735 project.worktrees(cx).next().unwrap().read(cx).id()
22736 })
22737 });
22738
22739 let buffer = project
22740 .update(cx, |project, cx| {
22741 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22742 })
22743 .await
22744 .unwrap();
22745
22746 let (editor, cx) = cx.add_window_view(|window, cx| {
22747 Editor::new(
22748 EditorMode::full(),
22749 MultiBuffer::build_from_buffer(buffer, cx),
22750 Some(project.clone()),
22751 window,
22752 cx,
22753 )
22754 });
22755
22756 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22757 let abs_path = project.read_with(cx, |project, cx| {
22758 project
22759 .absolute_path(&project_path, cx)
22760 .map(Arc::from)
22761 .unwrap()
22762 });
22763
22764 editor.update_in(cx, |editor, window, cx| {
22765 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
22766 });
22767
22768 let breakpoints = editor.update(cx, |editor, cx| {
22769 editor
22770 .breakpoint_store()
22771 .as_ref()
22772 .unwrap()
22773 .read(cx)
22774 .all_source_breakpoints(cx)
22775 });
22776
22777 assert_breakpoint(
22778 &breakpoints,
22779 &abs_path,
22780 vec![(0, Breakpoint::new_log("hello world"))],
22781 );
22782
22783 // Removing a log message from a log breakpoint should remove it
22784 editor.update_in(cx, |editor, window, cx| {
22785 add_log_breakpoint_at_cursor(editor, "", window, cx);
22786 });
22787
22788 let breakpoints = editor.update(cx, |editor, cx| {
22789 editor
22790 .breakpoint_store()
22791 .as_ref()
22792 .unwrap()
22793 .read(cx)
22794 .all_source_breakpoints(cx)
22795 });
22796
22797 assert_breakpoint(&breakpoints, &abs_path, vec![]);
22798
22799 editor.update_in(cx, |editor, window, cx| {
22800 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22801 editor.move_to_end(&MoveToEnd, window, cx);
22802 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22803 // Not adding a log message to a standard breakpoint shouldn't remove it
22804 add_log_breakpoint_at_cursor(editor, "", window, cx);
22805 });
22806
22807 let breakpoints = editor.update(cx, |editor, cx| {
22808 editor
22809 .breakpoint_store()
22810 .as_ref()
22811 .unwrap()
22812 .read(cx)
22813 .all_source_breakpoints(cx)
22814 });
22815
22816 assert_breakpoint(
22817 &breakpoints,
22818 &abs_path,
22819 vec![
22820 (0, Breakpoint::new_standard()),
22821 (3, Breakpoint::new_standard()),
22822 ],
22823 );
22824
22825 editor.update_in(cx, |editor, window, cx| {
22826 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
22827 });
22828
22829 let breakpoints = editor.update(cx, |editor, cx| {
22830 editor
22831 .breakpoint_store()
22832 .as_ref()
22833 .unwrap()
22834 .read(cx)
22835 .all_source_breakpoints(cx)
22836 });
22837
22838 assert_breakpoint(
22839 &breakpoints,
22840 &abs_path,
22841 vec![
22842 (0, Breakpoint::new_standard()),
22843 (3, Breakpoint::new_log("hello world")),
22844 ],
22845 );
22846
22847 editor.update_in(cx, |editor, window, cx| {
22848 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
22849 });
22850
22851 let breakpoints = editor.update(cx, |editor, cx| {
22852 editor
22853 .breakpoint_store()
22854 .as_ref()
22855 .unwrap()
22856 .read(cx)
22857 .all_source_breakpoints(cx)
22858 });
22859
22860 assert_breakpoint(
22861 &breakpoints,
22862 &abs_path,
22863 vec![
22864 (0, Breakpoint::new_standard()),
22865 (3, Breakpoint::new_log("hello Earth!!")),
22866 ],
22867 );
22868}
22869
22870/// This also tests that Editor::breakpoint_at_cursor_head is working properly
22871/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
22872/// or when breakpoints were placed out of order. This tests for a regression too
22873#[gpui::test]
22874async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
22875 init_test(cx, |_| {});
22876
22877 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22878 let fs = FakeFs::new(cx.executor());
22879 fs.insert_tree(
22880 path!("/a"),
22881 json!({
22882 "main.rs": sample_text,
22883 }),
22884 )
22885 .await;
22886 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22887 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22888 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22889
22890 let fs = FakeFs::new(cx.executor());
22891 fs.insert_tree(
22892 path!("/a"),
22893 json!({
22894 "main.rs": sample_text,
22895 }),
22896 )
22897 .await;
22898 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22899 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22900 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22901 let worktree_id = workspace
22902 .update(cx, |workspace, _window, cx| {
22903 workspace.project().update(cx, |project, cx| {
22904 project.worktrees(cx).next().unwrap().read(cx).id()
22905 })
22906 })
22907 .unwrap();
22908
22909 let buffer = project
22910 .update(cx, |project, cx| {
22911 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22912 })
22913 .await
22914 .unwrap();
22915
22916 let (editor, cx) = cx.add_window_view(|window, cx| {
22917 Editor::new(
22918 EditorMode::full(),
22919 MultiBuffer::build_from_buffer(buffer, cx),
22920 Some(project.clone()),
22921 window,
22922 cx,
22923 )
22924 });
22925
22926 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22927 let abs_path = project.read_with(cx, |project, cx| {
22928 project
22929 .absolute_path(&project_path, cx)
22930 .map(Arc::from)
22931 .unwrap()
22932 });
22933
22934 // assert we can add breakpoint on the first line
22935 editor.update_in(cx, |editor, window, cx| {
22936 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22937 editor.move_to_end(&MoveToEnd, window, cx);
22938 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22939 editor.move_up(&MoveUp, window, cx);
22940 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22941 });
22942
22943 let breakpoints = editor.update(cx, |editor, cx| {
22944 editor
22945 .breakpoint_store()
22946 .as_ref()
22947 .unwrap()
22948 .read(cx)
22949 .all_source_breakpoints(cx)
22950 });
22951
22952 assert_eq!(1, breakpoints.len());
22953 assert_breakpoint(
22954 &breakpoints,
22955 &abs_path,
22956 vec![
22957 (0, Breakpoint::new_standard()),
22958 (2, Breakpoint::new_standard()),
22959 (3, Breakpoint::new_standard()),
22960 ],
22961 );
22962
22963 editor.update_in(cx, |editor, window, cx| {
22964 editor.move_to_beginning(&MoveToBeginning, window, cx);
22965 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22966 editor.move_to_end(&MoveToEnd, window, cx);
22967 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22968 // Disabling a breakpoint that doesn't exist should do nothing
22969 editor.move_up(&MoveUp, window, cx);
22970 editor.move_up(&MoveUp, window, cx);
22971 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22972 });
22973
22974 let breakpoints = editor.update(cx, |editor, cx| {
22975 editor
22976 .breakpoint_store()
22977 .as_ref()
22978 .unwrap()
22979 .read(cx)
22980 .all_source_breakpoints(cx)
22981 });
22982
22983 let disable_breakpoint = {
22984 let mut bp = Breakpoint::new_standard();
22985 bp.state = BreakpointState::Disabled;
22986 bp
22987 };
22988
22989 assert_eq!(1, breakpoints.len());
22990 assert_breakpoint(
22991 &breakpoints,
22992 &abs_path,
22993 vec![
22994 (0, disable_breakpoint.clone()),
22995 (2, Breakpoint::new_standard()),
22996 (3, disable_breakpoint.clone()),
22997 ],
22998 );
22999
23000 editor.update_in(cx, |editor, window, cx| {
23001 editor.move_to_beginning(&MoveToBeginning, window, cx);
23002 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
23003 editor.move_to_end(&MoveToEnd, window, cx);
23004 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
23005 editor.move_up(&MoveUp, window, cx);
23006 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23007 });
23008
23009 let breakpoints = editor.update(cx, |editor, cx| {
23010 editor
23011 .breakpoint_store()
23012 .as_ref()
23013 .unwrap()
23014 .read(cx)
23015 .all_source_breakpoints(cx)
23016 });
23017
23018 assert_eq!(1, breakpoints.len());
23019 assert_breakpoint(
23020 &breakpoints,
23021 &abs_path,
23022 vec![
23023 (0, Breakpoint::new_standard()),
23024 (2, disable_breakpoint),
23025 (3, Breakpoint::new_standard()),
23026 ],
23027 );
23028}
23029
23030#[gpui::test]
23031async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
23032 init_test(cx, |_| {});
23033 let capabilities = lsp::ServerCapabilities {
23034 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
23035 prepare_provider: Some(true),
23036 work_done_progress_options: Default::default(),
23037 })),
23038 ..Default::default()
23039 };
23040 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
23041
23042 cx.set_state(indoc! {"
23043 struct Fˇoo {}
23044 "});
23045
23046 cx.update_editor(|editor, _, cx| {
23047 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
23048 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
23049 editor.highlight_background::<DocumentHighlightRead>(
23050 &[highlight_range],
23051 |theme| theme.colors().editor_document_highlight_read_background,
23052 cx,
23053 );
23054 });
23055
23056 let mut prepare_rename_handler = cx
23057 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
23058 move |_, _, _| async move {
23059 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
23060 start: lsp::Position {
23061 line: 0,
23062 character: 7,
23063 },
23064 end: lsp::Position {
23065 line: 0,
23066 character: 10,
23067 },
23068 })))
23069 },
23070 );
23071 let prepare_rename_task = cx
23072 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23073 .expect("Prepare rename was not started");
23074 prepare_rename_handler.next().await.unwrap();
23075 prepare_rename_task.await.expect("Prepare rename failed");
23076
23077 let mut rename_handler =
23078 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23079 let edit = lsp::TextEdit {
23080 range: lsp::Range {
23081 start: lsp::Position {
23082 line: 0,
23083 character: 7,
23084 },
23085 end: lsp::Position {
23086 line: 0,
23087 character: 10,
23088 },
23089 },
23090 new_text: "FooRenamed".to_string(),
23091 };
23092 Ok(Some(lsp::WorkspaceEdit::new(
23093 // Specify the same edit twice
23094 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
23095 )))
23096 });
23097 let rename_task = cx
23098 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23099 .expect("Confirm rename was not started");
23100 rename_handler.next().await.unwrap();
23101 rename_task.await.expect("Confirm rename failed");
23102 cx.run_until_parked();
23103
23104 // Despite two edits, only one is actually applied as those are identical
23105 cx.assert_editor_state(indoc! {"
23106 struct FooRenamedˇ {}
23107 "});
23108}
23109
23110#[gpui::test]
23111async fn test_rename_without_prepare(cx: &mut TestAppContext) {
23112 init_test(cx, |_| {});
23113 // These capabilities indicate that the server does not support prepare rename.
23114 let capabilities = lsp::ServerCapabilities {
23115 rename_provider: Some(lsp::OneOf::Left(true)),
23116 ..Default::default()
23117 };
23118 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
23119
23120 cx.set_state(indoc! {"
23121 struct Fˇoo {}
23122 "});
23123
23124 cx.update_editor(|editor, _window, cx| {
23125 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
23126 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
23127 editor.highlight_background::<DocumentHighlightRead>(
23128 &[highlight_range],
23129 |theme| theme.colors().editor_document_highlight_read_background,
23130 cx,
23131 );
23132 });
23133
23134 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23135 .expect("Prepare rename was not started")
23136 .await
23137 .expect("Prepare rename failed");
23138
23139 let mut rename_handler =
23140 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23141 let edit = lsp::TextEdit {
23142 range: lsp::Range {
23143 start: lsp::Position {
23144 line: 0,
23145 character: 7,
23146 },
23147 end: lsp::Position {
23148 line: 0,
23149 character: 10,
23150 },
23151 },
23152 new_text: "FooRenamed".to_string(),
23153 };
23154 Ok(Some(lsp::WorkspaceEdit::new(
23155 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
23156 )))
23157 });
23158 let rename_task = cx
23159 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23160 .expect("Confirm rename was not started");
23161 rename_handler.next().await.unwrap();
23162 rename_task.await.expect("Confirm rename failed");
23163 cx.run_until_parked();
23164
23165 // Correct range is renamed, as `surrounding_word` is used to find it.
23166 cx.assert_editor_state(indoc! {"
23167 struct FooRenamedˇ {}
23168 "});
23169}
23170
23171#[gpui::test]
23172async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
23173 init_test(cx, |_| {});
23174 let mut cx = EditorTestContext::new(cx).await;
23175
23176 let language = Arc::new(
23177 Language::new(
23178 LanguageConfig::default(),
23179 Some(tree_sitter_html::LANGUAGE.into()),
23180 )
23181 .with_brackets_query(
23182 r#"
23183 ("<" @open "/>" @close)
23184 ("</" @open ">" @close)
23185 ("<" @open ">" @close)
23186 ("\"" @open "\"" @close)
23187 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
23188 "#,
23189 )
23190 .unwrap(),
23191 );
23192 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23193
23194 cx.set_state(indoc! {"
23195 <span>ˇ</span>
23196 "});
23197 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23198 cx.assert_editor_state(indoc! {"
23199 <span>
23200 ˇ
23201 </span>
23202 "});
23203
23204 cx.set_state(indoc! {"
23205 <span><span></span>ˇ</span>
23206 "});
23207 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23208 cx.assert_editor_state(indoc! {"
23209 <span><span></span>
23210 ˇ</span>
23211 "});
23212
23213 cx.set_state(indoc! {"
23214 <span>ˇ
23215 </span>
23216 "});
23217 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23218 cx.assert_editor_state(indoc! {"
23219 <span>
23220 ˇ
23221 </span>
23222 "});
23223}
23224
23225#[gpui::test(iterations = 10)]
23226async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
23227 init_test(cx, |_| {});
23228
23229 let fs = FakeFs::new(cx.executor());
23230 fs.insert_tree(
23231 path!("/dir"),
23232 json!({
23233 "a.ts": "a",
23234 }),
23235 )
23236 .await;
23237
23238 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
23239 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23240 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23241
23242 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23243 language_registry.add(Arc::new(Language::new(
23244 LanguageConfig {
23245 name: "TypeScript".into(),
23246 matcher: LanguageMatcher {
23247 path_suffixes: vec!["ts".to_string()],
23248 ..Default::default()
23249 },
23250 ..Default::default()
23251 },
23252 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
23253 )));
23254 let mut fake_language_servers = language_registry.register_fake_lsp(
23255 "TypeScript",
23256 FakeLspAdapter {
23257 capabilities: lsp::ServerCapabilities {
23258 code_lens_provider: Some(lsp::CodeLensOptions {
23259 resolve_provider: Some(true),
23260 }),
23261 execute_command_provider: Some(lsp::ExecuteCommandOptions {
23262 commands: vec!["_the/command".to_string()],
23263 ..lsp::ExecuteCommandOptions::default()
23264 }),
23265 ..lsp::ServerCapabilities::default()
23266 },
23267 ..FakeLspAdapter::default()
23268 },
23269 );
23270
23271 let editor = workspace
23272 .update(cx, |workspace, window, cx| {
23273 workspace.open_abs_path(
23274 PathBuf::from(path!("/dir/a.ts")),
23275 OpenOptions::default(),
23276 window,
23277 cx,
23278 )
23279 })
23280 .unwrap()
23281 .await
23282 .unwrap()
23283 .downcast::<Editor>()
23284 .unwrap();
23285 cx.executor().run_until_parked();
23286
23287 let fake_server = fake_language_servers.next().await.unwrap();
23288
23289 let buffer = editor.update(cx, |editor, cx| {
23290 editor
23291 .buffer()
23292 .read(cx)
23293 .as_singleton()
23294 .expect("have opened a single file by path")
23295 });
23296
23297 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
23298 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
23299 drop(buffer_snapshot);
23300 let actions = cx
23301 .update_window(*workspace, |_, window, cx| {
23302 project.code_actions(&buffer, anchor..anchor, window, cx)
23303 })
23304 .unwrap();
23305
23306 fake_server
23307 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23308 Ok(Some(vec![
23309 lsp::CodeLens {
23310 range: lsp::Range::default(),
23311 command: Some(lsp::Command {
23312 title: "Code lens command".to_owned(),
23313 command: "_the/command".to_owned(),
23314 arguments: None,
23315 }),
23316 data: None,
23317 },
23318 lsp::CodeLens {
23319 range: lsp::Range::default(),
23320 command: Some(lsp::Command {
23321 title: "Command not in capabilities".to_owned(),
23322 command: "not in capabilities".to_owned(),
23323 arguments: None,
23324 }),
23325 data: None,
23326 },
23327 lsp::CodeLens {
23328 range: lsp::Range {
23329 start: lsp::Position {
23330 line: 1,
23331 character: 1,
23332 },
23333 end: lsp::Position {
23334 line: 1,
23335 character: 1,
23336 },
23337 },
23338 command: Some(lsp::Command {
23339 title: "Command not in range".to_owned(),
23340 command: "_the/command".to_owned(),
23341 arguments: None,
23342 }),
23343 data: None,
23344 },
23345 ]))
23346 })
23347 .next()
23348 .await;
23349
23350 let actions = actions.await.unwrap();
23351 assert_eq!(
23352 actions.len(),
23353 1,
23354 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
23355 );
23356 let action = actions[0].clone();
23357 let apply = project.update(cx, |project, cx| {
23358 project.apply_code_action(buffer.clone(), action, true, cx)
23359 });
23360
23361 // Resolving the code action does not populate its edits. In absence of
23362 // edits, we must execute the given command.
23363 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
23364 |mut lens, _| async move {
23365 let lens_command = lens.command.as_mut().expect("should have a command");
23366 assert_eq!(lens_command.title, "Code lens command");
23367 lens_command.arguments = Some(vec![json!("the-argument")]);
23368 Ok(lens)
23369 },
23370 );
23371
23372 // While executing the command, the language server sends the editor
23373 // a `workspaceEdit` request.
23374 fake_server
23375 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
23376 let fake = fake_server.clone();
23377 move |params, _| {
23378 assert_eq!(params.command, "_the/command");
23379 let fake = fake.clone();
23380 async move {
23381 fake.server
23382 .request::<lsp::request::ApplyWorkspaceEdit>(
23383 lsp::ApplyWorkspaceEditParams {
23384 label: None,
23385 edit: lsp::WorkspaceEdit {
23386 changes: Some(
23387 [(
23388 lsp::Uri::from_file_path(path!("/dir/a.ts")).unwrap(),
23389 vec![lsp::TextEdit {
23390 range: lsp::Range::new(
23391 lsp::Position::new(0, 0),
23392 lsp::Position::new(0, 0),
23393 ),
23394 new_text: "X".into(),
23395 }],
23396 )]
23397 .into_iter()
23398 .collect(),
23399 ),
23400 ..lsp::WorkspaceEdit::default()
23401 },
23402 },
23403 )
23404 .await
23405 .into_response()
23406 .unwrap();
23407 Ok(Some(json!(null)))
23408 }
23409 }
23410 })
23411 .next()
23412 .await;
23413
23414 // Applying the code lens command returns a project transaction containing the edits
23415 // sent by the language server in its `workspaceEdit` request.
23416 let transaction = apply.await.unwrap();
23417 assert!(transaction.0.contains_key(&buffer));
23418 buffer.update(cx, |buffer, cx| {
23419 assert_eq!(buffer.text(), "Xa");
23420 buffer.undo(cx);
23421 assert_eq!(buffer.text(), "a");
23422 });
23423
23424 let actions_after_edits = cx
23425 .update_window(*workspace, |_, window, cx| {
23426 project.code_actions(&buffer, anchor..anchor, window, cx)
23427 })
23428 .unwrap()
23429 .await
23430 .unwrap();
23431 assert_eq!(
23432 actions, actions_after_edits,
23433 "For the same selection, same code lens actions should be returned"
23434 );
23435
23436 let _responses =
23437 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23438 panic!("No more code lens requests are expected");
23439 });
23440 editor.update_in(cx, |editor, window, cx| {
23441 editor.select_all(&SelectAll, window, cx);
23442 });
23443 cx.executor().run_until_parked();
23444 let new_actions = cx
23445 .update_window(*workspace, |_, window, cx| {
23446 project.code_actions(&buffer, anchor..anchor, window, cx)
23447 })
23448 .unwrap()
23449 .await
23450 .unwrap();
23451 assert_eq!(
23452 actions, new_actions,
23453 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
23454 );
23455}
23456
23457#[gpui::test]
23458async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
23459 init_test(cx, |_| {});
23460
23461 let fs = FakeFs::new(cx.executor());
23462 let main_text = r#"fn main() {
23463println!("1");
23464println!("2");
23465println!("3");
23466println!("4");
23467println!("5");
23468}"#;
23469 let lib_text = "mod foo {}";
23470 fs.insert_tree(
23471 path!("/a"),
23472 json!({
23473 "lib.rs": lib_text,
23474 "main.rs": main_text,
23475 }),
23476 )
23477 .await;
23478
23479 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23480 let (workspace, cx) =
23481 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23482 let worktree_id = workspace.update(cx, |workspace, cx| {
23483 workspace.project().update(cx, |project, cx| {
23484 project.worktrees(cx).next().unwrap().read(cx).id()
23485 })
23486 });
23487
23488 let expected_ranges = vec![
23489 Point::new(0, 0)..Point::new(0, 0),
23490 Point::new(1, 0)..Point::new(1, 1),
23491 Point::new(2, 0)..Point::new(2, 2),
23492 Point::new(3, 0)..Point::new(3, 3),
23493 ];
23494
23495 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23496 let editor_1 = workspace
23497 .update_in(cx, |workspace, window, cx| {
23498 workspace.open_path(
23499 (worktree_id, rel_path("main.rs")),
23500 Some(pane_1.downgrade()),
23501 true,
23502 window,
23503 cx,
23504 )
23505 })
23506 .unwrap()
23507 .await
23508 .downcast::<Editor>()
23509 .unwrap();
23510 pane_1.update(cx, |pane, cx| {
23511 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23512 open_editor.update(cx, |editor, cx| {
23513 assert_eq!(
23514 editor.display_text(cx),
23515 main_text,
23516 "Original main.rs text on initial open",
23517 );
23518 assert_eq!(
23519 editor
23520 .selections
23521 .all::<Point>(cx)
23522 .into_iter()
23523 .map(|s| s.range())
23524 .collect::<Vec<_>>(),
23525 vec![Point::zero()..Point::zero()],
23526 "Default selections on initial open",
23527 );
23528 })
23529 });
23530 editor_1.update_in(cx, |editor, window, cx| {
23531 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23532 s.select_ranges(expected_ranges.clone());
23533 });
23534 });
23535
23536 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
23537 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
23538 });
23539 let editor_2 = workspace
23540 .update_in(cx, |workspace, window, cx| {
23541 workspace.open_path(
23542 (worktree_id, rel_path("main.rs")),
23543 Some(pane_2.downgrade()),
23544 true,
23545 window,
23546 cx,
23547 )
23548 })
23549 .unwrap()
23550 .await
23551 .downcast::<Editor>()
23552 .unwrap();
23553 pane_2.update(cx, |pane, cx| {
23554 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23555 open_editor.update(cx, |editor, cx| {
23556 assert_eq!(
23557 editor.display_text(cx),
23558 main_text,
23559 "Original main.rs text on initial open in another panel",
23560 );
23561 assert_eq!(
23562 editor
23563 .selections
23564 .all::<Point>(cx)
23565 .into_iter()
23566 .map(|s| s.range())
23567 .collect::<Vec<_>>(),
23568 vec![Point::zero()..Point::zero()],
23569 "Default selections on initial open in another panel",
23570 );
23571 })
23572 });
23573
23574 editor_2.update_in(cx, |editor, window, cx| {
23575 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
23576 });
23577
23578 let _other_editor_1 = workspace
23579 .update_in(cx, |workspace, window, cx| {
23580 workspace.open_path(
23581 (worktree_id, rel_path("lib.rs")),
23582 Some(pane_1.downgrade()),
23583 true,
23584 window,
23585 cx,
23586 )
23587 })
23588 .unwrap()
23589 .await
23590 .downcast::<Editor>()
23591 .unwrap();
23592 pane_1
23593 .update_in(cx, |pane, window, cx| {
23594 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23595 })
23596 .await
23597 .unwrap();
23598 drop(editor_1);
23599 pane_1.update(cx, |pane, cx| {
23600 pane.active_item()
23601 .unwrap()
23602 .downcast::<Editor>()
23603 .unwrap()
23604 .update(cx, |editor, cx| {
23605 assert_eq!(
23606 editor.display_text(cx),
23607 lib_text,
23608 "Other file should be open and active",
23609 );
23610 });
23611 assert_eq!(pane.items().count(), 1, "No other editors should be open");
23612 });
23613
23614 let _other_editor_2 = workspace
23615 .update_in(cx, |workspace, window, cx| {
23616 workspace.open_path(
23617 (worktree_id, rel_path("lib.rs")),
23618 Some(pane_2.downgrade()),
23619 true,
23620 window,
23621 cx,
23622 )
23623 })
23624 .unwrap()
23625 .await
23626 .downcast::<Editor>()
23627 .unwrap();
23628 pane_2
23629 .update_in(cx, |pane, window, cx| {
23630 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23631 })
23632 .await
23633 .unwrap();
23634 drop(editor_2);
23635 pane_2.update(cx, |pane, cx| {
23636 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23637 open_editor.update(cx, |editor, cx| {
23638 assert_eq!(
23639 editor.display_text(cx),
23640 lib_text,
23641 "Other file should be open and active in another panel too",
23642 );
23643 });
23644 assert_eq!(
23645 pane.items().count(),
23646 1,
23647 "No other editors should be open in another pane",
23648 );
23649 });
23650
23651 let _editor_1_reopened = workspace
23652 .update_in(cx, |workspace, window, cx| {
23653 workspace.open_path(
23654 (worktree_id, rel_path("main.rs")),
23655 Some(pane_1.downgrade()),
23656 true,
23657 window,
23658 cx,
23659 )
23660 })
23661 .unwrap()
23662 .await
23663 .downcast::<Editor>()
23664 .unwrap();
23665 let _editor_2_reopened = workspace
23666 .update_in(cx, |workspace, window, cx| {
23667 workspace.open_path(
23668 (worktree_id, rel_path("main.rs")),
23669 Some(pane_2.downgrade()),
23670 true,
23671 window,
23672 cx,
23673 )
23674 })
23675 .unwrap()
23676 .await
23677 .downcast::<Editor>()
23678 .unwrap();
23679 pane_1.update(cx, |pane, cx| {
23680 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23681 open_editor.update(cx, |editor, cx| {
23682 assert_eq!(
23683 editor.display_text(cx),
23684 main_text,
23685 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
23686 );
23687 assert_eq!(
23688 editor
23689 .selections
23690 .all::<Point>(cx)
23691 .into_iter()
23692 .map(|s| s.range())
23693 .collect::<Vec<_>>(),
23694 expected_ranges,
23695 "Previous editor in the 1st panel had selections and should get them restored on reopen",
23696 );
23697 })
23698 });
23699 pane_2.update(cx, |pane, cx| {
23700 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23701 open_editor.update(cx, |editor, cx| {
23702 assert_eq!(
23703 editor.display_text(cx),
23704 r#"fn main() {
23705⋯rintln!("1");
23706⋯intln!("2");
23707⋯ntln!("3");
23708println!("4");
23709println!("5");
23710}"#,
23711 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
23712 );
23713 assert_eq!(
23714 editor
23715 .selections
23716 .all::<Point>(cx)
23717 .into_iter()
23718 .map(|s| s.range())
23719 .collect::<Vec<_>>(),
23720 vec![Point::zero()..Point::zero()],
23721 "Previous editor in the 2nd pane had no selections changed hence should restore none",
23722 );
23723 })
23724 });
23725}
23726
23727#[gpui::test]
23728async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
23729 init_test(cx, |_| {});
23730
23731 let fs = FakeFs::new(cx.executor());
23732 let main_text = r#"fn main() {
23733println!("1");
23734println!("2");
23735println!("3");
23736println!("4");
23737println!("5");
23738}"#;
23739 let lib_text = "mod foo {}";
23740 fs.insert_tree(
23741 path!("/a"),
23742 json!({
23743 "lib.rs": lib_text,
23744 "main.rs": main_text,
23745 }),
23746 )
23747 .await;
23748
23749 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23750 let (workspace, cx) =
23751 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23752 let worktree_id = workspace.update(cx, |workspace, cx| {
23753 workspace.project().update(cx, |project, cx| {
23754 project.worktrees(cx).next().unwrap().read(cx).id()
23755 })
23756 });
23757
23758 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23759 let editor = workspace
23760 .update_in(cx, |workspace, window, cx| {
23761 workspace.open_path(
23762 (worktree_id, rel_path("main.rs")),
23763 Some(pane.downgrade()),
23764 true,
23765 window,
23766 cx,
23767 )
23768 })
23769 .unwrap()
23770 .await
23771 .downcast::<Editor>()
23772 .unwrap();
23773 pane.update(cx, |pane, cx| {
23774 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23775 open_editor.update(cx, |editor, cx| {
23776 assert_eq!(
23777 editor.display_text(cx),
23778 main_text,
23779 "Original main.rs text on initial open",
23780 );
23781 })
23782 });
23783 editor.update_in(cx, |editor, window, cx| {
23784 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
23785 });
23786
23787 cx.update_global(|store: &mut SettingsStore, cx| {
23788 store.update_user_settings(cx, |s| {
23789 s.workspace.restore_on_file_reopen = Some(false);
23790 });
23791 });
23792 editor.update_in(cx, |editor, window, cx| {
23793 editor.fold_ranges(
23794 vec![
23795 Point::new(1, 0)..Point::new(1, 1),
23796 Point::new(2, 0)..Point::new(2, 2),
23797 Point::new(3, 0)..Point::new(3, 3),
23798 ],
23799 false,
23800 window,
23801 cx,
23802 );
23803 });
23804 pane.update_in(cx, |pane, window, cx| {
23805 pane.close_all_items(&CloseAllItems::default(), window, cx)
23806 })
23807 .await
23808 .unwrap();
23809 pane.update(cx, |pane, _| {
23810 assert!(pane.active_item().is_none());
23811 });
23812 cx.update_global(|store: &mut SettingsStore, cx| {
23813 store.update_user_settings(cx, |s| {
23814 s.workspace.restore_on_file_reopen = Some(true);
23815 });
23816 });
23817
23818 let _editor_reopened = workspace
23819 .update_in(cx, |workspace, window, cx| {
23820 workspace.open_path(
23821 (worktree_id, rel_path("main.rs")),
23822 Some(pane.downgrade()),
23823 true,
23824 window,
23825 cx,
23826 )
23827 })
23828 .unwrap()
23829 .await
23830 .downcast::<Editor>()
23831 .unwrap();
23832 pane.update(cx, |pane, cx| {
23833 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23834 open_editor.update(cx, |editor, cx| {
23835 assert_eq!(
23836 editor.display_text(cx),
23837 main_text,
23838 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
23839 );
23840 })
23841 });
23842}
23843
23844#[gpui::test]
23845async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
23846 struct EmptyModalView {
23847 focus_handle: gpui::FocusHandle,
23848 }
23849 impl EventEmitter<DismissEvent> for EmptyModalView {}
23850 impl Render for EmptyModalView {
23851 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
23852 div()
23853 }
23854 }
23855 impl Focusable for EmptyModalView {
23856 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
23857 self.focus_handle.clone()
23858 }
23859 }
23860 impl workspace::ModalView for EmptyModalView {}
23861 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
23862 EmptyModalView {
23863 focus_handle: cx.focus_handle(),
23864 }
23865 }
23866
23867 init_test(cx, |_| {});
23868
23869 let fs = FakeFs::new(cx.executor());
23870 let project = Project::test(fs, [], cx).await;
23871 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23872 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
23873 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23874 let editor = cx.new_window_entity(|window, cx| {
23875 Editor::new(
23876 EditorMode::full(),
23877 buffer,
23878 Some(project.clone()),
23879 window,
23880 cx,
23881 )
23882 });
23883 workspace
23884 .update(cx, |workspace, window, cx| {
23885 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
23886 })
23887 .unwrap();
23888 editor.update_in(cx, |editor, window, cx| {
23889 editor.open_context_menu(&OpenContextMenu, window, cx);
23890 assert!(editor.mouse_context_menu.is_some());
23891 });
23892 workspace
23893 .update(cx, |workspace, window, cx| {
23894 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
23895 })
23896 .unwrap();
23897 cx.read(|cx| {
23898 assert!(editor.read(cx).mouse_context_menu.is_none());
23899 });
23900}
23901
23902fn set_linked_edit_ranges(
23903 opening: (Point, Point),
23904 closing: (Point, Point),
23905 editor: &mut Editor,
23906 cx: &mut Context<Editor>,
23907) {
23908 let Some((buffer, _)) = editor
23909 .buffer
23910 .read(cx)
23911 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
23912 else {
23913 panic!("Failed to get buffer for selection position");
23914 };
23915 let buffer = buffer.read(cx);
23916 let buffer_id = buffer.remote_id();
23917 let opening_range = buffer.anchor_before(opening.0)..buffer.anchor_after(opening.1);
23918 let closing_range = buffer.anchor_before(closing.0)..buffer.anchor_after(closing.1);
23919 let mut linked_ranges = HashMap::default();
23920 linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
23921 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
23922}
23923
23924#[gpui::test]
23925async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
23926 init_test(cx, |_| {});
23927
23928 let fs = FakeFs::new(cx.executor());
23929 fs.insert_file(path!("/file.html"), Default::default())
23930 .await;
23931
23932 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
23933
23934 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23935 let html_language = Arc::new(Language::new(
23936 LanguageConfig {
23937 name: "HTML".into(),
23938 matcher: LanguageMatcher {
23939 path_suffixes: vec!["html".to_string()],
23940 ..LanguageMatcher::default()
23941 },
23942 brackets: BracketPairConfig {
23943 pairs: vec![BracketPair {
23944 start: "<".into(),
23945 end: ">".into(),
23946 close: true,
23947 ..Default::default()
23948 }],
23949 ..Default::default()
23950 },
23951 ..Default::default()
23952 },
23953 Some(tree_sitter_html::LANGUAGE.into()),
23954 ));
23955 language_registry.add(html_language);
23956 let mut fake_servers = language_registry.register_fake_lsp(
23957 "HTML",
23958 FakeLspAdapter {
23959 capabilities: lsp::ServerCapabilities {
23960 completion_provider: Some(lsp::CompletionOptions {
23961 resolve_provider: Some(true),
23962 ..Default::default()
23963 }),
23964 ..Default::default()
23965 },
23966 ..Default::default()
23967 },
23968 );
23969
23970 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23971 let cx = &mut VisualTestContext::from_window(*workspace, cx);
23972
23973 let worktree_id = workspace
23974 .update(cx, |workspace, _window, cx| {
23975 workspace.project().update(cx, |project, cx| {
23976 project.worktrees(cx).next().unwrap().read(cx).id()
23977 })
23978 })
23979 .unwrap();
23980 project
23981 .update(cx, |project, cx| {
23982 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
23983 })
23984 .await
23985 .unwrap();
23986 let editor = workspace
23987 .update(cx, |workspace, window, cx| {
23988 workspace.open_path((worktree_id, rel_path("file.html")), None, true, window, cx)
23989 })
23990 .unwrap()
23991 .await
23992 .unwrap()
23993 .downcast::<Editor>()
23994 .unwrap();
23995
23996 let fake_server = fake_servers.next().await.unwrap();
23997 editor.update_in(cx, |editor, window, cx| {
23998 editor.set_text("<ad></ad>", window, cx);
23999 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
24000 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
24001 });
24002 set_linked_edit_ranges(
24003 (Point::new(0, 1), Point::new(0, 3)),
24004 (Point::new(0, 6), Point::new(0, 8)),
24005 editor,
24006 cx,
24007 );
24008 });
24009 let mut completion_handle =
24010 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
24011 Ok(Some(lsp::CompletionResponse::Array(vec![
24012 lsp::CompletionItem {
24013 label: "head".to_string(),
24014 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
24015 lsp::InsertReplaceEdit {
24016 new_text: "head".to_string(),
24017 insert: lsp::Range::new(
24018 lsp::Position::new(0, 1),
24019 lsp::Position::new(0, 3),
24020 ),
24021 replace: lsp::Range::new(
24022 lsp::Position::new(0, 1),
24023 lsp::Position::new(0, 3),
24024 ),
24025 },
24026 )),
24027 ..Default::default()
24028 },
24029 ])))
24030 });
24031 editor.update_in(cx, |editor, window, cx| {
24032 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
24033 });
24034 cx.run_until_parked();
24035 completion_handle.next().await.unwrap();
24036 editor.update(cx, |editor, _| {
24037 assert!(
24038 editor.context_menu_visible(),
24039 "Completion menu should be visible"
24040 );
24041 });
24042 editor.update_in(cx, |editor, window, cx| {
24043 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
24044 });
24045 cx.executor().run_until_parked();
24046 editor.update(cx, |editor, cx| {
24047 assert_eq!(editor.text(cx), "<head></head>");
24048 });
24049}
24050
24051#[gpui::test]
24052async fn test_linked_edits_on_typing_punctuation(cx: &mut TestAppContext) {
24053 init_test(cx, |_| {});
24054
24055 let mut cx = EditorTestContext::new(cx).await;
24056 let language = Arc::new(Language::new(
24057 LanguageConfig {
24058 name: "TSX".into(),
24059 matcher: LanguageMatcher {
24060 path_suffixes: vec!["tsx".to_string()],
24061 ..LanguageMatcher::default()
24062 },
24063 brackets: BracketPairConfig {
24064 pairs: vec![BracketPair {
24065 start: "<".into(),
24066 end: ">".into(),
24067 close: true,
24068 ..Default::default()
24069 }],
24070 ..Default::default()
24071 },
24072 linked_edit_characters: HashSet::from_iter(['.']),
24073 ..Default::default()
24074 },
24075 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
24076 ));
24077 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24078
24079 // Test typing > does not extend linked pair
24080 cx.set_state("<divˇ<div></div>");
24081 cx.update_editor(|editor, _, cx| {
24082 set_linked_edit_ranges(
24083 (Point::new(0, 1), Point::new(0, 4)),
24084 (Point::new(0, 11), Point::new(0, 14)),
24085 editor,
24086 cx,
24087 );
24088 });
24089 cx.update_editor(|editor, window, cx| {
24090 editor.handle_input(">", window, cx);
24091 });
24092 cx.assert_editor_state("<div>ˇ<div></div>");
24093
24094 // Test typing . do extend linked pair
24095 cx.set_state("<Animatedˇ></Animated>");
24096 cx.update_editor(|editor, _, cx| {
24097 set_linked_edit_ranges(
24098 (Point::new(0, 1), Point::new(0, 9)),
24099 (Point::new(0, 12), Point::new(0, 20)),
24100 editor,
24101 cx,
24102 );
24103 });
24104 cx.update_editor(|editor, window, cx| {
24105 editor.handle_input(".", window, cx);
24106 });
24107 cx.assert_editor_state("<Animated.ˇ></Animated.>");
24108 cx.update_editor(|editor, _, cx| {
24109 set_linked_edit_ranges(
24110 (Point::new(0, 1), Point::new(0, 10)),
24111 (Point::new(0, 13), Point::new(0, 21)),
24112 editor,
24113 cx,
24114 );
24115 });
24116 cx.update_editor(|editor, window, cx| {
24117 editor.handle_input("V", window, cx);
24118 });
24119 cx.assert_editor_state("<Animated.Vˇ></Animated.V>");
24120}
24121
24122#[gpui::test]
24123async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
24124 init_test(cx, |_| {});
24125
24126 let fs = FakeFs::new(cx.executor());
24127 fs.insert_tree(
24128 path!("/root"),
24129 json!({
24130 "a": {
24131 "main.rs": "fn main() {}",
24132 },
24133 "foo": {
24134 "bar": {
24135 "external_file.rs": "pub mod external {}",
24136 }
24137 }
24138 }),
24139 )
24140 .await;
24141
24142 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
24143 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24144 language_registry.add(rust_lang());
24145 let _fake_servers = language_registry.register_fake_lsp(
24146 "Rust",
24147 FakeLspAdapter {
24148 ..FakeLspAdapter::default()
24149 },
24150 );
24151 let (workspace, cx) =
24152 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24153 let worktree_id = workspace.update(cx, |workspace, cx| {
24154 workspace.project().update(cx, |project, cx| {
24155 project.worktrees(cx).next().unwrap().read(cx).id()
24156 })
24157 });
24158
24159 let assert_language_servers_count =
24160 |expected: usize, context: &str, cx: &mut VisualTestContext| {
24161 project.update(cx, |project, cx| {
24162 let current = project
24163 .lsp_store()
24164 .read(cx)
24165 .as_local()
24166 .unwrap()
24167 .language_servers
24168 .len();
24169 assert_eq!(expected, current, "{context}");
24170 });
24171 };
24172
24173 assert_language_servers_count(
24174 0,
24175 "No servers should be running before any file is open",
24176 cx,
24177 );
24178 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
24179 let main_editor = workspace
24180 .update_in(cx, |workspace, window, cx| {
24181 workspace.open_path(
24182 (worktree_id, rel_path("main.rs")),
24183 Some(pane.downgrade()),
24184 true,
24185 window,
24186 cx,
24187 )
24188 })
24189 .unwrap()
24190 .await
24191 .downcast::<Editor>()
24192 .unwrap();
24193 pane.update(cx, |pane, cx| {
24194 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24195 open_editor.update(cx, |editor, cx| {
24196 assert_eq!(
24197 editor.display_text(cx),
24198 "fn main() {}",
24199 "Original main.rs text on initial open",
24200 );
24201 });
24202 assert_eq!(open_editor, main_editor);
24203 });
24204 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
24205
24206 let external_editor = workspace
24207 .update_in(cx, |workspace, window, cx| {
24208 workspace.open_abs_path(
24209 PathBuf::from("/root/foo/bar/external_file.rs"),
24210 OpenOptions::default(),
24211 window,
24212 cx,
24213 )
24214 })
24215 .await
24216 .expect("opening external file")
24217 .downcast::<Editor>()
24218 .expect("downcasted external file's open element to editor");
24219 pane.update(cx, |pane, cx| {
24220 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24221 open_editor.update(cx, |editor, cx| {
24222 assert_eq!(
24223 editor.display_text(cx),
24224 "pub mod external {}",
24225 "External file is open now",
24226 );
24227 });
24228 assert_eq!(open_editor, external_editor);
24229 });
24230 assert_language_servers_count(
24231 1,
24232 "Second, external, *.rs file should join the existing server",
24233 cx,
24234 );
24235
24236 pane.update_in(cx, |pane, window, cx| {
24237 pane.close_active_item(&CloseActiveItem::default(), window, cx)
24238 })
24239 .await
24240 .unwrap();
24241 pane.update_in(cx, |pane, window, cx| {
24242 pane.navigate_backward(&Default::default(), window, cx);
24243 });
24244 cx.run_until_parked();
24245 pane.update(cx, |pane, cx| {
24246 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24247 open_editor.update(cx, |editor, cx| {
24248 assert_eq!(
24249 editor.display_text(cx),
24250 "pub mod external {}",
24251 "External file is open now",
24252 );
24253 });
24254 });
24255 assert_language_servers_count(
24256 1,
24257 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
24258 cx,
24259 );
24260
24261 cx.update(|_, cx| {
24262 workspace::reload(cx);
24263 });
24264 assert_language_servers_count(
24265 1,
24266 "After reloading the worktree with local and external files opened, only one project should be started",
24267 cx,
24268 );
24269}
24270
24271#[gpui::test]
24272async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
24273 init_test(cx, |_| {});
24274
24275 let mut cx = EditorTestContext::new(cx).await;
24276 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24277 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24278
24279 // test cursor move to start of each line on tab
24280 // for `if`, `elif`, `else`, `while`, `with` and `for`
24281 cx.set_state(indoc! {"
24282 def main():
24283 ˇ for item in items:
24284 ˇ while item.active:
24285 ˇ if item.value > 10:
24286 ˇ continue
24287 ˇ elif item.value < 0:
24288 ˇ break
24289 ˇ else:
24290 ˇ with item.context() as ctx:
24291 ˇ yield count
24292 ˇ else:
24293 ˇ log('while else')
24294 ˇ else:
24295 ˇ log('for else')
24296 "});
24297 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24298 cx.assert_editor_state(indoc! {"
24299 def main():
24300 ˇfor item in items:
24301 ˇwhile item.active:
24302 ˇif item.value > 10:
24303 ˇcontinue
24304 ˇelif item.value < 0:
24305 ˇbreak
24306 ˇelse:
24307 ˇwith item.context() as ctx:
24308 ˇyield count
24309 ˇelse:
24310 ˇlog('while else')
24311 ˇelse:
24312 ˇlog('for else')
24313 "});
24314 // test relative indent is preserved when tab
24315 // for `if`, `elif`, `else`, `while`, `with` and `for`
24316 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24317 cx.assert_editor_state(indoc! {"
24318 def main():
24319 ˇfor item in items:
24320 ˇwhile item.active:
24321 ˇif item.value > 10:
24322 ˇcontinue
24323 ˇelif item.value < 0:
24324 ˇbreak
24325 ˇelse:
24326 ˇwith item.context() as ctx:
24327 ˇyield count
24328 ˇelse:
24329 ˇlog('while else')
24330 ˇelse:
24331 ˇlog('for else')
24332 "});
24333
24334 // test cursor move to start of each line on tab
24335 // for `try`, `except`, `else`, `finally`, `match` and `def`
24336 cx.set_state(indoc! {"
24337 def main():
24338 ˇ try:
24339 ˇ fetch()
24340 ˇ except ValueError:
24341 ˇ handle_error()
24342 ˇ else:
24343 ˇ match value:
24344 ˇ case _:
24345 ˇ finally:
24346 ˇ def status():
24347 ˇ return 0
24348 "});
24349 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24350 cx.assert_editor_state(indoc! {"
24351 def main():
24352 ˇtry:
24353 ˇfetch()
24354 ˇexcept ValueError:
24355 ˇhandle_error()
24356 ˇelse:
24357 ˇmatch value:
24358 ˇcase _:
24359 ˇfinally:
24360 ˇdef status():
24361 ˇreturn 0
24362 "});
24363 // test relative indent is preserved when tab
24364 // for `try`, `except`, `else`, `finally`, `match` and `def`
24365 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24366 cx.assert_editor_state(indoc! {"
24367 def main():
24368 ˇtry:
24369 ˇfetch()
24370 ˇexcept ValueError:
24371 ˇhandle_error()
24372 ˇelse:
24373 ˇmatch value:
24374 ˇcase _:
24375 ˇfinally:
24376 ˇdef status():
24377 ˇreturn 0
24378 "});
24379}
24380
24381#[gpui::test]
24382async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
24383 init_test(cx, |_| {});
24384
24385 let mut cx = EditorTestContext::new(cx).await;
24386 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24387 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24388
24389 // test `else` auto outdents when typed inside `if` block
24390 cx.set_state(indoc! {"
24391 def main():
24392 if i == 2:
24393 return
24394 ˇ
24395 "});
24396 cx.update_editor(|editor, window, cx| {
24397 editor.handle_input("else:", window, cx);
24398 });
24399 cx.assert_editor_state(indoc! {"
24400 def main():
24401 if i == 2:
24402 return
24403 else:ˇ
24404 "});
24405
24406 // test `except` auto outdents when typed inside `try` block
24407 cx.set_state(indoc! {"
24408 def main():
24409 try:
24410 i = 2
24411 ˇ
24412 "});
24413 cx.update_editor(|editor, window, cx| {
24414 editor.handle_input("except:", window, cx);
24415 });
24416 cx.assert_editor_state(indoc! {"
24417 def main():
24418 try:
24419 i = 2
24420 except:ˇ
24421 "});
24422
24423 // test `else` auto outdents when typed inside `except` block
24424 cx.set_state(indoc! {"
24425 def main():
24426 try:
24427 i = 2
24428 except:
24429 j = 2
24430 ˇ
24431 "});
24432 cx.update_editor(|editor, window, cx| {
24433 editor.handle_input("else:", window, cx);
24434 });
24435 cx.assert_editor_state(indoc! {"
24436 def main():
24437 try:
24438 i = 2
24439 except:
24440 j = 2
24441 else:ˇ
24442 "});
24443
24444 // test `finally` auto outdents when typed inside `else` block
24445 cx.set_state(indoc! {"
24446 def main():
24447 try:
24448 i = 2
24449 except:
24450 j = 2
24451 else:
24452 k = 2
24453 ˇ
24454 "});
24455 cx.update_editor(|editor, window, cx| {
24456 editor.handle_input("finally:", window, cx);
24457 });
24458 cx.assert_editor_state(indoc! {"
24459 def main():
24460 try:
24461 i = 2
24462 except:
24463 j = 2
24464 else:
24465 k = 2
24466 finally:ˇ
24467 "});
24468
24469 // test `else` does not outdents when typed inside `except` block right after for block
24470 cx.set_state(indoc! {"
24471 def main():
24472 try:
24473 i = 2
24474 except:
24475 for i in range(n):
24476 pass
24477 ˇ
24478 "});
24479 cx.update_editor(|editor, window, cx| {
24480 editor.handle_input("else:", window, cx);
24481 });
24482 cx.assert_editor_state(indoc! {"
24483 def main():
24484 try:
24485 i = 2
24486 except:
24487 for i in range(n):
24488 pass
24489 else:ˇ
24490 "});
24491
24492 // test `finally` auto outdents when typed inside `else` block right after for block
24493 cx.set_state(indoc! {"
24494 def main():
24495 try:
24496 i = 2
24497 except:
24498 j = 2
24499 else:
24500 for i in range(n):
24501 pass
24502 ˇ
24503 "});
24504 cx.update_editor(|editor, window, cx| {
24505 editor.handle_input("finally:", window, cx);
24506 });
24507 cx.assert_editor_state(indoc! {"
24508 def main():
24509 try:
24510 i = 2
24511 except:
24512 j = 2
24513 else:
24514 for i in range(n):
24515 pass
24516 finally:ˇ
24517 "});
24518
24519 // test `except` outdents to inner "try" block
24520 cx.set_state(indoc! {"
24521 def main():
24522 try:
24523 i = 2
24524 if i == 2:
24525 try:
24526 i = 3
24527 ˇ
24528 "});
24529 cx.update_editor(|editor, window, cx| {
24530 editor.handle_input("except:", window, cx);
24531 });
24532 cx.assert_editor_state(indoc! {"
24533 def main():
24534 try:
24535 i = 2
24536 if i == 2:
24537 try:
24538 i = 3
24539 except:ˇ
24540 "});
24541
24542 // test `except` outdents to outer "try" block
24543 cx.set_state(indoc! {"
24544 def main():
24545 try:
24546 i = 2
24547 if i == 2:
24548 try:
24549 i = 3
24550 ˇ
24551 "});
24552 cx.update_editor(|editor, window, cx| {
24553 editor.handle_input("except:", window, cx);
24554 });
24555 cx.assert_editor_state(indoc! {"
24556 def main():
24557 try:
24558 i = 2
24559 if i == 2:
24560 try:
24561 i = 3
24562 except:ˇ
24563 "});
24564
24565 // test `else` stays at correct indent when typed after `for` block
24566 cx.set_state(indoc! {"
24567 def main():
24568 for i in range(10):
24569 if i == 3:
24570 break
24571 ˇ
24572 "});
24573 cx.update_editor(|editor, window, cx| {
24574 editor.handle_input("else:", window, cx);
24575 });
24576 cx.assert_editor_state(indoc! {"
24577 def main():
24578 for i in range(10):
24579 if i == 3:
24580 break
24581 else:ˇ
24582 "});
24583
24584 // test does not outdent on typing after line with square brackets
24585 cx.set_state(indoc! {"
24586 def f() -> list[str]:
24587 ˇ
24588 "});
24589 cx.update_editor(|editor, window, cx| {
24590 editor.handle_input("a", window, cx);
24591 });
24592 cx.assert_editor_state(indoc! {"
24593 def f() -> list[str]:
24594 aˇ
24595 "});
24596
24597 // test does not outdent on typing : after case keyword
24598 cx.set_state(indoc! {"
24599 match 1:
24600 caseˇ
24601 "});
24602 cx.update_editor(|editor, window, cx| {
24603 editor.handle_input(":", window, cx);
24604 });
24605 cx.assert_editor_state(indoc! {"
24606 match 1:
24607 case:ˇ
24608 "});
24609}
24610
24611#[gpui::test]
24612async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
24613 init_test(cx, |_| {});
24614 update_test_language_settings(cx, |settings| {
24615 settings.defaults.extend_comment_on_newline = Some(false);
24616 });
24617 let mut cx = EditorTestContext::new(cx).await;
24618 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24619 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24620
24621 // test correct indent after newline on comment
24622 cx.set_state(indoc! {"
24623 # COMMENT:ˇ
24624 "});
24625 cx.update_editor(|editor, window, cx| {
24626 editor.newline(&Newline, window, cx);
24627 });
24628 cx.assert_editor_state(indoc! {"
24629 # COMMENT:
24630 ˇ
24631 "});
24632
24633 // test correct indent after newline in brackets
24634 cx.set_state(indoc! {"
24635 {ˇ}
24636 "});
24637 cx.update_editor(|editor, window, cx| {
24638 editor.newline(&Newline, window, cx);
24639 });
24640 cx.run_until_parked();
24641 cx.assert_editor_state(indoc! {"
24642 {
24643 ˇ
24644 }
24645 "});
24646
24647 cx.set_state(indoc! {"
24648 (ˇ)
24649 "});
24650 cx.update_editor(|editor, window, cx| {
24651 editor.newline(&Newline, window, cx);
24652 });
24653 cx.run_until_parked();
24654 cx.assert_editor_state(indoc! {"
24655 (
24656 ˇ
24657 )
24658 "});
24659
24660 // do not indent after empty lists or dictionaries
24661 cx.set_state(indoc! {"
24662 a = []ˇ
24663 "});
24664 cx.update_editor(|editor, window, cx| {
24665 editor.newline(&Newline, window, cx);
24666 });
24667 cx.run_until_parked();
24668 cx.assert_editor_state(indoc! {"
24669 a = []
24670 ˇ
24671 "});
24672}
24673
24674#[gpui::test]
24675async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
24676 init_test(cx, |_| {});
24677
24678 let mut cx = EditorTestContext::new(cx).await;
24679 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24680 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24681
24682 // test cursor move to start of each line on tab
24683 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
24684 cx.set_state(indoc! {"
24685 function main() {
24686 ˇ for item in $items; do
24687 ˇ while [ -n \"$item\" ]; do
24688 ˇ if [ \"$value\" -gt 10 ]; then
24689 ˇ continue
24690 ˇ elif [ \"$value\" -lt 0 ]; then
24691 ˇ break
24692 ˇ else
24693 ˇ echo \"$item\"
24694 ˇ fi
24695 ˇ done
24696 ˇ done
24697 ˇ}
24698 "});
24699 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24700 cx.assert_editor_state(indoc! {"
24701 function main() {
24702 ˇfor item in $items; do
24703 ˇwhile [ -n \"$item\" ]; do
24704 ˇif [ \"$value\" -gt 10 ]; then
24705 ˇcontinue
24706 ˇelif [ \"$value\" -lt 0 ]; then
24707 ˇbreak
24708 ˇelse
24709 ˇecho \"$item\"
24710 ˇfi
24711 ˇdone
24712 ˇdone
24713 ˇ}
24714 "});
24715 // test relative indent is preserved when tab
24716 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24717 cx.assert_editor_state(indoc! {"
24718 function main() {
24719 ˇfor item in $items; do
24720 ˇwhile [ -n \"$item\" ]; do
24721 ˇif [ \"$value\" -gt 10 ]; then
24722 ˇcontinue
24723 ˇelif [ \"$value\" -lt 0 ]; then
24724 ˇbreak
24725 ˇelse
24726 ˇecho \"$item\"
24727 ˇfi
24728 ˇdone
24729 ˇdone
24730 ˇ}
24731 "});
24732
24733 // test cursor move to start of each line on tab
24734 // for `case` statement with patterns
24735 cx.set_state(indoc! {"
24736 function handle() {
24737 ˇ case \"$1\" in
24738 ˇ start)
24739 ˇ echo \"a\"
24740 ˇ ;;
24741 ˇ stop)
24742 ˇ echo \"b\"
24743 ˇ ;;
24744 ˇ *)
24745 ˇ echo \"c\"
24746 ˇ ;;
24747 ˇ esac
24748 ˇ}
24749 "});
24750 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24751 cx.assert_editor_state(indoc! {"
24752 function handle() {
24753 ˇcase \"$1\" in
24754 ˇstart)
24755 ˇecho \"a\"
24756 ˇ;;
24757 ˇstop)
24758 ˇecho \"b\"
24759 ˇ;;
24760 ˇ*)
24761 ˇecho \"c\"
24762 ˇ;;
24763 ˇesac
24764 ˇ}
24765 "});
24766}
24767
24768#[gpui::test]
24769async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
24770 init_test(cx, |_| {});
24771
24772 let mut cx = EditorTestContext::new(cx).await;
24773 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24774 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24775
24776 // test indents on comment insert
24777 cx.set_state(indoc! {"
24778 function main() {
24779 ˇ for item in $items; do
24780 ˇ while [ -n \"$item\" ]; do
24781 ˇ if [ \"$value\" -gt 10 ]; then
24782 ˇ continue
24783 ˇ elif [ \"$value\" -lt 0 ]; then
24784 ˇ break
24785 ˇ else
24786 ˇ echo \"$item\"
24787 ˇ fi
24788 ˇ done
24789 ˇ done
24790 ˇ}
24791 "});
24792 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
24793 cx.assert_editor_state(indoc! {"
24794 function main() {
24795 #ˇ for item in $items; do
24796 #ˇ while [ -n \"$item\" ]; do
24797 #ˇ if [ \"$value\" -gt 10 ]; then
24798 #ˇ continue
24799 #ˇ elif [ \"$value\" -lt 0 ]; then
24800 #ˇ break
24801 #ˇ else
24802 #ˇ echo \"$item\"
24803 #ˇ fi
24804 #ˇ done
24805 #ˇ done
24806 #ˇ}
24807 "});
24808}
24809
24810#[gpui::test]
24811async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
24812 init_test(cx, |_| {});
24813
24814 let mut cx = EditorTestContext::new(cx).await;
24815 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24816 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24817
24818 // test `else` auto outdents when typed inside `if` block
24819 cx.set_state(indoc! {"
24820 if [ \"$1\" = \"test\" ]; then
24821 echo \"foo bar\"
24822 ˇ
24823 "});
24824 cx.update_editor(|editor, window, cx| {
24825 editor.handle_input("else", window, cx);
24826 });
24827 cx.assert_editor_state(indoc! {"
24828 if [ \"$1\" = \"test\" ]; then
24829 echo \"foo bar\"
24830 elseˇ
24831 "});
24832
24833 // test `elif` auto outdents when typed inside `if` block
24834 cx.set_state(indoc! {"
24835 if [ \"$1\" = \"test\" ]; then
24836 echo \"foo bar\"
24837 ˇ
24838 "});
24839 cx.update_editor(|editor, window, cx| {
24840 editor.handle_input("elif", window, cx);
24841 });
24842 cx.assert_editor_state(indoc! {"
24843 if [ \"$1\" = \"test\" ]; then
24844 echo \"foo bar\"
24845 elifˇ
24846 "});
24847
24848 // test `fi` auto outdents when typed inside `else` block
24849 cx.set_state(indoc! {"
24850 if [ \"$1\" = \"test\" ]; then
24851 echo \"foo bar\"
24852 else
24853 echo \"bar baz\"
24854 ˇ
24855 "});
24856 cx.update_editor(|editor, window, cx| {
24857 editor.handle_input("fi", window, cx);
24858 });
24859 cx.assert_editor_state(indoc! {"
24860 if [ \"$1\" = \"test\" ]; then
24861 echo \"foo bar\"
24862 else
24863 echo \"bar baz\"
24864 fiˇ
24865 "});
24866
24867 // test `done` auto outdents when typed inside `while` block
24868 cx.set_state(indoc! {"
24869 while read line; do
24870 echo \"$line\"
24871 ˇ
24872 "});
24873 cx.update_editor(|editor, window, cx| {
24874 editor.handle_input("done", window, cx);
24875 });
24876 cx.assert_editor_state(indoc! {"
24877 while read line; do
24878 echo \"$line\"
24879 doneˇ
24880 "});
24881
24882 // test `done` auto outdents when typed inside `for` block
24883 cx.set_state(indoc! {"
24884 for file in *.txt; do
24885 cat \"$file\"
24886 ˇ
24887 "});
24888 cx.update_editor(|editor, window, cx| {
24889 editor.handle_input("done", window, cx);
24890 });
24891 cx.assert_editor_state(indoc! {"
24892 for file in *.txt; do
24893 cat \"$file\"
24894 doneˇ
24895 "});
24896
24897 // test `esac` auto outdents when typed inside `case` block
24898 cx.set_state(indoc! {"
24899 case \"$1\" in
24900 start)
24901 echo \"foo bar\"
24902 ;;
24903 stop)
24904 echo \"bar baz\"
24905 ;;
24906 ˇ
24907 "});
24908 cx.update_editor(|editor, window, cx| {
24909 editor.handle_input("esac", window, cx);
24910 });
24911 cx.assert_editor_state(indoc! {"
24912 case \"$1\" in
24913 start)
24914 echo \"foo bar\"
24915 ;;
24916 stop)
24917 echo \"bar baz\"
24918 ;;
24919 esacˇ
24920 "});
24921
24922 // test `*)` auto outdents when typed inside `case` block
24923 cx.set_state(indoc! {"
24924 case \"$1\" in
24925 start)
24926 echo \"foo bar\"
24927 ;;
24928 ˇ
24929 "});
24930 cx.update_editor(|editor, window, cx| {
24931 editor.handle_input("*)", window, cx);
24932 });
24933 cx.assert_editor_state(indoc! {"
24934 case \"$1\" in
24935 start)
24936 echo \"foo bar\"
24937 ;;
24938 *)ˇ
24939 "});
24940
24941 // test `fi` outdents to correct level with nested if blocks
24942 cx.set_state(indoc! {"
24943 if [ \"$1\" = \"test\" ]; then
24944 echo \"outer if\"
24945 if [ \"$2\" = \"debug\" ]; then
24946 echo \"inner if\"
24947 ˇ
24948 "});
24949 cx.update_editor(|editor, window, cx| {
24950 editor.handle_input("fi", window, cx);
24951 });
24952 cx.assert_editor_state(indoc! {"
24953 if [ \"$1\" = \"test\" ]; then
24954 echo \"outer if\"
24955 if [ \"$2\" = \"debug\" ]; then
24956 echo \"inner if\"
24957 fiˇ
24958 "});
24959}
24960
24961#[gpui::test]
24962async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
24963 init_test(cx, |_| {});
24964 update_test_language_settings(cx, |settings| {
24965 settings.defaults.extend_comment_on_newline = Some(false);
24966 });
24967 let mut cx = EditorTestContext::new(cx).await;
24968 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24969 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24970
24971 // test correct indent after newline on comment
24972 cx.set_state(indoc! {"
24973 # COMMENT:ˇ
24974 "});
24975 cx.update_editor(|editor, window, cx| {
24976 editor.newline(&Newline, window, cx);
24977 });
24978 cx.assert_editor_state(indoc! {"
24979 # COMMENT:
24980 ˇ
24981 "});
24982
24983 // test correct indent after newline after `then`
24984 cx.set_state(indoc! {"
24985
24986 if [ \"$1\" = \"test\" ]; thenˇ
24987 "});
24988 cx.update_editor(|editor, window, cx| {
24989 editor.newline(&Newline, window, cx);
24990 });
24991 cx.run_until_parked();
24992 cx.assert_editor_state(indoc! {"
24993
24994 if [ \"$1\" = \"test\" ]; then
24995 ˇ
24996 "});
24997
24998 // test correct indent after newline after `else`
24999 cx.set_state(indoc! {"
25000 if [ \"$1\" = \"test\" ]; then
25001 elseˇ
25002 "});
25003 cx.update_editor(|editor, window, cx| {
25004 editor.newline(&Newline, window, cx);
25005 });
25006 cx.run_until_parked();
25007 cx.assert_editor_state(indoc! {"
25008 if [ \"$1\" = \"test\" ]; then
25009 else
25010 ˇ
25011 "});
25012
25013 // test correct indent after newline after `elif`
25014 cx.set_state(indoc! {"
25015 if [ \"$1\" = \"test\" ]; then
25016 elifˇ
25017 "});
25018 cx.update_editor(|editor, window, cx| {
25019 editor.newline(&Newline, window, cx);
25020 });
25021 cx.run_until_parked();
25022 cx.assert_editor_state(indoc! {"
25023 if [ \"$1\" = \"test\" ]; then
25024 elif
25025 ˇ
25026 "});
25027
25028 // test correct indent after newline after `do`
25029 cx.set_state(indoc! {"
25030 for file in *.txt; doˇ
25031 "});
25032 cx.update_editor(|editor, window, cx| {
25033 editor.newline(&Newline, window, cx);
25034 });
25035 cx.run_until_parked();
25036 cx.assert_editor_state(indoc! {"
25037 for file in *.txt; do
25038 ˇ
25039 "});
25040
25041 // test correct indent after newline after case pattern
25042 cx.set_state(indoc! {"
25043 case \"$1\" in
25044 start)ˇ
25045 "});
25046 cx.update_editor(|editor, window, cx| {
25047 editor.newline(&Newline, window, cx);
25048 });
25049 cx.run_until_parked();
25050 cx.assert_editor_state(indoc! {"
25051 case \"$1\" in
25052 start)
25053 ˇ
25054 "});
25055
25056 // test correct indent after newline after case pattern
25057 cx.set_state(indoc! {"
25058 case \"$1\" in
25059 start)
25060 ;;
25061 *)ˇ
25062 "});
25063 cx.update_editor(|editor, window, cx| {
25064 editor.newline(&Newline, window, cx);
25065 });
25066 cx.run_until_parked();
25067 cx.assert_editor_state(indoc! {"
25068 case \"$1\" in
25069 start)
25070 ;;
25071 *)
25072 ˇ
25073 "});
25074
25075 // test correct indent after newline after function opening brace
25076 cx.set_state(indoc! {"
25077 function test() {ˇ}
25078 "});
25079 cx.update_editor(|editor, window, cx| {
25080 editor.newline(&Newline, window, cx);
25081 });
25082 cx.run_until_parked();
25083 cx.assert_editor_state(indoc! {"
25084 function test() {
25085 ˇ
25086 }
25087 "});
25088
25089 // test no extra indent after semicolon on same line
25090 cx.set_state(indoc! {"
25091 echo \"test\";ˇ
25092 "});
25093 cx.update_editor(|editor, window, cx| {
25094 editor.newline(&Newline, window, cx);
25095 });
25096 cx.run_until_parked();
25097 cx.assert_editor_state(indoc! {"
25098 echo \"test\";
25099 ˇ
25100 "});
25101}
25102
25103fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
25104 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
25105 point..point
25106}
25107
25108#[track_caller]
25109fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
25110 let (text, ranges) = marked_text_ranges(marked_text, true);
25111 assert_eq!(editor.text(cx), text);
25112 assert_eq!(
25113 editor.selections.ranges(cx),
25114 ranges,
25115 "Assert selections are {}",
25116 marked_text
25117 );
25118}
25119
25120pub fn handle_signature_help_request(
25121 cx: &mut EditorLspTestContext,
25122 mocked_response: lsp::SignatureHelp,
25123) -> impl Future<Output = ()> + use<> {
25124 let mut request =
25125 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
25126 let mocked_response = mocked_response.clone();
25127 async move { Ok(Some(mocked_response)) }
25128 });
25129
25130 async move {
25131 request.next().await;
25132 }
25133}
25134
25135#[track_caller]
25136pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
25137 cx.update_editor(|editor, _, _| {
25138 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
25139 let entries = menu.entries.borrow();
25140 let entries = entries
25141 .iter()
25142 .map(|entry| entry.string.as_str())
25143 .collect::<Vec<_>>();
25144 assert_eq!(entries, expected);
25145 } else {
25146 panic!("Expected completions menu");
25147 }
25148 });
25149}
25150
25151/// Handle completion request passing a marked string specifying where the completion
25152/// should be triggered from using '|' character, what range should be replaced, and what completions
25153/// should be returned using '<' and '>' to delimit the range.
25154///
25155/// Also see `handle_completion_request_with_insert_and_replace`.
25156#[track_caller]
25157pub fn handle_completion_request(
25158 marked_string: &str,
25159 completions: Vec<&'static str>,
25160 is_incomplete: bool,
25161 counter: Arc<AtomicUsize>,
25162 cx: &mut EditorLspTestContext,
25163) -> impl Future<Output = ()> {
25164 let complete_from_marker: TextRangeMarker = '|'.into();
25165 let replace_range_marker: TextRangeMarker = ('<', '>').into();
25166 let (_, mut marked_ranges) = marked_text_ranges_by(
25167 marked_string,
25168 vec![complete_from_marker.clone(), replace_range_marker.clone()],
25169 );
25170
25171 let complete_from_position =
25172 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
25173 let replace_range =
25174 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
25175
25176 let mut request =
25177 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
25178 let completions = completions.clone();
25179 counter.fetch_add(1, atomic::Ordering::Release);
25180 async move {
25181 assert_eq!(params.text_document_position.text_document.uri, url.clone());
25182 assert_eq!(
25183 params.text_document_position.position,
25184 complete_from_position
25185 );
25186 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
25187 is_incomplete,
25188 item_defaults: None,
25189 items: completions
25190 .iter()
25191 .map(|completion_text| lsp::CompletionItem {
25192 label: completion_text.to_string(),
25193 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
25194 range: replace_range,
25195 new_text: completion_text.to_string(),
25196 })),
25197 ..Default::default()
25198 })
25199 .collect(),
25200 })))
25201 }
25202 });
25203
25204 async move {
25205 request.next().await;
25206 }
25207}
25208
25209/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
25210/// given instead, which also contains an `insert` range.
25211///
25212/// This function uses markers to define ranges:
25213/// - `|` marks the cursor position
25214/// - `<>` marks the replace range
25215/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
25216pub fn handle_completion_request_with_insert_and_replace(
25217 cx: &mut EditorLspTestContext,
25218 marked_string: &str,
25219 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
25220 counter: Arc<AtomicUsize>,
25221) -> impl Future<Output = ()> {
25222 let complete_from_marker: TextRangeMarker = '|'.into();
25223 let replace_range_marker: TextRangeMarker = ('<', '>').into();
25224 let insert_range_marker: TextRangeMarker = ('{', '}').into();
25225
25226 let (_, mut marked_ranges) = marked_text_ranges_by(
25227 marked_string,
25228 vec![
25229 complete_from_marker.clone(),
25230 replace_range_marker.clone(),
25231 insert_range_marker.clone(),
25232 ],
25233 );
25234
25235 let complete_from_position =
25236 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
25237 let replace_range =
25238 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
25239
25240 let insert_range = match marked_ranges.remove(&insert_range_marker) {
25241 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
25242 _ => lsp::Range {
25243 start: replace_range.start,
25244 end: complete_from_position,
25245 },
25246 };
25247
25248 let mut request =
25249 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
25250 let completions = completions.clone();
25251 counter.fetch_add(1, atomic::Ordering::Release);
25252 async move {
25253 assert_eq!(params.text_document_position.text_document.uri, url.clone());
25254 assert_eq!(
25255 params.text_document_position.position, complete_from_position,
25256 "marker `|` position doesn't match",
25257 );
25258 Ok(Some(lsp::CompletionResponse::Array(
25259 completions
25260 .iter()
25261 .map(|(label, new_text)| lsp::CompletionItem {
25262 label: label.to_string(),
25263 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
25264 lsp::InsertReplaceEdit {
25265 insert: insert_range,
25266 replace: replace_range,
25267 new_text: new_text.to_string(),
25268 },
25269 )),
25270 ..Default::default()
25271 })
25272 .collect(),
25273 )))
25274 }
25275 });
25276
25277 async move {
25278 request.next().await;
25279 }
25280}
25281
25282fn handle_resolve_completion_request(
25283 cx: &mut EditorLspTestContext,
25284 edits: Option<Vec<(&'static str, &'static str)>>,
25285) -> impl Future<Output = ()> {
25286 let edits = edits.map(|edits| {
25287 edits
25288 .iter()
25289 .map(|(marked_string, new_text)| {
25290 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
25291 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
25292 lsp::TextEdit::new(replace_range, new_text.to_string())
25293 })
25294 .collect::<Vec<_>>()
25295 });
25296
25297 let mut request =
25298 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
25299 let edits = edits.clone();
25300 async move {
25301 Ok(lsp::CompletionItem {
25302 additional_text_edits: edits,
25303 ..Default::default()
25304 })
25305 }
25306 });
25307
25308 async move {
25309 request.next().await;
25310 }
25311}
25312
25313pub(crate) fn update_test_language_settings(
25314 cx: &mut TestAppContext,
25315 f: impl Fn(&mut AllLanguageSettingsContent),
25316) {
25317 cx.update(|cx| {
25318 SettingsStore::update_global(cx, |store, cx| {
25319 store.update_user_settings(cx, |settings| f(&mut settings.project.all_languages));
25320 });
25321 });
25322}
25323
25324pub(crate) fn update_test_project_settings(
25325 cx: &mut TestAppContext,
25326 f: impl Fn(&mut ProjectSettingsContent),
25327) {
25328 cx.update(|cx| {
25329 SettingsStore::update_global(cx, |store, cx| {
25330 store.update_user_settings(cx, |settings| f(&mut settings.project));
25331 });
25332 });
25333}
25334
25335pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
25336 cx.update(|cx| {
25337 assets::Assets.load_test_fonts(cx);
25338 let store = SettingsStore::test(cx);
25339 cx.set_global(store);
25340 theme::init(theme::LoadThemes::JustBase, cx);
25341 release_channel::init(SemanticVersion::default(), cx);
25342 client::init_settings(cx);
25343 language::init(cx);
25344 Project::init_settings(cx);
25345 workspace::init_settings(cx);
25346 crate::init(cx);
25347 });
25348 zlog::init_test();
25349 update_test_language_settings(cx, f);
25350}
25351
25352#[track_caller]
25353fn assert_hunk_revert(
25354 not_reverted_text_with_selections: &str,
25355 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
25356 expected_reverted_text_with_selections: &str,
25357 base_text: &str,
25358 cx: &mut EditorLspTestContext,
25359) {
25360 cx.set_state(not_reverted_text_with_selections);
25361 cx.set_head_text(base_text);
25362 cx.executor().run_until_parked();
25363
25364 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
25365 let snapshot = editor.snapshot(window, cx);
25366 let reverted_hunk_statuses = snapshot
25367 .buffer_snapshot()
25368 .diff_hunks_in_range(0..snapshot.buffer_snapshot().len())
25369 .map(|hunk| hunk.status().kind)
25370 .collect::<Vec<_>>();
25371
25372 editor.git_restore(&Default::default(), window, cx);
25373 reverted_hunk_statuses
25374 });
25375 cx.executor().run_until_parked();
25376 cx.assert_editor_state(expected_reverted_text_with_selections);
25377 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
25378}
25379
25380#[gpui::test(iterations = 10)]
25381async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
25382 init_test(cx, |_| {});
25383
25384 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
25385 let counter = diagnostic_requests.clone();
25386
25387 let fs = FakeFs::new(cx.executor());
25388 fs.insert_tree(
25389 path!("/a"),
25390 json!({
25391 "first.rs": "fn main() { let a = 5; }",
25392 "second.rs": "// Test file",
25393 }),
25394 )
25395 .await;
25396
25397 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25398 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25399 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25400
25401 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25402 language_registry.add(rust_lang());
25403 let mut fake_servers = language_registry.register_fake_lsp(
25404 "Rust",
25405 FakeLspAdapter {
25406 capabilities: lsp::ServerCapabilities {
25407 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
25408 lsp::DiagnosticOptions {
25409 identifier: None,
25410 inter_file_dependencies: true,
25411 workspace_diagnostics: true,
25412 work_done_progress_options: Default::default(),
25413 },
25414 )),
25415 ..Default::default()
25416 },
25417 ..Default::default()
25418 },
25419 );
25420
25421 let editor = workspace
25422 .update(cx, |workspace, window, cx| {
25423 workspace.open_abs_path(
25424 PathBuf::from(path!("/a/first.rs")),
25425 OpenOptions::default(),
25426 window,
25427 cx,
25428 )
25429 })
25430 .unwrap()
25431 .await
25432 .unwrap()
25433 .downcast::<Editor>()
25434 .unwrap();
25435 let fake_server = fake_servers.next().await.unwrap();
25436 let server_id = fake_server.server.server_id();
25437 let mut first_request = fake_server
25438 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
25439 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
25440 let result_id = Some(new_result_id.to_string());
25441 assert_eq!(
25442 params.text_document.uri,
25443 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25444 );
25445 async move {
25446 Ok(lsp::DocumentDiagnosticReportResult::Report(
25447 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
25448 related_documents: None,
25449 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
25450 items: Vec::new(),
25451 result_id,
25452 },
25453 }),
25454 ))
25455 }
25456 });
25457
25458 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
25459 project.update(cx, |project, cx| {
25460 let buffer_id = editor
25461 .read(cx)
25462 .buffer()
25463 .read(cx)
25464 .as_singleton()
25465 .expect("created a singleton buffer")
25466 .read(cx)
25467 .remote_id();
25468 let buffer_result_id = project
25469 .lsp_store()
25470 .read(cx)
25471 .result_id(server_id, buffer_id, cx);
25472 assert_eq!(expected, buffer_result_id);
25473 });
25474 };
25475
25476 ensure_result_id(None, cx);
25477 cx.executor().advance_clock(Duration::from_millis(60));
25478 cx.executor().run_until_parked();
25479 assert_eq!(
25480 diagnostic_requests.load(atomic::Ordering::Acquire),
25481 1,
25482 "Opening file should trigger diagnostic request"
25483 );
25484 first_request
25485 .next()
25486 .await
25487 .expect("should have sent the first diagnostics pull request");
25488 ensure_result_id(Some("1".to_string()), cx);
25489
25490 // Editing should trigger diagnostics
25491 editor.update_in(cx, |editor, window, cx| {
25492 editor.handle_input("2", window, cx)
25493 });
25494 cx.executor().advance_clock(Duration::from_millis(60));
25495 cx.executor().run_until_parked();
25496 assert_eq!(
25497 diagnostic_requests.load(atomic::Ordering::Acquire),
25498 2,
25499 "Editing should trigger diagnostic request"
25500 );
25501 ensure_result_id(Some("2".to_string()), cx);
25502
25503 // Moving cursor should not trigger diagnostic request
25504 editor.update_in(cx, |editor, window, cx| {
25505 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25506 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
25507 });
25508 });
25509 cx.executor().advance_clock(Duration::from_millis(60));
25510 cx.executor().run_until_parked();
25511 assert_eq!(
25512 diagnostic_requests.load(atomic::Ordering::Acquire),
25513 2,
25514 "Cursor movement should not trigger diagnostic request"
25515 );
25516 ensure_result_id(Some("2".to_string()), cx);
25517 // Multiple rapid edits should be debounced
25518 for _ in 0..5 {
25519 editor.update_in(cx, |editor, window, cx| {
25520 editor.handle_input("x", window, cx)
25521 });
25522 }
25523 cx.executor().advance_clock(Duration::from_millis(60));
25524 cx.executor().run_until_parked();
25525
25526 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
25527 assert!(
25528 final_requests <= 4,
25529 "Multiple rapid edits should be debounced (got {final_requests} requests)",
25530 );
25531 ensure_result_id(Some(final_requests.to_string()), cx);
25532}
25533
25534#[gpui::test]
25535async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
25536 // Regression test for issue #11671
25537 // Previously, adding a cursor after moving multiple cursors would reset
25538 // the cursor count instead of adding to the existing cursors.
25539 init_test(cx, |_| {});
25540 let mut cx = EditorTestContext::new(cx).await;
25541
25542 // Create a simple buffer with cursor at start
25543 cx.set_state(indoc! {"
25544 ˇaaaa
25545 bbbb
25546 cccc
25547 dddd
25548 eeee
25549 ffff
25550 gggg
25551 hhhh"});
25552
25553 // Add 2 cursors below (so we have 3 total)
25554 cx.update_editor(|editor, window, cx| {
25555 editor.add_selection_below(&Default::default(), window, cx);
25556 editor.add_selection_below(&Default::default(), window, cx);
25557 });
25558
25559 // Verify we have 3 cursors
25560 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
25561 assert_eq!(
25562 initial_count, 3,
25563 "Should have 3 cursors after adding 2 below"
25564 );
25565
25566 // Move down one line
25567 cx.update_editor(|editor, window, cx| {
25568 editor.move_down(&MoveDown, window, cx);
25569 });
25570
25571 // Add another cursor below
25572 cx.update_editor(|editor, window, cx| {
25573 editor.add_selection_below(&Default::default(), window, cx);
25574 });
25575
25576 // Should now have 4 cursors (3 original + 1 new)
25577 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
25578 assert_eq!(
25579 final_count, 4,
25580 "Should have 4 cursors after moving and adding another"
25581 );
25582}
25583
25584#[gpui::test(iterations = 10)]
25585async fn test_document_colors(cx: &mut TestAppContext) {
25586 let expected_color = Rgba {
25587 r: 0.33,
25588 g: 0.33,
25589 b: 0.33,
25590 a: 0.33,
25591 };
25592
25593 init_test(cx, |_| {});
25594
25595 let fs = FakeFs::new(cx.executor());
25596 fs.insert_tree(
25597 path!("/a"),
25598 json!({
25599 "first.rs": "fn main() { let a = 5; }",
25600 }),
25601 )
25602 .await;
25603
25604 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25605 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25606 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25607
25608 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25609 language_registry.add(rust_lang());
25610 let mut fake_servers = language_registry.register_fake_lsp(
25611 "Rust",
25612 FakeLspAdapter {
25613 capabilities: lsp::ServerCapabilities {
25614 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
25615 ..lsp::ServerCapabilities::default()
25616 },
25617 name: "rust-analyzer",
25618 ..FakeLspAdapter::default()
25619 },
25620 );
25621 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
25622 "Rust",
25623 FakeLspAdapter {
25624 capabilities: lsp::ServerCapabilities {
25625 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
25626 ..lsp::ServerCapabilities::default()
25627 },
25628 name: "not-rust-analyzer",
25629 ..FakeLspAdapter::default()
25630 },
25631 );
25632
25633 let editor = workspace
25634 .update(cx, |workspace, window, cx| {
25635 workspace.open_abs_path(
25636 PathBuf::from(path!("/a/first.rs")),
25637 OpenOptions::default(),
25638 window,
25639 cx,
25640 )
25641 })
25642 .unwrap()
25643 .await
25644 .unwrap()
25645 .downcast::<Editor>()
25646 .unwrap();
25647 let fake_language_server = fake_servers.next().await.unwrap();
25648 let fake_language_server_without_capabilities =
25649 fake_servers_without_capabilities.next().await.unwrap();
25650 let requests_made = Arc::new(AtomicUsize::new(0));
25651 let closure_requests_made = Arc::clone(&requests_made);
25652 let mut color_request_handle = fake_language_server
25653 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
25654 let requests_made = Arc::clone(&closure_requests_made);
25655 async move {
25656 assert_eq!(
25657 params.text_document.uri,
25658 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25659 );
25660 requests_made.fetch_add(1, atomic::Ordering::Release);
25661 Ok(vec![
25662 lsp::ColorInformation {
25663 range: lsp::Range {
25664 start: lsp::Position {
25665 line: 0,
25666 character: 0,
25667 },
25668 end: lsp::Position {
25669 line: 0,
25670 character: 1,
25671 },
25672 },
25673 color: lsp::Color {
25674 red: 0.33,
25675 green: 0.33,
25676 blue: 0.33,
25677 alpha: 0.33,
25678 },
25679 },
25680 lsp::ColorInformation {
25681 range: lsp::Range {
25682 start: lsp::Position {
25683 line: 0,
25684 character: 0,
25685 },
25686 end: lsp::Position {
25687 line: 0,
25688 character: 1,
25689 },
25690 },
25691 color: lsp::Color {
25692 red: 0.33,
25693 green: 0.33,
25694 blue: 0.33,
25695 alpha: 0.33,
25696 },
25697 },
25698 ])
25699 }
25700 });
25701
25702 let _handle = fake_language_server_without_capabilities
25703 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
25704 panic!("Should not be called");
25705 });
25706 cx.executor().advance_clock(Duration::from_millis(100));
25707 color_request_handle.next().await.unwrap();
25708 cx.run_until_parked();
25709 assert_eq!(
25710 1,
25711 requests_made.load(atomic::Ordering::Acquire),
25712 "Should query for colors once per editor open"
25713 );
25714 editor.update_in(cx, |editor, _, cx| {
25715 assert_eq!(
25716 vec![expected_color],
25717 extract_color_inlays(editor, cx),
25718 "Should have an initial inlay"
25719 );
25720 });
25721
25722 // opening another file in a split should not influence the LSP query counter
25723 workspace
25724 .update(cx, |workspace, window, cx| {
25725 assert_eq!(
25726 workspace.panes().len(),
25727 1,
25728 "Should have one pane with one editor"
25729 );
25730 workspace.move_item_to_pane_in_direction(
25731 &MoveItemToPaneInDirection {
25732 direction: SplitDirection::Right,
25733 focus: false,
25734 clone: true,
25735 },
25736 window,
25737 cx,
25738 );
25739 })
25740 .unwrap();
25741 cx.run_until_parked();
25742 workspace
25743 .update(cx, |workspace, _, cx| {
25744 let panes = workspace.panes();
25745 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
25746 for pane in panes {
25747 let editor = pane
25748 .read(cx)
25749 .active_item()
25750 .and_then(|item| item.downcast::<Editor>())
25751 .expect("Should have opened an editor in each split");
25752 let editor_file = editor
25753 .read(cx)
25754 .buffer()
25755 .read(cx)
25756 .as_singleton()
25757 .expect("test deals with singleton buffers")
25758 .read(cx)
25759 .file()
25760 .expect("test buffese should have a file")
25761 .path();
25762 assert_eq!(
25763 editor_file.as_ref(),
25764 rel_path("first.rs"),
25765 "Both editors should be opened for the same file"
25766 )
25767 }
25768 })
25769 .unwrap();
25770
25771 cx.executor().advance_clock(Duration::from_millis(500));
25772 let save = editor.update_in(cx, |editor, window, cx| {
25773 editor.move_to_end(&MoveToEnd, window, cx);
25774 editor.handle_input("dirty", window, cx);
25775 editor.save(
25776 SaveOptions {
25777 format: true,
25778 autosave: true,
25779 },
25780 project.clone(),
25781 window,
25782 cx,
25783 )
25784 });
25785 save.await.unwrap();
25786
25787 color_request_handle.next().await.unwrap();
25788 cx.run_until_parked();
25789 assert_eq!(
25790 3,
25791 requests_made.load(atomic::Ordering::Acquire),
25792 "Should query for colors once per save and once per formatting after save"
25793 );
25794
25795 drop(editor);
25796 let close = workspace
25797 .update(cx, |workspace, window, cx| {
25798 workspace.active_pane().update(cx, |pane, cx| {
25799 pane.close_active_item(&CloseActiveItem::default(), window, cx)
25800 })
25801 })
25802 .unwrap();
25803 close.await.unwrap();
25804 let close = workspace
25805 .update(cx, |workspace, window, cx| {
25806 workspace.active_pane().update(cx, |pane, cx| {
25807 pane.close_active_item(&CloseActiveItem::default(), window, cx)
25808 })
25809 })
25810 .unwrap();
25811 close.await.unwrap();
25812 assert_eq!(
25813 3,
25814 requests_made.load(atomic::Ordering::Acquire),
25815 "After saving and closing all editors, no extra requests should be made"
25816 );
25817 workspace
25818 .update(cx, |workspace, _, cx| {
25819 assert!(
25820 workspace.active_item(cx).is_none(),
25821 "Should close all editors"
25822 )
25823 })
25824 .unwrap();
25825
25826 workspace
25827 .update(cx, |workspace, window, cx| {
25828 workspace.active_pane().update(cx, |pane, cx| {
25829 pane.navigate_backward(&workspace::GoBack, window, cx);
25830 })
25831 })
25832 .unwrap();
25833 cx.executor().advance_clock(Duration::from_millis(100));
25834 cx.run_until_parked();
25835 let editor = workspace
25836 .update(cx, |workspace, _, cx| {
25837 workspace
25838 .active_item(cx)
25839 .expect("Should have reopened the editor again after navigating back")
25840 .downcast::<Editor>()
25841 .expect("Should be an editor")
25842 })
25843 .unwrap();
25844 color_request_handle.next().await.unwrap();
25845 assert_eq!(
25846 3,
25847 requests_made.load(atomic::Ordering::Acquire),
25848 "Cache should be reused on buffer close and reopen"
25849 );
25850 editor.update(cx, |editor, cx| {
25851 assert_eq!(
25852 vec![expected_color],
25853 extract_color_inlays(editor, cx),
25854 "Should have an initial inlay"
25855 );
25856 });
25857
25858 drop(color_request_handle);
25859 let closure_requests_made = Arc::clone(&requests_made);
25860 let mut empty_color_request_handle = fake_language_server
25861 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
25862 let requests_made = Arc::clone(&closure_requests_made);
25863 async move {
25864 assert_eq!(
25865 params.text_document.uri,
25866 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25867 );
25868 requests_made.fetch_add(1, atomic::Ordering::Release);
25869 Ok(Vec::new())
25870 }
25871 });
25872 let save = editor.update_in(cx, |editor, window, cx| {
25873 editor.move_to_end(&MoveToEnd, window, cx);
25874 editor.handle_input("dirty_again", window, cx);
25875 editor.save(
25876 SaveOptions {
25877 format: false,
25878 autosave: true,
25879 },
25880 project.clone(),
25881 window,
25882 cx,
25883 )
25884 });
25885 save.await.unwrap();
25886
25887 empty_color_request_handle.next().await.unwrap();
25888 cx.run_until_parked();
25889 assert_eq!(
25890 4,
25891 requests_made.load(atomic::Ordering::Acquire),
25892 "Should query for colors once per save only, as formatting was not requested"
25893 );
25894 editor.update(cx, |editor, cx| {
25895 assert_eq!(
25896 Vec::<Rgba>::new(),
25897 extract_color_inlays(editor, cx),
25898 "Should clear all colors when the server returns an empty response"
25899 );
25900 });
25901}
25902
25903#[gpui::test]
25904async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
25905 init_test(cx, |_| {});
25906 let (editor, cx) = cx.add_window_view(Editor::single_line);
25907 editor.update_in(cx, |editor, window, cx| {
25908 editor.set_text("oops\n\nwow\n", window, cx)
25909 });
25910 cx.run_until_parked();
25911 editor.update(cx, |editor, cx| {
25912 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
25913 });
25914 editor.update(cx, |editor, cx| editor.edit([(3..5, "")], cx));
25915 cx.run_until_parked();
25916 editor.update(cx, |editor, cx| {
25917 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
25918 });
25919}
25920
25921#[gpui::test]
25922async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
25923 init_test(cx, |_| {});
25924
25925 cx.update(|cx| {
25926 register_project_item::<Editor>(cx);
25927 });
25928
25929 let fs = FakeFs::new(cx.executor());
25930 fs.insert_tree("/root1", json!({})).await;
25931 fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
25932 .await;
25933
25934 let project = Project::test(fs, ["/root1".as_ref()], cx).await;
25935 let (workspace, cx) =
25936 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
25937
25938 let worktree_id = project.update(cx, |project, cx| {
25939 project.worktrees(cx).next().unwrap().read(cx).id()
25940 });
25941
25942 let handle = workspace
25943 .update_in(cx, |workspace, window, cx| {
25944 let project_path = (worktree_id, rel_path("one.pdf"));
25945 workspace.open_path(project_path, None, true, window, cx)
25946 })
25947 .await
25948 .unwrap();
25949
25950 assert_eq!(
25951 handle.to_any().entity_type(),
25952 TypeId::of::<InvalidBufferView>()
25953 );
25954}
25955
25956#[gpui::test]
25957async fn test_select_next_prev_syntax_node(cx: &mut TestAppContext) {
25958 init_test(cx, |_| {});
25959
25960 let language = Arc::new(Language::new(
25961 LanguageConfig::default(),
25962 Some(tree_sitter_rust::LANGUAGE.into()),
25963 ));
25964
25965 // Test hierarchical sibling navigation
25966 let text = r#"
25967 fn outer() {
25968 if condition {
25969 let a = 1;
25970 }
25971 let b = 2;
25972 }
25973
25974 fn another() {
25975 let c = 3;
25976 }
25977 "#;
25978
25979 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
25980 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
25981 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
25982
25983 // Wait for parsing to complete
25984 editor
25985 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
25986 .await;
25987
25988 editor.update_in(cx, |editor, window, cx| {
25989 // Start by selecting "let a = 1;" inside the if block
25990 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25991 s.select_display_ranges([
25992 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 26)
25993 ]);
25994 });
25995
25996 let initial_selection = editor.selections.display_ranges(cx);
25997 assert_eq!(initial_selection.len(), 1, "Should have one selection");
25998
25999 // Test select next sibling - should move up levels to find the next sibling
26000 // Since "let a = 1;" has no siblings in the if block, it should move up
26001 // to find "let b = 2;" which is a sibling of the if block
26002 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
26003 let next_selection = editor.selections.display_ranges(cx);
26004
26005 // Should have a selection and it should be different from the initial
26006 assert_eq!(
26007 next_selection.len(),
26008 1,
26009 "Should have one selection after next"
26010 );
26011 assert_ne!(
26012 next_selection[0], initial_selection[0],
26013 "Next sibling selection should be different"
26014 );
26015
26016 // Test hierarchical navigation by going to the end of the current function
26017 // and trying to navigate to the next function
26018 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26019 s.select_display_ranges([
26020 DisplayPoint::new(DisplayRow(5), 12)..DisplayPoint::new(DisplayRow(5), 22)
26021 ]);
26022 });
26023
26024 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
26025 let function_next_selection = editor.selections.display_ranges(cx);
26026
26027 // Should move to the next function
26028 assert_eq!(
26029 function_next_selection.len(),
26030 1,
26031 "Should have one selection after function next"
26032 );
26033
26034 // Test select previous sibling navigation
26035 editor.select_prev_syntax_node(&SelectPreviousSyntaxNode, window, cx);
26036 let prev_selection = editor.selections.display_ranges(cx);
26037
26038 // Should have a selection and it should be different
26039 assert_eq!(
26040 prev_selection.len(),
26041 1,
26042 "Should have one selection after prev"
26043 );
26044 assert_ne!(
26045 prev_selection[0], function_next_selection[0],
26046 "Previous sibling selection should be different from next"
26047 );
26048 });
26049}
26050
26051#[gpui::test]
26052async fn test_next_prev_document_highlight(cx: &mut TestAppContext) {
26053 init_test(cx, |_| {});
26054
26055 let mut cx = EditorTestContext::new(cx).await;
26056 cx.set_state(
26057 "let ˇvariable = 42;
26058let another = variable + 1;
26059let result = variable * 2;",
26060 );
26061
26062 // Set up document highlights manually (simulating LSP response)
26063 cx.update_editor(|editor, _window, cx| {
26064 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
26065
26066 // Create highlights for "variable" occurrences
26067 let highlight_ranges = [
26068 Point::new(0, 4)..Point::new(0, 12), // First "variable"
26069 Point::new(1, 14)..Point::new(1, 22), // Second "variable"
26070 Point::new(2, 13)..Point::new(2, 21), // Third "variable"
26071 ];
26072
26073 let anchor_ranges: Vec<_> = highlight_ranges
26074 .iter()
26075 .map(|range| range.clone().to_anchors(&buffer_snapshot))
26076 .collect();
26077
26078 editor.highlight_background::<DocumentHighlightRead>(
26079 &anchor_ranges,
26080 |theme| theme.colors().editor_document_highlight_read_background,
26081 cx,
26082 );
26083 });
26084
26085 // Go to next highlight - should move to second "variable"
26086 cx.update_editor(|editor, window, cx| {
26087 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26088 });
26089 cx.assert_editor_state(
26090 "let variable = 42;
26091let another = ˇvariable + 1;
26092let result = variable * 2;",
26093 );
26094
26095 // Go to next highlight - should move to third "variable"
26096 cx.update_editor(|editor, window, cx| {
26097 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26098 });
26099 cx.assert_editor_state(
26100 "let variable = 42;
26101let another = variable + 1;
26102let result = ˇvariable * 2;",
26103 );
26104
26105 // Go to next highlight - should stay at third "variable" (no wrap-around)
26106 cx.update_editor(|editor, window, cx| {
26107 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26108 });
26109 cx.assert_editor_state(
26110 "let variable = 42;
26111let another = variable + 1;
26112let result = ˇvariable * 2;",
26113 );
26114
26115 // Now test going backwards from third position
26116 cx.update_editor(|editor, window, cx| {
26117 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26118 });
26119 cx.assert_editor_state(
26120 "let variable = 42;
26121let another = ˇvariable + 1;
26122let result = variable * 2;",
26123 );
26124
26125 // Go to previous highlight - should move to first "variable"
26126 cx.update_editor(|editor, window, cx| {
26127 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26128 });
26129 cx.assert_editor_state(
26130 "let ˇvariable = 42;
26131let another = variable + 1;
26132let result = variable * 2;",
26133 );
26134
26135 // Go to previous highlight - should stay on first "variable"
26136 cx.update_editor(|editor, window, cx| {
26137 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26138 });
26139 cx.assert_editor_state(
26140 "let ˇvariable = 42;
26141let another = variable + 1;
26142let result = variable * 2;",
26143 );
26144}
26145
26146#[gpui::test]
26147async fn test_paste_url_from_other_app_creates_markdown_link_over_selected_text(
26148 cx: &mut gpui::TestAppContext,
26149) {
26150 init_test(cx, |_| {});
26151
26152 let url = "https://zed.dev";
26153
26154 let markdown_language = Arc::new(Language::new(
26155 LanguageConfig {
26156 name: "Markdown".into(),
26157 ..LanguageConfig::default()
26158 },
26159 None,
26160 ));
26161
26162 let mut cx = EditorTestContext::new(cx).await;
26163 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26164 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)");
26165
26166 cx.update_editor(|editor, window, cx| {
26167 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26168 editor.paste(&Paste, window, cx);
26169 });
26170
26171 cx.assert_editor_state(&format!(
26172 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)"
26173 ));
26174}
26175
26176#[gpui::test]
26177async fn test_paste_url_from_zed_copy_creates_markdown_link_over_selected_text(
26178 cx: &mut gpui::TestAppContext,
26179) {
26180 init_test(cx, |_| {});
26181
26182 let url = "https://zed.dev";
26183
26184 let markdown_language = Arc::new(Language::new(
26185 LanguageConfig {
26186 name: "Markdown".into(),
26187 ..LanguageConfig::default()
26188 },
26189 None,
26190 ));
26191
26192 let mut cx = EditorTestContext::new(cx).await;
26193 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26194 cx.set_state(&format!(
26195 "Hello, editor.\nZed is great (see this link: )\n«{url}ˇ»"
26196 ));
26197
26198 cx.update_editor(|editor, window, cx| {
26199 editor.copy(&Copy, window, cx);
26200 });
26201
26202 cx.set_state(&format!(
26203 "Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)\n{url}"
26204 ));
26205
26206 cx.update_editor(|editor, window, cx| {
26207 editor.paste(&Paste, window, cx);
26208 });
26209
26210 cx.assert_editor_state(&format!(
26211 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)\n{url}"
26212 ));
26213}
26214
26215#[gpui::test]
26216async fn test_paste_url_from_other_app_replaces_existing_url_without_creating_markdown_link(
26217 cx: &mut gpui::TestAppContext,
26218) {
26219 init_test(cx, |_| {});
26220
26221 let url = "https://zed.dev";
26222
26223 let markdown_language = Arc::new(Language::new(
26224 LanguageConfig {
26225 name: "Markdown".into(),
26226 ..LanguageConfig::default()
26227 },
26228 None,
26229 ));
26230
26231 let mut cx = EditorTestContext::new(cx).await;
26232 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26233 cx.set_state("Please visit zed's homepage: «https://www.apple.comˇ»");
26234
26235 cx.update_editor(|editor, window, cx| {
26236 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26237 editor.paste(&Paste, window, cx);
26238 });
26239
26240 cx.assert_editor_state(&format!("Please visit zed's homepage: {url}ˇ"));
26241}
26242
26243#[gpui::test]
26244async fn test_paste_plain_text_from_other_app_replaces_selection_without_creating_markdown_link(
26245 cx: &mut gpui::TestAppContext,
26246) {
26247 init_test(cx, |_| {});
26248
26249 let text = "Awesome";
26250
26251 let markdown_language = Arc::new(Language::new(
26252 LanguageConfig {
26253 name: "Markdown".into(),
26254 ..LanguageConfig::default()
26255 },
26256 None,
26257 ));
26258
26259 let mut cx = EditorTestContext::new(cx).await;
26260 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26261 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat»");
26262
26263 cx.update_editor(|editor, window, cx| {
26264 cx.write_to_clipboard(ClipboardItem::new_string(text.to_string()));
26265 editor.paste(&Paste, window, cx);
26266 });
26267
26268 cx.assert_editor_state(&format!("Hello, {text}ˇ.\nZed is {text}ˇ"));
26269}
26270
26271#[gpui::test]
26272async fn test_paste_url_from_other_app_without_creating_markdown_link_in_non_markdown_language(
26273 cx: &mut gpui::TestAppContext,
26274) {
26275 init_test(cx, |_| {});
26276
26277 let url = "https://zed.dev";
26278
26279 let markdown_language = Arc::new(Language::new(
26280 LanguageConfig {
26281 name: "Rust".into(),
26282 ..LanguageConfig::default()
26283 },
26284 None,
26285 ));
26286
26287 let mut cx = EditorTestContext::new(cx).await;
26288 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26289 cx.set_state("// Hello, «editorˇ».\n// Zed is «ˇgreat» (see this link: ˇ)");
26290
26291 cx.update_editor(|editor, window, cx| {
26292 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26293 editor.paste(&Paste, window, cx);
26294 });
26295
26296 cx.assert_editor_state(&format!(
26297 "// Hello, {url}ˇ.\n// Zed is {url}ˇ (see this link: {url}ˇ)"
26298 ));
26299}
26300
26301#[gpui::test]
26302async fn test_paste_url_from_other_app_creates_markdown_link_selectively_in_multi_buffer(
26303 cx: &mut TestAppContext,
26304) {
26305 init_test(cx, |_| {});
26306
26307 let url = "https://zed.dev";
26308
26309 let markdown_language = Arc::new(Language::new(
26310 LanguageConfig {
26311 name: "Markdown".into(),
26312 ..LanguageConfig::default()
26313 },
26314 None,
26315 ));
26316
26317 let (editor, cx) = cx.add_window_view(|window, cx| {
26318 let multi_buffer = MultiBuffer::build_multi(
26319 [
26320 ("this will embed -> link", vec![Point::row_range(0..1)]),
26321 ("this will replace -> link", vec![Point::row_range(0..1)]),
26322 ],
26323 cx,
26324 );
26325 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
26326 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26327 s.select_ranges(vec![
26328 Point::new(0, 19)..Point::new(0, 23),
26329 Point::new(1, 21)..Point::new(1, 25),
26330 ])
26331 });
26332 let first_buffer_id = multi_buffer
26333 .read(cx)
26334 .excerpt_buffer_ids()
26335 .into_iter()
26336 .next()
26337 .unwrap();
26338 let first_buffer = multi_buffer.read(cx).buffer(first_buffer_id).unwrap();
26339 first_buffer.update(cx, |buffer, cx| {
26340 buffer.set_language(Some(markdown_language.clone()), cx);
26341 });
26342
26343 editor
26344 });
26345 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
26346
26347 cx.update_editor(|editor, window, cx| {
26348 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26349 editor.paste(&Paste, window, cx);
26350 });
26351
26352 cx.assert_editor_state(&format!(
26353 "this will embed -> [link]({url})ˇ\nthis will replace -> {url}ˇ"
26354 ));
26355}
26356
26357#[gpui::test]
26358async fn test_race_in_multibuffer_save(cx: &mut TestAppContext) {
26359 init_test(cx, |_| {});
26360
26361 let fs = FakeFs::new(cx.executor());
26362 fs.insert_tree(
26363 path!("/project"),
26364 json!({
26365 "first.rs": "# First Document\nSome content here.",
26366 "second.rs": "Plain text content for second file.",
26367 }),
26368 )
26369 .await;
26370
26371 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
26372 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
26373 let cx = &mut VisualTestContext::from_window(*workspace, cx);
26374
26375 let language = rust_lang();
26376 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
26377 language_registry.add(language.clone());
26378 let mut fake_servers = language_registry.register_fake_lsp(
26379 "Rust",
26380 FakeLspAdapter {
26381 ..FakeLspAdapter::default()
26382 },
26383 );
26384
26385 let buffer1 = project
26386 .update(cx, |project, cx| {
26387 project.open_local_buffer(PathBuf::from(path!("/project/first.rs")), cx)
26388 })
26389 .await
26390 .unwrap();
26391 let buffer2 = project
26392 .update(cx, |project, cx| {
26393 project.open_local_buffer(PathBuf::from(path!("/project/second.rs")), cx)
26394 })
26395 .await
26396 .unwrap();
26397
26398 let multi_buffer = cx.new(|cx| {
26399 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
26400 multi_buffer.set_excerpts_for_path(
26401 PathKey::for_buffer(&buffer1, cx),
26402 buffer1.clone(),
26403 [Point::zero()..buffer1.read(cx).max_point()],
26404 3,
26405 cx,
26406 );
26407 multi_buffer.set_excerpts_for_path(
26408 PathKey::for_buffer(&buffer2, cx),
26409 buffer2.clone(),
26410 [Point::zero()..buffer1.read(cx).max_point()],
26411 3,
26412 cx,
26413 );
26414 multi_buffer
26415 });
26416
26417 let (editor, cx) = cx.add_window_view(|window, cx| {
26418 Editor::new(
26419 EditorMode::full(),
26420 multi_buffer,
26421 Some(project.clone()),
26422 window,
26423 cx,
26424 )
26425 });
26426
26427 let fake_language_server = fake_servers.next().await.unwrap();
26428
26429 buffer1.update(cx, |buffer, cx| buffer.edit([(0..0, "hello!")], None, cx));
26430
26431 let save = editor.update_in(cx, |editor, window, cx| {
26432 assert!(editor.is_dirty(cx));
26433
26434 editor.save(
26435 SaveOptions {
26436 format: true,
26437 autosave: true,
26438 },
26439 project,
26440 window,
26441 cx,
26442 )
26443 });
26444 let (start_edit_tx, start_edit_rx) = oneshot::channel();
26445 let (done_edit_tx, done_edit_rx) = oneshot::channel();
26446 let mut done_edit_rx = Some(done_edit_rx);
26447 let mut start_edit_tx = Some(start_edit_tx);
26448
26449 fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| {
26450 start_edit_tx.take().unwrap().send(()).unwrap();
26451 let done_edit_rx = done_edit_rx.take().unwrap();
26452 async move {
26453 done_edit_rx.await.unwrap();
26454 Ok(None)
26455 }
26456 });
26457
26458 start_edit_rx.await.unwrap();
26459 buffer2
26460 .update(cx, |buffer, cx| buffer.edit([(0..0, "world!")], None, cx))
26461 .unwrap();
26462
26463 done_edit_tx.send(()).unwrap();
26464
26465 save.await.unwrap();
26466 cx.update(|_, cx| assert!(editor.is_dirty(cx)));
26467}
26468
26469#[track_caller]
26470fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
26471 editor
26472 .all_inlays(cx)
26473 .into_iter()
26474 .filter_map(|inlay| inlay.get_color())
26475 .map(Rgba::from)
26476 .collect()
26477}