1use super::*;
2use crate::{
3 JoinLines,
4 code_context_menus::CodeContextMenu,
5 edit_prediction_tests::FakeEditPredictionProvider,
6 linked_editing_ranges::LinkedEditingRanges,
7 scroll::scroll_amount::ScrollAmount,
8 test::{
9 assert_text_with_selections, build_editor,
10 editor_lsp_test_context::{EditorLspTestContext, git_commit_lang},
11 editor_test_context::EditorTestContext,
12 select_ranges,
13 },
14};
15use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
16use collections::HashMap;
17use futures::StreamExt;
18use gpui::{
19 BackgroundExecutor, DismissEvent, Rgba, SemanticVersion, TestAppContext, UpdateGlobal,
20 VisualTestContext, WindowBounds, WindowOptions, div,
21};
22use indoc::indoc;
23use language::{
24 BracketPairConfig,
25 Capability::ReadWrite,
26 DiagnosticSourceKind, FakeLspAdapter, IndentGuideSettings, LanguageConfig,
27 LanguageConfigOverride, LanguageMatcher, LanguageName, Override, Point,
28 language_settings::{
29 CompletionSettingsContent, FormatterList, LanguageSettingsContent, LspInsertMode,
30 SelectedFormatter,
31 },
32 tree_sitter_python,
33};
34use language_settings::Formatter;
35use lsp::CompletionParams;
36use multi_buffer::{IndentGuide, PathKey};
37use parking_lot::Mutex;
38use pretty_assertions::{assert_eq, assert_ne};
39use project::{
40 FakeFs,
41 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
42 project_settings::LspSettings,
43};
44use serde_json::{self, json};
45use settings::{
46 AllLanguageSettingsContent, IndentGuideBackgroundColoring, IndentGuideColoring,
47 ProjectSettingsContent,
48};
49use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
50use std::{
51 iter,
52 sync::atomic::{self, AtomicUsize},
53};
54use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
55use text::ToPoint as _;
56use unindent::Unindent;
57use util::{
58 assert_set_eq, path,
59 rel_path::rel_path,
60 test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
61 uri,
62};
63use workspace::{
64 CloseActiveItem, CloseAllItems, CloseOtherItems, MoveItemToPaneInDirection, NavigationEntry,
65 OpenOptions, ViewId,
66 invalid_buffer_view::InvalidBufferView,
67 item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
68 register_project_item,
69};
70
71#[gpui::test]
72fn test_edit_events(cx: &mut TestAppContext) {
73 init_test(cx, |_| {});
74
75 let buffer = cx.new(|cx| {
76 let mut buffer = language::Buffer::local("123456", cx);
77 buffer.set_group_interval(Duration::from_secs(1));
78 buffer
79 });
80
81 let events = Rc::new(RefCell::new(Vec::new()));
82 let editor1 = cx.add_window({
83 let events = events.clone();
84 |window, cx| {
85 let entity = cx.entity();
86 cx.subscribe_in(
87 &entity,
88 window,
89 move |_, _, event: &EditorEvent, _, _| match event {
90 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
91 EditorEvent::BufferEdited => {
92 events.borrow_mut().push(("editor1", "buffer edited"))
93 }
94 _ => {}
95 },
96 )
97 .detach();
98 Editor::for_buffer(buffer.clone(), None, window, cx)
99 }
100 });
101
102 let editor2 = cx.add_window({
103 let events = events.clone();
104 |window, cx| {
105 cx.subscribe_in(
106 &cx.entity(),
107 window,
108 move |_, _, event: &EditorEvent, _, _| match event {
109 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
110 EditorEvent::BufferEdited => {
111 events.borrow_mut().push(("editor2", "buffer edited"))
112 }
113 _ => {}
114 },
115 )
116 .detach();
117 Editor::for_buffer(buffer.clone(), None, window, cx)
118 }
119 });
120
121 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
122
123 // Mutating editor 1 will emit an `Edited` event only for that editor.
124 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
125 assert_eq!(
126 mem::take(&mut *events.borrow_mut()),
127 [
128 ("editor1", "edited"),
129 ("editor1", "buffer edited"),
130 ("editor2", "buffer edited"),
131 ]
132 );
133
134 // Mutating editor 2 will emit an `Edited` event only for that editor.
135 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
136 assert_eq!(
137 mem::take(&mut *events.borrow_mut()),
138 [
139 ("editor2", "edited"),
140 ("editor1", "buffer edited"),
141 ("editor2", "buffer edited"),
142 ]
143 );
144
145 // Undoing on editor 1 will emit an `Edited` event only for that editor.
146 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
147 assert_eq!(
148 mem::take(&mut *events.borrow_mut()),
149 [
150 ("editor1", "edited"),
151 ("editor1", "buffer edited"),
152 ("editor2", "buffer edited"),
153 ]
154 );
155
156 // Redoing on editor 1 will emit an `Edited` event only for that editor.
157 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
158 assert_eq!(
159 mem::take(&mut *events.borrow_mut()),
160 [
161 ("editor1", "edited"),
162 ("editor1", "buffer edited"),
163 ("editor2", "buffer edited"),
164 ]
165 );
166
167 // Undoing on editor 2 will emit an `Edited` event only for that editor.
168 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
169 assert_eq!(
170 mem::take(&mut *events.borrow_mut()),
171 [
172 ("editor2", "edited"),
173 ("editor1", "buffer edited"),
174 ("editor2", "buffer edited"),
175 ]
176 );
177
178 // Redoing on editor 2 will emit an `Edited` event only for that editor.
179 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
180 assert_eq!(
181 mem::take(&mut *events.borrow_mut()),
182 [
183 ("editor2", "edited"),
184 ("editor1", "buffer edited"),
185 ("editor2", "buffer edited"),
186 ]
187 );
188
189 // No event is emitted when the mutation is a no-op.
190 _ = editor2.update(cx, |editor, window, cx| {
191 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
192 s.select_ranges([0..0])
193 });
194
195 editor.backspace(&Backspace, window, cx);
196 });
197 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
198}
199
200#[gpui::test]
201fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
202 init_test(cx, |_| {});
203
204 let mut now = Instant::now();
205 let group_interval = Duration::from_millis(1);
206 let buffer = cx.new(|cx| {
207 let mut buf = language::Buffer::local("123456", cx);
208 buf.set_group_interval(group_interval);
209 buf
210 });
211 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
212 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
213
214 _ = editor.update(cx, |editor, window, cx| {
215 editor.start_transaction_at(now, window, cx);
216 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
217 s.select_ranges([2..4])
218 });
219
220 editor.insert("cd", window, cx);
221 editor.end_transaction_at(now, cx);
222 assert_eq!(editor.text(cx), "12cd56");
223 assert_eq!(editor.selections.ranges(cx), vec![4..4]);
224
225 editor.start_transaction_at(now, window, cx);
226 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
227 s.select_ranges([4..5])
228 });
229 editor.insert("e", window, cx);
230 editor.end_transaction_at(now, cx);
231 assert_eq!(editor.text(cx), "12cde6");
232 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
233
234 now += group_interval + Duration::from_millis(1);
235 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
236 s.select_ranges([2..2])
237 });
238
239 // Simulate an edit in another editor
240 buffer.update(cx, |buffer, cx| {
241 buffer.start_transaction_at(now, cx);
242 buffer.edit([(0..1, "a")], None, cx);
243 buffer.edit([(1..1, "b")], None, cx);
244 buffer.end_transaction_at(now, cx);
245 });
246
247 assert_eq!(editor.text(cx), "ab2cde6");
248 assert_eq!(editor.selections.ranges(cx), vec![3..3]);
249
250 // Last transaction happened past the group interval in a different editor.
251 // Undo it individually and don't restore selections.
252 editor.undo(&Undo, window, cx);
253 assert_eq!(editor.text(cx), "12cde6");
254 assert_eq!(editor.selections.ranges(cx), vec![2..2]);
255
256 // First two transactions happened within the group interval in this editor.
257 // Undo them together and restore selections.
258 editor.undo(&Undo, window, cx);
259 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
260 assert_eq!(editor.text(cx), "123456");
261 assert_eq!(editor.selections.ranges(cx), vec![0..0]);
262
263 // Redo the first two transactions together.
264 editor.redo(&Redo, window, cx);
265 assert_eq!(editor.text(cx), "12cde6");
266 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
267
268 // Redo the last transaction on its own.
269 editor.redo(&Redo, window, cx);
270 assert_eq!(editor.text(cx), "ab2cde6");
271 assert_eq!(editor.selections.ranges(cx), vec![6..6]);
272
273 // Test empty transactions.
274 editor.start_transaction_at(now, window, cx);
275 editor.end_transaction_at(now, cx);
276 editor.undo(&Undo, window, cx);
277 assert_eq!(editor.text(cx), "12cde6");
278 });
279}
280
281#[gpui::test]
282fn test_ime_composition(cx: &mut TestAppContext) {
283 init_test(cx, |_| {});
284
285 let buffer = cx.new(|cx| {
286 let mut buffer = language::Buffer::local("abcde", cx);
287 // Ensure automatic grouping doesn't occur.
288 buffer.set_group_interval(Duration::ZERO);
289 buffer
290 });
291
292 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
293 cx.add_window(|window, cx| {
294 let mut editor = build_editor(buffer.clone(), window, cx);
295
296 // Start a new IME composition.
297 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
298 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
299 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
300 assert_eq!(editor.text(cx), "äbcde");
301 assert_eq!(
302 editor.marked_text_ranges(cx),
303 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
304 );
305
306 // Finalize IME composition.
307 editor.replace_text_in_range(None, "ā", window, cx);
308 assert_eq!(editor.text(cx), "ābcde");
309 assert_eq!(editor.marked_text_ranges(cx), None);
310
311 // IME composition edits are grouped and are undone/redone at once.
312 editor.undo(&Default::default(), window, cx);
313 assert_eq!(editor.text(cx), "abcde");
314 assert_eq!(editor.marked_text_ranges(cx), None);
315 editor.redo(&Default::default(), window, cx);
316 assert_eq!(editor.text(cx), "ābcde");
317 assert_eq!(editor.marked_text_ranges(cx), None);
318
319 // Start a new IME composition.
320 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
321 assert_eq!(
322 editor.marked_text_ranges(cx),
323 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
324 );
325
326 // Undoing during an IME composition cancels it.
327 editor.undo(&Default::default(), window, cx);
328 assert_eq!(editor.text(cx), "ābcde");
329 assert_eq!(editor.marked_text_ranges(cx), None);
330
331 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
332 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
333 assert_eq!(editor.text(cx), "ābcdè");
334 assert_eq!(
335 editor.marked_text_ranges(cx),
336 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
337 );
338
339 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
340 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
341 assert_eq!(editor.text(cx), "ābcdę");
342 assert_eq!(editor.marked_text_ranges(cx), None);
343
344 // Start a new IME composition with multiple cursors.
345 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
346 s.select_ranges([
347 OffsetUtf16(1)..OffsetUtf16(1),
348 OffsetUtf16(3)..OffsetUtf16(3),
349 OffsetUtf16(5)..OffsetUtf16(5),
350 ])
351 });
352 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
353 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
354 assert_eq!(
355 editor.marked_text_ranges(cx),
356 Some(vec![
357 OffsetUtf16(0)..OffsetUtf16(3),
358 OffsetUtf16(4)..OffsetUtf16(7),
359 OffsetUtf16(8)..OffsetUtf16(11)
360 ])
361 );
362
363 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
364 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
365 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
366 assert_eq!(
367 editor.marked_text_ranges(cx),
368 Some(vec![
369 OffsetUtf16(1)..OffsetUtf16(2),
370 OffsetUtf16(5)..OffsetUtf16(6),
371 OffsetUtf16(9)..OffsetUtf16(10)
372 ])
373 );
374
375 // Finalize IME composition with multiple cursors.
376 editor.replace_text_in_range(Some(9..10), "2", window, cx);
377 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
378 assert_eq!(editor.marked_text_ranges(cx), None);
379
380 editor
381 });
382}
383
384#[gpui::test]
385fn test_selection_with_mouse(cx: &mut TestAppContext) {
386 init_test(cx, |_| {});
387
388 let editor = cx.add_window(|window, cx| {
389 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
390 build_editor(buffer, window, cx)
391 });
392
393 _ = editor.update(cx, |editor, window, cx| {
394 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
395 });
396 assert_eq!(
397 editor
398 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
399 .unwrap(),
400 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
401 );
402
403 _ = editor.update(cx, |editor, window, cx| {
404 editor.update_selection(
405 DisplayPoint::new(DisplayRow(3), 3),
406 0,
407 gpui::Point::<f32>::default(),
408 window,
409 cx,
410 );
411 });
412
413 assert_eq!(
414 editor
415 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
416 .unwrap(),
417 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
418 );
419
420 _ = editor.update(cx, |editor, window, cx| {
421 editor.update_selection(
422 DisplayPoint::new(DisplayRow(1), 1),
423 0,
424 gpui::Point::<f32>::default(),
425 window,
426 cx,
427 );
428 });
429
430 assert_eq!(
431 editor
432 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
433 .unwrap(),
434 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
435 );
436
437 _ = editor.update(cx, |editor, window, cx| {
438 editor.end_selection(window, cx);
439 editor.update_selection(
440 DisplayPoint::new(DisplayRow(3), 3),
441 0,
442 gpui::Point::<f32>::default(),
443 window,
444 cx,
445 );
446 });
447
448 assert_eq!(
449 editor
450 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
451 .unwrap(),
452 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
453 );
454
455 _ = editor.update(cx, |editor, window, cx| {
456 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
457 editor.update_selection(
458 DisplayPoint::new(DisplayRow(0), 0),
459 0,
460 gpui::Point::<f32>::default(),
461 window,
462 cx,
463 );
464 });
465
466 assert_eq!(
467 editor
468 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
469 .unwrap(),
470 [
471 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
472 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
473 ]
474 );
475
476 _ = editor.update(cx, |editor, window, cx| {
477 editor.end_selection(window, cx);
478 });
479
480 assert_eq!(
481 editor
482 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
483 .unwrap(),
484 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
485 );
486}
487
488#[gpui::test]
489fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
490 init_test(cx, |_| {});
491
492 let editor = cx.add_window(|window, cx| {
493 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
494 build_editor(buffer, window, cx)
495 });
496
497 _ = editor.update(cx, |editor, window, cx| {
498 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
499 });
500
501 _ = editor.update(cx, |editor, window, cx| {
502 editor.end_selection(window, cx);
503 });
504
505 _ = editor.update(cx, |editor, window, cx| {
506 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
507 });
508
509 _ = editor.update(cx, |editor, window, cx| {
510 editor.end_selection(window, cx);
511 });
512
513 assert_eq!(
514 editor
515 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
516 .unwrap(),
517 [
518 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
519 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
520 ]
521 );
522
523 _ = editor.update(cx, |editor, window, cx| {
524 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
525 });
526
527 _ = editor.update(cx, |editor, window, cx| {
528 editor.end_selection(window, cx);
529 });
530
531 assert_eq!(
532 editor
533 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
534 .unwrap(),
535 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
536 );
537}
538
539#[gpui::test]
540fn test_canceling_pending_selection(cx: &mut TestAppContext) {
541 init_test(cx, |_| {});
542
543 let editor = cx.add_window(|window, cx| {
544 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
545 build_editor(buffer, window, cx)
546 });
547
548 _ = editor.update(cx, |editor, window, cx| {
549 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
550 assert_eq!(
551 editor.selections.display_ranges(cx),
552 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
553 );
554 });
555
556 _ = editor.update(cx, |editor, window, cx| {
557 editor.update_selection(
558 DisplayPoint::new(DisplayRow(3), 3),
559 0,
560 gpui::Point::<f32>::default(),
561 window,
562 cx,
563 );
564 assert_eq!(
565 editor.selections.display_ranges(cx),
566 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
567 );
568 });
569
570 _ = editor.update(cx, |editor, window, cx| {
571 editor.cancel(&Cancel, window, cx);
572 editor.update_selection(
573 DisplayPoint::new(DisplayRow(1), 1),
574 0,
575 gpui::Point::<f32>::default(),
576 window,
577 cx,
578 );
579 assert_eq!(
580 editor.selections.display_ranges(cx),
581 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
582 );
583 });
584}
585
586#[gpui::test]
587fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
588 init_test(cx, |_| {});
589
590 let editor = cx.add_window(|window, cx| {
591 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
592 build_editor(buffer, window, cx)
593 });
594
595 _ = editor.update(cx, |editor, window, cx| {
596 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
597 assert_eq!(
598 editor.selections.display_ranges(cx),
599 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
600 );
601
602 editor.move_down(&Default::default(), window, cx);
603 assert_eq!(
604 editor.selections.display_ranges(cx),
605 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
606 );
607
608 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
609 assert_eq!(
610 editor.selections.display_ranges(cx),
611 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
612 );
613
614 editor.move_up(&Default::default(), window, cx);
615 assert_eq!(
616 editor.selections.display_ranges(cx),
617 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
618 );
619 });
620}
621
622#[gpui::test]
623fn test_clone(cx: &mut TestAppContext) {
624 init_test(cx, |_| {});
625
626 let (text, selection_ranges) = marked_text_ranges(
627 indoc! {"
628 one
629 two
630 threeˇ
631 four
632 fiveˇ
633 "},
634 true,
635 );
636
637 let editor = cx.add_window(|window, cx| {
638 let buffer = MultiBuffer::build_simple(&text, cx);
639 build_editor(buffer, window, cx)
640 });
641
642 _ = editor.update(cx, |editor, window, cx| {
643 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
644 s.select_ranges(selection_ranges.clone())
645 });
646 editor.fold_creases(
647 vec![
648 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
649 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
650 ],
651 true,
652 window,
653 cx,
654 );
655 });
656
657 let cloned_editor = editor
658 .update(cx, |editor, _, cx| {
659 cx.open_window(Default::default(), |window, cx| {
660 cx.new(|cx| editor.clone(window, cx))
661 })
662 })
663 .unwrap()
664 .unwrap();
665
666 let snapshot = editor
667 .update(cx, |e, window, cx| e.snapshot(window, cx))
668 .unwrap();
669 let cloned_snapshot = cloned_editor
670 .update(cx, |e, window, cx| e.snapshot(window, cx))
671 .unwrap();
672
673 assert_eq!(
674 cloned_editor
675 .update(cx, |e, _, cx| e.display_text(cx))
676 .unwrap(),
677 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
678 );
679 assert_eq!(
680 cloned_snapshot
681 .folds_in_range(0..text.len())
682 .collect::<Vec<_>>(),
683 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
684 );
685 assert_set_eq!(
686 cloned_editor
687 .update(cx, |editor, _, cx| editor.selections.ranges::<Point>(cx))
688 .unwrap(),
689 editor
690 .update(cx, |editor, _, cx| editor.selections.ranges(cx))
691 .unwrap()
692 );
693 assert_set_eq!(
694 cloned_editor
695 .update(cx, |e, _window, cx| e.selections.display_ranges(cx))
696 .unwrap(),
697 editor
698 .update(cx, |e, _, cx| e.selections.display_ranges(cx))
699 .unwrap()
700 );
701}
702
703#[gpui::test]
704async fn test_navigation_history(cx: &mut TestAppContext) {
705 init_test(cx, |_| {});
706
707 use workspace::item::Item;
708
709 let fs = FakeFs::new(cx.executor());
710 let project = Project::test(fs, [], cx).await;
711 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
712 let pane = workspace
713 .update(cx, |workspace, _, _| workspace.active_pane().clone())
714 .unwrap();
715
716 _ = workspace.update(cx, |_v, window, cx| {
717 cx.new(|cx| {
718 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
719 let mut editor = build_editor(buffer, window, cx);
720 let handle = cx.entity();
721 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
722
723 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
724 editor.nav_history.as_mut().unwrap().pop_backward(cx)
725 }
726
727 // Move the cursor a small distance.
728 // Nothing is added to the navigation history.
729 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
730 s.select_display_ranges([
731 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
732 ])
733 });
734 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
735 s.select_display_ranges([
736 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
737 ])
738 });
739 assert!(pop_history(&mut editor, cx).is_none());
740
741 // Move the cursor a large distance.
742 // The history can jump back to the previous position.
743 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
744 s.select_display_ranges([
745 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
746 ])
747 });
748 let nav_entry = pop_history(&mut editor, cx).unwrap();
749 editor.navigate(nav_entry.data.unwrap(), window, cx);
750 assert_eq!(nav_entry.item.id(), cx.entity_id());
751 assert_eq!(
752 editor.selections.display_ranges(cx),
753 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
754 );
755 assert!(pop_history(&mut editor, cx).is_none());
756
757 // Move the cursor a small distance via the mouse.
758 // Nothing is added to the navigation history.
759 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
760 editor.end_selection(window, cx);
761 assert_eq!(
762 editor.selections.display_ranges(cx),
763 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
764 );
765 assert!(pop_history(&mut editor, cx).is_none());
766
767 // Move the cursor a large distance via the mouse.
768 // The history can jump back to the previous position.
769 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
770 editor.end_selection(window, cx);
771 assert_eq!(
772 editor.selections.display_ranges(cx),
773 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
774 );
775 let nav_entry = pop_history(&mut editor, cx).unwrap();
776 editor.navigate(nav_entry.data.unwrap(), window, cx);
777 assert_eq!(nav_entry.item.id(), cx.entity_id());
778 assert_eq!(
779 editor.selections.display_ranges(cx),
780 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
781 );
782 assert!(pop_history(&mut editor, cx).is_none());
783
784 // Set scroll position to check later
785 editor.set_scroll_position(gpui::Point::<f32>::new(5.5, 5.5), window, cx);
786 let original_scroll_position = editor.scroll_manager.anchor();
787
788 // Jump to the end of the document and adjust scroll
789 editor.move_to_end(&MoveToEnd, window, cx);
790 editor.set_scroll_position(gpui::Point::<f32>::new(-2.5, -0.5), window, cx);
791 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
792
793 let nav_entry = pop_history(&mut editor, cx).unwrap();
794 editor.navigate(nav_entry.data.unwrap(), window, cx);
795 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
796
797 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
798 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
799 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
800 let invalid_point = Point::new(9999, 0);
801 editor.navigate(
802 Box::new(NavigationData {
803 cursor_anchor: invalid_anchor,
804 cursor_position: invalid_point,
805 scroll_anchor: ScrollAnchor {
806 anchor: invalid_anchor,
807 offset: Default::default(),
808 },
809 scroll_top_row: invalid_point.row,
810 }),
811 window,
812 cx,
813 );
814 assert_eq!(
815 editor.selections.display_ranges(cx),
816 &[editor.max_point(cx)..editor.max_point(cx)]
817 );
818 assert_eq!(
819 editor.scroll_position(cx),
820 gpui::Point::new(0., editor.max_point(cx).row().as_f32())
821 );
822
823 editor
824 })
825 });
826}
827
828#[gpui::test]
829fn test_cancel(cx: &mut TestAppContext) {
830 init_test(cx, |_| {});
831
832 let editor = cx.add_window(|window, cx| {
833 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
834 build_editor(buffer, window, cx)
835 });
836
837 _ = editor.update(cx, |editor, window, cx| {
838 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
839 editor.update_selection(
840 DisplayPoint::new(DisplayRow(1), 1),
841 0,
842 gpui::Point::<f32>::default(),
843 window,
844 cx,
845 );
846 editor.end_selection(window, cx);
847
848 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
849 editor.update_selection(
850 DisplayPoint::new(DisplayRow(0), 3),
851 0,
852 gpui::Point::<f32>::default(),
853 window,
854 cx,
855 );
856 editor.end_selection(window, cx);
857 assert_eq!(
858 editor.selections.display_ranges(cx),
859 [
860 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
861 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
862 ]
863 );
864 });
865
866 _ = editor.update(cx, |editor, window, cx| {
867 editor.cancel(&Cancel, window, cx);
868 assert_eq!(
869 editor.selections.display_ranges(cx),
870 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
871 );
872 });
873
874 _ = editor.update(cx, |editor, window, cx| {
875 editor.cancel(&Cancel, window, cx);
876 assert_eq!(
877 editor.selections.display_ranges(cx),
878 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
879 );
880 });
881}
882
883#[gpui::test]
884fn test_fold_action(cx: &mut TestAppContext) {
885 init_test(cx, |_| {});
886
887 let editor = cx.add_window(|window, cx| {
888 let buffer = MultiBuffer::build_simple(
889 &"
890 impl Foo {
891 // Hello!
892
893 fn a() {
894 1
895 }
896
897 fn b() {
898 2
899 }
900
901 fn c() {
902 3
903 }
904 }
905 "
906 .unindent(),
907 cx,
908 );
909 build_editor(buffer, window, cx)
910 });
911
912 _ = editor.update(cx, |editor, window, cx| {
913 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
914 s.select_display_ranges([
915 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
916 ]);
917 });
918 editor.fold(&Fold, window, cx);
919 assert_eq!(
920 editor.display_text(cx),
921 "
922 impl Foo {
923 // Hello!
924
925 fn a() {
926 1
927 }
928
929 fn b() {⋯
930 }
931
932 fn c() {⋯
933 }
934 }
935 "
936 .unindent(),
937 );
938
939 editor.fold(&Fold, window, cx);
940 assert_eq!(
941 editor.display_text(cx),
942 "
943 impl Foo {⋯
944 }
945 "
946 .unindent(),
947 );
948
949 editor.unfold_lines(&UnfoldLines, window, cx);
950 assert_eq!(
951 editor.display_text(cx),
952 "
953 impl Foo {
954 // Hello!
955
956 fn a() {
957 1
958 }
959
960 fn b() {⋯
961 }
962
963 fn c() {⋯
964 }
965 }
966 "
967 .unindent(),
968 );
969
970 editor.unfold_lines(&UnfoldLines, window, cx);
971 assert_eq!(
972 editor.display_text(cx),
973 editor.buffer.read(cx).read(cx).text()
974 );
975 });
976}
977
978#[gpui::test]
979fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
980 init_test(cx, |_| {});
981
982 let editor = cx.add_window(|window, cx| {
983 let buffer = MultiBuffer::build_simple(
984 &"
985 class Foo:
986 # Hello!
987
988 def a():
989 print(1)
990
991 def b():
992 print(2)
993
994 def c():
995 print(3)
996 "
997 .unindent(),
998 cx,
999 );
1000 build_editor(buffer, window, cx)
1001 });
1002
1003 _ = editor.update(cx, |editor, window, cx| {
1004 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1005 s.select_display_ranges([
1006 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
1007 ]);
1008 });
1009 editor.fold(&Fold, window, cx);
1010 assert_eq!(
1011 editor.display_text(cx),
1012 "
1013 class Foo:
1014 # Hello!
1015
1016 def a():
1017 print(1)
1018
1019 def b():⋯
1020
1021 def c():⋯
1022 "
1023 .unindent(),
1024 );
1025
1026 editor.fold(&Fold, window, cx);
1027 assert_eq!(
1028 editor.display_text(cx),
1029 "
1030 class Foo:⋯
1031 "
1032 .unindent(),
1033 );
1034
1035 editor.unfold_lines(&UnfoldLines, window, cx);
1036 assert_eq!(
1037 editor.display_text(cx),
1038 "
1039 class Foo:
1040 # Hello!
1041
1042 def a():
1043 print(1)
1044
1045 def b():⋯
1046
1047 def c():⋯
1048 "
1049 .unindent(),
1050 );
1051
1052 editor.unfold_lines(&UnfoldLines, window, cx);
1053 assert_eq!(
1054 editor.display_text(cx),
1055 editor.buffer.read(cx).read(cx).text()
1056 );
1057 });
1058}
1059
1060#[gpui::test]
1061fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1062 init_test(cx, |_| {});
1063
1064 let editor = cx.add_window(|window, cx| {
1065 let buffer = MultiBuffer::build_simple(
1066 &"
1067 class Foo:
1068 # Hello!
1069
1070 def a():
1071 print(1)
1072
1073 def b():
1074 print(2)
1075
1076
1077 def c():
1078 print(3)
1079
1080
1081 "
1082 .unindent(),
1083 cx,
1084 );
1085 build_editor(buffer, window, cx)
1086 });
1087
1088 _ = editor.update(cx, |editor, window, cx| {
1089 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1090 s.select_display_ranges([
1091 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1092 ]);
1093 });
1094 editor.fold(&Fold, window, cx);
1095 assert_eq!(
1096 editor.display_text(cx),
1097 "
1098 class Foo:
1099 # Hello!
1100
1101 def a():
1102 print(1)
1103
1104 def b():⋯
1105
1106
1107 def c():⋯
1108
1109
1110 "
1111 .unindent(),
1112 );
1113
1114 editor.fold(&Fold, window, cx);
1115 assert_eq!(
1116 editor.display_text(cx),
1117 "
1118 class Foo:⋯
1119
1120
1121 "
1122 .unindent(),
1123 );
1124
1125 editor.unfold_lines(&UnfoldLines, window, cx);
1126 assert_eq!(
1127 editor.display_text(cx),
1128 "
1129 class Foo:
1130 # Hello!
1131
1132 def a():
1133 print(1)
1134
1135 def b():⋯
1136
1137
1138 def c():⋯
1139
1140
1141 "
1142 .unindent(),
1143 );
1144
1145 editor.unfold_lines(&UnfoldLines, window, cx);
1146 assert_eq!(
1147 editor.display_text(cx),
1148 editor.buffer.read(cx).read(cx).text()
1149 );
1150 });
1151}
1152
1153#[gpui::test]
1154fn test_fold_at_level(cx: &mut TestAppContext) {
1155 init_test(cx, |_| {});
1156
1157 let editor = cx.add_window(|window, cx| {
1158 let buffer = MultiBuffer::build_simple(
1159 &"
1160 class Foo:
1161 # Hello!
1162
1163 def a():
1164 print(1)
1165
1166 def b():
1167 print(2)
1168
1169
1170 class Bar:
1171 # World!
1172
1173 def a():
1174 print(1)
1175
1176 def b():
1177 print(2)
1178
1179
1180 "
1181 .unindent(),
1182 cx,
1183 );
1184 build_editor(buffer, window, cx)
1185 });
1186
1187 _ = editor.update(cx, |editor, window, cx| {
1188 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1189 assert_eq!(
1190 editor.display_text(cx),
1191 "
1192 class Foo:
1193 # Hello!
1194
1195 def a():⋯
1196
1197 def b():⋯
1198
1199
1200 class Bar:
1201 # World!
1202
1203 def a():⋯
1204
1205 def b():⋯
1206
1207
1208 "
1209 .unindent(),
1210 );
1211
1212 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1213 assert_eq!(
1214 editor.display_text(cx),
1215 "
1216 class Foo:⋯
1217
1218
1219 class Bar:⋯
1220
1221
1222 "
1223 .unindent(),
1224 );
1225
1226 editor.unfold_all(&UnfoldAll, window, cx);
1227 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1228 assert_eq!(
1229 editor.display_text(cx),
1230 "
1231 class Foo:
1232 # Hello!
1233
1234 def a():
1235 print(1)
1236
1237 def b():
1238 print(2)
1239
1240
1241 class Bar:
1242 # World!
1243
1244 def a():
1245 print(1)
1246
1247 def b():
1248 print(2)
1249
1250
1251 "
1252 .unindent(),
1253 );
1254
1255 assert_eq!(
1256 editor.display_text(cx),
1257 editor.buffer.read(cx).read(cx).text()
1258 );
1259 });
1260}
1261
1262#[gpui::test]
1263fn test_move_cursor(cx: &mut TestAppContext) {
1264 init_test(cx, |_| {});
1265
1266 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1267 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1268
1269 buffer.update(cx, |buffer, cx| {
1270 buffer.edit(
1271 vec![
1272 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1273 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1274 ],
1275 None,
1276 cx,
1277 );
1278 });
1279 _ = editor.update(cx, |editor, window, cx| {
1280 assert_eq!(
1281 editor.selections.display_ranges(cx),
1282 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1283 );
1284
1285 editor.move_down(&MoveDown, window, cx);
1286 assert_eq!(
1287 editor.selections.display_ranges(cx),
1288 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1289 );
1290
1291 editor.move_right(&MoveRight, window, cx);
1292 assert_eq!(
1293 editor.selections.display_ranges(cx),
1294 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1295 );
1296
1297 editor.move_left(&MoveLeft, window, cx);
1298 assert_eq!(
1299 editor.selections.display_ranges(cx),
1300 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1301 );
1302
1303 editor.move_up(&MoveUp, window, cx);
1304 assert_eq!(
1305 editor.selections.display_ranges(cx),
1306 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1307 );
1308
1309 editor.move_to_end(&MoveToEnd, window, cx);
1310 assert_eq!(
1311 editor.selections.display_ranges(cx),
1312 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1313 );
1314
1315 editor.move_to_beginning(&MoveToBeginning, window, cx);
1316 assert_eq!(
1317 editor.selections.display_ranges(cx),
1318 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1319 );
1320
1321 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1322 s.select_display_ranges([
1323 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1324 ]);
1325 });
1326 editor.select_to_beginning(&SelectToBeginning, window, cx);
1327 assert_eq!(
1328 editor.selections.display_ranges(cx),
1329 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1330 );
1331
1332 editor.select_to_end(&SelectToEnd, window, cx);
1333 assert_eq!(
1334 editor.selections.display_ranges(cx),
1335 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1336 );
1337 });
1338}
1339
1340#[gpui::test]
1341fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1342 init_test(cx, |_| {});
1343
1344 let editor = cx.add_window(|window, cx| {
1345 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1346 build_editor(buffer, window, cx)
1347 });
1348
1349 assert_eq!('🟥'.len_utf8(), 4);
1350 assert_eq!('α'.len_utf8(), 2);
1351
1352 _ = editor.update(cx, |editor, window, cx| {
1353 editor.fold_creases(
1354 vec![
1355 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1356 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1357 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1358 ],
1359 true,
1360 window,
1361 cx,
1362 );
1363 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1364
1365 editor.move_right(&MoveRight, window, cx);
1366 assert_eq!(
1367 editor.selections.display_ranges(cx),
1368 &[empty_range(0, "🟥".len())]
1369 );
1370 editor.move_right(&MoveRight, window, cx);
1371 assert_eq!(
1372 editor.selections.display_ranges(cx),
1373 &[empty_range(0, "🟥🟧".len())]
1374 );
1375 editor.move_right(&MoveRight, window, cx);
1376 assert_eq!(
1377 editor.selections.display_ranges(cx),
1378 &[empty_range(0, "🟥🟧⋯".len())]
1379 );
1380
1381 editor.move_down(&MoveDown, window, cx);
1382 assert_eq!(
1383 editor.selections.display_ranges(cx),
1384 &[empty_range(1, "ab⋯e".len())]
1385 );
1386 editor.move_left(&MoveLeft, window, cx);
1387 assert_eq!(
1388 editor.selections.display_ranges(cx),
1389 &[empty_range(1, "ab⋯".len())]
1390 );
1391 editor.move_left(&MoveLeft, window, cx);
1392 assert_eq!(
1393 editor.selections.display_ranges(cx),
1394 &[empty_range(1, "ab".len())]
1395 );
1396 editor.move_left(&MoveLeft, window, cx);
1397 assert_eq!(
1398 editor.selections.display_ranges(cx),
1399 &[empty_range(1, "a".len())]
1400 );
1401
1402 editor.move_down(&MoveDown, window, cx);
1403 assert_eq!(
1404 editor.selections.display_ranges(cx),
1405 &[empty_range(2, "α".len())]
1406 );
1407 editor.move_right(&MoveRight, window, cx);
1408 assert_eq!(
1409 editor.selections.display_ranges(cx),
1410 &[empty_range(2, "αβ".len())]
1411 );
1412 editor.move_right(&MoveRight, window, cx);
1413 assert_eq!(
1414 editor.selections.display_ranges(cx),
1415 &[empty_range(2, "αβ⋯".len())]
1416 );
1417 editor.move_right(&MoveRight, window, cx);
1418 assert_eq!(
1419 editor.selections.display_ranges(cx),
1420 &[empty_range(2, "αβ⋯ε".len())]
1421 );
1422
1423 editor.move_up(&MoveUp, window, cx);
1424 assert_eq!(
1425 editor.selections.display_ranges(cx),
1426 &[empty_range(1, "ab⋯e".len())]
1427 );
1428 editor.move_down(&MoveDown, window, cx);
1429 assert_eq!(
1430 editor.selections.display_ranges(cx),
1431 &[empty_range(2, "αβ⋯ε".len())]
1432 );
1433 editor.move_up(&MoveUp, window, cx);
1434 assert_eq!(
1435 editor.selections.display_ranges(cx),
1436 &[empty_range(1, "ab⋯e".len())]
1437 );
1438
1439 editor.move_up(&MoveUp, window, cx);
1440 assert_eq!(
1441 editor.selections.display_ranges(cx),
1442 &[empty_range(0, "🟥🟧".len())]
1443 );
1444 editor.move_left(&MoveLeft, window, cx);
1445 assert_eq!(
1446 editor.selections.display_ranges(cx),
1447 &[empty_range(0, "🟥".len())]
1448 );
1449 editor.move_left(&MoveLeft, window, cx);
1450 assert_eq!(
1451 editor.selections.display_ranges(cx),
1452 &[empty_range(0, "".len())]
1453 );
1454 });
1455}
1456
1457#[gpui::test]
1458fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1459 init_test(cx, |_| {});
1460
1461 let editor = cx.add_window(|window, cx| {
1462 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1463 build_editor(buffer, window, cx)
1464 });
1465 _ = editor.update(cx, |editor, window, cx| {
1466 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1467 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1468 });
1469
1470 // moving above start of document should move selection to start of document,
1471 // but the next move down should still be at the original goal_x
1472 editor.move_up(&MoveUp, window, cx);
1473 assert_eq!(
1474 editor.selections.display_ranges(cx),
1475 &[empty_range(0, "".len())]
1476 );
1477
1478 editor.move_down(&MoveDown, window, cx);
1479 assert_eq!(
1480 editor.selections.display_ranges(cx),
1481 &[empty_range(1, "abcd".len())]
1482 );
1483
1484 editor.move_down(&MoveDown, window, cx);
1485 assert_eq!(
1486 editor.selections.display_ranges(cx),
1487 &[empty_range(2, "αβγ".len())]
1488 );
1489
1490 editor.move_down(&MoveDown, window, cx);
1491 assert_eq!(
1492 editor.selections.display_ranges(cx),
1493 &[empty_range(3, "abcd".len())]
1494 );
1495
1496 editor.move_down(&MoveDown, window, cx);
1497 assert_eq!(
1498 editor.selections.display_ranges(cx),
1499 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1500 );
1501
1502 // moving past end of document should not change goal_x
1503 editor.move_down(&MoveDown, window, cx);
1504 assert_eq!(
1505 editor.selections.display_ranges(cx),
1506 &[empty_range(5, "".len())]
1507 );
1508
1509 editor.move_down(&MoveDown, window, cx);
1510 assert_eq!(
1511 editor.selections.display_ranges(cx),
1512 &[empty_range(5, "".len())]
1513 );
1514
1515 editor.move_up(&MoveUp, window, cx);
1516 assert_eq!(
1517 editor.selections.display_ranges(cx),
1518 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1519 );
1520
1521 editor.move_up(&MoveUp, window, cx);
1522 assert_eq!(
1523 editor.selections.display_ranges(cx),
1524 &[empty_range(3, "abcd".len())]
1525 );
1526
1527 editor.move_up(&MoveUp, window, cx);
1528 assert_eq!(
1529 editor.selections.display_ranges(cx),
1530 &[empty_range(2, "αβγ".len())]
1531 );
1532 });
1533}
1534
1535#[gpui::test]
1536fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1537 init_test(cx, |_| {});
1538 let move_to_beg = MoveToBeginningOfLine {
1539 stop_at_soft_wraps: true,
1540 stop_at_indent: true,
1541 };
1542
1543 let delete_to_beg = DeleteToBeginningOfLine {
1544 stop_at_indent: false,
1545 };
1546
1547 let move_to_end = MoveToEndOfLine {
1548 stop_at_soft_wraps: true,
1549 };
1550
1551 let editor = cx.add_window(|window, cx| {
1552 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1553 build_editor(buffer, window, cx)
1554 });
1555 _ = editor.update(cx, |editor, window, cx| {
1556 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1557 s.select_display_ranges([
1558 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1559 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1560 ]);
1561 });
1562 });
1563
1564 _ = editor.update(cx, |editor, window, cx| {
1565 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1566 assert_eq!(
1567 editor.selections.display_ranges(cx),
1568 &[
1569 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1570 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1571 ]
1572 );
1573 });
1574
1575 _ = editor.update(cx, |editor, window, cx| {
1576 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1577 assert_eq!(
1578 editor.selections.display_ranges(cx),
1579 &[
1580 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1581 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1582 ]
1583 );
1584 });
1585
1586 _ = editor.update(cx, |editor, window, cx| {
1587 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1588 assert_eq!(
1589 editor.selections.display_ranges(cx),
1590 &[
1591 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1592 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1593 ]
1594 );
1595 });
1596
1597 _ = editor.update(cx, |editor, window, cx| {
1598 editor.move_to_end_of_line(&move_to_end, window, cx);
1599 assert_eq!(
1600 editor.selections.display_ranges(cx),
1601 &[
1602 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1603 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1604 ]
1605 );
1606 });
1607
1608 // Moving to the end of line again is a no-op.
1609 _ = editor.update(cx, |editor, window, cx| {
1610 editor.move_to_end_of_line(&move_to_end, window, cx);
1611 assert_eq!(
1612 editor.selections.display_ranges(cx),
1613 &[
1614 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1615 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1616 ]
1617 );
1618 });
1619
1620 _ = editor.update(cx, |editor, window, cx| {
1621 editor.move_left(&MoveLeft, window, cx);
1622 editor.select_to_beginning_of_line(
1623 &SelectToBeginningOfLine {
1624 stop_at_soft_wraps: true,
1625 stop_at_indent: true,
1626 },
1627 window,
1628 cx,
1629 );
1630 assert_eq!(
1631 editor.selections.display_ranges(cx),
1632 &[
1633 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1634 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1635 ]
1636 );
1637 });
1638
1639 _ = editor.update(cx, |editor, window, cx| {
1640 editor.select_to_beginning_of_line(
1641 &SelectToBeginningOfLine {
1642 stop_at_soft_wraps: true,
1643 stop_at_indent: true,
1644 },
1645 window,
1646 cx,
1647 );
1648 assert_eq!(
1649 editor.selections.display_ranges(cx),
1650 &[
1651 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1652 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1653 ]
1654 );
1655 });
1656
1657 _ = editor.update(cx, |editor, window, cx| {
1658 editor.select_to_beginning_of_line(
1659 &SelectToBeginningOfLine {
1660 stop_at_soft_wraps: true,
1661 stop_at_indent: true,
1662 },
1663 window,
1664 cx,
1665 );
1666 assert_eq!(
1667 editor.selections.display_ranges(cx),
1668 &[
1669 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1670 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1671 ]
1672 );
1673 });
1674
1675 _ = editor.update(cx, |editor, window, cx| {
1676 editor.select_to_end_of_line(
1677 &SelectToEndOfLine {
1678 stop_at_soft_wraps: true,
1679 },
1680 window,
1681 cx,
1682 );
1683 assert_eq!(
1684 editor.selections.display_ranges(cx),
1685 &[
1686 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1687 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1688 ]
1689 );
1690 });
1691
1692 _ = editor.update(cx, |editor, window, cx| {
1693 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1694 assert_eq!(editor.display_text(cx), "ab\n de");
1695 assert_eq!(
1696 editor.selections.display_ranges(cx),
1697 &[
1698 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1699 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1700 ]
1701 );
1702 });
1703
1704 _ = editor.update(cx, |editor, window, cx| {
1705 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1706 assert_eq!(editor.display_text(cx), "\n");
1707 assert_eq!(
1708 editor.selections.display_ranges(cx),
1709 &[
1710 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1711 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1712 ]
1713 );
1714 });
1715}
1716
1717#[gpui::test]
1718fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1719 init_test(cx, |_| {});
1720 let move_to_beg = MoveToBeginningOfLine {
1721 stop_at_soft_wraps: false,
1722 stop_at_indent: false,
1723 };
1724
1725 let move_to_end = MoveToEndOfLine {
1726 stop_at_soft_wraps: false,
1727 };
1728
1729 let editor = cx.add_window(|window, cx| {
1730 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1731 build_editor(buffer, window, cx)
1732 });
1733
1734 _ = editor.update(cx, |editor, window, cx| {
1735 editor.set_wrap_width(Some(140.0.into()), cx);
1736
1737 // We expect the following lines after wrapping
1738 // ```
1739 // thequickbrownfox
1740 // jumpedoverthelazydo
1741 // gs
1742 // ```
1743 // The final `gs` was soft-wrapped onto a new line.
1744 assert_eq!(
1745 "thequickbrownfox\njumpedoverthelaz\nydogs",
1746 editor.display_text(cx),
1747 );
1748
1749 // First, let's assert behavior on the first line, that was not soft-wrapped.
1750 // Start the cursor at the `k` on the first line
1751 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1752 s.select_display_ranges([
1753 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1754 ]);
1755 });
1756
1757 // Moving to the beginning of the line should put us at the beginning of the line.
1758 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1759 assert_eq!(
1760 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1761 editor.selections.display_ranges(cx)
1762 );
1763
1764 // Moving to the end of the line should put us at the end of the line.
1765 editor.move_to_end_of_line(&move_to_end, window, cx);
1766 assert_eq!(
1767 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1768 editor.selections.display_ranges(cx)
1769 );
1770
1771 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1772 // Start the cursor at the last line (`y` that was wrapped to a new line)
1773 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1774 s.select_display_ranges([
1775 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1776 ]);
1777 });
1778
1779 // Moving to the beginning of the line should put us at the start of the second line of
1780 // display text, i.e., the `j`.
1781 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1782 assert_eq!(
1783 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1784 editor.selections.display_ranges(cx)
1785 );
1786
1787 // Moving to the beginning of the line again should be a no-op.
1788 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1789 assert_eq!(
1790 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1791 editor.selections.display_ranges(cx)
1792 );
1793
1794 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1795 // next display line.
1796 editor.move_to_end_of_line(&move_to_end, window, cx);
1797 assert_eq!(
1798 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1799 editor.selections.display_ranges(cx)
1800 );
1801
1802 // Moving to the end of the line again should be a no-op.
1803 editor.move_to_end_of_line(&move_to_end, window, cx);
1804 assert_eq!(
1805 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1806 editor.selections.display_ranges(cx)
1807 );
1808 });
1809}
1810
1811#[gpui::test]
1812fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1813 init_test(cx, |_| {});
1814
1815 let move_to_beg = MoveToBeginningOfLine {
1816 stop_at_soft_wraps: true,
1817 stop_at_indent: true,
1818 };
1819
1820 let select_to_beg = SelectToBeginningOfLine {
1821 stop_at_soft_wraps: true,
1822 stop_at_indent: true,
1823 };
1824
1825 let delete_to_beg = DeleteToBeginningOfLine {
1826 stop_at_indent: true,
1827 };
1828
1829 let move_to_end = MoveToEndOfLine {
1830 stop_at_soft_wraps: false,
1831 };
1832
1833 let editor = cx.add_window(|window, cx| {
1834 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1835 build_editor(buffer, window, cx)
1836 });
1837
1838 _ = editor.update(cx, |editor, window, cx| {
1839 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1840 s.select_display_ranges([
1841 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1842 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1843 ]);
1844 });
1845
1846 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1847 // and the second cursor at the first non-whitespace character in the line.
1848 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1849 assert_eq!(
1850 editor.selections.display_ranges(cx),
1851 &[
1852 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1853 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1854 ]
1855 );
1856
1857 // Moving to the beginning of the line again should be a no-op for the first cursor,
1858 // and should move the second cursor to the beginning of the line.
1859 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1860 assert_eq!(
1861 editor.selections.display_ranges(cx),
1862 &[
1863 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1864 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1865 ]
1866 );
1867
1868 // Moving to the beginning of the line again should still be a no-op for the first cursor,
1869 // and should move the second cursor back to the first non-whitespace character in the line.
1870 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1871 assert_eq!(
1872 editor.selections.display_ranges(cx),
1873 &[
1874 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1875 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1876 ]
1877 );
1878
1879 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
1880 // and to the first non-whitespace character in the line for the second cursor.
1881 editor.move_to_end_of_line(&move_to_end, window, cx);
1882 editor.move_left(&MoveLeft, window, cx);
1883 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1884 assert_eq!(
1885 editor.selections.display_ranges(cx),
1886 &[
1887 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1888 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1889 ]
1890 );
1891
1892 // Selecting to the beginning of the line again should be a no-op for the first cursor,
1893 // and should select to the beginning of the line for the second cursor.
1894 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1895 assert_eq!(
1896 editor.selections.display_ranges(cx),
1897 &[
1898 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1899 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1900 ]
1901 );
1902
1903 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
1904 // and should delete to the first non-whitespace character in the line for the second cursor.
1905 editor.move_to_end_of_line(&move_to_end, window, cx);
1906 editor.move_left(&MoveLeft, window, cx);
1907 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1908 assert_eq!(editor.text(cx), "c\n f");
1909 });
1910}
1911
1912#[gpui::test]
1913fn test_beginning_of_line_with_cursor_between_line_start_and_indent(cx: &mut TestAppContext) {
1914 init_test(cx, |_| {});
1915
1916 let move_to_beg = MoveToBeginningOfLine {
1917 stop_at_soft_wraps: true,
1918 stop_at_indent: true,
1919 };
1920
1921 let editor = cx.add_window(|window, cx| {
1922 let buffer = MultiBuffer::build_simple(" hello\nworld", cx);
1923 build_editor(buffer, window, cx)
1924 });
1925
1926 _ = editor.update(cx, |editor, window, cx| {
1927 // test cursor between line_start and indent_start
1928 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1929 s.select_display_ranges([
1930 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3)
1931 ]);
1932 });
1933
1934 // cursor should move to line_start
1935 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1936 assert_eq!(
1937 editor.selections.display_ranges(cx),
1938 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1939 );
1940
1941 // cursor should move to indent_start
1942 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1943 assert_eq!(
1944 editor.selections.display_ranges(cx),
1945 &[DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 4)]
1946 );
1947
1948 // cursor should move to back to line_start
1949 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1950 assert_eq!(
1951 editor.selections.display_ranges(cx),
1952 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1953 );
1954 });
1955}
1956
1957#[gpui::test]
1958fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
1959 init_test(cx, |_| {});
1960
1961 let editor = cx.add_window(|window, cx| {
1962 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
1963 build_editor(buffer, window, cx)
1964 });
1965 _ = editor.update(cx, |editor, window, cx| {
1966 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1967 s.select_display_ranges([
1968 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
1969 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
1970 ])
1971 });
1972 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1973 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1974
1975 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1976 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
1977
1978 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1979 assert_selection_ranges("use ˇstd::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1980
1981 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1982 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1983
1984 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1985 assert_selection_ranges("ˇuse std::str::{foo, ˇbar}\n\n {baz.qux()}", editor, cx);
1986
1987 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1988 assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1989
1990 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1991 assert_selection_ranges("use stdˇ::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
1992
1993 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1994 assert_selection_ranges("use std::ˇstr::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1995
1996 editor.move_right(&MoveRight, window, cx);
1997 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1998 assert_selection_ranges(
1999 "use std::«ˇs»tr::{foo, bar}\n«ˇ\n» {baz.qux()}",
2000 editor,
2001 cx,
2002 );
2003
2004 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
2005 assert_selection_ranges(
2006 "use std«ˇ::s»tr::{foo, bar«ˇ}\n\n» {baz.qux()}",
2007 editor,
2008 cx,
2009 );
2010
2011 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
2012 assert_selection_ranges(
2013 "use std::«ˇs»tr::{foo, bar}«ˇ\n\n» {baz.qux()}",
2014 editor,
2015 cx,
2016 );
2017 });
2018}
2019
2020#[gpui::test]
2021fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
2022 init_test(cx, |_| {});
2023
2024 let editor = cx.add_window(|window, cx| {
2025 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
2026 build_editor(buffer, window, cx)
2027 });
2028
2029 _ = editor.update(cx, |editor, window, cx| {
2030 editor.set_wrap_width(Some(140.0.into()), cx);
2031 assert_eq!(
2032 editor.display_text(cx),
2033 "use one::{\n two::three::\n four::five\n};"
2034 );
2035
2036 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2037 s.select_display_ranges([
2038 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
2039 ]);
2040 });
2041
2042 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2043 assert_eq!(
2044 editor.selections.display_ranges(cx),
2045 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
2046 );
2047
2048 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2049 assert_eq!(
2050 editor.selections.display_ranges(cx),
2051 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2052 );
2053
2054 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2055 assert_eq!(
2056 editor.selections.display_ranges(cx),
2057 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2058 );
2059
2060 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2061 assert_eq!(
2062 editor.selections.display_ranges(cx),
2063 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2064 );
2065
2066 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2067 assert_eq!(
2068 editor.selections.display_ranges(cx),
2069 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2070 );
2071
2072 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2073 assert_eq!(
2074 editor.selections.display_ranges(cx),
2075 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2076 );
2077 });
2078}
2079
2080#[gpui::test]
2081async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2082 init_test(cx, |_| {});
2083 let mut cx = EditorTestContext::new(cx).await;
2084
2085 let line_height = cx.editor(|editor, window, _| {
2086 editor
2087 .style()
2088 .unwrap()
2089 .text
2090 .line_height_in_pixels(window.rem_size())
2091 });
2092 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2093
2094 cx.set_state(
2095 &r#"ˇone
2096 two
2097
2098 three
2099 fourˇ
2100 five
2101
2102 six"#
2103 .unindent(),
2104 );
2105
2106 cx.update_editor(|editor, window, cx| {
2107 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2108 });
2109 cx.assert_editor_state(
2110 &r#"one
2111 two
2112 ˇ
2113 three
2114 four
2115 five
2116 ˇ
2117 six"#
2118 .unindent(),
2119 );
2120
2121 cx.update_editor(|editor, window, cx| {
2122 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2123 });
2124 cx.assert_editor_state(
2125 &r#"one
2126 two
2127
2128 three
2129 four
2130 five
2131 ˇ
2132 sixˇ"#
2133 .unindent(),
2134 );
2135
2136 cx.update_editor(|editor, window, cx| {
2137 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2138 });
2139 cx.assert_editor_state(
2140 &r#"one
2141 two
2142
2143 three
2144 four
2145 five
2146
2147 sixˇ"#
2148 .unindent(),
2149 );
2150
2151 cx.update_editor(|editor, window, cx| {
2152 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2153 });
2154 cx.assert_editor_state(
2155 &r#"one
2156 two
2157
2158 three
2159 four
2160 five
2161 ˇ
2162 six"#
2163 .unindent(),
2164 );
2165
2166 cx.update_editor(|editor, window, cx| {
2167 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2168 });
2169 cx.assert_editor_state(
2170 &r#"one
2171 two
2172 ˇ
2173 three
2174 four
2175 five
2176
2177 six"#
2178 .unindent(),
2179 );
2180
2181 cx.update_editor(|editor, window, cx| {
2182 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2183 });
2184 cx.assert_editor_state(
2185 &r#"ˇone
2186 two
2187
2188 three
2189 four
2190 five
2191
2192 six"#
2193 .unindent(),
2194 );
2195}
2196
2197#[gpui::test]
2198async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2199 init_test(cx, |_| {});
2200 let mut cx = EditorTestContext::new(cx).await;
2201 let line_height = cx.editor(|editor, window, _| {
2202 editor
2203 .style()
2204 .unwrap()
2205 .text
2206 .line_height_in_pixels(window.rem_size())
2207 });
2208 let window = cx.window;
2209 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2210
2211 cx.set_state(
2212 r#"ˇone
2213 two
2214 three
2215 four
2216 five
2217 six
2218 seven
2219 eight
2220 nine
2221 ten
2222 "#,
2223 );
2224
2225 cx.update_editor(|editor, window, cx| {
2226 assert_eq!(
2227 editor.snapshot(window, cx).scroll_position(),
2228 gpui::Point::new(0., 0.)
2229 );
2230 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2231 assert_eq!(
2232 editor.snapshot(window, cx).scroll_position(),
2233 gpui::Point::new(0., 3.)
2234 );
2235 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2236 assert_eq!(
2237 editor.snapshot(window, cx).scroll_position(),
2238 gpui::Point::new(0., 6.)
2239 );
2240 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2241 assert_eq!(
2242 editor.snapshot(window, cx).scroll_position(),
2243 gpui::Point::new(0., 3.)
2244 );
2245
2246 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2247 assert_eq!(
2248 editor.snapshot(window, cx).scroll_position(),
2249 gpui::Point::new(0., 1.)
2250 );
2251 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2252 assert_eq!(
2253 editor.snapshot(window, cx).scroll_position(),
2254 gpui::Point::new(0., 3.)
2255 );
2256 });
2257}
2258
2259#[gpui::test]
2260async fn test_autoscroll(cx: &mut TestAppContext) {
2261 init_test(cx, |_| {});
2262 let mut cx = EditorTestContext::new(cx).await;
2263
2264 let line_height = cx.update_editor(|editor, window, cx| {
2265 editor.set_vertical_scroll_margin(2, cx);
2266 editor
2267 .style()
2268 .unwrap()
2269 .text
2270 .line_height_in_pixels(window.rem_size())
2271 });
2272 let window = cx.window;
2273 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2274
2275 cx.set_state(
2276 r#"ˇone
2277 two
2278 three
2279 four
2280 five
2281 six
2282 seven
2283 eight
2284 nine
2285 ten
2286 "#,
2287 );
2288 cx.update_editor(|editor, window, cx| {
2289 assert_eq!(
2290 editor.snapshot(window, cx).scroll_position(),
2291 gpui::Point::new(0., 0.0)
2292 );
2293 });
2294
2295 // Add a cursor below the visible area. Since both cursors cannot fit
2296 // on screen, the editor autoscrolls to reveal the newest cursor, and
2297 // allows the vertical scroll margin below that cursor.
2298 cx.update_editor(|editor, window, cx| {
2299 editor.change_selections(Default::default(), window, cx, |selections| {
2300 selections.select_ranges([
2301 Point::new(0, 0)..Point::new(0, 0),
2302 Point::new(6, 0)..Point::new(6, 0),
2303 ]);
2304 })
2305 });
2306 cx.update_editor(|editor, window, cx| {
2307 assert_eq!(
2308 editor.snapshot(window, cx).scroll_position(),
2309 gpui::Point::new(0., 3.0)
2310 );
2311 });
2312
2313 // Move down. The editor cursor scrolls down to track the newest cursor.
2314 cx.update_editor(|editor, window, cx| {
2315 editor.move_down(&Default::default(), window, cx);
2316 });
2317 cx.update_editor(|editor, window, cx| {
2318 assert_eq!(
2319 editor.snapshot(window, cx).scroll_position(),
2320 gpui::Point::new(0., 4.0)
2321 );
2322 });
2323
2324 // Add a cursor above the visible area. Since both cursors fit on screen,
2325 // the editor scrolls to show both.
2326 cx.update_editor(|editor, window, cx| {
2327 editor.change_selections(Default::default(), window, cx, |selections| {
2328 selections.select_ranges([
2329 Point::new(1, 0)..Point::new(1, 0),
2330 Point::new(6, 0)..Point::new(6, 0),
2331 ]);
2332 })
2333 });
2334 cx.update_editor(|editor, window, cx| {
2335 assert_eq!(
2336 editor.snapshot(window, cx).scroll_position(),
2337 gpui::Point::new(0., 1.0)
2338 );
2339 });
2340}
2341
2342#[gpui::test]
2343async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2344 init_test(cx, |_| {});
2345 let mut cx = EditorTestContext::new(cx).await;
2346
2347 let line_height = cx.editor(|editor, window, _cx| {
2348 editor
2349 .style()
2350 .unwrap()
2351 .text
2352 .line_height_in_pixels(window.rem_size())
2353 });
2354 let window = cx.window;
2355 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2356 cx.set_state(
2357 &r#"
2358 ˇone
2359 two
2360 threeˇ
2361 four
2362 five
2363 six
2364 seven
2365 eight
2366 nine
2367 ten
2368 "#
2369 .unindent(),
2370 );
2371
2372 cx.update_editor(|editor, window, cx| {
2373 editor.move_page_down(&MovePageDown::default(), window, cx)
2374 });
2375 cx.assert_editor_state(
2376 &r#"
2377 one
2378 two
2379 three
2380 ˇfour
2381 five
2382 sixˇ
2383 seven
2384 eight
2385 nine
2386 ten
2387 "#
2388 .unindent(),
2389 );
2390
2391 cx.update_editor(|editor, window, cx| {
2392 editor.move_page_down(&MovePageDown::default(), window, cx)
2393 });
2394 cx.assert_editor_state(
2395 &r#"
2396 one
2397 two
2398 three
2399 four
2400 five
2401 six
2402 ˇseven
2403 eight
2404 nineˇ
2405 ten
2406 "#
2407 .unindent(),
2408 );
2409
2410 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2411 cx.assert_editor_state(
2412 &r#"
2413 one
2414 two
2415 three
2416 ˇfour
2417 five
2418 sixˇ
2419 seven
2420 eight
2421 nine
2422 ten
2423 "#
2424 .unindent(),
2425 );
2426
2427 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2428 cx.assert_editor_state(
2429 &r#"
2430 ˇone
2431 two
2432 threeˇ
2433 four
2434 five
2435 six
2436 seven
2437 eight
2438 nine
2439 ten
2440 "#
2441 .unindent(),
2442 );
2443
2444 // Test select collapsing
2445 cx.update_editor(|editor, window, cx| {
2446 editor.move_page_down(&MovePageDown::default(), window, cx);
2447 editor.move_page_down(&MovePageDown::default(), window, cx);
2448 editor.move_page_down(&MovePageDown::default(), window, cx);
2449 });
2450 cx.assert_editor_state(
2451 &r#"
2452 one
2453 two
2454 three
2455 four
2456 five
2457 six
2458 seven
2459 eight
2460 nine
2461 ˇten
2462 ˇ"#
2463 .unindent(),
2464 );
2465}
2466
2467#[gpui::test]
2468async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2469 init_test(cx, |_| {});
2470 let mut cx = EditorTestContext::new(cx).await;
2471 cx.set_state("one «two threeˇ» four");
2472 cx.update_editor(|editor, window, cx| {
2473 editor.delete_to_beginning_of_line(
2474 &DeleteToBeginningOfLine {
2475 stop_at_indent: false,
2476 },
2477 window,
2478 cx,
2479 );
2480 assert_eq!(editor.text(cx), " four");
2481 });
2482}
2483
2484#[gpui::test]
2485async fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2486 init_test(cx, |_| {});
2487
2488 let mut cx = EditorTestContext::new(cx).await;
2489
2490 // For an empty selection, the preceding word fragment is deleted.
2491 // For non-empty selections, only selected characters are deleted.
2492 cx.set_state("onˇe two t«hreˇ»e four");
2493 cx.update_editor(|editor, window, cx| {
2494 editor.delete_to_previous_word_start(
2495 &DeleteToPreviousWordStart {
2496 ignore_newlines: false,
2497 ignore_brackets: false,
2498 },
2499 window,
2500 cx,
2501 );
2502 });
2503 cx.assert_editor_state("ˇe two tˇe four");
2504
2505 cx.set_state("e tˇwo te «fˇ»our");
2506 cx.update_editor(|editor, window, cx| {
2507 editor.delete_to_next_word_end(
2508 &DeleteToNextWordEnd {
2509 ignore_newlines: false,
2510 ignore_brackets: false,
2511 },
2512 window,
2513 cx,
2514 );
2515 });
2516 cx.assert_editor_state("e tˇ te ˇour");
2517}
2518
2519#[gpui::test]
2520async fn test_delete_whitespaces(cx: &mut TestAppContext) {
2521 init_test(cx, |_| {});
2522
2523 let mut cx = EditorTestContext::new(cx).await;
2524
2525 cx.set_state("here is some text ˇwith a space");
2526 cx.update_editor(|editor, window, cx| {
2527 editor.delete_to_previous_word_start(
2528 &DeleteToPreviousWordStart {
2529 ignore_newlines: false,
2530 ignore_brackets: true,
2531 },
2532 window,
2533 cx,
2534 );
2535 });
2536 // Continuous whitespace sequences are removed entirely, words behind them are not affected by the deletion action.
2537 cx.assert_editor_state("here is some textˇwith a space");
2538
2539 cx.set_state("here is some text ˇwith a space");
2540 cx.update_editor(|editor, window, cx| {
2541 editor.delete_to_previous_word_start(
2542 &DeleteToPreviousWordStart {
2543 ignore_newlines: false,
2544 ignore_brackets: false,
2545 },
2546 window,
2547 cx,
2548 );
2549 });
2550 cx.assert_editor_state("here is some textˇwith a space");
2551
2552 cx.set_state("here is some textˇ with a space");
2553 cx.update_editor(|editor, window, cx| {
2554 editor.delete_to_next_word_end(
2555 &DeleteToNextWordEnd {
2556 ignore_newlines: false,
2557 ignore_brackets: true,
2558 },
2559 window,
2560 cx,
2561 );
2562 });
2563 // Same happens in the other direction.
2564 cx.assert_editor_state("here is some textˇwith a space");
2565
2566 cx.set_state("here is some textˇ with a space");
2567 cx.update_editor(|editor, window, cx| {
2568 editor.delete_to_next_word_end(
2569 &DeleteToNextWordEnd {
2570 ignore_newlines: false,
2571 ignore_brackets: false,
2572 },
2573 window,
2574 cx,
2575 );
2576 });
2577 cx.assert_editor_state("here is some textˇwith a space");
2578
2579 cx.set_state("here is some textˇ with a space");
2580 cx.update_editor(|editor, window, cx| {
2581 editor.delete_to_next_word_end(
2582 &DeleteToNextWordEnd {
2583 ignore_newlines: true,
2584 ignore_brackets: false,
2585 },
2586 window,
2587 cx,
2588 );
2589 });
2590 cx.assert_editor_state("here is some textˇwith a space");
2591 cx.update_editor(|editor, window, cx| {
2592 editor.delete_to_previous_word_start(
2593 &DeleteToPreviousWordStart {
2594 ignore_newlines: true,
2595 ignore_brackets: false,
2596 },
2597 window,
2598 cx,
2599 );
2600 });
2601 cx.assert_editor_state("here is some ˇwith a space");
2602 cx.update_editor(|editor, window, cx| {
2603 editor.delete_to_previous_word_start(
2604 &DeleteToPreviousWordStart {
2605 ignore_newlines: true,
2606 ignore_brackets: false,
2607 },
2608 window,
2609 cx,
2610 );
2611 });
2612 // Single whitespaces are removed with the word behind them.
2613 cx.assert_editor_state("here is ˇwith a space");
2614 cx.update_editor(|editor, window, cx| {
2615 editor.delete_to_previous_word_start(
2616 &DeleteToPreviousWordStart {
2617 ignore_newlines: true,
2618 ignore_brackets: false,
2619 },
2620 window,
2621 cx,
2622 );
2623 });
2624 cx.assert_editor_state("here ˇwith a space");
2625 cx.update_editor(|editor, window, cx| {
2626 editor.delete_to_previous_word_start(
2627 &DeleteToPreviousWordStart {
2628 ignore_newlines: true,
2629 ignore_brackets: false,
2630 },
2631 window,
2632 cx,
2633 );
2634 });
2635 cx.assert_editor_state("ˇwith a space");
2636 cx.update_editor(|editor, window, cx| {
2637 editor.delete_to_previous_word_start(
2638 &DeleteToPreviousWordStart {
2639 ignore_newlines: true,
2640 ignore_brackets: false,
2641 },
2642 window,
2643 cx,
2644 );
2645 });
2646 cx.assert_editor_state("ˇwith a space");
2647 cx.update_editor(|editor, window, cx| {
2648 editor.delete_to_next_word_end(
2649 &DeleteToNextWordEnd {
2650 ignore_newlines: true,
2651 ignore_brackets: false,
2652 },
2653 window,
2654 cx,
2655 );
2656 });
2657 // Same happens in the other direction.
2658 cx.assert_editor_state("ˇ a space");
2659 cx.update_editor(|editor, window, cx| {
2660 editor.delete_to_next_word_end(
2661 &DeleteToNextWordEnd {
2662 ignore_newlines: true,
2663 ignore_brackets: false,
2664 },
2665 window,
2666 cx,
2667 );
2668 });
2669 cx.assert_editor_state("ˇ space");
2670 cx.update_editor(|editor, window, cx| {
2671 editor.delete_to_next_word_end(
2672 &DeleteToNextWordEnd {
2673 ignore_newlines: true,
2674 ignore_brackets: false,
2675 },
2676 window,
2677 cx,
2678 );
2679 });
2680 cx.assert_editor_state("ˇ");
2681 cx.update_editor(|editor, window, cx| {
2682 editor.delete_to_next_word_end(
2683 &DeleteToNextWordEnd {
2684 ignore_newlines: true,
2685 ignore_brackets: false,
2686 },
2687 window,
2688 cx,
2689 );
2690 });
2691 cx.assert_editor_state("ˇ");
2692 cx.update_editor(|editor, window, cx| {
2693 editor.delete_to_previous_word_start(
2694 &DeleteToPreviousWordStart {
2695 ignore_newlines: true,
2696 ignore_brackets: false,
2697 },
2698 window,
2699 cx,
2700 );
2701 });
2702 cx.assert_editor_state("ˇ");
2703}
2704
2705#[gpui::test]
2706async fn test_delete_to_bracket(cx: &mut TestAppContext) {
2707 init_test(cx, |_| {});
2708
2709 let language = Arc::new(
2710 Language::new(
2711 LanguageConfig {
2712 brackets: BracketPairConfig {
2713 pairs: vec![
2714 BracketPair {
2715 start: "\"".to_string(),
2716 end: "\"".to_string(),
2717 close: true,
2718 surround: true,
2719 newline: false,
2720 },
2721 BracketPair {
2722 start: "(".to_string(),
2723 end: ")".to_string(),
2724 close: true,
2725 surround: true,
2726 newline: true,
2727 },
2728 ],
2729 ..BracketPairConfig::default()
2730 },
2731 ..LanguageConfig::default()
2732 },
2733 Some(tree_sitter_rust::LANGUAGE.into()),
2734 )
2735 .with_brackets_query(
2736 r#"
2737 ("(" @open ")" @close)
2738 ("\"" @open "\"" @close)
2739 "#,
2740 )
2741 .unwrap(),
2742 );
2743
2744 let mut cx = EditorTestContext::new(cx).await;
2745 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2746
2747 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2748 cx.update_editor(|editor, window, cx| {
2749 editor.delete_to_previous_word_start(
2750 &DeleteToPreviousWordStart {
2751 ignore_newlines: true,
2752 ignore_brackets: false,
2753 },
2754 window,
2755 cx,
2756 );
2757 });
2758 // Deletion stops before brackets if asked to not ignore them.
2759 cx.assert_editor_state(r#"macro!("ˇCOMMENT");"#);
2760 cx.update_editor(|editor, window, cx| {
2761 editor.delete_to_previous_word_start(
2762 &DeleteToPreviousWordStart {
2763 ignore_newlines: true,
2764 ignore_brackets: false,
2765 },
2766 window,
2767 cx,
2768 );
2769 });
2770 // Deletion has to remove a single bracket and then stop again.
2771 cx.assert_editor_state(r#"macro!(ˇCOMMENT");"#);
2772
2773 cx.update_editor(|editor, window, cx| {
2774 editor.delete_to_previous_word_start(
2775 &DeleteToPreviousWordStart {
2776 ignore_newlines: true,
2777 ignore_brackets: false,
2778 },
2779 window,
2780 cx,
2781 );
2782 });
2783 cx.assert_editor_state(r#"macro!ˇCOMMENT");"#);
2784
2785 cx.update_editor(|editor, window, cx| {
2786 editor.delete_to_previous_word_start(
2787 &DeleteToPreviousWordStart {
2788 ignore_newlines: true,
2789 ignore_brackets: false,
2790 },
2791 window,
2792 cx,
2793 );
2794 });
2795 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2796
2797 cx.update_editor(|editor, window, cx| {
2798 editor.delete_to_previous_word_start(
2799 &DeleteToPreviousWordStart {
2800 ignore_newlines: true,
2801 ignore_brackets: false,
2802 },
2803 window,
2804 cx,
2805 );
2806 });
2807 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2808
2809 cx.update_editor(|editor, window, cx| {
2810 editor.delete_to_next_word_end(
2811 &DeleteToNextWordEnd {
2812 ignore_newlines: true,
2813 ignore_brackets: false,
2814 },
2815 window,
2816 cx,
2817 );
2818 });
2819 // Brackets on the right are not paired anymore, hence deletion does not stop at them
2820 cx.assert_editor_state(r#"ˇ");"#);
2821
2822 cx.update_editor(|editor, window, cx| {
2823 editor.delete_to_next_word_end(
2824 &DeleteToNextWordEnd {
2825 ignore_newlines: true,
2826 ignore_brackets: false,
2827 },
2828 window,
2829 cx,
2830 );
2831 });
2832 cx.assert_editor_state(r#"ˇ"#);
2833
2834 cx.update_editor(|editor, window, cx| {
2835 editor.delete_to_next_word_end(
2836 &DeleteToNextWordEnd {
2837 ignore_newlines: true,
2838 ignore_brackets: false,
2839 },
2840 window,
2841 cx,
2842 );
2843 });
2844 cx.assert_editor_state(r#"ˇ"#);
2845
2846 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2847 cx.update_editor(|editor, window, cx| {
2848 editor.delete_to_previous_word_start(
2849 &DeleteToPreviousWordStart {
2850 ignore_newlines: true,
2851 ignore_brackets: true,
2852 },
2853 window,
2854 cx,
2855 );
2856 });
2857 cx.assert_editor_state(r#"macroˇCOMMENT");"#);
2858}
2859
2860#[gpui::test]
2861fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2862 init_test(cx, |_| {});
2863
2864 let editor = cx.add_window(|window, cx| {
2865 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2866 build_editor(buffer, window, cx)
2867 });
2868 let del_to_prev_word_start = DeleteToPreviousWordStart {
2869 ignore_newlines: false,
2870 ignore_brackets: false,
2871 };
2872 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2873 ignore_newlines: true,
2874 ignore_brackets: false,
2875 };
2876
2877 _ = editor.update(cx, |editor, window, cx| {
2878 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2879 s.select_display_ranges([
2880 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2881 ])
2882 });
2883 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2884 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
2885 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2886 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
2887 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2888 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
2889 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2890 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
2891 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2892 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
2893 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2894 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2895 });
2896}
2897
2898#[gpui::test]
2899fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
2900 init_test(cx, |_| {});
2901
2902 let editor = cx.add_window(|window, cx| {
2903 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
2904 build_editor(buffer, window, cx)
2905 });
2906 let del_to_next_word_end = DeleteToNextWordEnd {
2907 ignore_newlines: false,
2908 ignore_brackets: false,
2909 };
2910 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
2911 ignore_newlines: true,
2912 ignore_brackets: false,
2913 };
2914
2915 _ = editor.update(cx, |editor, window, cx| {
2916 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2917 s.select_display_ranges([
2918 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2919 ])
2920 });
2921 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2922 assert_eq!(
2923 editor.buffer.read(cx).read(cx).text(),
2924 "one\n two\nthree\n four"
2925 );
2926 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2927 assert_eq!(
2928 editor.buffer.read(cx).read(cx).text(),
2929 "\n two\nthree\n four"
2930 );
2931 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2932 assert_eq!(
2933 editor.buffer.read(cx).read(cx).text(),
2934 "two\nthree\n four"
2935 );
2936 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2937 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
2938 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2939 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
2940 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2941 assert_eq!(editor.buffer.read(cx).read(cx).text(), "four");
2942 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2943 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2944 });
2945}
2946
2947#[gpui::test]
2948fn test_newline(cx: &mut TestAppContext) {
2949 init_test(cx, |_| {});
2950
2951 let editor = cx.add_window(|window, cx| {
2952 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
2953 build_editor(buffer, window, cx)
2954 });
2955
2956 _ = editor.update(cx, |editor, window, cx| {
2957 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2958 s.select_display_ranges([
2959 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2960 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2961 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
2962 ])
2963 });
2964
2965 editor.newline(&Newline, window, cx);
2966 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
2967 });
2968}
2969
2970#[gpui::test]
2971fn test_newline_with_old_selections(cx: &mut TestAppContext) {
2972 init_test(cx, |_| {});
2973
2974 let editor = cx.add_window(|window, cx| {
2975 let buffer = MultiBuffer::build_simple(
2976 "
2977 a
2978 b(
2979 X
2980 )
2981 c(
2982 X
2983 )
2984 "
2985 .unindent()
2986 .as_str(),
2987 cx,
2988 );
2989 let mut editor = build_editor(buffer, window, cx);
2990 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2991 s.select_ranges([
2992 Point::new(2, 4)..Point::new(2, 5),
2993 Point::new(5, 4)..Point::new(5, 5),
2994 ])
2995 });
2996 editor
2997 });
2998
2999 _ = editor.update(cx, |editor, window, cx| {
3000 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3001 editor.buffer.update(cx, |buffer, cx| {
3002 buffer.edit(
3003 [
3004 (Point::new(1, 2)..Point::new(3, 0), ""),
3005 (Point::new(4, 2)..Point::new(6, 0), ""),
3006 ],
3007 None,
3008 cx,
3009 );
3010 assert_eq!(
3011 buffer.read(cx).text(),
3012 "
3013 a
3014 b()
3015 c()
3016 "
3017 .unindent()
3018 );
3019 });
3020 assert_eq!(
3021 editor.selections.ranges(cx),
3022 &[
3023 Point::new(1, 2)..Point::new(1, 2),
3024 Point::new(2, 2)..Point::new(2, 2),
3025 ],
3026 );
3027
3028 editor.newline(&Newline, window, cx);
3029 assert_eq!(
3030 editor.text(cx),
3031 "
3032 a
3033 b(
3034 )
3035 c(
3036 )
3037 "
3038 .unindent()
3039 );
3040
3041 // The selections are moved after the inserted newlines
3042 assert_eq!(
3043 editor.selections.ranges(cx),
3044 &[
3045 Point::new(2, 0)..Point::new(2, 0),
3046 Point::new(4, 0)..Point::new(4, 0),
3047 ],
3048 );
3049 });
3050}
3051
3052#[gpui::test]
3053async fn test_newline_above(cx: &mut TestAppContext) {
3054 init_test(cx, |settings| {
3055 settings.defaults.tab_size = NonZeroU32::new(4)
3056 });
3057
3058 let language = Arc::new(
3059 Language::new(
3060 LanguageConfig::default(),
3061 Some(tree_sitter_rust::LANGUAGE.into()),
3062 )
3063 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3064 .unwrap(),
3065 );
3066
3067 let mut cx = EditorTestContext::new(cx).await;
3068 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3069 cx.set_state(indoc! {"
3070 const a: ˇA = (
3071 (ˇ
3072 «const_functionˇ»(ˇ),
3073 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3074 )ˇ
3075 ˇ);ˇ
3076 "});
3077
3078 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
3079 cx.assert_editor_state(indoc! {"
3080 ˇ
3081 const a: A = (
3082 ˇ
3083 (
3084 ˇ
3085 ˇ
3086 const_function(),
3087 ˇ
3088 ˇ
3089 ˇ
3090 ˇ
3091 something_else,
3092 ˇ
3093 )
3094 ˇ
3095 ˇ
3096 );
3097 "});
3098}
3099
3100#[gpui::test]
3101async fn test_newline_below(cx: &mut TestAppContext) {
3102 init_test(cx, |settings| {
3103 settings.defaults.tab_size = NonZeroU32::new(4)
3104 });
3105
3106 let language = Arc::new(
3107 Language::new(
3108 LanguageConfig::default(),
3109 Some(tree_sitter_rust::LANGUAGE.into()),
3110 )
3111 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3112 .unwrap(),
3113 );
3114
3115 let mut cx = EditorTestContext::new(cx).await;
3116 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3117 cx.set_state(indoc! {"
3118 const a: ˇA = (
3119 (ˇ
3120 «const_functionˇ»(ˇ),
3121 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3122 )ˇ
3123 ˇ);ˇ
3124 "});
3125
3126 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
3127 cx.assert_editor_state(indoc! {"
3128 const a: A = (
3129 ˇ
3130 (
3131 ˇ
3132 const_function(),
3133 ˇ
3134 ˇ
3135 something_else,
3136 ˇ
3137 ˇ
3138 ˇ
3139 ˇ
3140 )
3141 ˇ
3142 );
3143 ˇ
3144 ˇ
3145 "});
3146}
3147
3148#[gpui::test]
3149async fn test_newline_comments(cx: &mut TestAppContext) {
3150 init_test(cx, |settings| {
3151 settings.defaults.tab_size = NonZeroU32::new(4)
3152 });
3153
3154 let language = Arc::new(Language::new(
3155 LanguageConfig {
3156 line_comments: vec!["// ".into()],
3157 ..LanguageConfig::default()
3158 },
3159 None,
3160 ));
3161 {
3162 let mut cx = EditorTestContext::new(cx).await;
3163 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3164 cx.set_state(indoc! {"
3165 // Fooˇ
3166 "});
3167
3168 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3169 cx.assert_editor_state(indoc! {"
3170 // Foo
3171 // ˇ
3172 "});
3173 // Ensure that we add comment prefix when existing line contains space
3174 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3175 cx.assert_editor_state(
3176 indoc! {"
3177 // Foo
3178 //s
3179 // ˇ
3180 "}
3181 .replace("s", " ") // s is used as space placeholder to prevent format on save
3182 .as_str(),
3183 );
3184 // Ensure that we add comment prefix when existing line does not contain space
3185 cx.set_state(indoc! {"
3186 // Foo
3187 //ˇ
3188 "});
3189 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3190 cx.assert_editor_state(indoc! {"
3191 // Foo
3192 //
3193 // ˇ
3194 "});
3195 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
3196 cx.set_state(indoc! {"
3197 ˇ// Foo
3198 "});
3199 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3200 cx.assert_editor_state(indoc! {"
3201
3202 ˇ// Foo
3203 "});
3204 }
3205 // Ensure that comment continuations can be disabled.
3206 update_test_language_settings(cx, |settings| {
3207 settings.defaults.extend_comment_on_newline = Some(false);
3208 });
3209 let mut cx = EditorTestContext::new(cx).await;
3210 cx.set_state(indoc! {"
3211 // Fooˇ
3212 "});
3213 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3214 cx.assert_editor_state(indoc! {"
3215 // Foo
3216 ˇ
3217 "});
3218}
3219
3220#[gpui::test]
3221async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
3222 init_test(cx, |settings| {
3223 settings.defaults.tab_size = NonZeroU32::new(4)
3224 });
3225
3226 let language = Arc::new(Language::new(
3227 LanguageConfig {
3228 line_comments: vec!["// ".into(), "/// ".into()],
3229 ..LanguageConfig::default()
3230 },
3231 None,
3232 ));
3233 {
3234 let mut cx = EditorTestContext::new(cx).await;
3235 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3236 cx.set_state(indoc! {"
3237 //ˇ
3238 "});
3239 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3240 cx.assert_editor_state(indoc! {"
3241 //
3242 // ˇ
3243 "});
3244
3245 cx.set_state(indoc! {"
3246 ///ˇ
3247 "});
3248 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3249 cx.assert_editor_state(indoc! {"
3250 ///
3251 /// ˇ
3252 "});
3253 }
3254}
3255
3256#[gpui::test]
3257async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
3258 init_test(cx, |settings| {
3259 settings.defaults.tab_size = NonZeroU32::new(4)
3260 });
3261
3262 let language = Arc::new(
3263 Language::new(
3264 LanguageConfig {
3265 documentation_comment: Some(language::BlockCommentConfig {
3266 start: "/**".into(),
3267 end: "*/".into(),
3268 prefix: "* ".into(),
3269 tab_size: 1,
3270 }),
3271
3272 ..LanguageConfig::default()
3273 },
3274 Some(tree_sitter_rust::LANGUAGE.into()),
3275 )
3276 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
3277 .unwrap(),
3278 );
3279
3280 {
3281 let mut cx = EditorTestContext::new(cx).await;
3282 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3283 cx.set_state(indoc! {"
3284 /**ˇ
3285 "});
3286
3287 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3288 cx.assert_editor_state(indoc! {"
3289 /**
3290 * ˇ
3291 "});
3292 // Ensure that if cursor is before the comment start,
3293 // we do not actually insert a comment prefix.
3294 cx.set_state(indoc! {"
3295 ˇ/**
3296 "});
3297 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3298 cx.assert_editor_state(indoc! {"
3299
3300 ˇ/**
3301 "});
3302 // Ensure that if cursor is between it doesn't add comment prefix.
3303 cx.set_state(indoc! {"
3304 /*ˇ*
3305 "});
3306 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3307 cx.assert_editor_state(indoc! {"
3308 /*
3309 ˇ*
3310 "});
3311 // Ensure that if suffix exists on same line after cursor it adds new line.
3312 cx.set_state(indoc! {"
3313 /**ˇ*/
3314 "});
3315 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3316 cx.assert_editor_state(indoc! {"
3317 /**
3318 * ˇ
3319 */
3320 "});
3321 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3322 cx.set_state(indoc! {"
3323 /**ˇ */
3324 "});
3325 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3326 cx.assert_editor_state(indoc! {"
3327 /**
3328 * ˇ
3329 */
3330 "});
3331 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3332 cx.set_state(indoc! {"
3333 /** ˇ*/
3334 "});
3335 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3336 cx.assert_editor_state(
3337 indoc! {"
3338 /**s
3339 * ˇ
3340 */
3341 "}
3342 .replace("s", " ") // s is used as space placeholder to prevent format on save
3343 .as_str(),
3344 );
3345 // Ensure that delimiter space is preserved when newline on already
3346 // spaced delimiter.
3347 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3348 cx.assert_editor_state(
3349 indoc! {"
3350 /**s
3351 *s
3352 * ˇ
3353 */
3354 "}
3355 .replace("s", " ") // s is used as space placeholder to prevent format on save
3356 .as_str(),
3357 );
3358 // Ensure that delimiter space is preserved when space is not
3359 // on existing delimiter.
3360 cx.set_state(indoc! {"
3361 /**
3362 *ˇ
3363 */
3364 "});
3365 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3366 cx.assert_editor_state(indoc! {"
3367 /**
3368 *
3369 * ˇ
3370 */
3371 "});
3372 // Ensure that if suffix exists on same line after cursor it
3373 // doesn't add extra new line if prefix is not on same line.
3374 cx.set_state(indoc! {"
3375 /**
3376 ˇ*/
3377 "});
3378 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3379 cx.assert_editor_state(indoc! {"
3380 /**
3381
3382 ˇ*/
3383 "});
3384 // Ensure that it detects suffix after existing prefix.
3385 cx.set_state(indoc! {"
3386 /**ˇ/
3387 "});
3388 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3389 cx.assert_editor_state(indoc! {"
3390 /**
3391 ˇ/
3392 "});
3393 // Ensure that if suffix exists on same line before
3394 // cursor it does not add comment prefix.
3395 cx.set_state(indoc! {"
3396 /** */ˇ
3397 "});
3398 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3399 cx.assert_editor_state(indoc! {"
3400 /** */
3401 ˇ
3402 "});
3403 // Ensure that if suffix exists on same line before
3404 // cursor it does not add comment prefix.
3405 cx.set_state(indoc! {"
3406 /**
3407 *
3408 */ˇ
3409 "});
3410 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3411 cx.assert_editor_state(indoc! {"
3412 /**
3413 *
3414 */
3415 ˇ
3416 "});
3417
3418 // Ensure that inline comment followed by code
3419 // doesn't add comment prefix on newline
3420 cx.set_state(indoc! {"
3421 /** */ textˇ
3422 "});
3423 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3424 cx.assert_editor_state(indoc! {"
3425 /** */ text
3426 ˇ
3427 "});
3428
3429 // Ensure that text after comment end tag
3430 // doesn't add comment prefix on newline
3431 cx.set_state(indoc! {"
3432 /**
3433 *
3434 */ˇtext
3435 "});
3436 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3437 cx.assert_editor_state(indoc! {"
3438 /**
3439 *
3440 */
3441 ˇtext
3442 "});
3443
3444 // Ensure if not comment block it doesn't
3445 // add comment prefix on newline
3446 cx.set_state(indoc! {"
3447 * textˇ
3448 "});
3449 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3450 cx.assert_editor_state(indoc! {"
3451 * text
3452 ˇ
3453 "});
3454 }
3455 // Ensure that comment continuations can be disabled.
3456 update_test_language_settings(cx, |settings| {
3457 settings.defaults.extend_comment_on_newline = Some(false);
3458 });
3459 let mut cx = EditorTestContext::new(cx).await;
3460 cx.set_state(indoc! {"
3461 /**ˇ
3462 "});
3463 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3464 cx.assert_editor_state(indoc! {"
3465 /**
3466 ˇ
3467 "});
3468}
3469
3470#[gpui::test]
3471async fn test_newline_comments_with_block_comment(cx: &mut TestAppContext) {
3472 init_test(cx, |settings| {
3473 settings.defaults.tab_size = NonZeroU32::new(4)
3474 });
3475
3476 let lua_language = Arc::new(Language::new(
3477 LanguageConfig {
3478 line_comments: vec!["--".into()],
3479 block_comment: Some(language::BlockCommentConfig {
3480 start: "--[[".into(),
3481 prefix: "".into(),
3482 end: "]]".into(),
3483 tab_size: 0,
3484 }),
3485 ..LanguageConfig::default()
3486 },
3487 None,
3488 ));
3489
3490 let mut cx = EditorTestContext::new(cx).await;
3491 cx.update_buffer(|buffer, cx| buffer.set_language(Some(lua_language), cx));
3492
3493 // Line with line comment should extend
3494 cx.set_state(indoc! {"
3495 --ˇ
3496 "});
3497 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3498 cx.assert_editor_state(indoc! {"
3499 --
3500 --ˇ
3501 "});
3502
3503 // Line with block comment that matches line comment should not extend
3504 cx.set_state(indoc! {"
3505 --[[ˇ
3506 "});
3507 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3508 cx.assert_editor_state(indoc! {"
3509 --[[
3510 ˇ
3511 "});
3512}
3513
3514#[gpui::test]
3515fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3516 init_test(cx, |_| {});
3517
3518 let editor = cx.add_window(|window, cx| {
3519 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3520 let mut editor = build_editor(buffer, window, cx);
3521 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3522 s.select_ranges([3..4, 11..12, 19..20])
3523 });
3524 editor
3525 });
3526
3527 _ = editor.update(cx, |editor, window, cx| {
3528 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3529 editor.buffer.update(cx, |buffer, cx| {
3530 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
3531 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3532 });
3533 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
3534
3535 editor.insert("Z", window, cx);
3536 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3537
3538 // The selections are moved after the inserted characters
3539 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
3540 });
3541}
3542
3543#[gpui::test]
3544async fn test_tab(cx: &mut TestAppContext) {
3545 init_test(cx, |settings| {
3546 settings.defaults.tab_size = NonZeroU32::new(3)
3547 });
3548
3549 let mut cx = EditorTestContext::new(cx).await;
3550 cx.set_state(indoc! {"
3551 ˇabˇc
3552 ˇ🏀ˇ🏀ˇefg
3553 dˇ
3554 "});
3555 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3556 cx.assert_editor_state(indoc! {"
3557 ˇab ˇc
3558 ˇ🏀 ˇ🏀 ˇefg
3559 d ˇ
3560 "});
3561
3562 cx.set_state(indoc! {"
3563 a
3564 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3565 "});
3566 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3567 cx.assert_editor_state(indoc! {"
3568 a
3569 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3570 "});
3571}
3572
3573#[gpui::test]
3574async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3575 init_test(cx, |_| {});
3576
3577 let mut cx = EditorTestContext::new(cx).await;
3578 let language = Arc::new(
3579 Language::new(
3580 LanguageConfig::default(),
3581 Some(tree_sitter_rust::LANGUAGE.into()),
3582 )
3583 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3584 .unwrap(),
3585 );
3586 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3587
3588 // test when all cursors are not at suggested indent
3589 // then simply move to their suggested indent location
3590 cx.set_state(indoc! {"
3591 const a: B = (
3592 c(
3593 ˇ
3594 ˇ )
3595 );
3596 "});
3597 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3598 cx.assert_editor_state(indoc! {"
3599 const a: B = (
3600 c(
3601 ˇ
3602 ˇ)
3603 );
3604 "});
3605
3606 // test cursor already at suggested indent not moving when
3607 // other cursors are yet to reach their suggested indents
3608 cx.set_state(indoc! {"
3609 ˇ
3610 const a: B = (
3611 c(
3612 d(
3613 ˇ
3614 )
3615 ˇ
3616 ˇ )
3617 );
3618 "});
3619 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3620 cx.assert_editor_state(indoc! {"
3621 ˇ
3622 const a: B = (
3623 c(
3624 d(
3625 ˇ
3626 )
3627 ˇ
3628 ˇ)
3629 );
3630 "});
3631 // test when all cursors are at suggested indent then tab is inserted
3632 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3633 cx.assert_editor_state(indoc! {"
3634 ˇ
3635 const a: B = (
3636 c(
3637 d(
3638 ˇ
3639 )
3640 ˇ
3641 ˇ)
3642 );
3643 "});
3644
3645 // test when current indent is less than suggested indent,
3646 // we adjust line to match suggested indent and move cursor to it
3647 //
3648 // when no other cursor is at word boundary, all of them should move
3649 cx.set_state(indoc! {"
3650 const a: B = (
3651 c(
3652 d(
3653 ˇ
3654 ˇ )
3655 ˇ )
3656 );
3657 "});
3658 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3659 cx.assert_editor_state(indoc! {"
3660 const a: B = (
3661 c(
3662 d(
3663 ˇ
3664 ˇ)
3665 ˇ)
3666 );
3667 "});
3668
3669 // test when current indent is less than suggested indent,
3670 // we adjust line to match suggested indent and move cursor to it
3671 //
3672 // when some other cursor is at word boundary, it should not move
3673 cx.set_state(indoc! {"
3674 const a: B = (
3675 c(
3676 d(
3677 ˇ
3678 ˇ )
3679 ˇ)
3680 );
3681 "});
3682 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3683 cx.assert_editor_state(indoc! {"
3684 const a: B = (
3685 c(
3686 d(
3687 ˇ
3688 ˇ)
3689 ˇ)
3690 );
3691 "});
3692
3693 // test when current indent is more than suggested indent,
3694 // we just move cursor to current indent instead of suggested indent
3695 //
3696 // when no other cursor is at word boundary, all of them should move
3697 cx.set_state(indoc! {"
3698 const a: B = (
3699 c(
3700 d(
3701 ˇ
3702 ˇ )
3703 ˇ )
3704 );
3705 "});
3706 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3707 cx.assert_editor_state(indoc! {"
3708 const a: B = (
3709 c(
3710 d(
3711 ˇ
3712 ˇ)
3713 ˇ)
3714 );
3715 "});
3716 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3717 cx.assert_editor_state(indoc! {"
3718 const a: B = (
3719 c(
3720 d(
3721 ˇ
3722 ˇ)
3723 ˇ)
3724 );
3725 "});
3726
3727 // test when current indent is more than suggested indent,
3728 // we just move cursor to current indent instead of suggested indent
3729 //
3730 // when some other cursor is at word boundary, it doesn't move
3731 cx.set_state(indoc! {"
3732 const a: B = (
3733 c(
3734 d(
3735 ˇ
3736 ˇ )
3737 ˇ)
3738 );
3739 "});
3740 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3741 cx.assert_editor_state(indoc! {"
3742 const a: B = (
3743 c(
3744 d(
3745 ˇ
3746 ˇ)
3747 ˇ)
3748 );
3749 "});
3750
3751 // handle auto-indent when there are multiple cursors on the same line
3752 cx.set_state(indoc! {"
3753 const a: B = (
3754 c(
3755 ˇ ˇ
3756 ˇ )
3757 );
3758 "});
3759 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3760 cx.assert_editor_state(indoc! {"
3761 const a: B = (
3762 c(
3763 ˇ
3764 ˇ)
3765 );
3766 "});
3767}
3768
3769#[gpui::test]
3770async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3771 init_test(cx, |settings| {
3772 settings.defaults.tab_size = NonZeroU32::new(3)
3773 });
3774
3775 let mut cx = EditorTestContext::new(cx).await;
3776 cx.set_state(indoc! {"
3777 ˇ
3778 \t ˇ
3779 \t ˇ
3780 \t ˇ
3781 \t \t\t \t \t\t \t\t \t \t ˇ
3782 "});
3783
3784 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3785 cx.assert_editor_state(indoc! {"
3786 ˇ
3787 \t ˇ
3788 \t ˇ
3789 \t ˇ
3790 \t \t\t \t \t\t \t\t \t \t ˇ
3791 "});
3792}
3793
3794#[gpui::test]
3795async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3796 init_test(cx, |settings| {
3797 settings.defaults.tab_size = NonZeroU32::new(4)
3798 });
3799
3800 let language = Arc::new(
3801 Language::new(
3802 LanguageConfig::default(),
3803 Some(tree_sitter_rust::LANGUAGE.into()),
3804 )
3805 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3806 .unwrap(),
3807 );
3808
3809 let mut cx = EditorTestContext::new(cx).await;
3810 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3811 cx.set_state(indoc! {"
3812 fn a() {
3813 if b {
3814 \t ˇc
3815 }
3816 }
3817 "});
3818
3819 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3820 cx.assert_editor_state(indoc! {"
3821 fn a() {
3822 if b {
3823 ˇc
3824 }
3825 }
3826 "});
3827}
3828
3829#[gpui::test]
3830async fn test_indent_outdent(cx: &mut TestAppContext) {
3831 init_test(cx, |settings| {
3832 settings.defaults.tab_size = NonZeroU32::new(4);
3833 });
3834
3835 let mut cx = EditorTestContext::new(cx).await;
3836
3837 cx.set_state(indoc! {"
3838 «oneˇ» «twoˇ»
3839 three
3840 four
3841 "});
3842 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3843 cx.assert_editor_state(indoc! {"
3844 «oneˇ» «twoˇ»
3845 three
3846 four
3847 "});
3848
3849 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3850 cx.assert_editor_state(indoc! {"
3851 «oneˇ» «twoˇ»
3852 three
3853 four
3854 "});
3855
3856 // select across line ending
3857 cx.set_state(indoc! {"
3858 one two
3859 t«hree
3860 ˇ» four
3861 "});
3862 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3863 cx.assert_editor_state(indoc! {"
3864 one two
3865 t«hree
3866 ˇ» four
3867 "});
3868
3869 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3870 cx.assert_editor_state(indoc! {"
3871 one two
3872 t«hree
3873 ˇ» four
3874 "});
3875
3876 // Ensure that indenting/outdenting works when the cursor is at column 0.
3877 cx.set_state(indoc! {"
3878 one two
3879 ˇthree
3880 four
3881 "});
3882 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3883 cx.assert_editor_state(indoc! {"
3884 one two
3885 ˇthree
3886 four
3887 "});
3888
3889 cx.set_state(indoc! {"
3890 one two
3891 ˇ three
3892 four
3893 "});
3894 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3895 cx.assert_editor_state(indoc! {"
3896 one two
3897 ˇthree
3898 four
3899 "});
3900}
3901
3902#[gpui::test]
3903async fn test_indent_yaml_comments_with_multiple_cursors(cx: &mut TestAppContext) {
3904 // This is a regression test for issue #33761
3905 init_test(cx, |_| {});
3906
3907 let mut cx = EditorTestContext::new(cx).await;
3908 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3909 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3910
3911 cx.set_state(
3912 r#"ˇ# ingress:
3913ˇ# api:
3914ˇ# enabled: false
3915ˇ# pathType: Prefix
3916ˇ# console:
3917ˇ# enabled: false
3918ˇ# pathType: Prefix
3919"#,
3920 );
3921
3922 // Press tab to indent all lines
3923 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3924
3925 cx.assert_editor_state(
3926 r#" ˇ# ingress:
3927 ˇ# api:
3928 ˇ# enabled: false
3929 ˇ# pathType: Prefix
3930 ˇ# console:
3931 ˇ# enabled: false
3932 ˇ# pathType: Prefix
3933"#,
3934 );
3935}
3936
3937#[gpui::test]
3938async fn test_indent_yaml_non_comments_with_multiple_cursors(cx: &mut TestAppContext) {
3939 // This is a test to make sure our fix for issue #33761 didn't break anything
3940 init_test(cx, |_| {});
3941
3942 let mut cx = EditorTestContext::new(cx).await;
3943 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3944 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3945
3946 cx.set_state(
3947 r#"ˇingress:
3948ˇ api:
3949ˇ enabled: false
3950ˇ pathType: Prefix
3951"#,
3952 );
3953
3954 // Press tab to indent all lines
3955 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3956
3957 cx.assert_editor_state(
3958 r#"ˇingress:
3959 ˇapi:
3960 ˇenabled: false
3961 ˇpathType: Prefix
3962"#,
3963 );
3964}
3965
3966#[gpui::test]
3967async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
3968 init_test(cx, |settings| {
3969 settings.defaults.hard_tabs = Some(true);
3970 });
3971
3972 let mut cx = EditorTestContext::new(cx).await;
3973
3974 // select two ranges on one line
3975 cx.set_state(indoc! {"
3976 «oneˇ» «twoˇ»
3977 three
3978 four
3979 "});
3980 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3981 cx.assert_editor_state(indoc! {"
3982 \t«oneˇ» «twoˇ»
3983 three
3984 four
3985 "});
3986 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3987 cx.assert_editor_state(indoc! {"
3988 \t\t«oneˇ» «twoˇ»
3989 three
3990 four
3991 "});
3992 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3993 cx.assert_editor_state(indoc! {"
3994 \t«oneˇ» «twoˇ»
3995 three
3996 four
3997 "});
3998 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3999 cx.assert_editor_state(indoc! {"
4000 «oneˇ» «twoˇ»
4001 three
4002 four
4003 "});
4004
4005 // select across a line ending
4006 cx.set_state(indoc! {"
4007 one two
4008 t«hree
4009 ˇ»four
4010 "});
4011 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4012 cx.assert_editor_state(indoc! {"
4013 one two
4014 \tt«hree
4015 ˇ»four
4016 "});
4017 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4018 cx.assert_editor_state(indoc! {"
4019 one two
4020 \t\tt«hree
4021 ˇ»four
4022 "});
4023 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4024 cx.assert_editor_state(indoc! {"
4025 one two
4026 \tt«hree
4027 ˇ»four
4028 "});
4029 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4030 cx.assert_editor_state(indoc! {"
4031 one two
4032 t«hree
4033 ˇ»four
4034 "});
4035
4036 // Ensure that indenting/outdenting works when the cursor is at column 0.
4037 cx.set_state(indoc! {"
4038 one two
4039 ˇthree
4040 four
4041 "});
4042 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4043 cx.assert_editor_state(indoc! {"
4044 one two
4045 ˇthree
4046 four
4047 "});
4048 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4049 cx.assert_editor_state(indoc! {"
4050 one two
4051 \tˇthree
4052 four
4053 "});
4054 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4055 cx.assert_editor_state(indoc! {"
4056 one two
4057 ˇthree
4058 four
4059 "});
4060}
4061
4062#[gpui::test]
4063fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
4064 init_test(cx, |settings| {
4065 settings.languages.0.extend([
4066 (
4067 "TOML".into(),
4068 LanguageSettingsContent {
4069 tab_size: NonZeroU32::new(2),
4070 ..Default::default()
4071 },
4072 ),
4073 (
4074 "Rust".into(),
4075 LanguageSettingsContent {
4076 tab_size: NonZeroU32::new(4),
4077 ..Default::default()
4078 },
4079 ),
4080 ]);
4081 });
4082
4083 let toml_language = Arc::new(Language::new(
4084 LanguageConfig {
4085 name: "TOML".into(),
4086 ..Default::default()
4087 },
4088 None,
4089 ));
4090 let rust_language = Arc::new(Language::new(
4091 LanguageConfig {
4092 name: "Rust".into(),
4093 ..Default::default()
4094 },
4095 None,
4096 ));
4097
4098 let toml_buffer =
4099 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
4100 let rust_buffer =
4101 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
4102 let multibuffer = cx.new(|cx| {
4103 let mut multibuffer = MultiBuffer::new(ReadWrite);
4104 multibuffer.push_excerpts(
4105 toml_buffer.clone(),
4106 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
4107 cx,
4108 );
4109 multibuffer.push_excerpts(
4110 rust_buffer.clone(),
4111 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
4112 cx,
4113 );
4114 multibuffer
4115 });
4116
4117 cx.add_window(|window, cx| {
4118 let mut editor = build_editor(multibuffer, window, cx);
4119
4120 assert_eq!(
4121 editor.text(cx),
4122 indoc! {"
4123 a = 1
4124 b = 2
4125
4126 const c: usize = 3;
4127 "}
4128 );
4129
4130 select_ranges(
4131 &mut editor,
4132 indoc! {"
4133 «aˇ» = 1
4134 b = 2
4135
4136 «const c:ˇ» usize = 3;
4137 "},
4138 window,
4139 cx,
4140 );
4141
4142 editor.tab(&Tab, window, cx);
4143 assert_text_with_selections(
4144 &mut editor,
4145 indoc! {"
4146 «aˇ» = 1
4147 b = 2
4148
4149 «const c:ˇ» usize = 3;
4150 "},
4151 cx,
4152 );
4153 editor.backtab(&Backtab, window, cx);
4154 assert_text_with_selections(
4155 &mut editor,
4156 indoc! {"
4157 «aˇ» = 1
4158 b = 2
4159
4160 «const c:ˇ» usize = 3;
4161 "},
4162 cx,
4163 );
4164
4165 editor
4166 });
4167}
4168
4169#[gpui::test]
4170async fn test_backspace(cx: &mut TestAppContext) {
4171 init_test(cx, |_| {});
4172
4173 let mut cx = EditorTestContext::new(cx).await;
4174
4175 // Basic backspace
4176 cx.set_state(indoc! {"
4177 onˇe two three
4178 fou«rˇ» five six
4179 seven «ˇeight nine
4180 »ten
4181 "});
4182 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4183 cx.assert_editor_state(indoc! {"
4184 oˇe two three
4185 fouˇ five six
4186 seven ˇten
4187 "});
4188
4189 // Test backspace inside and around indents
4190 cx.set_state(indoc! {"
4191 zero
4192 ˇone
4193 ˇtwo
4194 ˇ ˇ ˇ three
4195 ˇ ˇ four
4196 "});
4197 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4198 cx.assert_editor_state(indoc! {"
4199 zero
4200 ˇone
4201 ˇtwo
4202 ˇ threeˇ four
4203 "});
4204}
4205
4206#[gpui::test]
4207async fn test_delete(cx: &mut TestAppContext) {
4208 init_test(cx, |_| {});
4209
4210 let mut cx = EditorTestContext::new(cx).await;
4211 cx.set_state(indoc! {"
4212 onˇe two three
4213 fou«rˇ» five six
4214 seven «ˇeight nine
4215 »ten
4216 "});
4217 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
4218 cx.assert_editor_state(indoc! {"
4219 onˇ two three
4220 fouˇ five six
4221 seven ˇten
4222 "});
4223}
4224
4225#[gpui::test]
4226fn test_delete_line(cx: &mut TestAppContext) {
4227 init_test(cx, |_| {});
4228
4229 let editor = cx.add_window(|window, cx| {
4230 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4231 build_editor(buffer, window, cx)
4232 });
4233 _ = editor.update(cx, |editor, window, cx| {
4234 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4235 s.select_display_ranges([
4236 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4237 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4238 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4239 ])
4240 });
4241 editor.delete_line(&DeleteLine, window, cx);
4242 assert_eq!(editor.display_text(cx), "ghi");
4243 assert_eq!(
4244 editor.selections.display_ranges(cx),
4245 vec![
4246 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
4247 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
4248 ]
4249 );
4250 });
4251
4252 let editor = cx.add_window(|window, cx| {
4253 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4254 build_editor(buffer, window, cx)
4255 });
4256 _ = editor.update(cx, |editor, window, cx| {
4257 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4258 s.select_display_ranges([
4259 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
4260 ])
4261 });
4262 editor.delete_line(&DeleteLine, window, cx);
4263 assert_eq!(editor.display_text(cx), "ghi\n");
4264 assert_eq!(
4265 editor.selections.display_ranges(cx),
4266 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
4267 );
4268 });
4269}
4270
4271#[gpui::test]
4272fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
4273 init_test(cx, |_| {});
4274
4275 cx.add_window(|window, cx| {
4276 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4277 let mut editor = build_editor(buffer.clone(), window, cx);
4278 let buffer = buffer.read(cx).as_singleton().unwrap();
4279
4280 assert_eq!(
4281 editor.selections.ranges::<Point>(cx),
4282 &[Point::new(0, 0)..Point::new(0, 0)]
4283 );
4284
4285 // When on single line, replace newline at end by space
4286 editor.join_lines(&JoinLines, window, cx);
4287 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4288 assert_eq!(
4289 editor.selections.ranges::<Point>(cx),
4290 &[Point::new(0, 3)..Point::new(0, 3)]
4291 );
4292
4293 // When multiple lines are selected, remove newlines that are spanned by the selection
4294 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4295 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
4296 });
4297 editor.join_lines(&JoinLines, window, cx);
4298 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
4299 assert_eq!(
4300 editor.selections.ranges::<Point>(cx),
4301 &[Point::new(0, 11)..Point::new(0, 11)]
4302 );
4303
4304 // Undo should be transactional
4305 editor.undo(&Undo, window, cx);
4306 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4307 assert_eq!(
4308 editor.selections.ranges::<Point>(cx),
4309 &[Point::new(0, 5)..Point::new(2, 2)]
4310 );
4311
4312 // When joining an empty line don't insert a space
4313 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4314 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
4315 });
4316 editor.join_lines(&JoinLines, window, cx);
4317 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
4318 assert_eq!(
4319 editor.selections.ranges::<Point>(cx),
4320 [Point::new(2, 3)..Point::new(2, 3)]
4321 );
4322
4323 // We can remove trailing newlines
4324 editor.join_lines(&JoinLines, window, cx);
4325 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4326 assert_eq!(
4327 editor.selections.ranges::<Point>(cx),
4328 [Point::new(2, 3)..Point::new(2, 3)]
4329 );
4330
4331 // We don't blow up on the last line
4332 editor.join_lines(&JoinLines, window, cx);
4333 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4334 assert_eq!(
4335 editor.selections.ranges::<Point>(cx),
4336 [Point::new(2, 3)..Point::new(2, 3)]
4337 );
4338
4339 // reset to test indentation
4340 editor.buffer.update(cx, |buffer, cx| {
4341 buffer.edit(
4342 [
4343 (Point::new(1, 0)..Point::new(1, 2), " "),
4344 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
4345 ],
4346 None,
4347 cx,
4348 )
4349 });
4350
4351 // We remove any leading spaces
4352 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
4353 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4354 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
4355 });
4356 editor.join_lines(&JoinLines, window, cx);
4357 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
4358
4359 // We don't insert a space for a line containing only spaces
4360 editor.join_lines(&JoinLines, window, cx);
4361 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
4362
4363 // We ignore any leading tabs
4364 editor.join_lines(&JoinLines, window, cx);
4365 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
4366
4367 editor
4368 });
4369}
4370
4371#[gpui::test]
4372fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
4373 init_test(cx, |_| {});
4374
4375 cx.add_window(|window, cx| {
4376 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4377 let mut editor = build_editor(buffer.clone(), window, cx);
4378 let buffer = buffer.read(cx).as_singleton().unwrap();
4379
4380 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4381 s.select_ranges([
4382 Point::new(0, 2)..Point::new(1, 1),
4383 Point::new(1, 2)..Point::new(1, 2),
4384 Point::new(3, 1)..Point::new(3, 2),
4385 ])
4386 });
4387
4388 editor.join_lines(&JoinLines, window, cx);
4389 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
4390
4391 assert_eq!(
4392 editor.selections.ranges::<Point>(cx),
4393 [
4394 Point::new(0, 7)..Point::new(0, 7),
4395 Point::new(1, 3)..Point::new(1, 3)
4396 ]
4397 );
4398 editor
4399 });
4400}
4401
4402#[gpui::test]
4403async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
4404 init_test(cx, |_| {});
4405
4406 let mut cx = EditorTestContext::new(cx).await;
4407
4408 let diff_base = r#"
4409 Line 0
4410 Line 1
4411 Line 2
4412 Line 3
4413 "#
4414 .unindent();
4415
4416 cx.set_state(
4417 &r#"
4418 ˇLine 0
4419 Line 1
4420 Line 2
4421 Line 3
4422 "#
4423 .unindent(),
4424 );
4425
4426 cx.set_head_text(&diff_base);
4427 executor.run_until_parked();
4428
4429 // Join lines
4430 cx.update_editor(|editor, window, cx| {
4431 editor.join_lines(&JoinLines, window, cx);
4432 });
4433 executor.run_until_parked();
4434
4435 cx.assert_editor_state(
4436 &r#"
4437 Line 0ˇ Line 1
4438 Line 2
4439 Line 3
4440 "#
4441 .unindent(),
4442 );
4443 // Join again
4444 cx.update_editor(|editor, window, cx| {
4445 editor.join_lines(&JoinLines, window, cx);
4446 });
4447 executor.run_until_parked();
4448
4449 cx.assert_editor_state(
4450 &r#"
4451 Line 0 Line 1ˇ Line 2
4452 Line 3
4453 "#
4454 .unindent(),
4455 );
4456}
4457
4458#[gpui::test]
4459async fn test_custom_newlines_cause_no_false_positive_diffs(
4460 executor: BackgroundExecutor,
4461 cx: &mut TestAppContext,
4462) {
4463 init_test(cx, |_| {});
4464 let mut cx = EditorTestContext::new(cx).await;
4465 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
4466 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
4467 executor.run_until_parked();
4468
4469 cx.update_editor(|editor, window, cx| {
4470 let snapshot = editor.snapshot(window, cx);
4471 assert_eq!(
4472 snapshot
4473 .buffer_snapshot
4474 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
4475 .collect::<Vec<_>>(),
4476 Vec::new(),
4477 "Should not have any diffs for files with custom newlines"
4478 );
4479 });
4480}
4481
4482#[gpui::test]
4483async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppContext) {
4484 init_test(cx, |_| {});
4485
4486 let mut cx = EditorTestContext::new(cx).await;
4487
4488 // Test sort_lines_case_insensitive()
4489 cx.set_state(indoc! {"
4490 «z
4491 y
4492 x
4493 Z
4494 Y
4495 Xˇ»
4496 "});
4497 cx.update_editor(|e, window, cx| {
4498 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
4499 });
4500 cx.assert_editor_state(indoc! {"
4501 «x
4502 X
4503 y
4504 Y
4505 z
4506 Zˇ»
4507 "});
4508
4509 // Test sort_lines_by_length()
4510 //
4511 // Demonstrates:
4512 // - ∞ is 3 bytes UTF-8, but sorted by its char count (1)
4513 // - sort is stable
4514 cx.set_state(indoc! {"
4515 «123
4516 æ
4517 12
4518 ∞
4519 1
4520 æˇ»
4521 "});
4522 cx.update_editor(|e, window, cx| e.sort_lines_by_length(&SortLinesByLength, window, cx));
4523 cx.assert_editor_state(indoc! {"
4524 «æ
4525 ∞
4526 1
4527 æ
4528 12
4529 123ˇ»
4530 "});
4531
4532 // Test reverse_lines()
4533 cx.set_state(indoc! {"
4534 «5
4535 4
4536 3
4537 2
4538 1ˇ»
4539 "});
4540 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4541 cx.assert_editor_state(indoc! {"
4542 «1
4543 2
4544 3
4545 4
4546 5ˇ»
4547 "});
4548
4549 // Skip testing shuffle_line()
4550
4551 // From here on out, test more complex cases of manipulate_immutable_lines() with a single driver method: sort_lines_case_sensitive()
4552 // Since all methods calling manipulate_immutable_lines() are doing the exact same general thing (reordering lines)
4553
4554 // Don't manipulate when cursor is on single line, but expand the selection
4555 cx.set_state(indoc! {"
4556 ddˇdd
4557 ccc
4558 bb
4559 a
4560 "});
4561 cx.update_editor(|e, window, cx| {
4562 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4563 });
4564 cx.assert_editor_state(indoc! {"
4565 «ddddˇ»
4566 ccc
4567 bb
4568 a
4569 "});
4570
4571 // Basic manipulate case
4572 // Start selection moves to column 0
4573 // End of selection shrinks to fit shorter line
4574 cx.set_state(indoc! {"
4575 dd«d
4576 ccc
4577 bb
4578 aaaaaˇ»
4579 "});
4580 cx.update_editor(|e, window, cx| {
4581 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4582 });
4583 cx.assert_editor_state(indoc! {"
4584 «aaaaa
4585 bb
4586 ccc
4587 dddˇ»
4588 "});
4589
4590 // Manipulate case with newlines
4591 cx.set_state(indoc! {"
4592 dd«d
4593 ccc
4594
4595 bb
4596 aaaaa
4597
4598 ˇ»
4599 "});
4600 cx.update_editor(|e, window, cx| {
4601 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4602 });
4603 cx.assert_editor_state(indoc! {"
4604 «
4605
4606 aaaaa
4607 bb
4608 ccc
4609 dddˇ»
4610
4611 "});
4612
4613 // Adding new line
4614 cx.set_state(indoc! {"
4615 aa«a
4616 bbˇ»b
4617 "});
4618 cx.update_editor(|e, window, cx| {
4619 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added_line"))
4620 });
4621 cx.assert_editor_state(indoc! {"
4622 «aaa
4623 bbb
4624 added_lineˇ»
4625 "});
4626
4627 // Removing line
4628 cx.set_state(indoc! {"
4629 aa«a
4630 bbbˇ»
4631 "});
4632 cx.update_editor(|e, window, cx| {
4633 e.manipulate_immutable_lines(window, cx, |lines| {
4634 lines.pop();
4635 })
4636 });
4637 cx.assert_editor_state(indoc! {"
4638 «aaaˇ»
4639 "});
4640
4641 // Removing all lines
4642 cx.set_state(indoc! {"
4643 aa«a
4644 bbbˇ»
4645 "});
4646 cx.update_editor(|e, window, cx| {
4647 e.manipulate_immutable_lines(window, cx, |lines| {
4648 lines.drain(..);
4649 })
4650 });
4651 cx.assert_editor_state(indoc! {"
4652 ˇ
4653 "});
4654}
4655
4656#[gpui::test]
4657async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4658 init_test(cx, |_| {});
4659
4660 let mut cx = EditorTestContext::new(cx).await;
4661
4662 // Consider continuous selection as single selection
4663 cx.set_state(indoc! {"
4664 Aaa«aa
4665 cˇ»c«c
4666 bb
4667 aaaˇ»aa
4668 "});
4669 cx.update_editor(|e, window, cx| {
4670 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4671 });
4672 cx.assert_editor_state(indoc! {"
4673 «Aaaaa
4674 ccc
4675 bb
4676 aaaaaˇ»
4677 "});
4678
4679 cx.set_state(indoc! {"
4680 Aaa«aa
4681 cˇ»c«c
4682 bb
4683 aaaˇ»aa
4684 "});
4685 cx.update_editor(|e, window, cx| {
4686 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4687 });
4688 cx.assert_editor_state(indoc! {"
4689 «Aaaaa
4690 ccc
4691 bbˇ»
4692 "});
4693
4694 // Consider non continuous selection as distinct dedup operations
4695 cx.set_state(indoc! {"
4696 «aaaaa
4697 bb
4698 aaaaa
4699 aaaaaˇ»
4700
4701 aaa«aaˇ»
4702 "});
4703 cx.update_editor(|e, window, cx| {
4704 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4705 });
4706 cx.assert_editor_state(indoc! {"
4707 «aaaaa
4708 bbˇ»
4709
4710 «aaaaaˇ»
4711 "});
4712}
4713
4714#[gpui::test]
4715async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4716 init_test(cx, |_| {});
4717
4718 let mut cx = EditorTestContext::new(cx).await;
4719
4720 cx.set_state(indoc! {"
4721 «Aaa
4722 aAa
4723 Aaaˇ»
4724 "});
4725 cx.update_editor(|e, window, cx| {
4726 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4727 });
4728 cx.assert_editor_state(indoc! {"
4729 «Aaa
4730 aAaˇ»
4731 "});
4732
4733 cx.set_state(indoc! {"
4734 «Aaa
4735 aAa
4736 aaAˇ»
4737 "});
4738 cx.update_editor(|e, window, cx| {
4739 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4740 });
4741 cx.assert_editor_state(indoc! {"
4742 «Aaaˇ»
4743 "});
4744}
4745
4746#[gpui::test]
4747async fn test_wrap_in_tag_single_selection(cx: &mut TestAppContext) {
4748 init_test(cx, |_| {});
4749
4750 let mut cx = EditorTestContext::new(cx).await;
4751
4752 let js_language = Arc::new(Language::new(
4753 LanguageConfig {
4754 name: "JavaScript".into(),
4755 wrap_characters: Some(language::WrapCharactersConfig {
4756 start_prefix: "<".into(),
4757 start_suffix: ">".into(),
4758 end_prefix: "</".into(),
4759 end_suffix: ">".into(),
4760 }),
4761 ..LanguageConfig::default()
4762 },
4763 None,
4764 ));
4765
4766 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
4767
4768 cx.set_state(indoc! {"
4769 «testˇ»
4770 "});
4771 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4772 cx.assert_editor_state(indoc! {"
4773 <«ˇ»>test</«ˇ»>
4774 "});
4775
4776 cx.set_state(indoc! {"
4777 «test
4778 testˇ»
4779 "});
4780 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4781 cx.assert_editor_state(indoc! {"
4782 <«ˇ»>test
4783 test</«ˇ»>
4784 "});
4785
4786 cx.set_state(indoc! {"
4787 teˇst
4788 "});
4789 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4790 cx.assert_editor_state(indoc! {"
4791 te<«ˇ»></«ˇ»>st
4792 "});
4793}
4794
4795#[gpui::test]
4796async fn test_wrap_in_tag_multi_selection(cx: &mut TestAppContext) {
4797 init_test(cx, |_| {});
4798
4799 let mut cx = EditorTestContext::new(cx).await;
4800
4801 let js_language = Arc::new(Language::new(
4802 LanguageConfig {
4803 name: "JavaScript".into(),
4804 wrap_characters: Some(language::WrapCharactersConfig {
4805 start_prefix: "<".into(),
4806 start_suffix: ">".into(),
4807 end_prefix: "</".into(),
4808 end_suffix: ">".into(),
4809 }),
4810 ..LanguageConfig::default()
4811 },
4812 None,
4813 ));
4814
4815 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
4816
4817 cx.set_state(indoc! {"
4818 «testˇ»
4819 «testˇ» «testˇ»
4820 «testˇ»
4821 "});
4822 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4823 cx.assert_editor_state(indoc! {"
4824 <«ˇ»>test</«ˇ»>
4825 <«ˇ»>test</«ˇ»> <«ˇ»>test</«ˇ»>
4826 <«ˇ»>test</«ˇ»>
4827 "});
4828
4829 cx.set_state(indoc! {"
4830 «test
4831 testˇ»
4832 «test
4833 testˇ»
4834 "});
4835 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4836 cx.assert_editor_state(indoc! {"
4837 <«ˇ»>test
4838 test</«ˇ»>
4839 <«ˇ»>test
4840 test</«ˇ»>
4841 "});
4842}
4843
4844#[gpui::test]
4845async fn test_wrap_in_tag_does_nothing_in_unsupported_languages(cx: &mut TestAppContext) {
4846 init_test(cx, |_| {});
4847
4848 let mut cx = EditorTestContext::new(cx).await;
4849
4850 let plaintext_language = Arc::new(Language::new(
4851 LanguageConfig {
4852 name: "Plain Text".into(),
4853 ..LanguageConfig::default()
4854 },
4855 None,
4856 ));
4857
4858 cx.update_buffer(|buffer, cx| buffer.set_language(Some(plaintext_language), cx));
4859
4860 cx.set_state(indoc! {"
4861 «testˇ»
4862 "});
4863 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4864 cx.assert_editor_state(indoc! {"
4865 «testˇ»
4866 "});
4867}
4868
4869#[gpui::test]
4870async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
4871 init_test(cx, |_| {});
4872
4873 let mut cx = EditorTestContext::new(cx).await;
4874
4875 // Manipulate with multiple selections on a single line
4876 cx.set_state(indoc! {"
4877 dd«dd
4878 cˇ»c«c
4879 bb
4880 aaaˇ»aa
4881 "});
4882 cx.update_editor(|e, window, cx| {
4883 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4884 });
4885 cx.assert_editor_state(indoc! {"
4886 «aaaaa
4887 bb
4888 ccc
4889 ddddˇ»
4890 "});
4891
4892 // Manipulate with multiple disjoin selections
4893 cx.set_state(indoc! {"
4894 5«
4895 4
4896 3
4897 2
4898 1ˇ»
4899
4900 dd«dd
4901 ccc
4902 bb
4903 aaaˇ»aa
4904 "});
4905 cx.update_editor(|e, window, cx| {
4906 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4907 });
4908 cx.assert_editor_state(indoc! {"
4909 «1
4910 2
4911 3
4912 4
4913 5ˇ»
4914
4915 «aaaaa
4916 bb
4917 ccc
4918 ddddˇ»
4919 "});
4920
4921 // Adding lines on each selection
4922 cx.set_state(indoc! {"
4923 2«
4924 1ˇ»
4925
4926 bb«bb
4927 aaaˇ»aa
4928 "});
4929 cx.update_editor(|e, window, cx| {
4930 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added line"))
4931 });
4932 cx.assert_editor_state(indoc! {"
4933 «2
4934 1
4935 added lineˇ»
4936
4937 «bbbb
4938 aaaaa
4939 added lineˇ»
4940 "});
4941
4942 // Removing lines on each selection
4943 cx.set_state(indoc! {"
4944 2«
4945 1ˇ»
4946
4947 bb«bb
4948 aaaˇ»aa
4949 "});
4950 cx.update_editor(|e, window, cx| {
4951 e.manipulate_immutable_lines(window, cx, |lines| {
4952 lines.pop();
4953 })
4954 });
4955 cx.assert_editor_state(indoc! {"
4956 «2ˇ»
4957
4958 «bbbbˇ»
4959 "});
4960}
4961
4962#[gpui::test]
4963async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) {
4964 init_test(cx, |settings| {
4965 settings.defaults.tab_size = NonZeroU32::new(3)
4966 });
4967
4968 let mut cx = EditorTestContext::new(cx).await;
4969
4970 // MULTI SELECTION
4971 // Ln.1 "«" tests empty lines
4972 // Ln.9 tests just leading whitespace
4973 cx.set_state(indoc! {"
4974 «
4975 abc // No indentationˇ»
4976 «\tabc // 1 tabˇ»
4977 \t\tabc « ˇ» // 2 tabs
4978 \t ab«c // Tab followed by space
4979 \tabc // Space followed by tab (3 spaces should be the result)
4980 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4981 abˇ»ˇc ˇ ˇ // Already space indented«
4982 \t
4983 \tabc\tdef // Only the leading tab is manipulatedˇ»
4984 "});
4985 cx.update_editor(|e, window, cx| {
4986 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4987 });
4988 cx.assert_editor_state(
4989 indoc! {"
4990 «
4991 abc // No indentation
4992 abc // 1 tab
4993 abc // 2 tabs
4994 abc // Tab followed by space
4995 abc // Space followed by tab (3 spaces should be the result)
4996 abc // Mixed indentation (tab conversion depends on the column)
4997 abc // Already space indented
4998 ·
4999 abc\tdef // Only the leading tab is manipulatedˇ»
5000 "}
5001 .replace("·", "")
5002 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5003 );
5004
5005 // Test on just a few lines, the others should remain unchanged
5006 // Only lines (3, 5, 10, 11) should change
5007 cx.set_state(
5008 indoc! {"
5009 ·
5010 abc // No indentation
5011 \tabcˇ // 1 tab
5012 \t\tabc // 2 tabs
5013 \t abcˇ // Tab followed by space
5014 \tabc // Space followed by tab (3 spaces should be the result)
5015 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5016 abc // Already space indented
5017 «\t
5018 \tabc\tdef // Only the leading tab is manipulatedˇ»
5019 "}
5020 .replace("·", "")
5021 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5022 );
5023 cx.update_editor(|e, window, cx| {
5024 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5025 });
5026 cx.assert_editor_state(
5027 indoc! {"
5028 ·
5029 abc // No indentation
5030 « abc // 1 tabˇ»
5031 \t\tabc // 2 tabs
5032 « abc // Tab followed by spaceˇ»
5033 \tabc // Space followed by tab (3 spaces should be the result)
5034 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5035 abc // Already space indented
5036 « ·
5037 abc\tdef // Only the leading tab is manipulatedˇ»
5038 "}
5039 .replace("·", "")
5040 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5041 );
5042
5043 // SINGLE SELECTION
5044 // Ln.1 "«" tests empty lines
5045 // Ln.9 tests just leading whitespace
5046 cx.set_state(indoc! {"
5047 «
5048 abc // No indentation
5049 \tabc // 1 tab
5050 \t\tabc // 2 tabs
5051 \t abc // Tab followed by space
5052 \tabc // Space followed by tab (3 spaces should be the result)
5053 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5054 abc // Already space indented
5055 \t
5056 \tabc\tdef // Only the leading tab is manipulatedˇ»
5057 "});
5058 cx.update_editor(|e, window, cx| {
5059 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5060 });
5061 cx.assert_editor_state(
5062 indoc! {"
5063 «
5064 abc // No indentation
5065 abc // 1 tab
5066 abc // 2 tabs
5067 abc // Tab followed by space
5068 abc // Space followed by tab (3 spaces should be the result)
5069 abc // Mixed indentation (tab conversion depends on the column)
5070 abc // Already space indented
5071 ·
5072 abc\tdef // Only the leading tab is manipulatedˇ»
5073 "}
5074 .replace("·", "")
5075 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5076 );
5077}
5078
5079#[gpui::test]
5080async fn test_convert_indentation_to_tabs(cx: &mut TestAppContext) {
5081 init_test(cx, |settings| {
5082 settings.defaults.tab_size = NonZeroU32::new(3)
5083 });
5084
5085 let mut cx = EditorTestContext::new(cx).await;
5086
5087 // MULTI SELECTION
5088 // Ln.1 "«" tests empty lines
5089 // Ln.11 tests just leading whitespace
5090 cx.set_state(indoc! {"
5091 «
5092 abˇ»ˇc // No indentation
5093 abc ˇ ˇ // 1 space (< 3 so dont convert)
5094 abc « // 2 spaces (< 3 so dont convert)
5095 abc // 3 spaces (convert)
5096 abc ˇ» // 5 spaces (1 tab + 2 spaces)
5097 «\tˇ»\t«\tˇ»abc // Already tab indented
5098 «\t abc // Tab followed by space
5099 \tabc // Space followed by tab (should be consumed due to tab)
5100 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5101 \tˇ» «\t
5102 abcˇ» \t ˇˇˇ // Only the leading spaces should be converted
5103 "});
5104 cx.update_editor(|e, window, cx| {
5105 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5106 });
5107 cx.assert_editor_state(indoc! {"
5108 «
5109 abc // No indentation
5110 abc // 1 space (< 3 so dont convert)
5111 abc // 2 spaces (< 3 so dont convert)
5112 \tabc // 3 spaces (convert)
5113 \t abc // 5 spaces (1 tab + 2 spaces)
5114 \t\t\tabc // Already tab indented
5115 \t abc // Tab followed by space
5116 \tabc // Space followed by tab (should be consumed due to tab)
5117 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5118 \t\t\t
5119 \tabc \t // Only the leading spaces should be convertedˇ»
5120 "});
5121
5122 // Test on just a few lines, the other should remain unchanged
5123 // Only lines (4, 8, 11, 12) should change
5124 cx.set_state(
5125 indoc! {"
5126 ·
5127 abc // No indentation
5128 abc // 1 space (< 3 so dont convert)
5129 abc // 2 spaces (< 3 so dont convert)
5130 « abc // 3 spaces (convert)ˇ»
5131 abc // 5 spaces (1 tab + 2 spaces)
5132 \t\t\tabc // Already tab indented
5133 \t abc // Tab followed by space
5134 \tabc ˇ // Space followed by tab (should be consumed due to tab)
5135 \t\t \tabc // Mixed indentation
5136 \t \t \t \tabc // Mixed indentation
5137 \t \tˇ
5138 « abc \t // Only the leading spaces should be convertedˇ»
5139 "}
5140 .replace("·", "")
5141 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5142 );
5143 cx.update_editor(|e, window, cx| {
5144 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5145 });
5146 cx.assert_editor_state(
5147 indoc! {"
5148 ·
5149 abc // No indentation
5150 abc // 1 space (< 3 so dont convert)
5151 abc // 2 spaces (< 3 so dont convert)
5152 «\tabc // 3 spaces (convert)ˇ»
5153 abc // 5 spaces (1 tab + 2 spaces)
5154 \t\t\tabc // Already tab indented
5155 \t abc // Tab followed by space
5156 «\tabc // Space followed by tab (should be consumed due to tab)ˇ»
5157 \t\t \tabc // Mixed indentation
5158 \t \t \t \tabc // Mixed indentation
5159 «\t\t\t
5160 \tabc \t // Only the leading spaces should be convertedˇ»
5161 "}
5162 .replace("·", "")
5163 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5164 );
5165
5166 // SINGLE SELECTION
5167 // Ln.1 "«" tests empty lines
5168 // Ln.11 tests just leading whitespace
5169 cx.set_state(indoc! {"
5170 «
5171 abc // No indentation
5172 abc // 1 space (< 3 so dont convert)
5173 abc // 2 spaces (< 3 so dont convert)
5174 abc // 3 spaces (convert)
5175 abc // 5 spaces (1 tab + 2 spaces)
5176 \t\t\tabc // Already tab indented
5177 \t abc // Tab followed by space
5178 \tabc // Space followed by tab (should be consumed due to tab)
5179 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5180 \t \t
5181 abc \t // Only the leading spaces should be convertedˇ»
5182 "});
5183 cx.update_editor(|e, window, cx| {
5184 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5185 });
5186 cx.assert_editor_state(indoc! {"
5187 «
5188 abc // No indentation
5189 abc // 1 space (< 3 so dont convert)
5190 abc // 2 spaces (< 3 so dont convert)
5191 \tabc // 3 spaces (convert)
5192 \t abc // 5 spaces (1 tab + 2 spaces)
5193 \t\t\tabc // Already tab indented
5194 \t abc // Tab followed by space
5195 \tabc // Space followed by tab (should be consumed due to tab)
5196 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5197 \t\t\t
5198 \tabc \t // Only the leading spaces should be convertedˇ»
5199 "});
5200}
5201
5202#[gpui::test]
5203async fn test_toggle_case(cx: &mut TestAppContext) {
5204 init_test(cx, |_| {});
5205
5206 let mut cx = EditorTestContext::new(cx).await;
5207
5208 // If all lower case -> upper case
5209 cx.set_state(indoc! {"
5210 «hello worldˇ»
5211 "});
5212 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5213 cx.assert_editor_state(indoc! {"
5214 «HELLO WORLDˇ»
5215 "});
5216
5217 // If all upper case -> lower case
5218 cx.set_state(indoc! {"
5219 «HELLO WORLDˇ»
5220 "});
5221 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5222 cx.assert_editor_state(indoc! {"
5223 «hello worldˇ»
5224 "});
5225
5226 // If any upper case characters are identified -> lower case
5227 // This matches JetBrains IDEs
5228 cx.set_state(indoc! {"
5229 «hEllo worldˇ»
5230 "});
5231 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5232 cx.assert_editor_state(indoc! {"
5233 «hello worldˇ»
5234 "});
5235}
5236
5237#[gpui::test]
5238async fn test_convert_to_sentence_case(cx: &mut TestAppContext) {
5239 init_test(cx, |_| {});
5240
5241 let mut cx = EditorTestContext::new(cx).await;
5242
5243 cx.set_state(indoc! {"
5244 «implement-windows-supportˇ»
5245 "});
5246 cx.update_editor(|e, window, cx| {
5247 e.convert_to_sentence_case(&ConvertToSentenceCase, window, cx)
5248 });
5249 cx.assert_editor_state(indoc! {"
5250 «Implement windows supportˇ»
5251 "});
5252}
5253
5254#[gpui::test]
5255async fn test_manipulate_text(cx: &mut TestAppContext) {
5256 init_test(cx, |_| {});
5257
5258 let mut cx = EditorTestContext::new(cx).await;
5259
5260 // Test convert_to_upper_case()
5261 cx.set_state(indoc! {"
5262 «hello worldˇ»
5263 "});
5264 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5265 cx.assert_editor_state(indoc! {"
5266 «HELLO WORLDˇ»
5267 "});
5268
5269 // Test convert_to_lower_case()
5270 cx.set_state(indoc! {"
5271 «HELLO WORLDˇ»
5272 "});
5273 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
5274 cx.assert_editor_state(indoc! {"
5275 «hello worldˇ»
5276 "});
5277
5278 // Test multiple line, single selection case
5279 cx.set_state(indoc! {"
5280 «The quick brown
5281 fox jumps over
5282 the lazy dogˇ»
5283 "});
5284 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
5285 cx.assert_editor_state(indoc! {"
5286 «The Quick Brown
5287 Fox Jumps Over
5288 The Lazy Dogˇ»
5289 "});
5290
5291 // Test multiple line, single selection case
5292 cx.set_state(indoc! {"
5293 «The quick brown
5294 fox jumps over
5295 the lazy dogˇ»
5296 "});
5297 cx.update_editor(|e, window, cx| {
5298 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
5299 });
5300 cx.assert_editor_state(indoc! {"
5301 «TheQuickBrown
5302 FoxJumpsOver
5303 TheLazyDogˇ»
5304 "});
5305
5306 // From here on out, test more complex cases of manipulate_text()
5307
5308 // Test no selection case - should affect words cursors are in
5309 // Cursor at beginning, middle, and end of word
5310 cx.set_state(indoc! {"
5311 ˇhello big beauˇtiful worldˇ
5312 "});
5313 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5314 cx.assert_editor_state(indoc! {"
5315 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
5316 "});
5317
5318 // Test multiple selections on a single line and across multiple lines
5319 cx.set_state(indoc! {"
5320 «Theˇ» quick «brown
5321 foxˇ» jumps «overˇ»
5322 the «lazyˇ» dog
5323 "});
5324 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5325 cx.assert_editor_state(indoc! {"
5326 «THEˇ» quick «BROWN
5327 FOXˇ» jumps «OVERˇ»
5328 the «LAZYˇ» dog
5329 "});
5330
5331 // Test case where text length grows
5332 cx.set_state(indoc! {"
5333 «tschüߡ»
5334 "});
5335 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5336 cx.assert_editor_state(indoc! {"
5337 «TSCHÜSSˇ»
5338 "});
5339
5340 // Test to make sure we don't crash when text shrinks
5341 cx.set_state(indoc! {"
5342 aaa_bbbˇ
5343 "});
5344 cx.update_editor(|e, window, cx| {
5345 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5346 });
5347 cx.assert_editor_state(indoc! {"
5348 «aaaBbbˇ»
5349 "});
5350
5351 // Test to make sure we all aware of the fact that each word can grow and shrink
5352 // Final selections should be aware of this fact
5353 cx.set_state(indoc! {"
5354 aaa_bˇbb bbˇb_ccc ˇccc_ddd
5355 "});
5356 cx.update_editor(|e, window, cx| {
5357 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5358 });
5359 cx.assert_editor_state(indoc! {"
5360 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
5361 "});
5362
5363 cx.set_state(indoc! {"
5364 «hElLo, WoRld!ˇ»
5365 "});
5366 cx.update_editor(|e, window, cx| {
5367 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
5368 });
5369 cx.assert_editor_state(indoc! {"
5370 «HeLlO, wOrLD!ˇ»
5371 "});
5372
5373 // Test selections with `line_mode() = true`.
5374 cx.update_editor(|editor, _window, _cx| editor.selections.set_line_mode(true));
5375 cx.set_state(indoc! {"
5376 «The quick brown
5377 fox jumps over
5378 tˇ»he lazy dog
5379 "});
5380 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5381 cx.assert_editor_state(indoc! {"
5382 «THE QUICK BROWN
5383 FOX JUMPS OVER
5384 THE LAZY DOGˇ»
5385 "});
5386}
5387
5388#[gpui::test]
5389fn test_duplicate_line(cx: &mut TestAppContext) {
5390 init_test(cx, |_| {});
5391
5392 let editor = cx.add_window(|window, cx| {
5393 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5394 build_editor(buffer, window, cx)
5395 });
5396 _ = editor.update(cx, |editor, window, cx| {
5397 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5398 s.select_display_ranges([
5399 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5400 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5401 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5402 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5403 ])
5404 });
5405 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5406 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5407 assert_eq!(
5408 editor.selections.display_ranges(cx),
5409 vec![
5410 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
5411 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
5412 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5413 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
5414 ]
5415 );
5416 });
5417
5418 let editor = cx.add_window(|window, cx| {
5419 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5420 build_editor(buffer, window, cx)
5421 });
5422 _ = editor.update(cx, |editor, window, cx| {
5423 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5424 s.select_display_ranges([
5425 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5426 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5427 ])
5428 });
5429 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5430 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5431 assert_eq!(
5432 editor.selections.display_ranges(cx),
5433 vec![
5434 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
5435 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
5436 ]
5437 );
5438 });
5439
5440 // With `move_upwards` the selections stay in place, except for
5441 // the lines inserted above them
5442 let editor = cx.add_window(|window, cx| {
5443 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5444 build_editor(buffer, window, cx)
5445 });
5446 _ = editor.update(cx, |editor, window, cx| {
5447 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5448 s.select_display_ranges([
5449 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5450 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5451 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5452 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5453 ])
5454 });
5455 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5456 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5457 assert_eq!(
5458 editor.selections.display_ranges(cx),
5459 vec![
5460 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5461 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5462 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
5463 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
5464 ]
5465 );
5466 });
5467
5468 let editor = cx.add_window(|window, cx| {
5469 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5470 build_editor(buffer, window, cx)
5471 });
5472 _ = editor.update(cx, |editor, window, cx| {
5473 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5474 s.select_display_ranges([
5475 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5476 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5477 ])
5478 });
5479 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5480 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5481 assert_eq!(
5482 editor.selections.display_ranges(cx),
5483 vec![
5484 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5485 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5486 ]
5487 );
5488 });
5489
5490 let editor = cx.add_window(|window, cx| {
5491 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5492 build_editor(buffer, window, cx)
5493 });
5494 _ = editor.update(cx, |editor, window, cx| {
5495 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5496 s.select_display_ranges([
5497 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5498 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5499 ])
5500 });
5501 editor.duplicate_selection(&DuplicateSelection, window, cx);
5502 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
5503 assert_eq!(
5504 editor.selections.display_ranges(cx),
5505 vec![
5506 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5507 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
5508 ]
5509 );
5510 });
5511}
5512
5513#[gpui::test]
5514fn test_move_line_up_down(cx: &mut TestAppContext) {
5515 init_test(cx, |_| {});
5516
5517 let editor = cx.add_window(|window, cx| {
5518 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5519 build_editor(buffer, window, cx)
5520 });
5521 _ = editor.update(cx, |editor, window, cx| {
5522 editor.fold_creases(
5523 vec![
5524 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5525 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5526 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5527 ],
5528 true,
5529 window,
5530 cx,
5531 );
5532 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5533 s.select_display_ranges([
5534 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5535 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5536 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5537 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
5538 ])
5539 });
5540 assert_eq!(
5541 editor.display_text(cx),
5542 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
5543 );
5544
5545 editor.move_line_up(&MoveLineUp, window, cx);
5546 assert_eq!(
5547 editor.display_text(cx),
5548 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
5549 );
5550 assert_eq!(
5551 editor.selections.display_ranges(cx),
5552 vec![
5553 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5554 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5555 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5556 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5557 ]
5558 );
5559 });
5560
5561 _ = editor.update(cx, |editor, window, cx| {
5562 editor.move_line_down(&MoveLineDown, window, cx);
5563 assert_eq!(
5564 editor.display_text(cx),
5565 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
5566 );
5567 assert_eq!(
5568 editor.selections.display_ranges(cx),
5569 vec![
5570 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5571 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5572 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5573 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5574 ]
5575 );
5576 });
5577
5578 _ = editor.update(cx, |editor, window, cx| {
5579 editor.move_line_down(&MoveLineDown, window, cx);
5580 assert_eq!(
5581 editor.display_text(cx),
5582 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
5583 );
5584 assert_eq!(
5585 editor.selections.display_ranges(cx),
5586 vec![
5587 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5588 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5589 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5590 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5591 ]
5592 );
5593 });
5594
5595 _ = editor.update(cx, |editor, window, cx| {
5596 editor.move_line_up(&MoveLineUp, window, cx);
5597 assert_eq!(
5598 editor.display_text(cx),
5599 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
5600 );
5601 assert_eq!(
5602 editor.selections.display_ranges(cx),
5603 vec![
5604 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5605 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5606 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5607 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5608 ]
5609 );
5610 });
5611}
5612
5613#[gpui::test]
5614fn test_move_line_up_selection_at_end_of_fold(cx: &mut TestAppContext) {
5615 init_test(cx, |_| {});
5616 let editor = cx.add_window(|window, cx| {
5617 let buffer = MultiBuffer::build_simple("\n\n\n\n\n\naaaa\nbbbb\ncccc", cx);
5618 build_editor(buffer, window, cx)
5619 });
5620 _ = editor.update(cx, |editor, window, cx| {
5621 editor.fold_creases(
5622 vec![Crease::simple(
5623 Point::new(6, 4)..Point::new(7, 4),
5624 FoldPlaceholder::test(),
5625 )],
5626 true,
5627 window,
5628 cx,
5629 );
5630 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5631 s.select_ranges([Point::new(7, 4)..Point::new(7, 4)])
5632 });
5633 assert_eq!(editor.display_text(cx), "\n\n\n\n\n\naaaa⋯\ncccc");
5634 editor.move_line_up(&MoveLineUp, window, cx);
5635 let buffer_text = editor.buffer.read(cx).snapshot(cx).text();
5636 assert_eq!(buffer_text, "\n\n\n\n\naaaa\nbbbb\n\ncccc");
5637 });
5638}
5639
5640#[gpui::test]
5641fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
5642 init_test(cx, |_| {});
5643
5644 let editor = cx.add_window(|window, cx| {
5645 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5646 build_editor(buffer, window, cx)
5647 });
5648 _ = editor.update(cx, |editor, window, cx| {
5649 let snapshot = editor.buffer.read(cx).snapshot(cx);
5650 editor.insert_blocks(
5651 [BlockProperties {
5652 style: BlockStyle::Fixed,
5653 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
5654 height: Some(1),
5655 render: Arc::new(|_| div().into_any()),
5656 priority: 0,
5657 }],
5658 Some(Autoscroll::fit()),
5659 cx,
5660 );
5661 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5662 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
5663 });
5664 editor.move_line_down(&MoveLineDown, window, cx);
5665 });
5666}
5667
5668#[gpui::test]
5669async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
5670 init_test(cx, |_| {});
5671
5672 let mut cx = EditorTestContext::new(cx).await;
5673 cx.set_state(
5674 &"
5675 ˇzero
5676 one
5677 two
5678 three
5679 four
5680 five
5681 "
5682 .unindent(),
5683 );
5684
5685 // Create a four-line block that replaces three lines of text.
5686 cx.update_editor(|editor, window, cx| {
5687 let snapshot = editor.snapshot(window, cx);
5688 let snapshot = &snapshot.buffer_snapshot;
5689 let placement = BlockPlacement::Replace(
5690 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
5691 );
5692 editor.insert_blocks(
5693 [BlockProperties {
5694 placement,
5695 height: Some(4),
5696 style: BlockStyle::Sticky,
5697 render: Arc::new(|_| gpui::div().into_any_element()),
5698 priority: 0,
5699 }],
5700 None,
5701 cx,
5702 );
5703 });
5704
5705 // Move down so that the cursor touches the block.
5706 cx.update_editor(|editor, window, cx| {
5707 editor.move_down(&Default::default(), window, cx);
5708 });
5709 cx.assert_editor_state(
5710 &"
5711 zero
5712 «one
5713 two
5714 threeˇ»
5715 four
5716 five
5717 "
5718 .unindent(),
5719 );
5720
5721 // Move down past the block.
5722 cx.update_editor(|editor, window, cx| {
5723 editor.move_down(&Default::default(), window, cx);
5724 });
5725 cx.assert_editor_state(
5726 &"
5727 zero
5728 one
5729 two
5730 three
5731 ˇfour
5732 five
5733 "
5734 .unindent(),
5735 );
5736}
5737
5738#[gpui::test]
5739fn test_transpose(cx: &mut TestAppContext) {
5740 init_test(cx, |_| {});
5741
5742 _ = cx.add_window(|window, cx| {
5743 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
5744 editor.set_style(EditorStyle::default(), window, cx);
5745 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5746 s.select_ranges([1..1])
5747 });
5748 editor.transpose(&Default::default(), window, cx);
5749 assert_eq!(editor.text(cx), "bac");
5750 assert_eq!(editor.selections.ranges(cx), [2..2]);
5751
5752 editor.transpose(&Default::default(), window, cx);
5753 assert_eq!(editor.text(cx), "bca");
5754 assert_eq!(editor.selections.ranges(cx), [3..3]);
5755
5756 editor.transpose(&Default::default(), window, cx);
5757 assert_eq!(editor.text(cx), "bac");
5758 assert_eq!(editor.selections.ranges(cx), [3..3]);
5759
5760 editor
5761 });
5762
5763 _ = cx.add_window(|window, cx| {
5764 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5765 editor.set_style(EditorStyle::default(), window, cx);
5766 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5767 s.select_ranges([3..3])
5768 });
5769 editor.transpose(&Default::default(), window, cx);
5770 assert_eq!(editor.text(cx), "acb\nde");
5771 assert_eq!(editor.selections.ranges(cx), [3..3]);
5772
5773 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5774 s.select_ranges([4..4])
5775 });
5776 editor.transpose(&Default::default(), window, cx);
5777 assert_eq!(editor.text(cx), "acbd\ne");
5778 assert_eq!(editor.selections.ranges(cx), [5..5]);
5779
5780 editor.transpose(&Default::default(), window, cx);
5781 assert_eq!(editor.text(cx), "acbde\n");
5782 assert_eq!(editor.selections.ranges(cx), [6..6]);
5783
5784 editor.transpose(&Default::default(), window, cx);
5785 assert_eq!(editor.text(cx), "acbd\ne");
5786 assert_eq!(editor.selections.ranges(cx), [6..6]);
5787
5788 editor
5789 });
5790
5791 _ = cx.add_window(|window, cx| {
5792 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5793 editor.set_style(EditorStyle::default(), window, cx);
5794 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5795 s.select_ranges([1..1, 2..2, 4..4])
5796 });
5797 editor.transpose(&Default::default(), window, cx);
5798 assert_eq!(editor.text(cx), "bacd\ne");
5799 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
5800
5801 editor.transpose(&Default::default(), window, cx);
5802 assert_eq!(editor.text(cx), "bcade\n");
5803 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
5804
5805 editor.transpose(&Default::default(), window, cx);
5806 assert_eq!(editor.text(cx), "bcda\ne");
5807 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5808
5809 editor.transpose(&Default::default(), window, cx);
5810 assert_eq!(editor.text(cx), "bcade\n");
5811 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5812
5813 editor.transpose(&Default::default(), window, cx);
5814 assert_eq!(editor.text(cx), "bcaed\n");
5815 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
5816
5817 editor
5818 });
5819
5820 _ = cx.add_window(|window, cx| {
5821 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
5822 editor.set_style(EditorStyle::default(), window, cx);
5823 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5824 s.select_ranges([4..4])
5825 });
5826 editor.transpose(&Default::default(), window, cx);
5827 assert_eq!(editor.text(cx), "🏀🍐✋");
5828 assert_eq!(editor.selections.ranges(cx), [8..8]);
5829
5830 editor.transpose(&Default::default(), window, cx);
5831 assert_eq!(editor.text(cx), "🏀✋🍐");
5832 assert_eq!(editor.selections.ranges(cx), [11..11]);
5833
5834 editor.transpose(&Default::default(), window, cx);
5835 assert_eq!(editor.text(cx), "🏀🍐✋");
5836 assert_eq!(editor.selections.ranges(cx), [11..11]);
5837
5838 editor
5839 });
5840}
5841
5842#[gpui::test]
5843async fn test_rewrap(cx: &mut TestAppContext) {
5844 init_test(cx, |settings| {
5845 settings.languages.0.extend([
5846 (
5847 "Markdown".into(),
5848 LanguageSettingsContent {
5849 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5850 preferred_line_length: Some(40),
5851 ..Default::default()
5852 },
5853 ),
5854 (
5855 "Plain Text".into(),
5856 LanguageSettingsContent {
5857 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5858 preferred_line_length: Some(40),
5859 ..Default::default()
5860 },
5861 ),
5862 (
5863 "C++".into(),
5864 LanguageSettingsContent {
5865 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5866 preferred_line_length: Some(40),
5867 ..Default::default()
5868 },
5869 ),
5870 (
5871 "Python".into(),
5872 LanguageSettingsContent {
5873 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5874 preferred_line_length: Some(40),
5875 ..Default::default()
5876 },
5877 ),
5878 (
5879 "Rust".into(),
5880 LanguageSettingsContent {
5881 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5882 preferred_line_length: Some(40),
5883 ..Default::default()
5884 },
5885 ),
5886 ])
5887 });
5888
5889 let mut cx = EditorTestContext::new(cx).await;
5890
5891 let cpp_language = Arc::new(Language::new(
5892 LanguageConfig {
5893 name: "C++".into(),
5894 line_comments: vec!["// ".into()],
5895 ..LanguageConfig::default()
5896 },
5897 None,
5898 ));
5899 let python_language = Arc::new(Language::new(
5900 LanguageConfig {
5901 name: "Python".into(),
5902 line_comments: vec!["# ".into()],
5903 ..LanguageConfig::default()
5904 },
5905 None,
5906 ));
5907 let markdown_language = Arc::new(Language::new(
5908 LanguageConfig {
5909 name: "Markdown".into(),
5910 rewrap_prefixes: vec![
5911 regex::Regex::new("\\d+\\.\\s+").unwrap(),
5912 regex::Regex::new("[-*+]\\s+").unwrap(),
5913 ],
5914 ..LanguageConfig::default()
5915 },
5916 None,
5917 ));
5918 let rust_language = Arc::new(
5919 Language::new(
5920 LanguageConfig {
5921 name: "Rust".into(),
5922 line_comments: vec!["// ".into(), "/// ".into()],
5923 ..LanguageConfig::default()
5924 },
5925 Some(tree_sitter_rust::LANGUAGE.into()),
5926 )
5927 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
5928 .unwrap(),
5929 );
5930
5931 let plaintext_language = Arc::new(Language::new(
5932 LanguageConfig {
5933 name: "Plain Text".into(),
5934 ..LanguageConfig::default()
5935 },
5936 None,
5937 ));
5938
5939 // Test basic rewrapping of a long line with a cursor
5940 assert_rewrap(
5941 indoc! {"
5942 // ˇThis is a long comment that needs to be wrapped.
5943 "},
5944 indoc! {"
5945 // ˇThis is a long comment that needs to
5946 // be wrapped.
5947 "},
5948 cpp_language.clone(),
5949 &mut cx,
5950 );
5951
5952 // Test rewrapping a full selection
5953 assert_rewrap(
5954 indoc! {"
5955 «// This selected long comment needs to be wrapped.ˇ»"
5956 },
5957 indoc! {"
5958 «// This selected long comment needs to
5959 // be wrapped.ˇ»"
5960 },
5961 cpp_language.clone(),
5962 &mut cx,
5963 );
5964
5965 // Test multiple cursors on different lines within the same paragraph are preserved after rewrapping
5966 assert_rewrap(
5967 indoc! {"
5968 // ˇThis is the first line.
5969 // Thisˇ is the second line.
5970 // This is the thirdˇ line, all part of one paragraph.
5971 "},
5972 indoc! {"
5973 // ˇThis is the first line. Thisˇ is the
5974 // second line. This is the thirdˇ line,
5975 // all part of one paragraph.
5976 "},
5977 cpp_language.clone(),
5978 &mut cx,
5979 );
5980
5981 // Test multiple cursors in different paragraphs trigger separate rewraps
5982 assert_rewrap(
5983 indoc! {"
5984 // ˇThis is the first paragraph, first line.
5985 // ˇThis is the first paragraph, second line.
5986
5987 // ˇThis is the second paragraph, first line.
5988 // ˇThis is the second paragraph, second line.
5989 "},
5990 indoc! {"
5991 // ˇThis is the first paragraph, first
5992 // line. ˇThis is the first paragraph,
5993 // second line.
5994
5995 // ˇThis is the second paragraph, first
5996 // line. ˇThis is the second paragraph,
5997 // second line.
5998 "},
5999 cpp_language.clone(),
6000 &mut cx,
6001 );
6002
6003 // Test that change in comment prefix (e.g., `//` to `///`) trigger seperate rewraps
6004 assert_rewrap(
6005 indoc! {"
6006 «// A regular long long comment to be wrapped.
6007 /// A documentation long comment to be wrapped.ˇ»
6008 "},
6009 indoc! {"
6010 «// A regular long long comment to be
6011 // wrapped.
6012 /// A documentation long comment to be
6013 /// wrapped.ˇ»
6014 "},
6015 rust_language.clone(),
6016 &mut cx,
6017 );
6018
6019 // Test that change in indentation level trigger seperate rewraps
6020 assert_rewrap(
6021 indoc! {"
6022 fn foo() {
6023 «// This is a long comment at the base indent.
6024 // This is a long comment at the next indent.ˇ»
6025 }
6026 "},
6027 indoc! {"
6028 fn foo() {
6029 «// This is a long comment at the
6030 // base indent.
6031 // This is a long comment at the
6032 // next indent.ˇ»
6033 }
6034 "},
6035 rust_language.clone(),
6036 &mut cx,
6037 );
6038
6039 // Test that different comment prefix characters (e.g., '#') are handled correctly
6040 assert_rewrap(
6041 indoc! {"
6042 # ˇThis is a long comment using a pound sign.
6043 "},
6044 indoc! {"
6045 # ˇThis is a long comment using a pound
6046 # sign.
6047 "},
6048 python_language,
6049 &mut cx,
6050 );
6051
6052 // Test rewrapping only affects comments, not code even when selected
6053 assert_rewrap(
6054 indoc! {"
6055 «/// This doc comment is long and should be wrapped.
6056 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6057 "},
6058 indoc! {"
6059 «/// This doc comment is long and should
6060 /// be wrapped.
6061 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6062 "},
6063 rust_language.clone(),
6064 &mut cx,
6065 );
6066
6067 // Test that rewrapping works in Markdown documents where `allow_rewrap` is `Anywhere`
6068 assert_rewrap(
6069 indoc! {"
6070 # Header
6071
6072 A long long long line of markdown text to wrap.ˇ
6073 "},
6074 indoc! {"
6075 # Header
6076
6077 A long long long line of markdown text
6078 to wrap.ˇ
6079 "},
6080 markdown_language.clone(),
6081 &mut cx,
6082 );
6083
6084 // Test that rewrapping boundary works and preserves relative indent for Markdown documents
6085 assert_rewrap(
6086 indoc! {"
6087 «1. This is a numbered list item that is very long and needs to be wrapped properly.
6088 2. This is a numbered list item that is very long and needs to be wrapped properly.
6089 - This is an unordered list item that is also very long and should not merge with the numbered item.ˇ»
6090 "},
6091 indoc! {"
6092 «1. This is a numbered list item that is
6093 very long and needs to be wrapped
6094 properly.
6095 2. This is a numbered list item that is
6096 very long and needs to be wrapped
6097 properly.
6098 - This is an unordered list item that is
6099 also very long and should not merge
6100 with the numbered item.ˇ»
6101 "},
6102 markdown_language.clone(),
6103 &mut cx,
6104 );
6105
6106 // Test that rewrapping add indents for rewrapping boundary if not exists already.
6107 assert_rewrap(
6108 indoc! {"
6109 «1. This is a numbered list item that is
6110 very long and needs to be wrapped
6111 properly.
6112 2. This is a numbered list item that is
6113 very long and needs to be wrapped
6114 properly.
6115 - This is an unordered list item that is
6116 also very long and should not merge with
6117 the numbered item.ˇ»
6118 "},
6119 indoc! {"
6120 «1. This is a numbered list item that is
6121 very long and needs to be wrapped
6122 properly.
6123 2. This is a numbered list item that is
6124 very long and needs to be wrapped
6125 properly.
6126 - This is an unordered list item that is
6127 also very long and should not merge
6128 with the numbered item.ˇ»
6129 "},
6130 markdown_language.clone(),
6131 &mut cx,
6132 );
6133
6134 // Test that rewrapping maintain indents even when they already exists.
6135 assert_rewrap(
6136 indoc! {"
6137 «1. This is a numbered list
6138 item that is very long and needs to be wrapped properly.
6139 2. This is a numbered list
6140 item that is very long and needs to be wrapped properly.
6141 - This is an unordered list item that is also very long and
6142 should not merge with the numbered item.ˇ»
6143 "},
6144 indoc! {"
6145 «1. This is a numbered list item that is
6146 very long and needs to be wrapped
6147 properly.
6148 2. This is a numbered list item that is
6149 very long and needs to be wrapped
6150 properly.
6151 - This is an unordered list item that is
6152 also very long and should not merge
6153 with the numbered item.ˇ»
6154 "},
6155 markdown_language,
6156 &mut cx,
6157 );
6158
6159 // Test that rewrapping works in plain text where `allow_rewrap` is `Anywhere`
6160 assert_rewrap(
6161 indoc! {"
6162 ˇThis is a very long line of plain text that will be wrapped.
6163 "},
6164 indoc! {"
6165 ˇThis is a very long line of plain text
6166 that will be wrapped.
6167 "},
6168 plaintext_language.clone(),
6169 &mut cx,
6170 );
6171
6172 // Test that non-commented code acts as a paragraph boundary within a selection
6173 assert_rewrap(
6174 indoc! {"
6175 «// This is the first long comment block to be wrapped.
6176 fn my_func(a: u32);
6177 // This is the second long comment block to be wrapped.ˇ»
6178 "},
6179 indoc! {"
6180 «// This is the first long comment block
6181 // to be wrapped.
6182 fn my_func(a: u32);
6183 // This is the second long comment block
6184 // to be wrapped.ˇ»
6185 "},
6186 rust_language,
6187 &mut cx,
6188 );
6189
6190 // Test rewrapping multiple selections, including ones with blank lines or tabs
6191 assert_rewrap(
6192 indoc! {"
6193 «ˇThis is a very long line that will be wrapped.
6194
6195 This is another paragraph in the same selection.»
6196
6197 «\tThis is a very long indented line that will be wrapped.ˇ»
6198 "},
6199 indoc! {"
6200 «ˇThis is a very long line that will be
6201 wrapped.
6202
6203 This is another paragraph in the same
6204 selection.»
6205
6206 «\tThis is a very long indented line
6207 \tthat will be wrapped.ˇ»
6208 "},
6209 plaintext_language,
6210 &mut cx,
6211 );
6212
6213 // Test that an empty comment line acts as a paragraph boundary
6214 assert_rewrap(
6215 indoc! {"
6216 // ˇThis is a long comment that will be wrapped.
6217 //
6218 // And this is another long comment that will also be wrapped.ˇ
6219 "},
6220 indoc! {"
6221 // ˇThis is a long comment that will be
6222 // wrapped.
6223 //
6224 // And this is another long comment that
6225 // will also be wrapped.ˇ
6226 "},
6227 cpp_language,
6228 &mut cx,
6229 );
6230
6231 #[track_caller]
6232 fn assert_rewrap(
6233 unwrapped_text: &str,
6234 wrapped_text: &str,
6235 language: Arc<Language>,
6236 cx: &mut EditorTestContext,
6237 ) {
6238 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6239 cx.set_state(unwrapped_text);
6240 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6241 cx.assert_editor_state(wrapped_text);
6242 }
6243}
6244
6245#[gpui::test]
6246async fn test_rewrap_block_comments(cx: &mut TestAppContext) {
6247 init_test(cx, |settings| {
6248 settings.languages.0.extend([(
6249 "Rust".into(),
6250 LanguageSettingsContent {
6251 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6252 preferred_line_length: Some(40),
6253 ..Default::default()
6254 },
6255 )])
6256 });
6257
6258 let mut cx = EditorTestContext::new(cx).await;
6259
6260 let rust_lang = Arc::new(
6261 Language::new(
6262 LanguageConfig {
6263 name: "Rust".into(),
6264 line_comments: vec!["// ".into()],
6265 block_comment: Some(BlockCommentConfig {
6266 start: "/*".into(),
6267 end: "*/".into(),
6268 prefix: "* ".into(),
6269 tab_size: 1,
6270 }),
6271 documentation_comment: Some(BlockCommentConfig {
6272 start: "/**".into(),
6273 end: "*/".into(),
6274 prefix: "* ".into(),
6275 tab_size: 1,
6276 }),
6277
6278 ..LanguageConfig::default()
6279 },
6280 Some(tree_sitter_rust::LANGUAGE.into()),
6281 )
6282 .with_override_query("[(line_comment) (block_comment)] @comment.inclusive")
6283 .unwrap(),
6284 );
6285
6286 // regular block comment
6287 assert_rewrap(
6288 indoc! {"
6289 /*
6290 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6291 */
6292 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6293 "},
6294 indoc! {"
6295 /*
6296 *ˇ Lorem ipsum dolor sit amet,
6297 * consectetur adipiscing elit.
6298 */
6299 /*
6300 *ˇ Lorem ipsum dolor sit amet,
6301 * consectetur adipiscing elit.
6302 */
6303 "},
6304 rust_lang.clone(),
6305 &mut cx,
6306 );
6307
6308 // indent is respected
6309 assert_rewrap(
6310 indoc! {"
6311 {}
6312 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6313 "},
6314 indoc! {"
6315 {}
6316 /*
6317 *ˇ Lorem ipsum dolor sit amet,
6318 * consectetur adipiscing elit.
6319 */
6320 "},
6321 rust_lang.clone(),
6322 &mut cx,
6323 );
6324
6325 // short block comments with inline delimiters
6326 assert_rewrap(
6327 indoc! {"
6328 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6329 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6330 */
6331 /*
6332 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6333 "},
6334 indoc! {"
6335 /*
6336 *ˇ Lorem ipsum dolor sit amet,
6337 * consectetur adipiscing elit.
6338 */
6339 /*
6340 *ˇ Lorem ipsum dolor sit amet,
6341 * consectetur adipiscing elit.
6342 */
6343 /*
6344 *ˇ Lorem ipsum dolor sit amet,
6345 * consectetur adipiscing elit.
6346 */
6347 "},
6348 rust_lang.clone(),
6349 &mut cx,
6350 );
6351
6352 // multiline block comment with inline start/end delimiters
6353 assert_rewrap(
6354 indoc! {"
6355 /*ˇ Lorem ipsum dolor sit amet,
6356 * consectetur adipiscing elit. */
6357 "},
6358 indoc! {"
6359 /*
6360 *ˇ Lorem ipsum dolor sit amet,
6361 * consectetur adipiscing elit.
6362 */
6363 "},
6364 rust_lang.clone(),
6365 &mut cx,
6366 );
6367
6368 // block comment rewrap still respects paragraph bounds
6369 assert_rewrap(
6370 indoc! {"
6371 /*
6372 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6373 *
6374 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6375 */
6376 "},
6377 indoc! {"
6378 /*
6379 *ˇ Lorem ipsum dolor sit amet,
6380 * consectetur adipiscing elit.
6381 *
6382 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6383 */
6384 "},
6385 rust_lang.clone(),
6386 &mut cx,
6387 );
6388
6389 // documentation comments
6390 assert_rewrap(
6391 indoc! {"
6392 /**ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6393 /**
6394 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6395 */
6396 "},
6397 indoc! {"
6398 /**
6399 *ˇ Lorem ipsum dolor sit amet,
6400 * consectetur adipiscing elit.
6401 */
6402 /**
6403 *ˇ Lorem ipsum dolor sit amet,
6404 * consectetur adipiscing elit.
6405 */
6406 "},
6407 rust_lang.clone(),
6408 &mut cx,
6409 );
6410
6411 // different, adjacent comments
6412 assert_rewrap(
6413 indoc! {"
6414 /**
6415 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6416 */
6417 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6418 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6419 "},
6420 indoc! {"
6421 /**
6422 *ˇ Lorem ipsum dolor sit amet,
6423 * consectetur adipiscing elit.
6424 */
6425 /*
6426 *ˇ Lorem ipsum dolor sit amet,
6427 * consectetur adipiscing elit.
6428 */
6429 //ˇ Lorem ipsum dolor sit amet,
6430 // consectetur adipiscing elit.
6431 "},
6432 rust_lang.clone(),
6433 &mut cx,
6434 );
6435
6436 // selection w/ single short block comment
6437 assert_rewrap(
6438 indoc! {"
6439 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6440 "},
6441 indoc! {"
6442 «/*
6443 * Lorem ipsum dolor sit amet,
6444 * consectetur adipiscing elit.
6445 */ˇ»
6446 "},
6447 rust_lang.clone(),
6448 &mut cx,
6449 );
6450
6451 // rewrapping a single comment w/ abutting comments
6452 assert_rewrap(
6453 indoc! {"
6454 /* ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. */
6455 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6456 "},
6457 indoc! {"
6458 /*
6459 * ˇLorem ipsum dolor sit amet,
6460 * consectetur adipiscing elit.
6461 */
6462 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6463 "},
6464 rust_lang.clone(),
6465 &mut cx,
6466 );
6467
6468 // selection w/ non-abutting short block comments
6469 assert_rewrap(
6470 indoc! {"
6471 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6472
6473 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6474 "},
6475 indoc! {"
6476 «/*
6477 * Lorem ipsum dolor sit amet,
6478 * consectetur adipiscing elit.
6479 */
6480
6481 /*
6482 * Lorem ipsum dolor sit amet,
6483 * consectetur adipiscing elit.
6484 */ˇ»
6485 "},
6486 rust_lang.clone(),
6487 &mut cx,
6488 );
6489
6490 // selection of multiline block comments
6491 assert_rewrap(
6492 indoc! {"
6493 «/* Lorem ipsum dolor sit amet,
6494 * consectetur adipiscing elit. */ˇ»
6495 "},
6496 indoc! {"
6497 «/*
6498 * Lorem ipsum dolor sit amet,
6499 * consectetur adipiscing elit.
6500 */ˇ»
6501 "},
6502 rust_lang.clone(),
6503 &mut cx,
6504 );
6505
6506 // partial selection of multiline block comments
6507 assert_rewrap(
6508 indoc! {"
6509 «/* Lorem ipsum dolor sit amet,ˇ»
6510 * consectetur adipiscing elit. */
6511 /* Lorem ipsum dolor sit amet,
6512 «* consectetur adipiscing elit. */ˇ»
6513 "},
6514 indoc! {"
6515 «/*
6516 * Lorem ipsum dolor sit amet,ˇ»
6517 * consectetur adipiscing elit. */
6518 /* Lorem ipsum dolor sit amet,
6519 «* consectetur adipiscing elit.
6520 */ˇ»
6521 "},
6522 rust_lang.clone(),
6523 &mut cx,
6524 );
6525
6526 // selection w/ abutting short block comments
6527 // TODO: should not be combined; should rewrap as 2 comments
6528 assert_rewrap(
6529 indoc! {"
6530 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6531 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6532 "},
6533 // desired behavior:
6534 // indoc! {"
6535 // «/*
6536 // * Lorem ipsum dolor sit amet,
6537 // * consectetur adipiscing elit.
6538 // */
6539 // /*
6540 // * Lorem ipsum dolor sit amet,
6541 // * consectetur adipiscing elit.
6542 // */ˇ»
6543 // "},
6544 // actual behaviour:
6545 indoc! {"
6546 «/*
6547 * Lorem ipsum dolor sit amet,
6548 * consectetur adipiscing elit. Lorem
6549 * ipsum dolor sit amet, consectetur
6550 * adipiscing elit.
6551 */ˇ»
6552 "},
6553 rust_lang.clone(),
6554 &mut cx,
6555 );
6556
6557 // TODO: same as above, but with delimiters on separate line
6558 // assert_rewrap(
6559 // indoc! {"
6560 // «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6561 // */
6562 // /*
6563 // * Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6564 // "},
6565 // // desired:
6566 // // indoc! {"
6567 // // «/*
6568 // // * Lorem ipsum dolor sit amet,
6569 // // * consectetur adipiscing elit.
6570 // // */
6571 // // /*
6572 // // * Lorem ipsum dolor sit amet,
6573 // // * consectetur adipiscing elit.
6574 // // */ˇ»
6575 // // "},
6576 // // actual: (but with trailing w/s on the empty lines)
6577 // indoc! {"
6578 // «/*
6579 // * Lorem ipsum dolor sit amet,
6580 // * consectetur adipiscing elit.
6581 // *
6582 // */
6583 // /*
6584 // *
6585 // * Lorem ipsum dolor sit amet,
6586 // * consectetur adipiscing elit.
6587 // */ˇ»
6588 // "},
6589 // rust_lang.clone(),
6590 // &mut cx,
6591 // );
6592
6593 // TODO these are unhandled edge cases; not correct, just documenting known issues
6594 assert_rewrap(
6595 indoc! {"
6596 /*
6597 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6598 */
6599 /*
6600 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6601 /*ˇ Lorem ipsum dolor sit amet */ /* consectetur adipiscing elit. */
6602 "},
6603 // desired:
6604 // indoc! {"
6605 // /*
6606 // *ˇ Lorem ipsum dolor sit amet,
6607 // * consectetur adipiscing elit.
6608 // */
6609 // /*
6610 // *ˇ Lorem ipsum dolor sit amet,
6611 // * consectetur adipiscing elit.
6612 // */
6613 // /*
6614 // *ˇ Lorem ipsum dolor sit amet
6615 // */ /* consectetur adipiscing elit. */
6616 // "},
6617 // actual:
6618 indoc! {"
6619 /*
6620 //ˇ Lorem ipsum dolor sit amet,
6621 // consectetur adipiscing elit.
6622 */
6623 /*
6624 * //ˇ Lorem ipsum dolor sit amet,
6625 * consectetur adipiscing elit.
6626 */
6627 /*
6628 *ˇ Lorem ipsum dolor sit amet */ /*
6629 * consectetur adipiscing elit.
6630 */
6631 "},
6632 rust_lang,
6633 &mut cx,
6634 );
6635
6636 #[track_caller]
6637 fn assert_rewrap(
6638 unwrapped_text: &str,
6639 wrapped_text: &str,
6640 language: Arc<Language>,
6641 cx: &mut EditorTestContext,
6642 ) {
6643 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6644 cx.set_state(unwrapped_text);
6645 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6646 cx.assert_editor_state(wrapped_text);
6647 }
6648}
6649
6650#[gpui::test]
6651async fn test_hard_wrap(cx: &mut TestAppContext) {
6652 init_test(cx, |_| {});
6653 let mut cx = EditorTestContext::new(cx).await;
6654
6655 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
6656 cx.update_editor(|editor, _, cx| {
6657 editor.set_hard_wrap(Some(14), cx);
6658 });
6659
6660 cx.set_state(indoc!(
6661 "
6662 one two three ˇ
6663 "
6664 ));
6665 cx.simulate_input("four");
6666 cx.run_until_parked();
6667
6668 cx.assert_editor_state(indoc!(
6669 "
6670 one two three
6671 fourˇ
6672 "
6673 ));
6674
6675 cx.update_editor(|editor, window, cx| {
6676 editor.newline(&Default::default(), window, cx);
6677 });
6678 cx.run_until_parked();
6679 cx.assert_editor_state(indoc!(
6680 "
6681 one two three
6682 four
6683 ˇ
6684 "
6685 ));
6686
6687 cx.simulate_input("five");
6688 cx.run_until_parked();
6689 cx.assert_editor_state(indoc!(
6690 "
6691 one two three
6692 four
6693 fiveˇ
6694 "
6695 ));
6696
6697 cx.update_editor(|editor, window, cx| {
6698 editor.newline(&Default::default(), window, cx);
6699 });
6700 cx.run_until_parked();
6701 cx.simulate_input("# ");
6702 cx.run_until_parked();
6703 cx.assert_editor_state(indoc!(
6704 "
6705 one two three
6706 four
6707 five
6708 # ˇ
6709 "
6710 ));
6711
6712 cx.update_editor(|editor, window, cx| {
6713 editor.newline(&Default::default(), window, cx);
6714 });
6715 cx.run_until_parked();
6716 cx.assert_editor_state(indoc!(
6717 "
6718 one two three
6719 four
6720 five
6721 #\x20
6722 #ˇ
6723 "
6724 ));
6725
6726 cx.simulate_input(" 6");
6727 cx.run_until_parked();
6728 cx.assert_editor_state(indoc!(
6729 "
6730 one two three
6731 four
6732 five
6733 #
6734 # 6ˇ
6735 "
6736 ));
6737}
6738
6739#[gpui::test]
6740async fn test_cut_line_ends(cx: &mut TestAppContext) {
6741 init_test(cx, |_| {});
6742
6743 let mut cx = EditorTestContext::new(cx).await;
6744
6745 cx.set_state(indoc! {"The quick brownˇ"});
6746 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
6747 cx.assert_editor_state(indoc! {"The quick brownˇ"});
6748
6749 cx.set_state(indoc! {"The emacs foxˇ"});
6750 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
6751 cx.assert_editor_state(indoc! {"The emacs foxˇ"});
6752
6753 cx.set_state(indoc! {"
6754 The quick« brownˇ»
6755 fox jumps overˇ
6756 the lazy dog"});
6757 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6758 cx.assert_editor_state(indoc! {"
6759 The quickˇ
6760 ˇthe lazy dog"});
6761
6762 cx.set_state(indoc! {"
6763 The quick« brownˇ»
6764 fox jumps overˇ
6765 the lazy dog"});
6766 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
6767 cx.assert_editor_state(indoc! {"
6768 The quickˇ
6769 fox jumps overˇthe lazy dog"});
6770
6771 cx.set_state(indoc! {"
6772 The quick« brownˇ»
6773 fox jumps overˇ
6774 the lazy dog"});
6775 cx.update_editor(|e, window, cx| {
6776 e.cut_to_end_of_line(
6777 &CutToEndOfLine {
6778 stop_at_newlines: true,
6779 },
6780 window,
6781 cx,
6782 )
6783 });
6784 cx.assert_editor_state(indoc! {"
6785 The quickˇ
6786 fox jumps overˇ
6787 the lazy dog"});
6788
6789 cx.set_state(indoc! {"
6790 The quick« brownˇ»
6791 fox jumps overˇ
6792 the lazy dog"});
6793 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
6794 cx.assert_editor_state(indoc! {"
6795 The quickˇ
6796 fox jumps overˇthe lazy dog"});
6797}
6798
6799#[gpui::test]
6800async fn test_clipboard(cx: &mut TestAppContext) {
6801 init_test(cx, |_| {});
6802
6803 let mut cx = EditorTestContext::new(cx).await;
6804
6805 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
6806 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6807 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
6808
6809 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
6810 cx.set_state("two ˇfour ˇsix ˇ");
6811 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6812 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
6813
6814 // Paste again but with only two cursors. Since the number of cursors doesn't
6815 // match the number of slices in the clipboard, the entire clipboard text
6816 // is pasted at each cursor.
6817 cx.set_state("ˇtwo one✅ four three six five ˇ");
6818 cx.update_editor(|e, window, cx| {
6819 e.handle_input("( ", window, cx);
6820 e.paste(&Paste, window, cx);
6821 e.handle_input(") ", window, cx);
6822 });
6823 cx.assert_editor_state(
6824 &([
6825 "( one✅ ",
6826 "three ",
6827 "five ) ˇtwo one✅ four three six five ( one✅ ",
6828 "three ",
6829 "five ) ˇ",
6830 ]
6831 .join("\n")),
6832 );
6833
6834 // Cut with three selections, one of which is full-line.
6835 cx.set_state(indoc! {"
6836 1«2ˇ»3
6837 4ˇ567
6838 «8ˇ»9"});
6839 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6840 cx.assert_editor_state(indoc! {"
6841 1ˇ3
6842 ˇ9"});
6843
6844 // Paste with three selections, noticing how the copied selection that was full-line
6845 // gets inserted before the second cursor.
6846 cx.set_state(indoc! {"
6847 1ˇ3
6848 9ˇ
6849 «oˇ»ne"});
6850 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6851 cx.assert_editor_state(indoc! {"
6852 12ˇ3
6853 4567
6854 9ˇ
6855 8ˇne"});
6856
6857 // Copy with a single cursor only, which writes the whole line into the clipboard.
6858 cx.set_state(indoc! {"
6859 The quick brown
6860 fox juˇmps over
6861 the lazy dog"});
6862 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6863 assert_eq!(
6864 cx.read_from_clipboard()
6865 .and_then(|item| item.text().as_deref().map(str::to_string)),
6866 Some("fox jumps over\n".to_string())
6867 );
6868
6869 // Paste with three selections, noticing how the copied full-line selection is inserted
6870 // before the empty selections but replaces the selection that is non-empty.
6871 cx.set_state(indoc! {"
6872 Tˇhe quick brown
6873 «foˇ»x jumps over
6874 tˇhe lazy dog"});
6875 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6876 cx.assert_editor_state(indoc! {"
6877 fox jumps over
6878 Tˇhe quick brown
6879 fox jumps over
6880 ˇx jumps over
6881 fox jumps over
6882 tˇhe lazy dog"});
6883}
6884
6885#[gpui::test]
6886async fn test_copy_trim(cx: &mut TestAppContext) {
6887 init_test(cx, |_| {});
6888
6889 let mut cx = EditorTestContext::new(cx).await;
6890 cx.set_state(
6891 r#" «for selection in selections.iter() {
6892 let mut start = selection.start;
6893 let mut end = selection.end;
6894 let is_entire_line = selection.is_empty();
6895 if is_entire_line {
6896 start = Point::new(start.row, 0);ˇ»
6897 end = cmp::min(max_point, Point::new(end.row + 1, 0));
6898 }
6899 "#,
6900 );
6901 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6902 assert_eq!(
6903 cx.read_from_clipboard()
6904 .and_then(|item| item.text().as_deref().map(str::to_string)),
6905 Some(
6906 "for selection in selections.iter() {
6907 let mut start = selection.start;
6908 let mut end = selection.end;
6909 let is_entire_line = selection.is_empty();
6910 if is_entire_line {
6911 start = Point::new(start.row, 0);"
6912 .to_string()
6913 ),
6914 "Regular copying preserves all indentation selected",
6915 );
6916 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6917 assert_eq!(
6918 cx.read_from_clipboard()
6919 .and_then(|item| item.text().as_deref().map(str::to_string)),
6920 Some(
6921 "for selection in selections.iter() {
6922let mut start = selection.start;
6923let mut end = selection.end;
6924let is_entire_line = selection.is_empty();
6925if is_entire_line {
6926 start = Point::new(start.row, 0);"
6927 .to_string()
6928 ),
6929 "Copying with stripping should strip all leading whitespaces"
6930 );
6931
6932 cx.set_state(
6933 r#" « for selection in selections.iter() {
6934 let mut start = selection.start;
6935 let mut end = selection.end;
6936 let is_entire_line = selection.is_empty();
6937 if is_entire_line {
6938 start = Point::new(start.row, 0);ˇ»
6939 end = cmp::min(max_point, Point::new(end.row + 1, 0));
6940 }
6941 "#,
6942 );
6943 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6944 assert_eq!(
6945 cx.read_from_clipboard()
6946 .and_then(|item| item.text().as_deref().map(str::to_string)),
6947 Some(
6948 " for selection in selections.iter() {
6949 let mut start = selection.start;
6950 let mut end = selection.end;
6951 let is_entire_line = selection.is_empty();
6952 if is_entire_line {
6953 start = Point::new(start.row, 0);"
6954 .to_string()
6955 ),
6956 "Regular copying preserves all indentation selected",
6957 );
6958 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6959 assert_eq!(
6960 cx.read_from_clipboard()
6961 .and_then(|item| item.text().as_deref().map(str::to_string)),
6962 Some(
6963 "for selection in selections.iter() {
6964let mut start = selection.start;
6965let mut end = selection.end;
6966let is_entire_line = selection.is_empty();
6967if is_entire_line {
6968 start = Point::new(start.row, 0);"
6969 .to_string()
6970 ),
6971 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
6972 );
6973
6974 cx.set_state(
6975 r#" «ˇ for selection in selections.iter() {
6976 let mut start = selection.start;
6977 let mut end = selection.end;
6978 let is_entire_line = selection.is_empty();
6979 if is_entire_line {
6980 start = Point::new(start.row, 0);»
6981 end = cmp::min(max_point, Point::new(end.row + 1, 0));
6982 }
6983 "#,
6984 );
6985 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6986 assert_eq!(
6987 cx.read_from_clipboard()
6988 .and_then(|item| item.text().as_deref().map(str::to_string)),
6989 Some(
6990 " for selection in selections.iter() {
6991 let mut start = selection.start;
6992 let mut end = selection.end;
6993 let is_entire_line = selection.is_empty();
6994 if is_entire_line {
6995 start = Point::new(start.row, 0);"
6996 .to_string()
6997 ),
6998 "Regular copying for reverse selection works the same",
6999 );
7000 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7001 assert_eq!(
7002 cx.read_from_clipboard()
7003 .and_then(|item| item.text().as_deref().map(str::to_string)),
7004 Some(
7005 "for selection in selections.iter() {
7006let mut start = selection.start;
7007let mut end = selection.end;
7008let is_entire_line = selection.is_empty();
7009if is_entire_line {
7010 start = Point::new(start.row, 0);"
7011 .to_string()
7012 ),
7013 "Copying with stripping for reverse selection works the same"
7014 );
7015
7016 cx.set_state(
7017 r#" for selection «in selections.iter() {
7018 let mut start = selection.start;
7019 let mut end = selection.end;
7020 let is_entire_line = selection.is_empty();
7021 if is_entire_line {
7022 start = Point::new(start.row, 0);ˇ»
7023 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7024 }
7025 "#,
7026 );
7027 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7028 assert_eq!(
7029 cx.read_from_clipboard()
7030 .and_then(|item| item.text().as_deref().map(str::to_string)),
7031 Some(
7032 "in selections.iter() {
7033 let mut start = selection.start;
7034 let mut end = selection.end;
7035 let is_entire_line = selection.is_empty();
7036 if is_entire_line {
7037 start = Point::new(start.row, 0);"
7038 .to_string()
7039 ),
7040 "When selecting past the indent, the copying works as usual",
7041 );
7042 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7043 assert_eq!(
7044 cx.read_from_clipboard()
7045 .and_then(|item| item.text().as_deref().map(str::to_string)),
7046 Some(
7047 "in selections.iter() {
7048 let mut start = selection.start;
7049 let mut end = selection.end;
7050 let is_entire_line = selection.is_empty();
7051 if is_entire_line {
7052 start = Point::new(start.row, 0);"
7053 .to_string()
7054 ),
7055 "When selecting past the indent, nothing is trimmed"
7056 );
7057
7058 cx.set_state(
7059 r#" «for selection in selections.iter() {
7060 let mut start = selection.start;
7061
7062 let mut end = selection.end;
7063 let is_entire_line = selection.is_empty();
7064 if is_entire_line {
7065 start = Point::new(start.row, 0);
7066ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
7067 }
7068 "#,
7069 );
7070 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7071 assert_eq!(
7072 cx.read_from_clipboard()
7073 .and_then(|item| item.text().as_deref().map(str::to_string)),
7074 Some(
7075 "for selection in selections.iter() {
7076let mut start = selection.start;
7077
7078let mut end = selection.end;
7079let is_entire_line = selection.is_empty();
7080if is_entire_line {
7081 start = Point::new(start.row, 0);
7082"
7083 .to_string()
7084 ),
7085 "Copying with stripping should ignore empty lines"
7086 );
7087}
7088
7089#[gpui::test]
7090async fn test_paste_multiline(cx: &mut TestAppContext) {
7091 init_test(cx, |_| {});
7092
7093 let mut cx = EditorTestContext::new(cx).await;
7094 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7095
7096 // Cut an indented block, without the leading whitespace.
7097 cx.set_state(indoc! {"
7098 const a: B = (
7099 c(),
7100 «d(
7101 e,
7102 f
7103 )ˇ»
7104 );
7105 "});
7106 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7107 cx.assert_editor_state(indoc! {"
7108 const a: B = (
7109 c(),
7110 ˇ
7111 );
7112 "});
7113
7114 // Paste it at the same position.
7115 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7116 cx.assert_editor_state(indoc! {"
7117 const a: B = (
7118 c(),
7119 d(
7120 e,
7121 f
7122 )ˇ
7123 );
7124 "});
7125
7126 // Paste it at a line with a lower indent level.
7127 cx.set_state(indoc! {"
7128 ˇ
7129 const a: B = (
7130 c(),
7131 );
7132 "});
7133 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7134 cx.assert_editor_state(indoc! {"
7135 d(
7136 e,
7137 f
7138 )ˇ
7139 const a: B = (
7140 c(),
7141 );
7142 "});
7143
7144 // Cut an indented block, with the leading whitespace.
7145 cx.set_state(indoc! {"
7146 const a: B = (
7147 c(),
7148 « d(
7149 e,
7150 f
7151 )
7152 ˇ»);
7153 "});
7154 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7155 cx.assert_editor_state(indoc! {"
7156 const a: B = (
7157 c(),
7158 ˇ);
7159 "});
7160
7161 // Paste it at the same position.
7162 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7163 cx.assert_editor_state(indoc! {"
7164 const a: B = (
7165 c(),
7166 d(
7167 e,
7168 f
7169 )
7170 ˇ);
7171 "});
7172
7173 // Paste it at a line with a higher indent level.
7174 cx.set_state(indoc! {"
7175 const a: B = (
7176 c(),
7177 d(
7178 e,
7179 fˇ
7180 )
7181 );
7182 "});
7183 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7184 cx.assert_editor_state(indoc! {"
7185 const a: B = (
7186 c(),
7187 d(
7188 e,
7189 f d(
7190 e,
7191 f
7192 )
7193 ˇ
7194 )
7195 );
7196 "});
7197
7198 // Copy an indented block, starting mid-line
7199 cx.set_state(indoc! {"
7200 const a: B = (
7201 c(),
7202 somethin«g(
7203 e,
7204 f
7205 )ˇ»
7206 );
7207 "});
7208 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7209
7210 // Paste it on a line with a lower indent level
7211 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
7212 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7213 cx.assert_editor_state(indoc! {"
7214 const a: B = (
7215 c(),
7216 something(
7217 e,
7218 f
7219 )
7220 );
7221 g(
7222 e,
7223 f
7224 )ˇ"});
7225}
7226
7227#[gpui::test]
7228async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
7229 init_test(cx, |_| {});
7230
7231 cx.write_to_clipboard(ClipboardItem::new_string(
7232 " d(\n e\n );\n".into(),
7233 ));
7234
7235 let mut cx = EditorTestContext::new(cx).await;
7236 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7237
7238 cx.set_state(indoc! {"
7239 fn a() {
7240 b();
7241 if c() {
7242 ˇ
7243 }
7244 }
7245 "});
7246
7247 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7248 cx.assert_editor_state(indoc! {"
7249 fn a() {
7250 b();
7251 if c() {
7252 d(
7253 e
7254 );
7255 ˇ
7256 }
7257 }
7258 "});
7259
7260 cx.set_state(indoc! {"
7261 fn a() {
7262 b();
7263 ˇ
7264 }
7265 "});
7266
7267 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7268 cx.assert_editor_state(indoc! {"
7269 fn a() {
7270 b();
7271 d(
7272 e
7273 );
7274 ˇ
7275 }
7276 "});
7277}
7278
7279#[gpui::test]
7280fn test_select_all(cx: &mut TestAppContext) {
7281 init_test(cx, |_| {});
7282
7283 let editor = cx.add_window(|window, cx| {
7284 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
7285 build_editor(buffer, window, cx)
7286 });
7287 _ = editor.update(cx, |editor, window, cx| {
7288 editor.select_all(&SelectAll, window, cx);
7289 assert_eq!(
7290 editor.selections.display_ranges(cx),
7291 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
7292 );
7293 });
7294}
7295
7296#[gpui::test]
7297fn test_select_line(cx: &mut TestAppContext) {
7298 init_test(cx, |_| {});
7299
7300 let editor = cx.add_window(|window, cx| {
7301 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
7302 build_editor(buffer, window, cx)
7303 });
7304 _ = editor.update(cx, |editor, window, cx| {
7305 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7306 s.select_display_ranges([
7307 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7308 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7309 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7310 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
7311 ])
7312 });
7313 editor.select_line(&SelectLine, window, cx);
7314 assert_eq!(
7315 editor.selections.display_ranges(cx),
7316 vec![
7317 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
7318 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
7319 ]
7320 );
7321 });
7322
7323 _ = editor.update(cx, |editor, window, cx| {
7324 editor.select_line(&SelectLine, window, cx);
7325 assert_eq!(
7326 editor.selections.display_ranges(cx),
7327 vec![
7328 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
7329 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
7330 ]
7331 );
7332 });
7333
7334 _ = editor.update(cx, |editor, window, cx| {
7335 editor.select_line(&SelectLine, window, cx);
7336 assert_eq!(
7337 editor.selections.display_ranges(cx),
7338 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
7339 );
7340 });
7341}
7342
7343#[gpui::test]
7344async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
7345 init_test(cx, |_| {});
7346 let mut cx = EditorTestContext::new(cx).await;
7347
7348 #[track_caller]
7349 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
7350 cx.set_state(initial_state);
7351 cx.update_editor(|e, window, cx| {
7352 e.split_selection_into_lines(&Default::default(), window, cx)
7353 });
7354 cx.assert_editor_state(expected_state);
7355 }
7356
7357 // Selection starts and ends at the middle of lines, left-to-right
7358 test(
7359 &mut cx,
7360 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
7361 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7362 );
7363 // Same thing, right-to-left
7364 test(
7365 &mut cx,
7366 "aa\nb«b\ncc\ndd\neˇ»e\nff",
7367 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7368 );
7369
7370 // Whole buffer, left-to-right, last line *doesn't* end with newline
7371 test(
7372 &mut cx,
7373 "«ˇaa\nbb\ncc\ndd\nee\nff»",
7374 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7375 );
7376 // Same thing, right-to-left
7377 test(
7378 &mut cx,
7379 "«aa\nbb\ncc\ndd\nee\nffˇ»",
7380 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7381 );
7382
7383 // Whole buffer, left-to-right, last line ends with newline
7384 test(
7385 &mut cx,
7386 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
7387 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7388 );
7389 // Same thing, right-to-left
7390 test(
7391 &mut cx,
7392 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
7393 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7394 );
7395
7396 // Starts at the end of a line, ends at the start of another
7397 test(
7398 &mut cx,
7399 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
7400 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
7401 );
7402}
7403
7404#[gpui::test]
7405async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
7406 init_test(cx, |_| {});
7407
7408 let editor = cx.add_window(|window, cx| {
7409 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
7410 build_editor(buffer, window, cx)
7411 });
7412
7413 // setup
7414 _ = editor.update(cx, |editor, window, cx| {
7415 editor.fold_creases(
7416 vec![
7417 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
7418 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
7419 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
7420 ],
7421 true,
7422 window,
7423 cx,
7424 );
7425 assert_eq!(
7426 editor.display_text(cx),
7427 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7428 );
7429 });
7430
7431 _ = editor.update(cx, |editor, window, cx| {
7432 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7433 s.select_display_ranges([
7434 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7435 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7436 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7437 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
7438 ])
7439 });
7440 editor.split_selection_into_lines(&Default::default(), window, cx);
7441 assert_eq!(
7442 editor.display_text(cx),
7443 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7444 );
7445 });
7446 EditorTestContext::for_editor(editor, cx)
7447 .await
7448 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
7449
7450 _ = editor.update(cx, |editor, window, cx| {
7451 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7452 s.select_display_ranges([
7453 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
7454 ])
7455 });
7456 editor.split_selection_into_lines(&Default::default(), window, cx);
7457 assert_eq!(
7458 editor.display_text(cx),
7459 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
7460 );
7461 assert_eq!(
7462 editor.selections.display_ranges(cx),
7463 [
7464 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
7465 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
7466 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
7467 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
7468 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
7469 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
7470 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
7471 ]
7472 );
7473 });
7474 EditorTestContext::for_editor(editor, cx)
7475 .await
7476 .assert_editor_state(
7477 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
7478 );
7479}
7480
7481#[gpui::test]
7482async fn test_add_selection_above_below(cx: &mut TestAppContext) {
7483 init_test(cx, |_| {});
7484
7485 let mut cx = EditorTestContext::new(cx).await;
7486
7487 cx.set_state(indoc!(
7488 r#"abc
7489 defˇghi
7490
7491 jk
7492 nlmo
7493 "#
7494 ));
7495
7496 cx.update_editor(|editor, window, cx| {
7497 editor.add_selection_above(&Default::default(), window, cx);
7498 });
7499
7500 cx.assert_editor_state(indoc!(
7501 r#"abcˇ
7502 defˇghi
7503
7504 jk
7505 nlmo
7506 "#
7507 ));
7508
7509 cx.update_editor(|editor, window, cx| {
7510 editor.add_selection_above(&Default::default(), window, cx);
7511 });
7512
7513 cx.assert_editor_state(indoc!(
7514 r#"abcˇ
7515 defˇghi
7516
7517 jk
7518 nlmo
7519 "#
7520 ));
7521
7522 cx.update_editor(|editor, window, cx| {
7523 editor.add_selection_below(&Default::default(), window, cx);
7524 });
7525
7526 cx.assert_editor_state(indoc!(
7527 r#"abc
7528 defˇghi
7529
7530 jk
7531 nlmo
7532 "#
7533 ));
7534
7535 cx.update_editor(|editor, window, cx| {
7536 editor.undo_selection(&Default::default(), window, cx);
7537 });
7538
7539 cx.assert_editor_state(indoc!(
7540 r#"abcˇ
7541 defˇghi
7542
7543 jk
7544 nlmo
7545 "#
7546 ));
7547
7548 cx.update_editor(|editor, window, cx| {
7549 editor.redo_selection(&Default::default(), window, cx);
7550 });
7551
7552 cx.assert_editor_state(indoc!(
7553 r#"abc
7554 defˇghi
7555
7556 jk
7557 nlmo
7558 "#
7559 ));
7560
7561 cx.update_editor(|editor, window, cx| {
7562 editor.add_selection_below(&Default::default(), window, cx);
7563 });
7564
7565 cx.assert_editor_state(indoc!(
7566 r#"abc
7567 defˇghi
7568 ˇ
7569 jk
7570 nlmo
7571 "#
7572 ));
7573
7574 cx.update_editor(|editor, window, cx| {
7575 editor.add_selection_below(&Default::default(), window, cx);
7576 });
7577
7578 cx.assert_editor_state(indoc!(
7579 r#"abc
7580 defˇghi
7581 ˇ
7582 jkˇ
7583 nlmo
7584 "#
7585 ));
7586
7587 cx.update_editor(|editor, window, cx| {
7588 editor.add_selection_below(&Default::default(), window, cx);
7589 });
7590
7591 cx.assert_editor_state(indoc!(
7592 r#"abc
7593 defˇghi
7594 ˇ
7595 jkˇ
7596 nlmˇo
7597 "#
7598 ));
7599
7600 cx.update_editor(|editor, window, cx| {
7601 editor.add_selection_below(&Default::default(), window, cx);
7602 });
7603
7604 cx.assert_editor_state(indoc!(
7605 r#"abc
7606 defˇghi
7607 ˇ
7608 jkˇ
7609 nlmˇo
7610 ˇ"#
7611 ));
7612
7613 // change selections
7614 cx.set_state(indoc!(
7615 r#"abc
7616 def«ˇg»hi
7617
7618 jk
7619 nlmo
7620 "#
7621 ));
7622
7623 cx.update_editor(|editor, window, cx| {
7624 editor.add_selection_below(&Default::default(), window, cx);
7625 });
7626
7627 cx.assert_editor_state(indoc!(
7628 r#"abc
7629 def«ˇg»hi
7630
7631 jk
7632 nlm«ˇo»
7633 "#
7634 ));
7635
7636 cx.update_editor(|editor, window, cx| {
7637 editor.add_selection_below(&Default::default(), window, cx);
7638 });
7639
7640 cx.assert_editor_state(indoc!(
7641 r#"abc
7642 def«ˇg»hi
7643
7644 jk
7645 nlm«ˇo»
7646 "#
7647 ));
7648
7649 cx.update_editor(|editor, window, cx| {
7650 editor.add_selection_above(&Default::default(), window, cx);
7651 });
7652
7653 cx.assert_editor_state(indoc!(
7654 r#"abc
7655 def«ˇg»hi
7656
7657 jk
7658 nlmo
7659 "#
7660 ));
7661
7662 cx.update_editor(|editor, window, cx| {
7663 editor.add_selection_above(&Default::default(), window, cx);
7664 });
7665
7666 cx.assert_editor_state(indoc!(
7667 r#"abc
7668 def«ˇg»hi
7669
7670 jk
7671 nlmo
7672 "#
7673 ));
7674
7675 // Change selections again
7676 cx.set_state(indoc!(
7677 r#"a«bc
7678 defgˇ»hi
7679
7680 jk
7681 nlmo
7682 "#
7683 ));
7684
7685 cx.update_editor(|editor, window, cx| {
7686 editor.add_selection_below(&Default::default(), window, cx);
7687 });
7688
7689 cx.assert_editor_state(indoc!(
7690 r#"a«bcˇ»
7691 d«efgˇ»hi
7692
7693 j«kˇ»
7694 nlmo
7695 "#
7696 ));
7697
7698 cx.update_editor(|editor, window, cx| {
7699 editor.add_selection_below(&Default::default(), window, cx);
7700 });
7701 cx.assert_editor_state(indoc!(
7702 r#"a«bcˇ»
7703 d«efgˇ»hi
7704
7705 j«kˇ»
7706 n«lmoˇ»
7707 "#
7708 ));
7709 cx.update_editor(|editor, window, cx| {
7710 editor.add_selection_above(&Default::default(), window, cx);
7711 });
7712
7713 cx.assert_editor_state(indoc!(
7714 r#"a«bcˇ»
7715 d«efgˇ»hi
7716
7717 j«kˇ»
7718 nlmo
7719 "#
7720 ));
7721
7722 // Change selections again
7723 cx.set_state(indoc!(
7724 r#"abc
7725 d«ˇefghi
7726
7727 jk
7728 nlm»o
7729 "#
7730 ));
7731
7732 cx.update_editor(|editor, window, cx| {
7733 editor.add_selection_above(&Default::default(), window, cx);
7734 });
7735
7736 cx.assert_editor_state(indoc!(
7737 r#"a«ˇbc»
7738 d«ˇef»ghi
7739
7740 j«ˇk»
7741 n«ˇlm»o
7742 "#
7743 ));
7744
7745 cx.update_editor(|editor, window, cx| {
7746 editor.add_selection_below(&Default::default(), window, cx);
7747 });
7748
7749 cx.assert_editor_state(indoc!(
7750 r#"abc
7751 d«ˇef»ghi
7752
7753 j«ˇk»
7754 n«ˇlm»o
7755 "#
7756 ));
7757}
7758
7759#[gpui::test]
7760async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
7761 init_test(cx, |_| {});
7762 let mut cx = EditorTestContext::new(cx).await;
7763
7764 cx.set_state(indoc!(
7765 r#"line onˇe
7766 liˇne two
7767 line three
7768 line four"#
7769 ));
7770
7771 cx.update_editor(|editor, window, cx| {
7772 editor.add_selection_below(&Default::default(), window, cx);
7773 });
7774
7775 // test multiple cursors expand in the same direction
7776 cx.assert_editor_state(indoc!(
7777 r#"line onˇe
7778 liˇne twˇo
7779 liˇne three
7780 line four"#
7781 ));
7782
7783 cx.update_editor(|editor, window, cx| {
7784 editor.add_selection_below(&Default::default(), window, cx);
7785 });
7786
7787 cx.update_editor(|editor, window, cx| {
7788 editor.add_selection_below(&Default::default(), window, cx);
7789 });
7790
7791 // test multiple cursors expand below overflow
7792 cx.assert_editor_state(indoc!(
7793 r#"line onˇe
7794 liˇne twˇo
7795 liˇne thˇree
7796 liˇne foˇur"#
7797 ));
7798
7799 cx.update_editor(|editor, window, cx| {
7800 editor.add_selection_above(&Default::default(), window, cx);
7801 });
7802
7803 // test multiple cursors retrieves back correctly
7804 cx.assert_editor_state(indoc!(
7805 r#"line onˇe
7806 liˇne twˇo
7807 liˇne thˇree
7808 line four"#
7809 ));
7810
7811 cx.update_editor(|editor, window, cx| {
7812 editor.add_selection_above(&Default::default(), window, cx);
7813 });
7814
7815 cx.update_editor(|editor, window, cx| {
7816 editor.add_selection_above(&Default::default(), window, cx);
7817 });
7818
7819 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
7820 cx.assert_editor_state(indoc!(
7821 r#"liˇne onˇe
7822 liˇne two
7823 line three
7824 line four"#
7825 ));
7826
7827 cx.update_editor(|editor, window, cx| {
7828 editor.undo_selection(&Default::default(), window, cx);
7829 });
7830
7831 // test undo
7832 cx.assert_editor_state(indoc!(
7833 r#"line onˇe
7834 liˇne twˇo
7835 line three
7836 line four"#
7837 ));
7838
7839 cx.update_editor(|editor, window, cx| {
7840 editor.redo_selection(&Default::default(), window, cx);
7841 });
7842
7843 // test redo
7844 cx.assert_editor_state(indoc!(
7845 r#"liˇne onˇe
7846 liˇne two
7847 line three
7848 line four"#
7849 ));
7850
7851 cx.set_state(indoc!(
7852 r#"abcd
7853 ef«ghˇ»
7854 ijkl
7855 «mˇ»nop"#
7856 ));
7857
7858 cx.update_editor(|editor, window, cx| {
7859 editor.add_selection_above(&Default::default(), window, cx);
7860 });
7861
7862 // test multiple selections expand in the same direction
7863 cx.assert_editor_state(indoc!(
7864 r#"ab«cdˇ»
7865 ef«ghˇ»
7866 «iˇ»jkl
7867 «mˇ»nop"#
7868 ));
7869
7870 cx.update_editor(|editor, window, cx| {
7871 editor.add_selection_above(&Default::default(), window, cx);
7872 });
7873
7874 // test multiple selection upward overflow
7875 cx.assert_editor_state(indoc!(
7876 r#"ab«cdˇ»
7877 «eˇ»f«ghˇ»
7878 «iˇ»jkl
7879 «mˇ»nop"#
7880 ));
7881
7882 cx.update_editor(|editor, window, cx| {
7883 editor.add_selection_below(&Default::default(), window, cx);
7884 });
7885
7886 // test multiple selection retrieves back correctly
7887 cx.assert_editor_state(indoc!(
7888 r#"abcd
7889 ef«ghˇ»
7890 «iˇ»jkl
7891 «mˇ»nop"#
7892 ));
7893
7894 cx.update_editor(|editor, window, cx| {
7895 editor.add_selection_below(&Default::default(), window, cx);
7896 });
7897
7898 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
7899 cx.assert_editor_state(indoc!(
7900 r#"abcd
7901 ef«ghˇ»
7902 ij«klˇ»
7903 «mˇ»nop"#
7904 ));
7905
7906 cx.update_editor(|editor, window, cx| {
7907 editor.undo_selection(&Default::default(), window, cx);
7908 });
7909
7910 // test undo
7911 cx.assert_editor_state(indoc!(
7912 r#"abcd
7913 ef«ghˇ»
7914 «iˇ»jkl
7915 «mˇ»nop"#
7916 ));
7917
7918 cx.update_editor(|editor, window, cx| {
7919 editor.redo_selection(&Default::default(), window, cx);
7920 });
7921
7922 // test redo
7923 cx.assert_editor_state(indoc!(
7924 r#"abcd
7925 ef«ghˇ»
7926 ij«klˇ»
7927 «mˇ»nop"#
7928 ));
7929}
7930
7931#[gpui::test]
7932async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
7933 init_test(cx, |_| {});
7934 let mut cx = EditorTestContext::new(cx).await;
7935
7936 cx.set_state(indoc!(
7937 r#"line onˇe
7938 liˇne two
7939 line three
7940 line four"#
7941 ));
7942
7943 cx.update_editor(|editor, window, cx| {
7944 editor.add_selection_below(&Default::default(), window, cx);
7945 editor.add_selection_below(&Default::default(), window, cx);
7946 editor.add_selection_below(&Default::default(), window, cx);
7947 });
7948
7949 // initial state with two multi cursor groups
7950 cx.assert_editor_state(indoc!(
7951 r#"line onˇe
7952 liˇne twˇo
7953 liˇne thˇree
7954 liˇne foˇur"#
7955 ));
7956
7957 // add single cursor in middle - simulate opt click
7958 cx.update_editor(|editor, window, cx| {
7959 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
7960 editor.begin_selection(new_cursor_point, true, 1, window, cx);
7961 editor.end_selection(window, cx);
7962 });
7963
7964 cx.assert_editor_state(indoc!(
7965 r#"line onˇe
7966 liˇne twˇo
7967 liˇneˇ thˇree
7968 liˇne foˇur"#
7969 ));
7970
7971 cx.update_editor(|editor, window, cx| {
7972 editor.add_selection_above(&Default::default(), window, cx);
7973 });
7974
7975 // test new added selection expands above and existing selection shrinks
7976 cx.assert_editor_state(indoc!(
7977 r#"line onˇe
7978 liˇneˇ twˇo
7979 liˇneˇ thˇree
7980 line four"#
7981 ));
7982
7983 cx.update_editor(|editor, window, cx| {
7984 editor.add_selection_above(&Default::default(), window, cx);
7985 });
7986
7987 // test new added selection expands above and existing selection shrinks
7988 cx.assert_editor_state(indoc!(
7989 r#"lineˇ onˇe
7990 liˇneˇ twˇo
7991 lineˇ three
7992 line four"#
7993 ));
7994
7995 // intial state with two selection groups
7996 cx.set_state(indoc!(
7997 r#"abcd
7998 ef«ghˇ»
7999 ijkl
8000 «mˇ»nop"#
8001 ));
8002
8003 cx.update_editor(|editor, window, cx| {
8004 editor.add_selection_above(&Default::default(), window, cx);
8005 editor.add_selection_above(&Default::default(), window, cx);
8006 });
8007
8008 cx.assert_editor_state(indoc!(
8009 r#"ab«cdˇ»
8010 «eˇ»f«ghˇ»
8011 «iˇ»jkl
8012 «mˇ»nop"#
8013 ));
8014
8015 // add single selection in middle - simulate opt drag
8016 cx.update_editor(|editor, window, cx| {
8017 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
8018 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8019 editor.update_selection(
8020 DisplayPoint::new(DisplayRow(2), 4),
8021 0,
8022 gpui::Point::<f32>::default(),
8023 window,
8024 cx,
8025 );
8026 editor.end_selection(window, cx);
8027 });
8028
8029 cx.assert_editor_state(indoc!(
8030 r#"ab«cdˇ»
8031 «eˇ»f«ghˇ»
8032 «iˇ»jk«lˇ»
8033 «mˇ»nop"#
8034 ));
8035
8036 cx.update_editor(|editor, window, cx| {
8037 editor.add_selection_below(&Default::default(), window, cx);
8038 });
8039
8040 // test new added selection expands below, others shrinks from above
8041 cx.assert_editor_state(indoc!(
8042 r#"abcd
8043 ef«ghˇ»
8044 «iˇ»jk«lˇ»
8045 «mˇ»no«pˇ»"#
8046 ));
8047}
8048
8049#[gpui::test]
8050async fn test_select_next(cx: &mut TestAppContext) {
8051 init_test(cx, |_| {});
8052
8053 let mut cx = EditorTestContext::new(cx).await;
8054 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8055
8056 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8057 .unwrap();
8058 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8059
8060 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8061 .unwrap();
8062 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8063
8064 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8065 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8066
8067 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8068 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8069
8070 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8071 .unwrap();
8072 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8073
8074 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8075 .unwrap();
8076 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8077
8078 // Test selection direction should be preserved
8079 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8080
8081 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8082 .unwrap();
8083 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
8084}
8085
8086#[gpui::test]
8087async fn test_select_all_matches(cx: &mut TestAppContext) {
8088 init_test(cx, |_| {});
8089
8090 let mut cx = EditorTestContext::new(cx).await;
8091
8092 // Test caret-only selections
8093 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8094 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8095 .unwrap();
8096 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8097
8098 // Test left-to-right selections
8099 cx.set_state("abc\n«abcˇ»\nabc");
8100 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8101 .unwrap();
8102 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
8103
8104 // Test right-to-left selections
8105 cx.set_state("abc\n«ˇabc»\nabc");
8106 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8107 .unwrap();
8108 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
8109
8110 // Test selecting whitespace with caret selection
8111 cx.set_state("abc\nˇ abc\nabc");
8112 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8113 .unwrap();
8114 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8115
8116 // Test selecting whitespace with left-to-right selection
8117 cx.set_state("abc\n«ˇ »abc\nabc");
8118 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8119 .unwrap();
8120 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
8121
8122 // Test no matches with right-to-left selection
8123 cx.set_state("abc\n« ˇ»abc\nabc");
8124 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8125 .unwrap();
8126 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8127
8128 // Test with a single word and clip_at_line_ends=true (#29823)
8129 cx.set_state("aˇbc");
8130 cx.update_editor(|e, window, cx| {
8131 e.set_clip_at_line_ends(true, cx);
8132 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8133 e.set_clip_at_line_ends(false, cx);
8134 });
8135 cx.assert_editor_state("«abcˇ»");
8136}
8137
8138#[gpui::test]
8139async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
8140 init_test(cx, |_| {});
8141
8142 let mut cx = EditorTestContext::new(cx).await;
8143
8144 let large_body_1 = "\nd".repeat(200);
8145 let large_body_2 = "\ne".repeat(200);
8146
8147 cx.set_state(&format!(
8148 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
8149 ));
8150 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
8151 let scroll_position = editor.scroll_position(cx);
8152 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
8153 scroll_position
8154 });
8155
8156 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8157 .unwrap();
8158 cx.assert_editor_state(&format!(
8159 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
8160 ));
8161 let scroll_position_after_selection =
8162 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
8163 assert_eq!(
8164 initial_scroll_position, scroll_position_after_selection,
8165 "Scroll position should not change after selecting all matches"
8166 );
8167}
8168
8169#[gpui::test]
8170async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
8171 init_test(cx, |_| {});
8172
8173 let mut cx = EditorLspTestContext::new_rust(
8174 lsp::ServerCapabilities {
8175 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8176 ..Default::default()
8177 },
8178 cx,
8179 )
8180 .await;
8181
8182 cx.set_state(indoc! {"
8183 line 1
8184 line 2
8185 linˇe 3
8186 line 4
8187 line 5
8188 "});
8189
8190 // Make an edit
8191 cx.update_editor(|editor, window, cx| {
8192 editor.handle_input("X", window, cx);
8193 });
8194
8195 // Move cursor to a different position
8196 cx.update_editor(|editor, window, cx| {
8197 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8198 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
8199 });
8200 });
8201
8202 cx.assert_editor_state(indoc! {"
8203 line 1
8204 line 2
8205 linXe 3
8206 line 4
8207 liˇne 5
8208 "});
8209
8210 cx.lsp
8211 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
8212 Ok(Some(vec![lsp::TextEdit::new(
8213 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8214 "PREFIX ".to_string(),
8215 )]))
8216 });
8217
8218 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
8219 .unwrap()
8220 .await
8221 .unwrap();
8222
8223 cx.assert_editor_state(indoc! {"
8224 PREFIX line 1
8225 line 2
8226 linXe 3
8227 line 4
8228 liˇne 5
8229 "});
8230
8231 // Undo formatting
8232 cx.update_editor(|editor, window, cx| {
8233 editor.undo(&Default::default(), window, cx);
8234 });
8235
8236 // Verify cursor moved back to position after edit
8237 cx.assert_editor_state(indoc! {"
8238 line 1
8239 line 2
8240 linXˇe 3
8241 line 4
8242 line 5
8243 "});
8244}
8245
8246#[gpui::test]
8247async fn test_undo_edit_prediction_scrolls_to_edit_pos(cx: &mut TestAppContext) {
8248 init_test(cx, |_| {});
8249
8250 let mut cx = EditorTestContext::new(cx).await;
8251
8252 let provider = cx.new(|_| FakeEditPredictionProvider::default());
8253 cx.update_editor(|editor, window, cx| {
8254 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
8255 });
8256
8257 cx.set_state(indoc! {"
8258 line 1
8259 line 2
8260 linˇe 3
8261 line 4
8262 line 5
8263 line 6
8264 line 7
8265 line 8
8266 line 9
8267 line 10
8268 "});
8269
8270 let snapshot = cx.buffer_snapshot();
8271 let edit_position = snapshot.anchor_after(Point::new(2, 4));
8272
8273 cx.update(|_, cx| {
8274 provider.update(cx, |provider, _| {
8275 provider.set_edit_prediction(Some(edit_prediction::EditPrediction::Local {
8276 id: None,
8277 edits: vec![(edit_position..edit_position, "X".into())],
8278 edit_preview: None,
8279 }))
8280 })
8281 });
8282
8283 cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
8284 cx.update_editor(|editor, window, cx| {
8285 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
8286 });
8287
8288 cx.assert_editor_state(indoc! {"
8289 line 1
8290 line 2
8291 lineXˇ 3
8292 line 4
8293 line 5
8294 line 6
8295 line 7
8296 line 8
8297 line 9
8298 line 10
8299 "});
8300
8301 cx.update_editor(|editor, window, cx| {
8302 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8303 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
8304 });
8305 });
8306
8307 cx.assert_editor_state(indoc! {"
8308 line 1
8309 line 2
8310 lineX 3
8311 line 4
8312 line 5
8313 line 6
8314 line 7
8315 line 8
8316 line 9
8317 liˇne 10
8318 "});
8319
8320 cx.update_editor(|editor, window, cx| {
8321 editor.undo(&Default::default(), window, cx);
8322 });
8323
8324 cx.assert_editor_state(indoc! {"
8325 line 1
8326 line 2
8327 lineˇ 3
8328 line 4
8329 line 5
8330 line 6
8331 line 7
8332 line 8
8333 line 9
8334 line 10
8335 "});
8336}
8337
8338#[gpui::test]
8339async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
8340 init_test(cx, |_| {});
8341
8342 let mut cx = EditorTestContext::new(cx).await;
8343 cx.set_state(
8344 r#"let foo = 2;
8345lˇet foo = 2;
8346let fooˇ = 2;
8347let foo = 2;
8348let foo = ˇ2;"#,
8349 );
8350
8351 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8352 .unwrap();
8353 cx.assert_editor_state(
8354 r#"let foo = 2;
8355«letˇ» foo = 2;
8356let «fooˇ» = 2;
8357let foo = 2;
8358let foo = «2ˇ»;"#,
8359 );
8360
8361 // noop for multiple selections with different contents
8362 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8363 .unwrap();
8364 cx.assert_editor_state(
8365 r#"let foo = 2;
8366«letˇ» foo = 2;
8367let «fooˇ» = 2;
8368let foo = 2;
8369let foo = «2ˇ»;"#,
8370 );
8371
8372 // Test last selection direction should be preserved
8373 cx.set_state(
8374 r#"let foo = 2;
8375let foo = 2;
8376let «fooˇ» = 2;
8377let «ˇfoo» = 2;
8378let foo = 2;"#,
8379 );
8380
8381 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8382 .unwrap();
8383 cx.assert_editor_state(
8384 r#"let foo = 2;
8385let foo = 2;
8386let «fooˇ» = 2;
8387let «ˇfoo» = 2;
8388let «ˇfoo» = 2;"#,
8389 );
8390}
8391
8392#[gpui::test]
8393async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
8394 init_test(cx, |_| {});
8395
8396 let mut cx =
8397 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
8398
8399 cx.assert_editor_state(indoc! {"
8400 ˇbbb
8401 ccc
8402
8403 bbb
8404 ccc
8405 "});
8406 cx.dispatch_action(SelectPrevious::default());
8407 cx.assert_editor_state(indoc! {"
8408 «bbbˇ»
8409 ccc
8410
8411 bbb
8412 ccc
8413 "});
8414 cx.dispatch_action(SelectPrevious::default());
8415 cx.assert_editor_state(indoc! {"
8416 «bbbˇ»
8417 ccc
8418
8419 «bbbˇ»
8420 ccc
8421 "});
8422}
8423
8424#[gpui::test]
8425async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
8426 init_test(cx, |_| {});
8427
8428 let mut cx = EditorTestContext::new(cx).await;
8429 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8430
8431 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8432 .unwrap();
8433 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8434
8435 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8436 .unwrap();
8437 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8438
8439 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8440 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8441
8442 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8443 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8444
8445 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8446 .unwrap();
8447 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
8448
8449 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8450 .unwrap();
8451 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8452}
8453
8454#[gpui::test]
8455async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
8456 init_test(cx, |_| {});
8457
8458 let mut cx = EditorTestContext::new(cx).await;
8459 cx.set_state("aˇ");
8460
8461 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8462 .unwrap();
8463 cx.assert_editor_state("«aˇ»");
8464 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8465 .unwrap();
8466 cx.assert_editor_state("«aˇ»");
8467}
8468
8469#[gpui::test]
8470async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
8471 init_test(cx, |_| {});
8472
8473 let mut cx = EditorTestContext::new(cx).await;
8474 cx.set_state(
8475 r#"let foo = 2;
8476lˇet foo = 2;
8477let fooˇ = 2;
8478let foo = 2;
8479let foo = ˇ2;"#,
8480 );
8481
8482 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8483 .unwrap();
8484 cx.assert_editor_state(
8485 r#"let foo = 2;
8486«letˇ» foo = 2;
8487let «fooˇ» = 2;
8488let foo = 2;
8489let foo = «2ˇ»;"#,
8490 );
8491
8492 // noop for multiple selections with different contents
8493 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8494 .unwrap();
8495 cx.assert_editor_state(
8496 r#"let foo = 2;
8497«letˇ» foo = 2;
8498let «fooˇ» = 2;
8499let foo = 2;
8500let foo = «2ˇ»;"#,
8501 );
8502}
8503
8504#[gpui::test]
8505async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
8506 init_test(cx, |_| {});
8507
8508 let mut cx = EditorTestContext::new(cx).await;
8509 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8510
8511 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8512 .unwrap();
8513 // selection direction is preserved
8514 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8515
8516 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8517 .unwrap();
8518 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8519
8520 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8521 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8522
8523 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8524 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8525
8526 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8527 .unwrap();
8528 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
8529
8530 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8531 .unwrap();
8532 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
8533}
8534
8535#[gpui::test]
8536async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
8537 init_test(cx, |_| {});
8538
8539 let language = Arc::new(Language::new(
8540 LanguageConfig::default(),
8541 Some(tree_sitter_rust::LANGUAGE.into()),
8542 ));
8543
8544 let text = r#"
8545 use mod1::mod2::{mod3, mod4};
8546
8547 fn fn_1(param1: bool, param2: &str) {
8548 let var1 = "text";
8549 }
8550 "#
8551 .unindent();
8552
8553 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8554 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8555 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8556
8557 editor
8558 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8559 .await;
8560
8561 editor.update_in(cx, |editor, window, cx| {
8562 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8563 s.select_display_ranges([
8564 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
8565 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
8566 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
8567 ]);
8568 });
8569 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8570 });
8571 editor.update(cx, |editor, cx| {
8572 assert_text_with_selections(
8573 editor,
8574 indoc! {r#"
8575 use mod1::mod2::{mod3, «mod4ˇ»};
8576
8577 fn fn_1«ˇ(param1: bool, param2: &str)» {
8578 let var1 = "«ˇtext»";
8579 }
8580 "#},
8581 cx,
8582 );
8583 });
8584
8585 editor.update_in(cx, |editor, window, cx| {
8586 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8587 });
8588 editor.update(cx, |editor, cx| {
8589 assert_text_with_selections(
8590 editor,
8591 indoc! {r#"
8592 use mod1::mod2::«{mod3, mod4}ˇ»;
8593
8594 «ˇfn fn_1(param1: bool, param2: &str) {
8595 let var1 = "text";
8596 }»
8597 "#},
8598 cx,
8599 );
8600 });
8601
8602 editor.update_in(cx, |editor, window, cx| {
8603 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8604 });
8605 assert_eq!(
8606 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
8607 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
8608 );
8609
8610 // Trying to expand the selected syntax node one more time has no effect.
8611 editor.update_in(cx, |editor, window, cx| {
8612 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8613 });
8614 assert_eq!(
8615 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
8616 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
8617 );
8618
8619 editor.update_in(cx, |editor, window, cx| {
8620 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8621 });
8622 editor.update(cx, |editor, cx| {
8623 assert_text_with_selections(
8624 editor,
8625 indoc! {r#"
8626 use mod1::mod2::«{mod3, mod4}ˇ»;
8627
8628 «ˇfn fn_1(param1: bool, param2: &str) {
8629 let var1 = "text";
8630 }»
8631 "#},
8632 cx,
8633 );
8634 });
8635
8636 editor.update_in(cx, |editor, window, cx| {
8637 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8638 });
8639 editor.update(cx, |editor, cx| {
8640 assert_text_with_selections(
8641 editor,
8642 indoc! {r#"
8643 use mod1::mod2::{mod3, «mod4ˇ»};
8644
8645 fn fn_1«ˇ(param1: bool, param2: &str)» {
8646 let var1 = "«ˇtext»";
8647 }
8648 "#},
8649 cx,
8650 );
8651 });
8652
8653 editor.update_in(cx, |editor, window, cx| {
8654 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8655 });
8656 editor.update(cx, |editor, cx| {
8657 assert_text_with_selections(
8658 editor,
8659 indoc! {r#"
8660 use mod1::mod2::{mod3, moˇd4};
8661
8662 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
8663 let var1 = "teˇxt";
8664 }
8665 "#},
8666 cx,
8667 );
8668 });
8669
8670 // Trying to shrink the selected syntax node one more time has no effect.
8671 editor.update_in(cx, |editor, window, cx| {
8672 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8673 });
8674 editor.update_in(cx, |editor, _, cx| {
8675 assert_text_with_selections(
8676 editor,
8677 indoc! {r#"
8678 use mod1::mod2::{mod3, moˇd4};
8679
8680 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
8681 let var1 = "teˇxt";
8682 }
8683 "#},
8684 cx,
8685 );
8686 });
8687
8688 // Ensure that we keep expanding the selection if the larger selection starts or ends within
8689 // a fold.
8690 editor.update_in(cx, |editor, window, cx| {
8691 editor.fold_creases(
8692 vec![
8693 Crease::simple(
8694 Point::new(0, 21)..Point::new(0, 24),
8695 FoldPlaceholder::test(),
8696 ),
8697 Crease::simple(
8698 Point::new(3, 20)..Point::new(3, 22),
8699 FoldPlaceholder::test(),
8700 ),
8701 ],
8702 true,
8703 window,
8704 cx,
8705 );
8706 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8707 });
8708 editor.update(cx, |editor, cx| {
8709 assert_text_with_selections(
8710 editor,
8711 indoc! {r#"
8712 use mod1::mod2::«{mod3, mod4}ˇ»;
8713
8714 fn fn_1«ˇ(param1: bool, param2: &str)» {
8715 let var1 = "«ˇtext»";
8716 }
8717 "#},
8718 cx,
8719 );
8720 });
8721}
8722
8723#[gpui::test]
8724async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
8725 init_test(cx, |_| {});
8726
8727 let language = Arc::new(Language::new(
8728 LanguageConfig::default(),
8729 Some(tree_sitter_rust::LANGUAGE.into()),
8730 ));
8731
8732 let text = "let a = 2;";
8733
8734 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8735 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8736 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8737
8738 editor
8739 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8740 .await;
8741
8742 // Test case 1: Cursor at end of word
8743 editor.update_in(cx, |editor, window, cx| {
8744 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8745 s.select_display_ranges([
8746 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
8747 ]);
8748 });
8749 });
8750 editor.update(cx, |editor, cx| {
8751 assert_text_with_selections(editor, "let aˇ = 2;", cx);
8752 });
8753 editor.update_in(cx, |editor, window, cx| {
8754 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8755 });
8756 editor.update(cx, |editor, cx| {
8757 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
8758 });
8759 editor.update_in(cx, |editor, window, cx| {
8760 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8761 });
8762 editor.update(cx, |editor, cx| {
8763 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
8764 });
8765
8766 // Test case 2: Cursor at end of statement
8767 editor.update_in(cx, |editor, window, cx| {
8768 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8769 s.select_display_ranges([
8770 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
8771 ]);
8772 });
8773 });
8774 editor.update(cx, |editor, cx| {
8775 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
8776 });
8777 editor.update_in(cx, |editor, window, cx| {
8778 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8779 });
8780 editor.update(cx, |editor, cx| {
8781 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
8782 });
8783}
8784
8785#[gpui::test]
8786async fn test_select_larger_syntax_node_for_cursor_at_symbol(cx: &mut TestAppContext) {
8787 init_test(cx, |_| {});
8788
8789 let language = Arc::new(Language::new(
8790 LanguageConfig {
8791 name: "JavaScript".into(),
8792 ..Default::default()
8793 },
8794 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
8795 ));
8796
8797 let text = r#"
8798 let a = {
8799 key: "value",
8800 };
8801 "#
8802 .unindent();
8803
8804 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8805 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8806 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8807
8808 editor
8809 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8810 .await;
8811
8812 // Test case 1: Cursor after '{'
8813 editor.update_in(cx, |editor, window, cx| {
8814 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8815 s.select_display_ranges([
8816 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 9)
8817 ]);
8818 });
8819 });
8820 editor.update(cx, |editor, cx| {
8821 assert_text_with_selections(
8822 editor,
8823 indoc! {r#"
8824 let a = {ˇ
8825 key: "value",
8826 };
8827 "#},
8828 cx,
8829 );
8830 });
8831 editor.update_in(cx, |editor, window, cx| {
8832 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8833 });
8834 editor.update(cx, |editor, cx| {
8835 assert_text_with_selections(
8836 editor,
8837 indoc! {r#"
8838 let a = «ˇ{
8839 key: "value",
8840 }»;
8841 "#},
8842 cx,
8843 );
8844 });
8845
8846 // Test case 2: Cursor after ':'
8847 editor.update_in(cx, |editor, window, cx| {
8848 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8849 s.select_display_ranges([
8850 DisplayPoint::new(DisplayRow(1), 8)..DisplayPoint::new(DisplayRow(1), 8)
8851 ]);
8852 });
8853 });
8854 editor.update(cx, |editor, cx| {
8855 assert_text_with_selections(
8856 editor,
8857 indoc! {r#"
8858 let a = {
8859 key:ˇ "value",
8860 };
8861 "#},
8862 cx,
8863 );
8864 });
8865 editor.update_in(cx, |editor, window, cx| {
8866 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8867 });
8868 editor.update(cx, |editor, cx| {
8869 assert_text_with_selections(
8870 editor,
8871 indoc! {r#"
8872 let a = {
8873 «ˇkey: "value"»,
8874 };
8875 "#},
8876 cx,
8877 );
8878 });
8879 editor.update_in(cx, |editor, window, cx| {
8880 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8881 });
8882 editor.update(cx, |editor, cx| {
8883 assert_text_with_selections(
8884 editor,
8885 indoc! {r#"
8886 let a = «ˇ{
8887 key: "value",
8888 }»;
8889 "#},
8890 cx,
8891 );
8892 });
8893
8894 // Test case 3: Cursor after ','
8895 editor.update_in(cx, |editor, window, cx| {
8896 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8897 s.select_display_ranges([
8898 DisplayPoint::new(DisplayRow(1), 17)..DisplayPoint::new(DisplayRow(1), 17)
8899 ]);
8900 });
8901 });
8902 editor.update(cx, |editor, cx| {
8903 assert_text_with_selections(
8904 editor,
8905 indoc! {r#"
8906 let a = {
8907 key: "value",ˇ
8908 };
8909 "#},
8910 cx,
8911 );
8912 });
8913 editor.update_in(cx, |editor, window, cx| {
8914 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8915 });
8916 editor.update(cx, |editor, cx| {
8917 assert_text_with_selections(
8918 editor,
8919 indoc! {r#"
8920 let a = «ˇ{
8921 key: "value",
8922 }»;
8923 "#},
8924 cx,
8925 );
8926 });
8927
8928 // Test case 4: Cursor after ';'
8929 editor.update_in(cx, |editor, window, cx| {
8930 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8931 s.select_display_ranges([
8932 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)
8933 ]);
8934 });
8935 });
8936 editor.update(cx, |editor, cx| {
8937 assert_text_with_selections(
8938 editor,
8939 indoc! {r#"
8940 let a = {
8941 key: "value",
8942 };ˇ
8943 "#},
8944 cx,
8945 );
8946 });
8947 editor.update_in(cx, |editor, window, cx| {
8948 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8949 });
8950 editor.update(cx, |editor, cx| {
8951 assert_text_with_selections(
8952 editor,
8953 indoc! {r#"
8954 «ˇlet a = {
8955 key: "value",
8956 };
8957 »"#},
8958 cx,
8959 );
8960 });
8961}
8962
8963#[gpui::test]
8964async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
8965 init_test(cx, |_| {});
8966
8967 let language = Arc::new(Language::new(
8968 LanguageConfig::default(),
8969 Some(tree_sitter_rust::LANGUAGE.into()),
8970 ));
8971
8972 let text = r#"
8973 use mod1::mod2::{mod3, mod4};
8974
8975 fn fn_1(param1: bool, param2: &str) {
8976 let var1 = "hello world";
8977 }
8978 "#
8979 .unindent();
8980
8981 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8982 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8983 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8984
8985 editor
8986 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8987 .await;
8988
8989 // Test 1: Cursor on a letter of a string word
8990 editor.update_in(cx, |editor, window, cx| {
8991 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8992 s.select_display_ranges([
8993 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
8994 ]);
8995 });
8996 });
8997 editor.update_in(cx, |editor, window, cx| {
8998 assert_text_with_selections(
8999 editor,
9000 indoc! {r#"
9001 use mod1::mod2::{mod3, mod4};
9002
9003 fn fn_1(param1: bool, param2: &str) {
9004 let var1 = "hˇello world";
9005 }
9006 "#},
9007 cx,
9008 );
9009 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9010 assert_text_with_selections(
9011 editor,
9012 indoc! {r#"
9013 use mod1::mod2::{mod3, mod4};
9014
9015 fn fn_1(param1: bool, param2: &str) {
9016 let var1 = "«ˇhello» world";
9017 }
9018 "#},
9019 cx,
9020 );
9021 });
9022
9023 // Test 2: Partial selection within a word
9024 editor.update_in(cx, |editor, window, cx| {
9025 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9026 s.select_display_ranges([
9027 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
9028 ]);
9029 });
9030 });
9031 editor.update_in(cx, |editor, window, cx| {
9032 assert_text_with_selections(
9033 editor,
9034 indoc! {r#"
9035 use mod1::mod2::{mod3, mod4};
9036
9037 fn fn_1(param1: bool, param2: &str) {
9038 let var1 = "h«elˇ»lo world";
9039 }
9040 "#},
9041 cx,
9042 );
9043 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9044 assert_text_with_selections(
9045 editor,
9046 indoc! {r#"
9047 use mod1::mod2::{mod3, mod4};
9048
9049 fn fn_1(param1: bool, param2: &str) {
9050 let var1 = "«ˇhello» world";
9051 }
9052 "#},
9053 cx,
9054 );
9055 });
9056
9057 // Test 3: Complete word already selected
9058 editor.update_in(cx, |editor, window, cx| {
9059 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9060 s.select_display_ranges([
9061 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
9062 ]);
9063 });
9064 });
9065 editor.update_in(cx, |editor, window, cx| {
9066 assert_text_with_selections(
9067 editor,
9068 indoc! {r#"
9069 use mod1::mod2::{mod3, mod4};
9070
9071 fn fn_1(param1: bool, param2: &str) {
9072 let var1 = "«helloˇ» world";
9073 }
9074 "#},
9075 cx,
9076 );
9077 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9078 assert_text_with_selections(
9079 editor,
9080 indoc! {r#"
9081 use mod1::mod2::{mod3, mod4};
9082
9083 fn fn_1(param1: bool, param2: &str) {
9084 let var1 = "«hello worldˇ»";
9085 }
9086 "#},
9087 cx,
9088 );
9089 });
9090
9091 // Test 4: Selection spanning across words
9092 editor.update_in(cx, |editor, window, cx| {
9093 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9094 s.select_display_ranges([
9095 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
9096 ]);
9097 });
9098 });
9099 editor.update_in(cx, |editor, window, cx| {
9100 assert_text_with_selections(
9101 editor,
9102 indoc! {r#"
9103 use mod1::mod2::{mod3, mod4};
9104
9105 fn fn_1(param1: bool, param2: &str) {
9106 let var1 = "hel«lo woˇ»rld";
9107 }
9108 "#},
9109 cx,
9110 );
9111 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9112 assert_text_with_selections(
9113 editor,
9114 indoc! {r#"
9115 use mod1::mod2::{mod3, mod4};
9116
9117 fn fn_1(param1: bool, param2: &str) {
9118 let var1 = "«ˇhello world»";
9119 }
9120 "#},
9121 cx,
9122 );
9123 });
9124
9125 // Test 5: Expansion beyond string
9126 editor.update_in(cx, |editor, window, cx| {
9127 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9128 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9129 assert_text_with_selections(
9130 editor,
9131 indoc! {r#"
9132 use mod1::mod2::{mod3, mod4};
9133
9134 fn fn_1(param1: bool, param2: &str) {
9135 «ˇlet var1 = "hello world";»
9136 }
9137 "#},
9138 cx,
9139 );
9140 });
9141}
9142
9143#[gpui::test]
9144async fn test_unwrap_syntax_nodes(cx: &mut gpui::TestAppContext) {
9145 init_test(cx, |_| {});
9146
9147 let mut cx = EditorTestContext::new(cx).await;
9148
9149 let language = Arc::new(Language::new(
9150 LanguageConfig::default(),
9151 Some(tree_sitter_rust::LANGUAGE.into()),
9152 ));
9153
9154 cx.update_buffer(|buffer, cx| {
9155 buffer.set_language(Some(language), cx);
9156 });
9157
9158 cx.set_state(indoc! { r#"use mod1::{mod2::{«mod3ˇ», mod4}, mod5::{mod6, «mod7ˇ»}};"# });
9159 cx.update_editor(|editor, window, cx| {
9160 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9161 });
9162
9163 cx.assert_editor_state(indoc! { r#"use mod1::{mod2::«mod3ˇ», mod5::«mod7ˇ»};"# });
9164}
9165
9166#[gpui::test]
9167async fn test_fold_function_bodies(cx: &mut TestAppContext) {
9168 init_test(cx, |_| {});
9169
9170 let base_text = r#"
9171 impl A {
9172 // this is an uncommitted comment
9173
9174 fn b() {
9175 c();
9176 }
9177
9178 // this is another uncommitted comment
9179
9180 fn d() {
9181 // e
9182 // f
9183 }
9184 }
9185
9186 fn g() {
9187 // h
9188 }
9189 "#
9190 .unindent();
9191
9192 let text = r#"
9193 ˇimpl A {
9194
9195 fn b() {
9196 c();
9197 }
9198
9199 fn d() {
9200 // e
9201 // f
9202 }
9203 }
9204
9205 fn g() {
9206 // h
9207 }
9208 "#
9209 .unindent();
9210
9211 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9212 cx.set_state(&text);
9213 cx.set_head_text(&base_text);
9214 cx.update_editor(|editor, window, cx| {
9215 editor.expand_all_diff_hunks(&Default::default(), window, cx);
9216 });
9217
9218 cx.assert_state_with_diff(
9219 "
9220 ˇimpl A {
9221 - // this is an uncommitted comment
9222
9223 fn b() {
9224 c();
9225 }
9226
9227 - // this is another uncommitted comment
9228 -
9229 fn d() {
9230 // e
9231 // f
9232 }
9233 }
9234
9235 fn g() {
9236 // h
9237 }
9238 "
9239 .unindent(),
9240 );
9241
9242 let expected_display_text = "
9243 impl A {
9244 // this is an uncommitted comment
9245
9246 fn b() {
9247 ⋯
9248 }
9249
9250 // this is another uncommitted comment
9251
9252 fn d() {
9253 ⋯
9254 }
9255 }
9256
9257 fn g() {
9258 ⋯
9259 }
9260 "
9261 .unindent();
9262
9263 cx.update_editor(|editor, window, cx| {
9264 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
9265 assert_eq!(editor.display_text(cx), expected_display_text);
9266 });
9267}
9268
9269#[gpui::test]
9270async fn test_autoindent(cx: &mut TestAppContext) {
9271 init_test(cx, |_| {});
9272
9273 let language = Arc::new(
9274 Language::new(
9275 LanguageConfig {
9276 brackets: BracketPairConfig {
9277 pairs: vec![
9278 BracketPair {
9279 start: "{".to_string(),
9280 end: "}".to_string(),
9281 close: false,
9282 surround: false,
9283 newline: true,
9284 },
9285 BracketPair {
9286 start: "(".to_string(),
9287 end: ")".to_string(),
9288 close: false,
9289 surround: false,
9290 newline: true,
9291 },
9292 ],
9293 ..Default::default()
9294 },
9295 ..Default::default()
9296 },
9297 Some(tree_sitter_rust::LANGUAGE.into()),
9298 )
9299 .with_indents_query(
9300 r#"
9301 (_ "(" ")" @end) @indent
9302 (_ "{" "}" @end) @indent
9303 "#,
9304 )
9305 .unwrap(),
9306 );
9307
9308 let text = "fn a() {}";
9309
9310 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9311 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9312 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9313 editor
9314 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9315 .await;
9316
9317 editor.update_in(cx, |editor, window, cx| {
9318 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9319 s.select_ranges([5..5, 8..8, 9..9])
9320 });
9321 editor.newline(&Newline, window, cx);
9322 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
9323 assert_eq!(
9324 editor.selections.ranges(cx),
9325 &[
9326 Point::new(1, 4)..Point::new(1, 4),
9327 Point::new(3, 4)..Point::new(3, 4),
9328 Point::new(5, 0)..Point::new(5, 0)
9329 ]
9330 );
9331 });
9332}
9333
9334#[gpui::test]
9335async fn test_autoindent_disabled(cx: &mut TestAppContext) {
9336 init_test(cx, |settings| settings.defaults.auto_indent = Some(false));
9337
9338 let language = Arc::new(
9339 Language::new(
9340 LanguageConfig {
9341 brackets: BracketPairConfig {
9342 pairs: vec![
9343 BracketPair {
9344 start: "{".to_string(),
9345 end: "}".to_string(),
9346 close: false,
9347 surround: false,
9348 newline: true,
9349 },
9350 BracketPair {
9351 start: "(".to_string(),
9352 end: ")".to_string(),
9353 close: false,
9354 surround: false,
9355 newline: true,
9356 },
9357 ],
9358 ..Default::default()
9359 },
9360 ..Default::default()
9361 },
9362 Some(tree_sitter_rust::LANGUAGE.into()),
9363 )
9364 .with_indents_query(
9365 r#"
9366 (_ "(" ")" @end) @indent
9367 (_ "{" "}" @end) @indent
9368 "#,
9369 )
9370 .unwrap(),
9371 );
9372
9373 let text = "fn a() {}";
9374
9375 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9376 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9377 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9378 editor
9379 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9380 .await;
9381
9382 editor.update_in(cx, |editor, window, cx| {
9383 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9384 s.select_ranges([5..5, 8..8, 9..9])
9385 });
9386 editor.newline(&Newline, window, cx);
9387 assert_eq!(
9388 editor.text(cx),
9389 indoc!(
9390 "
9391 fn a(
9392
9393 ) {
9394
9395 }
9396 "
9397 )
9398 );
9399 assert_eq!(
9400 editor.selections.ranges(cx),
9401 &[
9402 Point::new(1, 0)..Point::new(1, 0),
9403 Point::new(3, 0)..Point::new(3, 0),
9404 Point::new(5, 0)..Point::new(5, 0)
9405 ]
9406 );
9407 });
9408}
9409
9410#[gpui::test]
9411async fn test_autoindent_disabled_with_nested_language(cx: &mut TestAppContext) {
9412 init_test(cx, |settings| {
9413 settings.defaults.auto_indent = Some(true);
9414 settings.languages.0.insert(
9415 "python".into(),
9416 LanguageSettingsContent {
9417 auto_indent: Some(false),
9418 ..Default::default()
9419 },
9420 );
9421 });
9422
9423 let mut cx = EditorTestContext::new(cx).await;
9424
9425 let injected_language = Arc::new(
9426 Language::new(
9427 LanguageConfig {
9428 brackets: BracketPairConfig {
9429 pairs: vec![
9430 BracketPair {
9431 start: "{".to_string(),
9432 end: "}".to_string(),
9433 close: false,
9434 surround: false,
9435 newline: true,
9436 },
9437 BracketPair {
9438 start: "(".to_string(),
9439 end: ")".to_string(),
9440 close: true,
9441 surround: false,
9442 newline: true,
9443 },
9444 ],
9445 ..Default::default()
9446 },
9447 name: "python".into(),
9448 ..Default::default()
9449 },
9450 Some(tree_sitter_python::LANGUAGE.into()),
9451 )
9452 .with_indents_query(
9453 r#"
9454 (_ "(" ")" @end) @indent
9455 (_ "{" "}" @end) @indent
9456 "#,
9457 )
9458 .unwrap(),
9459 );
9460
9461 let language = Arc::new(
9462 Language::new(
9463 LanguageConfig {
9464 brackets: BracketPairConfig {
9465 pairs: vec![
9466 BracketPair {
9467 start: "{".to_string(),
9468 end: "}".to_string(),
9469 close: false,
9470 surround: false,
9471 newline: true,
9472 },
9473 BracketPair {
9474 start: "(".to_string(),
9475 end: ")".to_string(),
9476 close: true,
9477 surround: false,
9478 newline: true,
9479 },
9480 ],
9481 ..Default::default()
9482 },
9483 name: LanguageName::new("rust"),
9484 ..Default::default()
9485 },
9486 Some(tree_sitter_rust::LANGUAGE.into()),
9487 )
9488 .with_indents_query(
9489 r#"
9490 (_ "(" ")" @end) @indent
9491 (_ "{" "}" @end) @indent
9492 "#,
9493 )
9494 .unwrap()
9495 .with_injection_query(
9496 r#"
9497 (macro_invocation
9498 macro: (identifier) @_macro_name
9499 (token_tree) @injection.content
9500 (#set! injection.language "python"))
9501 "#,
9502 )
9503 .unwrap(),
9504 );
9505
9506 cx.language_registry().add(injected_language);
9507 cx.language_registry().add(language.clone());
9508
9509 cx.update_buffer(|buffer, cx| {
9510 buffer.set_language(Some(language), cx);
9511 });
9512
9513 cx.set_state(r#"struct A {ˇ}"#);
9514
9515 cx.update_editor(|editor, window, cx| {
9516 editor.newline(&Default::default(), window, cx);
9517 });
9518
9519 cx.assert_editor_state(indoc!(
9520 "struct A {
9521 ˇ
9522 }"
9523 ));
9524
9525 cx.set_state(r#"select_biased!(ˇ)"#);
9526
9527 cx.update_editor(|editor, window, cx| {
9528 editor.newline(&Default::default(), window, cx);
9529 editor.handle_input("def ", window, cx);
9530 editor.handle_input("(", window, cx);
9531 editor.newline(&Default::default(), window, cx);
9532 editor.handle_input("a", window, cx);
9533 });
9534
9535 cx.assert_editor_state(indoc!(
9536 "select_biased!(
9537 def (
9538 aˇ
9539 )
9540 )"
9541 ));
9542}
9543
9544#[gpui::test]
9545async fn test_autoindent_selections(cx: &mut TestAppContext) {
9546 init_test(cx, |_| {});
9547
9548 {
9549 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9550 cx.set_state(indoc! {"
9551 impl A {
9552
9553 fn b() {}
9554
9555 «fn c() {
9556
9557 }ˇ»
9558 }
9559 "});
9560
9561 cx.update_editor(|editor, window, cx| {
9562 editor.autoindent(&Default::default(), window, cx);
9563 });
9564
9565 cx.assert_editor_state(indoc! {"
9566 impl A {
9567
9568 fn b() {}
9569
9570 «fn c() {
9571
9572 }ˇ»
9573 }
9574 "});
9575 }
9576
9577 {
9578 let mut cx = EditorTestContext::new_multibuffer(
9579 cx,
9580 [indoc! { "
9581 impl A {
9582 «
9583 // a
9584 fn b(){}
9585 »
9586 «
9587 }
9588 fn c(){}
9589 »
9590 "}],
9591 );
9592
9593 let buffer = cx.update_editor(|editor, _, cx| {
9594 let buffer = editor.buffer().update(cx, |buffer, _| {
9595 buffer.all_buffers().iter().next().unwrap().clone()
9596 });
9597 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
9598 buffer
9599 });
9600
9601 cx.run_until_parked();
9602 cx.update_editor(|editor, window, cx| {
9603 editor.select_all(&Default::default(), window, cx);
9604 editor.autoindent(&Default::default(), window, cx)
9605 });
9606 cx.run_until_parked();
9607
9608 cx.update(|_, cx| {
9609 assert_eq!(
9610 buffer.read(cx).text(),
9611 indoc! { "
9612 impl A {
9613
9614 // a
9615 fn b(){}
9616
9617
9618 }
9619 fn c(){}
9620
9621 " }
9622 )
9623 });
9624 }
9625}
9626
9627#[gpui::test]
9628async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
9629 init_test(cx, |_| {});
9630
9631 let mut cx = EditorTestContext::new(cx).await;
9632
9633 let language = Arc::new(Language::new(
9634 LanguageConfig {
9635 brackets: BracketPairConfig {
9636 pairs: vec![
9637 BracketPair {
9638 start: "{".to_string(),
9639 end: "}".to_string(),
9640 close: true,
9641 surround: true,
9642 newline: true,
9643 },
9644 BracketPair {
9645 start: "(".to_string(),
9646 end: ")".to_string(),
9647 close: true,
9648 surround: true,
9649 newline: true,
9650 },
9651 BracketPair {
9652 start: "/*".to_string(),
9653 end: " */".to_string(),
9654 close: true,
9655 surround: true,
9656 newline: true,
9657 },
9658 BracketPair {
9659 start: "[".to_string(),
9660 end: "]".to_string(),
9661 close: false,
9662 surround: false,
9663 newline: true,
9664 },
9665 BracketPair {
9666 start: "\"".to_string(),
9667 end: "\"".to_string(),
9668 close: true,
9669 surround: true,
9670 newline: false,
9671 },
9672 BracketPair {
9673 start: "<".to_string(),
9674 end: ">".to_string(),
9675 close: false,
9676 surround: true,
9677 newline: true,
9678 },
9679 ],
9680 ..Default::default()
9681 },
9682 autoclose_before: "})]".to_string(),
9683 ..Default::default()
9684 },
9685 Some(tree_sitter_rust::LANGUAGE.into()),
9686 ));
9687
9688 cx.language_registry().add(language.clone());
9689 cx.update_buffer(|buffer, cx| {
9690 buffer.set_language(Some(language), cx);
9691 });
9692
9693 cx.set_state(
9694 &r#"
9695 🏀ˇ
9696 εˇ
9697 ❤️ˇ
9698 "#
9699 .unindent(),
9700 );
9701
9702 // autoclose multiple nested brackets at multiple cursors
9703 cx.update_editor(|editor, window, cx| {
9704 editor.handle_input("{", window, cx);
9705 editor.handle_input("{", window, cx);
9706 editor.handle_input("{", window, cx);
9707 });
9708 cx.assert_editor_state(
9709 &"
9710 🏀{{{ˇ}}}
9711 ε{{{ˇ}}}
9712 ❤️{{{ˇ}}}
9713 "
9714 .unindent(),
9715 );
9716
9717 // insert a different closing bracket
9718 cx.update_editor(|editor, window, cx| {
9719 editor.handle_input(")", window, cx);
9720 });
9721 cx.assert_editor_state(
9722 &"
9723 🏀{{{)ˇ}}}
9724 ε{{{)ˇ}}}
9725 ❤️{{{)ˇ}}}
9726 "
9727 .unindent(),
9728 );
9729
9730 // skip over the auto-closed brackets when typing a closing bracket
9731 cx.update_editor(|editor, window, cx| {
9732 editor.move_right(&MoveRight, window, cx);
9733 editor.handle_input("}", window, cx);
9734 editor.handle_input("}", window, cx);
9735 editor.handle_input("}", window, cx);
9736 });
9737 cx.assert_editor_state(
9738 &"
9739 🏀{{{)}}}}ˇ
9740 ε{{{)}}}}ˇ
9741 ❤️{{{)}}}}ˇ
9742 "
9743 .unindent(),
9744 );
9745
9746 // autoclose multi-character pairs
9747 cx.set_state(
9748 &"
9749 ˇ
9750 ˇ
9751 "
9752 .unindent(),
9753 );
9754 cx.update_editor(|editor, window, cx| {
9755 editor.handle_input("/", window, cx);
9756 editor.handle_input("*", window, cx);
9757 });
9758 cx.assert_editor_state(
9759 &"
9760 /*ˇ */
9761 /*ˇ */
9762 "
9763 .unindent(),
9764 );
9765
9766 // one cursor autocloses a multi-character pair, one cursor
9767 // does not autoclose.
9768 cx.set_state(
9769 &"
9770 /ˇ
9771 ˇ
9772 "
9773 .unindent(),
9774 );
9775 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
9776 cx.assert_editor_state(
9777 &"
9778 /*ˇ */
9779 *ˇ
9780 "
9781 .unindent(),
9782 );
9783
9784 // Don't autoclose if the next character isn't whitespace and isn't
9785 // listed in the language's "autoclose_before" section.
9786 cx.set_state("ˇa b");
9787 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
9788 cx.assert_editor_state("{ˇa b");
9789
9790 // Don't autoclose if `close` is false for the bracket pair
9791 cx.set_state("ˇ");
9792 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
9793 cx.assert_editor_state("[ˇ");
9794
9795 // Surround with brackets if text is selected
9796 cx.set_state("«aˇ» b");
9797 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
9798 cx.assert_editor_state("{«aˇ»} b");
9799
9800 // Autoclose when not immediately after a word character
9801 cx.set_state("a ˇ");
9802 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
9803 cx.assert_editor_state("a \"ˇ\"");
9804
9805 // Autoclose pair where the start and end characters are the same
9806 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
9807 cx.assert_editor_state("a \"\"ˇ");
9808
9809 // Don't autoclose when immediately after a word character
9810 cx.set_state("aˇ");
9811 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
9812 cx.assert_editor_state("a\"ˇ");
9813
9814 // Do autoclose when after a non-word character
9815 cx.set_state("{ˇ");
9816 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
9817 cx.assert_editor_state("{\"ˇ\"");
9818
9819 // Non identical pairs autoclose regardless of preceding character
9820 cx.set_state("aˇ");
9821 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
9822 cx.assert_editor_state("a{ˇ}");
9823
9824 // Don't autoclose pair if autoclose is disabled
9825 cx.set_state("ˇ");
9826 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
9827 cx.assert_editor_state("<ˇ");
9828
9829 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
9830 cx.set_state("«aˇ» b");
9831 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
9832 cx.assert_editor_state("<«aˇ»> b");
9833}
9834
9835#[gpui::test]
9836async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
9837 init_test(cx, |settings| {
9838 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
9839 });
9840
9841 let mut cx = EditorTestContext::new(cx).await;
9842
9843 let language = Arc::new(Language::new(
9844 LanguageConfig {
9845 brackets: BracketPairConfig {
9846 pairs: vec![
9847 BracketPair {
9848 start: "{".to_string(),
9849 end: "}".to_string(),
9850 close: true,
9851 surround: true,
9852 newline: true,
9853 },
9854 BracketPair {
9855 start: "(".to_string(),
9856 end: ")".to_string(),
9857 close: true,
9858 surround: true,
9859 newline: true,
9860 },
9861 BracketPair {
9862 start: "[".to_string(),
9863 end: "]".to_string(),
9864 close: false,
9865 surround: false,
9866 newline: true,
9867 },
9868 ],
9869 ..Default::default()
9870 },
9871 autoclose_before: "})]".to_string(),
9872 ..Default::default()
9873 },
9874 Some(tree_sitter_rust::LANGUAGE.into()),
9875 ));
9876
9877 cx.language_registry().add(language.clone());
9878 cx.update_buffer(|buffer, cx| {
9879 buffer.set_language(Some(language), cx);
9880 });
9881
9882 cx.set_state(
9883 &"
9884 ˇ
9885 ˇ
9886 ˇ
9887 "
9888 .unindent(),
9889 );
9890
9891 // ensure only matching closing brackets are skipped over
9892 cx.update_editor(|editor, window, cx| {
9893 editor.handle_input("}", window, cx);
9894 editor.move_left(&MoveLeft, window, cx);
9895 editor.handle_input(")", window, cx);
9896 editor.move_left(&MoveLeft, window, cx);
9897 });
9898 cx.assert_editor_state(
9899 &"
9900 ˇ)}
9901 ˇ)}
9902 ˇ)}
9903 "
9904 .unindent(),
9905 );
9906
9907 // skip-over closing brackets at multiple cursors
9908 cx.update_editor(|editor, window, cx| {
9909 editor.handle_input(")", window, cx);
9910 editor.handle_input("}", window, cx);
9911 });
9912 cx.assert_editor_state(
9913 &"
9914 )}ˇ
9915 )}ˇ
9916 )}ˇ
9917 "
9918 .unindent(),
9919 );
9920
9921 // ignore non-close brackets
9922 cx.update_editor(|editor, window, cx| {
9923 editor.handle_input("]", window, cx);
9924 editor.move_left(&MoveLeft, window, cx);
9925 editor.handle_input("]", window, cx);
9926 });
9927 cx.assert_editor_state(
9928 &"
9929 )}]ˇ]
9930 )}]ˇ]
9931 )}]ˇ]
9932 "
9933 .unindent(),
9934 );
9935}
9936
9937#[gpui::test]
9938async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
9939 init_test(cx, |_| {});
9940
9941 let mut cx = EditorTestContext::new(cx).await;
9942
9943 let html_language = Arc::new(
9944 Language::new(
9945 LanguageConfig {
9946 name: "HTML".into(),
9947 brackets: BracketPairConfig {
9948 pairs: vec![
9949 BracketPair {
9950 start: "<".into(),
9951 end: ">".into(),
9952 close: true,
9953 ..Default::default()
9954 },
9955 BracketPair {
9956 start: "{".into(),
9957 end: "}".into(),
9958 close: true,
9959 ..Default::default()
9960 },
9961 BracketPair {
9962 start: "(".into(),
9963 end: ")".into(),
9964 close: true,
9965 ..Default::default()
9966 },
9967 ],
9968 ..Default::default()
9969 },
9970 autoclose_before: "})]>".into(),
9971 ..Default::default()
9972 },
9973 Some(tree_sitter_html::LANGUAGE.into()),
9974 )
9975 .with_injection_query(
9976 r#"
9977 (script_element
9978 (raw_text) @injection.content
9979 (#set! injection.language "javascript"))
9980 "#,
9981 )
9982 .unwrap(),
9983 );
9984
9985 let javascript_language = Arc::new(Language::new(
9986 LanguageConfig {
9987 name: "JavaScript".into(),
9988 brackets: BracketPairConfig {
9989 pairs: vec![
9990 BracketPair {
9991 start: "/*".into(),
9992 end: " */".into(),
9993 close: true,
9994 ..Default::default()
9995 },
9996 BracketPair {
9997 start: "{".into(),
9998 end: "}".into(),
9999 close: true,
10000 ..Default::default()
10001 },
10002 BracketPair {
10003 start: "(".into(),
10004 end: ")".into(),
10005 close: true,
10006 ..Default::default()
10007 },
10008 ],
10009 ..Default::default()
10010 },
10011 autoclose_before: "})]>".into(),
10012 ..Default::default()
10013 },
10014 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
10015 ));
10016
10017 cx.language_registry().add(html_language.clone());
10018 cx.language_registry().add(javascript_language);
10019 cx.executor().run_until_parked();
10020
10021 cx.update_buffer(|buffer, cx| {
10022 buffer.set_language(Some(html_language), cx);
10023 });
10024
10025 cx.set_state(
10026 &r#"
10027 <body>ˇ
10028 <script>
10029 var x = 1;ˇ
10030 </script>
10031 </body>ˇ
10032 "#
10033 .unindent(),
10034 );
10035
10036 // Precondition: different languages are active at different locations.
10037 cx.update_editor(|editor, window, cx| {
10038 let snapshot = editor.snapshot(window, cx);
10039 let cursors = editor.selections.ranges::<usize>(cx);
10040 let languages = cursors
10041 .iter()
10042 .map(|c| snapshot.language_at(c.start).unwrap().name())
10043 .collect::<Vec<_>>();
10044 assert_eq!(
10045 languages,
10046 &["HTML".into(), "JavaScript".into(), "HTML".into()]
10047 );
10048 });
10049
10050 // Angle brackets autoclose in HTML, but not JavaScript.
10051 cx.update_editor(|editor, window, cx| {
10052 editor.handle_input("<", window, cx);
10053 editor.handle_input("a", window, cx);
10054 });
10055 cx.assert_editor_state(
10056 &r#"
10057 <body><aˇ>
10058 <script>
10059 var x = 1;<aˇ
10060 </script>
10061 </body><aˇ>
10062 "#
10063 .unindent(),
10064 );
10065
10066 // Curly braces and parens autoclose in both HTML and JavaScript.
10067 cx.update_editor(|editor, window, cx| {
10068 editor.handle_input(" b=", window, cx);
10069 editor.handle_input("{", window, cx);
10070 editor.handle_input("c", window, cx);
10071 editor.handle_input("(", window, cx);
10072 });
10073 cx.assert_editor_state(
10074 &r#"
10075 <body><a b={c(ˇ)}>
10076 <script>
10077 var x = 1;<a b={c(ˇ)}
10078 </script>
10079 </body><a b={c(ˇ)}>
10080 "#
10081 .unindent(),
10082 );
10083
10084 // Brackets that were already autoclosed are skipped.
10085 cx.update_editor(|editor, window, cx| {
10086 editor.handle_input(")", window, cx);
10087 editor.handle_input("d", window, cx);
10088 editor.handle_input("}", window, cx);
10089 });
10090 cx.assert_editor_state(
10091 &r#"
10092 <body><a b={c()d}ˇ>
10093 <script>
10094 var x = 1;<a b={c()d}ˇ
10095 </script>
10096 </body><a b={c()d}ˇ>
10097 "#
10098 .unindent(),
10099 );
10100 cx.update_editor(|editor, window, cx| {
10101 editor.handle_input(">", window, cx);
10102 });
10103 cx.assert_editor_state(
10104 &r#"
10105 <body><a b={c()d}>ˇ
10106 <script>
10107 var x = 1;<a b={c()d}>ˇ
10108 </script>
10109 </body><a b={c()d}>ˇ
10110 "#
10111 .unindent(),
10112 );
10113
10114 // Reset
10115 cx.set_state(
10116 &r#"
10117 <body>ˇ
10118 <script>
10119 var x = 1;ˇ
10120 </script>
10121 </body>ˇ
10122 "#
10123 .unindent(),
10124 );
10125
10126 cx.update_editor(|editor, window, cx| {
10127 editor.handle_input("<", window, cx);
10128 });
10129 cx.assert_editor_state(
10130 &r#"
10131 <body><ˇ>
10132 <script>
10133 var x = 1;<ˇ
10134 </script>
10135 </body><ˇ>
10136 "#
10137 .unindent(),
10138 );
10139
10140 // When backspacing, the closing angle brackets are removed.
10141 cx.update_editor(|editor, window, cx| {
10142 editor.backspace(&Backspace, window, cx);
10143 });
10144 cx.assert_editor_state(
10145 &r#"
10146 <body>ˇ
10147 <script>
10148 var x = 1;ˇ
10149 </script>
10150 </body>ˇ
10151 "#
10152 .unindent(),
10153 );
10154
10155 // Block comments autoclose in JavaScript, but not HTML.
10156 cx.update_editor(|editor, window, cx| {
10157 editor.handle_input("/", window, cx);
10158 editor.handle_input("*", window, cx);
10159 });
10160 cx.assert_editor_state(
10161 &r#"
10162 <body>/*ˇ
10163 <script>
10164 var x = 1;/*ˇ */
10165 </script>
10166 </body>/*ˇ
10167 "#
10168 .unindent(),
10169 );
10170}
10171
10172#[gpui::test]
10173async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
10174 init_test(cx, |_| {});
10175
10176 let mut cx = EditorTestContext::new(cx).await;
10177
10178 let rust_language = Arc::new(
10179 Language::new(
10180 LanguageConfig {
10181 name: "Rust".into(),
10182 brackets: serde_json::from_value(json!([
10183 { "start": "{", "end": "}", "close": true, "newline": true },
10184 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
10185 ]))
10186 .unwrap(),
10187 autoclose_before: "})]>".into(),
10188 ..Default::default()
10189 },
10190 Some(tree_sitter_rust::LANGUAGE.into()),
10191 )
10192 .with_override_query("(string_literal) @string")
10193 .unwrap(),
10194 );
10195
10196 cx.language_registry().add(rust_language.clone());
10197 cx.update_buffer(|buffer, cx| {
10198 buffer.set_language(Some(rust_language), cx);
10199 });
10200
10201 cx.set_state(
10202 &r#"
10203 let x = ˇ
10204 "#
10205 .unindent(),
10206 );
10207
10208 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
10209 cx.update_editor(|editor, window, cx| {
10210 editor.handle_input("\"", window, cx);
10211 });
10212 cx.assert_editor_state(
10213 &r#"
10214 let x = "ˇ"
10215 "#
10216 .unindent(),
10217 );
10218
10219 // Inserting another quotation mark. The cursor moves across the existing
10220 // automatically-inserted quotation mark.
10221 cx.update_editor(|editor, window, cx| {
10222 editor.handle_input("\"", window, cx);
10223 });
10224 cx.assert_editor_state(
10225 &r#"
10226 let x = ""ˇ
10227 "#
10228 .unindent(),
10229 );
10230
10231 // Reset
10232 cx.set_state(
10233 &r#"
10234 let x = ˇ
10235 "#
10236 .unindent(),
10237 );
10238
10239 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
10240 cx.update_editor(|editor, window, cx| {
10241 editor.handle_input("\"", window, cx);
10242 editor.handle_input(" ", window, cx);
10243 editor.move_left(&Default::default(), window, cx);
10244 editor.handle_input("\\", window, cx);
10245 editor.handle_input("\"", window, cx);
10246 });
10247 cx.assert_editor_state(
10248 &r#"
10249 let x = "\"ˇ "
10250 "#
10251 .unindent(),
10252 );
10253
10254 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
10255 // mark. Nothing is inserted.
10256 cx.update_editor(|editor, window, cx| {
10257 editor.move_right(&Default::default(), window, cx);
10258 editor.handle_input("\"", window, cx);
10259 });
10260 cx.assert_editor_state(
10261 &r#"
10262 let x = "\" "ˇ
10263 "#
10264 .unindent(),
10265 );
10266}
10267
10268#[gpui::test]
10269async fn test_surround_with_pair(cx: &mut TestAppContext) {
10270 init_test(cx, |_| {});
10271
10272 let language = Arc::new(Language::new(
10273 LanguageConfig {
10274 brackets: BracketPairConfig {
10275 pairs: vec![
10276 BracketPair {
10277 start: "{".to_string(),
10278 end: "}".to_string(),
10279 close: true,
10280 surround: true,
10281 newline: true,
10282 },
10283 BracketPair {
10284 start: "/* ".to_string(),
10285 end: "*/".to_string(),
10286 close: true,
10287 surround: true,
10288 ..Default::default()
10289 },
10290 ],
10291 ..Default::default()
10292 },
10293 ..Default::default()
10294 },
10295 Some(tree_sitter_rust::LANGUAGE.into()),
10296 ));
10297
10298 let text = r#"
10299 a
10300 b
10301 c
10302 "#
10303 .unindent();
10304
10305 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10306 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10307 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10308 editor
10309 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10310 .await;
10311
10312 editor.update_in(cx, |editor, window, cx| {
10313 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10314 s.select_display_ranges([
10315 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10316 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10317 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
10318 ])
10319 });
10320
10321 editor.handle_input("{", window, cx);
10322 editor.handle_input("{", window, cx);
10323 editor.handle_input("{", window, cx);
10324 assert_eq!(
10325 editor.text(cx),
10326 "
10327 {{{a}}}
10328 {{{b}}}
10329 {{{c}}}
10330 "
10331 .unindent()
10332 );
10333 assert_eq!(
10334 editor.selections.display_ranges(cx),
10335 [
10336 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
10337 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
10338 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
10339 ]
10340 );
10341
10342 editor.undo(&Undo, window, cx);
10343 editor.undo(&Undo, window, cx);
10344 editor.undo(&Undo, window, cx);
10345 assert_eq!(
10346 editor.text(cx),
10347 "
10348 a
10349 b
10350 c
10351 "
10352 .unindent()
10353 );
10354 assert_eq!(
10355 editor.selections.display_ranges(cx),
10356 [
10357 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10358 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10359 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10360 ]
10361 );
10362
10363 // Ensure inserting the first character of a multi-byte bracket pair
10364 // doesn't surround the selections with the bracket.
10365 editor.handle_input("/", window, cx);
10366 assert_eq!(
10367 editor.text(cx),
10368 "
10369 /
10370 /
10371 /
10372 "
10373 .unindent()
10374 );
10375 assert_eq!(
10376 editor.selections.display_ranges(cx),
10377 [
10378 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10379 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10380 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10381 ]
10382 );
10383
10384 editor.undo(&Undo, window, cx);
10385 assert_eq!(
10386 editor.text(cx),
10387 "
10388 a
10389 b
10390 c
10391 "
10392 .unindent()
10393 );
10394 assert_eq!(
10395 editor.selections.display_ranges(cx),
10396 [
10397 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10398 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10399 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10400 ]
10401 );
10402
10403 // Ensure inserting the last character of a multi-byte bracket pair
10404 // doesn't surround the selections with the bracket.
10405 editor.handle_input("*", window, cx);
10406 assert_eq!(
10407 editor.text(cx),
10408 "
10409 *
10410 *
10411 *
10412 "
10413 .unindent()
10414 );
10415 assert_eq!(
10416 editor.selections.display_ranges(cx),
10417 [
10418 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10419 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10420 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10421 ]
10422 );
10423 });
10424}
10425
10426#[gpui::test]
10427async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
10428 init_test(cx, |_| {});
10429
10430 let language = Arc::new(Language::new(
10431 LanguageConfig {
10432 brackets: BracketPairConfig {
10433 pairs: vec![BracketPair {
10434 start: "{".to_string(),
10435 end: "}".to_string(),
10436 close: true,
10437 surround: true,
10438 newline: true,
10439 }],
10440 ..Default::default()
10441 },
10442 autoclose_before: "}".to_string(),
10443 ..Default::default()
10444 },
10445 Some(tree_sitter_rust::LANGUAGE.into()),
10446 ));
10447
10448 let text = r#"
10449 a
10450 b
10451 c
10452 "#
10453 .unindent();
10454
10455 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10456 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10457 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10458 editor
10459 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10460 .await;
10461
10462 editor.update_in(cx, |editor, window, cx| {
10463 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10464 s.select_ranges([
10465 Point::new(0, 1)..Point::new(0, 1),
10466 Point::new(1, 1)..Point::new(1, 1),
10467 Point::new(2, 1)..Point::new(2, 1),
10468 ])
10469 });
10470
10471 editor.handle_input("{", window, cx);
10472 editor.handle_input("{", window, cx);
10473 editor.handle_input("_", window, cx);
10474 assert_eq!(
10475 editor.text(cx),
10476 "
10477 a{{_}}
10478 b{{_}}
10479 c{{_}}
10480 "
10481 .unindent()
10482 );
10483 assert_eq!(
10484 editor.selections.ranges::<Point>(cx),
10485 [
10486 Point::new(0, 4)..Point::new(0, 4),
10487 Point::new(1, 4)..Point::new(1, 4),
10488 Point::new(2, 4)..Point::new(2, 4)
10489 ]
10490 );
10491
10492 editor.backspace(&Default::default(), window, cx);
10493 editor.backspace(&Default::default(), window, cx);
10494 assert_eq!(
10495 editor.text(cx),
10496 "
10497 a{}
10498 b{}
10499 c{}
10500 "
10501 .unindent()
10502 );
10503 assert_eq!(
10504 editor.selections.ranges::<Point>(cx),
10505 [
10506 Point::new(0, 2)..Point::new(0, 2),
10507 Point::new(1, 2)..Point::new(1, 2),
10508 Point::new(2, 2)..Point::new(2, 2)
10509 ]
10510 );
10511
10512 editor.delete_to_previous_word_start(&Default::default(), window, cx);
10513 assert_eq!(
10514 editor.text(cx),
10515 "
10516 a
10517 b
10518 c
10519 "
10520 .unindent()
10521 );
10522 assert_eq!(
10523 editor.selections.ranges::<Point>(cx),
10524 [
10525 Point::new(0, 1)..Point::new(0, 1),
10526 Point::new(1, 1)..Point::new(1, 1),
10527 Point::new(2, 1)..Point::new(2, 1)
10528 ]
10529 );
10530 });
10531}
10532
10533#[gpui::test]
10534async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
10535 init_test(cx, |settings| {
10536 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
10537 });
10538
10539 let mut cx = EditorTestContext::new(cx).await;
10540
10541 let language = Arc::new(Language::new(
10542 LanguageConfig {
10543 brackets: BracketPairConfig {
10544 pairs: vec![
10545 BracketPair {
10546 start: "{".to_string(),
10547 end: "}".to_string(),
10548 close: true,
10549 surround: true,
10550 newline: true,
10551 },
10552 BracketPair {
10553 start: "(".to_string(),
10554 end: ")".to_string(),
10555 close: true,
10556 surround: true,
10557 newline: true,
10558 },
10559 BracketPair {
10560 start: "[".to_string(),
10561 end: "]".to_string(),
10562 close: false,
10563 surround: true,
10564 newline: true,
10565 },
10566 ],
10567 ..Default::default()
10568 },
10569 autoclose_before: "})]".to_string(),
10570 ..Default::default()
10571 },
10572 Some(tree_sitter_rust::LANGUAGE.into()),
10573 ));
10574
10575 cx.language_registry().add(language.clone());
10576 cx.update_buffer(|buffer, cx| {
10577 buffer.set_language(Some(language), cx);
10578 });
10579
10580 cx.set_state(
10581 &"
10582 {(ˇ)}
10583 [[ˇ]]
10584 {(ˇ)}
10585 "
10586 .unindent(),
10587 );
10588
10589 cx.update_editor(|editor, window, cx| {
10590 editor.backspace(&Default::default(), window, cx);
10591 editor.backspace(&Default::default(), window, cx);
10592 });
10593
10594 cx.assert_editor_state(
10595 &"
10596 ˇ
10597 ˇ]]
10598 ˇ
10599 "
10600 .unindent(),
10601 );
10602
10603 cx.update_editor(|editor, window, cx| {
10604 editor.handle_input("{", window, cx);
10605 editor.handle_input("{", window, cx);
10606 editor.move_right(&MoveRight, window, cx);
10607 editor.move_right(&MoveRight, window, cx);
10608 editor.move_left(&MoveLeft, window, cx);
10609 editor.move_left(&MoveLeft, window, cx);
10610 editor.backspace(&Default::default(), window, cx);
10611 });
10612
10613 cx.assert_editor_state(
10614 &"
10615 {ˇ}
10616 {ˇ}]]
10617 {ˇ}
10618 "
10619 .unindent(),
10620 );
10621
10622 cx.update_editor(|editor, window, cx| {
10623 editor.backspace(&Default::default(), window, cx);
10624 });
10625
10626 cx.assert_editor_state(
10627 &"
10628 ˇ
10629 ˇ]]
10630 ˇ
10631 "
10632 .unindent(),
10633 );
10634}
10635
10636#[gpui::test]
10637async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
10638 init_test(cx, |_| {});
10639
10640 let language = Arc::new(Language::new(
10641 LanguageConfig::default(),
10642 Some(tree_sitter_rust::LANGUAGE.into()),
10643 ));
10644
10645 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
10646 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10647 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10648 editor
10649 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10650 .await;
10651
10652 editor.update_in(cx, |editor, window, cx| {
10653 editor.set_auto_replace_emoji_shortcode(true);
10654
10655 editor.handle_input("Hello ", window, cx);
10656 editor.handle_input(":wave", window, cx);
10657 assert_eq!(editor.text(cx), "Hello :wave".unindent());
10658
10659 editor.handle_input(":", window, cx);
10660 assert_eq!(editor.text(cx), "Hello 👋".unindent());
10661
10662 editor.handle_input(" :smile", window, cx);
10663 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
10664
10665 editor.handle_input(":", window, cx);
10666 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
10667
10668 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
10669 editor.handle_input(":wave", window, cx);
10670 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
10671
10672 editor.handle_input(":", window, cx);
10673 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
10674
10675 editor.handle_input(":1", window, cx);
10676 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
10677
10678 editor.handle_input(":", window, cx);
10679 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
10680
10681 // Ensure shortcode does not get replaced when it is part of a word
10682 editor.handle_input(" Test:wave", window, cx);
10683 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
10684
10685 editor.handle_input(":", window, cx);
10686 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
10687
10688 editor.set_auto_replace_emoji_shortcode(false);
10689
10690 // Ensure shortcode does not get replaced when auto replace is off
10691 editor.handle_input(" :wave", window, cx);
10692 assert_eq!(
10693 editor.text(cx),
10694 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
10695 );
10696
10697 editor.handle_input(":", window, cx);
10698 assert_eq!(
10699 editor.text(cx),
10700 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
10701 );
10702 });
10703}
10704
10705#[gpui::test]
10706async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
10707 init_test(cx, |_| {});
10708
10709 let (text, insertion_ranges) = marked_text_ranges(
10710 indoc! {"
10711 ˇ
10712 "},
10713 false,
10714 );
10715
10716 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
10717 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10718
10719 _ = editor.update_in(cx, |editor, window, cx| {
10720 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
10721
10722 editor
10723 .insert_snippet(&insertion_ranges, snippet, window, cx)
10724 .unwrap();
10725
10726 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
10727 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
10728 assert_eq!(editor.text(cx), expected_text);
10729 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
10730 }
10731
10732 assert(
10733 editor,
10734 cx,
10735 indoc! {"
10736 type «» =•
10737 "},
10738 );
10739
10740 assert!(editor.context_menu_visible(), "There should be a matches");
10741 });
10742}
10743
10744#[gpui::test]
10745async fn test_snippets(cx: &mut TestAppContext) {
10746 init_test(cx, |_| {});
10747
10748 let mut cx = EditorTestContext::new(cx).await;
10749
10750 cx.set_state(indoc! {"
10751 a.ˇ b
10752 a.ˇ b
10753 a.ˇ b
10754 "});
10755
10756 cx.update_editor(|editor, window, cx| {
10757 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
10758 let insertion_ranges = editor
10759 .selections
10760 .all(cx)
10761 .iter()
10762 .map(|s| s.range())
10763 .collect::<Vec<_>>();
10764 editor
10765 .insert_snippet(&insertion_ranges, snippet, window, cx)
10766 .unwrap();
10767 });
10768
10769 cx.assert_editor_state(indoc! {"
10770 a.f(«oneˇ», two, «threeˇ») b
10771 a.f(«oneˇ», two, «threeˇ») b
10772 a.f(«oneˇ», two, «threeˇ») b
10773 "});
10774
10775 // Can't move earlier than the first tab stop
10776 cx.update_editor(|editor, window, cx| {
10777 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
10778 });
10779 cx.assert_editor_state(indoc! {"
10780 a.f(«oneˇ», two, «threeˇ») b
10781 a.f(«oneˇ», two, «threeˇ») b
10782 a.f(«oneˇ», two, «threeˇ») b
10783 "});
10784
10785 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
10786 cx.assert_editor_state(indoc! {"
10787 a.f(one, «twoˇ», three) b
10788 a.f(one, «twoˇ», three) b
10789 a.f(one, «twoˇ», three) b
10790 "});
10791
10792 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
10793 cx.assert_editor_state(indoc! {"
10794 a.f(«oneˇ», two, «threeˇ») b
10795 a.f(«oneˇ», two, «threeˇ») b
10796 a.f(«oneˇ», two, «threeˇ») b
10797 "});
10798
10799 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
10800 cx.assert_editor_state(indoc! {"
10801 a.f(one, «twoˇ», three) b
10802 a.f(one, «twoˇ», three) b
10803 a.f(one, «twoˇ», three) b
10804 "});
10805 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
10806 cx.assert_editor_state(indoc! {"
10807 a.f(one, two, three)ˇ b
10808 a.f(one, two, three)ˇ b
10809 a.f(one, two, three)ˇ b
10810 "});
10811
10812 // As soon as the last tab stop is reached, snippet state is gone
10813 cx.update_editor(|editor, window, cx| {
10814 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
10815 });
10816 cx.assert_editor_state(indoc! {"
10817 a.f(one, two, three)ˇ b
10818 a.f(one, two, three)ˇ b
10819 a.f(one, two, three)ˇ b
10820 "});
10821}
10822
10823#[gpui::test]
10824async fn test_snippet_indentation(cx: &mut TestAppContext) {
10825 init_test(cx, |_| {});
10826
10827 let mut cx = EditorTestContext::new(cx).await;
10828
10829 cx.update_editor(|editor, window, cx| {
10830 let snippet = Snippet::parse(indoc! {"
10831 /*
10832 * Multiline comment with leading indentation
10833 *
10834 * $1
10835 */
10836 $0"})
10837 .unwrap();
10838 let insertion_ranges = editor
10839 .selections
10840 .all(cx)
10841 .iter()
10842 .map(|s| s.range())
10843 .collect::<Vec<_>>();
10844 editor
10845 .insert_snippet(&insertion_ranges, snippet, window, cx)
10846 .unwrap();
10847 });
10848
10849 cx.assert_editor_state(indoc! {"
10850 /*
10851 * Multiline comment with leading indentation
10852 *
10853 * ˇ
10854 */
10855 "});
10856
10857 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
10858 cx.assert_editor_state(indoc! {"
10859 /*
10860 * Multiline comment with leading indentation
10861 *
10862 *•
10863 */
10864 ˇ"});
10865}
10866
10867#[gpui::test]
10868async fn test_document_format_during_save(cx: &mut TestAppContext) {
10869 init_test(cx, |_| {});
10870
10871 let fs = FakeFs::new(cx.executor());
10872 fs.insert_file(path!("/file.rs"), Default::default()).await;
10873
10874 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
10875
10876 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10877 language_registry.add(rust_lang());
10878 let mut fake_servers = language_registry.register_fake_lsp(
10879 "Rust",
10880 FakeLspAdapter {
10881 capabilities: lsp::ServerCapabilities {
10882 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10883 ..Default::default()
10884 },
10885 ..Default::default()
10886 },
10887 );
10888
10889 let buffer = project
10890 .update(cx, |project, cx| {
10891 project.open_local_buffer(path!("/file.rs"), cx)
10892 })
10893 .await
10894 .unwrap();
10895
10896 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10897 let (editor, cx) = cx.add_window_view(|window, cx| {
10898 build_editor_with_project(project.clone(), buffer, window, cx)
10899 });
10900 editor.update_in(cx, |editor, window, cx| {
10901 editor.set_text("one\ntwo\nthree\n", window, cx)
10902 });
10903 assert!(cx.read(|cx| editor.is_dirty(cx)));
10904
10905 cx.executor().start_waiting();
10906 let fake_server = fake_servers.next().await.unwrap();
10907
10908 {
10909 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10910 move |params, _| async move {
10911 assert_eq!(
10912 params.text_document.uri,
10913 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
10914 );
10915 assert_eq!(params.options.tab_size, 4);
10916 Ok(Some(vec![lsp::TextEdit::new(
10917 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
10918 ", ".to_string(),
10919 )]))
10920 },
10921 );
10922 let save = editor
10923 .update_in(cx, |editor, window, cx| {
10924 editor.save(
10925 SaveOptions {
10926 format: true,
10927 autosave: false,
10928 },
10929 project.clone(),
10930 window,
10931 cx,
10932 )
10933 })
10934 .unwrap();
10935 cx.executor().start_waiting();
10936 save.await;
10937
10938 assert_eq!(
10939 editor.update(cx, |editor, cx| editor.text(cx)),
10940 "one, two\nthree\n"
10941 );
10942 assert!(!cx.read(|cx| editor.is_dirty(cx)));
10943 }
10944
10945 {
10946 editor.update_in(cx, |editor, window, cx| {
10947 editor.set_text("one\ntwo\nthree\n", window, cx)
10948 });
10949 assert!(cx.read(|cx| editor.is_dirty(cx)));
10950
10951 // Ensure we can still save even if formatting hangs.
10952 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10953 move |params, _| async move {
10954 assert_eq!(
10955 params.text_document.uri,
10956 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
10957 );
10958 futures::future::pending::<()>().await;
10959 unreachable!()
10960 },
10961 );
10962 let save = editor
10963 .update_in(cx, |editor, window, cx| {
10964 editor.save(
10965 SaveOptions {
10966 format: true,
10967 autosave: false,
10968 },
10969 project.clone(),
10970 window,
10971 cx,
10972 )
10973 })
10974 .unwrap();
10975 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
10976 cx.executor().start_waiting();
10977 save.await;
10978 assert_eq!(
10979 editor.update(cx, |editor, cx| editor.text(cx)),
10980 "one\ntwo\nthree\n"
10981 );
10982 }
10983
10984 // Set rust language override and assert overridden tabsize is sent to language server
10985 update_test_language_settings(cx, |settings| {
10986 settings.languages.0.insert(
10987 "Rust".into(),
10988 LanguageSettingsContent {
10989 tab_size: NonZeroU32::new(8),
10990 ..Default::default()
10991 },
10992 );
10993 });
10994
10995 {
10996 editor.update_in(cx, |editor, window, cx| {
10997 editor.set_text("somehting_new\n", window, cx)
10998 });
10999 assert!(cx.read(|cx| editor.is_dirty(cx)));
11000 let _formatting_request_signal = fake_server
11001 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
11002 assert_eq!(
11003 params.text_document.uri,
11004 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11005 );
11006 assert_eq!(params.options.tab_size, 8);
11007 Ok(Some(vec![]))
11008 });
11009 let save = editor
11010 .update_in(cx, |editor, window, cx| {
11011 editor.save(
11012 SaveOptions {
11013 format: true,
11014 autosave: false,
11015 },
11016 project.clone(),
11017 window,
11018 cx,
11019 )
11020 })
11021 .unwrap();
11022 cx.executor().start_waiting();
11023 save.await;
11024 }
11025}
11026
11027#[gpui::test]
11028async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
11029 init_test(cx, |settings| {
11030 settings.defaults.ensure_final_newline_on_save = Some(false);
11031 });
11032
11033 let fs = FakeFs::new(cx.executor());
11034 fs.insert_file(path!("/file.txt"), "foo".into()).await;
11035
11036 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
11037
11038 let buffer = project
11039 .update(cx, |project, cx| {
11040 project.open_local_buffer(path!("/file.txt"), cx)
11041 })
11042 .await
11043 .unwrap();
11044
11045 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11046 let (editor, cx) = cx.add_window_view(|window, cx| {
11047 build_editor_with_project(project.clone(), buffer, window, cx)
11048 });
11049 editor.update_in(cx, |editor, window, cx| {
11050 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
11051 s.select_ranges([0..0])
11052 });
11053 });
11054 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11055
11056 editor.update_in(cx, |editor, window, cx| {
11057 editor.handle_input("\n", window, cx)
11058 });
11059 cx.run_until_parked();
11060 save(&editor, &project, cx).await;
11061 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11062
11063 editor.update_in(cx, |editor, window, cx| {
11064 editor.undo(&Default::default(), window, cx);
11065 });
11066 save(&editor, &project, cx).await;
11067 assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11068
11069 editor.update_in(cx, |editor, window, cx| {
11070 editor.redo(&Default::default(), window, cx);
11071 });
11072 cx.run_until_parked();
11073 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11074
11075 async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
11076 let save = editor
11077 .update_in(cx, |editor, window, cx| {
11078 editor.save(
11079 SaveOptions {
11080 format: true,
11081 autosave: false,
11082 },
11083 project.clone(),
11084 window,
11085 cx,
11086 )
11087 })
11088 .unwrap();
11089 cx.executor().start_waiting();
11090 save.await;
11091 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11092 }
11093}
11094
11095#[gpui::test]
11096async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
11097 init_test(cx, |_| {});
11098
11099 let cols = 4;
11100 let rows = 10;
11101 let sample_text_1 = sample_text(rows, cols, 'a');
11102 assert_eq!(
11103 sample_text_1,
11104 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
11105 );
11106 let sample_text_2 = sample_text(rows, cols, 'l');
11107 assert_eq!(
11108 sample_text_2,
11109 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
11110 );
11111 let sample_text_3 = sample_text(rows, cols, 'v');
11112 assert_eq!(
11113 sample_text_3,
11114 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
11115 );
11116
11117 let fs = FakeFs::new(cx.executor());
11118 fs.insert_tree(
11119 path!("/a"),
11120 json!({
11121 "main.rs": sample_text_1,
11122 "other.rs": sample_text_2,
11123 "lib.rs": sample_text_3,
11124 }),
11125 )
11126 .await;
11127
11128 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11129 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11130 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11131
11132 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11133 language_registry.add(rust_lang());
11134 let mut fake_servers = language_registry.register_fake_lsp(
11135 "Rust",
11136 FakeLspAdapter {
11137 capabilities: lsp::ServerCapabilities {
11138 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11139 ..Default::default()
11140 },
11141 ..Default::default()
11142 },
11143 );
11144
11145 let worktree = project.update(cx, |project, cx| {
11146 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
11147 assert_eq!(worktrees.len(), 1);
11148 worktrees.pop().unwrap()
11149 });
11150 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
11151
11152 let buffer_1 = project
11153 .update(cx, |project, cx| {
11154 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
11155 })
11156 .await
11157 .unwrap();
11158 let buffer_2 = project
11159 .update(cx, |project, cx| {
11160 project.open_buffer((worktree_id, rel_path("other.rs")), cx)
11161 })
11162 .await
11163 .unwrap();
11164 let buffer_3 = project
11165 .update(cx, |project, cx| {
11166 project.open_buffer((worktree_id, rel_path("lib.rs")), cx)
11167 })
11168 .await
11169 .unwrap();
11170
11171 let multi_buffer = cx.new(|cx| {
11172 let mut multi_buffer = MultiBuffer::new(ReadWrite);
11173 multi_buffer.push_excerpts(
11174 buffer_1.clone(),
11175 [
11176 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11177 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11178 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11179 ],
11180 cx,
11181 );
11182 multi_buffer.push_excerpts(
11183 buffer_2.clone(),
11184 [
11185 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11186 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11187 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11188 ],
11189 cx,
11190 );
11191 multi_buffer.push_excerpts(
11192 buffer_3.clone(),
11193 [
11194 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11195 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11196 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11197 ],
11198 cx,
11199 );
11200 multi_buffer
11201 });
11202 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
11203 Editor::new(
11204 EditorMode::full(),
11205 multi_buffer,
11206 Some(project.clone()),
11207 window,
11208 cx,
11209 )
11210 });
11211
11212 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11213 editor.change_selections(
11214 SelectionEffects::scroll(Autoscroll::Next),
11215 window,
11216 cx,
11217 |s| s.select_ranges(Some(1..2)),
11218 );
11219 editor.insert("|one|two|three|", window, cx);
11220 });
11221 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11222 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11223 editor.change_selections(
11224 SelectionEffects::scroll(Autoscroll::Next),
11225 window,
11226 cx,
11227 |s| s.select_ranges(Some(60..70)),
11228 );
11229 editor.insert("|four|five|six|", window, cx);
11230 });
11231 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11232
11233 // First two buffers should be edited, but not the third one.
11234 assert_eq!(
11235 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11236 "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}",
11237 );
11238 buffer_1.update(cx, |buffer, _| {
11239 assert!(buffer.is_dirty());
11240 assert_eq!(
11241 buffer.text(),
11242 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
11243 )
11244 });
11245 buffer_2.update(cx, |buffer, _| {
11246 assert!(buffer.is_dirty());
11247 assert_eq!(
11248 buffer.text(),
11249 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
11250 )
11251 });
11252 buffer_3.update(cx, |buffer, _| {
11253 assert!(!buffer.is_dirty());
11254 assert_eq!(buffer.text(), sample_text_3,)
11255 });
11256 cx.executor().run_until_parked();
11257
11258 cx.executor().start_waiting();
11259 let save = multi_buffer_editor
11260 .update_in(cx, |editor, window, cx| {
11261 editor.save(
11262 SaveOptions {
11263 format: true,
11264 autosave: false,
11265 },
11266 project.clone(),
11267 window,
11268 cx,
11269 )
11270 })
11271 .unwrap();
11272
11273 let fake_server = fake_servers.next().await.unwrap();
11274 fake_server
11275 .server
11276 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
11277 Ok(Some(vec![lsp::TextEdit::new(
11278 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11279 format!("[{} formatted]", params.text_document.uri),
11280 )]))
11281 })
11282 .detach();
11283 save.await;
11284
11285 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
11286 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
11287 assert_eq!(
11288 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11289 uri!(
11290 "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}"
11291 ),
11292 );
11293 buffer_1.update(cx, |buffer, _| {
11294 assert!(!buffer.is_dirty());
11295 assert_eq!(
11296 buffer.text(),
11297 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
11298 )
11299 });
11300 buffer_2.update(cx, |buffer, _| {
11301 assert!(!buffer.is_dirty());
11302 assert_eq!(
11303 buffer.text(),
11304 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
11305 )
11306 });
11307 buffer_3.update(cx, |buffer, _| {
11308 assert!(!buffer.is_dirty());
11309 assert_eq!(buffer.text(), sample_text_3,)
11310 });
11311}
11312
11313#[gpui::test]
11314async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
11315 init_test(cx, |_| {});
11316
11317 let fs = FakeFs::new(cx.executor());
11318 fs.insert_tree(
11319 path!("/dir"),
11320 json!({
11321 "file1.rs": "fn main() { println!(\"hello\"); }",
11322 "file2.rs": "fn test() { println!(\"test\"); }",
11323 "file3.rs": "fn other() { println!(\"other\"); }\n",
11324 }),
11325 )
11326 .await;
11327
11328 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
11329 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11330 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11331
11332 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11333 language_registry.add(rust_lang());
11334
11335 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
11336 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
11337
11338 // Open three buffers
11339 let buffer_1 = project
11340 .update(cx, |project, cx| {
11341 project.open_buffer((worktree_id, rel_path("file1.rs")), cx)
11342 })
11343 .await
11344 .unwrap();
11345 let buffer_2 = project
11346 .update(cx, |project, cx| {
11347 project.open_buffer((worktree_id, rel_path("file2.rs")), cx)
11348 })
11349 .await
11350 .unwrap();
11351 let buffer_3 = project
11352 .update(cx, |project, cx| {
11353 project.open_buffer((worktree_id, rel_path("file3.rs")), cx)
11354 })
11355 .await
11356 .unwrap();
11357
11358 // Create a multi-buffer with all three buffers
11359 let multi_buffer = cx.new(|cx| {
11360 let mut multi_buffer = MultiBuffer::new(ReadWrite);
11361 multi_buffer.push_excerpts(
11362 buffer_1.clone(),
11363 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11364 cx,
11365 );
11366 multi_buffer.push_excerpts(
11367 buffer_2.clone(),
11368 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11369 cx,
11370 );
11371 multi_buffer.push_excerpts(
11372 buffer_3.clone(),
11373 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11374 cx,
11375 );
11376 multi_buffer
11377 });
11378
11379 let editor = cx.new_window_entity(|window, cx| {
11380 Editor::new(
11381 EditorMode::full(),
11382 multi_buffer,
11383 Some(project.clone()),
11384 window,
11385 cx,
11386 )
11387 });
11388
11389 // Edit only the first buffer
11390 editor.update_in(cx, |editor, window, cx| {
11391 editor.change_selections(
11392 SelectionEffects::scroll(Autoscroll::Next),
11393 window,
11394 cx,
11395 |s| s.select_ranges(Some(10..10)),
11396 );
11397 editor.insert("// edited", window, cx);
11398 });
11399
11400 // Verify that only buffer 1 is dirty
11401 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
11402 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11403 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11404
11405 // Get write counts after file creation (files were created with initial content)
11406 // We expect each file to have been written once during creation
11407 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
11408 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
11409 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
11410
11411 // Perform autosave
11412 let save_task = editor.update_in(cx, |editor, window, cx| {
11413 editor.save(
11414 SaveOptions {
11415 format: true,
11416 autosave: true,
11417 },
11418 project.clone(),
11419 window,
11420 cx,
11421 )
11422 });
11423 save_task.await.unwrap();
11424
11425 // Only the dirty buffer should have been saved
11426 assert_eq!(
11427 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
11428 1,
11429 "Buffer 1 was dirty, so it should have been written once during autosave"
11430 );
11431 assert_eq!(
11432 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
11433 0,
11434 "Buffer 2 was clean, so it should not have been written during autosave"
11435 );
11436 assert_eq!(
11437 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
11438 0,
11439 "Buffer 3 was clean, so it should not have been written during autosave"
11440 );
11441
11442 // Verify buffer states after autosave
11443 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11444 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11445 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11446
11447 // Now perform a manual save (format = true)
11448 let save_task = editor.update_in(cx, |editor, window, cx| {
11449 editor.save(
11450 SaveOptions {
11451 format: true,
11452 autosave: false,
11453 },
11454 project.clone(),
11455 window,
11456 cx,
11457 )
11458 });
11459 save_task.await.unwrap();
11460
11461 // During manual save, clean buffers don't get written to disk
11462 // They just get did_save called for language server notifications
11463 assert_eq!(
11464 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
11465 1,
11466 "Buffer 1 should only have been written once total (during autosave, not manual save)"
11467 );
11468 assert_eq!(
11469 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
11470 0,
11471 "Buffer 2 should not have been written at all"
11472 );
11473 assert_eq!(
11474 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
11475 0,
11476 "Buffer 3 should not have been written at all"
11477 );
11478}
11479
11480async fn setup_range_format_test(
11481 cx: &mut TestAppContext,
11482) -> (
11483 Entity<Project>,
11484 Entity<Editor>,
11485 &mut gpui::VisualTestContext,
11486 lsp::FakeLanguageServer,
11487) {
11488 init_test(cx, |_| {});
11489
11490 let fs = FakeFs::new(cx.executor());
11491 fs.insert_file(path!("/file.rs"), Default::default()).await;
11492
11493 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11494
11495 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11496 language_registry.add(rust_lang());
11497 let mut fake_servers = language_registry.register_fake_lsp(
11498 "Rust",
11499 FakeLspAdapter {
11500 capabilities: lsp::ServerCapabilities {
11501 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
11502 ..lsp::ServerCapabilities::default()
11503 },
11504 ..FakeLspAdapter::default()
11505 },
11506 );
11507
11508 let buffer = project
11509 .update(cx, |project, cx| {
11510 project.open_local_buffer(path!("/file.rs"), cx)
11511 })
11512 .await
11513 .unwrap();
11514
11515 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11516 let (editor, cx) = cx.add_window_view(|window, cx| {
11517 build_editor_with_project(project.clone(), buffer, window, cx)
11518 });
11519
11520 cx.executor().start_waiting();
11521 let fake_server = fake_servers.next().await.unwrap();
11522
11523 (project, editor, cx, fake_server)
11524}
11525
11526#[gpui::test]
11527async fn test_range_format_on_save_success(cx: &mut TestAppContext) {
11528 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11529
11530 editor.update_in(cx, |editor, window, cx| {
11531 editor.set_text("one\ntwo\nthree\n", window, cx)
11532 });
11533 assert!(cx.read(|cx| editor.is_dirty(cx)));
11534
11535 let save = editor
11536 .update_in(cx, |editor, window, cx| {
11537 editor.save(
11538 SaveOptions {
11539 format: true,
11540 autosave: false,
11541 },
11542 project.clone(),
11543 window,
11544 cx,
11545 )
11546 })
11547 .unwrap();
11548 fake_server
11549 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
11550 assert_eq!(
11551 params.text_document.uri,
11552 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11553 );
11554 assert_eq!(params.options.tab_size, 4);
11555 Ok(Some(vec![lsp::TextEdit::new(
11556 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11557 ", ".to_string(),
11558 )]))
11559 })
11560 .next()
11561 .await;
11562 cx.executor().start_waiting();
11563 save.await;
11564 assert_eq!(
11565 editor.update(cx, |editor, cx| editor.text(cx)),
11566 "one, two\nthree\n"
11567 );
11568 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11569}
11570
11571#[gpui::test]
11572async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) {
11573 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11574
11575 editor.update_in(cx, |editor, window, cx| {
11576 editor.set_text("one\ntwo\nthree\n", window, cx)
11577 });
11578 assert!(cx.read(|cx| editor.is_dirty(cx)));
11579
11580 // Test that save still works when formatting hangs
11581 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
11582 move |params, _| async move {
11583 assert_eq!(
11584 params.text_document.uri,
11585 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11586 );
11587 futures::future::pending::<()>().await;
11588 unreachable!()
11589 },
11590 );
11591 let save = editor
11592 .update_in(cx, |editor, window, cx| {
11593 editor.save(
11594 SaveOptions {
11595 format: true,
11596 autosave: false,
11597 },
11598 project.clone(),
11599 window,
11600 cx,
11601 )
11602 })
11603 .unwrap();
11604 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11605 cx.executor().start_waiting();
11606 save.await;
11607 assert_eq!(
11608 editor.update(cx, |editor, cx| editor.text(cx)),
11609 "one\ntwo\nthree\n"
11610 );
11611 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11612}
11613
11614#[gpui::test]
11615async fn test_range_format_not_called_for_clean_buffer(cx: &mut TestAppContext) {
11616 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11617
11618 // Buffer starts clean, no formatting should be requested
11619 let save = editor
11620 .update_in(cx, |editor, window, cx| {
11621 editor.save(
11622 SaveOptions {
11623 format: false,
11624 autosave: false,
11625 },
11626 project.clone(),
11627 window,
11628 cx,
11629 )
11630 })
11631 .unwrap();
11632 let _pending_format_request = fake_server
11633 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
11634 panic!("Should not be invoked");
11635 })
11636 .next();
11637 cx.executor().start_waiting();
11638 save.await;
11639 cx.run_until_parked();
11640}
11641
11642#[gpui::test]
11643async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppContext) {
11644 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11645
11646 // Set Rust language override and assert overridden tabsize is sent to language server
11647 update_test_language_settings(cx, |settings| {
11648 settings.languages.0.insert(
11649 "Rust".into(),
11650 LanguageSettingsContent {
11651 tab_size: NonZeroU32::new(8),
11652 ..Default::default()
11653 },
11654 );
11655 });
11656
11657 editor.update_in(cx, |editor, window, cx| {
11658 editor.set_text("something_new\n", window, cx)
11659 });
11660 assert!(cx.read(|cx| editor.is_dirty(cx)));
11661 let save = editor
11662 .update_in(cx, |editor, window, cx| {
11663 editor.save(
11664 SaveOptions {
11665 format: true,
11666 autosave: false,
11667 },
11668 project.clone(),
11669 window,
11670 cx,
11671 )
11672 })
11673 .unwrap();
11674 fake_server
11675 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
11676 assert_eq!(
11677 params.text_document.uri,
11678 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11679 );
11680 assert_eq!(params.options.tab_size, 8);
11681 Ok(Some(Vec::new()))
11682 })
11683 .next()
11684 .await;
11685 save.await;
11686}
11687
11688#[gpui::test]
11689async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
11690 init_test(cx, |settings| {
11691 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
11692 Formatter::LanguageServer { name: None },
11693 )))
11694 });
11695
11696 let fs = FakeFs::new(cx.executor());
11697 fs.insert_file(path!("/file.rs"), Default::default()).await;
11698
11699 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11700
11701 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11702 language_registry.add(Arc::new(Language::new(
11703 LanguageConfig {
11704 name: "Rust".into(),
11705 matcher: LanguageMatcher {
11706 path_suffixes: vec!["rs".to_string()],
11707 ..Default::default()
11708 },
11709 ..LanguageConfig::default()
11710 },
11711 Some(tree_sitter_rust::LANGUAGE.into()),
11712 )));
11713 update_test_language_settings(cx, |settings| {
11714 // Enable Prettier formatting for the same buffer, and ensure
11715 // LSP is called instead of Prettier.
11716 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
11717 });
11718 let mut fake_servers = language_registry.register_fake_lsp(
11719 "Rust",
11720 FakeLspAdapter {
11721 capabilities: lsp::ServerCapabilities {
11722 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11723 ..Default::default()
11724 },
11725 ..Default::default()
11726 },
11727 );
11728
11729 let buffer = project
11730 .update(cx, |project, cx| {
11731 project.open_local_buffer(path!("/file.rs"), cx)
11732 })
11733 .await
11734 .unwrap();
11735
11736 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11737 let (editor, cx) = cx.add_window_view(|window, cx| {
11738 build_editor_with_project(project.clone(), buffer, window, cx)
11739 });
11740 editor.update_in(cx, |editor, window, cx| {
11741 editor.set_text("one\ntwo\nthree\n", window, cx)
11742 });
11743
11744 cx.executor().start_waiting();
11745 let fake_server = fake_servers.next().await.unwrap();
11746
11747 let format = editor
11748 .update_in(cx, |editor, window, cx| {
11749 editor.perform_format(
11750 project.clone(),
11751 FormatTrigger::Manual,
11752 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
11753 window,
11754 cx,
11755 )
11756 })
11757 .unwrap();
11758 fake_server
11759 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
11760 assert_eq!(
11761 params.text_document.uri,
11762 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11763 );
11764 assert_eq!(params.options.tab_size, 4);
11765 Ok(Some(vec![lsp::TextEdit::new(
11766 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11767 ", ".to_string(),
11768 )]))
11769 })
11770 .next()
11771 .await;
11772 cx.executor().start_waiting();
11773 format.await;
11774 assert_eq!(
11775 editor.update(cx, |editor, cx| editor.text(cx)),
11776 "one, two\nthree\n"
11777 );
11778
11779 editor.update_in(cx, |editor, window, cx| {
11780 editor.set_text("one\ntwo\nthree\n", window, cx)
11781 });
11782 // Ensure we don't lock if formatting hangs.
11783 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11784 move |params, _| async move {
11785 assert_eq!(
11786 params.text_document.uri,
11787 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11788 );
11789 futures::future::pending::<()>().await;
11790 unreachable!()
11791 },
11792 );
11793 let format = editor
11794 .update_in(cx, |editor, window, cx| {
11795 editor.perform_format(
11796 project,
11797 FormatTrigger::Manual,
11798 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
11799 window,
11800 cx,
11801 )
11802 })
11803 .unwrap();
11804 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11805 cx.executor().start_waiting();
11806 format.await;
11807 assert_eq!(
11808 editor.update(cx, |editor, cx| editor.text(cx)),
11809 "one\ntwo\nthree\n"
11810 );
11811}
11812
11813#[gpui::test]
11814async fn test_multiple_formatters(cx: &mut TestAppContext) {
11815 init_test(cx, |settings| {
11816 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
11817 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
11818 Formatter::LanguageServer { name: None },
11819 Formatter::CodeActions(
11820 [
11821 ("code-action-1".into(), true),
11822 ("code-action-2".into(), true),
11823 ]
11824 .into_iter()
11825 .collect(),
11826 ),
11827 ])))
11828 });
11829
11830 let fs = FakeFs::new(cx.executor());
11831 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
11832 .await;
11833
11834 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11835 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11836 language_registry.add(rust_lang());
11837
11838 let mut fake_servers = language_registry.register_fake_lsp(
11839 "Rust",
11840 FakeLspAdapter {
11841 capabilities: lsp::ServerCapabilities {
11842 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11843 execute_command_provider: Some(lsp::ExecuteCommandOptions {
11844 commands: vec!["the-command-for-code-action-1".into()],
11845 ..Default::default()
11846 }),
11847 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
11848 ..Default::default()
11849 },
11850 ..Default::default()
11851 },
11852 );
11853
11854 let buffer = project
11855 .update(cx, |project, cx| {
11856 project.open_local_buffer(path!("/file.rs"), cx)
11857 })
11858 .await
11859 .unwrap();
11860
11861 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11862 let (editor, cx) = cx.add_window_view(|window, cx| {
11863 build_editor_with_project(project.clone(), buffer, window, cx)
11864 });
11865
11866 cx.executor().start_waiting();
11867
11868 let fake_server = fake_servers.next().await.unwrap();
11869 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11870 move |_params, _| async move {
11871 Ok(Some(vec![lsp::TextEdit::new(
11872 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
11873 "applied-formatting\n".to_string(),
11874 )]))
11875 },
11876 );
11877 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
11878 move |params, _| async move {
11879 assert_eq!(
11880 params.context.only,
11881 Some(vec!["code-action-1".into(), "code-action-2".into()])
11882 );
11883 let uri = lsp::Uri::from_file_path(path!("/file.rs")).unwrap();
11884 Ok(Some(vec![
11885 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
11886 kind: Some("code-action-1".into()),
11887 edit: Some(lsp::WorkspaceEdit::new(
11888 [(
11889 uri.clone(),
11890 vec![lsp::TextEdit::new(
11891 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
11892 "applied-code-action-1-edit\n".to_string(),
11893 )],
11894 )]
11895 .into_iter()
11896 .collect(),
11897 )),
11898 command: Some(lsp::Command {
11899 command: "the-command-for-code-action-1".into(),
11900 ..Default::default()
11901 }),
11902 ..Default::default()
11903 }),
11904 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
11905 kind: Some("code-action-2".into()),
11906 edit: Some(lsp::WorkspaceEdit::new(
11907 [(
11908 uri,
11909 vec![lsp::TextEdit::new(
11910 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
11911 "applied-code-action-2-edit\n".to_string(),
11912 )],
11913 )]
11914 .into_iter()
11915 .collect(),
11916 )),
11917 ..Default::default()
11918 }),
11919 ]))
11920 },
11921 );
11922
11923 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
11924 move |params, _| async move { Ok(params) }
11925 });
11926
11927 let command_lock = Arc::new(futures::lock::Mutex::new(()));
11928 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
11929 let fake = fake_server.clone();
11930 let lock = command_lock.clone();
11931 move |params, _| {
11932 assert_eq!(params.command, "the-command-for-code-action-1");
11933 let fake = fake.clone();
11934 let lock = lock.clone();
11935 async move {
11936 lock.lock().await;
11937 fake.server
11938 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
11939 label: None,
11940 edit: lsp::WorkspaceEdit {
11941 changes: Some(
11942 [(
11943 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
11944 vec![lsp::TextEdit {
11945 range: lsp::Range::new(
11946 lsp::Position::new(0, 0),
11947 lsp::Position::new(0, 0),
11948 ),
11949 new_text: "applied-code-action-1-command\n".into(),
11950 }],
11951 )]
11952 .into_iter()
11953 .collect(),
11954 ),
11955 ..Default::default()
11956 },
11957 })
11958 .await
11959 .into_response()
11960 .unwrap();
11961 Ok(Some(json!(null)))
11962 }
11963 }
11964 });
11965
11966 cx.executor().start_waiting();
11967 editor
11968 .update_in(cx, |editor, window, cx| {
11969 editor.perform_format(
11970 project.clone(),
11971 FormatTrigger::Manual,
11972 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
11973 window,
11974 cx,
11975 )
11976 })
11977 .unwrap()
11978 .await;
11979 editor.update(cx, |editor, cx| {
11980 assert_eq!(
11981 editor.text(cx),
11982 r#"
11983 applied-code-action-2-edit
11984 applied-code-action-1-command
11985 applied-code-action-1-edit
11986 applied-formatting
11987 one
11988 two
11989 three
11990 "#
11991 .unindent()
11992 );
11993 });
11994
11995 editor.update_in(cx, |editor, window, cx| {
11996 editor.undo(&Default::default(), window, cx);
11997 assert_eq!(editor.text(cx), "one \ntwo \nthree");
11998 });
11999
12000 // Perform a manual edit while waiting for an LSP command
12001 // that's being run as part of a formatting code action.
12002 let lock_guard = command_lock.lock().await;
12003 let format = editor
12004 .update_in(cx, |editor, window, cx| {
12005 editor.perform_format(
12006 project.clone(),
12007 FormatTrigger::Manual,
12008 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12009 window,
12010 cx,
12011 )
12012 })
12013 .unwrap();
12014 cx.run_until_parked();
12015 editor.update(cx, |editor, cx| {
12016 assert_eq!(
12017 editor.text(cx),
12018 r#"
12019 applied-code-action-1-edit
12020 applied-formatting
12021 one
12022 two
12023 three
12024 "#
12025 .unindent()
12026 );
12027
12028 editor.buffer.update(cx, |buffer, cx| {
12029 let ix = buffer.len(cx);
12030 buffer.edit([(ix..ix, "edited\n")], None, cx);
12031 });
12032 });
12033
12034 // Allow the LSP command to proceed. Because the buffer was edited,
12035 // the second code action will not be run.
12036 drop(lock_guard);
12037 format.await;
12038 editor.update_in(cx, |editor, window, cx| {
12039 assert_eq!(
12040 editor.text(cx),
12041 r#"
12042 applied-code-action-1-command
12043 applied-code-action-1-edit
12044 applied-formatting
12045 one
12046 two
12047 three
12048 edited
12049 "#
12050 .unindent()
12051 );
12052
12053 // The manual edit is undone first, because it is the last thing the user did
12054 // (even though the command completed afterwards).
12055 editor.undo(&Default::default(), window, cx);
12056 assert_eq!(
12057 editor.text(cx),
12058 r#"
12059 applied-code-action-1-command
12060 applied-code-action-1-edit
12061 applied-formatting
12062 one
12063 two
12064 three
12065 "#
12066 .unindent()
12067 );
12068
12069 // All the formatting (including the command, which completed after the manual edit)
12070 // is undone together.
12071 editor.undo(&Default::default(), window, cx);
12072 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12073 });
12074}
12075
12076#[gpui::test]
12077async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
12078 init_test(cx, |settings| {
12079 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
12080 Formatter::LanguageServer { name: None },
12081 ])))
12082 });
12083
12084 let fs = FakeFs::new(cx.executor());
12085 fs.insert_file(path!("/file.ts"), Default::default()).await;
12086
12087 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12088
12089 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12090 language_registry.add(Arc::new(Language::new(
12091 LanguageConfig {
12092 name: "TypeScript".into(),
12093 matcher: LanguageMatcher {
12094 path_suffixes: vec!["ts".to_string()],
12095 ..Default::default()
12096 },
12097 ..LanguageConfig::default()
12098 },
12099 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
12100 )));
12101 update_test_language_settings(cx, |settings| {
12102 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
12103 });
12104 let mut fake_servers = language_registry.register_fake_lsp(
12105 "TypeScript",
12106 FakeLspAdapter {
12107 capabilities: lsp::ServerCapabilities {
12108 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
12109 ..Default::default()
12110 },
12111 ..Default::default()
12112 },
12113 );
12114
12115 let buffer = project
12116 .update(cx, |project, cx| {
12117 project.open_local_buffer(path!("/file.ts"), cx)
12118 })
12119 .await
12120 .unwrap();
12121
12122 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12123 let (editor, cx) = cx.add_window_view(|window, cx| {
12124 build_editor_with_project(project.clone(), buffer, window, cx)
12125 });
12126 editor.update_in(cx, |editor, window, cx| {
12127 editor.set_text(
12128 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12129 window,
12130 cx,
12131 )
12132 });
12133
12134 cx.executor().start_waiting();
12135 let fake_server = fake_servers.next().await.unwrap();
12136
12137 let format = editor
12138 .update_in(cx, |editor, window, cx| {
12139 editor.perform_code_action_kind(
12140 project.clone(),
12141 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12142 window,
12143 cx,
12144 )
12145 })
12146 .unwrap();
12147 fake_server
12148 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
12149 assert_eq!(
12150 params.text_document.uri,
12151 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12152 );
12153 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
12154 lsp::CodeAction {
12155 title: "Organize Imports".to_string(),
12156 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
12157 edit: Some(lsp::WorkspaceEdit {
12158 changes: Some(
12159 [(
12160 params.text_document.uri.clone(),
12161 vec![lsp::TextEdit::new(
12162 lsp::Range::new(
12163 lsp::Position::new(1, 0),
12164 lsp::Position::new(2, 0),
12165 ),
12166 "".to_string(),
12167 )],
12168 )]
12169 .into_iter()
12170 .collect(),
12171 ),
12172 ..Default::default()
12173 }),
12174 ..Default::default()
12175 },
12176 )]))
12177 })
12178 .next()
12179 .await;
12180 cx.executor().start_waiting();
12181 format.await;
12182 assert_eq!(
12183 editor.update(cx, |editor, cx| editor.text(cx)),
12184 "import { a } from 'module';\n\nconst x = a;\n"
12185 );
12186
12187 editor.update_in(cx, |editor, window, cx| {
12188 editor.set_text(
12189 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12190 window,
12191 cx,
12192 )
12193 });
12194 // Ensure we don't lock if code action hangs.
12195 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
12196 move |params, _| async move {
12197 assert_eq!(
12198 params.text_document.uri,
12199 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12200 );
12201 futures::future::pending::<()>().await;
12202 unreachable!()
12203 },
12204 );
12205 let format = editor
12206 .update_in(cx, |editor, window, cx| {
12207 editor.perform_code_action_kind(
12208 project,
12209 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12210 window,
12211 cx,
12212 )
12213 })
12214 .unwrap();
12215 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
12216 cx.executor().start_waiting();
12217 format.await;
12218 assert_eq!(
12219 editor.update(cx, |editor, cx| editor.text(cx)),
12220 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
12221 );
12222}
12223
12224#[gpui::test]
12225async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
12226 init_test(cx, |_| {});
12227
12228 let mut cx = EditorLspTestContext::new_rust(
12229 lsp::ServerCapabilities {
12230 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12231 ..Default::default()
12232 },
12233 cx,
12234 )
12235 .await;
12236
12237 cx.set_state(indoc! {"
12238 one.twoˇ
12239 "});
12240
12241 // The format request takes a long time. When it completes, it inserts
12242 // a newline and an indent before the `.`
12243 cx.lsp
12244 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
12245 let executor = cx.background_executor().clone();
12246 async move {
12247 executor.timer(Duration::from_millis(100)).await;
12248 Ok(Some(vec![lsp::TextEdit {
12249 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
12250 new_text: "\n ".into(),
12251 }]))
12252 }
12253 });
12254
12255 // Submit a format request.
12256 let format_1 = cx
12257 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12258 .unwrap();
12259 cx.executor().run_until_parked();
12260
12261 // Submit a second format request.
12262 let format_2 = cx
12263 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12264 .unwrap();
12265 cx.executor().run_until_parked();
12266
12267 // Wait for both format requests to complete
12268 cx.executor().advance_clock(Duration::from_millis(200));
12269 cx.executor().start_waiting();
12270 format_1.await.unwrap();
12271 cx.executor().start_waiting();
12272 format_2.await.unwrap();
12273
12274 // The formatting edits only happens once.
12275 cx.assert_editor_state(indoc! {"
12276 one
12277 .twoˇ
12278 "});
12279}
12280
12281#[gpui::test]
12282async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
12283 init_test(cx, |settings| {
12284 settings.defaults.formatter = Some(SelectedFormatter::Auto)
12285 });
12286
12287 let mut cx = EditorLspTestContext::new_rust(
12288 lsp::ServerCapabilities {
12289 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12290 ..Default::default()
12291 },
12292 cx,
12293 )
12294 .await;
12295
12296 // Set up a buffer white some trailing whitespace and no trailing newline.
12297 cx.set_state(
12298 &[
12299 "one ", //
12300 "twoˇ", //
12301 "three ", //
12302 "four", //
12303 ]
12304 .join("\n"),
12305 );
12306
12307 // Submit a format request.
12308 let format = cx
12309 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12310 .unwrap();
12311
12312 // Record which buffer changes have been sent to the language server
12313 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
12314 cx.lsp
12315 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
12316 let buffer_changes = buffer_changes.clone();
12317 move |params, _| {
12318 buffer_changes.lock().extend(
12319 params
12320 .content_changes
12321 .into_iter()
12322 .map(|e| (e.range.unwrap(), e.text)),
12323 );
12324 }
12325 });
12326
12327 // Handle formatting requests to the language server.
12328 cx.lsp
12329 .set_request_handler::<lsp::request::Formatting, _, _>({
12330 let buffer_changes = buffer_changes.clone();
12331 move |_, _| {
12332 // When formatting is requested, trailing whitespace has already been stripped,
12333 // and the trailing newline has already been added.
12334 assert_eq!(
12335 &buffer_changes.lock()[1..],
12336 &[
12337 (
12338 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
12339 "".into()
12340 ),
12341 (
12342 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
12343 "".into()
12344 ),
12345 (
12346 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
12347 "\n".into()
12348 ),
12349 ]
12350 );
12351
12352 // Insert blank lines between each line of the buffer.
12353 async move {
12354 Ok(Some(vec![
12355 lsp::TextEdit {
12356 range: lsp::Range::new(
12357 lsp::Position::new(1, 0),
12358 lsp::Position::new(1, 0),
12359 ),
12360 new_text: "\n".into(),
12361 },
12362 lsp::TextEdit {
12363 range: lsp::Range::new(
12364 lsp::Position::new(2, 0),
12365 lsp::Position::new(2, 0),
12366 ),
12367 new_text: "\n".into(),
12368 },
12369 ]))
12370 }
12371 }
12372 });
12373
12374 // After formatting the buffer, the trailing whitespace is stripped,
12375 // a newline is appended, and the edits provided by the language server
12376 // have been applied.
12377 format.await.unwrap();
12378 cx.assert_editor_state(
12379 &[
12380 "one", //
12381 "", //
12382 "twoˇ", //
12383 "", //
12384 "three", //
12385 "four", //
12386 "", //
12387 ]
12388 .join("\n"),
12389 );
12390
12391 // Undoing the formatting undoes the trailing whitespace removal, the
12392 // trailing newline, and the LSP edits.
12393 cx.update_buffer(|buffer, cx| buffer.undo(cx));
12394 cx.assert_editor_state(
12395 &[
12396 "one ", //
12397 "twoˇ", //
12398 "three ", //
12399 "four", //
12400 ]
12401 .join("\n"),
12402 );
12403}
12404
12405#[gpui::test]
12406async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
12407 cx: &mut TestAppContext,
12408) {
12409 init_test(cx, |_| {});
12410
12411 cx.update(|cx| {
12412 cx.update_global::<SettingsStore, _>(|settings, cx| {
12413 settings.update_user_settings(cx, |settings| {
12414 settings.editor.auto_signature_help = Some(true);
12415 });
12416 });
12417 });
12418
12419 let mut cx = EditorLspTestContext::new_rust(
12420 lsp::ServerCapabilities {
12421 signature_help_provider: Some(lsp::SignatureHelpOptions {
12422 ..Default::default()
12423 }),
12424 ..Default::default()
12425 },
12426 cx,
12427 )
12428 .await;
12429
12430 let language = Language::new(
12431 LanguageConfig {
12432 name: "Rust".into(),
12433 brackets: BracketPairConfig {
12434 pairs: vec![
12435 BracketPair {
12436 start: "{".to_string(),
12437 end: "}".to_string(),
12438 close: true,
12439 surround: true,
12440 newline: true,
12441 },
12442 BracketPair {
12443 start: "(".to_string(),
12444 end: ")".to_string(),
12445 close: true,
12446 surround: true,
12447 newline: true,
12448 },
12449 BracketPair {
12450 start: "/*".to_string(),
12451 end: " */".to_string(),
12452 close: true,
12453 surround: true,
12454 newline: true,
12455 },
12456 BracketPair {
12457 start: "[".to_string(),
12458 end: "]".to_string(),
12459 close: false,
12460 surround: false,
12461 newline: true,
12462 },
12463 BracketPair {
12464 start: "\"".to_string(),
12465 end: "\"".to_string(),
12466 close: true,
12467 surround: true,
12468 newline: false,
12469 },
12470 BracketPair {
12471 start: "<".to_string(),
12472 end: ">".to_string(),
12473 close: false,
12474 surround: true,
12475 newline: true,
12476 },
12477 ],
12478 ..Default::default()
12479 },
12480 autoclose_before: "})]".to_string(),
12481 ..Default::default()
12482 },
12483 Some(tree_sitter_rust::LANGUAGE.into()),
12484 );
12485 let language = Arc::new(language);
12486
12487 cx.language_registry().add(language.clone());
12488 cx.update_buffer(|buffer, cx| {
12489 buffer.set_language(Some(language), cx);
12490 });
12491
12492 cx.set_state(
12493 &r#"
12494 fn main() {
12495 sampleˇ
12496 }
12497 "#
12498 .unindent(),
12499 );
12500
12501 cx.update_editor(|editor, window, cx| {
12502 editor.handle_input("(", window, cx);
12503 });
12504 cx.assert_editor_state(
12505 &"
12506 fn main() {
12507 sample(ˇ)
12508 }
12509 "
12510 .unindent(),
12511 );
12512
12513 let mocked_response = lsp::SignatureHelp {
12514 signatures: vec![lsp::SignatureInformation {
12515 label: "fn sample(param1: u8, param2: u8)".to_string(),
12516 documentation: None,
12517 parameters: Some(vec![
12518 lsp::ParameterInformation {
12519 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12520 documentation: None,
12521 },
12522 lsp::ParameterInformation {
12523 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12524 documentation: None,
12525 },
12526 ]),
12527 active_parameter: None,
12528 }],
12529 active_signature: Some(0),
12530 active_parameter: Some(0),
12531 };
12532 handle_signature_help_request(&mut cx, mocked_response).await;
12533
12534 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12535 .await;
12536
12537 cx.editor(|editor, _, _| {
12538 let signature_help_state = editor.signature_help_state.popover().cloned();
12539 let signature = signature_help_state.unwrap();
12540 assert_eq!(
12541 signature.signatures[signature.current_signature].label,
12542 "fn sample(param1: u8, param2: u8)"
12543 );
12544 });
12545}
12546
12547#[gpui::test]
12548async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
12549 init_test(cx, |_| {});
12550
12551 cx.update(|cx| {
12552 cx.update_global::<SettingsStore, _>(|settings, cx| {
12553 settings.update_user_settings(cx, |settings| {
12554 settings.editor.auto_signature_help = Some(false);
12555 settings.editor.show_signature_help_after_edits = Some(false);
12556 });
12557 });
12558 });
12559
12560 let mut cx = EditorLspTestContext::new_rust(
12561 lsp::ServerCapabilities {
12562 signature_help_provider: Some(lsp::SignatureHelpOptions {
12563 ..Default::default()
12564 }),
12565 ..Default::default()
12566 },
12567 cx,
12568 )
12569 .await;
12570
12571 let language = Language::new(
12572 LanguageConfig {
12573 name: "Rust".into(),
12574 brackets: BracketPairConfig {
12575 pairs: vec![
12576 BracketPair {
12577 start: "{".to_string(),
12578 end: "}".to_string(),
12579 close: true,
12580 surround: true,
12581 newline: true,
12582 },
12583 BracketPair {
12584 start: "(".to_string(),
12585 end: ")".to_string(),
12586 close: true,
12587 surround: true,
12588 newline: true,
12589 },
12590 BracketPair {
12591 start: "/*".to_string(),
12592 end: " */".to_string(),
12593 close: true,
12594 surround: true,
12595 newline: true,
12596 },
12597 BracketPair {
12598 start: "[".to_string(),
12599 end: "]".to_string(),
12600 close: false,
12601 surround: false,
12602 newline: true,
12603 },
12604 BracketPair {
12605 start: "\"".to_string(),
12606 end: "\"".to_string(),
12607 close: true,
12608 surround: true,
12609 newline: false,
12610 },
12611 BracketPair {
12612 start: "<".to_string(),
12613 end: ">".to_string(),
12614 close: false,
12615 surround: true,
12616 newline: true,
12617 },
12618 ],
12619 ..Default::default()
12620 },
12621 autoclose_before: "})]".to_string(),
12622 ..Default::default()
12623 },
12624 Some(tree_sitter_rust::LANGUAGE.into()),
12625 );
12626 let language = Arc::new(language);
12627
12628 cx.language_registry().add(language.clone());
12629 cx.update_buffer(|buffer, cx| {
12630 buffer.set_language(Some(language), cx);
12631 });
12632
12633 // Ensure that signature_help is not called when no signature help is enabled.
12634 cx.set_state(
12635 &r#"
12636 fn main() {
12637 sampleˇ
12638 }
12639 "#
12640 .unindent(),
12641 );
12642 cx.update_editor(|editor, window, cx| {
12643 editor.handle_input("(", window, cx);
12644 });
12645 cx.assert_editor_state(
12646 &"
12647 fn main() {
12648 sample(ˇ)
12649 }
12650 "
12651 .unindent(),
12652 );
12653 cx.editor(|editor, _, _| {
12654 assert!(editor.signature_help_state.task().is_none());
12655 });
12656
12657 let mocked_response = lsp::SignatureHelp {
12658 signatures: vec![lsp::SignatureInformation {
12659 label: "fn sample(param1: u8, param2: u8)".to_string(),
12660 documentation: None,
12661 parameters: Some(vec![
12662 lsp::ParameterInformation {
12663 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12664 documentation: None,
12665 },
12666 lsp::ParameterInformation {
12667 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12668 documentation: None,
12669 },
12670 ]),
12671 active_parameter: None,
12672 }],
12673 active_signature: Some(0),
12674 active_parameter: Some(0),
12675 };
12676
12677 // Ensure that signature_help is called when enabled afte edits
12678 cx.update(|_, cx| {
12679 cx.update_global::<SettingsStore, _>(|settings, cx| {
12680 settings.update_user_settings(cx, |settings| {
12681 settings.editor.auto_signature_help = Some(false);
12682 settings.editor.show_signature_help_after_edits = Some(true);
12683 });
12684 });
12685 });
12686 cx.set_state(
12687 &r#"
12688 fn main() {
12689 sampleˇ
12690 }
12691 "#
12692 .unindent(),
12693 );
12694 cx.update_editor(|editor, window, cx| {
12695 editor.handle_input("(", window, cx);
12696 });
12697 cx.assert_editor_state(
12698 &"
12699 fn main() {
12700 sample(ˇ)
12701 }
12702 "
12703 .unindent(),
12704 );
12705 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12706 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12707 .await;
12708 cx.update_editor(|editor, _, _| {
12709 let signature_help_state = editor.signature_help_state.popover().cloned();
12710 assert!(signature_help_state.is_some());
12711 let signature = signature_help_state.unwrap();
12712 assert_eq!(
12713 signature.signatures[signature.current_signature].label,
12714 "fn sample(param1: u8, param2: u8)"
12715 );
12716 editor.signature_help_state = SignatureHelpState::default();
12717 });
12718
12719 // Ensure that signature_help is called when auto signature help override is enabled
12720 cx.update(|_, cx| {
12721 cx.update_global::<SettingsStore, _>(|settings, cx| {
12722 settings.update_user_settings(cx, |settings| {
12723 settings.editor.auto_signature_help = Some(true);
12724 settings.editor.show_signature_help_after_edits = Some(false);
12725 });
12726 });
12727 });
12728 cx.set_state(
12729 &r#"
12730 fn main() {
12731 sampleˇ
12732 }
12733 "#
12734 .unindent(),
12735 );
12736 cx.update_editor(|editor, window, cx| {
12737 editor.handle_input("(", window, cx);
12738 });
12739 cx.assert_editor_state(
12740 &"
12741 fn main() {
12742 sample(ˇ)
12743 }
12744 "
12745 .unindent(),
12746 );
12747 handle_signature_help_request(&mut cx, mocked_response).await;
12748 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12749 .await;
12750 cx.editor(|editor, _, _| {
12751 let signature_help_state = editor.signature_help_state.popover().cloned();
12752 assert!(signature_help_state.is_some());
12753 let signature = signature_help_state.unwrap();
12754 assert_eq!(
12755 signature.signatures[signature.current_signature].label,
12756 "fn sample(param1: u8, param2: u8)"
12757 );
12758 });
12759}
12760
12761#[gpui::test]
12762async fn test_signature_help(cx: &mut TestAppContext) {
12763 init_test(cx, |_| {});
12764 cx.update(|cx| {
12765 cx.update_global::<SettingsStore, _>(|settings, cx| {
12766 settings.update_user_settings(cx, |settings| {
12767 settings.editor.auto_signature_help = Some(true);
12768 });
12769 });
12770 });
12771
12772 let mut cx = EditorLspTestContext::new_rust(
12773 lsp::ServerCapabilities {
12774 signature_help_provider: Some(lsp::SignatureHelpOptions {
12775 ..Default::default()
12776 }),
12777 ..Default::default()
12778 },
12779 cx,
12780 )
12781 .await;
12782
12783 // A test that directly calls `show_signature_help`
12784 cx.update_editor(|editor, window, cx| {
12785 editor.show_signature_help(&ShowSignatureHelp, window, cx);
12786 });
12787
12788 let mocked_response = lsp::SignatureHelp {
12789 signatures: vec![lsp::SignatureInformation {
12790 label: "fn sample(param1: u8, param2: u8)".to_string(),
12791 documentation: None,
12792 parameters: Some(vec![
12793 lsp::ParameterInformation {
12794 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12795 documentation: None,
12796 },
12797 lsp::ParameterInformation {
12798 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12799 documentation: None,
12800 },
12801 ]),
12802 active_parameter: None,
12803 }],
12804 active_signature: Some(0),
12805 active_parameter: Some(0),
12806 };
12807 handle_signature_help_request(&mut cx, mocked_response).await;
12808
12809 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12810 .await;
12811
12812 cx.editor(|editor, _, _| {
12813 let signature_help_state = editor.signature_help_state.popover().cloned();
12814 assert!(signature_help_state.is_some());
12815 let signature = signature_help_state.unwrap();
12816 assert_eq!(
12817 signature.signatures[signature.current_signature].label,
12818 "fn sample(param1: u8, param2: u8)"
12819 );
12820 });
12821
12822 // When exiting outside from inside the brackets, `signature_help` is closed.
12823 cx.set_state(indoc! {"
12824 fn main() {
12825 sample(ˇ);
12826 }
12827
12828 fn sample(param1: u8, param2: u8) {}
12829 "});
12830
12831 cx.update_editor(|editor, window, cx| {
12832 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12833 s.select_ranges([0..0])
12834 });
12835 });
12836
12837 let mocked_response = lsp::SignatureHelp {
12838 signatures: Vec::new(),
12839 active_signature: None,
12840 active_parameter: None,
12841 };
12842 handle_signature_help_request(&mut cx, mocked_response).await;
12843
12844 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
12845 .await;
12846
12847 cx.editor(|editor, _, _| {
12848 assert!(!editor.signature_help_state.is_shown());
12849 });
12850
12851 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
12852 cx.set_state(indoc! {"
12853 fn main() {
12854 sample(ˇ);
12855 }
12856
12857 fn sample(param1: u8, param2: u8) {}
12858 "});
12859
12860 let mocked_response = lsp::SignatureHelp {
12861 signatures: vec![lsp::SignatureInformation {
12862 label: "fn sample(param1: u8, param2: u8)".to_string(),
12863 documentation: None,
12864 parameters: Some(vec![
12865 lsp::ParameterInformation {
12866 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12867 documentation: None,
12868 },
12869 lsp::ParameterInformation {
12870 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12871 documentation: None,
12872 },
12873 ]),
12874 active_parameter: None,
12875 }],
12876 active_signature: Some(0),
12877 active_parameter: Some(0),
12878 };
12879 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12880 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12881 .await;
12882 cx.editor(|editor, _, _| {
12883 assert!(editor.signature_help_state.is_shown());
12884 });
12885
12886 // Restore the popover with more parameter input
12887 cx.set_state(indoc! {"
12888 fn main() {
12889 sample(param1, param2ˇ);
12890 }
12891
12892 fn sample(param1: u8, param2: u8) {}
12893 "});
12894
12895 let mocked_response = lsp::SignatureHelp {
12896 signatures: vec![lsp::SignatureInformation {
12897 label: "fn sample(param1: u8, param2: u8)".to_string(),
12898 documentation: None,
12899 parameters: Some(vec![
12900 lsp::ParameterInformation {
12901 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12902 documentation: None,
12903 },
12904 lsp::ParameterInformation {
12905 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12906 documentation: None,
12907 },
12908 ]),
12909 active_parameter: None,
12910 }],
12911 active_signature: Some(0),
12912 active_parameter: Some(1),
12913 };
12914 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12915 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12916 .await;
12917
12918 // When selecting a range, the popover is gone.
12919 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
12920 cx.update_editor(|editor, window, cx| {
12921 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12922 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
12923 })
12924 });
12925 cx.assert_editor_state(indoc! {"
12926 fn main() {
12927 sample(param1, «ˇparam2»);
12928 }
12929
12930 fn sample(param1: u8, param2: u8) {}
12931 "});
12932 cx.editor(|editor, _, _| {
12933 assert!(!editor.signature_help_state.is_shown());
12934 });
12935
12936 // When unselecting again, the popover is back if within the brackets.
12937 cx.update_editor(|editor, window, cx| {
12938 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12939 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
12940 })
12941 });
12942 cx.assert_editor_state(indoc! {"
12943 fn main() {
12944 sample(param1, ˇparam2);
12945 }
12946
12947 fn sample(param1: u8, param2: u8) {}
12948 "});
12949 handle_signature_help_request(&mut cx, mocked_response).await;
12950 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12951 .await;
12952 cx.editor(|editor, _, _| {
12953 assert!(editor.signature_help_state.is_shown());
12954 });
12955
12956 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
12957 cx.update_editor(|editor, window, cx| {
12958 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12959 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
12960 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
12961 })
12962 });
12963 cx.assert_editor_state(indoc! {"
12964 fn main() {
12965 sample(param1, ˇparam2);
12966 }
12967
12968 fn sample(param1: u8, param2: u8) {}
12969 "});
12970
12971 let mocked_response = lsp::SignatureHelp {
12972 signatures: vec![lsp::SignatureInformation {
12973 label: "fn sample(param1: u8, param2: u8)".to_string(),
12974 documentation: None,
12975 parameters: Some(vec![
12976 lsp::ParameterInformation {
12977 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12978 documentation: None,
12979 },
12980 lsp::ParameterInformation {
12981 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12982 documentation: None,
12983 },
12984 ]),
12985 active_parameter: None,
12986 }],
12987 active_signature: Some(0),
12988 active_parameter: Some(1),
12989 };
12990 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12991 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12992 .await;
12993 cx.update_editor(|editor, _, cx| {
12994 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
12995 });
12996 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
12997 .await;
12998 cx.update_editor(|editor, window, cx| {
12999 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13000 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13001 })
13002 });
13003 cx.assert_editor_state(indoc! {"
13004 fn main() {
13005 sample(param1, «ˇparam2»);
13006 }
13007
13008 fn sample(param1: u8, param2: u8) {}
13009 "});
13010 cx.update_editor(|editor, window, cx| {
13011 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13012 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13013 })
13014 });
13015 cx.assert_editor_state(indoc! {"
13016 fn main() {
13017 sample(param1, ˇparam2);
13018 }
13019
13020 fn sample(param1: u8, param2: u8) {}
13021 "});
13022 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
13023 .await;
13024}
13025
13026#[gpui::test]
13027async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
13028 init_test(cx, |_| {});
13029
13030 let mut cx = EditorLspTestContext::new_rust(
13031 lsp::ServerCapabilities {
13032 signature_help_provider: Some(lsp::SignatureHelpOptions {
13033 ..Default::default()
13034 }),
13035 ..Default::default()
13036 },
13037 cx,
13038 )
13039 .await;
13040
13041 cx.set_state(indoc! {"
13042 fn main() {
13043 overloadedˇ
13044 }
13045 "});
13046
13047 cx.update_editor(|editor, window, cx| {
13048 editor.handle_input("(", window, cx);
13049 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13050 });
13051
13052 // Mock response with 3 signatures
13053 let mocked_response = lsp::SignatureHelp {
13054 signatures: vec![
13055 lsp::SignatureInformation {
13056 label: "fn overloaded(x: i32)".to_string(),
13057 documentation: None,
13058 parameters: Some(vec![lsp::ParameterInformation {
13059 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13060 documentation: None,
13061 }]),
13062 active_parameter: None,
13063 },
13064 lsp::SignatureInformation {
13065 label: "fn overloaded(x: i32, y: i32)".to_string(),
13066 documentation: None,
13067 parameters: Some(vec![
13068 lsp::ParameterInformation {
13069 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13070 documentation: None,
13071 },
13072 lsp::ParameterInformation {
13073 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13074 documentation: None,
13075 },
13076 ]),
13077 active_parameter: None,
13078 },
13079 lsp::SignatureInformation {
13080 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
13081 documentation: None,
13082 parameters: Some(vec![
13083 lsp::ParameterInformation {
13084 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13085 documentation: None,
13086 },
13087 lsp::ParameterInformation {
13088 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13089 documentation: None,
13090 },
13091 lsp::ParameterInformation {
13092 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
13093 documentation: None,
13094 },
13095 ]),
13096 active_parameter: None,
13097 },
13098 ],
13099 active_signature: Some(1),
13100 active_parameter: Some(0),
13101 };
13102 handle_signature_help_request(&mut cx, mocked_response).await;
13103
13104 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13105 .await;
13106
13107 // Verify we have multiple signatures and the right one is selected
13108 cx.editor(|editor, _, _| {
13109 let popover = editor.signature_help_state.popover().cloned().unwrap();
13110 assert_eq!(popover.signatures.len(), 3);
13111 // active_signature was 1, so that should be the current
13112 assert_eq!(popover.current_signature, 1);
13113 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
13114 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
13115 assert_eq!(
13116 popover.signatures[2].label,
13117 "fn overloaded(x: i32, y: i32, z: i32)"
13118 );
13119 });
13120
13121 // Test navigation functionality
13122 cx.update_editor(|editor, window, cx| {
13123 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13124 });
13125
13126 cx.editor(|editor, _, _| {
13127 let popover = editor.signature_help_state.popover().cloned().unwrap();
13128 assert_eq!(popover.current_signature, 2);
13129 });
13130
13131 // Test wrap around
13132 cx.update_editor(|editor, window, cx| {
13133 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13134 });
13135
13136 cx.editor(|editor, _, _| {
13137 let popover = editor.signature_help_state.popover().cloned().unwrap();
13138 assert_eq!(popover.current_signature, 0);
13139 });
13140
13141 // Test previous navigation
13142 cx.update_editor(|editor, window, cx| {
13143 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
13144 });
13145
13146 cx.editor(|editor, _, _| {
13147 let popover = editor.signature_help_state.popover().cloned().unwrap();
13148 assert_eq!(popover.current_signature, 2);
13149 });
13150}
13151
13152#[gpui::test]
13153async fn test_completion_mode(cx: &mut TestAppContext) {
13154 init_test(cx, |_| {});
13155 let mut cx = EditorLspTestContext::new_rust(
13156 lsp::ServerCapabilities {
13157 completion_provider: Some(lsp::CompletionOptions {
13158 resolve_provider: Some(true),
13159 ..Default::default()
13160 }),
13161 ..Default::default()
13162 },
13163 cx,
13164 )
13165 .await;
13166
13167 struct Run {
13168 run_description: &'static str,
13169 initial_state: String,
13170 buffer_marked_text: String,
13171 completion_label: &'static str,
13172 completion_text: &'static str,
13173 expected_with_insert_mode: String,
13174 expected_with_replace_mode: String,
13175 expected_with_replace_subsequence_mode: String,
13176 expected_with_replace_suffix_mode: String,
13177 }
13178
13179 let runs = [
13180 Run {
13181 run_description: "Start of word matches completion text",
13182 initial_state: "before ediˇ after".into(),
13183 buffer_marked_text: "before <edi|> after".into(),
13184 completion_label: "editor",
13185 completion_text: "editor",
13186 expected_with_insert_mode: "before editorˇ after".into(),
13187 expected_with_replace_mode: "before editorˇ after".into(),
13188 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13189 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13190 },
13191 Run {
13192 run_description: "Accept same text at the middle of the word",
13193 initial_state: "before ediˇtor after".into(),
13194 buffer_marked_text: "before <edi|tor> after".into(),
13195 completion_label: "editor",
13196 completion_text: "editor",
13197 expected_with_insert_mode: "before editorˇtor after".into(),
13198 expected_with_replace_mode: "before editorˇ after".into(),
13199 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13200 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13201 },
13202 Run {
13203 run_description: "End of word matches completion text -- cursor at end",
13204 initial_state: "before torˇ after".into(),
13205 buffer_marked_text: "before <tor|> after".into(),
13206 completion_label: "editor",
13207 completion_text: "editor",
13208 expected_with_insert_mode: "before editorˇ after".into(),
13209 expected_with_replace_mode: "before editorˇ after".into(),
13210 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13211 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13212 },
13213 Run {
13214 run_description: "End of word matches completion text -- cursor at start",
13215 initial_state: "before ˇtor after".into(),
13216 buffer_marked_text: "before <|tor> after".into(),
13217 completion_label: "editor",
13218 completion_text: "editor",
13219 expected_with_insert_mode: "before editorˇtor after".into(),
13220 expected_with_replace_mode: "before editorˇ after".into(),
13221 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13222 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13223 },
13224 Run {
13225 run_description: "Prepend text containing whitespace",
13226 initial_state: "pˇfield: bool".into(),
13227 buffer_marked_text: "<p|field>: bool".into(),
13228 completion_label: "pub ",
13229 completion_text: "pub ",
13230 expected_with_insert_mode: "pub ˇfield: bool".into(),
13231 expected_with_replace_mode: "pub ˇ: bool".into(),
13232 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
13233 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
13234 },
13235 Run {
13236 run_description: "Add element to start of list",
13237 initial_state: "[element_ˇelement_2]".into(),
13238 buffer_marked_text: "[<element_|element_2>]".into(),
13239 completion_label: "element_1",
13240 completion_text: "element_1",
13241 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
13242 expected_with_replace_mode: "[element_1ˇ]".into(),
13243 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
13244 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
13245 },
13246 Run {
13247 run_description: "Add element to start of list -- first and second elements are equal",
13248 initial_state: "[elˇelement]".into(),
13249 buffer_marked_text: "[<el|element>]".into(),
13250 completion_label: "element",
13251 completion_text: "element",
13252 expected_with_insert_mode: "[elementˇelement]".into(),
13253 expected_with_replace_mode: "[elementˇ]".into(),
13254 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
13255 expected_with_replace_suffix_mode: "[elementˇ]".into(),
13256 },
13257 Run {
13258 run_description: "Ends with matching suffix",
13259 initial_state: "SubˇError".into(),
13260 buffer_marked_text: "<Sub|Error>".into(),
13261 completion_label: "SubscriptionError",
13262 completion_text: "SubscriptionError",
13263 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
13264 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13265 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13266 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
13267 },
13268 Run {
13269 run_description: "Suffix is a subsequence -- contiguous",
13270 initial_state: "SubˇErr".into(),
13271 buffer_marked_text: "<Sub|Err>".into(),
13272 completion_label: "SubscriptionError",
13273 completion_text: "SubscriptionError",
13274 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
13275 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13276 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13277 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
13278 },
13279 Run {
13280 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
13281 initial_state: "Suˇscrirr".into(),
13282 buffer_marked_text: "<Su|scrirr>".into(),
13283 completion_label: "SubscriptionError",
13284 completion_text: "SubscriptionError",
13285 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
13286 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13287 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13288 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
13289 },
13290 Run {
13291 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
13292 initial_state: "foo(indˇix)".into(),
13293 buffer_marked_text: "foo(<ind|ix>)".into(),
13294 completion_label: "node_index",
13295 completion_text: "node_index",
13296 expected_with_insert_mode: "foo(node_indexˇix)".into(),
13297 expected_with_replace_mode: "foo(node_indexˇ)".into(),
13298 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
13299 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
13300 },
13301 Run {
13302 run_description: "Replace range ends before cursor - should extend to cursor",
13303 initial_state: "before editˇo after".into(),
13304 buffer_marked_text: "before <{ed}>it|o after".into(),
13305 completion_label: "editor",
13306 completion_text: "editor",
13307 expected_with_insert_mode: "before editorˇo after".into(),
13308 expected_with_replace_mode: "before editorˇo after".into(),
13309 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
13310 expected_with_replace_suffix_mode: "before editorˇo after".into(),
13311 },
13312 Run {
13313 run_description: "Uses label for suffix matching",
13314 initial_state: "before ediˇtor after".into(),
13315 buffer_marked_text: "before <edi|tor> after".into(),
13316 completion_label: "editor",
13317 completion_text: "editor()",
13318 expected_with_insert_mode: "before editor()ˇtor after".into(),
13319 expected_with_replace_mode: "before editor()ˇ after".into(),
13320 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
13321 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
13322 },
13323 Run {
13324 run_description: "Case insensitive subsequence and suffix matching",
13325 initial_state: "before EDiˇtoR after".into(),
13326 buffer_marked_text: "before <EDi|toR> after".into(),
13327 completion_label: "editor",
13328 completion_text: "editor",
13329 expected_with_insert_mode: "before editorˇtoR after".into(),
13330 expected_with_replace_mode: "before editorˇ after".into(),
13331 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13332 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13333 },
13334 ];
13335
13336 for run in runs {
13337 let run_variations = [
13338 (LspInsertMode::Insert, run.expected_with_insert_mode),
13339 (LspInsertMode::Replace, run.expected_with_replace_mode),
13340 (
13341 LspInsertMode::ReplaceSubsequence,
13342 run.expected_with_replace_subsequence_mode,
13343 ),
13344 (
13345 LspInsertMode::ReplaceSuffix,
13346 run.expected_with_replace_suffix_mode,
13347 ),
13348 ];
13349
13350 for (lsp_insert_mode, expected_text) in run_variations {
13351 eprintln!(
13352 "run = {:?}, mode = {lsp_insert_mode:.?}",
13353 run.run_description,
13354 );
13355
13356 update_test_language_settings(&mut cx, |settings| {
13357 settings.defaults.completions = Some(CompletionSettingsContent {
13358 lsp_insert_mode: Some(lsp_insert_mode),
13359 words: Some(WordsCompletionMode::Disabled),
13360 words_min_length: Some(0),
13361 ..Default::default()
13362 });
13363 });
13364
13365 cx.set_state(&run.initial_state);
13366 cx.update_editor(|editor, window, cx| {
13367 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13368 });
13369
13370 let counter = Arc::new(AtomicUsize::new(0));
13371 handle_completion_request_with_insert_and_replace(
13372 &mut cx,
13373 &run.buffer_marked_text,
13374 vec![(run.completion_label, run.completion_text)],
13375 counter.clone(),
13376 )
13377 .await;
13378 cx.condition(|editor, _| editor.context_menu_visible())
13379 .await;
13380 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13381
13382 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13383 editor
13384 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13385 .unwrap()
13386 });
13387 cx.assert_editor_state(&expected_text);
13388 handle_resolve_completion_request(&mut cx, None).await;
13389 apply_additional_edits.await.unwrap();
13390 }
13391 }
13392}
13393
13394#[gpui::test]
13395async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
13396 init_test(cx, |_| {});
13397 let mut cx = EditorLspTestContext::new_rust(
13398 lsp::ServerCapabilities {
13399 completion_provider: Some(lsp::CompletionOptions {
13400 resolve_provider: Some(true),
13401 ..Default::default()
13402 }),
13403 ..Default::default()
13404 },
13405 cx,
13406 )
13407 .await;
13408
13409 let initial_state = "SubˇError";
13410 let buffer_marked_text = "<Sub|Error>";
13411 let completion_text = "SubscriptionError";
13412 let expected_with_insert_mode = "SubscriptionErrorˇError";
13413 let expected_with_replace_mode = "SubscriptionErrorˇ";
13414
13415 update_test_language_settings(&mut cx, |settings| {
13416 settings.defaults.completions = Some(CompletionSettingsContent {
13417 words: Some(WordsCompletionMode::Disabled),
13418 words_min_length: Some(0),
13419 // set the opposite here to ensure that the action is overriding the default behavior
13420 lsp_insert_mode: Some(LspInsertMode::Insert),
13421 ..Default::default()
13422 });
13423 });
13424
13425 cx.set_state(initial_state);
13426 cx.update_editor(|editor, window, cx| {
13427 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13428 });
13429
13430 let counter = Arc::new(AtomicUsize::new(0));
13431 handle_completion_request_with_insert_and_replace(
13432 &mut cx,
13433 buffer_marked_text,
13434 vec![(completion_text, completion_text)],
13435 counter.clone(),
13436 )
13437 .await;
13438 cx.condition(|editor, _| editor.context_menu_visible())
13439 .await;
13440 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13441
13442 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13443 editor
13444 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13445 .unwrap()
13446 });
13447 cx.assert_editor_state(expected_with_replace_mode);
13448 handle_resolve_completion_request(&mut cx, None).await;
13449 apply_additional_edits.await.unwrap();
13450
13451 update_test_language_settings(&mut cx, |settings| {
13452 settings.defaults.completions = Some(CompletionSettingsContent {
13453 words: Some(WordsCompletionMode::Disabled),
13454 words_min_length: Some(0),
13455 // set the opposite here to ensure that the action is overriding the default behavior
13456 lsp_insert_mode: Some(LspInsertMode::Replace),
13457 ..Default::default()
13458 });
13459 });
13460
13461 cx.set_state(initial_state);
13462 cx.update_editor(|editor, window, cx| {
13463 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13464 });
13465 handle_completion_request_with_insert_and_replace(
13466 &mut cx,
13467 buffer_marked_text,
13468 vec![(completion_text, completion_text)],
13469 counter.clone(),
13470 )
13471 .await;
13472 cx.condition(|editor, _| editor.context_menu_visible())
13473 .await;
13474 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
13475
13476 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13477 editor
13478 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
13479 .unwrap()
13480 });
13481 cx.assert_editor_state(expected_with_insert_mode);
13482 handle_resolve_completion_request(&mut cx, None).await;
13483 apply_additional_edits.await.unwrap();
13484}
13485
13486#[gpui::test]
13487async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
13488 init_test(cx, |_| {});
13489 let mut cx = EditorLspTestContext::new_rust(
13490 lsp::ServerCapabilities {
13491 completion_provider: Some(lsp::CompletionOptions {
13492 resolve_provider: Some(true),
13493 ..Default::default()
13494 }),
13495 ..Default::default()
13496 },
13497 cx,
13498 )
13499 .await;
13500
13501 // scenario: surrounding text matches completion text
13502 let completion_text = "to_offset";
13503 let initial_state = indoc! {"
13504 1. buf.to_offˇsuffix
13505 2. buf.to_offˇsuf
13506 3. buf.to_offˇfix
13507 4. buf.to_offˇ
13508 5. into_offˇensive
13509 6. ˇsuffix
13510 7. let ˇ //
13511 8. aaˇzz
13512 9. buf.to_off«zzzzzˇ»suffix
13513 10. buf.«ˇzzzzz»suffix
13514 11. to_off«ˇzzzzz»
13515
13516 buf.to_offˇsuffix // newest cursor
13517 "};
13518 let completion_marked_buffer = indoc! {"
13519 1. buf.to_offsuffix
13520 2. buf.to_offsuf
13521 3. buf.to_offfix
13522 4. buf.to_off
13523 5. into_offensive
13524 6. suffix
13525 7. let //
13526 8. aazz
13527 9. buf.to_offzzzzzsuffix
13528 10. buf.zzzzzsuffix
13529 11. to_offzzzzz
13530
13531 buf.<to_off|suffix> // newest cursor
13532 "};
13533 let expected = indoc! {"
13534 1. buf.to_offsetˇ
13535 2. buf.to_offsetˇsuf
13536 3. buf.to_offsetˇfix
13537 4. buf.to_offsetˇ
13538 5. into_offsetˇensive
13539 6. to_offsetˇsuffix
13540 7. let to_offsetˇ //
13541 8. aato_offsetˇzz
13542 9. buf.to_offsetˇ
13543 10. buf.to_offsetˇsuffix
13544 11. to_offsetˇ
13545
13546 buf.to_offsetˇ // newest cursor
13547 "};
13548 cx.set_state(initial_state);
13549 cx.update_editor(|editor, window, cx| {
13550 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13551 });
13552 handle_completion_request_with_insert_and_replace(
13553 &mut cx,
13554 completion_marked_buffer,
13555 vec![(completion_text, completion_text)],
13556 Arc::new(AtomicUsize::new(0)),
13557 )
13558 .await;
13559 cx.condition(|editor, _| editor.context_menu_visible())
13560 .await;
13561 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13562 editor
13563 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13564 .unwrap()
13565 });
13566 cx.assert_editor_state(expected);
13567 handle_resolve_completion_request(&mut cx, None).await;
13568 apply_additional_edits.await.unwrap();
13569
13570 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
13571 let completion_text = "foo_and_bar";
13572 let initial_state = indoc! {"
13573 1. ooanbˇ
13574 2. zooanbˇ
13575 3. ooanbˇz
13576 4. zooanbˇz
13577 5. ooanˇ
13578 6. oanbˇ
13579
13580 ooanbˇ
13581 "};
13582 let completion_marked_buffer = indoc! {"
13583 1. ooanb
13584 2. zooanb
13585 3. ooanbz
13586 4. zooanbz
13587 5. ooan
13588 6. oanb
13589
13590 <ooanb|>
13591 "};
13592 let expected = indoc! {"
13593 1. foo_and_barˇ
13594 2. zfoo_and_barˇ
13595 3. foo_and_barˇz
13596 4. zfoo_and_barˇz
13597 5. ooanfoo_and_barˇ
13598 6. oanbfoo_and_barˇ
13599
13600 foo_and_barˇ
13601 "};
13602 cx.set_state(initial_state);
13603 cx.update_editor(|editor, window, cx| {
13604 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13605 });
13606 handle_completion_request_with_insert_and_replace(
13607 &mut cx,
13608 completion_marked_buffer,
13609 vec![(completion_text, completion_text)],
13610 Arc::new(AtomicUsize::new(0)),
13611 )
13612 .await;
13613 cx.condition(|editor, _| editor.context_menu_visible())
13614 .await;
13615 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13616 editor
13617 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13618 .unwrap()
13619 });
13620 cx.assert_editor_state(expected);
13621 handle_resolve_completion_request(&mut cx, None).await;
13622 apply_additional_edits.await.unwrap();
13623
13624 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
13625 // (expects the same as if it was inserted at the end)
13626 let completion_text = "foo_and_bar";
13627 let initial_state = indoc! {"
13628 1. ooˇanb
13629 2. zooˇanb
13630 3. ooˇanbz
13631 4. zooˇanbz
13632
13633 ooˇanb
13634 "};
13635 let completion_marked_buffer = indoc! {"
13636 1. ooanb
13637 2. zooanb
13638 3. ooanbz
13639 4. zooanbz
13640
13641 <oo|anb>
13642 "};
13643 let expected = indoc! {"
13644 1. foo_and_barˇ
13645 2. zfoo_and_barˇ
13646 3. foo_and_barˇz
13647 4. zfoo_and_barˇz
13648
13649 foo_and_barˇ
13650 "};
13651 cx.set_state(initial_state);
13652 cx.update_editor(|editor, window, cx| {
13653 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13654 });
13655 handle_completion_request_with_insert_and_replace(
13656 &mut cx,
13657 completion_marked_buffer,
13658 vec![(completion_text, completion_text)],
13659 Arc::new(AtomicUsize::new(0)),
13660 )
13661 .await;
13662 cx.condition(|editor, _| editor.context_menu_visible())
13663 .await;
13664 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13665 editor
13666 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13667 .unwrap()
13668 });
13669 cx.assert_editor_state(expected);
13670 handle_resolve_completion_request(&mut cx, None).await;
13671 apply_additional_edits.await.unwrap();
13672}
13673
13674// This used to crash
13675#[gpui::test]
13676async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
13677 init_test(cx, |_| {});
13678
13679 let buffer_text = indoc! {"
13680 fn main() {
13681 10.satu;
13682
13683 //
13684 // separate cursors so they open in different excerpts (manually reproducible)
13685 //
13686
13687 10.satu20;
13688 }
13689 "};
13690 let multibuffer_text_with_selections = indoc! {"
13691 fn main() {
13692 10.satuˇ;
13693
13694 //
13695
13696 //
13697
13698 10.satuˇ20;
13699 }
13700 "};
13701 let expected_multibuffer = indoc! {"
13702 fn main() {
13703 10.saturating_sub()ˇ;
13704
13705 //
13706
13707 //
13708
13709 10.saturating_sub()ˇ;
13710 }
13711 "};
13712
13713 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
13714 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
13715
13716 let fs = FakeFs::new(cx.executor());
13717 fs.insert_tree(
13718 path!("/a"),
13719 json!({
13720 "main.rs": buffer_text,
13721 }),
13722 )
13723 .await;
13724
13725 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13726 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13727 language_registry.add(rust_lang());
13728 let mut fake_servers = language_registry.register_fake_lsp(
13729 "Rust",
13730 FakeLspAdapter {
13731 capabilities: lsp::ServerCapabilities {
13732 completion_provider: Some(lsp::CompletionOptions {
13733 resolve_provider: None,
13734 ..lsp::CompletionOptions::default()
13735 }),
13736 ..lsp::ServerCapabilities::default()
13737 },
13738 ..FakeLspAdapter::default()
13739 },
13740 );
13741 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13742 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13743 let buffer = project
13744 .update(cx, |project, cx| {
13745 project.open_local_buffer(path!("/a/main.rs"), cx)
13746 })
13747 .await
13748 .unwrap();
13749
13750 let multi_buffer = cx.new(|cx| {
13751 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
13752 multi_buffer.push_excerpts(
13753 buffer.clone(),
13754 [ExcerptRange::new(0..first_excerpt_end)],
13755 cx,
13756 );
13757 multi_buffer.push_excerpts(
13758 buffer.clone(),
13759 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
13760 cx,
13761 );
13762 multi_buffer
13763 });
13764
13765 let editor = workspace
13766 .update(cx, |_, window, cx| {
13767 cx.new(|cx| {
13768 Editor::new(
13769 EditorMode::Full {
13770 scale_ui_elements_with_buffer_font_size: false,
13771 show_active_line_background: false,
13772 sized_by_content: false,
13773 },
13774 multi_buffer.clone(),
13775 Some(project.clone()),
13776 window,
13777 cx,
13778 )
13779 })
13780 })
13781 .unwrap();
13782
13783 let pane = workspace
13784 .update(cx, |workspace, _, _| workspace.active_pane().clone())
13785 .unwrap();
13786 pane.update_in(cx, |pane, window, cx| {
13787 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
13788 });
13789
13790 let fake_server = fake_servers.next().await.unwrap();
13791
13792 editor.update_in(cx, |editor, window, cx| {
13793 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13794 s.select_ranges([
13795 Point::new(1, 11)..Point::new(1, 11),
13796 Point::new(7, 11)..Point::new(7, 11),
13797 ])
13798 });
13799
13800 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
13801 });
13802
13803 editor.update_in(cx, |editor, window, cx| {
13804 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13805 });
13806
13807 fake_server
13808 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13809 let completion_item = lsp::CompletionItem {
13810 label: "saturating_sub()".into(),
13811 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
13812 lsp::InsertReplaceEdit {
13813 new_text: "saturating_sub()".to_owned(),
13814 insert: lsp::Range::new(
13815 lsp::Position::new(7, 7),
13816 lsp::Position::new(7, 11),
13817 ),
13818 replace: lsp::Range::new(
13819 lsp::Position::new(7, 7),
13820 lsp::Position::new(7, 13),
13821 ),
13822 },
13823 )),
13824 ..lsp::CompletionItem::default()
13825 };
13826
13827 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
13828 })
13829 .next()
13830 .await
13831 .unwrap();
13832
13833 cx.condition(&editor, |editor, _| editor.context_menu_visible())
13834 .await;
13835
13836 editor
13837 .update_in(cx, |editor, window, cx| {
13838 editor
13839 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13840 .unwrap()
13841 })
13842 .await
13843 .unwrap();
13844
13845 editor.update(cx, |editor, cx| {
13846 assert_text_with_selections(editor, expected_multibuffer, cx);
13847 })
13848}
13849
13850#[gpui::test]
13851async fn test_completion(cx: &mut TestAppContext) {
13852 init_test(cx, |_| {});
13853
13854 let mut cx = EditorLspTestContext::new_rust(
13855 lsp::ServerCapabilities {
13856 completion_provider: Some(lsp::CompletionOptions {
13857 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
13858 resolve_provider: Some(true),
13859 ..Default::default()
13860 }),
13861 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
13862 ..Default::default()
13863 },
13864 cx,
13865 )
13866 .await;
13867 let counter = Arc::new(AtomicUsize::new(0));
13868
13869 cx.set_state(indoc! {"
13870 oneˇ
13871 two
13872 three
13873 "});
13874 cx.simulate_keystroke(".");
13875 handle_completion_request(
13876 indoc! {"
13877 one.|<>
13878 two
13879 three
13880 "},
13881 vec!["first_completion", "second_completion"],
13882 true,
13883 counter.clone(),
13884 &mut cx,
13885 )
13886 .await;
13887 cx.condition(|editor, _| editor.context_menu_visible())
13888 .await;
13889 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13890
13891 let _handler = handle_signature_help_request(
13892 &mut cx,
13893 lsp::SignatureHelp {
13894 signatures: vec![lsp::SignatureInformation {
13895 label: "test signature".to_string(),
13896 documentation: None,
13897 parameters: Some(vec![lsp::ParameterInformation {
13898 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
13899 documentation: None,
13900 }]),
13901 active_parameter: None,
13902 }],
13903 active_signature: None,
13904 active_parameter: None,
13905 },
13906 );
13907 cx.update_editor(|editor, window, cx| {
13908 assert!(
13909 !editor.signature_help_state.is_shown(),
13910 "No signature help was called for"
13911 );
13912 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13913 });
13914 cx.run_until_parked();
13915 cx.update_editor(|editor, _, _| {
13916 assert!(
13917 !editor.signature_help_state.is_shown(),
13918 "No signature help should be shown when completions menu is open"
13919 );
13920 });
13921
13922 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13923 editor.context_menu_next(&Default::default(), window, cx);
13924 editor
13925 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13926 .unwrap()
13927 });
13928 cx.assert_editor_state(indoc! {"
13929 one.second_completionˇ
13930 two
13931 three
13932 "});
13933
13934 handle_resolve_completion_request(
13935 &mut cx,
13936 Some(vec![
13937 (
13938 //This overlaps with the primary completion edit which is
13939 //misbehavior from the LSP spec, test that we filter it out
13940 indoc! {"
13941 one.second_ˇcompletion
13942 two
13943 threeˇ
13944 "},
13945 "overlapping additional edit",
13946 ),
13947 (
13948 indoc! {"
13949 one.second_completion
13950 two
13951 threeˇ
13952 "},
13953 "\nadditional edit",
13954 ),
13955 ]),
13956 )
13957 .await;
13958 apply_additional_edits.await.unwrap();
13959 cx.assert_editor_state(indoc! {"
13960 one.second_completionˇ
13961 two
13962 three
13963 additional edit
13964 "});
13965
13966 cx.set_state(indoc! {"
13967 one.second_completion
13968 twoˇ
13969 threeˇ
13970 additional edit
13971 "});
13972 cx.simulate_keystroke(" ");
13973 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
13974 cx.simulate_keystroke("s");
13975 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
13976
13977 cx.assert_editor_state(indoc! {"
13978 one.second_completion
13979 two sˇ
13980 three sˇ
13981 additional edit
13982 "});
13983 handle_completion_request(
13984 indoc! {"
13985 one.second_completion
13986 two s
13987 three <s|>
13988 additional edit
13989 "},
13990 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
13991 true,
13992 counter.clone(),
13993 &mut cx,
13994 )
13995 .await;
13996 cx.condition(|editor, _| editor.context_menu_visible())
13997 .await;
13998 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
13999
14000 cx.simulate_keystroke("i");
14001
14002 handle_completion_request(
14003 indoc! {"
14004 one.second_completion
14005 two si
14006 three <si|>
14007 additional edit
14008 "},
14009 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14010 true,
14011 counter.clone(),
14012 &mut cx,
14013 )
14014 .await;
14015 cx.condition(|editor, _| editor.context_menu_visible())
14016 .await;
14017 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14018
14019 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14020 editor
14021 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14022 .unwrap()
14023 });
14024 cx.assert_editor_state(indoc! {"
14025 one.second_completion
14026 two sixth_completionˇ
14027 three sixth_completionˇ
14028 additional edit
14029 "});
14030
14031 apply_additional_edits.await.unwrap();
14032
14033 update_test_language_settings(&mut cx, |settings| {
14034 settings.defaults.show_completions_on_input = Some(false);
14035 });
14036 cx.set_state("editorˇ");
14037 cx.simulate_keystroke(".");
14038 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14039 cx.simulate_keystrokes("c l o");
14040 cx.assert_editor_state("editor.cloˇ");
14041 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14042 cx.update_editor(|editor, window, cx| {
14043 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14044 });
14045 handle_completion_request(
14046 "editor.<clo|>",
14047 vec!["close", "clobber"],
14048 true,
14049 counter.clone(),
14050 &mut cx,
14051 )
14052 .await;
14053 cx.condition(|editor, _| editor.context_menu_visible())
14054 .await;
14055 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
14056
14057 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14058 editor
14059 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14060 .unwrap()
14061 });
14062 cx.assert_editor_state("editor.clobberˇ");
14063 handle_resolve_completion_request(&mut cx, None).await;
14064 apply_additional_edits.await.unwrap();
14065}
14066
14067#[gpui::test]
14068async fn test_completion_reuse(cx: &mut TestAppContext) {
14069 init_test(cx, |_| {});
14070
14071 let mut cx = EditorLspTestContext::new_rust(
14072 lsp::ServerCapabilities {
14073 completion_provider: Some(lsp::CompletionOptions {
14074 trigger_characters: Some(vec![".".to_string()]),
14075 ..Default::default()
14076 }),
14077 ..Default::default()
14078 },
14079 cx,
14080 )
14081 .await;
14082
14083 let counter = Arc::new(AtomicUsize::new(0));
14084 cx.set_state("objˇ");
14085 cx.simulate_keystroke(".");
14086
14087 // Initial completion request returns complete results
14088 let is_incomplete = false;
14089 handle_completion_request(
14090 "obj.|<>",
14091 vec!["a", "ab", "abc"],
14092 is_incomplete,
14093 counter.clone(),
14094 &mut cx,
14095 )
14096 .await;
14097 cx.run_until_parked();
14098 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14099 cx.assert_editor_state("obj.ˇ");
14100 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14101
14102 // Type "a" - filters existing completions
14103 cx.simulate_keystroke("a");
14104 cx.run_until_parked();
14105 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14106 cx.assert_editor_state("obj.aˇ");
14107 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14108
14109 // Type "b" - filters existing completions
14110 cx.simulate_keystroke("b");
14111 cx.run_until_parked();
14112 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14113 cx.assert_editor_state("obj.abˇ");
14114 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14115
14116 // Type "c" - filters existing completions
14117 cx.simulate_keystroke("c");
14118 cx.run_until_parked();
14119 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14120 cx.assert_editor_state("obj.abcˇ");
14121 check_displayed_completions(vec!["abc"], &mut cx);
14122
14123 // Backspace to delete "c" - filters existing completions
14124 cx.update_editor(|editor, window, cx| {
14125 editor.backspace(&Backspace, window, cx);
14126 });
14127 cx.run_until_parked();
14128 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14129 cx.assert_editor_state("obj.abˇ");
14130 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14131
14132 // Moving cursor to the left dismisses menu.
14133 cx.update_editor(|editor, window, cx| {
14134 editor.move_left(&MoveLeft, window, cx);
14135 });
14136 cx.run_until_parked();
14137 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14138 cx.assert_editor_state("obj.aˇb");
14139 cx.update_editor(|editor, _, _| {
14140 assert_eq!(editor.context_menu_visible(), false);
14141 });
14142
14143 // Type "b" - new request
14144 cx.simulate_keystroke("b");
14145 let is_incomplete = false;
14146 handle_completion_request(
14147 "obj.<ab|>a",
14148 vec!["ab", "abc"],
14149 is_incomplete,
14150 counter.clone(),
14151 &mut cx,
14152 )
14153 .await;
14154 cx.run_until_parked();
14155 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14156 cx.assert_editor_state("obj.abˇb");
14157 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14158
14159 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
14160 cx.update_editor(|editor, window, cx| {
14161 editor.backspace(&Backspace, window, cx);
14162 });
14163 let is_incomplete = false;
14164 handle_completion_request(
14165 "obj.<a|>b",
14166 vec!["a", "ab", "abc"],
14167 is_incomplete,
14168 counter.clone(),
14169 &mut cx,
14170 )
14171 .await;
14172 cx.run_until_parked();
14173 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14174 cx.assert_editor_state("obj.aˇb");
14175 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14176
14177 // Backspace to delete "a" - dismisses menu.
14178 cx.update_editor(|editor, window, cx| {
14179 editor.backspace(&Backspace, window, cx);
14180 });
14181 cx.run_until_parked();
14182 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14183 cx.assert_editor_state("obj.ˇb");
14184 cx.update_editor(|editor, _, _| {
14185 assert_eq!(editor.context_menu_visible(), false);
14186 });
14187}
14188
14189#[gpui::test]
14190async fn test_word_completion(cx: &mut TestAppContext) {
14191 let lsp_fetch_timeout_ms = 10;
14192 init_test(cx, |language_settings| {
14193 language_settings.defaults.completions = Some(CompletionSettingsContent {
14194 words_min_length: Some(0),
14195 lsp_fetch_timeout_ms: Some(10),
14196 lsp_insert_mode: Some(LspInsertMode::Insert),
14197 ..Default::default()
14198 });
14199 });
14200
14201 let mut cx = EditorLspTestContext::new_rust(
14202 lsp::ServerCapabilities {
14203 completion_provider: Some(lsp::CompletionOptions {
14204 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14205 ..lsp::CompletionOptions::default()
14206 }),
14207 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14208 ..lsp::ServerCapabilities::default()
14209 },
14210 cx,
14211 )
14212 .await;
14213
14214 let throttle_completions = Arc::new(AtomicBool::new(false));
14215
14216 let lsp_throttle_completions = throttle_completions.clone();
14217 let _completion_requests_handler =
14218 cx.lsp
14219 .server
14220 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
14221 let lsp_throttle_completions = lsp_throttle_completions.clone();
14222 let cx = cx.clone();
14223 async move {
14224 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
14225 cx.background_executor()
14226 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
14227 .await;
14228 }
14229 Ok(Some(lsp::CompletionResponse::Array(vec![
14230 lsp::CompletionItem {
14231 label: "first".into(),
14232 ..lsp::CompletionItem::default()
14233 },
14234 lsp::CompletionItem {
14235 label: "last".into(),
14236 ..lsp::CompletionItem::default()
14237 },
14238 ])))
14239 }
14240 });
14241
14242 cx.set_state(indoc! {"
14243 oneˇ
14244 two
14245 three
14246 "});
14247 cx.simulate_keystroke(".");
14248 cx.executor().run_until_parked();
14249 cx.condition(|editor, _| editor.context_menu_visible())
14250 .await;
14251 cx.update_editor(|editor, window, cx| {
14252 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14253 {
14254 assert_eq!(
14255 completion_menu_entries(menu),
14256 &["first", "last"],
14257 "When LSP server is fast to reply, no fallback word completions are used"
14258 );
14259 } else {
14260 panic!("expected completion menu to be open");
14261 }
14262 editor.cancel(&Cancel, window, cx);
14263 });
14264 cx.executor().run_until_parked();
14265 cx.condition(|editor, _| !editor.context_menu_visible())
14266 .await;
14267
14268 throttle_completions.store(true, atomic::Ordering::Release);
14269 cx.simulate_keystroke(".");
14270 cx.executor()
14271 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
14272 cx.executor().run_until_parked();
14273 cx.condition(|editor, _| editor.context_menu_visible())
14274 .await;
14275 cx.update_editor(|editor, _, _| {
14276 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14277 {
14278 assert_eq!(completion_menu_entries(menu), &["one", "three", "two"],
14279 "When LSP server is slow, document words can be shown instead, if configured accordingly");
14280 } else {
14281 panic!("expected completion menu to be open");
14282 }
14283 });
14284}
14285
14286#[gpui::test]
14287async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
14288 init_test(cx, |language_settings| {
14289 language_settings.defaults.completions = Some(CompletionSettingsContent {
14290 words: Some(WordsCompletionMode::Enabled),
14291 words_min_length: Some(0),
14292 lsp_insert_mode: Some(LspInsertMode::Insert),
14293 ..Default::default()
14294 });
14295 });
14296
14297 let mut cx = EditorLspTestContext::new_rust(
14298 lsp::ServerCapabilities {
14299 completion_provider: Some(lsp::CompletionOptions {
14300 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14301 ..lsp::CompletionOptions::default()
14302 }),
14303 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14304 ..lsp::ServerCapabilities::default()
14305 },
14306 cx,
14307 )
14308 .await;
14309
14310 let _completion_requests_handler =
14311 cx.lsp
14312 .server
14313 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14314 Ok(Some(lsp::CompletionResponse::Array(vec![
14315 lsp::CompletionItem {
14316 label: "first".into(),
14317 ..lsp::CompletionItem::default()
14318 },
14319 lsp::CompletionItem {
14320 label: "last".into(),
14321 ..lsp::CompletionItem::default()
14322 },
14323 ])))
14324 });
14325
14326 cx.set_state(indoc! {"ˇ
14327 first
14328 last
14329 second
14330 "});
14331 cx.simulate_keystroke(".");
14332 cx.executor().run_until_parked();
14333 cx.condition(|editor, _| editor.context_menu_visible())
14334 .await;
14335 cx.update_editor(|editor, _, _| {
14336 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14337 {
14338 assert_eq!(
14339 completion_menu_entries(menu),
14340 &["first", "last", "second"],
14341 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
14342 );
14343 } else {
14344 panic!("expected completion menu to be open");
14345 }
14346 });
14347}
14348
14349#[gpui::test]
14350async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
14351 init_test(cx, |language_settings| {
14352 language_settings.defaults.completions = Some(CompletionSettingsContent {
14353 words: Some(WordsCompletionMode::Disabled),
14354 words_min_length: Some(0),
14355 lsp_insert_mode: Some(LspInsertMode::Insert),
14356 ..Default::default()
14357 });
14358 });
14359
14360 let mut cx = EditorLspTestContext::new_rust(
14361 lsp::ServerCapabilities {
14362 completion_provider: Some(lsp::CompletionOptions {
14363 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14364 ..lsp::CompletionOptions::default()
14365 }),
14366 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14367 ..lsp::ServerCapabilities::default()
14368 },
14369 cx,
14370 )
14371 .await;
14372
14373 let _completion_requests_handler =
14374 cx.lsp
14375 .server
14376 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14377 panic!("LSP completions should not be queried when dealing with word completions")
14378 });
14379
14380 cx.set_state(indoc! {"ˇ
14381 first
14382 last
14383 second
14384 "});
14385 cx.update_editor(|editor, window, cx| {
14386 editor.show_word_completions(&ShowWordCompletions, window, cx);
14387 });
14388 cx.executor().run_until_parked();
14389 cx.condition(|editor, _| editor.context_menu_visible())
14390 .await;
14391 cx.update_editor(|editor, _, _| {
14392 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14393 {
14394 assert_eq!(
14395 completion_menu_entries(menu),
14396 &["first", "last", "second"],
14397 "`ShowWordCompletions` action should show word completions"
14398 );
14399 } else {
14400 panic!("expected completion menu to be open");
14401 }
14402 });
14403
14404 cx.simulate_keystroke("l");
14405 cx.executor().run_until_parked();
14406 cx.condition(|editor, _| editor.context_menu_visible())
14407 .await;
14408 cx.update_editor(|editor, _, _| {
14409 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14410 {
14411 assert_eq!(
14412 completion_menu_entries(menu),
14413 &["last"],
14414 "After showing word completions, further editing should filter them and not query the LSP"
14415 );
14416 } else {
14417 panic!("expected completion menu to be open");
14418 }
14419 });
14420}
14421
14422#[gpui::test]
14423async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
14424 init_test(cx, |language_settings| {
14425 language_settings.defaults.completions = Some(CompletionSettingsContent {
14426 words_min_length: Some(0),
14427 lsp: Some(false),
14428 lsp_insert_mode: Some(LspInsertMode::Insert),
14429 ..Default::default()
14430 });
14431 });
14432
14433 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14434
14435 cx.set_state(indoc! {"ˇ
14436 0_usize
14437 let
14438 33
14439 4.5f32
14440 "});
14441 cx.update_editor(|editor, window, cx| {
14442 editor.show_completions(&ShowCompletions::default(), window, cx);
14443 });
14444 cx.executor().run_until_parked();
14445 cx.condition(|editor, _| editor.context_menu_visible())
14446 .await;
14447 cx.update_editor(|editor, window, cx| {
14448 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14449 {
14450 assert_eq!(
14451 completion_menu_entries(menu),
14452 &["let"],
14453 "With no digits in the completion query, no digits should be in the word completions"
14454 );
14455 } else {
14456 panic!("expected completion menu to be open");
14457 }
14458 editor.cancel(&Cancel, window, cx);
14459 });
14460
14461 cx.set_state(indoc! {"3ˇ
14462 0_usize
14463 let
14464 3
14465 33.35f32
14466 "});
14467 cx.update_editor(|editor, window, cx| {
14468 editor.show_completions(&ShowCompletions::default(), window, cx);
14469 });
14470 cx.executor().run_until_parked();
14471 cx.condition(|editor, _| editor.context_menu_visible())
14472 .await;
14473 cx.update_editor(|editor, _, _| {
14474 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14475 {
14476 assert_eq!(completion_menu_entries(menu), &["33", "35f32"], "The digit is in the completion query, \
14477 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
14478 } else {
14479 panic!("expected completion menu to be open");
14480 }
14481 });
14482}
14483
14484#[gpui::test]
14485async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) {
14486 init_test(cx, |language_settings| {
14487 language_settings.defaults.completions = Some(CompletionSettingsContent {
14488 words: Some(WordsCompletionMode::Enabled),
14489 words_min_length: Some(3),
14490 lsp_insert_mode: Some(LspInsertMode::Insert),
14491 ..Default::default()
14492 });
14493 });
14494
14495 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14496 cx.set_state(indoc! {"ˇ
14497 wow
14498 wowen
14499 wowser
14500 "});
14501 cx.simulate_keystroke("w");
14502 cx.executor().run_until_parked();
14503 cx.update_editor(|editor, _, _| {
14504 if editor.context_menu.borrow_mut().is_some() {
14505 panic!(
14506 "expected completion menu to be hidden, as words completion threshold is not met"
14507 );
14508 }
14509 });
14510
14511 cx.update_editor(|editor, window, cx| {
14512 editor.show_word_completions(&ShowWordCompletions, window, cx);
14513 });
14514 cx.executor().run_until_parked();
14515 cx.update_editor(|editor, window, cx| {
14516 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14517 {
14518 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");
14519 } else {
14520 panic!("expected completion menu to be open after the word completions are called with an action");
14521 }
14522
14523 editor.cancel(&Cancel, window, cx);
14524 });
14525 cx.update_editor(|editor, _, _| {
14526 if editor.context_menu.borrow_mut().is_some() {
14527 panic!("expected completion menu to be hidden after canceling");
14528 }
14529 });
14530
14531 cx.simulate_keystroke("o");
14532 cx.executor().run_until_parked();
14533 cx.update_editor(|editor, _, _| {
14534 if editor.context_menu.borrow_mut().is_some() {
14535 panic!(
14536 "expected completion menu to be hidden, as words completion threshold is not met still"
14537 );
14538 }
14539 });
14540
14541 cx.simulate_keystroke("w");
14542 cx.executor().run_until_parked();
14543 cx.update_editor(|editor, _, _| {
14544 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14545 {
14546 assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word");
14547 } else {
14548 panic!("expected completion menu to be open after the word completions threshold is met");
14549 }
14550 });
14551}
14552
14553#[gpui::test]
14554async fn test_word_completions_disabled(cx: &mut TestAppContext) {
14555 init_test(cx, |language_settings| {
14556 language_settings.defaults.completions = Some(CompletionSettingsContent {
14557 words: Some(WordsCompletionMode::Enabled),
14558 words_min_length: Some(0),
14559 lsp_insert_mode: Some(LspInsertMode::Insert),
14560 ..Default::default()
14561 });
14562 });
14563
14564 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14565 cx.update_editor(|editor, _, _| {
14566 editor.disable_word_completions();
14567 });
14568 cx.set_state(indoc! {"ˇ
14569 wow
14570 wowen
14571 wowser
14572 "});
14573 cx.simulate_keystroke("w");
14574 cx.executor().run_until_parked();
14575 cx.update_editor(|editor, _, _| {
14576 if editor.context_menu.borrow_mut().is_some() {
14577 panic!(
14578 "expected completion menu to be hidden, as words completion are disabled for this editor"
14579 );
14580 }
14581 });
14582
14583 cx.update_editor(|editor, window, cx| {
14584 editor.show_word_completions(&ShowWordCompletions, window, cx);
14585 });
14586 cx.executor().run_until_parked();
14587 cx.update_editor(|editor, _, _| {
14588 if editor.context_menu.borrow_mut().is_some() {
14589 panic!(
14590 "expected completion menu to be hidden even if called for explicitly, as words completion are disabled for this editor"
14591 );
14592 }
14593 });
14594}
14595
14596fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
14597 let position = || lsp::Position {
14598 line: params.text_document_position.position.line,
14599 character: params.text_document_position.position.character,
14600 };
14601 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14602 range: lsp::Range {
14603 start: position(),
14604 end: position(),
14605 },
14606 new_text: text.to_string(),
14607 }))
14608}
14609
14610#[gpui::test]
14611async fn test_multiline_completion(cx: &mut TestAppContext) {
14612 init_test(cx, |_| {});
14613
14614 let fs = FakeFs::new(cx.executor());
14615 fs.insert_tree(
14616 path!("/a"),
14617 json!({
14618 "main.ts": "a",
14619 }),
14620 )
14621 .await;
14622
14623 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14624 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14625 let typescript_language = Arc::new(Language::new(
14626 LanguageConfig {
14627 name: "TypeScript".into(),
14628 matcher: LanguageMatcher {
14629 path_suffixes: vec!["ts".to_string()],
14630 ..LanguageMatcher::default()
14631 },
14632 line_comments: vec!["// ".into()],
14633 ..LanguageConfig::default()
14634 },
14635 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
14636 ));
14637 language_registry.add(typescript_language.clone());
14638 let mut fake_servers = language_registry.register_fake_lsp(
14639 "TypeScript",
14640 FakeLspAdapter {
14641 capabilities: lsp::ServerCapabilities {
14642 completion_provider: Some(lsp::CompletionOptions {
14643 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14644 ..lsp::CompletionOptions::default()
14645 }),
14646 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14647 ..lsp::ServerCapabilities::default()
14648 },
14649 // Emulate vtsls label generation
14650 label_for_completion: Some(Box::new(|item, _| {
14651 let text = if let Some(description) = item
14652 .label_details
14653 .as_ref()
14654 .and_then(|label_details| label_details.description.as_ref())
14655 {
14656 format!("{} {}", item.label, description)
14657 } else if let Some(detail) = &item.detail {
14658 format!("{} {}", item.label, detail)
14659 } else {
14660 item.label.clone()
14661 };
14662 let len = text.len();
14663 Some(language::CodeLabel {
14664 text,
14665 runs: Vec::new(),
14666 filter_range: 0..len,
14667 })
14668 })),
14669 ..FakeLspAdapter::default()
14670 },
14671 );
14672 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14673 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14674 let worktree_id = workspace
14675 .update(cx, |workspace, _window, cx| {
14676 workspace.project().update(cx, |project, cx| {
14677 project.worktrees(cx).next().unwrap().read(cx).id()
14678 })
14679 })
14680 .unwrap();
14681 let _buffer = project
14682 .update(cx, |project, cx| {
14683 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
14684 })
14685 .await
14686 .unwrap();
14687 let editor = workspace
14688 .update(cx, |workspace, window, cx| {
14689 workspace.open_path((worktree_id, rel_path("main.ts")), None, true, window, cx)
14690 })
14691 .unwrap()
14692 .await
14693 .unwrap()
14694 .downcast::<Editor>()
14695 .unwrap();
14696 let fake_server = fake_servers.next().await.unwrap();
14697
14698 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
14699 let multiline_label_2 = "a\nb\nc\n";
14700 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
14701 let multiline_description = "d\ne\nf\n";
14702 let multiline_detail_2 = "g\nh\ni\n";
14703
14704 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
14705 move |params, _| async move {
14706 Ok(Some(lsp::CompletionResponse::Array(vec![
14707 lsp::CompletionItem {
14708 label: multiline_label.to_string(),
14709 text_edit: gen_text_edit(¶ms, "new_text_1"),
14710 ..lsp::CompletionItem::default()
14711 },
14712 lsp::CompletionItem {
14713 label: "single line label 1".to_string(),
14714 detail: Some(multiline_detail.to_string()),
14715 text_edit: gen_text_edit(¶ms, "new_text_2"),
14716 ..lsp::CompletionItem::default()
14717 },
14718 lsp::CompletionItem {
14719 label: "single line label 2".to_string(),
14720 label_details: Some(lsp::CompletionItemLabelDetails {
14721 description: Some(multiline_description.to_string()),
14722 detail: None,
14723 }),
14724 text_edit: gen_text_edit(¶ms, "new_text_2"),
14725 ..lsp::CompletionItem::default()
14726 },
14727 lsp::CompletionItem {
14728 label: multiline_label_2.to_string(),
14729 detail: Some(multiline_detail_2.to_string()),
14730 text_edit: gen_text_edit(¶ms, "new_text_3"),
14731 ..lsp::CompletionItem::default()
14732 },
14733 lsp::CompletionItem {
14734 label: "Label with many spaces and \t but without newlines".to_string(),
14735 detail: Some(
14736 "Details with many spaces and \t but without newlines".to_string(),
14737 ),
14738 text_edit: gen_text_edit(¶ms, "new_text_4"),
14739 ..lsp::CompletionItem::default()
14740 },
14741 ])))
14742 },
14743 );
14744
14745 editor.update_in(cx, |editor, window, cx| {
14746 cx.focus_self(window);
14747 editor.move_to_end(&MoveToEnd, window, cx);
14748 editor.handle_input(".", window, cx);
14749 });
14750 cx.run_until_parked();
14751 completion_handle.next().await.unwrap();
14752
14753 editor.update(cx, |editor, _| {
14754 assert!(editor.context_menu_visible());
14755 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14756 {
14757 let completion_labels = menu
14758 .completions
14759 .borrow()
14760 .iter()
14761 .map(|c| c.label.text.clone())
14762 .collect::<Vec<_>>();
14763 assert_eq!(
14764 completion_labels,
14765 &[
14766 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
14767 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
14768 "single line label 2 d e f ",
14769 "a b c g h i ",
14770 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
14771 ],
14772 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
14773 );
14774
14775 for completion in menu
14776 .completions
14777 .borrow()
14778 .iter() {
14779 assert_eq!(
14780 completion.label.filter_range,
14781 0..completion.label.text.len(),
14782 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
14783 );
14784 }
14785 } else {
14786 panic!("expected completion menu to be open");
14787 }
14788 });
14789}
14790
14791#[gpui::test]
14792async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
14793 init_test(cx, |_| {});
14794 let mut cx = EditorLspTestContext::new_rust(
14795 lsp::ServerCapabilities {
14796 completion_provider: Some(lsp::CompletionOptions {
14797 trigger_characters: Some(vec![".".to_string()]),
14798 ..Default::default()
14799 }),
14800 ..Default::default()
14801 },
14802 cx,
14803 )
14804 .await;
14805 cx.lsp
14806 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14807 Ok(Some(lsp::CompletionResponse::Array(vec![
14808 lsp::CompletionItem {
14809 label: "first".into(),
14810 ..Default::default()
14811 },
14812 lsp::CompletionItem {
14813 label: "last".into(),
14814 ..Default::default()
14815 },
14816 ])))
14817 });
14818 cx.set_state("variableˇ");
14819 cx.simulate_keystroke(".");
14820 cx.executor().run_until_parked();
14821
14822 cx.update_editor(|editor, _, _| {
14823 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14824 {
14825 assert_eq!(completion_menu_entries(menu), &["first", "last"]);
14826 } else {
14827 panic!("expected completion menu to be open");
14828 }
14829 });
14830
14831 cx.update_editor(|editor, window, cx| {
14832 editor.move_page_down(&MovePageDown::default(), window, cx);
14833 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14834 {
14835 assert!(
14836 menu.selected_item == 1,
14837 "expected PageDown to select the last item from the context menu"
14838 );
14839 } else {
14840 panic!("expected completion menu to stay open after PageDown");
14841 }
14842 });
14843
14844 cx.update_editor(|editor, window, cx| {
14845 editor.move_page_up(&MovePageUp::default(), window, cx);
14846 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14847 {
14848 assert!(
14849 menu.selected_item == 0,
14850 "expected PageUp to select the first item from the context menu"
14851 );
14852 } else {
14853 panic!("expected completion menu to stay open after PageUp");
14854 }
14855 });
14856}
14857
14858#[gpui::test]
14859async fn test_as_is_completions(cx: &mut TestAppContext) {
14860 init_test(cx, |_| {});
14861 let mut cx = EditorLspTestContext::new_rust(
14862 lsp::ServerCapabilities {
14863 completion_provider: Some(lsp::CompletionOptions {
14864 ..Default::default()
14865 }),
14866 ..Default::default()
14867 },
14868 cx,
14869 )
14870 .await;
14871 cx.lsp
14872 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14873 Ok(Some(lsp::CompletionResponse::Array(vec![
14874 lsp::CompletionItem {
14875 label: "unsafe".into(),
14876 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14877 range: lsp::Range {
14878 start: lsp::Position {
14879 line: 1,
14880 character: 2,
14881 },
14882 end: lsp::Position {
14883 line: 1,
14884 character: 3,
14885 },
14886 },
14887 new_text: "unsafe".to_string(),
14888 })),
14889 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
14890 ..Default::default()
14891 },
14892 ])))
14893 });
14894 cx.set_state("fn a() {}\n nˇ");
14895 cx.executor().run_until_parked();
14896 cx.update_editor(|editor, window, cx| {
14897 editor.show_completions(
14898 &ShowCompletions {
14899 trigger: Some("\n".into()),
14900 },
14901 window,
14902 cx,
14903 );
14904 });
14905 cx.executor().run_until_parked();
14906
14907 cx.update_editor(|editor, window, cx| {
14908 editor.confirm_completion(&Default::default(), window, cx)
14909 });
14910 cx.executor().run_until_parked();
14911 cx.assert_editor_state("fn a() {}\n unsafeˇ");
14912}
14913
14914#[gpui::test]
14915async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
14916 init_test(cx, |_| {});
14917 let language =
14918 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
14919 let mut cx = EditorLspTestContext::new(
14920 language,
14921 lsp::ServerCapabilities {
14922 completion_provider: Some(lsp::CompletionOptions {
14923 ..lsp::CompletionOptions::default()
14924 }),
14925 ..lsp::ServerCapabilities::default()
14926 },
14927 cx,
14928 )
14929 .await;
14930
14931 cx.set_state(
14932 "#ifndef BAR_H
14933#define BAR_H
14934
14935#include <stdbool.h>
14936
14937int fn_branch(bool do_branch1, bool do_branch2);
14938
14939#endif // BAR_H
14940ˇ",
14941 );
14942 cx.executor().run_until_parked();
14943 cx.update_editor(|editor, window, cx| {
14944 editor.handle_input("#", window, cx);
14945 });
14946 cx.executor().run_until_parked();
14947 cx.update_editor(|editor, window, cx| {
14948 editor.handle_input("i", window, cx);
14949 });
14950 cx.executor().run_until_parked();
14951 cx.update_editor(|editor, window, cx| {
14952 editor.handle_input("n", window, cx);
14953 });
14954 cx.executor().run_until_parked();
14955 cx.assert_editor_state(
14956 "#ifndef BAR_H
14957#define BAR_H
14958
14959#include <stdbool.h>
14960
14961int fn_branch(bool do_branch1, bool do_branch2);
14962
14963#endif // BAR_H
14964#inˇ",
14965 );
14966
14967 cx.lsp
14968 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14969 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
14970 is_incomplete: false,
14971 item_defaults: None,
14972 items: vec![lsp::CompletionItem {
14973 kind: Some(lsp::CompletionItemKind::SNIPPET),
14974 label_details: Some(lsp::CompletionItemLabelDetails {
14975 detail: Some("header".to_string()),
14976 description: None,
14977 }),
14978 label: " include".to_string(),
14979 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14980 range: lsp::Range {
14981 start: lsp::Position {
14982 line: 8,
14983 character: 1,
14984 },
14985 end: lsp::Position {
14986 line: 8,
14987 character: 1,
14988 },
14989 },
14990 new_text: "include \"$0\"".to_string(),
14991 })),
14992 sort_text: Some("40b67681include".to_string()),
14993 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
14994 filter_text: Some("include".to_string()),
14995 insert_text: Some("include \"$0\"".to_string()),
14996 ..lsp::CompletionItem::default()
14997 }],
14998 })))
14999 });
15000 cx.update_editor(|editor, window, cx| {
15001 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15002 });
15003 cx.executor().run_until_parked();
15004 cx.update_editor(|editor, window, cx| {
15005 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15006 });
15007 cx.executor().run_until_parked();
15008 cx.assert_editor_state(
15009 "#ifndef BAR_H
15010#define BAR_H
15011
15012#include <stdbool.h>
15013
15014int fn_branch(bool do_branch1, bool do_branch2);
15015
15016#endif // BAR_H
15017#include \"ˇ\"",
15018 );
15019
15020 cx.lsp
15021 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15022 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15023 is_incomplete: true,
15024 item_defaults: None,
15025 items: vec![lsp::CompletionItem {
15026 kind: Some(lsp::CompletionItemKind::FILE),
15027 label: "AGL/".to_string(),
15028 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15029 range: lsp::Range {
15030 start: lsp::Position {
15031 line: 8,
15032 character: 10,
15033 },
15034 end: lsp::Position {
15035 line: 8,
15036 character: 11,
15037 },
15038 },
15039 new_text: "AGL/".to_string(),
15040 })),
15041 sort_text: Some("40b67681AGL/".to_string()),
15042 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
15043 filter_text: Some("AGL/".to_string()),
15044 insert_text: Some("AGL/".to_string()),
15045 ..lsp::CompletionItem::default()
15046 }],
15047 })))
15048 });
15049 cx.update_editor(|editor, window, cx| {
15050 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15051 });
15052 cx.executor().run_until_parked();
15053 cx.update_editor(|editor, window, cx| {
15054 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15055 });
15056 cx.executor().run_until_parked();
15057 cx.assert_editor_state(
15058 r##"#ifndef BAR_H
15059#define BAR_H
15060
15061#include <stdbool.h>
15062
15063int fn_branch(bool do_branch1, bool do_branch2);
15064
15065#endif // BAR_H
15066#include "AGL/ˇ"##,
15067 );
15068
15069 cx.update_editor(|editor, window, cx| {
15070 editor.handle_input("\"", window, cx);
15071 });
15072 cx.executor().run_until_parked();
15073 cx.assert_editor_state(
15074 r##"#ifndef BAR_H
15075#define BAR_H
15076
15077#include <stdbool.h>
15078
15079int fn_branch(bool do_branch1, bool do_branch2);
15080
15081#endif // BAR_H
15082#include "AGL/"ˇ"##,
15083 );
15084}
15085
15086#[gpui::test]
15087async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
15088 init_test(cx, |_| {});
15089
15090 let mut cx = EditorLspTestContext::new_rust(
15091 lsp::ServerCapabilities {
15092 completion_provider: Some(lsp::CompletionOptions {
15093 trigger_characters: Some(vec![".".to_string()]),
15094 resolve_provider: Some(true),
15095 ..Default::default()
15096 }),
15097 ..Default::default()
15098 },
15099 cx,
15100 )
15101 .await;
15102
15103 cx.set_state("fn main() { let a = 2ˇ; }");
15104 cx.simulate_keystroke(".");
15105 let completion_item = lsp::CompletionItem {
15106 label: "Some".into(),
15107 kind: Some(lsp::CompletionItemKind::SNIPPET),
15108 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
15109 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
15110 kind: lsp::MarkupKind::Markdown,
15111 value: "```rust\nSome(2)\n```".to_string(),
15112 })),
15113 deprecated: Some(false),
15114 sort_text: Some("Some".to_string()),
15115 filter_text: Some("Some".to_string()),
15116 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15117 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15118 range: lsp::Range {
15119 start: lsp::Position {
15120 line: 0,
15121 character: 22,
15122 },
15123 end: lsp::Position {
15124 line: 0,
15125 character: 22,
15126 },
15127 },
15128 new_text: "Some(2)".to_string(),
15129 })),
15130 additional_text_edits: Some(vec![lsp::TextEdit {
15131 range: lsp::Range {
15132 start: lsp::Position {
15133 line: 0,
15134 character: 20,
15135 },
15136 end: lsp::Position {
15137 line: 0,
15138 character: 22,
15139 },
15140 },
15141 new_text: "".to_string(),
15142 }]),
15143 ..Default::default()
15144 };
15145
15146 let closure_completion_item = completion_item.clone();
15147 let counter = Arc::new(AtomicUsize::new(0));
15148 let counter_clone = counter.clone();
15149 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15150 let task_completion_item = closure_completion_item.clone();
15151 counter_clone.fetch_add(1, atomic::Ordering::Release);
15152 async move {
15153 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15154 is_incomplete: true,
15155 item_defaults: None,
15156 items: vec![task_completion_item],
15157 })))
15158 }
15159 });
15160
15161 cx.condition(|editor, _| editor.context_menu_visible())
15162 .await;
15163 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
15164 assert!(request.next().await.is_some());
15165 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15166
15167 cx.simulate_keystrokes("S o m");
15168 cx.condition(|editor, _| editor.context_menu_visible())
15169 .await;
15170 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
15171 assert!(request.next().await.is_some());
15172 assert!(request.next().await.is_some());
15173 assert!(request.next().await.is_some());
15174 request.close();
15175 assert!(request.next().await.is_none());
15176 assert_eq!(
15177 counter.load(atomic::Ordering::Acquire),
15178 4,
15179 "With the completions menu open, only one LSP request should happen per input"
15180 );
15181}
15182
15183#[gpui::test]
15184async fn test_toggle_comment(cx: &mut TestAppContext) {
15185 init_test(cx, |_| {});
15186 let mut cx = EditorTestContext::new(cx).await;
15187 let language = Arc::new(Language::new(
15188 LanguageConfig {
15189 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15190 ..Default::default()
15191 },
15192 Some(tree_sitter_rust::LANGUAGE.into()),
15193 ));
15194 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15195
15196 // If multiple selections intersect a line, the line is only toggled once.
15197 cx.set_state(indoc! {"
15198 fn a() {
15199 «//b();
15200 ˇ»// «c();
15201 //ˇ» d();
15202 }
15203 "});
15204
15205 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15206
15207 cx.assert_editor_state(indoc! {"
15208 fn a() {
15209 «b();
15210 c();
15211 ˇ» d();
15212 }
15213 "});
15214
15215 // The comment prefix is inserted at the same column for every line in a
15216 // selection.
15217 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15218
15219 cx.assert_editor_state(indoc! {"
15220 fn a() {
15221 // «b();
15222 // c();
15223 ˇ»// d();
15224 }
15225 "});
15226
15227 // If a selection ends at the beginning of a line, that line is not toggled.
15228 cx.set_selections_state(indoc! {"
15229 fn a() {
15230 // b();
15231 «// c();
15232 ˇ» // d();
15233 }
15234 "});
15235
15236 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15237
15238 cx.assert_editor_state(indoc! {"
15239 fn a() {
15240 // b();
15241 «c();
15242 ˇ» // d();
15243 }
15244 "});
15245
15246 // If a selection span a single line and is empty, the line is toggled.
15247 cx.set_state(indoc! {"
15248 fn a() {
15249 a();
15250 b();
15251 ˇ
15252 }
15253 "});
15254
15255 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15256
15257 cx.assert_editor_state(indoc! {"
15258 fn a() {
15259 a();
15260 b();
15261 //•ˇ
15262 }
15263 "});
15264
15265 // If a selection span multiple lines, empty lines are not toggled.
15266 cx.set_state(indoc! {"
15267 fn a() {
15268 «a();
15269
15270 c();ˇ»
15271 }
15272 "});
15273
15274 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15275
15276 cx.assert_editor_state(indoc! {"
15277 fn a() {
15278 // «a();
15279
15280 // c();ˇ»
15281 }
15282 "});
15283
15284 // If a selection includes multiple comment prefixes, all lines are uncommented.
15285 cx.set_state(indoc! {"
15286 fn a() {
15287 «// a();
15288 /// b();
15289 //! c();ˇ»
15290 }
15291 "});
15292
15293 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15294
15295 cx.assert_editor_state(indoc! {"
15296 fn a() {
15297 «a();
15298 b();
15299 c();ˇ»
15300 }
15301 "});
15302}
15303
15304#[gpui::test]
15305async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
15306 init_test(cx, |_| {});
15307 let mut cx = EditorTestContext::new(cx).await;
15308 let language = Arc::new(Language::new(
15309 LanguageConfig {
15310 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15311 ..Default::default()
15312 },
15313 Some(tree_sitter_rust::LANGUAGE.into()),
15314 ));
15315 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15316
15317 let toggle_comments = &ToggleComments {
15318 advance_downwards: false,
15319 ignore_indent: true,
15320 };
15321
15322 // If multiple selections intersect a line, the line is only toggled once.
15323 cx.set_state(indoc! {"
15324 fn a() {
15325 // «b();
15326 // c();
15327 // ˇ» d();
15328 }
15329 "});
15330
15331 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15332
15333 cx.assert_editor_state(indoc! {"
15334 fn a() {
15335 «b();
15336 c();
15337 ˇ» d();
15338 }
15339 "});
15340
15341 // The comment prefix is inserted at the beginning of each line
15342 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15343
15344 cx.assert_editor_state(indoc! {"
15345 fn a() {
15346 // «b();
15347 // c();
15348 // ˇ» d();
15349 }
15350 "});
15351
15352 // If a selection ends at the beginning of a line, that line is not toggled.
15353 cx.set_selections_state(indoc! {"
15354 fn a() {
15355 // b();
15356 // «c();
15357 ˇ»// d();
15358 }
15359 "});
15360
15361 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15362
15363 cx.assert_editor_state(indoc! {"
15364 fn a() {
15365 // b();
15366 «c();
15367 ˇ»// d();
15368 }
15369 "});
15370
15371 // If a selection span a single line and is empty, the line is toggled.
15372 cx.set_state(indoc! {"
15373 fn a() {
15374 a();
15375 b();
15376 ˇ
15377 }
15378 "});
15379
15380 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15381
15382 cx.assert_editor_state(indoc! {"
15383 fn a() {
15384 a();
15385 b();
15386 //ˇ
15387 }
15388 "});
15389
15390 // If a selection span multiple lines, empty lines are not toggled.
15391 cx.set_state(indoc! {"
15392 fn a() {
15393 «a();
15394
15395 c();ˇ»
15396 }
15397 "});
15398
15399 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15400
15401 cx.assert_editor_state(indoc! {"
15402 fn a() {
15403 // «a();
15404
15405 // c();ˇ»
15406 }
15407 "});
15408
15409 // If a selection includes multiple comment prefixes, all lines are uncommented.
15410 cx.set_state(indoc! {"
15411 fn a() {
15412 // «a();
15413 /// b();
15414 //! c();ˇ»
15415 }
15416 "});
15417
15418 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15419
15420 cx.assert_editor_state(indoc! {"
15421 fn a() {
15422 «a();
15423 b();
15424 c();ˇ»
15425 }
15426 "});
15427}
15428
15429#[gpui::test]
15430async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
15431 init_test(cx, |_| {});
15432
15433 let language = Arc::new(Language::new(
15434 LanguageConfig {
15435 line_comments: vec!["// ".into()],
15436 ..Default::default()
15437 },
15438 Some(tree_sitter_rust::LANGUAGE.into()),
15439 ));
15440
15441 let mut cx = EditorTestContext::new(cx).await;
15442
15443 cx.language_registry().add(language.clone());
15444 cx.update_buffer(|buffer, cx| {
15445 buffer.set_language(Some(language), cx);
15446 });
15447
15448 let toggle_comments = &ToggleComments {
15449 advance_downwards: true,
15450 ignore_indent: false,
15451 };
15452
15453 // Single cursor on one line -> advance
15454 // Cursor moves horizontally 3 characters as well on non-blank line
15455 cx.set_state(indoc!(
15456 "fn a() {
15457 ˇdog();
15458 cat();
15459 }"
15460 ));
15461 cx.update_editor(|editor, window, cx| {
15462 editor.toggle_comments(toggle_comments, window, cx);
15463 });
15464 cx.assert_editor_state(indoc!(
15465 "fn a() {
15466 // dog();
15467 catˇ();
15468 }"
15469 ));
15470
15471 // Single selection on one line -> don't advance
15472 cx.set_state(indoc!(
15473 "fn a() {
15474 «dog()ˇ»;
15475 cat();
15476 }"
15477 ));
15478 cx.update_editor(|editor, window, cx| {
15479 editor.toggle_comments(toggle_comments, window, cx);
15480 });
15481 cx.assert_editor_state(indoc!(
15482 "fn a() {
15483 // «dog()ˇ»;
15484 cat();
15485 }"
15486 ));
15487
15488 // Multiple cursors on one line -> advance
15489 cx.set_state(indoc!(
15490 "fn a() {
15491 ˇdˇog();
15492 cat();
15493 }"
15494 ));
15495 cx.update_editor(|editor, window, cx| {
15496 editor.toggle_comments(toggle_comments, window, cx);
15497 });
15498 cx.assert_editor_state(indoc!(
15499 "fn a() {
15500 // dog();
15501 catˇ(ˇ);
15502 }"
15503 ));
15504
15505 // Multiple cursors on one line, with selection -> don't advance
15506 cx.set_state(indoc!(
15507 "fn a() {
15508 ˇdˇog«()ˇ»;
15509 cat();
15510 }"
15511 ));
15512 cx.update_editor(|editor, window, cx| {
15513 editor.toggle_comments(toggle_comments, window, cx);
15514 });
15515 cx.assert_editor_state(indoc!(
15516 "fn a() {
15517 // ˇdˇog«()ˇ»;
15518 cat();
15519 }"
15520 ));
15521
15522 // Single cursor on one line -> advance
15523 // Cursor moves to column 0 on blank line
15524 cx.set_state(indoc!(
15525 "fn a() {
15526 ˇdog();
15527
15528 cat();
15529 }"
15530 ));
15531 cx.update_editor(|editor, window, cx| {
15532 editor.toggle_comments(toggle_comments, window, cx);
15533 });
15534 cx.assert_editor_state(indoc!(
15535 "fn a() {
15536 // dog();
15537 ˇ
15538 cat();
15539 }"
15540 ));
15541
15542 // Single cursor on one line -> advance
15543 // Cursor starts and ends at column 0
15544 cx.set_state(indoc!(
15545 "fn a() {
15546 ˇ dog();
15547 cat();
15548 }"
15549 ));
15550 cx.update_editor(|editor, window, cx| {
15551 editor.toggle_comments(toggle_comments, window, cx);
15552 });
15553 cx.assert_editor_state(indoc!(
15554 "fn a() {
15555 // dog();
15556 ˇ cat();
15557 }"
15558 ));
15559}
15560
15561#[gpui::test]
15562async fn test_toggle_block_comment(cx: &mut TestAppContext) {
15563 init_test(cx, |_| {});
15564
15565 let mut cx = EditorTestContext::new(cx).await;
15566
15567 let html_language = Arc::new(
15568 Language::new(
15569 LanguageConfig {
15570 name: "HTML".into(),
15571 block_comment: Some(BlockCommentConfig {
15572 start: "<!-- ".into(),
15573 prefix: "".into(),
15574 end: " -->".into(),
15575 tab_size: 0,
15576 }),
15577 ..Default::default()
15578 },
15579 Some(tree_sitter_html::LANGUAGE.into()),
15580 )
15581 .with_injection_query(
15582 r#"
15583 (script_element
15584 (raw_text) @injection.content
15585 (#set! injection.language "javascript"))
15586 "#,
15587 )
15588 .unwrap(),
15589 );
15590
15591 let javascript_language = Arc::new(Language::new(
15592 LanguageConfig {
15593 name: "JavaScript".into(),
15594 line_comments: vec!["// ".into()],
15595 ..Default::default()
15596 },
15597 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
15598 ));
15599
15600 cx.language_registry().add(html_language.clone());
15601 cx.language_registry().add(javascript_language);
15602 cx.update_buffer(|buffer, cx| {
15603 buffer.set_language(Some(html_language), cx);
15604 });
15605
15606 // Toggle comments for empty selections
15607 cx.set_state(
15608 &r#"
15609 <p>A</p>ˇ
15610 <p>B</p>ˇ
15611 <p>C</p>ˇ
15612 "#
15613 .unindent(),
15614 );
15615 cx.update_editor(|editor, window, cx| {
15616 editor.toggle_comments(&ToggleComments::default(), window, cx)
15617 });
15618 cx.assert_editor_state(
15619 &r#"
15620 <!-- <p>A</p>ˇ -->
15621 <!-- <p>B</p>ˇ -->
15622 <!-- <p>C</p>ˇ -->
15623 "#
15624 .unindent(),
15625 );
15626 cx.update_editor(|editor, window, cx| {
15627 editor.toggle_comments(&ToggleComments::default(), window, cx)
15628 });
15629 cx.assert_editor_state(
15630 &r#"
15631 <p>A</p>ˇ
15632 <p>B</p>ˇ
15633 <p>C</p>ˇ
15634 "#
15635 .unindent(),
15636 );
15637
15638 // Toggle comments for mixture of empty and non-empty selections, where
15639 // multiple selections occupy a given line.
15640 cx.set_state(
15641 &r#"
15642 <p>A«</p>
15643 <p>ˇ»B</p>ˇ
15644 <p>C«</p>
15645 <p>ˇ»D</p>ˇ
15646 "#
15647 .unindent(),
15648 );
15649
15650 cx.update_editor(|editor, window, cx| {
15651 editor.toggle_comments(&ToggleComments::default(), window, cx)
15652 });
15653 cx.assert_editor_state(
15654 &r#"
15655 <!-- <p>A«</p>
15656 <p>ˇ»B</p>ˇ -->
15657 <!-- <p>C«</p>
15658 <p>ˇ»D</p>ˇ -->
15659 "#
15660 .unindent(),
15661 );
15662 cx.update_editor(|editor, window, cx| {
15663 editor.toggle_comments(&ToggleComments::default(), window, cx)
15664 });
15665 cx.assert_editor_state(
15666 &r#"
15667 <p>A«</p>
15668 <p>ˇ»B</p>ˇ
15669 <p>C«</p>
15670 <p>ˇ»D</p>ˇ
15671 "#
15672 .unindent(),
15673 );
15674
15675 // Toggle comments when different languages are active for different
15676 // selections.
15677 cx.set_state(
15678 &r#"
15679 ˇ<script>
15680 ˇvar x = new Y();
15681 ˇ</script>
15682 "#
15683 .unindent(),
15684 );
15685 cx.executor().run_until_parked();
15686 cx.update_editor(|editor, window, cx| {
15687 editor.toggle_comments(&ToggleComments::default(), window, cx)
15688 });
15689 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
15690 // Uncommenting and commenting from this position brings in even more wrong artifacts.
15691 cx.assert_editor_state(
15692 &r#"
15693 <!-- ˇ<script> -->
15694 // ˇvar x = new Y();
15695 <!-- ˇ</script> -->
15696 "#
15697 .unindent(),
15698 );
15699}
15700
15701#[gpui::test]
15702fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
15703 init_test(cx, |_| {});
15704
15705 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
15706 let multibuffer = cx.new(|cx| {
15707 let mut multibuffer = MultiBuffer::new(ReadWrite);
15708 multibuffer.push_excerpts(
15709 buffer.clone(),
15710 [
15711 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
15712 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
15713 ],
15714 cx,
15715 );
15716 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
15717 multibuffer
15718 });
15719
15720 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
15721 editor.update_in(cx, |editor, window, cx| {
15722 assert_eq!(editor.text(cx), "aaaa\nbbbb");
15723 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15724 s.select_ranges([
15725 Point::new(0, 0)..Point::new(0, 0),
15726 Point::new(1, 0)..Point::new(1, 0),
15727 ])
15728 });
15729
15730 editor.handle_input("X", window, cx);
15731 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
15732 assert_eq!(
15733 editor.selections.ranges(cx),
15734 [
15735 Point::new(0, 1)..Point::new(0, 1),
15736 Point::new(1, 1)..Point::new(1, 1),
15737 ]
15738 );
15739
15740 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
15741 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15742 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
15743 });
15744 editor.backspace(&Default::default(), window, cx);
15745 assert_eq!(editor.text(cx), "Xa\nbbb");
15746 assert_eq!(
15747 editor.selections.ranges(cx),
15748 [Point::new(1, 0)..Point::new(1, 0)]
15749 );
15750
15751 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15752 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
15753 });
15754 editor.backspace(&Default::default(), window, cx);
15755 assert_eq!(editor.text(cx), "X\nbb");
15756 assert_eq!(
15757 editor.selections.ranges(cx),
15758 [Point::new(0, 1)..Point::new(0, 1)]
15759 );
15760 });
15761}
15762
15763#[gpui::test]
15764fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
15765 init_test(cx, |_| {});
15766
15767 let markers = vec![('[', ']').into(), ('(', ')').into()];
15768 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
15769 indoc! {"
15770 [aaaa
15771 (bbbb]
15772 cccc)",
15773 },
15774 markers.clone(),
15775 );
15776 let excerpt_ranges = markers.into_iter().map(|marker| {
15777 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
15778 ExcerptRange::new(context)
15779 });
15780 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
15781 let multibuffer = cx.new(|cx| {
15782 let mut multibuffer = MultiBuffer::new(ReadWrite);
15783 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
15784 multibuffer
15785 });
15786
15787 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
15788 editor.update_in(cx, |editor, window, cx| {
15789 let (expected_text, selection_ranges) = marked_text_ranges(
15790 indoc! {"
15791 aaaa
15792 bˇbbb
15793 bˇbbˇb
15794 cccc"
15795 },
15796 true,
15797 );
15798 assert_eq!(editor.text(cx), expected_text);
15799 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15800 s.select_ranges(selection_ranges)
15801 });
15802
15803 editor.handle_input("X", window, cx);
15804
15805 let (expected_text, expected_selections) = marked_text_ranges(
15806 indoc! {"
15807 aaaa
15808 bXˇbbXb
15809 bXˇbbXˇb
15810 cccc"
15811 },
15812 false,
15813 );
15814 assert_eq!(editor.text(cx), expected_text);
15815 assert_eq!(editor.selections.ranges(cx), expected_selections);
15816
15817 editor.newline(&Newline, window, cx);
15818 let (expected_text, expected_selections) = marked_text_ranges(
15819 indoc! {"
15820 aaaa
15821 bX
15822 ˇbbX
15823 b
15824 bX
15825 ˇbbX
15826 ˇb
15827 cccc"
15828 },
15829 false,
15830 );
15831 assert_eq!(editor.text(cx), expected_text);
15832 assert_eq!(editor.selections.ranges(cx), expected_selections);
15833 });
15834}
15835
15836#[gpui::test]
15837fn test_refresh_selections(cx: &mut TestAppContext) {
15838 init_test(cx, |_| {});
15839
15840 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
15841 let mut excerpt1_id = None;
15842 let multibuffer = cx.new(|cx| {
15843 let mut multibuffer = MultiBuffer::new(ReadWrite);
15844 excerpt1_id = multibuffer
15845 .push_excerpts(
15846 buffer.clone(),
15847 [
15848 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
15849 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
15850 ],
15851 cx,
15852 )
15853 .into_iter()
15854 .next();
15855 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
15856 multibuffer
15857 });
15858
15859 let editor = cx.add_window(|window, cx| {
15860 let mut editor = build_editor(multibuffer.clone(), window, cx);
15861 let snapshot = editor.snapshot(window, cx);
15862 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15863 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
15864 });
15865 editor.begin_selection(
15866 Point::new(2, 1).to_display_point(&snapshot),
15867 true,
15868 1,
15869 window,
15870 cx,
15871 );
15872 assert_eq!(
15873 editor.selections.ranges(cx),
15874 [
15875 Point::new(1, 3)..Point::new(1, 3),
15876 Point::new(2, 1)..Point::new(2, 1),
15877 ]
15878 );
15879 editor
15880 });
15881
15882 // Refreshing selections is a no-op when excerpts haven't changed.
15883 _ = editor.update(cx, |editor, window, cx| {
15884 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
15885 assert_eq!(
15886 editor.selections.ranges(cx),
15887 [
15888 Point::new(1, 3)..Point::new(1, 3),
15889 Point::new(2, 1)..Point::new(2, 1),
15890 ]
15891 );
15892 });
15893
15894 multibuffer.update(cx, |multibuffer, cx| {
15895 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
15896 });
15897 _ = editor.update(cx, |editor, window, cx| {
15898 // Removing an excerpt causes the first selection to become degenerate.
15899 assert_eq!(
15900 editor.selections.ranges(cx),
15901 [
15902 Point::new(0, 0)..Point::new(0, 0),
15903 Point::new(0, 1)..Point::new(0, 1)
15904 ]
15905 );
15906
15907 // Refreshing selections will relocate the first selection to the original buffer
15908 // location.
15909 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
15910 assert_eq!(
15911 editor.selections.ranges(cx),
15912 [
15913 Point::new(0, 1)..Point::new(0, 1),
15914 Point::new(0, 3)..Point::new(0, 3)
15915 ]
15916 );
15917 assert!(editor.selections.pending_anchor().is_some());
15918 });
15919}
15920
15921#[gpui::test]
15922fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
15923 init_test(cx, |_| {});
15924
15925 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
15926 let mut excerpt1_id = None;
15927 let multibuffer = cx.new(|cx| {
15928 let mut multibuffer = MultiBuffer::new(ReadWrite);
15929 excerpt1_id = multibuffer
15930 .push_excerpts(
15931 buffer.clone(),
15932 [
15933 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
15934 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
15935 ],
15936 cx,
15937 )
15938 .into_iter()
15939 .next();
15940 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
15941 multibuffer
15942 });
15943
15944 let editor = cx.add_window(|window, cx| {
15945 let mut editor = build_editor(multibuffer.clone(), window, cx);
15946 let snapshot = editor.snapshot(window, cx);
15947 editor.begin_selection(
15948 Point::new(1, 3).to_display_point(&snapshot),
15949 false,
15950 1,
15951 window,
15952 cx,
15953 );
15954 assert_eq!(
15955 editor.selections.ranges(cx),
15956 [Point::new(1, 3)..Point::new(1, 3)]
15957 );
15958 editor
15959 });
15960
15961 multibuffer.update(cx, |multibuffer, cx| {
15962 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
15963 });
15964 _ = editor.update(cx, |editor, window, cx| {
15965 assert_eq!(
15966 editor.selections.ranges(cx),
15967 [Point::new(0, 0)..Point::new(0, 0)]
15968 );
15969
15970 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
15971 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
15972 assert_eq!(
15973 editor.selections.ranges(cx),
15974 [Point::new(0, 3)..Point::new(0, 3)]
15975 );
15976 assert!(editor.selections.pending_anchor().is_some());
15977 });
15978}
15979
15980#[gpui::test]
15981async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
15982 init_test(cx, |_| {});
15983
15984 let language = Arc::new(
15985 Language::new(
15986 LanguageConfig {
15987 brackets: BracketPairConfig {
15988 pairs: vec![
15989 BracketPair {
15990 start: "{".to_string(),
15991 end: "}".to_string(),
15992 close: true,
15993 surround: true,
15994 newline: true,
15995 },
15996 BracketPair {
15997 start: "/* ".to_string(),
15998 end: " */".to_string(),
15999 close: true,
16000 surround: true,
16001 newline: true,
16002 },
16003 ],
16004 ..Default::default()
16005 },
16006 ..Default::default()
16007 },
16008 Some(tree_sitter_rust::LANGUAGE.into()),
16009 )
16010 .with_indents_query("")
16011 .unwrap(),
16012 );
16013
16014 let text = concat!(
16015 "{ }\n", //
16016 " x\n", //
16017 " /* */\n", //
16018 "x\n", //
16019 "{{} }\n", //
16020 );
16021
16022 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
16023 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
16024 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
16025 editor
16026 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
16027 .await;
16028
16029 editor.update_in(cx, |editor, window, cx| {
16030 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16031 s.select_display_ranges([
16032 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
16033 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
16034 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
16035 ])
16036 });
16037 editor.newline(&Newline, window, cx);
16038
16039 assert_eq!(
16040 editor.buffer().read(cx).read(cx).text(),
16041 concat!(
16042 "{ \n", // Suppress rustfmt
16043 "\n", //
16044 "}\n", //
16045 " x\n", //
16046 " /* \n", //
16047 " \n", //
16048 " */\n", //
16049 "x\n", //
16050 "{{} \n", //
16051 "}\n", //
16052 )
16053 );
16054 });
16055}
16056
16057#[gpui::test]
16058fn test_highlighted_ranges(cx: &mut TestAppContext) {
16059 init_test(cx, |_| {});
16060
16061 let editor = cx.add_window(|window, cx| {
16062 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
16063 build_editor(buffer, window, cx)
16064 });
16065
16066 _ = editor.update(cx, |editor, window, cx| {
16067 struct Type1;
16068 struct Type2;
16069
16070 let buffer = editor.buffer.read(cx).snapshot(cx);
16071
16072 let anchor_range =
16073 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
16074
16075 editor.highlight_background::<Type1>(
16076 &[
16077 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
16078 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
16079 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
16080 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
16081 ],
16082 |_| Hsla::red(),
16083 cx,
16084 );
16085 editor.highlight_background::<Type2>(
16086 &[
16087 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
16088 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
16089 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
16090 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
16091 ],
16092 |_| Hsla::green(),
16093 cx,
16094 );
16095
16096 let snapshot = editor.snapshot(window, cx);
16097 let highlighted_ranges = editor.sorted_background_highlights_in_range(
16098 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
16099 &snapshot,
16100 cx.theme(),
16101 );
16102 assert_eq!(
16103 highlighted_ranges,
16104 &[
16105 (
16106 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
16107 Hsla::green(),
16108 ),
16109 (
16110 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
16111 Hsla::red(),
16112 ),
16113 (
16114 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
16115 Hsla::green(),
16116 ),
16117 (
16118 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16119 Hsla::red(),
16120 ),
16121 ]
16122 );
16123 assert_eq!(
16124 editor.sorted_background_highlights_in_range(
16125 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
16126 &snapshot,
16127 cx.theme(),
16128 ),
16129 &[(
16130 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16131 Hsla::red(),
16132 )]
16133 );
16134 });
16135}
16136
16137#[gpui::test]
16138async fn test_following(cx: &mut TestAppContext) {
16139 init_test(cx, |_| {});
16140
16141 let fs = FakeFs::new(cx.executor());
16142 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16143
16144 let buffer = project.update(cx, |project, cx| {
16145 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, false, cx);
16146 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
16147 });
16148 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
16149 let follower = cx.update(|cx| {
16150 cx.open_window(
16151 WindowOptions {
16152 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
16153 gpui::Point::new(px(0.), px(0.)),
16154 gpui::Point::new(px(10.), px(80.)),
16155 ))),
16156 ..Default::default()
16157 },
16158 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
16159 )
16160 .unwrap()
16161 });
16162
16163 let is_still_following = Rc::new(RefCell::new(true));
16164 let follower_edit_event_count = Rc::new(RefCell::new(0));
16165 let pending_update = Rc::new(RefCell::new(None));
16166 let leader_entity = leader.root(cx).unwrap();
16167 let follower_entity = follower.root(cx).unwrap();
16168 _ = follower.update(cx, {
16169 let update = pending_update.clone();
16170 let is_still_following = is_still_following.clone();
16171 let follower_edit_event_count = follower_edit_event_count.clone();
16172 |_, window, cx| {
16173 cx.subscribe_in(
16174 &leader_entity,
16175 window,
16176 move |_, leader, event, window, cx| {
16177 leader.read(cx).add_event_to_update_proto(
16178 event,
16179 &mut update.borrow_mut(),
16180 window,
16181 cx,
16182 );
16183 },
16184 )
16185 .detach();
16186
16187 cx.subscribe_in(
16188 &follower_entity,
16189 window,
16190 move |_, _, event: &EditorEvent, _window, _cx| {
16191 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
16192 *is_still_following.borrow_mut() = false;
16193 }
16194
16195 if let EditorEvent::BufferEdited = event {
16196 *follower_edit_event_count.borrow_mut() += 1;
16197 }
16198 },
16199 )
16200 .detach();
16201 }
16202 });
16203
16204 // Update the selections only
16205 _ = leader.update(cx, |leader, window, cx| {
16206 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16207 s.select_ranges([1..1])
16208 });
16209 });
16210 follower
16211 .update(cx, |follower, window, cx| {
16212 follower.apply_update_proto(
16213 &project,
16214 pending_update.borrow_mut().take().unwrap(),
16215 window,
16216 cx,
16217 )
16218 })
16219 .unwrap()
16220 .await
16221 .unwrap();
16222 _ = follower.update(cx, |follower, _, cx| {
16223 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
16224 });
16225 assert!(*is_still_following.borrow());
16226 assert_eq!(*follower_edit_event_count.borrow(), 0);
16227
16228 // Update the scroll position only
16229 _ = leader.update(cx, |leader, window, cx| {
16230 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16231 });
16232 follower
16233 .update(cx, |follower, window, cx| {
16234 follower.apply_update_proto(
16235 &project,
16236 pending_update.borrow_mut().take().unwrap(),
16237 window,
16238 cx,
16239 )
16240 })
16241 .unwrap()
16242 .await
16243 .unwrap();
16244 assert_eq!(
16245 follower
16246 .update(cx, |follower, _, cx| follower.scroll_position(cx))
16247 .unwrap(),
16248 gpui::Point::new(1.5, 3.5)
16249 );
16250 assert!(*is_still_following.borrow());
16251 assert_eq!(*follower_edit_event_count.borrow(), 0);
16252
16253 // Update the selections and scroll position. The follower's scroll position is updated
16254 // via autoscroll, not via the leader's exact scroll position.
16255 _ = leader.update(cx, |leader, window, cx| {
16256 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16257 s.select_ranges([0..0])
16258 });
16259 leader.request_autoscroll(Autoscroll::newest(), cx);
16260 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16261 });
16262 follower
16263 .update(cx, |follower, window, cx| {
16264 follower.apply_update_proto(
16265 &project,
16266 pending_update.borrow_mut().take().unwrap(),
16267 window,
16268 cx,
16269 )
16270 })
16271 .unwrap()
16272 .await
16273 .unwrap();
16274 _ = follower.update(cx, |follower, _, cx| {
16275 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
16276 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
16277 });
16278 assert!(*is_still_following.borrow());
16279
16280 // Creating a pending selection that precedes another selection
16281 _ = leader.update(cx, |leader, window, cx| {
16282 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16283 s.select_ranges([1..1])
16284 });
16285 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
16286 });
16287 follower
16288 .update(cx, |follower, window, cx| {
16289 follower.apply_update_proto(
16290 &project,
16291 pending_update.borrow_mut().take().unwrap(),
16292 window,
16293 cx,
16294 )
16295 })
16296 .unwrap()
16297 .await
16298 .unwrap();
16299 _ = follower.update(cx, |follower, _, cx| {
16300 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
16301 });
16302 assert!(*is_still_following.borrow());
16303
16304 // Extend the pending selection so that it surrounds another selection
16305 _ = leader.update(cx, |leader, window, cx| {
16306 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
16307 });
16308 follower
16309 .update(cx, |follower, window, cx| {
16310 follower.apply_update_proto(
16311 &project,
16312 pending_update.borrow_mut().take().unwrap(),
16313 window,
16314 cx,
16315 )
16316 })
16317 .unwrap()
16318 .await
16319 .unwrap();
16320 _ = follower.update(cx, |follower, _, cx| {
16321 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
16322 });
16323
16324 // Scrolling locally breaks the follow
16325 _ = follower.update(cx, |follower, window, cx| {
16326 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
16327 follower.set_scroll_anchor(
16328 ScrollAnchor {
16329 anchor: top_anchor,
16330 offset: gpui::Point::new(0.0, 0.5),
16331 },
16332 window,
16333 cx,
16334 );
16335 });
16336 assert!(!(*is_still_following.borrow()));
16337}
16338
16339#[gpui::test]
16340async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
16341 init_test(cx, |_| {});
16342
16343 let fs = FakeFs::new(cx.executor());
16344 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16345 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16346 let pane = workspace
16347 .update(cx, |workspace, _, _| workspace.active_pane().clone())
16348 .unwrap();
16349
16350 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16351
16352 let leader = pane.update_in(cx, |_, window, cx| {
16353 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
16354 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
16355 });
16356
16357 // Start following the editor when it has no excerpts.
16358 let mut state_message =
16359 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16360 let workspace_entity = workspace.root(cx).unwrap();
16361 let follower_1 = cx
16362 .update_window(*workspace.deref(), |_, window, cx| {
16363 Editor::from_state_proto(
16364 workspace_entity,
16365 ViewId {
16366 creator: CollaboratorId::PeerId(PeerId::default()),
16367 id: 0,
16368 },
16369 &mut state_message,
16370 window,
16371 cx,
16372 )
16373 })
16374 .unwrap()
16375 .unwrap()
16376 .await
16377 .unwrap();
16378
16379 let update_message = Rc::new(RefCell::new(None));
16380 follower_1.update_in(cx, {
16381 let update = update_message.clone();
16382 |_, window, cx| {
16383 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
16384 leader.read(cx).add_event_to_update_proto(
16385 event,
16386 &mut update.borrow_mut(),
16387 window,
16388 cx,
16389 );
16390 })
16391 .detach();
16392 }
16393 });
16394
16395 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
16396 (
16397 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, false, cx),
16398 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, false, cx),
16399 )
16400 });
16401
16402 // Insert some excerpts.
16403 leader.update(cx, |leader, cx| {
16404 leader.buffer.update(cx, |multibuffer, cx| {
16405 multibuffer.set_excerpts_for_path(
16406 PathKey::namespaced(1, "b.txt".into()),
16407 buffer_1.clone(),
16408 vec![
16409 Point::row_range(0..3),
16410 Point::row_range(1..6),
16411 Point::row_range(12..15),
16412 ],
16413 0,
16414 cx,
16415 );
16416 multibuffer.set_excerpts_for_path(
16417 PathKey::namespaced(1, "a.txt".into()),
16418 buffer_2.clone(),
16419 vec![Point::row_range(0..6), Point::row_range(8..12)],
16420 0,
16421 cx,
16422 );
16423 });
16424 });
16425
16426 // Apply the update of adding the excerpts.
16427 follower_1
16428 .update_in(cx, |follower, window, cx| {
16429 follower.apply_update_proto(
16430 &project,
16431 update_message.borrow().clone().unwrap(),
16432 window,
16433 cx,
16434 )
16435 })
16436 .await
16437 .unwrap();
16438 assert_eq!(
16439 follower_1.update(cx, |editor, cx| editor.text(cx)),
16440 leader.update(cx, |editor, cx| editor.text(cx))
16441 );
16442 update_message.borrow_mut().take();
16443
16444 // Start following separately after it already has excerpts.
16445 let mut state_message =
16446 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16447 let workspace_entity = workspace.root(cx).unwrap();
16448 let follower_2 = cx
16449 .update_window(*workspace.deref(), |_, window, cx| {
16450 Editor::from_state_proto(
16451 workspace_entity,
16452 ViewId {
16453 creator: CollaboratorId::PeerId(PeerId::default()),
16454 id: 0,
16455 },
16456 &mut state_message,
16457 window,
16458 cx,
16459 )
16460 })
16461 .unwrap()
16462 .unwrap()
16463 .await
16464 .unwrap();
16465 assert_eq!(
16466 follower_2.update(cx, |editor, cx| editor.text(cx)),
16467 leader.update(cx, |editor, cx| editor.text(cx))
16468 );
16469
16470 // Remove some excerpts.
16471 leader.update(cx, |leader, cx| {
16472 leader.buffer.update(cx, |multibuffer, cx| {
16473 let excerpt_ids = multibuffer.excerpt_ids();
16474 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
16475 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
16476 });
16477 });
16478
16479 // Apply the update of removing the excerpts.
16480 follower_1
16481 .update_in(cx, |follower, window, cx| {
16482 follower.apply_update_proto(
16483 &project,
16484 update_message.borrow().clone().unwrap(),
16485 window,
16486 cx,
16487 )
16488 })
16489 .await
16490 .unwrap();
16491 follower_2
16492 .update_in(cx, |follower, window, cx| {
16493 follower.apply_update_proto(
16494 &project,
16495 update_message.borrow().clone().unwrap(),
16496 window,
16497 cx,
16498 )
16499 })
16500 .await
16501 .unwrap();
16502 update_message.borrow_mut().take();
16503 assert_eq!(
16504 follower_1.update(cx, |editor, cx| editor.text(cx)),
16505 leader.update(cx, |editor, cx| editor.text(cx))
16506 );
16507}
16508
16509#[gpui::test]
16510async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16511 init_test(cx, |_| {});
16512
16513 let mut cx = EditorTestContext::new(cx).await;
16514 let lsp_store =
16515 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
16516
16517 cx.set_state(indoc! {"
16518 ˇfn func(abc def: i32) -> u32 {
16519 }
16520 "});
16521
16522 cx.update(|_, cx| {
16523 lsp_store.update(cx, |lsp_store, cx| {
16524 lsp_store
16525 .update_diagnostics(
16526 LanguageServerId(0),
16527 lsp::PublishDiagnosticsParams {
16528 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
16529 version: None,
16530 diagnostics: vec![
16531 lsp::Diagnostic {
16532 range: lsp::Range::new(
16533 lsp::Position::new(0, 11),
16534 lsp::Position::new(0, 12),
16535 ),
16536 severity: Some(lsp::DiagnosticSeverity::ERROR),
16537 ..Default::default()
16538 },
16539 lsp::Diagnostic {
16540 range: lsp::Range::new(
16541 lsp::Position::new(0, 12),
16542 lsp::Position::new(0, 15),
16543 ),
16544 severity: Some(lsp::DiagnosticSeverity::ERROR),
16545 ..Default::default()
16546 },
16547 lsp::Diagnostic {
16548 range: lsp::Range::new(
16549 lsp::Position::new(0, 25),
16550 lsp::Position::new(0, 28),
16551 ),
16552 severity: Some(lsp::DiagnosticSeverity::ERROR),
16553 ..Default::default()
16554 },
16555 ],
16556 },
16557 None,
16558 DiagnosticSourceKind::Pushed,
16559 &[],
16560 cx,
16561 )
16562 .unwrap()
16563 });
16564 });
16565
16566 executor.run_until_parked();
16567
16568 cx.update_editor(|editor, window, cx| {
16569 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16570 });
16571
16572 cx.assert_editor_state(indoc! {"
16573 fn func(abc def: i32) -> ˇu32 {
16574 }
16575 "});
16576
16577 cx.update_editor(|editor, window, cx| {
16578 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16579 });
16580
16581 cx.assert_editor_state(indoc! {"
16582 fn func(abc ˇdef: i32) -> u32 {
16583 }
16584 "});
16585
16586 cx.update_editor(|editor, window, cx| {
16587 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16588 });
16589
16590 cx.assert_editor_state(indoc! {"
16591 fn func(abcˇ def: i32) -> u32 {
16592 }
16593 "});
16594
16595 cx.update_editor(|editor, window, cx| {
16596 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16597 });
16598
16599 cx.assert_editor_state(indoc! {"
16600 fn func(abc def: i32) -> ˇu32 {
16601 }
16602 "});
16603}
16604
16605#[gpui::test]
16606async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16607 init_test(cx, |_| {});
16608
16609 let mut cx = EditorTestContext::new(cx).await;
16610
16611 let diff_base = r#"
16612 use some::mod;
16613
16614 const A: u32 = 42;
16615
16616 fn main() {
16617 println!("hello");
16618
16619 println!("world");
16620 }
16621 "#
16622 .unindent();
16623
16624 // Edits are modified, removed, modified, added
16625 cx.set_state(
16626 &r#"
16627 use some::modified;
16628
16629 ˇ
16630 fn main() {
16631 println!("hello there");
16632
16633 println!("around the");
16634 println!("world");
16635 }
16636 "#
16637 .unindent(),
16638 );
16639
16640 cx.set_head_text(&diff_base);
16641 executor.run_until_parked();
16642
16643 cx.update_editor(|editor, window, cx| {
16644 //Wrap around the bottom of the buffer
16645 for _ in 0..3 {
16646 editor.go_to_next_hunk(&GoToHunk, window, cx);
16647 }
16648 });
16649
16650 cx.assert_editor_state(
16651 &r#"
16652 ˇuse some::modified;
16653
16654
16655 fn main() {
16656 println!("hello there");
16657
16658 println!("around the");
16659 println!("world");
16660 }
16661 "#
16662 .unindent(),
16663 );
16664
16665 cx.update_editor(|editor, window, cx| {
16666 //Wrap around the top of the buffer
16667 for _ in 0..2 {
16668 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16669 }
16670 });
16671
16672 cx.assert_editor_state(
16673 &r#"
16674 use some::modified;
16675
16676
16677 fn main() {
16678 ˇ println!("hello there");
16679
16680 println!("around the");
16681 println!("world");
16682 }
16683 "#
16684 .unindent(),
16685 );
16686
16687 cx.update_editor(|editor, window, cx| {
16688 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16689 });
16690
16691 cx.assert_editor_state(
16692 &r#"
16693 use some::modified;
16694
16695 ˇ
16696 fn main() {
16697 println!("hello there");
16698
16699 println!("around the");
16700 println!("world");
16701 }
16702 "#
16703 .unindent(),
16704 );
16705
16706 cx.update_editor(|editor, window, cx| {
16707 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16708 });
16709
16710 cx.assert_editor_state(
16711 &r#"
16712 ˇuse some::modified;
16713
16714
16715 fn main() {
16716 println!("hello there");
16717
16718 println!("around the");
16719 println!("world");
16720 }
16721 "#
16722 .unindent(),
16723 );
16724
16725 cx.update_editor(|editor, window, cx| {
16726 for _ in 0..2 {
16727 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16728 }
16729 });
16730
16731 cx.assert_editor_state(
16732 &r#"
16733 use some::modified;
16734
16735
16736 fn main() {
16737 ˇ println!("hello there");
16738
16739 println!("around the");
16740 println!("world");
16741 }
16742 "#
16743 .unindent(),
16744 );
16745
16746 cx.update_editor(|editor, window, cx| {
16747 editor.fold(&Fold, window, cx);
16748 });
16749
16750 cx.update_editor(|editor, window, cx| {
16751 editor.go_to_next_hunk(&GoToHunk, window, cx);
16752 });
16753
16754 cx.assert_editor_state(
16755 &r#"
16756 ˇuse some::modified;
16757
16758
16759 fn main() {
16760 println!("hello there");
16761
16762 println!("around the");
16763 println!("world");
16764 }
16765 "#
16766 .unindent(),
16767 );
16768}
16769
16770#[test]
16771fn test_split_words() {
16772 fn split(text: &str) -> Vec<&str> {
16773 split_words(text).collect()
16774 }
16775
16776 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
16777 assert_eq!(split("hello_world"), &["hello_", "world"]);
16778 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
16779 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
16780 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
16781 assert_eq!(split("helloworld"), &["helloworld"]);
16782
16783 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
16784}
16785
16786#[gpui::test]
16787async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
16788 init_test(cx, |_| {});
16789
16790 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
16791 let mut assert = |before, after| {
16792 let _state_context = cx.set_state(before);
16793 cx.run_until_parked();
16794 cx.update_editor(|editor, window, cx| {
16795 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
16796 });
16797 cx.run_until_parked();
16798 cx.assert_editor_state(after);
16799 };
16800
16801 // Outside bracket jumps to outside of matching bracket
16802 assert("console.logˇ(var);", "console.log(var)ˇ;");
16803 assert("console.log(var)ˇ;", "console.logˇ(var);");
16804
16805 // Inside bracket jumps to inside of matching bracket
16806 assert("console.log(ˇvar);", "console.log(varˇ);");
16807 assert("console.log(varˇ);", "console.log(ˇvar);");
16808
16809 // When outside a bracket and inside, favor jumping to the inside bracket
16810 assert(
16811 "console.log('foo', [1, 2, 3]ˇ);",
16812 "console.log(ˇ'foo', [1, 2, 3]);",
16813 );
16814 assert(
16815 "console.log(ˇ'foo', [1, 2, 3]);",
16816 "console.log('foo', [1, 2, 3]ˇ);",
16817 );
16818
16819 // Bias forward if two options are equally likely
16820 assert(
16821 "let result = curried_fun()ˇ();",
16822 "let result = curried_fun()()ˇ;",
16823 );
16824
16825 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
16826 assert(
16827 indoc! {"
16828 function test() {
16829 console.log('test')ˇ
16830 }"},
16831 indoc! {"
16832 function test() {
16833 console.logˇ('test')
16834 }"},
16835 );
16836}
16837
16838#[gpui::test]
16839async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
16840 init_test(cx, |_| {});
16841
16842 let fs = FakeFs::new(cx.executor());
16843 fs.insert_tree(
16844 path!("/a"),
16845 json!({
16846 "main.rs": "fn main() { let a = 5; }",
16847 "other.rs": "// Test file",
16848 }),
16849 )
16850 .await;
16851 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
16852
16853 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
16854 language_registry.add(Arc::new(Language::new(
16855 LanguageConfig {
16856 name: "Rust".into(),
16857 matcher: LanguageMatcher {
16858 path_suffixes: vec!["rs".to_string()],
16859 ..Default::default()
16860 },
16861 brackets: BracketPairConfig {
16862 pairs: vec![BracketPair {
16863 start: "{".to_string(),
16864 end: "}".to_string(),
16865 close: true,
16866 surround: true,
16867 newline: true,
16868 }],
16869 disabled_scopes_by_bracket_ix: Vec::new(),
16870 },
16871 ..Default::default()
16872 },
16873 Some(tree_sitter_rust::LANGUAGE.into()),
16874 )));
16875 let mut fake_servers = language_registry.register_fake_lsp(
16876 "Rust",
16877 FakeLspAdapter {
16878 capabilities: lsp::ServerCapabilities {
16879 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
16880 first_trigger_character: "{".to_string(),
16881 more_trigger_character: None,
16882 }),
16883 ..Default::default()
16884 },
16885 ..Default::default()
16886 },
16887 );
16888
16889 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16890
16891 let cx = &mut VisualTestContext::from_window(*workspace, cx);
16892
16893 let worktree_id = workspace
16894 .update(cx, |workspace, _, cx| {
16895 workspace.project().update(cx, |project, cx| {
16896 project.worktrees(cx).next().unwrap().read(cx).id()
16897 })
16898 })
16899 .unwrap();
16900
16901 let buffer = project
16902 .update(cx, |project, cx| {
16903 project.open_local_buffer(path!("/a/main.rs"), cx)
16904 })
16905 .await
16906 .unwrap();
16907 let editor_handle = workspace
16908 .update(cx, |workspace, window, cx| {
16909 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
16910 })
16911 .unwrap()
16912 .await
16913 .unwrap()
16914 .downcast::<Editor>()
16915 .unwrap();
16916
16917 cx.executor().start_waiting();
16918 let fake_server = fake_servers.next().await.unwrap();
16919
16920 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
16921 |params, _| async move {
16922 assert_eq!(
16923 params.text_document_position.text_document.uri,
16924 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
16925 );
16926 assert_eq!(
16927 params.text_document_position.position,
16928 lsp::Position::new(0, 21),
16929 );
16930
16931 Ok(Some(vec![lsp::TextEdit {
16932 new_text: "]".to_string(),
16933 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
16934 }]))
16935 },
16936 );
16937
16938 editor_handle.update_in(cx, |editor, window, cx| {
16939 window.focus(&editor.focus_handle(cx));
16940 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16941 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
16942 });
16943 editor.handle_input("{", window, cx);
16944 });
16945
16946 cx.executor().run_until_parked();
16947
16948 buffer.update(cx, |buffer, _| {
16949 assert_eq!(
16950 buffer.text(),
16951 "fn main() { let a = {5}; }",
16952 "No extra braces from on type formatting should appear in the buffer"
16953 )
16954 });
16955}
16956
16957#[gpui::test(iterations = 20, seeds(31))]
16958async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
16959 init_test(cx, |_| {});
16960
16961 let mut cx = EditorLspTestContext::new_rust(
16962 lsp::ServerCapabilities {
16963 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
16964 first_trigger_character: ".".to_string(),
16965 more_trigger_character: None,
16966 }),
16967 ..Default::default()
16968 },
16969 cx,
16970 )
16971 .await;
16972
16973 cx.update_buffer(|buffer, _| {
16974 // This causes autoindent to be async.
16975 buffer.set_sync_parse_timeout(Duration::ZERO)
16976 });
16977
16978 cx.set_state("fn c() {\n d()ˇ\n}\n");
16979 cx.simulate_keystroke("\n");
16980 cx.run_until_parked();
16981
16982 let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap());
16983 let mut request =
16984 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
16985 let buffer_cloned = buffer_cloned.clone();
16986 async move {
16987 buffer_cloned.update(&mut cx, |buffer, _| {
16988 assert_eq!(
16989 buffer.text(),
16990 "fn c() {\n d()\n .\n}\n",
16991 "OnTypeFormatting should triggered after autoindent applied"
16992 )
16993 })?;
16994
16995 Ok(Some(vec![]))
16996 }
16997 });
16998
16999 cx.simulate_keystroke(".");
17000 cx.run_until_parked();
17001
17002 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
17003 assert!(request.next().await.is_some());
17004 request.close();
17005 assert!(request.next().await.is_none());
17006}
17007
17008#[gpui::test]
17009async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
17010 init_test(cx, |_| {});
17011
17012 let fs = FakeFs::new(cx.executor());
17013 fs.insert_tree(
17014 path!("/a"),
17015 json!({
17016 "main.rs": "fn main() { let a = 5; }",
17017 "other.rs": "// Test file",
17018 }),
17019 )
17020 .await;
17021
17022 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17023
17024 let server_restarts = Arc::new(AtomicUsize::new(0));
17025 let closure_restarts = Arc::clone(&server_restarts);
17026 let language_server_name = "test language server";
17027 let language_name: LanguageName = "Rust".into();
17028
17029 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17030 language_registry.add(Arc::new(Language::new(
17031 LanguageConfig {
17032 name: language_name.clone(),
17033 matcher: LanguageMatcher {
17034 path_suffixes: vec!["rs".to_string()],
17035 ..Default::default()
17036 },
17037 ..Default::default()
17038 },
17039 Some(tree_sitter_rust::LANGUAGE.into()),
17040 )));
17041 let mut fake_servers = language_registry.register_fake_lsp(
17042 "Rust",
17043 FakeLspAdapter {
17044 name: language_server_name,
17045 initialization_options: Some(json!({
17046 "testOptionValue": true
17047 })),
17048 initializer: Some(Box::new(move |fake_server| {
17049 let task_restarts = Arc::clone(&closure_restarts);
17050 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
17051 task_restarts.fetch_add(1, atomic::Ordering::Release);
17052 futures::future::ready(Ok(()))
17053 });
17054 })),
17055 ..Default::default()
17056 },
17057 );
17058
17059 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17060 let _buffer = project
17061 .update(cx, |project, cx| {
17062 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
17063 })
17064 .await
17065 .unwrap();
17066 let _fake_server = fake_servers.next().await.unwrap();
17067 update_test_language_settings(cx, |language_settings| {
17068 language_settings.languages.0.insert(
17069 language_name.clone().0,
17070 LanguageSettingsContent {
17071 tab_size: NonZeroU32::new(8),
17072 ..Default::default()
17073 },
17074 );
17075 });
17076 cx.executor().run_until_parked();
17077 assert_eq!(
17078 server_restarts.load(atomic::Ordering::Acquire),
17079 0,
17080 "Should not restart LSP server on an unrelated change"
17081 );
17082
17083 update_test_project_settings(cx, |project_settings| {
17084 project_settings.lsp.insert(
17085 "Some other server name".into(),
17086 LspSettings {
17087 binary: None,
17088 settings: None,
17089 initialization_options: Some(json!({
17090 "some other init value": false
17091 })),
17092 enable_lsp_tasks: false,
17093 fetch: None,
17094 },
17095 );
17096 });
17097 cx.executor().run_until_parked();
17098 assert_eq!(
17099 server_restarts.load(atomic::Ordering::Acquire),
17100 0,
17101 "Should not restart LSP server on an unrelated LSP settings change"
17102 );
17103
17104 update_test_project_settings(cx, |project_settings| {
17105 project_settings.lsp.insert(
17106 language_server_name.into(),
17107 LspSettings {
17108 binary: None,
17109 settings: None,
17110 initialization_options: Some(json!({
17111 "anotherInitValue": false
17112 })),
17113 enable_lsp_tasks: false,
17114 fetch: None,
17115 },
17116 );
17117 });
17118 cx.executor().run_until_parked();
17119 assert_eq!(
17120 server_restarts.load(atomic::Ordering::Acquire),
17121 1,
17122 "Should restart LSP server on a related LSP settings change"
17123 );
17124
17125 update_test_project_settings(cx, |project_settings| {
17126 project_settings.lsp.insert(
17127 language_server_name.into(),
17128 LspSettings {
17129 binary: None,
17130 settings: None,
17131 initialization_options: Some(json!({
17132 "anotherInitValue": false
17133 })),
17134 enable_lsp_tasks: false,
17135 fetch: None,
17136 },
17137 );
17138 });
17139 cx.executor().run_until_parked();
17140 assert_eq!(
17141 server_restarts.load(atomic::Ordering::Acquire),
17142 1,
17143 "Should not restart LSP server on a related LSP settings change that is the same"
17144 );
17145
17146 update_test_project_settings(cx, |project_settings| {
17147 project_settings.lsp.insert(
17148 language_server_name.into(),
17149 LspSettings {
17150 binary: None,
17151 settings: None,
17152 initialization_options: None,
17153 enable_lsp_tasks: false,
17154 fetch: None,
17155 },
17156 );
17157 });
17158 cx.executor().run_until_parked();
17159 assert_eq!(
17160 server_restarts.load(atomic::Ordering::Acquire),
17161 2,
17162 "Should restart LSP server on another related LSP settings change"
17163 );
17164}
17165
17166#[gpui::test]
17167async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
17168 init_test(cx, |_| {});
17169
17170 let mut cx = EditorLspTestContext::new_rust(
17171 lsp::ServerCapabilities {
17172 completion_provider: Some(lsp::CompletionOptions {
17173 trigger_characters: Some(vec![".".to_string()]),
17174 resolve_provider: Some(true),
17175 ..Default::default()
17176 }),
17177 ..Default::default()
17178 },
17179 cx,
17180 )
17181 .await;
17182
17183 cx.set_state("fn main() { let a = 2ˇ; }");
17184 cx.simulate_keystroke(".");
17185 let completion_item = lsp::CompletionItem {
17186 label: "some".into(),
17187 kind: Some(lsp::CompletionItemKind::SNIPPET),
17188 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
17189 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
17190 kind: lsp::MarkupKind::Markdown,
17191 value: "```rust\nSome(2)\n```".to_string(),
17192 })),
17193 deprecated: Some(false),
17194 sort_text: Some("fffffff2".to_string()),
17195 filter_text: Some("some".to_string()),
17196 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
17197 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17198 range: lsp::Range {
17199 start: lsp::Position {
17200 line: 0,
17201 character: 22,
17202 },
17203 end: lsp::Position {
17204 line: 0,
17205 character: 22,
17206 },
17207 },
17208 new_text: "Some(2)".to_string(),
17209 })),
17210 additional_text_edits: Some(vec![lsp::TextEdit {
17211 range: lsp::Range {
17212 start: lsp::Position {
17213 line: 0,
17214 character: 20,
17215 },
17216 end: lsp::Position {
17217 line: 0,
17218 character: 22,
17219 },
17220 },
17221 new_text: "".to_string(),
17222 }]),
17223 ..Default::default()
17224 };
17225
17226 let closure_completion_item = completion_item.clone();
17227 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17228 let task_completion_item = closure_completion_item.clone();
17229 async move {
17230 Ok(Some(lsp::CompletionResponse::Array(vec![
17231 task_completion_item,
17232 ])))
17233 }
17234 });
17235
17236 request.next().await;
17237
17238 cx.condition(|editor, _| editor.context_menu_visible())
17239 .await;
17240 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
17241 editor
17242 .confirm_completion(&ConfirmCompletion::default(), window, cx)
17243 .unwrap()
17244 });
17245 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
17246
17247 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
17248 let task_completion_item = completion_item.clone();
17249 async move { Ok(task_completion_item) }
17250 })
17251 .next()
17252 .await
17253 .unwrap();
17254 apply_additional_edits.await.unwrap();
17255 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
17256}
17257
17258#[gpui::test]
17259async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
17260 init_test(cx, |_| {});
17261
17262 let mut cx = EditorLspTestContext::new_rust(
17263 lsp::ServerCapabilities {
17264 completion_provider: Some(lsp::CompletionOptions {
17265 trigger_characters: Some(vec![".".to_string()]),
17266 resolve_provider: Some(true),
17267 ..Default::default()
17268 }),
17269 ..Default::default()
17270 },
17271 cx,
17272 )
17273 .await;
17274
17275 cx.set_state("fn main() { let a = 2ˇ; }");
17276 cx.simulate_keystroke(".");
17277
17278 let item1 = lsp::CompletionItem {
17279 label: "method id()".to_string(),
17280 filter_text: Some("id".to_string()),
17281 detail: None,
17282 documentation: None,
17283 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17284 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17285 new_text: ".id".to_string(),
17286 })),
17287 ..lsp::CompletionItem::default()
17288 };
17289
17290 let item2 = lsp::CompletionItem {
17291 label: "other".to_string(),
17292 filter_text: Some("other".to_string()),
17293 detail: None,
17294 documentation: None,
17295 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17296 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17297 new_text: ".other".to_string(),
17298 })),
17299 ..lsp::CompletionItem::default()
17300 };
17301
17302 let item1 = item1.clone();
17303 cx.set_request_handler::<lsp::request::Completion, _, _>({
17304 let item1 = item1.clone();
17305 move |_, _, _| {
17306 let item1 = item1.clone();
17307 let item2 = item2.clone();
17308 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
17309 }
17310 })
17311 .next()
17312 .await;
17313
17314 cx.condition(|editor, _| editor.context_menu_visible())
17315 .await;
17316 cx.update_editor(|editor, _, _| {
17317 let context_menu = editor.context_menu.borrow_mut();
17318 let context_menu = context_menu
17319 .as_ref()
17320 .expect("Should have the context menu deployed");
17321 match context_menu {
17322 CodeContextMenu::Completions(completions_menu) => {
17323 let completions = completions_menu.completions.borrow_mut();
17324 assert_eq!(
17325 completions
17326 .iter()
17327 .map(|completion| &completion.label.text)
17328 .collect::<Vec<_>>(),
17329 vec!["method id()", "other"]
17330 )
17331 }
17332 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17333 }
17334 });
17335
17336 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
17337 let item1 = item1.clone();
17338 move |_, item_to_resolve, _| {
17339 let item1 = item1.clone();
17340 async move {
17341 if item1 == item_to_resolve {
17342 Ok(lsp::CompletionItem {
17343 label: "method id()".to_string(),
17344 filter_text: Some("id".to_string()),
17345 detail: Some("Now resolved!".to_string()),
17346 documentation: Some(lsp::Documentation::String("Docs".to_string())),
17347 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17348 range: lsp::Range::new(
17349 lsp::Position::new(0, 22),
17350 lsp::Position::new(0, 22),
17351 ),
17352 new_text: ".id".to_string(),
17353 })),
17354 ..lsp::CompletionItem::default()
17355 })
17356 } else {
17357 Ok(item_to_resolve)
17358 }
17359 }
17360 }
17361 })
17362 .next()
17363 .await
17364 .unwrap();
17365 cx.run_until_parked();
17366
17367 cx.update_editor(|editor, window, cx| {
17368 editor.context_menu_next(&Default::default(), window, cx);
17369 });
17370
17371 cx.update_editor(|editor, _, _| {
17372 let context_menu = editor.context_menu.borrow_mut();
17373 let context_menu = context_menu
17374 .as_ref()
17375 .expect("Should have the context menu deployed");
17376 match context_menu {
17377 CodeContextMenu::Completions(completions_menu) => {
17378 let completions = completions_menu.completions.borrow_mut();
17379 assert_eq!(
17380 completions
17381 .iter()
17382 .map(|completion| &completion.label.text)
17383 .collect::<Vec<_>>(),
17384 vec!["method id() Now resolved!", "other"],
17385 "Should update first completion label, but not second as the filter text did not match."
17386 );
17387 }
17388 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17389 }
17390 });
17391}
17392
17393#[gpui::test]
17394async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
17395 init_test(cx, |_| {});
17396 let mut cx = EditorLspTestContext::new_rust(
17397 lsp::ServerCapabilities {
17398 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
17399 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
17400 completion_provider: Some(lsp::CompletionOptions {
17401 resolve_provider: Some(true),
17402 ..Default::default()
17403 }),
17404 ..Default::default()
17405 },
17406 cx,
17407 )
17408 .await;
17409 cx.set_state(indoc! {"
17410 struct TestStruct {
17411 field: i32
17412 }
17413
17414 fn mainˇ() {
17415 let unused_var = 42;
17416 let test_struct = TestStruct { field: 42 };
17417 }
17418 "});
17419 let symbol_range = cx.lsp_range(indoc! {"
17420 struct TestStruct {
17421 field: i32
17422 }
17423
17424 «fn main»() {
17425 let unused_var = 42;
17426 let test_struct = TestStruct { field: 42 };
17427 }
17428 "});
17429 let mut hover_requests =
17430 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
17431 Ok(Some(lsp::Hover {
17432 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
17433 kind: lsp::MarkupKind::Markdown,
17434 value: "Function documentation".to_string(),
17435 }),
17436 range: Some(symbol_range),
17437 }))
17438 });
17439
17440 // Case 1: Test that code action menu hide hover popover
17441 cx.dispatch_action(Hover);
17442 hover_requests.next().await;
17443 cx.condition(|editor, _| editor.hover_state.visible()).await;
17444 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
17445 move |_, _, _| async move {
17446 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
17447 lsp::CodeAction {
17448 title: "Remove unused variable".to_string(),
17449 kind: Some(CodeActionKind::QUICKFIX),
17450 edit: Some(lsp::WorkspaceEdit {
17451 changes: Some(
17452 [(
17453 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
17454 vec![lsp::TextEdit {
17455 range: lsp::Range::new(
17456 lsp::Position::new(5, 4),
17457 lsp::Position::new(5, 27),
17458 ),
17459 new_text: "".to_string(),
17460 }],
17461 )]
17462 .into_iter()
17463 .collect(),
17464 ),
17465 ..Default::default()
17466 }),
17467 ..Default::default()
17468 },
17469 )]))
17470 },
17471 );
17472 cx.update_editor(|editor, window, cx| {
17473 editor.toggle_code_actions(
17474 &ToggleCodeActions {
17475 deployed_from: None,
17476 quick_launch: false,
17477 },
17478 window,
17479 cx,
17480 );
17481 });
17482 code_action_requests.next().await;
17483 cx.run_until_parked();
17484 cx.condition(|editor, _| editor.context_menu_visible())
17485 .await;
17486 cx.update_editor(|editor, _, _| {
17487 assert!(
17488 !editor.hover_state.visible(),
17489 "Hover popover should be hidden when code action menu is shown"
17490 );
17491 // Hide code actions
17492 editor.context_menu.take();
17493 });
17494
17495 // Case 2: Test that code completions hide hover popover
17496 cx.dispatch_action(Hover);
17497 hover_requests.next().await;
17498 cx.condition(|editor, _| editor.hover_state.visible()).await;
17499 let counter = Arc::new(AtomicUsize::new(0));
17500 let mut completion_requests =
17501 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17502 let counter = counter.clone();
17503 async move {
17504 counter.fetch_add(1, atomic::Ordering::Release);
17505 Ok(Some(lsp::CompletionResponse::Array(vec![
17506 lsp::CompletionItem {
17507 label: "main".into(),
17508 kind: Some(lsp::CompletionItemKind::FUNCTION),
17509 detail: Some("() -> ()".to_string()),
17510 ..Default::default()
17511 },
17512 lsp::CompletionItem {
17513 label: "TestStruct".into(),
17514 kind: Some(lsp::CompletionItemKind::STRUCT),
17515 detail: Some("struct TestStruct".to_string()),
17516 ..Default::default()
17517 },
17518 ])))
17519 }
17520 });
17521 cx.update_editor(|editor, window, cx| {
17522 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
17523 });
17524 completion_requests.next().await;
17525 cx.condition(|editor, _| editor.context_menu_visible())
17526 .await;
17527 cx.update_editor(|editor, _, _| {
17528 assert!(
17529 !editor.hover_state.visible(),
17530 "Hover popover should be hidden when completion menu is shown"
17531 );
17532 });
17533}
17534
17535#[gpui::test]
17536async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
17537 init_test(cx, |_| {});
17538
17539 let mut cx = EditorLspTestContext::new_rust(
17540 lsp::ServerCapabilities {
17541 completion_provider: Some(lsp::CompletionOptions {
17542 trigger_characters: Some(vec![".".to_string()]),
17543 resolve_provider: Some(true),
17544 ..Default::default()
17545 }),
17546 ..Default::default()
17547 },
17548 cx,
17549 )
17550 .await;
17551
17552 cx.set_state("fn main() { let a = 2ˇ; }");
17553 cx.simulate_keystroke(".");
17554
17555 let unresolved_item_1 = lsp::CompletionItem {
17556 label: "id".to_string(),
17557 filter_text: Some("id".to_string()),
17558 detail: None,
17559 documentation: None,
17560 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17561 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17562 new_text: ".id".to_string(),
17563 })),
17564 ..lsp::CompletionItem::default()
17565 };
17566 let resolved_item_1 = lsp::CompletionItem {
17567 additional_text_edits: Some(vec![lsp::TextEdit {
17568 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
17569 new_text: "!!".to_string(),
17570 }]),
17571 ..unresolved_item_1.clone()
17572 };
17573 let unresolved_item_2 = lsp::CompletionItem {
17574 label: "other".to_string(),
17575 filter_text: Some("other".to_string()),
17576 detail: None,
17577 documentation: None,
17578 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17579 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17580 new_text: ".other".to_string(),
17581 })),
17582 ..lsp::CompletionItem::default()
17583 };
17584 let resolved_item_2 = lsp::CompletionItem {
17585 additional_text_edits: Some(vec![lsp::TextEdit {
17586 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
17587 new_text: "??".to_string(),
17588 }]),
17589 ..unresolved_item_2.clone()
17590 };
17591
17592 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
17593 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
17594 cx.lsp
17595 .server
17596 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
17597 let unresolved_item_1 = unresolved_item_1.clone();
17598 let resolved_item_1 = resolved_item_1.clone();
17599 let unresolved_item_2 = unresolved_item_2.clone();
17600 let resolved_item_2 = resolved_item_2.clone();
17601 let resolve_requests_1 = resolve_requests_1.clone();
17602 let resolve_requests_2 = resolve_requests_2.clone();
17603 move |unresolved_request, _| {
17604 let unresolved_item_1 = unresolved_item_1.clone();
17605 let resolved_item_1 = resolved_item_1.clone();
17606 let unresolved_item_2 = unresolved_item_2.clone();
17607 let resolved_item_2 = resolved_item_2.clone();
17608 let resolve_requests_1 = resolve_requests_1.clone();
17609 let resolve_requests_2 = resolve_requests_2.clone();
17610 async move {
17611 if unresolved_request == unresolved_item_1 {
17612 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
17613 Ok(resolved_item_1.clone())
17614 } else if unresolved_request == unresolved_item_2 {
17615 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
17616 Ok(resolved_item_2.clone())
17617 } else {
17618 panic!("Unexpected completion item {unresolved_request:?}")
17619 }
17620 }
17621 }
17622 })
17623 .detach();
17624
17625 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17626 let unresolved_item_1 = unresolved_item_1.clone();
17627 let unresolved_item_2 = unresolved_item_2.clone();
17628 async move {
17629 Ok(Some(lsp::CompletionResponse::Array(vec![
17630 unresolved_item_1,
17631 unresolved_item_2,
17632 ])))
17633 }
17634 })
17635 .next()
17636 .await;
17637
17638 cx.condition(|editor, _| editor.context_menu_visible())
17639 .await;
17640 cx.update_editor(|editor, _, _| {
17641 let context_menu = editor.context_menu.borrow_mut();
17642 let context_menu = context_menu
17643 .as_ref()
17644 .expect("Should have the context menu deployed");
17645 match context_menu {
17646 CodeContextMenu::Completions(completions_menu) => {
17647 let completions = completions_menu.completions.borrow_mut();
17648 assert_eq!(
17649 completions
17650 .iter()
17651 .map(|completion| &completion.label.text)
17652 .collect::<Vec<_>>(),
17653 vec!["id", "other"]
17654 )
17655 }
17656 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17657 }
17658 });
17659 cx.run_until_parked();
17660
17661 cx.update_editor(|editor, window, cx| {
17662 editor.context_menu_next(&ContextMenuNext, window, cx);
17663 });
17664 cx.run_until_parked();
17665 cx.update_editor(|editor, window, cx| {
17666 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
17667 });
17668 cx.run_until_parked();
17669 cx.update_editor(|editor, window, cx| {
17670 editor.context_menu_next(&ContextMenuNext, window, cx);
17671 });
17672 cx.run_until_parked();
17673 cx.update_editor(|editor, window, cx| {
17674 editor
17675 .compose_completion(&ComposeCompletion::default(), window, cx)
17676 .expect("No task returned")
17677 })
17678 .await
17679 .expect("Completion failed");
17680 cx.run_until_parked();
17681
17682 cx.update_editor(|editor, _, cx| {
17683 assert_eq!(
17684 resolve_requests_1.load(atomic::Ordering::Acquire),
17685 1,
17686 "Should always resolve once despite multiple selections"
17687 );
17688 assert_eq!(
17689 resolve_requests_2.load(atomic::Ordering::Acquire),
17690 1,
17691 "Should always resolve once after multiple selections and applying the completion"
17692 );
17693 assert_eq!(
17694 editor.text(cx),
17695 "fn main() { let a = ??.other; }",
17696 "Should use resolved data when applying the completion"
17697 );
17698 });
17699}
17700
17701#[gpui::test]
17702async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
17703 init_test(cx, |_| {});
17704
17705 let item_0 = lsp::CompletionItem {
17706 label: "abs".into(),
17707 insert_text: Some("abs".into()),
17708 data: Some(json!({ "very": "special"})),
17709 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
17710 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
17711 lsp::InsertReplaceEdit {
17712 new_text: "abs".to_string(),
17713 insert: lsp::Range::default(),
17714 replace: lsp::Range::default(),
17715 },
17716 )),
17717 ..lsp::CompletionItem::default()
17718 };
17719 let items = iter::once(item_0.clone())
17720 .chain((11..51).map(|i| lsp::CompletionItem {
17721 label: format!("item_{}", i),
17722 insert_text: Some(format!("item_{}", i)),
17723 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
17724 ..lsp::CompletionItem::default()
17725 }))
17726 .collect::<Vec<_>>();
17727
17728 let default_commit_characters = vec!["?".to_string()];
17729 let default_data = json!({ "default": "data"});
17730 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
17731 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
17732 let default_edit_range = lsp::Range {
17733 start: lsp::Position {
17734 line: 0,
17735 character: 5,
17736 },
17737 end: lsp::Position {
17738 line: 0,
17739 character: 5,
17740 },
17741 };
17742
17743 let mut cx = EditorLspTestContext::new_rust(
17744 lsp::ServerCapabilities {
17745 completion_provider: Some(lsp::CompletionOptions {
17746 trigger_characters: Some(vec![".".to_string()]),
17747 resolve_provider: Some(true),
17748 ..Default::default()
17749 }),
17750 ..Default::default()
17751 },
17752 cx,
17753 )
17754 .await;
17755
17756 cx.set_state("fn main() { let a = 2ˇ; }");
17757 cx.simulate_keystroke(".");
17758
17759 let completion_data = default_data.clone();
17760 let completion_characters = default_commit_characters.clone();
17761 let completion_items = items.clone();
17762 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17763 let default_data = completion_data.clone();
17764 let default_commit_characters = completion_characters.clone();
17765 let items = completion_items.clone();
17766 async move {
17767 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
17768 items,
17769 item_defaults: Some(lsp::CompletionListItemDefaults {
17770 data: Some(default_data.clone()),
17771 commit_characters: Some(default_commit_characters.clone()),
17772 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
17773 default_edit_range,
17774 )),
17775 insert_text_format: Some(default_insert_text_format),
17776 insert_text_mode: Some(default_insert_text_mode),
17777 }),
17778 ..lsp::CompletionList::default()
17779 })))
17780 }
17781 })
17782 .next()
17783 .await;
17784
17785 let resolved_items = Arc::new(Mutex::new(Vec::new()));
17786 cx.lsp
17787 .server
17788 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
17789 let closure_resolved_items = resolved_items.clone();
17790 move |item_to_resolve, _| {
17791 let closure_resolved_items = closure_resolved_items.clone();
17792 async move {
17793 closure_resolved_items.lock().push(item_to_resolve.clone());
17794 Ok(item_to_resolve)
17795 }
17796 }
17797 })
17798 .detach();
17799
17800 cx.condition(|editor, _| editor.context_menu_visible())
17801 .await;
17802 cx.run_until_parked();
17803 cx.update_editor(|editor, _, _| {
17804 let menu = editor.context_menu.borrow_mut();
17805 match menu.as_ref().expect("should have the completions menu") {
17806 CodeContextMenu::Completions(completions_menu) => {
17807 assert_eq!(
17808 completions_menu
17809 .entries
17810 .borrow()
17811 .iter()
17812 .map(|mat| mat.string.clone())
17813 .collect::<Vec<String>>(),
17814 items
17815 .iter()
17816 .map(|completion| completion.label.clone())
17817 .collect::<Vec<String>>()
17818 );
17819 }
17820 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
17821 }
17822 });
17823 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
17824 // with 4 from the end.
17825 assert_eq!(
17826 *resolved_items.lock(),
17827 [&items[0..16], &items[items.len() - 4..items.len()]]
17828 .concat()
17829 .iter()
17830 .cloned()
17831 .map(|mut item| {
17832 if item.data.is_none() {
17833 item.data = Some(default_data.clone());
17834 }
17835 item
17836 })
17837 .collect::<Vec<lsp::CompletionItem>>(),
17838 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
17839 );
17840 resolved_items.lock().clear();
17841
17842 cx.update_editor(|editor, window, cx| {
17843 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
17844 });
17845 cx.run_until_parked();
17846 // Completions that have already been resolved are skipped.
17847 assert_eq!(
17848 *resolved_items.lock(),
17849 items[items.len() - 17..items.len() - 4]
17850 .iter()
17851 .cloned()
17852 .map(|mut item| {
17853 if item.data.is_none() {
17854 item.data = Some(default_data.clone());
17855 }
17856 item
17857 })
17858 .collect::<Vec<lsp::CompletionItem>>()
17859 );
17860 resolved_items.lock().clear();
17861}
17862
17863#[gpui::test]
17864async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
17865 init_test(cx, |_| {});
17866
17867 let mut cx = EditorLspTestContext::new(
17868 Language::new(
17869 LanguageConfig {
17870 matcher: LanguageMatcher {
17871 path_suffixes: vec!["jsx".into()],
17872 ..Default::default()
17873 },
17874 overrides: [(
17875 "element".into(),
17876 LanguageConfigOverride {
17877 completion_query_characters: Override::Set(['-'].into_iter().collect()),
17878 ..Default::default()
17879 },
17880 )]
17881 .into_iter()
17882 .collect(),
17883 ..Default::default()
17884 },
17885 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
17886 )
17887 .with_override_query("(jsx_self_closing_element) @element")
17888 .unwrap(),
17889 lsp::ServerCapabilities {
17890 completion_provider: Some(lsp::CompletionOptions {
17891 trigger_characters: Some(vec![":".to_string()]),
17892 ..Default::default()
17893 }),
17894 ..Default::default()
17895 },
17896 cx,
17897 )
17898 .await;
17899
17900 cx.lsp
17901 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
17902 Ok(Some(lsp::CompletionResponse::Array(vec![
17903 lsp::CompletionItem {
17904 label: "bg-blue".into(),
17905 ..Default::default()
17906 },
17907 lsp::CompletionItem {
17908 label: "bg-red".into(),
17909 ..Default::default()
17910 },
17911 lsp::CompletionItem {
17912 label: "bg-yellow".into(),
17913 ..Default::default()
17914 },
17915 ])))
17916 });
17917
17918 cx.set_state(r#"<p class="bgˇ" />"#);
17919
17920 // Trigger completion when typing a dash, because the dash is an extra
17921 // word character in the 'element' scope, which contains the cursor.
17922 cx.simulate_keystroke("-");
17923 cx.executor().run_until_parked();
17924 cx.update_editor(|editor, _, _| {
17925 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17926 {
17927 assert_eq!(
17928 completion_menu_entries(menu),
17929 &["bg-blue", "bg-red", "bg-yellow"]
17930 );
17931 } else {
17932 panic!("expected completion menu to be open");
17933 }
17934 });
17935
17936 cx.simulate_keystroke("l");
17937 cx.executor().run_until_parked();
17938 cx.update_editor(|editor, _, _| {
17939 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17940 {
17941 assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]);
17942 } else {
17943 panic!("expected completion menu to be open");
17944 }
17945 });
17946
17947 // When filtering completions, consider the character after the '-' to
17948 // be the start of a subword.
17949 cx.set_state(r#"<p class="yelˇ" />"#);
17950 cx.simulate_keystroke("l");
17951 cx.executor().run_until_parked();
17952 cx.update_editor(|editor, _, _| {
17953 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17954 {
17955 assert_eq!(completion_menu_entries(menu), &["bg-yellow"]);
17956 } else {
17957 panic!("expected completion menu to be open");
17958 }
17959 });
17960}
17961
17962fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
17963 let entries = menu.entries.borrow();
17964 entries.iter().map(|mat| mat.string.clone()).collect()
17965}
17966
17967#[gpui::test]
17968async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
17969 init_test(cx, |settings| {
17970 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
17971 Formatter::Prettier,
17972 )))
17973 });
17974
17975 let fs = FakeFs::new(cx.executor());
17976 fs.insert_file(path!("/file.ts"), Default::default()).await;
17977
17978 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
17979 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17980
17981 language_registry.add(Arc::new(Language::new(
17982 LanguageConfig {
17983 name: "TypeScript".into(),
17984 matcher: LanguageMatcher {
17985 path_suffixes: vec!["ts".to_string()],
17986 ..Default::default()
17987 },
17988 ..Default::default()
17989 },
17990 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
17991 )));
17992 update_test_language_settings(cx, |settings| {
17993 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
17994 });
17995
17996 let test_plugin = "test_plugin";
17997 let _ = language_registry.register_fake_lsp(
17998 "TypeScript",
17999 FakeLspAdapter {
18000 prettier_plugins: vec![test_plugin],
18001 ..Default::default()
18002 },
18003 );
18004
18005 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
18006 let buffer = project
18007 .update(cx, |project, cx| {
18008 project.open_local_buffer(path!("/file.ts"), cx)
18009 })
18010 .await
18011 .unwrap();
18012
18013 let buffer_text = "one\ntwo\nthree\n";
18014 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
18015 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
18016 editor.update_in(cx, |editor, window, cx| {
18017 editor.set_text(buffer_text, window, cx)
18018 });
18019
18020 editor
18021 .update_in(cx, |editor, window, cx| {
18022 editor.perform_format(
18023 project.clone(),
18024 FormatTrigger::Manual,
18025 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18026 window,
18027 cx,
18028 )
18029 })
18030 .unwrap()
18031 .await;
18032 assert_eq!(
18033 editor.update(cx, |editor, cx| editor.text(cx)),
18034 buffer_text.to_string() + prettier_format_suffix,
18035 "Test prettier formatting was not applied to the original buffer text",
18036 );
18037
18038 update_test_language_settings(cx, |settings| {
18039 settings.defaults.formatter = Some(SelectedFormatter::Auto)
18040 });
18041 let format = editor.update_in(cx, |editor, window, cx| {
18042 editor.perform_format(
18043 project.clone(),
18044 FormatTrigger::Manual,
18045 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18046 window,
18047 cx,
18048 )
18049 });
18050 format.await.unwrap();
18051 assert_eq!(
18052 editor.update(cx, |editor, cx| editor.text(cx)),
18053 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
18054 "Autoformatting (via test prettier) was not applied to the original buffer text",
18055 );
18056}
18057
18058#[gpui::test]
18059async fn test_addition_reverts(cx: &mut TestAppContext) {
18060 init_test(cx, |_| {});
18061 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18062 let base_text = indoc! {r#"
18063 struct Row;
18064 struct Row1;
18065 struct Row2;
18066
18067 struct Row4;
18068 struct Row5;
18069 struct Row6;
18070
18071 struct Row8;
18072 struct Row9;
18073 struct Row10;"#};
18074
18075 // When addition hunks are not adjacent to carets, no hunk revert is performed
18076 assert_hunk_revert(
18077 indoc! {r#"struct Row;
18078 struct Row1;
18079 struct Row1.1;
18080 struct Row1.2;
18081 struct Row2;ˇ
18082
18083 struct Row4;
18084 struct Row5;
18085 struct Row6;
18086
18087 struct Row8;
18088 ˇstruct Row9;
18089 struct Row9.1;
18090 struct Row9.2;
18091 struct Row9.3;
18092 struct Row10;"#},
18093 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18094 indoc! {r#"struct Row;
18095 struct Row1;
18096 struct Row1.1;
18097 struct Row1.2;
18098 struct Row2;ˇ
18099
18100 struct Row4;
18101 struct Row5;
18102 struct Row6;
18103
18104 struct Row8;
18105 ˇstruct Row9;
18106 struct Row9.1;
18107 struct Row9.2;
18108 struct Row9.3;
18109 struct Row10;"#},
18110 base_text,
18111 &mut cx,
18112 );
18113 // Same for selections
18114 assert_hunk_revert(
18115 indoc! {r#"struct Row;
18116 struct Row1;
18117 struct Row2;
18118 struct Row2.1;
18119 struct Row2.2;
18120 «ˇ
18121 struct Row4;
18122 struct» Row5;
18123 «struct Row6;
18124 ˇ»
18125 struct Row9.1;
18126 struct Row9.2;
18127 struct Row9.3;
18128 struct Row8;
18129 struct Row9;
18130 struct Row10;"#},
18131 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18132 indoc! {r#"struct Row;
18133 struct Row1;
18134 struct Row2;
18135 struct Row2.1;
18136 struct Row2.2;
18137 «ˇ
18138 struct Row4;
18139 struct» Row5;
18140 «struct Row6;
18141 ˇ»
18142 struct Row9.1;
18143 struct Row9.2;
18144 struct Row9.3;
18145 struct Row8;
18146 struct Row9;
18147 struct Row10;"#},
18148 base_text,
18149 &mut cx,
18150 );
18151
18152 // When carets and selections intersect the addition hunks, those are reverted.
18153 // Adjacent carets got merged.
18154 assert_hunk_revert(
18155 indoc! {r#"struct Row;
18156 ˇ// something on the top
18157 struct Row1;
18158 struct Row2;
18159 struct Roˇw3.1;
18160 struct Row2.2;
18161 struct Row2.3;ˇ
18162
18163 struct Row4;
18164 struct ˇRow5.1;
18165 struct Row5.2;
18166 struct «Rowˇ»5.3;
18167 struct Row5;
18168 struct Row6;
18169 ˇ
18170 struct Row9.1;
18171 struct «Rowˇ»9.2;
18172 struct «ˇRow»9.3;
18173 struct Row8;
18174 struct Row9;
18175 «ˇ// something on bottom»
18176 struct Row10;"#},
18177 vec![
18178 DiffHunkStatusKind::Added,
18179 DiffHunkStatusKind::Added,
18180 DiffHunkStatusKind::Added,
18181 DiffHunkStatusKind::Added,
18182 DiffHunkStatusKind::Added,
18183 ],
18184 indoc! {r#"struct Row;
18185 ˇstruct Row1;
18186 struct Row2;
18187 ˇ
18188 struct Row4;
18189 ˇstruct Row5;
18190 struct Row6;
18191 ˇ
18192 ˇstruct Row8;
18193 struct Row9;
18194 ˇstruct Row10;"#},
18195 base_text,
18196 &mut cx,
18197 );
18198}
18199
18200#[gpui::test]
18201async fn test_modification_reverts(cx: &mut TestAppContext) {
18202 init_test(cx, |_| {});
18203 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18204 let base_text = indoc! {r#"
18205 struct Row;
18206 struct Row1;
18207 struct Row2;
18208
18209 struct Row4;
18210 struct Row5;
18211 struct Row6;
18212
18213 struct Row8;
18214 struct Row9;
18215 struct Row10;"#};
18216
18217 // Modification hunks behave the same as the addition ones.
18218 assert_hunk_revert(
18219 indoc! {r#"struct Row;
18220 struct Row1;
18221 struct Row33;
18222 ˇ
18223 struct Row4;
18224 struct Row5;
18225 struct Row6;
18226 ˇ
18227 struct Row99;
18228 struct Row9;
18229 struct Row10;"#},
18230 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18231 indoc! {r#"struct Row;
18232 struct Row1;
18233 struct Row33;
18234 ˇ
18235 struct Row4;
18236 struct Row5;
18237 struct Row6;
18238 ˇ
18239 struct Row99;
18240 struct Row9;
18241 struct Row10;"#},
18242 base_text,
18243 &mut cx,
18244 );
18245 assert_hunk_revert(
18246 indoc! {r#"struct Row;
18247 struct Row1;
18248 struct Row33;
18249 «ˇ
18250 struct Row4;
18251 struct» Row5;
18252 «struct Row6;
18253 ˇ»
18254 struct Row99;
18255 struct Row9;
18256 struct Row10;"#},
18257 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18258 indoc! {r#"struct Row;
18259 struct Row1;
18260 struct Row33;
18261 «ˇ
18262 struct Row4;
18263 struct» Row5;
18264 «struct Row6;
18265 ˇ»
18266 struct Row99;
18267 struct Row9;
18268 struct Row10;"#},
18269 base_text,
18270 &mut cx,
18271 );
18272
18273 assert_hunk_revert(
18274 indoc! {r#"ˇstruct Row1.1;
18275 struct Row1;
18276 «ˇstr»uct Row22;
18277
18278 struct ˇRow44;
18279 struct Row5;
18280 struct «Rˇ»ow66;ˇ
18281
18282 «struˇ»ct Row88;
18283 struct Row9;
18284 struct Row1011;ˇ"#},
18285 vec![
18286 DiffHunkStatusKind::Modified,
18287 DiffHunkStatusKind::Modified,
18288 DiffHunkStatusKind::Modified,
18289 DiffHunkStatusKind::Modified,
18290 DiffHunkStatusKind::Modified,
18291 DiffHunkStatusKind::Modified,
18292 ],
18293 indoc! {r#"struct Row;
18294 ˇstruct Row1;
18295 struct Row2;
18296 ˇ
18297 struct Row4;
18298 ˇstruct Row5;
18299 struct Row6;
18300 ˇ
18301 struct Row8;
18302 ˇstruct Row9;
18303 struct Row10;ˇ"#},
18304 base_text,
18305 &mut cx,
18306 );
18307}
18308
18309#[gpui::test]
18310async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
18311 init_test(cx, |_| {});
18312 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18313 let base_text = indoc! {r#"
18314 one
18315
18316 two
18317 three
18318 "#};
18319
18320 cx.set_head_text(base_text);
18321 cx.set_state("\nˇ\n");
18322 cx.executor().run_until_parked();
18323 cx.update_editor(|editor, _window, cx| {
18324 editor.expand_selected_diff_hunks(cx);
18325 });
18326 cx.executor().run_until_parked();
18327 cx.update_editor(|editor, window, cx| {
18328 editor.backspace(&Default::default(), window, cx);
18329 });
18330 cx.run_until_parked();
18331 cx.assert_state_with_diff(
18332 indoc! {r#"
18333
18334 - two
18335 - threeˇ
18336 +
18337 "#}
18338 .to_string(),
18339 );
18340}
18341
18342#[gpui::test]
18343async fn test_deletion_reverts(cx: &mut TestAppContext) {
18344 init_test(cx, |_| {});
18345 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18346 let base_text = indoc! {r#"struct Row;
18347struct Row1;
18348struct Row2;
18349
18350struct Row4;
18351struct Row5;
18352struct Row6;
18353
18354struct Row8;
18355struct Row9;
18356struct Row10;"#};
18357
18358 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
18359 assert_hunk_revert(
18360 indoc! {r#"struct Row;
18361 struct Row2;
18362
18363 ˇstruct Row4;
18364 struct Row5;
18365 struct Row6;
18366 ˇ
18367 struct Row8;
18368 struct Row10;"#},
18369 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18370 indoc! {r#"struct Row;
18371 struct Row2;
18372
18373 ˇstruct Row4;
18374 struct Row5;
18375 struct Row6;
18376 ˇ
18377 struct Row8;
18378 struct Row10;"#},
18379 base_text,
18380 &mut cx,
18381 );
18382 assert_hunk_revert(
18383 indoc! {r#"struct Row;
18384 struct Row2;
18385
18386 «ˇstruct Row4;
18387 struct» Row5;
18388 «struct Row6;
18389 ˇ»
18390 struct Row8;
18391 struct Row10;"#},
18392 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18393 indoc! {r#"struct Row;
18394 struct Row2;
18395
18396 «ˇstruct Row4;
18397 struct» Row5;
18398 «struct Row6;
18399 ˇ»
18400 struct Row8;
18401 struct Row10;"#},
18402 base_text,
18403 &mut cx,
18404 );
18405
18406 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
18407 assert_hunk_revert(
18408 indoc! {r#"struct Row;
18409 ˇstruct Row2;
18410
18411 struct Row4;
18412 struct Row5;
18413 struct Row6;
18414
18415 struct Row8;ˇ
18416 struct Row10;"#},
18417 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18418 indoc! {r#"struct Row;
18419 struct Row1;
18420 ˇstruct Row2;
18421
18422 struct Row4;
18423 struct Row5;
18424 struct Row6;
18425
18426 struct Row8;ˇ
18427 struct Row9;
18428 struct Row10;"#},
18429 base_text,
18430 &mut cx,
18431 );
18432 assert_hunk_revert(
18433 indoc! {r#"struct Row;
18434 struct Row2«ˇ;
18435 struct Row4;
18436 struct» Row5;
18437 «struct Row6;
18438
18439 struct Row8;ˇ»
18440 struct Row10;"#},
18441 vec![
18442 DiffHunkStatusKind::Deleted,
18443 DiffHunkStatusKind::Deleted,
18444 DiffHunkStatusKind::Deleted,
18445 ],
18446 indoc! {r#"struct Row;
18447 struct Row1;
18448 struct Row2«ˇ;
18449
18450 struct Row4;
18451 struct» Row5;
18452 «struct Row6;
18453
18454 struct Row8;ˇ»
18455 struct Row9;
18456 struct Row10;"#},
18457 base_text,
18458 &mut cx,
18459 );
18460}
18461
18462#[gpui::test]
18463async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
18464 init_test(cx, |_| {});
18465
18466 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
18467 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
18468 let base_text_3 =
18469 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
18470
18471 let text_1 = edit_first_char_of_every_line(base_text_1);
18472 let text_2 = edit_first_char_of_every_line(base_text_2);
18473 let text_3 = edit_first_char_of_every_line(base_text_3);
18474
18475 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
18476 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
18477 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
18478
18479 let multibuffer = cx.new(|cx| {
18480 let mut multibuffer = MultiBuffer::new(ReadWrite);
18481 multibuffer.push_excerpts(
18482 buffer_1.clone(),
18483 [
18484 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18485 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18486 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18487 ],
18488 cx,
18489 );
18490 multibuffer.push_excerpts(
18491 buffer_2.clone(),
18492 [
18493 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18494 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18495 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18496 ],
18497 cx,
18498 );
18499 multibuffer.push_excerpts(
18500 buffer_3.clone(),
18501 [
18502 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18503 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18504 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18505 ],
18506 cx,
18507 );
18508 multibuffer
18509 });
18510
18511 let fs = FakeFs::new(cx.executor());
18512 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
18513 let (editor, cx) = cx
18514 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
18515 editor.update_in(cx, |editor, _window, cx| {
18516 for (buffer, diff_base) in [
18517 (buffer_1.clone(), base_text_1),
18518 (buffer_2.clone(), base_text_2),
18519 (buffer_3.clone(), base_text_3),
18520 ] {
18521 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
18522 editor
18523 .buffer
18524 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
18525 }
18526 });
18527 cx.executor().run_until_parked();
18528
18529 editor.update_in(cx, |editor, window, cx| {
18530 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}");
18531 editor.select_all(&SelectAll, window, cx);
18532 editor.git_restore(&Default::default(), window, cx);
18533 });
18534 cx.executor().run_until_parked();
18535
18536 // When all ranges are selected, all buffer hunks are reverted.
18537 editor.update(cx, |editor, cx| {
18538 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");
18539 });
18540 buffer_1.update(cx, |buffer, _| {
18541 assert_eq!(buffer.text(), base_text_1);
18542 });
18543 buffer_2.update(cx, |buffer, _| {
18544 assert_eq!(buffer.text(), base_text_2);
18545 });
18546 buffer_3.update(cx, |buffer, _| {
18547 assert_eq!(buffer.text(), base_text_3);
18548 });
18549
18550 editor.update_in(cx, |editor, window, cx| {
18551 editor.undo(&Default::default(), window, cx);
18552 });
18553
18554 editor.update_in(cx, |editor, window, cx| {
18555 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18556 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
18557 });
18558 editor.git_restore(&Default::default(), window, cx);
18559 });
18560
18561 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
18562 // but not affect buffer_2 and its related excerpts.
18563 editor.update(cx, |editor, cx| {
18564 assert_eq!(
18565 editor.text(cx),
18566 "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}"
18567 );
18568 });
18569 buffer_1.update(cx, |buffer, _| {
18570 assert_eq!(buffer.text(), base_text_1);
18571 });
18572 buffer_2.update(cx, |buffer, _| {
18573 assert_eq!(
18574 buffer.text(),
18575 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
18576 );
18577 });
18578 buffer_3.update(cx, |buffer, _| {
18579 assert_eq!(
18580 buffer.text(),
18581 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
18582 );
18583 });
18584
18585 fn edit_first_char_of_every_line(text: &str) -> String {
18586 text.split('\n')
18587 .map(|line| format!("X{}", &line[1..]))
18588 .collect::<Vec<_>>()
18589 .join("\n")
18590 }
18591}
18592
18593#[gpui::test]
18594async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
18595 init_test(cx, |_| {});
18596
18597 let cols = 4;
18598 let rows = 10;
18599 let sample_text_1 = sample_text(rows, cols, 'a');
18600 assert_eq!(
18601 sample_text_1,
18602 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
18603 );
18604 let sample_text_2 = sample_text(rows, cols, 'l');
18605 assert_eq!(
18606 sample_text_2,
18607 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
18608 );
18609 let sample_text_3 = sample_text(rows, cols, 'v');
18610 assert_eq!(
18611 sample_text_3,
18612 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
18613 );
18614
18615 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
18616 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
18617 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
18618
18619 let multi_buffer = cx.new(|cx| {
18620 let mut multibuffer = MultiBuffer::new(ReadWrite);
18621 multibuffer.push_excerpts(
18622 buffer_1.clone(),
18623 [
18624 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18625 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18626 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18627 ],
18628 cx,
18629 );
18630 multibuffer.push_excerpts(
18631 buffer_2.clone(),
18632 [
18633 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18634 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18635 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18636 ],
18637 cx,
18638 );
18639 multibuffer.push_excerpts(
18640 buffer_3.clone(),
18641 [
18642 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18643 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18644 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18645 ],
18646 cx,
18647 );
18648 multibuffer
18649 });
18650
18651 let fs = FakeFs::new(cx.executor());
18652 fs.insert_tree(
18653 "/a",
18654 json!({
18655 "main.rs": sample_text_1,
18656 "other.rs": sample_text_2,
18657 "lib.rs": sample_text_3,
18658 }),
18659 )
18660 .await;
18661 let project = Project::test(fs, ["/a".as_ref()], cx).await;
18662 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18663 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18664 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18665 Editor::new(
18666 EditorMode::full(),
18667 multi_buffer,
18668 Some(project.clone()),
18669 window,
18670 cx,
18671 )
18672 });
18673 let multibuffer_item_id = workspace
18674 .update(cx, |workspace, window, cx| {
18675 assert!(
18676 workspace.active_item(cx).is_none(),
18677 "active item should be None before the first item is added"
18678 );
18679 workspace.add_item_to_active_pane(
18680 Box::new(multi_buffer_editor.clone()),
18681 None,
18682 true,
18683 window,
18684 cx,
18685 );
18686 let active_item = workspace
18687 .active_item(cx)
18688 .expect("should have an active item after adding the multi buffer");
18689 assert!(
18690 !active_item.is_singleton(cx),
18691 "A multi buffer was expected to active after adding"
18692 );
18693 active_item.item_id()
18694 })
18695 .unwrap();
18696 cx.executor().run_until_parked();
18697
18698 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18699 editor.change_selections(
18700 SelectionEffects::scroll(Autoscroll::Next),
18701 window,
18702 cx,
18703 |s| s.select_ranges(Some(1..2)),
18704 );
18705 editor.open_excerpts(&OpenExcerpts, window, cx);
18706 });
18707 cx.executor().run_until_parked();
18708 let first_item_id = workspace
18709 .update(cx, |workspace, window, cx| {
18710 let active_item = workspace
18711 .active_item(cx)
18712 .expect("should have an active item after navigating into the 1st buffer");
18713 let first_item_id = active_item.item_id();
18714 assert_ne!(
18715 first_item_id, multibuffer_item_id,
18716 "Should navigate into the 1st buffer and activate it"
18717 );
18718 assert!(
18719 active_item.is_singleton(cx),
18720 "New active item should be a singleton buffer"
18721 );
18722 assert_eq!(
18723 active_item
18724 .act_as::<Editor>(cx)
18725 .expect("should have navigated into an editor for the 1st buffer")
18726 .read(cx)
18727 .text(cx),
18728 sample_text_1
18729 );
18730
18731 workspace
18732 .go_back(workspace.active_pane().downgrade(), window, cx)
18733 .detach_and_log_err(cx);
18734
18735 first_item_id
18736 })
18737 .unwrap();
18738 cx.executor().run_until_parked();
18739 workspace
18740 .update(cx, |workspace, _, cx| {
18741 let active_item = workspace
18742 .active_item(cx)
18743 .expect("should have an active item after navigating back");
18744 assert_eq!(
18745 active_item.item_id(),
18746 multibuffer_item_id,
18747 "Should navigate back to the multi buffer"
18748 );
18749 assert!(!active_item.is_singleton(cx));
18750 })
18751 .unwrap();
18752
18753 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18754 editor.change_selections(
18755 SelectionEffects::scroll(Autoscroll::Next),
18756 window,
18757 cx,
18758 |s| s.select_ranges(Some(39..40)),
18759 );
18760 editor.open_excerpts(&OpenExcerpts, window, cx);
18761 });
18762 cx.executor().run_until_parked();
18763 let second_item_id = workspace
18764 .update(cx, |workspace, window, cx| {
18765 let active_item = workspace
18766 .active_item(cx)
18767 .expect("should have an active item after navigating into the 2nd buffer");
18768 let second_item_id = active_item.item_id();
18769 assert_ne!(
18770 second_item_id, multibuffer_item_id,
18771 "Should navigate away from the multibuffer"
18772 );
18773 assert_ne!(
18774 second_item_id, first_item_id,
18775 "Should navigate into the 2nd buffer and activate it"
18776 );
18777 assert!(
18778 active_item.is_singleton(cx),
18779 "New active item should be a singleton buffer"
18780 );
18781 assert_eq!(
18782 active_item
18783 .act_as::<Editor>(cx)
18784 .expect("should have navigated into an editor")
18785 .read(cx)
18786 .text(cx),
18787 sample_text_2
18788 );
18789
18790 workspace
18791 .go_back(workspace.active_pane().downgrade(), window, cx)
18792 .detach_and_log_err(cx);
18793
18794 second_item_id
18795 })
18796 .unwrap();
18797 cx.executor().run_until_parked();
18798 workspace
18799 .update(cx, |workspace, _, cx| {
18800 let active_item = workspace
18801 .active_item(cx)
18802 .expect("should have an active item after navigating back from the 2nd buffer");
18803 assert_eq!(
18804 active_item.item_id(),
18805 multibuffer_item_id,
18806 "Should navigate back from the 2nd buffer to the multi buffer"
18807 );
18808 assert!(!active_item.is_singleton(cx));
18809 })
18810 .unwrap();
18811
18812 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18813 editor.change_selections(
18814 SelectionEffects::scroll(Autoscroll::Next),
18815 window,
18816 cx,
18817 |s| s.select_ranges(Some(70..70)),
18818 );
18819 editor.open_excerpts(&OpenExcerpts, window, cx);
18820 });
18821 cx.executor().run_until_parked();
18822 workspace
18823 .update(cx, |workspace, window, cx| {
18824 let active_item = workspace
18825 .active_item(cx)
18826 .expect("should have an active item after navigating into the 3rd buffer");
18827 let third_item_id = active_item.item_id();
18828 assert_ne!(
18829 third_item_id, multibuffer_item_id,
18830 "Should navigate into the 3rd buffer and activate it"
18831 );
18832 assert_ne!(third_item_id, first_item_id);
18833 assert_ne!(third_item_id, second_item_id);
18834 assert!(
18835 active_item.is_singleton(cx),
18836 "New active item should be a singleton buffer"
18837 );
18838 assert_eq!(
18839 active_item
18840 .act_as::<Editor>(cx)
18841 .expect("should have navigated into an editor")
18842 .read(cx)
18843 .text(cx),
18844 sample_text_3
18845 );
18846
18847 workspace
18848 .go_back(workspace.active_pane().downgrade(), window, cx)
18849 .detach_and_log_err(cx);
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 from the 3rd buffer");
18858 assert_eq!(
18859 active_item.item_id(),
18860 multibuffer_item_id,
18861 "Should navigate back from the 3rd buffer to the multi buffer"
18862 );
18863 assert!(!active_item.is_singleton(cx));
18864 })
18865 .unwrap();
18866}
18867
18868#[gpui::test]
18869async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
18870 init_test(cx, |_| {});
18871
18872 let mut cx = EditorTestContext::new(cx).await;
18873
18874 let diff_base = r#"
18875 use some::mod;
18876
18877 const A: u32 = 42;
18878
18879 fn main() {
18880 println!("hello");
18881
18882 println!("world");
18883 }
18884 "#
18885 .unindent();
18886
18887 cx.set_state(
18888 &r#"
18889 use some::modified;
18890
18891 ˇ
18892 fn main() {
18893 println!("hello there");
18894
18895 println!("around the");
18896 println!("world");
18897 }
18898 "#
18899 .unindent(),
18900 );
18901
18902 cx.set_head_text(&diff_base);
18903 executor.run_until_parked();
18904
18905 cx.update_editor(|editor, window, cx| {
18906 editor.go_to_next_hunk(&GoToHunk, window, cx);
18907 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
18908 });
18909 executor.run_until_parked();
18910 cx.assert_state_with_diff(
18911 r#"
18912 use some::modified;
18913
18914
18915 fn main() {
18916 - println!("hello");
18917 + ˇ println!("hello there");
18918
18919 println!("around the");
18920 println!("world");
18921 }
18922 "#
18923 .unindent(),
18924 );
18925
18926 cx.update_editor(|editor, window, cx| {
18927 for _ in 0..2 {
18928 editor.go_to_next_hunk(&GoToHunk, window, cx);
18929 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
18930 }
18931 });
18932 executor.run_until_parked();
18933 cx.assert_state_with_diff(
18934 r#"
18935 - use some::mod;
18936 + ˇuse some::modified;
18937
18938
18939 fn main() {
18940 - println!("hello");
18941 + println!("hello there");
18942
18943 + println!("around the");
18944 println!("world");
18945 }
18946 "#
18947 .unindent(),
18948 );
18949
18950 cx.update_editor(|editor, window, cx| {
18951 editor.go_to_next_hunk(&GoToHunk, window, cx);
18952 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
18953 });
18954 executor.run_until_parked();
18955 cx.assert_state_with_diff(
18956 r#"
18957 - use some::mod;
18958 + use some::modified;
18959
18960 - const A: u32 = 42;
18961 ˇ
18962 fn main() {
18963 - println!("hello");
18964 + println!("hello there");
18965
18966 + println!("around the");
18967 println!("world");
18968 }
18969 "#
18970 .unindent(),
18971 );
18972
18973 cx.update_editor(|editor, window, cx| {
18974 editor.cancel(&Cancel, window, cx);
18975 });
18976
18977 cx.assert_state_with_diff(
18978 r#"
18979 use some::modified;
18980
18981 ˇ
18982 fn main() {
18983 println!("hello there");
18984
18985 println!("around the");
18986 println!("world");
18987 }
18988 "#
18989 .unindent(),
18990 );
18991}
18992
18993#[gpui::test]
18994async fn test_diff_base_change_with_expanded_diff_hunks(
18995 executor: BackgroundExecutor,
18996 cx: &mut TestAppContext,
18997) {
18998 init_test(cx, |_| {});
18999
19000 let mut cx = EditorTestContext::new(cx).await;
19001
19002 let diff_base = r#"
19003 use some::mod1;
19004 use some::mod2;
19005
19006 const A: u32 = 42;
19007 const B: u32 = 42;
19008 const C: u32 = 42;
19009
19010 fn main() {
19011 println!("hello");
19012
19013 println!("world");
19014 }
19015 "#
19016 .unindent();
19017
19018 cx.set_state(
19019 &r#"
19020 use some::mod2;
19021
19022 const A: u32 = 42;
19023 const C: u32 = 42;
19024
19025 fn main(ˇ) {
19026 //println!("hello");
19027
19028 println!("world");
19029 //
19030 //
19031 }
19032 "#
19033 .unindent(),
19034 );
19035
19036 cx.set_head_text(&diff_base);
19037 executor.run_until_parked();
19038
19039 cx.update_editor(|editor, window, cx| {
19040 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19041 });
19042 executor.run_until_parked();
19043 cx.assert_state_with_diff(
19044 r#"
19045 - use some::mod1;
19046 use some::mod2;
19047
19048 const A: u32 = 42;
19049 - const B: u32 = 42;
19050 const C: u32 = 42;
19051
19052 fn main(ˇ) {
19053 - println!("hello");
19054 + //println!("hello");
19055
19056 println!("world");
19057 + //
19058 + //
19059 }
19060 "#
19061 .unindent(),
19062 );
19063
19064 cx.set_head_text("new diff base!");
19065 executor.run_until_parked();
19066 cx.assert_state_with_diff(
19067 r#"
19068 - new diff base!
19069 + use some::mod2;
19070 +
19071 + const A: u32 = 42;
19072 + const C: u32 = 42;
19073 +
19074 + fn main(ˇ) {
19075 + //println!("hello");
19076 +
19077 + println!("world");
19078 + //
19079 + //
19080 + }
19081 "#
19082 .unindent(),
19083 );
19084}
19085
19086#[gpui::test]
19087async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
19088 init_test(cx, |_| {});
19089
19090 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19091 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19092 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19093 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19094 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
19095 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
19096
19097 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
19098 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
19099 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
19100
19101 let multi_buffer = cx.new(|cx| {
19102 let mut multibuffer = MultiBuffer::new(ReadWrite);
19103 multibuffer.push_excerpts(
19104 buffer_1.clone(),
19105 [
19106 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19107 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19108 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19109 ],
19110 cx,
19111 );
19112 multibuffer.push_excerpts(
19113 buffer_2.clone(),
19114 [
19115 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19116 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19117 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19118 ],
19119 cx,
19120 );
19121 multibuffer.push_excerpts(
19122 buffer_3.clone(),
19123 [
19124 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19125 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19126 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19127 ],
19128 cx,
19129 );
19130 multibuffer
19131 });
19132
19133 let editor =
19134 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19135 editor
19136 .update(cx, |editor, _window, cx| {
19137 for (buffer, diff_base) in [
19138 (buffer_1.clone(), file_1_old),
19139 (buffer_2.clone(), file_2_old),
19140 (buffer_3.clone(), file_3_old),
19141 ] {
19142 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
19143 editor
19144 .buffer
19145 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
19146 }
19147 })
19148 .unwrap();
19149
19150 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19151 cx.run_until_parked();
19152
19153 cx.assert_editor_state(
19154 &"
19155 ˇaaa
19156 ccc
19157 ddd
19158
19159 ggg
19160 hhh
19161
19162
19163 lll
19164 mmm
19165 NNN
19166
19167 qqq
19168 rrr
19169
19170 uuu
19171 111
19172 222
19173 333
19174
19175 666
19176 777
19177
19178 000
19179 !!!"
19180 .unindent(),
19181 );
19182
19183 cx.update_editor(|editor, window, cx| {
19184 editor.select_all(&SelectAll, window, cx);
19185 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19186 });
19187 cx.executor().run_until_parked();
19188
19189 cx.assert_state_with_diff(
19190 "
19191 «aaa
19192 - bbb
19193 ccc
19194 ddd
19195
19196 ggg
19197 hhh
19198
19199
19200 lll
19201 mmm
19202 - nnn
19203 + NNN
19204
19205 qqq
19206 rrr
19207
19208 uuu
19209 111
19210 222
19211 333
19212
19213 + 666
19214 777
19215
19216 000
19217 !!!ˇ»"
19218 .unindent(),
19219 );
19220}
19221
19222#[gpui::test]
19223async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
19224 init_test(cx, |_| {});
19225
19226 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
19227 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
19228
19229 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
19230 let multi_buffer = cx.new(|cx| {
19231 let mut multibuffer = MultiBuffer::new(ReadWrite);
19232 multibuffer.push_excerpts(
19233 buffer.clone(),
19234 [
19235 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
19236 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
19237 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
19238 ],
19239 cx,
19240 );
19241 multibuffer
19242 });
19243
19244 let editor =
19245 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19246 editor
19247 .update(cx, |editor, _window, cx| {
19248 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
19249 editor
19250 .buffer
19251 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
19252 })
19253 .unwrap();
19254
19255 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19256 cx.run_until_parked();
19257
19258 cx.update_editor(|editor, window, cx| {
19259 editor.expand_all_diff_hunks(&Default::default(), window, cx)
19260 });
19261 cx.executor().run_until_parked();
19262
19263 // When the start of a hunk coincides with the start of its excerpt,
19264 // the hunk is expanded. When the start of a hunk is earlier than
19265 // the start of its excerpt, the hunk is not expanded.
19266 cx.assert_state_with_diff(
19267 "
19268 ˇaaa
19269 - bbb
19270 + BBB
19271
19272 - ddd
19273 - eee
19274 + DDD
19275 + EEE
19276 fff
19277
19278 iii
19279 "
19280 .unindent(),
19281 );
19282}
19283
19284#[gpui::test]
19285async fn test_edits_around_expanded_insertion_hunks(
19286 executor: BackgroundExecutor,
19287 cx: &mut TestAppContext,
19288) {
19289 init_test(cx, |_| {});
19290
19291 let mut cx = EditorTestContext::new(cx).await;
19292
19293 let diff_base = r#"
19294 use some::mod1;
19295 use some::mod2;
19296
19297 const A: u32 = 42;
19298
19299 fn main() {
19300 println!("hello");
19301
19302 println!("world");
19303 }
19304 "#
19305 .unindent();
19306 executor.run_until_parked();
19307 cx.set_state(
19308 &r#"
19309 use some::mod1;
19310 use some::mod2;
19311
19312 const A: u32 = 42;
19313 const B: u32 = 42;
19314 const C: u32 = 42;
19315 ˇ
19316
19317 fn main() {
19318 println!("hello");
19319
19320 println!("world");
19321 }
19322 "#
19323 .unindent(),
19324 );
19325
19326 cx.set_head_text(&diff_base);
19327 executor.run_until_parked();
19328
19329 cx.update_editor(|editor, window, cx| {
19330 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19331 });
19332 executor.run_until_parked();
19333
19334 cx.assert_state_with_diff(
19335 r#"
19336 use some::mod1;
19337 use some::mod2;
19338
19339 const A: u32 = 42;
19340 + const B: u32 = 42;
19341 + const C: u32 = 42;
19342 + ˇ
19343
19344 fn main() {
19345 println!("hello");
19346
19347 println!("world");
19348 }
19349 "#
19350 .unindent(),
19351 );
19352
19353 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
19354 executor.run_until_parked();
19355
19356 cx.assert_state_with_diff(
19357 r#"
19358 use some::mod1;
19359 use some::mod2;
19360
19361 const A: u32 = 42;
19362 + const B: u32 = 42;
19363 + const C: u32 = 42;
19364 + const D: u32 = 42;
19365 + ˇ
19366
19367 fn main() {
19368 println!("hello");
19369
19370 println!("world");
19371 }
19372 "#
19373 .unindent(),
19374 );
19375
19376 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
19377 executor.run_until_parked();
19378
19379 cx.assert_state_with_diff(
19380 r#"
19381 use some::mod1;
19382 use some::mod2;
19383
19384 const A: u32 = 42;
19385 + const B: u32 = 42;
19386 + const C: u32 = 42;
19387 + const D: u32 = 42;
19388 + const E: u32 = 42;
19389 + ˇ
19390
19391 fn main() {
19392 println!("hello");
19393
19394 println!("world");
19395 }
19396 "#
19397 .unindent(),
19398 );
19399
19400 cx.update_editor(|editor, window, cx| {
19401 editor.delete_line(&DeleteLine, window, cx);
19402 });
19403 executor.run_until_parked();
19404
19405 cx.assert_state_with_diff(
19406 r#"
19407 use some::mod1;
19408 use some::mod2;
19409
19410 const A: u32 = 42;
19411 + const B: u32 = 42;
19412 + const C: u32 = 42;
19413 + const D: u32 = 42;
19414 + const E: u32 = 42;
19415 ˇ
19416 fn main() {
19417 println!("hello");
19418
19419 println!("world");
19420 }
19421 "#
19422 .unindent(),
19423 );
19424
19425 cx.update_editor(|editor, window, cx| {
19426 editor.move_up(&MoveUp, window, cx);
19427 editor.delete_line(&DeleteLine, window, cx);
19428 editor.move_up(&MoveUp, window, cx);
19429 editor.delete_line(&DeleteLine, window, cx);
19430 editor.move_up(&MoveUp, window, cx);
19431 editor.delete_line(&DeleteLine, window, cx);
19432 });
19433 executor.run_until_parked();
19434 cx.assert_state_with_diff(
19435 r#"
19436 use some::mod1;
19437 use some::mod2;
19438
19439 const A: u32 = 42;
19440 + const B: u32 = 42;
19441 ˇ
19442 fn main() {
19443 println!("hello");
19444
19445 println!("world");
19446 }
19447 "#
19448 .unindent(),
19449 );
19450
19451 cx.update_editor(|editor, window, cx| {
19452 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
19453 editor.delete_line(&DeleteLine, window, cx);
19454 });
19455 executor.run_until_parked();
19456 cx.assert_state_with_diff(
19457 r#"
19458 ˇ
19459 fn main() {
19460 println!("hello");
19461
19462 println!("world");
19463 }
19464 "#
19465 .unindent(),
19466 );
19467}
19468
19469#[gpui::test]
19470async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
19471 init_test(cx, |_| {});
19472
19473 let mut cx = EditorTestContext::new(cx).await;
19474 cx.set_head_text(indoc! { "
19475 one
19476 two
19477 three
19478 four
19479 five
19480 "
19481 });
19482 cx.set_state(indoc! { "
19483 one
19484 ˇthree
19485 five
19486 "});
19487 cx.run_until_parked();
19488 cx.update_editor(|editor, window, cx| {
19489 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19490 });
19491 cx.assert_state_with_diff(
19492 indoc! { "
19493 one
19494 - two
19495 ˇthree
19496 - four
19497 five
19498 "}
19499 .to_string(),
19500 );
19501 cx.update_editor(|editor, window, cx| {
19502 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19503 });
19504
19505 cx.assert_state_with_diff(
19506 indoc! { "
19507 one
19508 ˇthree
19509 five
19510 "}
19511 .to_string(),
19512 );
19513
19514 cx.set_state(indoc! { "
19515 one
19516 ˇTWO
19517 three
19518 four
19519 five
19520 "});
19521 cx.run_until_parked();
19522 cx.update_editor(|editor, window, cx| {
19523 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19524 });
19525
19526 cx.assert_state_with_diff(
19527 indoc! { "
19528 one
19529 - two
19530 + ˇTWO
19531 three
19532 four
19533 five
19534 "}
19535 .to_string(),
19536 );
19537 cx.update_editor(|editor, window, cx| {
19538 editor.move_up(&Default::default(), window, cx);
19539 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19540 });
19541 cx.assert_state_with_diff(
19542 indoc! { "
19543 one
19544 ˇTWO
19545 three
19546 four
19547 five
19548 "}
19549 .to_string(),
19550 );
19551}
19552
19553#[gpui::test]
19554async fn test_edits_around_expanded_deletion_hunks(
19555 executor: BackgroundExecutor,
19556 cx: &mut TestAppContext,
19557) {
19558 init_test(cx, |_| {});
19559
19560 let mut cx = EditorTestContext::new(cx).await;
19561
19562 let diff_base = r#"
19563 use some::mod1;
19564 use some::mod2;
19565
19566 const A: u32 = 42;
19567 const B: u32 = 42;
19568 const C: u32 = 42;
19569
19570
19571 fn main() {
19572 println!("hello");
19573
19574 println!("world");
19575 }
19576 "#
19577 .unindent();
19578 executor.run_until_parked();
19579 cx.set_state(
19580 &r#"
19581 use some::mod1;
19582 use some::mod2;
19583
19584 ˇconst B: u32 = 42;
19585 const C: u32 = 42;
19586
19587
19588 fn main() {
19589 println!("hello");
19590
19591 println!("world");
19592 }
19593 "#
19594 .unindent(),
19595 );
19596
19597 cx.set_head_text(&diff_base);
19598 executor.run_until_parked();
19599
19600 cx.update_editor(|editor, window, cx| {
19601 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19602 });
19603 executor.run_until_parked();
19604
19605 cx.assert_state_with_diff(
19606 r#"
19607 use some::mod1;
19608 use some::mod2;
19609
19610 - const A: u32 = 42;
19611 ˇconst B: u32 = 42;
19612 const C: u32 = 42;
19613
19614
19615 fn main() {
19616 println!("hello");
19617
19618 println!("world");
19619 }
19620 "#
19621 .unindent(),
19622 );
19623
19624 cx.update_editor(|editor, window, cx| {
19625 editor.delete_line(&DeleteLine, window, cx);
19626 });
19627 executor.run_until_parked();
19628 cx.assert_state_with_diff(
19629 r#"
19630 use some::mod1;
19631 use some::mod2;
19632
19633 - const A: u32 = 42;
19634 - const B: u32 = 42;
19635 ˇconst C: u32 = 42;
19636
19637
19638 fn main() {
19639 println!("hello");
19640
19641 println!("world");
19642 }
19643 "#
19644 .unindent(),
19645 );
19646
19647 cx.update_editor(|editor, window, cx| {
19648 editor.delete_line(&DeleteLine, window, cx);
19649 });
19650 executor.run_until_parked();
19651 cx.assert_state_with_diff(
19652 r#"
19653 use some::mod1;
19654 use some::mod2;
19655
19656 - const A: u32 = 42;
19657 - const B: u32 = 42;
19658 - const C: u32 = 42;
19659 ˇ
19660
19661 fn main() {
19662 println!("hello");
19663
19664 println!("world");
19665 }
19666 "#
19667 .unindent(),
19668 );
19669
19670 cx.update_editor(|editor, window, cx| {
19671 editor.handle_input("replacement", window, cx);
19672 });
19673 executor.run_until_parked();
19674 cx.assert_state_with_diff(
19675 r#"
19676 use some::mod1;
19677 use some::mod2;
19678
19679 - const A: u32 = 42;
19680 - const B: u32 = 42;
19681 - const C: u32 = 42;
19682 -
19683 + replacementˇ
19684
19685 fn main() {
19686 println!("hello");
19687
19688 println!("world");
19689 }
19690 "#
19691 .unindent(),
19692 );
19693}
19694
19695#[gpui::test]
19696async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19697 init_test(cx, |_| {});
19698
19699 let mut cx = EditorTestContext::new(cx).await;
19700
19701 let base_text = r#"
19702 one
19703 two
19704 three
19705 four
19706 five
19707 "#
19708 .unindent();
19709 executor.run_until_parked();
19710 cx.set_state(
19711 &r#"
19712 one
19713 two
19714 fˇour
19715 five
19716 "#
19717 .unindent(),
19718 );
19719
19720 cx.set_head_text(&base_text);
19721 executor.run_until_parked();
19722
19723 cx.update_editor(|editor, window, cx| {
19724 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19725 });
19726 executor.run_until_parked();
19727
19728 cx.assert_state_with_diff(
19729 r#"
19730 one
19731 two
19732 - three
19733 fˇour
19734 five
19735 "#
19736 .unindent(),
19737 );
19738
19739 cx.update_editor(|editor, window, cx| {
19740 editor.backspace(&Backspace, window, cx);
19741 editor.backspace(&Backspace, window, cx);
19742 });
19743 executor.run_until_parked();
19744 cx.assert_state_with_diff(
19745 r#"
19746 one
19747 two
19748 - threeˇ
19749 - four
19750 + our
19751 five
19752 "#
19753 .unindent(),
19754 );
19755}
19756
19757#[gpui::test]
19758async fn test_edit_after_expanded_modification_hunk(
19759 executor: BackgroundExecutor,
19760 cx: &mut TestAppContext,
19761) {
19762 init_test(cx, |_| {});
19763
19764 let mut cx = EditorTestContext::new(cx).await;
19765
19766 let diff_base = r#"
19767 use some::mod1;
19768 use some::mod2;
19769
19770 const A: u32 = 42;
19771 const B: u32 = 42;
19772 const C: u32 = 42;
19773 const D: u32 = 42;
19774
19775
19776 fn main() {
19777 println!("hello");
19778
19779 println!("world");
19780 }"#
19781 .unindent();
19782
19783 cx.set_state(
19784 &r#"
19785 use some::mod1;
19786 use some::mod2;
19787
19788 const A: u32 = 42;
19789 const B: u32 = 42;
19790 const C: u32 = 43ˇ
19791 const D: u32 = 42;
19792
19793
19794 fn main() {
19795 println!("hello");
19796
19797 println!("world");
19798 }"#
19799 .unindent(),
19800 );
19801
19802 cx.set_head_text(&diff_base);
19803 executor.run_until_parked();
19804 cx.update_editor(|editor, window, cx| {
19805 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19806 });
19807 executor.run_until_parked();
19808
19809 cx.assert_state_with_diff(
19810 r#"
19811 use some::mod1;
19812 use some::mod2;
19813
19814 const A: u32 = 42;
19815 const B: u32 = 42;
19816 - const C: u32 = 42;
19817 + const C: u32 = 43ˇ
19818 const D: u32 = 42;
19819
19820
19821 fn main() {
19822 println!("hello");
19823
19824 println!("world");
19825 }"#
19826 .unindent(),
19827 );
19828
19829 cx.update_editor(|editor, window, cx| {
19830 editor.handle_input("\nnew_line\n", window, cx);
19831 });
19832 executor.run_until_parked();
19833
19834 cx.assert_state_with_diff(
19835 r#"
19836 use some::mod1;
19837 use some::mod2;
19838
19839 const A: u32 = 42;
19840 const B: u32 = 42;
19841 - const C: u32 = 42;
19842 + const C: u32 = 43
19843 + new_line
19844 + ˇ
19845 const D: u32 = 42;
19846
19847
19848 fn main() {
19849 println!("hello");
19850
19851 println!("world");
19852 }"#
19853 .unindent(),
19854 );
19855}
19856
19857#[gpui::test]
19858async fn test_stage_and_unstage_added_file_hunk(
19859 executor: BackgroundExecutor,
19860 cx: &mut TestAppContext,
19861) {
19862 init_test(cx, |_| {});
19863
19864 let mut cx = EditorTestContext::new(cx).await;
19865 cx.update_editor(|editor, _, cx| {
19866 editor.set_expand_all_diff_hunks(cx);
19867 });
19868
19869 let working_copy = r#"
19870 ˇfn main() {
19871 println!("hello, world!");
19872 }
19873 "#
19874 .unindent();
19875
19876 cx.set_state(&working_copy);
19877 executor.run_until_parked();
19878
19879 cx.assert_state_with_diff(
19880 r#"
19881 + ˇfn main() {
19882 + println!("hello, world!");
19883 + }
19884 "#
19885 .unindent(),
19886 );
19887 cx.assert_index_text(None);
19888
19889 cx.update_editor(|editor, window, cx| {
19890 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19891 });
19892 executor.run_until_parked();
19893 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
19894 cx.assert_state_with_diff(
19895 r#"
19896 + ˇfn main() {
19897 + println!("hello, world!");
19898 + }
19899 "#
19900 .unindent(),
19901 );
19902
19903 cx.update_editor(|editor, window, cx| {
19904 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19905 });
19906 executor.run_until_parked();
19907 cx.assert_index_text(None);
19908}
19909
19910async fn setup_indent_guides_editor(
19911 text: &str,
19912 cx: &mut TestAppContext,
19913) -> (BufferId, EditorTestContext) {
19914 init_test(cx, |_| {});
19915
19916 let mut cx = EditorTestContext::new(cx).await;
19917
19918 let buffer_id = cx.update_editor(|editor, window, cx| {
19919 editor.set_text(text, window, cx);
19920 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
19921
19922 buffer_ids[0]
19923 });
19924
19925 (buffer_id, cx)
19926}
19927
19928fn assert_indent_guides(
19929 range: Range<u32>,
19930 expected: Vec<IndentGuide>,
19931 active_indices: Option<Vec<usize>>,
19932 cx: &mut EditorTestContext,
19933) {
19934 let indent_guides = cx.update_editor(|editor, window, cx| {
19935 let snapshot = editor.snapshot(window, cx).display_snapshot;
19936 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
19937 editor,
19938 MultiBufferRow(range.start)..MultiBufferRow(range.end),
19939 true,
19940 &snapshot,
19941 cx,
19942 );
19943
19944 indent_guides.sort_by(|a, b| {
19945 a.depth.cmp(&b.depth).then(
19946 a.start_row
19947 .cmp(&b.start_row)
19948 .then(a.end_row.cmp(&b.end_row)),
19949 )
19950 });
19951 indent_guides
19952 });
19953
19954 if let Some(expected) = active_indices {
19955 let active_indices = cx.update_editor(|editor, window, cx| {
19956 let snapshot = editor.snapshot(window, cx).display_snapshot;
19957 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
19958 });
19959
19960 assert_eq!(
19961 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
19962 expected,
19963 "Active indent guide indices do not match"
19964 );
19965 }
19966
19967 assert_eq!(indent_guides, expected, "Indent guides do not match");
19968}
19969
19970fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
19971 IndentGuide {
19972 buffer_id,
19973 start_row: MultiBufferRow(start_row),
19974 end_row: MultiBufferRow(end_row),
19975 depth,
19976 tab_size: 4,
19977 settings: IndentGuideSettings {
19978 enabled: true,
19979 line_width: 1,
19980 active_line_width: 1,
19981 coloring: IndentGuideColoring::default(),
19982 background_coloring: IndentGuideBackgroundColoring::default(),
19983 },
19984 }
19985}
19986
19987#[gpui::test]
19988async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
19989 let (buffer_id, mut cx) = setup_indent_guides_editor(
19990 &"
19991 fn main() {
19992 let a = 1;
19993 }"
19994 .unindent(),
19995 cx,
19996 )
19997 .await;
19998
19999 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20000}
20001
20002#[gpui::test]
20003async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
20004 let (buffer_id, mut cx) = setup_indent_guides_editor(
20005 &"
20006 fn main() {
20007 let a = 1;
20008 let b = 2;
20009 }"
20010 .unindent(),
20011 cx,
20012 )
20013 .await;
20014
20015 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
20016}
20017
20018#[gpui::test]
20019async fn test_indent_guide_nested(cx: &mut TestAppContext) {
20020 let (buffer_id, mut cx) = setup_indent_guides_editor(
20021 &"
20022 fn main() {
20023 let a = 1;
20024 if a == 3 {
20025 let b = 2;
20026 } else {
20027 let c = 3;
20028 }
20029 }"
20030 .unindent(),
20031 cx,
20032 )
20033 .await;
20034
20035 assert_indent_guides(
20036 0..8,
20037 vec![
20038 indent_guide(buffer_id, 1, 6, 0),
20039 indent_guide(buffer_id, 3, 3, 1),
20040 indent_guide(buffer_id, 5, 5, 1),
20041 ],
20042 None,
20043 &mut cx,
20044 );
20045}
20046
20047#[gpui::test]
20048async fn test_indent_guide_tab(cx: &mut TestAppContext) {
20049 let (buffer_id, mut cx) = setup_indent_guides_editor(
20050 &"
20051 fn main() {
20052 let a = 1;
20053 let b = 2;
20054 let c = 3;
20055 }"
20056 .unindent(),
20057 cx,
20058 )
20059 .await;
20060
20061 assert_indent_guides(
20062 0..5,
20063 vec![
20064 indent_guide(buffer_id, 1, 3, 0),
20065 indent_guide(buffer_id, 2, 2, 1),
20066 ],
20067 None,
20068 &mut cx,
20069 );
20070}
20071
20072#[gpui::test]
20073async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
20074 let (buffer_id, mut cx) = setup_indent_guides_editor(
20075 &"
20076 fn main() {
20077 let a = 1;
20078
20079 let c = 3;
20080 }"
20081 .unindent(),
20082 cx,
20083 )
20084 .await;
20085
20086 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
20087}
20088
20089#[gpui::test]
20090async fn test_indent_guide_complex(cx: &mut TestAppContext) {
20091 let (buffer_id, mut cx) = setup_indent_guides_editor(
20092 &"
20093 fn main() {
20094 let a = 1;
20095
20096 let c = 3;
20097
20098 if a == 3 {
20099 let b = 2;
20100 } else {
20101 let c = 3;
20102 }
20103 }"
20104 .unindent(),
20105 cx,
20106 )
20107 .await;
20108
20109 assert_indent_guides(
20110 0..11,
20111 vec![
20112 indent_guide(buffer_id, 1, 9, 0),
20113 indent_guide(buffer_id, 6, 6, 1),
20114 indent_guide(buffer_id, 8, 8, 1),
20115 ],
20116 None,
20117 &mut cx,
20118 );
20119}
20120
20121#[gpui::test]
20122async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
20123 let (buffer_id, mut cx) = setup_indent_guides_editor(
20124 &"
20125 fn main() {
20126 let a = 1;
20127
20128 let c = 3;
20129
20130 if a == 3 {
20131 let b = 2;
20132 } else {
20133 let c = 3;
20134 }
20135 }"
20136 .unindent(),
20137 cx,
20138 )
20139 .await;
20140
20141 assert_indent_guides(
20142 1..11,
20143 vec![
20144 indent_guide(buffer_id, 1, 9, 0),
20145 indent_guide(buffer_id, 6, 6, 1),
20146 indent_guide(buffer_id, 8, 8, 1),
20147 ],
20148 None,
20149 &mut cx,
20150 );
20151}
20152
20153#[gpui::test]
20154async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
20155 let (buffer_id, mut cx) = setup_indent_guides_editor(
20156 &"
20157 fn main() {
20158 let a = 1;
20159
20160 let c = 3;
20161
20162 if a == 3 {
20163 let b = 2;
20164 } else {
20165 let c = 3;
20166 }
20167 }"
20168 .unindent(),
20169 cx,
20170 )
20171 .await;
20172
20173 assert_indent_guides(
20174 1..10,
20175 vec![
20176 indent_guide(buffer_id, 1, 9, 0),
20177 indent_guide(buffer_id, 6, 6, 1),
20178 indent_guide(buffer_id, 8, 8, 1),
20179 ],
20180 None,
20181 &mut cx,
20182 );
20183}
20184
20185#[gpui::test]
20186async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
20187 let (buffer_id, mut cx) = setup_indent_guides_editor(
20188 &"
20189 fn main() {
20190 if a {
20191 b(
20192 c,
20193 d,
20194 )
20195 } else {
20196 e(
20197 f
20198 )
20199 }
20200 }"
20201 .unindent(),
20202 cx,
20203 )
20204 .await;
20205
20206 assert_indent_guides(
20207 0..11,
20208 vec![
20209 indent_guide(buffer_id, 1, 10, 0),
20210 indent_guide(buffer_id, 2, 5, 1),
20211 indent_guide(buffer_id, 7, 9, 1),
20212 indent_guide(buffer_id, 3, 4, 2),
20213 indent_guide(buffer_id, 8, 8, 2),
20214 ],
20215 None,
20216 &mut cx,
20217 );
20218
20219 cx.update_editor(|editor, window, cx| {
20220 editor.fold_at(MultiBufferRow(2), window, cx);
20221 assert_eq!(
20222 editor.display_text(cx),
20223 "
20224 fn main() {
20225 if a {
20226 b(⋯
20227 )
20228 } else {
20229 e(
20230 f
20231 )
20232 }
20233 }"
20234 .unindent()
20235 );
20236 });
20237
20238 assert_indent_guides(
20239 0..11,
20240 vec![
20241 indent_guide(buffer_id, 1, 10, 0),
20242 indent_guide(buffer_id, 2, 5, 1),
20243 indent_guide(buffer_id, 7, 9, 1),
20244 indent_guide(buffer_id, 8, 8, 2),
20245 ],
20246 None,
20247 &mut cx,
20248 );
20249}
20250
20251#[gpui::test]
20252async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
20253 let (buffer_id, mut cx) = setup_indent_guides_editor(
20254 &"
20255 block1
20256 block2
20257 block3
20258 block4
20259 block2
20260 block1
20261 block1"
20262 .unindent(),
20263 cx,
20264 )
20265 .await;
20266
20267 assert_indent_guides(
20268 1..10,
20269 vec![
20270 indent_guide(buffer_id, 1, 4, 0),
20271 indent_guide(buffer_id, 2, 3, 1),
20272 indent_guide(buffer_id, 3, 3, 2),
20273 ],
20274 None,
20275 &mut cx,
20276 );
20277}
20278
20279#[gpui::test]
20280async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
20281 let (buffer_id, mut cx) = setup_indent_guides_editor(
20282 &"
20283 block1
20284 block2
20285 block3
20286
20287 block1
20288 block1"
20289 .unindent(),
20290 cx,
20291 )
20292 .await;
20293
20294 assert_indent_guides(
20295 0..6,
20296 vec![
20297 indent_guide(buffer_id, 1, 2, 0),
20298 indent_guide(buffer_id, 2, 2, 1),
20299 ],
20300 None,
20301 &mut cx,
20302 );
20303}
20304
20305#[gpui::test]
20306async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
20307 let (buffer_id, mut cx) = setup_indent_guides_editor(
20308 &"
20309 function component() {
20310 \treturn (
20311 \t\t\t
20312 \t\t<div>
20313 \t\t\t<abc></abc>
20314 \t\t</div>
20315 \t)
20316 }"
20317 .unindent(),
20318 cx,
20319 )
20320 .await;
20321
20322 assert_indent_guides(
20323 0..8,
20324 vec![
20325 indent_guide(buffer_id, 1, 6, 0),
20326 indent_guide(buffer_id, 2, 5, 1),
20327 indent_guide(buffer_id, 4, 4, 2),
20328 ],
20329 None,
20330 &mut cx,
20331 );
20332}
20333
20334#[gpui::test]
20335async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
20336 let (buffer_id, mut cx) = setup_indent_guides_editor(
20337 &"
20338 function component() {
20339 \treturn (
20340 \t
20341 \t\t<div>
20342 \t\t\t<abc></abc>
20343 \t\t</div>
20344 \t)
20345 }"
20346 .unindent(),
20347 cx,
20348 )
20349 .await;
20350
20351 assert_indent_guides(
20352 0..8,
20353 vec![
20354 indent_guide(buffer_id, 1, 6, 0),
20355 indent_guide(buffer_id, 2, 5, 1),
20356 indent_guide(buffer_id, 4, 4, 2),
20357 ],
20358 None,
20359 &mut cx,
20360 );
20361}
20362
20363#[gpui::test]
20364async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
20365 let (buffer_id, mut cx) = setup_indent_guides_editor(
20366 &"
20367 block1
20368
20369
20370
20371 block2
20372 "
20373 .unindent(),
20374 cx,
20375 )
20376 .await;
20377
20378 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20379}
20380
20381#[gpui::test]
20382async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
20383 let (buffer_id, mut cx) = setup_indent_guides_editor(
20384 &"
20385 def a:
20386 \tb = 3
20387 \tif True:
20388 \t\tc = 4
20389 \t\td = 5
20390 \tprint(b)
20391 "
20392 .unindent(),
20393 cx,
20394 )
20395 .await;
20396
20397 assert_indent_guides(
20398 0..6,
20399 vec![
20400 indent_guide(buffer_id, 1, 5, 0),
20401 indent_guide(buffer_id, 3, 4, 1),
20402 ],
20403 None,
20404 &mut cx,
20405 );
20406}
20407
20408#[gpui::test]
20409async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
20410 let (buffer_id, mut cx) = setup_indent_guides_editor(
20411 &"
20412 fn main() {
20413 let a = 1;
20414 }"
20415 .unindent(),
20416 cx,
20417 )
20418 .await;
20419
20420 cx.update_editor(|editor, window, cx| {
20421 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20422 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20423 });
20424 });
20425
20426 assert_indent_guides(
20427 0..3,
20428 vec![indent_guide(buffer_id, 1, 1, 0)],
20429 Some(vec![0]),
20430 &mut cx,
20431 );
20432}
20433
20434#[gpui::test]
20435async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
20436 let (buffer_id, mut cx) = setup_indent_guides_editor(
20437 &"
20438 fn main() {
20439 if 1 == 2 {
20440 let a = 1;
20441 }
20442 }"
20443 .unindent(),
20444 cx,
20445 )
20446 .await;
20447
20448 cx.update_editor(|editor, window, cx| {
20449 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20450 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20451 });
20452 });
20453
20454 assert_indent_guides(
20455 0..4,
20456 vec![
20457 indent_guide(buffer_id, 1, 3, 0),
20458 indent_guide(buffer_id, 2, 2, 1),
20459 ],
20460 Some(vec![1]),
20461 &mut cx,
20462 );
20463
20464 cx.update_editor(|editor, window, cx| {
20465 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20466 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20467 });
20468 });
20469
20470 assert_indent_guides(
20471 0..4,
20472 vec![
20473 indent_guide(buffer_id, 1, 3, 0),
20474 indent_guide(buffer_id, 2, 2, 1),
20475 ],
20476 Some(vec![1]),
20477 &mut cx,
20478 );
20479
20480 cx.update_editor(|editor, window, cx| {
20481 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20482 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
20483 });
20484 });
20485
20486 assert_indent_guides(
20487 0..4,
20488 vec![
20489 indent_guide(buffer_id, 1, 3, 0),
20490 indent_guide(buffer_id, 2, 2, 1),
20491 ],
20492 Some(vec![0]),
20493 &mut cx,
20494 );
20495}
20496
20497#[gpui::test]
20498async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
20499 let (buffer_id, mut cx) = setup_indent_guides_editor(
20500 &"
20501 fn main() {
20502 let a = 1;
20503
20504 let b = 2;
20505 }"
20506 .unindent(),
20507 cx,
20508 )
20509 .await;
20510
20511 cx.update_editor(|editor, window, cx| {
20512 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20513 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20514 });
20515 });
20516
20517 assert_indent_guides(
20518 0..5,
20519 vec![indent_guide(buffer_id, 1, 3, 0)],
20520 Some(vec![0]),
20521 &mut cx,
20522 );
20523}
20524
20525#[gpui::test]
20526async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
20527 let (buffer_id, mut cx) = setup_indent_guides_editor(
20528 &"
20529 def m:
20530 a = 1
20531 pass"
20532 .unindent(),
20533 cx,
20534 )
20535 .await;
20536
20537 cx.update_editor(|editor, window, cx| {
20538 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20539 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20540 });
20541 });
20542
20543 assert_indent_guides(
20544 0..3,
20545 vec![indent_guide(buffer_id, 1, 2, 0)],
20546 Some(vec![0]),
20547 &mut cx,
20548 );
20549}
20550
20551#[gpui::test]
20552async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
20553 init_test(cx, |_| {});
20554 let mut cx = EditorTestContext::new(cx).await;
20555 let text = indoc! {
20556 "
20557 impl A {
20558 fn b() {
20559 0;
20560 3;
20561 5;
20562 6;
20563 7;
20564 }
20565 }
20566 "
20567 };
20568 let base_text = indoc! {
20569 "
20570 impl A {
20571 fn b() {
20572 0;
20573 1;
20574 2;
20575 3;
20576 4;
20577 }
20578 fn c() {
20579 5;
20580 6;
20581 7;
20582 }
20583 }
20584 "
20585 };
20586
20587 cx.update_editor(|editor, window, cx| {
20588 editor.set_text(text, window, cx);
20589
20590 editor.buffer().update(cx, |multibuffer, cx| {
20591 let buffer = multibuffer.as_singleton().unwrap();
20592 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
20593
20594 multibuffer.set_all_diff_hunks_expanded(cx);
20595 multibuffer.add_diff(diff, cx);
20596
20597 buffer.read(cx).remote_id()
20598 })
20599 });
20600 cx.run_until_parked();
20601
20602 cx.assert_state_with_diff(
20603 indoc! { "
20604 impl A {
20605 fn b() {
20606 0;
20607 - 1;
20608 - 2;
20609 3;
20610 - 4;
20611 - }
20612 - fn c() {
20613 5;
20614 6;
20615 7;
20616 }
20617 }
20618 ˇ"
20619 }
20620 .to_string(),
20621 );
20622
20623 let mut actual_guides = cx.update_editor(|editor, window, cx| {
20624 editor
20625 .snapshot(window, cx)
20626 .buffer_snapshot
20627 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
20628 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
20629 .collect::<Vec<_>>()
20630 });
20631 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
20632 assert_eq!(
20633 actual_guides,
20634 vec![
20635 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
20636 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
20637 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
20638 ]
20639 );
20640}
20641
20642#[gpui::test]
20643async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
20644 init_test(cx, |_| {});
20645 let mut cx = EditorTestContext::new(cx).await;
20646
20647 let diff_base = r#"
20648 a
20649 b
20650 c
20651 "#
20652 .unindent();
20653
20654 cx.set_state(
20655 &r#"
20656 ˇA
20657 b
20658 C
20659 "#
20660 .unindent(),
20661 );
20662 cx.set_head_text(&diff_base);
20663 cx.update_editor(|editor, window, cx| {
20664 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20665 });
20666 executor.run_until_parked();
20667
20668 let both_hunks_expanded = r#"
20669 - a
20670 + ˇA
20671 b
20672 - c
20673 + C
20674 "#
20675 .unindent();
20676
20677 cx.assert_state_with_diff(both_hunks_expanded.clone());
20678
20679 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20680 let snapshot = editor.snapshot(window, cx);
20681 let hunks = editor
20682 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
20683 .collect::<Vec<_>>();
20684 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20685 let buffer_id = hunks[0].buffer_id;
20686 hunks
20687 .into_iter()
20688 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20689 .collect::<Vec<_>>()
20690 });
20691 assert_eq!(hunk_ranges.len(), 2);
20692
20693 cx.update_editor(|editor, _, cx| {
20694 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20695 });
20696 executor.run_until_parked();
20697
20698 let second_hunk_expanded = r#"
20699 ˇA
20700 b
20701 - c
20702 + C
20703 "#
20704 .unindent();
20705
20706 cx.assert_state_with_diff(second_hunk_expanded);
20707
20708 cx.update_editor(|editor, _, cx| {
20709 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20710 });
20711 executor.run_until_parked();
20712
20713 cx.assert_state_with_diff(both_hunks_expanded.clone());
20714
20715 cx.update_editor(|editor, _, cx| {
20716 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20717 });
20718 executor.run_until_parked();
20719
20720 let first_hunk_expanded = r#"
20721 - a
20722 + ˇA
20723 b
20724 C
20725 "#
20726 .unindent();
20727
20728 cx.assert_state_with_diff(first_hunk_expanded);
20729
20730 cx.update_editor(|editor, _, cx| {
20731 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20732 });
20733 executor.run_until_parked();
20734
20735 cx.assert_state_with_diff(both_hunks_expanded);
20736
20737 cx.set_state(
20738 &r#"
20739 ˇA
20740 b
20741 "#
20742 .unindent(),
20743 );
20744 cx.run_until_parked();
20745
20746 // TODO this cursor position seems bad
20747 cx.assert_state_with_diff(
20748 r#"
20749 - ˇa
20750 + A
20751 b
20752 "#
20753 .unindent(),
20754 );
20755
20756 cx.update_editor(|editor, window, cx| {
20757 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20758 });
20759
20760 cx.assert_state_with_diff(
20761 r#"
20762 - ˇa
20763 + A
20764 b
20765 - c
20766 "#
20767 .unindent(),
20768 );
20769
20770 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20771 let snapshot = editor.snapshot(window, cx);
20772 let hunks = editor
20773 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
20774 .collect::<Vec<_>>();
20775 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20776 let buffer_id = hunks[0].buffer_id;
20777 hunks
20778 .into_iter()
20779 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20780 .collect::<Vec<_>>()
20781 });
20782 assert_eq!(hunk_ranges.len(), 2);
20783
20784 cx.update_editor(|editor, _, cx| {
20785 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20786 });
20787 executor.run_until_parked();
20788
20789 cx.assert_state_with_diff(
20790 r#"
20791 - ˇa
20792 + A
20793 b
20794 "#
20795 .unindent(),
20796 );
20797}
20798
20799#[gpui::test]
20800async fn test_toggle_deletion_hunk_at_start_of_file(
20801 executor: BackgroundExecutor,
20802 cx: &mut TestAppContext,
20803) {
20804 init_test(cx, |_| {});
20805 let mut cx = EditorTestContext::new(cx).await;
20806
20807 let diff_base = r#"
20808 a
20809 b
20810 c
20811 "#
20812 .unindent();
20813
20814 cx.set_state(
20815 &r#"
20816 ˇb
20817 c
20818 "#
20819 .unindent(),
20820 );
20821 cx.set_head_text(&diff_base);
20822 cx.update_editor(|editor, window, cx| {
20823 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20824 });
20825 executor.run_until_parked();
20826
20827 let hunk_expanded = r#"
20828 - a
20829 ˇb
20830 c
20831 "#
20832 .unindent();
20833
20834 cx.assert_state_with_diff(hunk_expanded.clone());
20835
20836 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20837 let snapshot = editor.snapshot(window, cx);
20838 let hunks = editor
20839 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
20840 .collect::<Vec<_>>();
20841 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20842 let buffer_id = hunks[0].buffer_id;
20843 hunks
20844 .into_iter()
20845 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20846 .collect::<Vec<_>>()
20847 });
20848 assert_eq!(hunk_ranges.len(), 1);
20849
20850 cx.update_editor(|editor, _, cx| {
20851 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20852 });
20853 executor.run_until_parked();
20854
20855 let hunk_collapsed = r#"
20856 ˇb
20857 c
20858 "#
20859 .unindent();
20860
20861 cx.assert_state_with_diff(hunk_collapsed);
20862
20863 cx.update_editor(|editor, _, cx| {
20864 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20865 });
20866 executor.run_until_parked();
20867
20868 cx.assert_state_with_diff(hunk_expanded);
20869}
20870
20871#[gpui::test]
20872async fn test_display_diff_hunks(cx: &mut TestAppContext) {
20873 init_test(cx, |_| {});
20874
20875 let fs = FakeFs::new(cx.executor());
20876 fs.insert_tree(
20877 path!("/test"),
20878 json!({
20879 ".git": {},
20880 "file-1": "ONE\n",
20881 "file-2": "TWO\n",
20882 "file-3": "THREE\n",
20883 }),
20884 )
20885 .await;
20886
20887 fs.set_head_for_repo(
20888 path!("/test/.git").as_ref(),
20889 &[
20890 ("file-1", "one\n".into()),
20891 ("file-2", "two\n".into()),
20892 ("file-3", "three\n".into()),
20893 ],
20894 "deadbeef",
20895 );
20896
20897 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
20898 let mut buffers = vec![];
20899 for i in 1..=3 {
20900 let buffer = project
20901 .update(cx, |project, cx| {
20902 let path = format!(path!("/test/file-{}"), i);
20903 project.open_local_buffer(path, cx)
20904 })
20905 .await
20906 .unwrap();
20907 buffers.push(buffer);
20908 }
20909
20910 let multibuffer = cx.new(|cx| {
20911 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
20912 multibuffer.set_all_diff_hunks_expanded(cx);
20913 for buffer in &buffers {
20914 let snapshot = buffer.read(cx).snapshot();
20915 multibuffer.set_excerpts_for_path(
20916 PathKey::namespaced(
20917 0,
20918 buffer.read(cx).file().unwrap().path().as_unix_str().into(),
20919 ),
20920 buffer.clone(),
20921 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
20922 2,
20923 cx,
20924 );
20925 }
20926 multibuffer
20927 });
20928
20929 let editor = cx.add_window(|window, cx| {
20930 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
20931 });
20932 cx.run_until_parked();
20933
20934 let snapshot = editor
20935 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
20936 .unwrap();
20937 let hunks = snapshot
20938 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
20939 .map(|hunk| match hunk {
20940 DisplayDiffHunk::Unfolded {
20941 display_row_range, ..
20942 } => display_row_range,
20943 DisplayDiffHunk::Folded { .. } => unreachable!(),
20944 })
20945 .collect::<Vec<_>>();
20946 assert_eq!(
20947 hunks,
20948 [
20949 DisplayRow(2)..DisplayRow(4),
20950 DisplayRow(7)..DisplayRow(9),
20951 DisplayRow(12)..DisplayRow(14),
20952 ]
20953 );
20954}
20955
20956#[gpui::test]
20957async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
20958 init_test(cx, |_| {});
20959
20960 let mut cx = EditorTestContext::new(cx).await;
20961 cx.set_head_text(indoc! { "
20962 one
20963 two
20964 three
20965 four
20966 five
20967 "
20968 });
20969 cx.set_index_text(indoc! { "
20970 one
20971 two
20972 three
20973 four
20974 five
20975 "
20976 });
20977 cx.set_state(indoc! {"
20978 one
20979 TWO
20980 ˇTHREE
20981 FOUR
20982 five
20983 "});
20984 cx.run_until_parked();
20985 cx.update_editor(|editor, window, cx| {
20986 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20987 });
20988 cx.run_until_parked();
20989 cx.assert_index_text(Some(indoc! {"
20990 one
20991 TWO
20992 THREE
20993 FOUR
20994 five
20995 "}));
20996 cx.set_state(indoc! { "
20997 one
20998 TWO
20999 ˇTHREE-HUNDRED
21000 FOUR
21001 five
21002 "});
21003 cx.run_until_parked();
21004 cx.update_editor(|editor, window, cx| {
21005 let snapshot = editor.snapshot(window, cx);
21006 let hunks = editor
21007 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
21008 .collect::<Vec<_>>();
21009 assert_eq!(hunks.len(), 1);
21010 assert_eq!(
21011 hunks[0].status(),
21012 DiffHunkStatus {
21013 kind: DiffHunkStatusKind::Modified,
21014 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
21015 }
21016 );
21017
21018 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21019 });
21020 cx.run_until_parked();
21021 cx.assert_index_text(Some(indoc! {"
21022 one
21023 TWO
21024 THREE-HUNDRED
21025 FOUR
21026 five
21027 "}));
21028}
21029
21030#[gpui::test]
21031fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
21032 init_test(cx, |_| {});
21033
21034 let editor = cx.add_window(|window, cx| {
21035 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
21036 build_editor(buffer, window, cx)
21037 });
21038
21039 let render_args = Arc::new(Mutex::new(None));
21040 let snapshot = editor
21041 .update(cx, |editor, window, cx| {
21042 let snapshot = editor.buffer().read(cx).snapshot(cx);
21043 let range =
21044 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
21045
21046 struct RenderArgs {
21047 row: MultiBufferRow,
21048 folded: bool,
21049 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
21050 }
21051
21052 let crease = Crease::inline(
21053 range,
21054 FoldPlaceholder::test(),
21055 {
21056 let toggle_callback = render_args.clone();
21057 move |row, folded, callback, _window, _cx| {
21058 *toggle_callback.lock() = Some(RenderArgs {
21059 row,
21060 folded,
21061 callback,
21062 });
21063 div()
21064 }
21065 },
21066 |_row, _folded, _window, _cx| div(),
21067 );
21068
21069 editor.insert_creases(Some(crease), cx);
21070 let snapshot = editor.snapshot(window, cx);
21071 let _div =
21072 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
21073 snapshot
21074 })
21075 .unwrap();
21076
21077 let render_args = render_args.lock().take().unwrap();
21078 assert_eq!(render_args.row, MultiBufferRow(1));
21079 assert!(!render_args.folded);
21080 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21081
21082 cx.update_window(*editor, |_, window, cx| {
21083 (render_args.callback)(true, window, cx)
21084 })
21085 .unwrap();
21086 let snapshot = editor
21087 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21088 .unwrap();
21089 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
21090
21091 cx.update_window(*editor, |_, window, cx| {
21092 (render_args.callback)(false, window, cx)
21093 })
21094 .unwrap();
21095 let snapshot = editor
21096 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21097 .unwrap();
21098 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21099}
21100
21101#[gpui::test]
21102async fn test_input_text(cx: &mut TestAppContext) {
21103 init_test(cx, |_| {});
21104 let mut cx = EditorTestContext::new(cx).await;
21105
21106 cx.set_state(
21107 &r#"ˇone
21108 two
21109
21110 three
21111 fourˇ
21112 five
21113
21114 siˇx"#
21115 .unindent(),
21116 );
21117
21118 cx.dispatch_action(HandleInput(String::new()));
21119 cx.assert_editor_state(
21120 &r#"ˇone
21121 two
21122
21123 three
21124 fourˇ
21125 five
21126
21127 siˇx"#
21128 .unindent(),
21129 );
21130
21131 cx.dispatch_action(HandleInput("AAAA".to_string()));
21132 cx.assert_editor_state(
21133 &r#"AAAAˇone
21134 two
21135
21136 three
21137 fourAAAAˇ
21138 five
21139
21140 siAAAAˇx"#
21141 .unindent(),
21142 );
21143}
21144
21145#[gpui::test]
21146async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
21147 init_test(cx, |_| {});
21148
21149 let mut cx = EditorTestContext::new(cx).await;
21150 cx.set_state(
21151 r#"let foo = 1;
21152let foo = 2;
21153let foo = 3;
21154let fooˇ = 4;
21155let foo = 5;
21156let foo = 6;
21157let foo = 7;
21158let foo = 8;
21159let foo = 9;
21160let foo = 10;
21161let foo = 11;
21162let foo = 12;
21163let foo = 13;
21164let foo = 14;
21165let foo = 15;"#,
21166 );
21167
21168 cx.update_editor(|e, window, cx| {
21169 assert_eq!(
21170 e.next_scroll_position,
21171 NextScrollCursorCenterTopBottom::Center,
21172 "Default next scroll direction is center",
21173 );
21174
21175 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21176 assert_eq!(
21177 e.next_scroll_position,
21178 NextScrollCursorCenterTopBottom::Top,
21179 "After center, next scroll direction should be top",
21180 );
21181
21182 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21183 assert_eq!(
21184 e.next_scroll_position,
21185 NextScrollCursorCenterTopBottom::Bottom,
21186 "After top, next scroll direction should be bottom",
21187 );
21188
21189 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21190 assert_eq!(
21191 e.next_scroll_position,
21192 NextScrollCursorCenterTopBottom::Center,
21193 "After bottom, scrolling should start over",
21194 );
21195
21196 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21197 assert_eq!(
21198 e.next_scroll_position,
21199 NextScrollCursorCenterTopBottom::Top,
21200 "Scrolling continues if retriggered fast enough"
21201 );
21202 });
21203
21204 cx.executor()
21205 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
21206 cx.executor().run_until_parked();
21207 cx.update_editor(|e, _, _| {
21208 assert_eq!(
21209 e.next_scroll_position,
21210 NextScrollCursorCenterTopBottom::Center,
21211 "If scrolling is not triggered fast enough, it should reset"
21212 );
21213 });
21214}
21215
21216#[gpui::test]
21217async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
21218 init_test(cx, |_| {});
21219 let mut cx = EditorLspTestContext::new_rust(
21220 lsp::ServerCapabilities {
21221 definition_provider: Some(lsp::OneOf::Left(true)),
21222 references_provider: Some(lsp::OneOf::Left(true)),
21223 ..lsp::ServerCapabilities::default()
21224 },
21225 cx,
21226 )
21227 .await;
21228
21229 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
21230 let go_to_definition = cx
21231 .lsp
21232 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21233 move |params, _| async move {
21234 if empty_go_to_definition {
21235 Ok(None)
21236 } else {
21237 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
21238 uri: params.text_document_position_params.text_document.uri,
21239 range: lsp::Range::new(
21240 lsp::Position::new(4, 3),
21241 lsp::Position::new(4, 6),
21242 ),
21243 })))
21244 }
21245 },
21246 );
21247 let references = cx
21248 .lsp
21249 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21250 Ok(Some(vec![lsp::Location {
21251 uri: params.text_document_position.text_document.uri,
21252 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
21253 }]))
21254 });
21255 (go_to_definition, references)
21256 };
21257
21258 cx.set_state(
21259 &r#"fn one() {
21260 let mut a = ˇtwo();
21261 }
21262
21263 fn two() {}"#
21264 .unindent(),
21265 );
21266 set_up_lsp_handlers(false, &mut cx);
21267 let navigated = cx
21268 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21269 .await
21270 .expect("Failed to navigate to definition");
21271 assert_eq!(
21272 navigated,
21273 Navigated::Yes,
21274 "Should have navigated to definition from the GetDefinition response"
21275 );
21276 cx.assert_editor_state(
21277 &r#"fn one() {
21278 let mut a = two();
21279 }
21280
21281 fn «twoˇ»() {}"#
21282 .unindent(),
21283 );
21284
21285 let editors = cx.update_workspace(|workspace, _, cx| {
21286 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21287 });
21288 cx.update_editor(|_, _, test_editor_cx| {
21289 assert_eq!(
21290 editors.len(),
21291 1,
21292 "Initially, only one, test, editor should be open in the workspace"
21293 );
21294 assert_eq!(
21295 test_editor_cx.entity(),
21296 editors.last().expect("Asserted len is 1").clone()
21297 );
21298 });
21299
21300 set_up_lsp_handlers(true, &mut cx);
21301 let navigated = cx
21302 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21303 .await
21304 .expect("Failed to navigate to lookup references");
21305 assert_eq!(
21306 navigated,
21307 Navigated::Yes,
21308 "Should have navigated to references as a fallback after empty GoToDefinition response"
21309 );
21310 // We should not change the selections in the existing file,
21311 // if opening another milti buffer with the references
21312 cx.assert_editor_state(
21313 &r#"fn one() {
21314 let mut a = two();
21315 }
21316
21317 fn «twoˇ»() {}"#
21318 .unindent(),
21319 );
21320 let editors = cx.update_workspace(|workspace, _, cx| {
21321 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21322 });
21323 cx.update_editor(|_, _, test_editor_cx| {
21324 assert_eq!(
21325 editors.len(),
21326 2,
21327 "After falling back to references search, we open a new editor with the results"
21328 );
21329 let references_fallback_text = editors
21330 .into_iter()
21331 .find(|new_editor| *new_editor != test_editor_cx.entity())
21332 .expect("Should have one non-test editor now")
21333 .read(test_editor_cx)
21334 .text(test_editor_cx);
21335 assert_eq!(
21336 references_fallback_text, "fn one() {\n let mut a = two();\n}",
21337 "Should use the range from the references response and not the GoToDefinition one"
21338 );
21339 });
21340}
21341
21342#[gpui::test]
21343async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
21344 init_test(cx, |_| {});
21345 cx.update(|cx| {
21346 let mut editor_settings = EditorSettings::get_global(cx).clone();
21347 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
21348 EditorSettings::override_global(editor_settings, cx);
21349 });
21350 let mut cx = EditorLspTestContext::new_rust(
21351 lsp::ServerCapabilities {
21352 definition_provider: Some(lsp::OneOf::Left(true)),
21353 references_provider: Some(lsp::OneOf::Left(true)),
21354 ..lsp::ServerCapabilities::default()
21355 },
21356 cx,
21357 )
21358 .await;
21359 let original_state = r#"fn one() {
21360 let mut a = ˇtwo();
21361 }
21362
21363 fn two() {}"#
21364 .unindent();
21365 cx.set_state(&original_state);
21366
21367 let mut go_to_definition = cx
21368 .lsp
21369 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21370 move |_, _| async move { Ok(None) },
21371 );
21372 let _references = cx
21373 .lsp
21374 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
21375 panic!("Should not call for references with no go to definition fallback")
21376 });
21377
21378 let navigated = cx
21379 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21380 .await
21381 .expect("Failed to navigate to lookup references");
21382 go_to_definition
21383 .next()
21384 .await
21385 .expect("Should have called the go_to_definition handler");
21386
21387 assert_eq!(
21388 navigated,
21389 Navigated::No,
21390 "Should have navigated to references as a fallback after empty GoToDefinition response"
21391 );
21392 cx.assert_editor_state(&original_state);
21393 let editors = cx.update_workspace(|workspace, _, cx| {
21394 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21395 });
21396 cx.update_editor(|_, _, _| {
21397 assert_eq!(
21398 editors.len(),
21399 1,
21400 "After unsuccessful fallback, no other editor should have been opened"
21401 );
21402 });
21403}
21404
21405#[gpui::test]
21406async fn test_find_all_references_editor_reuse(cx: &mut TestAppContext) {
21407 init_test(cx, |_| {});
21408 let mut cx = EditorLspTestContext::new_rust(
21409 lsp::ServerCapabilities {
21410 references_provider: Some(lsp::OneOf::Left(true)),
21411 ..lsp::ServerCapabilities::default()
21412 },
21413 cx,
21414 )
21415 .await;
21416
21417 cx.set_state(
21418 &r#"
21419 fn one() {
21420 let mut a = two();
21421 }
21422
21423 fn ˇtwo() {}"#
21424 .unindent(),
21425 );
21426 cx.lsp
21427 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21428 Ok(Some(vec![
21429 lsp::Location {
21430 uri: params.text_document_position.text_document.uri.clone(),
21431 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
21432 },
21433 lsp::Location {
21434 uri: params.text_document_position.text_document.uri,
21435 range: lsp::Range::new(lsp::Position::new(4, 4), lsp::Position::new(4, 7)),
21436 },
21437 ]))
21438 });
21439 let navigated = cx
21440 .update_editor(|editor, window, cx| {
21441 editor.find_all_references(&FindAllReferences, window, cx)
21442 })
21443 .unwrap()
21444 .await
21445 .expect("Failed to navigate to references");
21446 assert_eq!(
21447 navigated,
21448 Navigated::Yes,
21449 "Should have navigated to references from the FindAllReferences response"
21450 );
21451 cx.assert_editor_state(
21452 &r#"fn one() {
21453 let mut a = two();
21454 }
21455
21456 fn ˇtwo() {}"#
21457 .unindent(),
21458 );
21459
21460 let editors = cx.update_workspace(|workspace, _, cx| {
21461 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21462 });
21463 cx.update_editor(|_, _, _| {
21464 assert_eq!(editors.len(), 2, "We should have opened a new multibuffer");
21465 });
21466
21467 cx.set_state(
21468 &r#"fn one() {
21469 let mut a = ˇtwo();
21470 }
21471
21472 fn two() {}"#
21473 .unindent(),
21474 );
21475 let navigated = cx
21476 .update_editor(|editor, window, cx| {
21477 editor.find_all_references(&FindAllReferences, window, cx)
21478 })
21479 .unwrap()
21480 .await
21481 .expect("Failed to navigate to references");
21482 assert_eq!(
21483 navigated,
21484 Navigated::Yes,
21485 "Should have navigated to references from the FindAllReferences response"
21486 );
21487 cx.assert_editor_state(
21488 &r#"fn one() {
21489 let mut a = ˇtwo();
21490 }
21491
21492 fn two() {}"#
21493 .unindent(),
21494 );
21495 let editors = cx.update_workspace(|workspace, _, cx| {
21496 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21497 });
21498 cx.update_editor(|_, _, _| {
21499 assert_eq!(
21500 editors.len(),
21501 2,
21502 "should have re-used the previous multibuffer"
21503 );
21504 });
21505
21506 cx.set_state(
21507 &r#"fn one() {
21508 let mut a = ˇtwo();
21509 }
21510 fn three() {}
21511 fn two() {}"#
21512 .unindent(),
21513 );
21514 cx.lsp
21515 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21516 Ok(Some(vec![
21517 lsp::Location {
21518 uri: params.text_document_position.text_document.uri.clone(),
21519 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
21520 },
21521 lsp::Location {
21522 uri: params.text_document_position.text_document.uri,
21523 range: lsp::Range::new(lsp::Position::new(5, 4), lsp::Position::new(5, 7)),
21524 },
21525 ]))
21526 });
21527 let navigated = cx
21528 .update_editor(|editor, window, cx| {
21529 editor.find_all_references(&FindAllReferences, window, cx)
21530 })
21531 .unwrap()
21532 .await
21533 .expect("Failed to navigate to references");
21534 assert_eq!(
21535 navigated,
21536 Navigated::Yes,
21537 "Should have navigated to references from the FindAllReferences response"
21538 );
21539 cx.assert_editor_state(
21540 &r#"fn one() {
21541 let mut a = ˇtwo();
21542 }
21543 fn three() {}
21544 fn two() {}"#
21545 .unindent(),
21546 );
21547 let editors = cx.update_workspace(|workspace, _, cx| {
21548 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21549 });
21550 cx.update_editor(|_, _, _| {
21551 assert_eq!(
21552 editors.len(),
21553 3,
21554 "should have used a new multibuffer as offsets changed"
21555 );
21556 });
21557}
21558#[gpui::test]
21559async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
21560 init_test(cx, |_| {});
21561
21562 let language = Arc::new(Language::new(
21563 LanguageConfig::default(),
21564 Some(tree_sitter_rust::LANGUAGE.into()),
21565 ));
21566
21567 let text = r#"
21568 #[cfg(test)]
21569 mod tests() {
21570 #[test]
21571 fn runnable_1() {
21572 let a = 1;
21573 }
21574
21575 #[test]
21576 fn runnable_2() {
21577 let a = 1;
21578 let b = 2;
21579 }
21580 }
21581 "#
21582 .unindent();
21583
21584 let fs = FakeFs::new(cx.executor());
21585 fs.insert_file("/file.rs", Default::default()).await;
21586
21587 let project = Project::test(fs, ["/a".as_ref()], cx).await;
21588 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21589 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21590 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
21591 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
21592
21593 let editor = cx.new_window_entity(|window, cx| {
21594 Editor::new(
21595 EditorMode::full(),
21596 multi_buffer,
21597 Some(project.clone()),
21598 window,
21599 cx,
21600 )
21601 });
21602
21603 editor.update_in(cx, |editor, window, cx| {
21604 let snapshot = editor.buffer().read(cx).snapshot(cx);
21605 editor.tasks.insert(
21606 (buffer.read(cx).remote_id(), 3),
21607 RunnableTasks {
21608 templates: vec![],
21609 offset: snapshot.anchor_before(43),
21610 column: 0,
21611 extra_variables: HashMap::default(),
21612 context_range: BufferOffset(43)..BufferOffset(85),
21613 },
21614 );
21615 editor.tasks.insert(
21616 (buffer.read(cx).remote_id(), 8),
21617 RunnableTasks {
21618 templates: vec![],
21619 offset: snapshot.anchor_before(86),
21620 column: 0,
21621 extra_variables: HashMap::default(),
21622 context_range: BufferOffset(86)..BufferOffset(191),
21623 },
21624 );
21625
21626 // Test finding task when cursor is inside function body
21627 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21628 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
21629 });
21630 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
21631 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
21632
21633 // Test finding task when cursor is on function name
21634 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21635 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
21636 });
21637 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
21638 assert_eq!(row, 8, "Should find task when cursor is on function name");
21639 });
21640}
21641
21642#[gpui::test]
21643async fn test_folding_buffers(cx: &mut TestAppContext) {
21644 init_test(cx, |_| {});
21645
21646 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
21647 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
21648 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
21649
21650 let fs = FakeFs::new(cx.executor());
21651 fs.insert_tree(
21652 path!("/a"),
21653 json!({
21654 "first.rs": sample_text_1,
21655 "second.rs": sample_text_2,
21656 "third.rs": sample_text_3,
21657 }),
21658 )
21659 .await;
21660 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21661 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21662 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21663 let worktree = project.update(cx, |project, cx| {
21664 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
21665 assert_eq!(worktrees.len(), 1);
21666 worktrees.pop().unwrap()
21667 });
21668 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
21669
21670 let buffer_1 = project
21671 .update(cx, |project, cx| {
21672 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
21673 })
21674 .await
21675 .unwrap();
21676 let buffer_2 = project
21677 .update(cx, |project, cx| {
21678 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
21679 })
21680 .await
21681 .unwrap();
21682 let buffer_3 = project
21683 .update(cx, |project, cx| {
21684 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
21685 })
21686 .await
21687 .unwrap();
21688
21689 let multi_buffer = cx.new(|cx| {
21690 let mut multi_buffer = MultiBuffer::new(ReadWrite);
21691 multi_buffer.push_excerpts(
21692 buffer_1.clone(),
21693 [
21694 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21695 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21696 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21697 ],
21698 cx,
21699 );
21700 multi_buffer.push_excerpts(
21701 buffer_2.clone(),
21702 [
21703 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21704 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21705 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21706 ],
21707 cx,
21708 );
21709 multi_buffer.push_excerpts(
21710 buffer_3.clone(),
21711 [
21712 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21713 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21714 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21715 ],
21716 cx,
21717 );
21718 multi_buffer
21719 });
21720 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
21721 Editor::new(
21722 EditorMode::full(),
21723 multi_buffer.clone(),
21724 Some(project.clone()),
21725 window,
21726 cx,
21727 )
21728 });
21729
21730 assert_eq!(
21731 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21732 "\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",
21733 );
21734
21735 multi_buffer_editor.update(cx, |editor, cx| {
21736 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
21737 });
21738 assert_eq!(
21739 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21740 "\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",
21741 "After folding the first buffer, its text should not be displayed"
21742 );
21743
21744 multi_buffer_editor.update(cx, |editor, cx| {
21745 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
21746 });
21747 assert_eq!(
21748 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21749 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
21750 "After folding the second buffer, its text should not be displayed"
21751 );
21752
21753 multi_buffer_editor.update(cx, |editor, cx| {
21754 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
21755 });
21756 assert_eq!(
21757 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21758 "\n\n\n\n\n",
21759 "After folding the third buffer, its text should not be displayed"
21760 );
21761
21762 // Emulate selection inside the fold logic, that should work
21763 multi_buffer_editor.update_in(cx, |editor, window, cx| {
21764 editor
21765 .snapshot(window, cx)
21766 .next_line_boundary(Point::new(0, 4));
21767 });
21768
21769 multi_buffer_editor.update(cx, |editor, cx| {
21770 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
21771 });
21772 assert_eq!(
21773 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21774 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
21775 "After unfolding the second buffer, its text should be displayed"
21776 );
21777
21778 // Typing inside of buffer 1 causes that buffer to be unfolded.
21779 multi_buffer_editor.update_in(cx, |editor, window, cx| {
21780 assert_eq!(
21781 multi_buffer
21782 .read(cx)
21783 .snapshot(cx)
21784 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
21785 .collect::<String>(),
21786 "bbbb"
21787 );
21788 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
21789 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
21790 });
21791 editor.handle_input("B", window, cx);
21792 });
21793
21794 assert_eq!(
21795 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21796 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
21797 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
21798 );
21799
21800 multi_buffer_editor.update(cx, |editor, cx| {
21801 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
21802 });
21803 assert_eq!(
21804 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21805 "\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",
21806 "After unfolding the all buffers, all original text should be displayed"
21807 );
21808}
21809
21810#[gpui::test]
21811async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
21812 init_test(cx, |_| {});
21813
21814 let sample_text_1 = "1111\n2222\n3333".to_string();
21815 let sample_text_2 = "4444\n5555\n6666".to_string();
21816 let sample_text_3 = "7777\n8888\n9999".to_string();
21817
21818 let fs = FakeFs::new(cx.executor());
21819 fs.insert_tree(
21820 path!("/a"),
21821 json!({
21822 "first.rs": sample_text_1,
21823 "second.rs": sample_text_2,
21824 "third.rs": sample_text_3,
21825 }),
21826 )
21827 .await;
21828 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21829 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21830 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21831 let worktree = project.update(cx, |project, cx| {
21832 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
21833 assert_eq!(worktrees.len(), 1);
21834 worktrees.pop().unwrap()
21835 });
21836 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
21837
21838 let buffer_1 = project
21839 .update(cx, |project, cx| {
21840 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
21841 })
21842 .await
21843 .unwrap();
21844 let buffer_2 = project
21845 .update(cx, |project, cx| {
21846 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
21847 })
21848 .await
21849 .unwrap();
21850 let buffer_3 = project
21851 .update(cx, |project, cx| {
21852 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
21853 })
21854 .await
21855 .unwrap();
21856
21857 let multi_buffer = cx.new(|cx| {
21858 let mut multi_buffer = MultiBuffer::new(ReadWrite);
21859 multi_buffer.push_excerpts(
21860 buffer_1.clone(),
21861 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
21862 cx,
21863 );
21864 multi_buffer.push_excerpts(
21865 buffer_2.clone(),
21866 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
21867 cx,
21868 );
21869 multi_buffer.push_excerpts(
21870 buffer_3.clone(),
21871 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
21872 cx,
21873 );
21874 multi_buffer
21875 });
21876
21877 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
21878 Editor::new(
21879 EditorMode::full(),
21880 multi_buffer,
21881 Some(project.clone()),
21882 window,
21883 cx,
21884 )
21885 });
21886
21887 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
21888 assert_eq!(
21889 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21890 full_text,
21891 );
21892
21893 multi_buffer_editor.update(cx, |editor, cx| {
21894 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
21895 });
21896 assert_eq!(
21897 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21898 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
21899 "After folding the first buffer, its text should not be displayed"
21900 );
21901
21902 multi_buffer_editor.update(cx, |editor, cx| {
21903 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
21904 });
21905
21906 assert_eq!(
21907 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21908 "\n\n\n\n\n\n7777\n8888\n9999",
21909 "After folding the second buffer, its text should not be displayed"
21910 );
21911
21912 multi_buffer_editor.update(cx, |editor, cx| {
21913 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
21914 });
21915 assert_eq!(
21916 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21917 "\n\n\n\n\n",
21918 "After folding the third buffer, its text should not be displayed"
21919 );
21920
21921 multi_buffer_editor.update(cx, |editor, cx| {
21922 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
21923 });
21924 assert_eq!(
21925 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21926 "\n\n\n\n4444\n5555\n6666\n\n",
21927 "After unfolding the second buffer, its text should be displayed"
21928 );
21929
21930 multi_buffer_editor.update(cx, |editor, cx| {
21931 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
21932 });
21933 assert_eq!(
21934 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21935 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
21936 "After unfolding the first buffer, its text should be displayed"
21937 );
21938
21939 multi_buffer_editor.update(cx, |editor, cx| {
21940 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
21941 });
21942 assert_eq!(
21943 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21944 full_text,
21945 "After unfolding all buffers, all original text should be displayed"
21946 );
21947}
21948
21949#[gpui::test]
21950async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
21951 init_test(cx, |_| {});
21952
21953 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
21954
21955 let fs = FakeFs::new(cx.executor());
21956 fs.insert_tree(
21957 path!("/a"),
21958 json!({
21959 "main.rs": sample_text,
21960 }),
21961 )
21962 .await;
21963 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21964 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21965 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21966 let worktree = project.update(cx, |project, cx| {
21967 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
21968 assert_eq!(worktrees.len(), 1);
21969 worktrees.pop().unwrap()
21970 });
21971 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
21972
21973 let buffer_1 = project
21974 .update(cx, |project, cx| {
21975 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
21976 })
21977 .await
21978 .unwrap();
21979
21980 let multi_buffer = cx.new(|cx| {
21981 let mut multi_buffer = MultiBuffer::new(ReadWrite);
21982 multi_buffer.push_excerpts(
21983 buffer_1.clone(),
21984 [ExcerptRange::new(
21985 Point::new(0, 0)
21986 ..Point::new(
21987 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
21988 0,
21989 ),
21990 )],
21991 cx,
21992 );
21993 multi_buffer
21994 });
21995 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
21996 Editor::new(
21997 EditorMode::full(),
21998 multi_buffer,
21999 Some(project.clone()),
22000 window,
22001 cx,
22002 )
22003 });
22004
22005 let selection_range = Point::new(1, 0)..Point::new(2, 0);
22006 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22007 enum TestHighlight {}
22008 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
22009 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
22010 editor.highlight_text::<TestHighlight>(
22011 vec![highlight_range.clone()],
22012 HighlightStyle::color(Hsla::green()),
22013 cx,
22014 );
22015 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22016 s.select_ranges(Some(highlight_range))
22017 });
22018 });
22019
22020 let full_text = format!("\n\n{sample_text}");
22021 assert_eq!(
22022 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22023 full_text,
22024 );
22025}
22026
22027#[gpui::test]
22028async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
22029 init_test(cx, |_| {});
22030 cx.update(|cx| {
22031 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
22032 "keymaps/default-linux.json",
22033 cx,
22034 )
22035 .unwrap();
22036 cx.bind_keys(default_key_bindings);
22037 });
22038
22039 let (editor, cx) = cx.add_window_view(|window, cx| {
22040 let multi_buffer = MultiBuffer::build_multi(
22041 [
22042 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
22043 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
22044 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
22045 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
22046 ],
22047 cx,
22048 );
22049 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
22050
22051 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
22052 // fold all but the second buffer, so that we test navigating between two
22053 // adjacent folded buffers, as well as folded buffers at the start and
22054 // end the multibuffer
22055 editor.fold_buffer(buffer_ids[0], cx);
22056 editor.fold_buffer(buffer_ids[2], cx);
22057 editor.fold_buffer(buffer_ids[3], cx);
22058
22059 editor
22060 });
22061 cx.simulate_resize(size(px(1000.), px(1000.)));
22062
22063 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
22064 cx.assert_excerpts_with_selections(indoc! {"
22065 [EXCERPT]
22066 ˇ[FOLDED]
22067 [EXCERPT]
22068 a1
22069 b1
22070 [EXCERPT]
22071 [FOLDED]
22072 [EXCERPT]
22073 [FOLDED]
22074 "
22075 });
22076 cx.simulate_keystroke("down");
22077 cx.assert_excerpts_with_selections(indoc! {"
22078 [EXCERPT]
22079 [FOLDED]
22080 [EXCERPT]
22081 ˇa1
22082 b1
22083 [EXCERPT]
22084 [FOLDED]
22085 [EXCERPT]
22086 [FOLDED]
22087 "
22088 });
22089 cx.simulate_keystroke("down");
22090 cx.assert_excerpts_with_selections(indoc! {"
22091 [EXCERPT]
22092 [FOLDED]
22093 [EXCERPT]
22094 a1
22095 ˇb1
22096 [EXCERPT]
22097 [FOLDED]
22098 [EXCERPT]
22099 [FOLDED]
22100 "
22101 });
22102 cx.simulate_keystroke("down");
22103 cx.assert_excerpts_with_selections(indoc! {"
22104 [EXCERPT]
22105 [FOLDED]
22106 [EXCERPT]
22107 a1
22108 b1
22109 ˇ[EXCERPT]
22110 [FOLDED]
22111 [EXCERPT]
22112 [FOLDED]
22113 "
22114 });
22115 cx.simulate_keystroke("down");
22116 cx.assert_excerpts_with_selections(indoc! {"
22117 [EXCERPT]
22118 [FOLDED]
22119 [EXCERPT]
22120 a1
22121 b1
22122 [EXCERPT]
22123 ˇ[FOLDED]
22124 [EXCERPT]
22125 [FOLDED]
22126 "
22127 });
22128 for _ in 0..5 {
22129 cx.simulate_keystroke("down");
22130 cx.assert_excerpts_with_selections(indoc! {"
22131 [EXCERPT]
22132 [FOLDED]
22133 [EXCERPT]
22134 a1
22135 b1
22136 [EXCERPT]
22137 [FOLDED]
22138 [EXCERPT]
22139 ˇ[FOLDED]
22140 "
22141 });
22142 }
22143
22144 cx.simulate_keystroke("up");
22145 cx.assert_excerpts_with_selections(indoc! {"
22146 [EXCERPT]
22147 [FOLDED]
22148 [EXCERPT]
22149 a1
22150 b1
22151 [EXCERPT]
22152 ˇ[FOLDED]
22153 [EXCERPT]
22154 [FOLDED]
22155 "
22156 });
22157 cx.simulate_keystroke("up");
22158 cx.assert_excerpts_with_selections(indoc! {"
22159 [EXCERPT]
22160 [FOLDED]
22161 [EXCERPT]
22162 a1
22163 b1
22164 ˇ[EXCERPT]
22165 [FOLDED]
22166 [EXCERPT]
22167 [FOLDED]
22168 "
22169 });
22170 cx.simulate_keystroke("up");
22171 cx.assert_excerpts_with_selections(indoc! {"
22172 [EXCERPT]
22173 [FOLDED]
22174 [EXCERPT]
22175 a1
22176 ˇb1
22177 [EXCERPT]
22178 [FOLDED]
22179 [EXCERPT]
22180 [FOLDED]
22181 "
22182 });
22183 cx.simulate_keystroke("up");
22184 cx.assert_excerpts_with_selections(indoc! {"
22185 [EXCERPT]
22186 [FOLDED]
22187 [EXCERPT]
22188 ˇa1
22189 b1
22190 [EXCERPT]
22191 [FOLDED]
22192 [EXCERPT]
22193 [FOLDED]
22194 "
22195 });
22196 for _ in 0..5 {
22197 cx.simulate_keystroke("up");
22198 cx.assert_excerpts_with_selections(indoc! {"
22199 [EXCERPT]
22200 ˇ[FOLDED]
22201 [EXCERPT]
22202 a1
22203 b1
22204 [EXCERPT]
22205 [FOLDED]
22206 [EXCERPT]
22207 [FOLDED]
22208 "
22209 });
22210 }
22211}
22212
22213#[gpui::test]
22214async fn test_edit_prediction_text(cx: &mut TestAppContext) {
22215 init_test(cx, |_| {});
22216
22217 // Simple insertion
22218 assert_highlighted_edits(
22219 "Hello, world!",
22220 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
22221 true,
22222 cx,
22223 |highlighted_edits, cx| {
22224 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
22225 assert_eq!(highlighted_edits.highlights.len(), 1);
22226 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
22227 assert_eq!(
22228 highlighted_edits.highlights[0].1.background_color,
22229 Some(cx.theme().status().created_background)
22230 );
22231 },
22232 )
22233 .await;
22234
22235 // Replacement
22236 assert_highlighted_edits(
22237 "This is a test.",
22238 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
22239 false,
22240 cx,
22241 |highlighted_edits, cx| {
22242 assert_eq!(highlighted_edits.text, "That is a test.");
22243 assert_eq!(highlighted_edits.highlights.len(), 1);
22244 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
22245 assert_eq!(
22246 highlighted_edits.highlights[0].1.background_color,
22247 Some(cx.theme().status().created_background)
22248 );
22249 },
22250 )
22251 .await;
22252
22253 // Multiple edits
22254 assert_highlighted_edits(
22255 "Hello, world!",
22256 vec![
22257 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
22258 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
22259 ],
22260 false,
22261 cx,
22262 |highlighted_edits, cx| {
22263 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
22264 assert_eq!(highlighted_edits.highlights.len(), 2);
22265 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
22266 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
22267 assert_eq!(
22268 highlighted_edits.highlights[0].1.background_color,
22269 Some(cx.theme().status().created_background)
22270 );
22271 assert_eq!(
22272 highlighted_edits.highlights[1].1.background_color,
22273 Some(cx.theme().status().created_background)
22274 );
22275 },
22276 )
22277 .await;
22278
22279 // Multiple lines with edits
22280 assert_highlighted_edits(
22281 "First line\nSecond line\nThird line\nFourth line",
22282 vec![
22283 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
22284 (
22285 Point::new(2, 0)..Point::new(2, 10),
22286 "New third line".to_string(),
22287 ),
22288 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
22289 ],
22290 false,
22291 cx,
22292 |highlighted_edits, cx| {
22293 assert_eq!(
22294 highlighted_edits.text,
22295 "Second modified\nNew third line\nFourth updated line"
22296 );
22297 assert_eq!(highlighted_edits.highlights.len(), 3);
22298 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
22299 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
22300 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
22301 for highlight in &highlighted_edits.highlights {
22302 assert_eq!(
22303 highlight.1.background_color,
22304 Some(cx.theme().status().created_background)
22305 );
22306 }
22307 },
22308 )
22309 .await;
22310}
22311
22312#[gpui::test]
22313async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
22314 init_test(cx, |_| {});
22315
22316 // Deletion
22317 assert_highlighted_edits(
22318 "Hello, world!",
22319 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
22320 true,
22321 cx,
22322 |highlighted_edits, cx| {
22323 assert_eq!(highlighted_edits.text, "Hello, world!");
22324 assert_eq!(highlighted_edits.highlights.len(), 1);
22325 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
22326 assert_eq!(
22327 highlighted_edits.highlights[0].1.background_color,
22328 Some(cx.theme().status().deleted_background)
22329 );
22330 },
22331 )
22332 .await;
22333
22334 // Insertion
22335 assert_highlighted_edits(
22336 "Hello, world!",
22337 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
22338 true,
22339 cx,
22340 |highlighted_edits, cx| {
22341 assert_eq!(highlighted_edits.highlights.len(), 1);
22342 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
22343 assert_eq!(
22344 highlighted_edits.highlights[0].1.background_color,
22345 Some(cx.theme().status().created_background)
22346 );
22347 },
22348 )
22349 .await;
22350}
22351
22352async fn assert_highlighted_edits(
22353 text: &str,
22354 edits: Vec<(Range<Point>, String)>,
22355 include_deletions: bool,
22356 cx: &mut TestAppContext,
22357 assertion_fn: impl Fn(HighlightedText, &App),
22358) {
22359 let window = cx.add_window(|window, cx| {
22360 let buffer = MultiBuffer::build_simple(text, cx);
22361 Editor::new(EditorMode::full(), buffer, None, window, cx)
22362 });
22363 let cx = &mut VisualTestContext::from_window(*window, cx);
22364
22365 let (buffer, snapshot) = window
22366 .update(cx, |editor, _window, cx| {
22367 (
22368 editor.buffer().clone(),
22369 editor.buffer().read(cx).snapshot(cx),
22370 )
22371 })
22372 .unwrap();
22373
22374 let edits = edits
22375 .into_iter()
22376 .map(|(range, edit)| {
22377 (
22378 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
22379 edit,
22380 )
22381 })
22382 .collect::<Vec<_>>();
22383
22384 let text_anchor_edits = edits
22385 .clone()
22386 .into_iter()
22387 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
22388 .collect::<Vec<_>>();
22389
22390 let edit_preview = window
22391 .update(cx, |_, _window, cx| {
22392 buffer
22393 .read(cx)
22394 .as_singleton()
22395 .unwrap()
22396 .read(cx)
22397 .preview_edits(text_anchor_edits.into(), cx)
22398 })
22399 .unwrap()
22400 .await;
22401
22402 cx.update(|_window, cx| {
22403 let highlighted_edits = edit_prediction_edit_text(
22404 snapshot.as_singleton().unwrap().2,
22405 &edits,
22406 &edit_preview,
22407 include_deletions,
22408 cx,
22409 );
22410 assertion_fn(highlighted_edits, cx)
22411 });
22412}
22413
22414#[track_caller]
22415fn assert_breakpoint(
22416 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
22417 path: &Arc<Path>,
22418 expected: Vec<(u32, Breakpoint)>,
22419) {
22420 if expected.is_empty() {
22421 assert!(!breakpoints.contains_key(path), "{}", path.display());
22422 } else {
22423 let mut breakpoint = breakpoints
22424 .get(path)
22425 .unwrap()
22426 .iter()
22427 .map(|breakpoint| {
22428 (
22429 breakpoint.row,
22430 Breakpoint {
22431 message: breakpoint.message.clone(),
22432 state: breakpoint.state,
22433 condition: breakpoint.condition.clone(),
22434 hit_condition: breakpoint.hit_condition.clone(),
22435 },
22436 )
22437 })
22438 .collect::<Vec<_>>();
22439
22440 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
22441
22442 assert_eq!(expected, breakpoint);
22443 }
22444}
22445
22446fn add_log_breakpoint_at_cursor(
22447 editor: &mut Editor,
22448 log_message: &str,
22449 window: &mut Window,
22450 cx: &mut Context<Editor>,
22451) {
22452 let (anchor, bp) = editor
22453 .breakpoints_at_cursors(window, cx)
22454 .first()
22455 .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
22456 .unwrap_or_else(|| {
22457 let cursor_position: Point = editor.selections.newest(cx).head();
22458
22459 let breakpoint_position = editor
22460 .snapshot(window, cx)
22461 .display_snapshot
22462 .buffer_snapshot
22463 .anchor_before(Point::new(cursor_position.row, 0));
22464
22465 (breakpoint_position, Breakpoint::new_log(log_message))
22466 });
22467
22468 editor.edit_breakpoint_at_anchor(
22469 anchor,
22470 bp,
22471 BreakpointEditAction::EditLogMessage(log_message.into()),
22472 cx,
22473 );
22474}
22475
22476#[gpui::test]
22477async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
22478 init_test(cx, |_| {});
22479
22480 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22481 let fs = FakeFs::new(cx.executor());
22482 fs.insert_tree(
22483 path!("/a"),
22484 json!({
22485 "main.rs": sample_text,
22486 }),
22487 )
22488 .await;
22489 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22490 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22491 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22492
22493 let fs = FakeFs::new(cx.executor());
22494 fs.insert_tree(
22495 path!("/a"),
22496 json!({
22497 "main.rs": sample_text,
22498 }),
22499 )
22500 .await;
22501 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22502 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22503 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22504 let worktree_id = workspace
22505 .update(cx, |workspace, _window, cx| {
22506 workspace.project().update(cx, |project, cx| {
22507 project.worktrees(cx).next().unwrap().read(cx).id()
22508 })
22509 })
22510 .unwrap();
22511
22512 let buffer = project
22513 .update(cx, |project, cx| {
22514 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22515 })
22516 .await
22517 .unwrap();
22518
22519 let (editor, cx) = cx.add_window_view(|window, cx| {
22520 Editor::new(
22521 EditorMode::full(),
22522 MultiBuffer::build_from_buffer(buffer, cx),
22523 Some(project.clone()),
22524 window,
22525 cx,
22526 )
22527 });
22528
22529 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22530 let abs_path = project.read_with(cx, |project, cx| {
22531 project
22532 .absolute_path(&project_path, cx)
22533 .map(Arc::from)
22534 .unwrap()
22535 });
22536
22537 // assert we can add breakpoint on the first line
22538 editor.update_in(cx, |editor, window, cx| {
22539 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22540 editor.move_to_end(&MoveToEnd, window, cx);
22541 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22542 });
22543
22544 let breakpoints = editor.update(cx, |editor, cx| {
22545 editor
22546 .breakpoint_store()
22547 .as_ref()
22548 .unwrap()
22549 .read(cx)
22550 .all_source_breakpoints(cx)
22551 });
22552
22553 assert_eq!(1, breakpoints.len());
22554 assert_breakpoint(
22555 &breakpoints,
22556 &abs_path,
22557 vec![
22558 (0, Breakpoint::new_standard()),
22559 (3, Breakpoint::new_standard()),
22560 ],
22561 );
22562
22563 editor.update_in(cx, |editor, window, cx| {
22564 editor.move_to_beginning(&MoveToBeginning, window, cx);
22565 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22566 });
22567
22568 let breakpoints = editor.update(cx, |editor, cx| {
22569 editor
22570 .breakpoint_store()
22571 .as_ref()
22572 .unwrap()
22573 .read(cx)
22574 .all_source_breakpoints(cx)
22575 });
22576
22577 assert_eq!(1, breakpoints.len());
22578 assert_breakpoint(
22579 &breakpoints,
22580 &abs_path,
22581 vec![(3, Breakpoint::new_standard())],
22582 );
22583
22584 editor.update_in(cx, |editor, window, cx| {
22585 editor.move_to_end(&MoveToEnd, window, cx);
22586 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22587 });
22588
22589 let breakpoints = editor.update(cx, |editor, cx| {
22590 editor
22591 .breakpoint_store()
22592 .as_ref()
22593 .unwrap()
22594 .read(cx)
22595 .all_source_breakpoints(cx)
22596 });
22597
22598 assert_eq!(0, breakpoints.len());
22599 assert_breakpoint(&breakpoints, &abs_path, vec![]);
22600}
22601
22602#[gpui::test]
22603async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
22604 init_test(cx, |_| {});
22605
22606 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22607
22608 let fs = FakeFs::new(cx.executor());
22609 fs.insert_tree(
22610 path!("/a"),
22611 json!({
22612 "main.rs": sample_text,
22613 }),
22614 )
22615 .await;
22616 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22617 let (workspace, cx) =
22618 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
22619
22620 let worktree_id = workspace.update(cx, |workspace, cx| {
22621 workspace.project().update(cx, |project, cx| {
22622 project.worktrees(cx).next().unwrap().read(cx).id()
22623 })
22624 });
22625
22626 let buffer = project
22627 .update(cx, |project, cx| {
22628 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22629 })
22630 .await
22631 .unwrap();
22632
22633 let (editor, cx) = cx.add_window_view(|window, cx| {
22634 Editor::new(
22635 EditorMode::full(),
22636 MultiBuffer::build_from_buffer(buffer, cx),
22637 Some(project.clone()),
22638 window,
22639 cx,
22640 )
22641 });
22642
22643 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22644 let abs_path = project.read_with(cx, |project, cx| {
22645 project
22646 .absolute_path(&project_path, cx)
22647 .map(Arc::from)
22648 .unwrap()
22649 });
22650
22651 editor.update_in(cx, |editor, window, cx| {
22652 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
22653 });
22654
22655 let breakpoints = editor.update(cx, |editor, cx| {
22656 editor
22657 .breakpoint_store()
22658 .as_ref()
22659 .unwrap()
22660 .read(cx)
22661 .all_source_breakpoints(cx)
22662 });
22663
22664 assert_breakpoint(
22665 &breakpoints,
22666 &abs_path,
22667 vec![(0, Breakpoint::new_log("hello world"))],
22668 );
22669
22670 // Removing a log message from a log breakpoint should remove it
22671 editor.update_in(cx, |editor, window, cx| {
22672 add_log_breakpoint_at_cursor(editor, "", window, cx);
22673 });
22674
22675 let breakpoints = editor.update(cx, |editor, cx| {
22676 editor
22677 .breakpoint_store()
22678 .as_ref()
22679 .unwrap()
22680 .read(cx)
22681 .all_source_breakpoints(cx)
22682 });
22683
22684 assert_breakpoint(&breakpoints, &abs_path, vec![]);
22685
22686 editor.update_in(cx, |editor, window, cx| {
22687 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22688 editor.move_to_end(&MoveToEnd, window, cx);
22689 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22690 // Not adding a log message to a standard breakpoint shouldn't remove it
22691 add_log_breakpoint_at_cursor(editor, "", window, cx);
22692 });
22693
22694 let breakpoints = editor.update(cx, |editor, cx| {
22695 editor
22696 .breakpoint_store()
22697 .as_ref()
22698 .unwrap()
22699 .read(cx)
22700 .all_source_breakpoints(cx)
22701 });
22702
22703 assert_breakpoint(
22704 &breakpoints,
22705 &abs_path,
22706 vec![
22707 (0, Breakpoint::new_standard()),
22708 (3, Breakpoint::new_standard()),
22709 ],
22710 );
22711
22712 editor.update_in(cx, |editor, window, cx| {
22713 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
22714 });
22715
22716 let breakpoints = editor.update(cx, |editor, cx| {
22717 editor
22718 .breakpoint_store()
22719 .as_ref()
22720 .unwrap()
22721 .read(cx)
22722 .all_source_breakpoints(cx)
22723 });
22724
22725 assert_breakpoint(
22726 &breakpoints,
22727 &abs_path,
22728 vec![
22729 (0, Breakpoint::new_standard()),
22730 (3, Breakpoint::new_log("hello world")),
22731 ],
22732 );
22733
22734 editor.update_in(cx, |editor, window, cx| {
22735 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
22736 });
22737
22738 let breakpoints = editor.update(cx, |editor, cx| {
22739 editor
22740 .breakpoint_store()
22741 .as_ref()
22742 .unwrap()
22743 .read(cx)
22744 .all_source_breakpoints(cx)
22745 });
22746
22747 assert_breakpoint(
22748 &breakpoints,
22749 &abs_path,
22750 vec![
22751 (0, Breakpoint::new_standard()),
22752 (3, Breakpoint::new_log("hello Earth!!")),
22753 ],
22754 );
22755}
22756
22757/// This also tests that Editor::breakpoint_at_cursor_head is working properly
22758/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
22759/// or when breakpoints were placed out of order. This tests for a regression too
22760#[gpui::test]
22761async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
22762 init_test(cx, |_| {});
22763
22764 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22765 let fs = FakeFs::new(cx.executor());
22766 fs.insert_tree(
22767 path!("/a"),
22768 json!({
22769 "main.rs": sample_text,
22770 }),
22771 )
22772 .await;
22773 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22774 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22775 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22776
22777 let fs = FakeFs::new(cx.executor());
22778 fs.insert_tree(
22779 path!("/a"),
22780 json!({
22781 "main.rs": sample_text,
22782 }),
22783 )
22784 .await;
22785 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22786 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22787 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22788 let worktree_id = workspace
22789 .update(cx, |workspace, _window, cx| {
22790 workspace.project().update(cx, |project, cx| {
22791 project.worktrees(cx).next().unwrap().read(cx).id()
22792 })
22793 })
22794 .unwrap();
22795
22796 let buffer = project
22797 .update(cx, |project, cx| {
22798 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22799 })
22800 .await
22801 .unwrap();
22802
22803 let (editor, cx) = cx.add_window_view(|window, cx| {
22804 Editor::new(
22805 EditorMode::full(),
22806 MultiBuffer::build_from_buffer(buffer, cx),
22807 Some(project.clone()),
22808 window,
22809 cx,
22810 )
22811 });
22812
22813 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22814 let abs_path = project.read_with(cx, |project, cx| {
22815 project
22816 .absolute_path(&project_path, cx)
22817 .map(Arc::from)
22818 .unwrap()
22819 });
22820
22821 // assert we can add breakpoint on the first line
22822 editor.update_in(cx, |editor, window, cx| {
22823 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22824 editor.move_to_end(&MoveToEnd, window, cx);
22825 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22826 editor.move_up(&MoveUp, window, cx);
22827 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22828 });
22829
22830 let breakpoints = editor.update(cx, |editor, cx| {
22831 editor
22832 .breakpoint_store()
22833 .as_ref()
22834 .unwrap()
22835 .read(cx)
22836 .all_source_breakpoints(cx)
22837 });
22838
22839 assert_eq!(1, breakpoints.len());
22840 assert_breakpoint(
22841 &breakpoints,
22842 &abs_path,
22843 vec![
22844 (0, Breakpoint::new_standard()),
22845 (2, Breakpoint::new_standard()),
22846 (3, Breakpoint::new_standard()),
22847 ],
22848 );
22849
22850 editor.update_in(cx, |editor, window, cx| {
22851 editor.move_to_beginning(&MoveToBeginning, window, cx);
22852 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22853 editor.move_to_end(&MoveToEnd, window, cx);
22854 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22855 // Disabling a breakpoint that doesn't exist should do nothing
22856 editor.move_up(&MoveUp, window, cx);
22857 editor.move_up(&MoveUp, window, cx);
22858 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22859 });
22860
22861 let breakpoints = editor.update(cx, |editor, cx| {
22862 editor
22863 .breakpoint_store()
22864 .as_ref()
22865 .unwrap()
22866 .read(cx)
22867 .all_source_breakpoints(cx)
22868 });
22869
22870 let disable_breakpoint = {
22871 let mut bp = Breakpoint::new_standard();
22872 bp.state = BreakpointState::Disabled;
22873 bp
22874 };
22875
22876 assert_eq!(1, breakpoints.len());
22877 assert_breakpoint(
22878 &breakpoints,
22879 &abs_path,
22880 vec![
22881 (0, disable_breakpoint.clone()),
22882 (2, Breakpoint::new_standard()),
22883 (3, disable_breakpoint.clone()),
22884 ],
22885 );
22886
22887 editor.update_in(cx, |editor, window, cx| {
22888 editor.move_to_beginning(&MoveToBeginning, window, cx);
22889 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
22890 editor.move_to_end(&MoveToEnd, window, cx);
22891 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
22892 editor.move_up(&MoveUp, window, cx);
22893 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22894 });
22895
22896 let breakpoints = editor.update(cx, |editor, cx| {
22897 editor
22898 .breakpoint_store()
22899 .as_ref()
22900 .unwrap()
22901 .read(cx)
22902 .all_source_breakpoints(cx)
22903 });
22904
22905 assert_eq!(1, breakpoints.len());
22906 assert_breakpoint(
22907 &breakpoints,
22908 &abs_path,
22909 vec![
22910 (0, Breakpoint::new_standard()),
22911 (2, disable_breakpoint),
22912 (3, Breakpoint::new_standard()),
22913 ],
22914 );
22915}
22916
22917#[gpui::test]
22918async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
22919 init_test(cx, |_| {});
22920 let capabilities = lsp::ServerCapabilities {
22921 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
22922 prepare_provider: Some(true),
22923 work_done_progress_options: Default::default(),
22924 })),
22925 ..Default::default()
22926 };
22927 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
22928
22929 cx.set_state(indoc! {"
22930 struct Fˇoo {}
22931 "});
22932
22933 cx.update_editor(|editor, _, cx| {
22934 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
22935 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
22936 editor.highlight_background::<DocumentHighlightRead>(
22937 &[highlight_range],
22938 |theme| theme.colors().editor_document_highlight_read_background,
22939 cx,
22940 );
22941 });
22942
22943 let mut prepare_rename_handler = cx
22944 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
22945 move |_, _, _| async move {
22946 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
22947 start: lsp::Position {
22948 line: 0,
22949 character: 7,
22950 },
22951 end: lsp::Position {
22952 line: 0,
22953 character: 10,
22954 },
22955 })))
22956 },
22957 );
22958 let prepare_rename_task = cx
22959 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
22960 .expect("Prepare rename was not started");
22961 prepare_rename_handler.next().await.unwrap();
22962 prepare_rename_task.await.expect("Prepare rename failed");
22963
22964 let mut rename_handler =
22965 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
22966 let edit = lsp::TextEdit {
22967 range: lsp::Range {
22968 start: lsp::Position {
22969 line: 0,
22970 character: 7,
22971 },
22972 end: lsp::Position {
22973 line: 0,
22974 character: 10,
22975 },
22976 },
22977 new_text: "FooRenamed".to_string(),
22978 };
22979 Ok(Some(lsp::WorkspaceEdit::new(
22980 // Specify the same edit twice
22981 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
22982 )))
22983 });
22984 let rename_task = cx
22985 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
22986 .expect("Confirm rename was not started");
22987 rename_handler.next().await.unwrap();
22988 rename_task.await.expect("Confirm rename failed");
22989 cx.run_until_parked();
22990
22991 // Despite two edits, only one is actually applied as those are identical
22992 cx.assert_editor_state(indoc! {"
22993 struct FooRenamedˇ {}
22994 "});
22995}
22996
22997#[gpui::test]
22998async fn test_rename_without_prepare(cx: &mut TestAppContext) {
22999 init_test(cx, |_| {});
23000 // These capabilities indicate that the server does not support prepare rename.
23001 let capabilities = lsp::ServerCapabilities {
23002 rename_provider: Some(lsp::OneOf::Left(true)),
23003 ..Default::default()
23004 };
23005 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
23006
23007 cx.set_state(indoc! {"
23008 struct Fˇoo {}
23009 "});
23010
23011 cx.update_editor(|editor, _window, cx| {
23012 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
23013 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
23014 editor.highlight_background::<DocumentHighlightRead>(
23015 &[highlight_range],
23016 |theme| theme.colors().editor_document_highlight_read_background,
23017 cx,
23018 );
23019 });
23020
23021 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23022 .expect("Prepare rename was not started")
23023 .await
23024 .expect("Prepare rename failed");
23025
23026 let mut rename_handler =
23027 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23028 let edit = lsp::TextEdit {
23029 range: lsp::Range {
23030 start: lsp::Position {
23031 line: 0,
23032 character: 7,
23033 },
23034 end: lsp::Position {
23035 line: 0,
23036 character: 10,
23037 },
23038 },
23039 new_text: "FooRenamed".to_string(),
23040 };
23041 Ok(Some(lsp::WorkspaceEdit::new(
23042 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
23043 )))
23044 });
23045 let rename_task = cx
23046 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23047 .expect("Confirm rename was not started");
23048 rename_handler.next().await.unwrap();
23049 rename_task.await.expect("Confirm rename failed");
23050 cx.run_until_parked();
23051
23052 // Correct range is renamed, as `surrounding_word` is used to find it.
23053 cx.assert_editor_state(indoc! {"
23054 struct FooRenamedˇ {}
23055 "});
23056}
23057
23058#[gpui::test]
23059async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
23060 init_test(cx, |_| {});
23061 let mut cx = EditorTestContext::new(cx).await;
23062
23063 let language = Arc::new(
23064 Language::new(
23065 LanguageConfig::default(),
23066 Some(tree_sitter_html::LANGUAGE.into()),
23067 )
23068 .with_brackets_query(
23069 r#"
23070 ("<" @open "/>" @close)
23071 ("</" @open ">" @close)
23072 ("<" @open ">" @close)
23073 ("\"" @open "\"" @close)
23074 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
23075 "#,
23076 )
23077 .unwrap(),
23078 );
23079 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23080
23081 cx.set_state(indoc! {"
23082 <span>ˇ</span>
23083 "});
23084 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23085 cx.assert_editor_state(indoc! {"
23086 <span>
23087 ˇ
23088 </span>
23089 "});
23090
23091 cx.set_state(indoc! {"
23092 <span><span></span>ˇ</span>
23093 "});
23094 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23095 cx.assert_editor_state(indoc! {"
23096 <span><span></span>
23097 ˇ</span>
23098 "});
23099
23100 cx.set_state(indoc! {"
23101 <span>ˇ
23102 </span>
23103 "});
23104 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23105 cx.assert_editor_state(indoc! {"
23106 <span>
23107 ˇ
23108 </span>
23109 "});
23110}
23111
23112#[gpui::test(iterations = 10)]
23113async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
23114 init_test(cx, |_| {});
23115
23116 let fs = FakeFs::new(cx.executor());
23117 fs.insert_tree(
23118 path!("/dir"),
23119 json!({
23120 "a.ts": "a",
23121 }),
23122 )
23123 .await;
23124
23125 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
23126 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23127 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23128
23129 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23130 language_registry.add(Arc::new(Language::new(
23131 LanguageConfig {
23132 name: "TypeScript".into(),
23133 matcher: LanguageMatcher {
23134 path_suffixes: vec!["ts".to_string()],
23135 ..Default::default()
23136 },
23137 ..Default::default()
23138 },
23139 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
23140 )));
23141 let mut fake_language_servers = language_registry.register_fake_lsp(
23142 "TypeScript",
23143 FakeLspAdapter {
23144 capabilities: lsp::ServerCapabilities {
23145 code_lens_provider: Some(lsp::CodeLensOptions {
23146 resolve_provider: Some(true),
23147 }),
23148 execute_command_provider: Some(lsp::ExecuteCommandOptions {
23149 commands: vec!["_the/command".to_string()],
23150 ..lsp::ExecuteCommandOptions::default()
23151 }),
23152 ..lsp::ServerCapabilities::default()
23153 },
23154 ..FakeLspAdapter::default()
23155 },
23156 );
23157
23158 let editor = workspace
23159 .update(cx, |workspace, window, cx| {
23160 workspace.open_abs_path(
23161 PathBuf::from(path!("/dir/a.ts")),
23162 OpenOptions::default(),
23163 window,
23164 cx,
23165 )
23166 })
23167 .unwrap()
23168 .await
23169 .unwrap()
23170 .downcast::<Editor>()
23171 .unwrap();
23172 cx.executor().run_until_parked();
23173
23174 let fake_server = fake_language_servers.next().await.unwrap();
23175
23176 let buffer = editor.update(cx, |editor, cx| {
23177 editor
23178 .buffer()
23179 .read(cx)
23180 .as_singleton()
23181 .expect("have opened a single file by path")
23182 });
23183
23184 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
23185 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
23186 drop(buffer_snapshot);
23187 let actions = cx
23188 .update_window(*workspace, |_, window, cx| {
23189 project.code_actions(&buffer, anchor..anchor, window, cx)
23190 })
23191 .unwrap();
23192
23193 fake_server
23194 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23195 Ok(Some(vec![
23196 lsp::CodeLens {
23197 range: lsp::Range::default(),
23198 command: Some(lsp::Command {
23199 title: "Code lens command".to_owned(),
23200 command: "_the/command".to_owned(),
23201 arguments: None,
23202 }),
23203 data: None,
23204 },
23205 lsp::CodeLens {
23206 range: lsp::Range::default(),
23207 command: Some(lsp::Command {
23208 title: "Command not in capabilities".to_owned(),
23209 command: "not in capabilities".to_owned(),
23210 arguments: None,
23211 }),
23212 data: None,
23213 },
23214 lsp::CodeLens {
23215 range: lsp::Range {
23216 start: lsp::Position {
23217 line: 1,
23218 character: 1,
23219 },
23220 end: lsp::Position {
23221 line: 1,
23222 character: 1,
23223 },
23224 },
23225 command: Some(lsp::Command {
23226 title: "Command not in range".to_owned(),
23227 command: "_the/command".to_owned(),
23228 arguments: None,
23229 }),
23230 data: None,
23231 },
23232 ]))
23233 })
23234 .next()
23235 .await;
23236
23237 let actions = actions.await.unwrap();
23238 assert_eq!(
23239 actions.len(),
23240 1,
23241 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
23242 );
23243 let action = actions[0].clone();
23244 let apply = project.update(cx, |project, cx| {
23245 project.apply_code_action(buffer.clone(), action, true, cx)
23246 });
23247
23248 // Resolving the code action does not populate its edits. In absence of
23249 // edits, we must execute the given command.
23250 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
23251 |mut lens, _| async move {
23252 let lens_command = lens.command.as_mut().expect("should have a command");
23253 assert_eq!(lens_command.title, "Code lens command");
23254 lens_command.arguments = Some(vec![json!("the-argument")]);
23255 Ok(lens)
23256 },
23257 );
23258
23259 // While executing the command, the language server sends the editor
23260 // a `workspaceEdit` request.
23261 fake_server
23262 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
23263 let fake = fake_server.clone();
23264 move |params, _| {
23265 assert_eq!(params.command, "_the/command");
23266 let fake = fake.clone();
23267 async move {
23268 fake.server
23269 .request::<lsp::request::ApplyWorkspaceEdit>(
23270 lsp::ApplyWorkspaceEditParams {
23271 label: None,
23272 edit: lsp::WorkspaceEdit {
23273 changes: Some(
23274 [(
23275 lsp::Uri::from_file_path(path!("/dir/a.ts")).unwrap(),
23276 vec![lsp::TextEdit {
23277 range: lsp::Range::new(
23278 lsp::Position::new(0, 0),
23279 lsp::Position::new(0, 0),
23280 ),
23281 new_text: "X".into(),
23282 }],
23283 )]
23284 .into_iter()
23285 .collect(),
23286 ),
23287 ..lsp::WorkspaceEdit::default()
23288 },
23289 },
23290 )
23291 .await
23292 .into_response()
23293 .unwrap();
23294 Ok(Some(json!(null)))
23295 }
23296 }
23297 })
23298 .next()
23299 .await;
23300
23301 // Applying the code lens command returns a project transaction containing the edits
23302 // sent by the language server in its `workspaceEdit` request.
23303 let transaction = apply.await.unwrap();
23304 assert!(transaction.0.contains_key(&buffer));
23305 buffer.update(cx, |buffer, cx| {
23306 assert_eq!(buffer.text(), "Xa");
23307 buffer.undo(cx);
23308 assert_eq!(buffer.text(), "a");
23309 });
23310
23311 let actions_after_edits = cx
23312 .update_window(*workspace, |_, window, cx| {
23313 project.code_actions(&buffer, anchor..anchor, window, cx)
23314 })
23315 .unwrap()
23316 .await
23317 .unwrap();
23318 assert_eq!(
23319 actions, actions_after_edits,
23320 "For the same selection, same code lens actions should be returned"
23321 );
23322
23323 let _responses =
23324 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23325 panic!("No more code lens requests are expected");
23326 });
23327 editor.update_in(cx, |editor, window, cx| {
23328 editor.select_all(&SelectAll, window, cx);
23329 });
23330 cx.executor().run_until_parked();
23331 let new_actions = cx
23332 .update_window(*workspace, |_, window, cx| {
23333 project.code_actions(&buffer, anchor..anchor, window, cx)
23334 })
23335 .unwrap()
23336 .await
23337 .unwrap();
23338 assert_eq!(
23339 actions, new_actions,
23340 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
23341 );
23342}
23343
23344#[gpui::test]
23345async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
23346 init_test(cx, |_| {});
23347
23348 let fs = FakeFs::new(cx.executor());
23349 let main_text = r#"fn main() {
23350println!("1");
23351println!("2");
23352println!("3");
23353println!("4");
23354println!("5");
23355}"#;
23356 let lib_text = "mod foo {}";
23357 fs.insert_tree(
23358 path!("/a"),
23359 json!({
23360 "lib.rs": lib_text,
23361 "main.rs": main_text,
23362 }),
23363 )
23364 .await;
23365
23366 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23367 let (workspace, cx) =
23368 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23369 let worktree_id = workspace.update(cx, |workspace, cx| {
23370 workspace.project().update(cx, |project, cx| {
23371 project.worktrees(cx).next().unwrap().read(cx).id()
23372 })
23373 });
23374
23375 let expected_ranges = vec![
23376 Point::new(0, 0)..Point::new(0, 0),
23377 Point::new(1, 0)..Point::new(1, 1),
23378 Point::new(2, 0)..Point::new(2, 2),
23379 Point::new(3, 0)..Point::new(3, 3),
23380 ];
23381
23382 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23383 let editor_1 = workspace
23384 .update_in(cx, |workspace, window, cx| {
23385 workspace.open_path(
23386 (worktree_id, rel_path("main.rs")),
23387 Some(pane_1.downgrade()),
23388 true,
23389 window,
23390 cx,
23391 )
23392 })
23393 .unwrap()
23394 .await
23395 .downcast::<Editor>()
23396 .unwrap();
23397 pane_1.update(cx, |pane, cx| {
23398 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23399 open_editor.update(cx, |editor, cx| {
23400 assert_eq!(
23401 editor.display_text(cx),
23402 main_text,
23403 "Original main.rs text on initial open",
23404 );
23405 assert_eq!(
23406 editor
23407 .selections
23408 .all::<Point>(cx)
23409 .into_iter()
23410 .map(|s| s.range())
23411 .collect::<Vec<_>>(),
23412 vec![Point::zero()..Point::zero()],
23413 "Default selections on initial open",
23414 );
23415 })
23416 });
23417 editor_1.update_in(cx, |editor, window, cx| {
23418 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23419 s.select_ranges(expected_ranges.clone());
23420 });
23421 });
23422
23423 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
23424 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
23425 });
23426 let editor_2 = workspace
23427 .update_in(cx, |workspace, window, cx| {
23428 workspace.open_path(
23429 (worktree_id, rel_path("main.rs")),
23430 Some(pane_2.downgrade()),
23431 true,
23432 window,
23433 cx,
23434 )
23435 })
23436 .unwrap()
23437 .await
23438 .downcast::<Editor>()
23439 .unwrap();
23440 pane_2.update(cx, |pane, cx| {
23441 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23442 open_editor.update(cx, |editor, cx| {
23443 assert_eq!(
23444 editor.display_text(cx),
23445 main_text,
23446 "Original main.rs text on initial open in another panel",
23447 );
23448 assert_eq!(
23449 editor
23450 .selections
23451 .all::<Point>(cx)
23452 .into_iter()
23453 .map(|s| s.range())
23454 .collect::<Vec<_>>(),
23455 vec![Point::zero()..Point::zero()],
23456 "Default selections on initial open in another panel",
23457 );
23458 })
23459 });
23460
23461 editor_2.update_in(cx, |editor, window, cx| {
23462 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
23463 });
23464
23465 let _other_editor_1 = workspace
23466 .update_in(cx, |workspace, window, cx| {
23467 workspace.open_path(
23468 (worktree_id, rel_path("lib.rs")),
23469 Some(pane_1.downgrade()),
23470 true,
23471 window,
23472 cx,
23473 )
23474 })
23475 .unwrap()
23476 .await
23477 .downcast::<Editor>()
23478 .unwrap();
23479 pane_1
23480 .update_in(cx, |pane, window, cx| {
23481 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23482 })
23483 .await
23484 .unwrap();
23485 drop(editor_1);
23486 pane_1.update(cx, |pane, cx| {
23487 pane.active_item()
23488 .unwrap()
23489 .downcast::<Editor>()
23490 .unwrap()
23491 .update(cx, |editor, cx| {
23492 assert_eq!(
23493 editor.display_text(cx),
23494 lib_text,
23495 "Other file should be open and active",
23496 );
23497 });
23498 assert_eq!(pane.items().count(), 1, "No other editors should be open");
23499 });
23500
23501 let _other_editor_2 = workspace
23502 .update_in(cx, |workspace, window, cx| {
23503 workspace.open_path(
23504 (worktree_id, rel_path("lib.rs")),
23505 Some(pane_2.downgrade()),
23506 true,
23507 window,
23508 cx,
23509 )
23510 })
23511 .unwrap()
23512 .await
23513 .downcast::<Editor>()
23514 .unwrap();
23515 pane_2
23516 .update_in(cx, |pane, window, cx| {
23517 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23518 })
23519 .await
23520 .unwrap();
23521 drop(editor_2);
23522 pane_2.update(cx, |pane, cx| {
23523 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23524 open_editor.update(cx, |editor, cx| {
23525 assert_eq!(
23526 editor.display_text(cx),
23527 lib_text,
23528 "Other file should be open and active in another panel too",
23529 );
23530 });
23531 assert_eq!(
23532 pane.items().count(),
23533 1,
23534 "No other editors should be open in another pane",
23535 );
23536 });
23537
23538 let _editor_1_reopened = workspace
23539 .update_in(cx, |workspace, window, cx| {
23540 workspace.open_path(
23541 (worktree_id, rel_path("main.rs")),
23542 Some(pane_1.downgrade()),
23543 true,
23544 window,
23545 cx,
23546 )
23547 })
23548 .unwrap()
23549 .await
23550 .downcast::<Editor>()
23551 .unwrap();
23552 let _editor_2_reopened = workspace
23553 .update_in(cx, |workspace, window, cx| {
23554 workspace.open_path(
23555 (worktree_id, rel_path("main.rs")),
23556 Some(pane_2.downgrade()),
23557 true,
23558 window,
23559 cx,
23560 )
23561 })
23562 .unwrap()
23563 .await
23564 .downcast::<Editor>()
23565 .unwrap();
23566 pane_1.update(cx, |pane, cx| {
23567 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23568 open_editor.update(cx, |editor, cx| {
23569 assert_eq!(
23570 editor.display_text(cx),
23571 main_text,
23572 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
23573 );
23574 assert_eq!(
23575 editor
23576 .selections
23577 .all::<Point>(cx)
23578 .into_iter()
23579 .map(|s| s.range())
23580 .collect::<Vec<_>>(),
23581 expected_ranges,
23582 "Previous editor in the 1st panel had selections and should get them restored on reopen",
23583 );
23584 })
23585 });
23586 pane_2.update(cx, |pane, cx| {
23587 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23588 open_editor.update(cx, |editor, cx| {
23589 assert_eq!(
23590 editor.display_text(cx),
23591 r#"fn main() {
23592⋯rintln!("1");
23593⋯intln!("2");
23594⋯ntln!("3");
23595println!("4");
23596println!("5");
23597}"#,
23598 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
23599 );
23600 assert_eq!(
23601 editor
23602 .selections
23603 .all::<Point>(cx)
23604 .into_iter()
23605 .map(|s| s.range())
23606 .collect::<Vec<_>>(),
23607 vec![Point::zero()..Point::zero()],
23608 "Previous editor in the 2nd pane had no selections changed hence should restore none",
23609 );
23610 })
23611 });
23612}
23613
23614#[gpui::test]
23615async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
23616 init_test(cx, |_| {});
23617
23618 let fs = FakeFs::new(cx.executor());
23619 let main_text = r#"fn main() {
23620println!("1");
23621println!("2");
23622println!("3");
23623println!("4");
23624println!("5");
23625}"#;
23626 let lib_text = "mod foo {}";
23627 fs.insert_tree(
23628 path!("/a"),
23629 json!({
23630 "lib.rs": lib_text,
23631 "main.rs": main_text,
23632 }),
23633 )
23634 .await;
23635
23636 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23637 let (workspace, cx) =
23638 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23639 let worktree_id = workspace.update(cx, |workspace, cx| {
23640 workspace.project().update(cx, |project, cx| {
23641 project.worktrees(cx).next().unwrap().read(cx).id()
23642 })
23643 });
23644
23645 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23646 let editor = workspace
23647 .update_in(cx, |workspace, window, cx| {
23648 workspace.open_path(
23649 (worktree_id, rel_path("main.rs")),
23650 Some(pane.downgrade()),
23651 true,
23652 window,
23653 cx,
23654 )
23655 })
23656 .unwrap()
23657 .await
23658 .downcast::<Editor>()
23659 .unwrap();
23660 pane.update(cx, |pane, cx| {
23661 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23662 open_editor.update(cx, |editor, cx| {
23663 assert_eq!(
23664 editor.display_text(cx),
23665 main_text,
23666 "Original main.rs text on initial open",
23667 );
23668 })
23669 });
23670 editor.update_in(cx, |editor, window, cx| {
23671 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
23672 });
23673
23674 cx.update_global(|store: &mut SettingsStore, cx| {
23675 store.update_user_settings(cx, |s| {
23676 s.workspace.restore_on_file_reopen = Some(false);
23677 });
23678 });
23679 editor.update_in(cx, |editor, window, cx| {
23680 editor.fold_ranges(
23681 vec![
23682 Point::new(1, 0)..Point::new(1, 1),
23683 Point::new(2, 0)..Point::new(2, 2),
23684 Point::new(3, 0)..Point::new(3, 3),
23685 ],
23686 false,
23687 window,
23688 cx,
23689 );
23690 });
23691 pane.update_in(cx, |pane, window, cx| {
23692 pane.close_all_items(&CloseAllItems::default(), window, cx)
23693 })
23694 .await
23695 .unwrap();
23696 pane.update(cx, |pane, _| {
23697 assert!(pane.active_item().is_none());
23698 });
23699 cx.update_global(|store: &mut SettingsStore, cx| {
23700 store.update_user_settings(cx, |s| {
23701 s.workspace.restore_on_file_reopen = Some(true);
23702 });
23703 });
23704
23705 let _editor_reopened = workspace
23706 .update_in(cx, |workspace, window, cx| {
23707 workspace.open_path(
23708 (worktree_id, rel_path("main.rs")),
23709 Some(pane.downgrade()),
23710 true,
23711 window,
23712 cx,
23713 )
23714 })
23715 .unwrap()
23716 .await
23717 .downcast::<Editor>()
23718 .unwrap();
23719 pane.update(cx, |pane, cx| {
23720 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23721 open_editor.update(cx, |editor, cx| {
23722 assert_eq!(
23723 editor.display_text(cx),
23724 main_text,
23725 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
23726 );
23727 })
23728 });
23729}
23730
23731#[gpui::test]
23732async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
23733 struct EmptyModalView {
23734 focus_handle: gpui::FocusHandle,
23735 }
23736 impl EventEmitter<DismissEvent> for EmptyModalView {}
23737 impl Render for EmptyModalView {
23738 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
23739 div()
23740 }
23741 }
23742 impl Focusable for EmptyModalView {
23743 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
23744 self.focus_handle.clone()
23745 }
23746 }
23747 impl workspace::ModalView for EmptyModalView {}
23748 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
23749 EmptyModalView {
23750 focus_handle: cx.focus_handle(),
23751 }
23752 }
23753
23754 init_test(cx, |_| {});
23755
23756 let fs = FakeFs::new(cx.executor());
23757 let project = Project::test(fs, [], cx).await;
23758 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23759 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
23760 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23761 let editor = cx.new_window_entity(|window, cx| {
23762 Editor::new(
23763 EditorMode::full(),
23764 buffer,
23765 Some(project.clone()),
23766 window,
23767 cx,
23768 )
23769 });
23770 workspace
23771 .update(cx, |workspace, window, cx| {
23772 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
23773 })
23774 .unwrap();
23775 editor.update_in(cx, |editor, window, cx| {
23776 editor.open_context_menu(&OpenContextMenu, window, cx);
23777 assert!(editor.mouse_context_menu.is_some());
23778 });
23779 workspace
23780 .update(cx, |workspace, window, cx| {
23781 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
23782 })
23783 .unwrap();
23784 cx.read(|cx| {
23785 assert!(editor.read(cx).mouse_context_menu.is_none());
23786 });
23787}
23788
23789fn set_linked_edit_ranges(
23790 opening: (Point, Point),
23791 closing: (Point, Point),
23792 editor: &mut Editor,
23793 cx: &mut Context<Editor>,
23794) {
23795 let Some((buffer, _)) = editor
23796 .buffer
23797 .read(cx)
23798 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
23799 else {
23800 panic!("Failed to get buffer for selection position");
23801 };
23802 let buffer = buffer.read(cx);
23803 let buffer_id = buffer.remote_id();
23804 let opening_range = buffer.anchor_before(opening.0)..buffer.anchor_after(opening.1);
23805 let closing_range = buffer.anchor_before(closing.0)..buffer.anchor_after(closing.1);
23806 let mut linked_ranges = HashMap::default();
23807 linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
23808 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
23809}
23810
23811#[gpui::test]
23812async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
23813 init_test(cx, |_| {});
23814
23815 let fs = FakeFs::new(cx.executor());
23816 fs.insert_file(path!("/file.html"), Default::default())
23817 .await;
23818
23819 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
23820
23821 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23822 let html_language = Arc::new(Language::new(
23823 LanguageConfig {
23824 name: "HTML".into(),
23825 matcher: LanguageMatcher {
23826 path_suffixes: vec!["html".to_string()],
23827 ..LanguageMatcher::default()
23828 },
23829 brackets: BracketPairConfig {
23830 pairs: vec![BracketPair {
23831 start: "<".into(),
23832 end: ">".into(),
23833 close: true,
23834 ..Default::default()
23835 }],
23836 ..Default::default()
23837 },
23838 ..Default::default()
23839 },
23840 Some(tree_sitter_html::LANGUAGE.into()),
23841 ));
23842 language_registry.add(html_language);
23843 let mut fake_servers = language_registry.register_fake_lsp(
23844 "HTML",
23845 FakeLspAdapter {
23846 capabilities: lsp::ServerCapabilities {
23847 completion_provider: Some(lsp::CompletionOptions {
23848 resolve_provider: Some(true),
23849 ..Default::default()
23850 }),
23851 ..Default::default()
23852 },
23853 ..Default::default()
23854 },
23855 );
23856
23857 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23858 let cx = &mut VisualTestContext::from_window(*workspace, cx);
23859
23860 let worktree_id = workspace
23861 .update(cx, |workspace, _window, cx| {
23862 workspace.project().update(cx, |project, cx| {
23863 project.worktrees(cx).next().unwrap().read(cx).id()
23864 })
23865 })
23866 .unwrap();
23867 project
23868 .update(cx, |project, cx| {
23869 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
23870 })
23871 .await
23872 .unwrap();
23873 let editor = workspace
23874 .update(cx, |workspace, window, cx| {
23875 workspace.open_path((worktree_id, rel_path("file.html")), None, true, window, cx)
23876 })
23877 .unwrap()
23878 .await
23879 .unwrap()
23880 .downcast::<Editor>()
23881 .unwrap();
23882
23883 let fake_server = fake_servers.next().await.unwrap();
23884 editor.update_in(cx, |editor, window, cx| {
23885 editor.set_text("<ad></ad>", window, cx);
23886 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
23887 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
23888 });
23889 set_linked_edit_ranges(
23890 (Point::new(0, 1), Point::new(0, 3)),
23891 (Point::new(0, 6), Point::new(0, 8)),
23892 editor,
23893 cx,
23894 );
23895 });
23896 let mut completion_handle =
23897 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
23898 Ok(Some(lsp::CompletionResponse::Array(vec![
23899 lsp::CompletionItem {
23900 label: "head".to_string(),
23901 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
23902 lsp::InsertReplaceEdit {
23903 new_text: "head".to_string(),
23904 insert: lsp::Range::new(
23905 lsp::Position::new(0, 1),
23906 lsp::Position::new(0, 3),
23907 ),
23908 replace: lsp::Range::new(
23909 lsp::Position::new(0, 1),
23910 lsp::Position::new(0, 3),
23911 ),
23912 },
23913 )),
23914 ..Default::default()
23915 },
23916 ])))
23917 });
23918 editor.update_in(cx, |editor, window, cx| {
23919 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
23920 });
23921 cx.run_until_parked();
23922 completion_handle.next().await.unwrap();
23923 editor.update(cx, |editor, _| {
23924 assert!(
23925 editor.context_menu_visible(),
23926 "Completion menu should be visible"
23927 );
23928 });
23929 editor.update_in(cx, |editor, window, cx| {
23930 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
23931 });
23932 cx.executor().run_until_parked();
23933 editor.update(cx, |editor, cx| {
23934 assert_eq!(editor.text(cx), "<head></head>");
23935 });
23936}
23937
23938#[gpui::test]
23939async fn test_linked_edits_on_typing_punctuation(cx: &mut TestAppContext) {
23940 init_test(cx, |_| {});
23941
23942 let mut cx = EditorTestContext::new(cx).await;
23943 let language = Arc::new(Language::new(
23944 LanguageConfig {
23945 name: "TSX".into(),
23946 matcher: LanguageMatcher {
23947 path_suffixes: vec!["tsx".to_string()],
23948 ..LanguageMatcher::default()
23949 },
23950 brackets: BracketPairConfig {
23951 pairs: vec![BracketPair {
23952 start: "<".into(),
23953 end: ">".into(),
23954 close: true,
23955 ..Default::default()
23956 }],
23957 ..Default::default()
23958 },
23959 linked_edit_characters: HashSet::from_iter(['.']),
23960 ..Default::default()
23961 },
23962 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
23963 ));
23964 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23965
23966 // Test typing > does not extend linked pair
23967 cx.set_state("<divˇ<div></div>");
23968 cx.update_editor(|editor, _, cx| {
23969 set_linked_edit_ranges(
23970 (Point::new(0, 1), Point::new(0, 4)),
23971 (Point::new(0, 11), Point::new(0, 14)),
23972 editor,
23973 cx,
23974 );
23975 });
23976 cx.update_editor(|editor, window, cx| {
23977 editor.handle_input(">", window, cx);
23978 });
23979 cx.assert_editor_state("<div>ˇ<div></div>");
23980
23981 // Test typing . do extend linked pair
23982 cx.set_state("<Animatedˇ></Animated>");
23983 cx.update_editor(|editor, _, cx| {
23984 set_linked_edit_ranges(
23985 (Point::new(0, 1), Point::new(0, 9)),
23986 (Point::new(0, 12), Point::new(0, 20)),
23987 editor,
23988 cx,
23989 );
23990 });
23991 cx.update_editor(|editor, window, cx| {
23992 editor.handle_input(".", window, cx);
23993 });
23994 cx.assert_editor_state("<Animated.ˇ></Animated.>");
23995 cx.update_editor(|editor, _, cx| {
23996 set_linked_edit_ranges(
23997 (Point::new(0, 1), Point::new(0, 10)),
23998 (Point::new(0, 13), Point::new(0, 21)),
23999 editor,
24000 cx,
24001 );
24002 });
24003 cx.update_editor(|editor, window, cx| {
24004 editor.handle_input("V", window, cx);
24005 });
24006 cx.assert_editor_state("<Animated.Vˇ></Animated.V>");
24007}
24008
24009#[gpui::test]
24010async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
24011 init_test(cx, |_| {});
24012
24013 let fs = FakeFs::new(cx.executor());
24014 fs.insert_tree(
24015 path!("/root"),
24016 json!({
24017 "a": {
24018 "main.rs": "fn main() {}",
24019 },
24020 "foo": {
24021 "bar": {
24022 "external_file.rs": "pub mod external {}",
24023 }
24024 }
24025 }),
24026 )
24027 .await;
24028
24029 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
24030 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24031 language_registry.add(rust_lang());
24032 let _fake_servers = language_registry.register_fake_lsp(
24033 "Rust",
24034 FakeLspAdapter {
24035 ..FakeLspAdapter::default()
24036 },
24037 );
24038 let (workspace, cx) =
24039 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24040 let worktree_id = workspace.update(cx, |workspace, cx| {
24041 workspace.project().update(cx, |project, cx| {
24042 project.worktrees(cx).next().unwrap().read(cx).id()
24043 })
24044 });
24045
24046 let assert_language_servers_count =
24047 |expected: usize, context: &str, cx: &mut VisualTestContext| {
24048 project.update(cx, |project, cx| {
24049 let current = project
24050 .lsp_store()
24051 .read(cx)
24052 .as_local()
24053 .unwrap()
24054 .language_servers
24055 .len();
24056 assert_eq!(expected, current, "{context}");
24057 });
24058 };
24059
24060 assert_language_servers_count(
24061 0,
24062 "No servers should be running before any file is open",
24063 cx,
24064 );
24065 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
24066 let main_editor = workspace
24067 .update_in(cx, |workspace, window, cx| {
24068 workspace.open_path(
24069 (worktree_id, rel_path("main.rs")),
24070 Some(pane.downgrade()),
24071 true,
24072 window,
24073 cx,
24074 )
24075 })
24076 .unwrap()
24077 .await
24078 .downcast::<Editor>()
24079 .unwrap();
24080 pane.update(cx, |pane, cx| {
24081 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24082 open_editor.update(cx, |editor, cx| {
24083 assert_eq!(
24084 editor.display_text(cx),
24085 "fn main() {}",
24086 "Original main.rs text on initial open",
24087 );
24088 });
24089 assert_eq!(open_editor, main_editor);
24090 });
24091 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
24092
24093 let external_editor = workspace
24094 .update_in(cx, |workspace, window, cx| {
24095 workspace.open_abs_path(
24096 PathBuf::from("/root/foo/bar/external_file.rs"),
24097 OpenOptions::default(),
24098 window,
24099 cx,
24100 )
24101 })
24102 .await
24103 .expect("opening external file")
24104 .downcast::<Editor>()
24105 .expect("downcasted external file's open element to editor");
24106 pane.update(cx, |pane, cx| {
24107 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24108 open_editor.update(cx, |editor, cx| {
24109 assert_eq!(
24110 editor.display_text(cx),
24111 "pub mod external {}",
24112 "External file is open now",
24113 );
24114 });
24115 assert_eq!(open_editor, external_editor);
24116 });
24117 assert_language_servers_count(
24118 1,
24119 "Second, external, *.rs file should join the existing server",
24120 cx,
24121 );
24122
24123 pane.update_in(cx, |pane, window, cx| {
24124 pane.close_active_item(&CloseActiveItem::default(), window, cx)
24125 })
24126 .await
24127 .unwrap();
24128 pane.update_in(cx, |pane, window, cx| {
24129 pane.navigate_backward(&Default::default(), window, cx);
24130 });
24131 cx.run_until_parked();
24132 pane.update(cx, |pane, cx| {
24133 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24134 open_editor.update(cx, |editor, cx| {
24135 assert_eq!(
24136 editor.display_text(cx),
24137 "pub mod external {}",
24138 "External file is open now",
24139 );
24140 });
24141 });
24142 assert_language_servers_count(
24143 1,
24144 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
24145 cx,
24146 );
24147
24148 cx.update(|_, cx| {
24149 workspace::reload(cx);
24150 });
24151 assert_language_servers_count(
24152 1,
24153 "After reloading the worktree with local and external files opened, only one project should be started",
24154 cx,
24155 );
24156}
24157
24158#[gpui::test]
24159async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
24160 init_test(cx, |_| {});
24161
24162 let mut cx = EditorTestContext::new(cx).await;
24163 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24164 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24165
24166 // test cursor move to start of each line on tab
24167 // for `if`, `elif`, `else`, `while`, `with` and `for`
24168 cx.set_state(indoc! {"
24169 def main():
24170 ˇ for item in items:
24171 ˇ while item.active:
24172 ˇ if item.value > 10:
24173 ˇ continue
24174 ˇ elif item.value < 0:
24175 ˇ break
24176 ˇ else:
24177 ˇ with item.context() as ctx:
24178 ˇ yield count
24179 ˇ else:
24180 ˇ log('while else')
24181 ˇ else:
24182 ˇ log('for else')
24183 "});
24184 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24185 cx.assert_editor_state(indoc! {"
24186 def main():
24187 ˇfor item in items:
24188 ˇwhile item.active:
24189 ˇif item.value > 10:
24190 ˇcontinue
24191 ˇelif item.value < 0:
24192 ˇbreak
24193 ˇelse:
24194 ˇwith item.context() as ctx:
24195 ˇyield count
24196 ˇelse:
24197 ˇlog('while else')
24198 ˇelse:
24199 ˇlog('for else')
24200 "});
24201 // test relative indent is preserved when tab
24202 // for `if`, `elif`, `else`, `while`, `with` and `for`
24203 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24204 cx.assert_editor_state(indoc! {"
24205 def main():
24206 ˇfor item in items:
24207 ˇwhile item.active:
24208 ˇif item.value > 10:
24209 ˇcontinue
24210 ˇelif item.value < 0:
24211 ˇbreak
24212 ˇelse:
24213 ˇwith item.context() as ctx:
24214 ˇyield count
24215 ˇelse:
24216 ˇlog('while else')
24217 ˇelse:
24218 ˇlog('for else')
24219 "});
24220
24221 // test cursor move to start of each line on tab
24222 // for `try`, `except`, `else`, `finally`, `match` and `def`
24223 cx.set_state(indoc! {"
24224 def main():
24225 ˇ try:
24226 ˇ fetch()
24227 ˇ except ValueError:
24228 ˇ handle_error()
24229 ˇ else:
24230 ˇ match value:
24231 ˇ case _:
24232 ˇ finally:
24233 ˇ def status():
24234 ˇ return 0
24235 "});
24236 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24237 cx.assert_editor_state(indoc! {"
24238 def main():
24239 ˇtry:
24240 ˇfetch()
24241 ˇexcept ValueError:
24242 ˇhandle_error()
24243 ˇelse:
24244 ˇmatch value:
24245 ˇcase _:
24246 ˇfinally:
24247 ˇdef status():
24248 ˇreturn 0
24249 "});
24250 // test relative indent is preserved when tab
24251 // for `try`, `except`, `else`, `finally`, `match` and `def`
24252 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24253 cx.assert_editor_state(indoc! {"
24254 def main():
24255 ˇtry:
24256 ˇfetch()
24257 ˇexcept ValueError:
24258 ˇhandle_error()
24259 ˇelse:
24260 ˇmatch value:
24261 ˇcase _:
24262 ˇfinally:
24263 ˇdef status():
24264 ˇreturn 0
24265 "});
24266}
24267
24268#[gpui::test]
24269async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
24270 init_test(cx, |_| {});
24271
24272 let mut cx = EditorTestContext::new(cx).await;
24273 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24274 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24275
24276 // test `else` auto outdents when typed inside `if` block
24277 cx.set_state(indoc! {"
24278 def main():
24279 if i == 2:
24280 return
24281 ˇ
24282 "});
24283 cx.update_editor(|editor, window, cx| {
24284 editor.handle_input("else:", window, cx);
24285 });
24286 cx.assert_editor_state(indoc! {"
24287 def main():
24288 if i == 2:
24289 return
24290 else:ˇ
24291 "});
24292
24293 // test `except` auto outdents when typed inside `try` block
24294 cx.set_state(indoc! {"
24295 def main():
24296 try:
24297 i = 2
24298 ˇ
24299 "});
24300 cx.update_editor(|editor, window, cx| {
24301 editor.handle_input("except:", window, cx);
24302 });
24303 cx.assert_editor_state(indoc! {"
24304 def main():
24305 try:
24306 i = 2
24307 except:ˇ
24308 "});
24309
24310 // test `else` auto outdents when typed inside `except` block
24311 cx.set_state(indoc! {"
24312 def main():
24313 try:
24314 i = 2
24315 except:
24316 j = 2
24317 ˇ
24318 "});
24319 cx.update_editor(|editor, window, cx| {
24320 editor.handle_input("else:", window, cx);
24321 });
24322 cx.assert_editor_state(indoc! {"
24323 def main():
24324 try:
24325 i = 2
24326 except:
24327 j = 2
24328 else:ˇ
24329 "});
24330
24331 // test `finally` auto outdents when typed inside `else` block
24332 cx.set_state(indoc! {"
24333 def main():
24334 try:
24335 i = 2
24336 except:
24337 j = 2
24338 else:
24339 k = 2
24340 ˇ
24341 "});
24342 cx.update_editor(|editor, window, cx| {
24343 editor.handle_input("finally:", window, cx);
24344 });
24345 cx.assert_editor_state(indoc! {"
24346 def main():
24347 try:
24348 i = 2
24349 except:
24350 j = 2
24351 else:
24352 k = 2
24353 finally:ˇ
24354 "});
24355
24356 // test `else` does not outdents when typed inside `except` block right after for block
24357 cx.set_state(indoc! {"
24358 def main():
24359 try:
24360 i = 2
24361 except:
24362 for i in range(n):
24363 pass
24364 ˇ
24365 "});
24366 cx.update_editor(|editor, window, cx| {
24367 editor.handle_input("else:", window, cx);
24368 });
24369 cx.assert_editor_state(indoc! {"
24370 def main():
24371 try:
24372 i = 2
24373 except:
24374 for i in range(n):
24375 pass
24376 else:ˇ
24377 "});
24378
24379 // test `finally` auto outdents when typed inside `else` block right after for block
24380 cx.set_state(indoc! {"
24381 def main():
24382 try:
24383 i = 2
24384 except:
24385 j = 2
24386 else:
24387 for i in range(n):
24388 pass
24389 ˇ
24390 "});
24391 cx.update_editor(|editor, window, cx| {
24392 editor.handle_input("finally:", window, cx);
24393 });
24394 cx.assert_editor_state(indoc! {"
24395 def main():
24396 try:
24397 i = 2
24398 except:
24399 j = 2
24400 else:
24401 for i in range(n):
24402 pass
24403 finally:ˇ
24404 "});
24405
24406 // test `except` outdents to inner "try" block
24407 cx.set_state(indoc! {"
24408 def main():
24409 try:
24410 i = 2
24411 if i == 2:
24412 try:
24413 i = 3
24414 ˇ
24415 "});
24416 cx.update_editor(|editor, window, cx| {
24417 editor.handle_input("except:", window, cx);
24418 });
24419 cx.assert_editor_state(indoc! {"
24420 def main():
24421 try:
24422 i = 2
24423 if i == 2:
24424 try:
24425 i = 3
24426 except:ˇ
24427 "});
24428
24429 // test `except` outdents to outer "try" block
24430 cx.set_state(indoc! {"
24431 def main():
24432 try:
24433 i = 2
24434 if i == 2:
24435 try:
24436 i = 3
24437 ˇ
24438 "});
24439 cx.update_editor(|editor, window, cx| {
24440 editor.handle_input("except:", window, cx);
24441 });
24442 cx.assert_editor_state(indoc! {"
24443 def main():
24444 try:
24445 i = 2
24446 if i == 2:
24447 try:
24448 i = 3
24449 except:ˇ
24450 "});
24451
24452 // test `else` stays at correct indent when typed after `for` block
24453 cx.set_state(indoc! {"
24454 def main():
24455 for i in range(10):
24456 if i == 3:
24457 break
24458 ˇ
24459 "});
24460 cx.update_editor(|editor, window, cx| {
24461 editor.handle_input("else:", window, cx);
24462 });
24463 cx.assert_editor_state(indoc! {"
24464 def main():
24465 for i in range(10):
24466 if i == 3:
24467 break
24468 else:ˇ
24469 "});
24470
24471 // test does not outdent on typing after line with square brackets
24472 cx.set_state(indoc! {"
24473 def f() -> list[str]:
24474 ˇ
24475 "});
24476 cx.update_editor(|editor, window, cx| {
24477 editor.handle_input("a", window, cx);
24478 });
24479 cx.assert_editor_state(indoc! {"
24480 def f() -> list[str]:
24481 aˇ
24482 "});
24483
24484 // test does not outdent on typing : after case keyword
24485 cx.set_state(indoc! {"
24486 match 1:
24487 caseˇ
24488 "});
24489 cx.update_editor(|editor, window, cx| {
24490 editor.handle_input(":", window, cx);
24491 });
24492 cx.assert_editor_state(indoc! {"
24493 match 1:
24494 case:ˇ
24495 "});
24496}
24497
24498#[gpui::test]
24499async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
24500 init_test(cx, |_| {});
24501 update_test_language_settings(cx, |settings| {
24502 settings.defaults.extend_comment_on_newline = Some(false);
24503 });
24504 let mut cx = EditorTestContext::new(cx).await;
24505 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24506 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24507
24508 // test correct indent after newline on comment
24509 cx.set_state(indoc! {"
24510 # COMMENT:ˇ
24511 "});
24512 cx.update_editor(|editor, window, cx| {
24513 editor.newline(&Newline, window, cx);
24514 });
24515 cx.assert_editor_state(indoc! {"
24516 # COMMENT:
24517 ˇ
24518 "});
24519
24520 // test correct indent after newline in brackets
24521 cx.set_state(indoc! {"
24522 {ˇ}
24523 "});
24524 cx.update_editor(|editor, window, cx| {
24525 editor.newline(&Newline, window, cx);
24526 });
24527 cx.run_until_parked();
24528 cx.assert_editor_state(indoc! {"
24529 {
24530 ˇ
24531 }
24532 "});
24533
24534 cx.set_state(indoc! {"
24535 (ˇ)
24536 "});
24537 cx.update_editor(|editor, window, cx| {
24538 editor.newline(&Newline, window, cx);
24539 });
24540 cx.run_until_parked();
24541 cx.assert_editor_state(indoc! {"
24542 (
24543 ˇ
24544 )
24545 "});
24546
24547 // do not indent after empty lists or dictionaries
24548 cx.set_state(indoc! {"
24549 a = []ˇ
24550 "});
24551 cx.update_editor(|editor, window, cx| {
24552 editor.newline(&Newline, window, cx);
24553 });
24554 cx.run_until_parked();
24555 cx.assert_editor_state(indoc! {"
24556 a = []
24557 ˇ
24558 "});
24559}
24560
24561#[gpui::test]
24562async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
24563 init_test(cx, |_| {});
24564
24565 let mut cx = EditorTestContext::new(cx).await;
24566 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24567 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24568
24569 // test cursor move to start of each line on tab
24570 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
24571 cx.set_state(indoc! {"
24572 function main() {
24573 ˇ for item in $items; do
24574 ˇ while [ -n \"$item\" ]; do
24575 ˇ if [ \"$value\" -gt 10 ]; then
24576 ˇ continue
24577 ˇ elif [ \"$value\" -lt 0 ]; then
24578 ˇ break
24579 ˇ else
24580 ˇ echo \"$item\"
24581 ˇ fi
24582 ˇ done
24583 ˇ done
24584 ˇ}
24585 "});
24586 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24587 cx.assert_editor_state(indoc! {"
24588 function main() {
24589 ˇfor item in $items; do
24590 ˇwhile [ -n \"$item\" ]; do
24591 ˇif [ \"$value\" -gt 10 ]; then
24592 ˇcontinue
24593 ˇelif [ \"$value\" -lt 0 ]; then
24594 ˇbreak
24595 ˇelse
24596 ˇecho \"$item\"
24597 ˇfi
24598 ˇdone
24599 ˇdone
24600 ˇ}
24601 "});
24602 // test relative indent is preserved when tab
24603 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24604 cx.assert_editor_state(indoc! {"
24605 function main() {
24606 ˇfor item in $items; do
24607 ˇwhile [ -n \"$item\" ]; do
24608 ˇif [ \"$value\" -gt 10 ]; then
24609 ˇcontinue
24610 ˇelif [ \"$value\" -lt 0 ]; then
24611 ˇbreak
24612 ˇelse
24613 ˇecho \"$item\"
24614 ˇfi
24615 ˇdone
24616 ˇdone
24617 ˇ}
24618 "});
24619
24620 // test cursor move to start of each line on tab
24621 // for `case` statement with patterns
24622 cx.set_state(indoc! {"
24623 function handle() {
24624 ˇ case \"$1\" in
24625 ˇ start)
24626 ˇ echo \"a\"
24627 ˇ ;;
24628 ˇ stop)
24629 ˇ echo \"b\"
24630 ˇ ;;
24631 ˇ *)
24632 ˇ echo \"c\"
24633 ˇ ;;
24634 ˇ esac
24635 ˇ}
24636 "});
24637 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24638 cx.assert_editor_state(indoc! {"
24639 function handle() {
24640 ˇcase \"$1\" in
24641 ˇstart)
24642 ˇecho \"a\"
24643 ˇ;;
24644 ˇstop)
24645 ˇecho \"b\"
24646 ˇ;;
24647 ˇ*)
24648 ˇecho \"c\"
24649 ˇ;;
24650 ˇesac
24651 ˇ}
24652 "});
24653}
24654
24655#[gpui::test]
24656async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
24657 init_test(cx, |_| {});
24658
24659 let mut cx = EditorTestContext::new(cx).await;
24660 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24661 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24662
24663 // test indents on comment insert
24664 cx.set_state(indoc! {"
24665 function main() {
24666 ˇ for item in $items; do
24667 ˇ while [ -n \"$item\" ]; do
24668 ˇ if [ \"$value\" -gt 10 ]; then
24669 ˇ continue
24670 ˇ elif [ \"$value\" -lt 0 ]; then
24671 ˇ break
24672 ˇ else
24673 ˇ echo \"$item\"
24674 ˇ fi
24675 ˇ done
24676 ˇ done
24677 ˇ}
24678 "});
24679 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
24680 cx.assert_editor_state(indoc! {"
24681 function main() {
24682 #ˇ for item in $items; do
24683 #ˇ while [ -n \"$item\" ]; do
24684 #ˇ if [ \"$value\" -gt 10 ]; then
24685 #ˇ continue
24686 #ˇ elif [ \"$value\" -lt 0 ]; then
24687 #ˇ break
24688 #ˇ else
24689 #ˇ echo \"$item\"
24690 #ˇ fi
24691 #ˇ done
24692 #ˇ done
24693 #ˇ}
24694 "});
24695}
24696
24697#[gpui::test]
24698async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
24699 init_test(cx, |_| {});
24700
24701 let mut cx = EditorTestContext::new(cx).await;
24702 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24703 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24704
24705 // test `else` auto outdents when typed inside `if` block
24706 cx.set_state(indoc! {"
24707 if [ \"$1\" = \"test\" ]; then
24708 echo \"foo bar\"
24709 ˇ
24710 "});
24711 cx.update_editor(|editor, window, cx| {
24712 editor.handle_input("else", window, cx);
24713 });
24714 cx.assert_editor_state(indoc! {"
24715 if [ \"$1\" = \"test\" ]; then
24716 echo \"foo bar\"
24717 elseˇ
24718 "});
24719
24720 // test `elif` auto outdents when typed inside `if` block
24721 cx.set_state(indoc! {"
24722 if [ \"$1\" = \"test\" ]; then
24723 echo \"foo bar\"
24724 ˇ
24725 "});
24726 cx.update_editor(|editor, window, cx| {
24727 editor.handle_input("elif", window, cx);
24728 });
24729 cx.assert_editor_state(indoc! {"
24730 if [ \"$1\" = \"test\" ]; then
24731 echo \"foo bar\"
24732 elifˇ
24733 "});
24734
24735 // test `fi` auto outdents when typed inside `else` block
24736 cx.set_state(indoc! {"
24737 if [ \"$1\" = \"test\" ]; then
24738 echo \"foo bar\"
24739 else
24740 echo \"bar baz\"
24741 ˇ
24742 "});
24743 cx.update_editor(|editor, window, cx| {
24744 editor.handle_input("fi", window, cx);
24745 });
24746 cx.assert_editor_state(indoc! {"
24747 if [ \"$1\" = \"test\" ]; then
24748 echo \"foo bar\"
24749 else
24750 echo \"bar baz\"
24751 fiˇ
24752 "});
24753
24754 // test `done` auto outdents when typed inside `while` block
24755 cx.set_state(indoc! {"
24756 while read line; do
24757 echo \"$line\"
24758 ˇ
24759 "});
24760 cx.update_editor(|editor, window, cx| {
24761 editor.handle_input("done", window, cx);
24762 });
24763 cx.assert_editor_state(indoc! {"
24764 while read line; do
24765 echo \"$line\"
24766 doneˇ
24767 "});
24768
24769 // test `done` auto outdents when typed inside `for` block
24770 cx.set_state(indoc! {"
24771 for file in *.txt; do
24772 cat \"$file\"
24773 ˇ
24774 "});
24775 cx.update_editor(|editor, window, cx| {
24776 editor.handle_input("done", window, cx);
24777 });
24778 cx.assert_editor_state(indoc! {"
24779 for file in *.txt; do
24780 cat \"$file\"
24781 doneˇ
24782 "});
24783
24784 // test `esac` auto outdents when typed inside `case` block
24785 cx.set_state(indoc! {"
24786 case \"$1\" in
24787 start)
24788 echo \"foo bar\"
24789 ;;
24790 stop)
24791 echo \"bar baz\"
24792 ;;
24793 ˇ
24794 "});
24795 cx.update_editor(|editor, window, cx| {
24796 editor.handle_input("esac", window, cx);
24797 });
24798 cx.assert_editor_state(indoc! {"
24799 case \"$1\" in
24800 start)
24801 echo \"foo bar\"
24802 ;;
24803 stop)
24804 echo \"bar baz\"
24805 ;;
24806 esacˇ
24807 "});
24808
24809 // test `*)` auto outdents when typed inside `case` block
24810 cx.set_state(indoc! {"
24811 case \"$1\" in
24812 start)
24813 echo \"foo bar\"
24814 ;;
24815 ˇ
24816 "});
24817 cx.update_editor(|editor, window, cx| {
24818 editor.handle_input("*)", window, cx);
24819 });
24820 cx.assert_editor_state(indoc! {"
24821 case \"$1\" in
24822 start)
24823 echo \"foo bar\"
24824 ;;
24825 *)ˇ
24826 "});
24827
24828 // test `fi` outdents to correct level with nested if blocks
24829 cx.set_state(indoc! {"
24830 if [ \"$1\" = \"test\" ]; then
24831 echo \"outer if\"
24832 if [ \"$2\" = \"debug\" ]; then
24833 echo \"inner if\"
24834 ˇ
24835 "});
24836 cx.update_editor(|editor, window, cx| {
24837 editor.handle_input("fi", window, cx);
24838 });
24839 cx.assert_editor_state(indoc! {"
24840 if [ \"$1\" = \"test\" ]; then
24841 echo \"outer if\"
24842 if [ \"$2\" = \"debug\" ]; then
24843 echo \"inner if\"
24844 fiˇ
24845 "});
24846}
24847
24848#[gpui::test]
24849async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
24850 init_test(cx, |_| {});
24851 update_test_language_settings(cx, |settings| {
24852 settings.defaults.extend_comment_on_newline = Some(false);
24853 });
24854 let mut cx = EditorTestContext::new(cx).await;
24855 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24856 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24857
24858 // test correct indent after newline on comment
24859 cx.set_state(indoc! {"
24860 # COMMENT:ˇ
24861 "});
24862 cx.update_editor(|editor, window, cx| {
24863 editor.newline(&Newline, window, cx);
24864 });
24865 cx.assert_editor_state(indoc! {"
24866 # COMMENT:
24867 ˇ
24868 "});
24869
24870 // test correct indent after newline after `then`
24871 cx.set_state(indoc! {"
24872
24873 if [ \"$1\" = \"test\" ]; thenˇ
24874 "});
24875 cx.update_editor(|editor, window, cx| {
24876 editor.newline(&Newline, window, cx);
24877 });
24878 cx.run_until_parked();
24879 cx.assert_editor_state(indoc! {"
24880
24881 if [ \"$1\" = \"test\" ]; then
24882 ˇ
24883 "});
24884
24885 // test correct indent after newline after `else`
24886 cx.set_state(indoc! {"
24887 if [ \"$1\" = \"test\" ]; then
24888 elseˇ
24889 "});
24890 cx.update_editor(|editor, window, cx| {
24891 editor.newline(&Newline, window, cx);
24892 });
24893 cx.run_until_parked();
24894 cx.assert_editor_state(indoc! {"
24895 if [ \"$1\" = \"test\" ]; then
24896 else
24897 ˇ
24898 "});
24899
24900 // test correct indent after newline after `elif`
24901 cx.set_state(indoc! {"
24902 if [ \"$1\" = \"test\" ]; then
24903 elifˇ
24904 "});
24905 cx.update_editor(|editor, window, cx| {
24906 editor.newline(&Newline, window, cx);
24907 });
24908 cx.run_until_parked();
24909 cx.assert_editor_state(indoc! {"
24910 if [ \"$1\" = \"test\" ]; then
24911 elif
24912 ˇ
24913 "});
24914
24915 // test correct indent after newline after `do`
24916 cx.set_state(indoc! {"
24917 for file in *.txt; doˇ
24918 "});
24919 cx.update_editor(|editor, window, cx| {
24920 editor.newline(&Newline, window, cx);
24921 });
24922 cx.run_until_parked();
24923 cx.assert_editor_state(indoc! {"
24924 for file in *.txt; do
24925 ˇ
24926 "});
24927
24928 // test correct indent after newline after case pattern
24929 cx.set_state(indoc! {"
24930 case \"$1\" in
24931 start)ˇ
24932 "});
24933 cx.update_editor(|editor, window, cx| {
24934 editor.newline(&Newline, window, cx);
24935 });
24936 cx.run_until_parked();
24937 cx.assert_editor_state(indoc! {"
24938 case \"$1\" in
24939 start)
24940 ˇ
24941 "});
24942
24943 // test correct indent after newline after case pattern
24944 cx.set_state(indoc! {"
24945 case \"$1\" in
24946 start)
24947 ;;
24948 *)ˇ
24949 "});
24950 cx.update_editor(|editor, window, cx| {
24951 editor.newline(&Newline, window, cx);
24952 });
24953 cx.run_until_parked();
24954 cx.assert_editor_state(indoc! {"
24955 case \"$1\" in
24956 start)
24957 ;;
24958 *)
24959 ˇ
24960 "});
24961
24962 // test correct indent after newline after function opening brace
24963 cx.set_state(indoc! {"
24964 function test() {ˇ}
24965 "});
24966 cx.update_editor(|editor, window, cx| {
24967 editor.newline(&Newline, window, cx);
24968 });
24969 cx.run_until_parked();
24970 cx.assert_editor_state(indoc! {"
24971 function test() {
24972 ˇ
24973 }
24974 "});
24975
24976 // test no extra indent after semicolon on same line
24977 cx.set_state(indoc! {"
24978 echo \"test\";ˇ
24979 "});
24980 cx.update_editor(|editor, window, cx| {
24981 editor.newline(&Newline, window, cx);
24982 });
24983 cx.run_until_parked();
24984 cx.assert_editor_state(indoc! {"
24985 echo \"test\";
24986 ˇ
24987 "});
24988}
24989
24990fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
24991 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
24992 point..point
24993}
24994
24995#[track_caller]
24996fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
24997 let (text, ranges) = marked_text_ranges(marked_text, true);
24998 assert_eq!(editor.text(cx), text);
24999 assert_eq!(
25000 editor.selections.ranges(cx),
25001 ranges,
25002 "Assert selections are {}",
25003 marked_text
25004 );
25005}
25006
25007pub fn handle_signature_help_request(
25008 cx: &mut EditorLspTestContext,
25009 mocked_response: lsp::SignatureHelp,
25010) -> impl Future<Output = ()> + use<> {
25011 let mut request =
25012 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
25013 let mocked_response = mocked_response.clone();
25014 async move { Ok(Some(mocked_response)) }
25015 });
25016
25017 async move {
25018 request.next().await;
25019 }
25020}
25021
25022#[track_caller]
25023pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
25024 cx.update_editor(|editor, _, _| {
25025 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
25026 let entries = menu.entries.borrow();
25027 let entries = entries
25028 .iter()
25029 .map(|entry| entry.string.as_str())
25030 .collect::<Vec<_>>();
25031 assert_eq!(entries, expected);
25032 } else {
25033 panic!("Expected completions menu");
25034 }
25035 });
25036}
25037
25038/// Handle completion request passing a marked string specifying where the completion
25039/// should be triggered from using '|' character, what range should be replaced, and what completions
25040/// should be returned using '<' and '>' to delimit the range.
25041///
25042/// Also see `handle_completion_request_with_insert_and_replace`.
25043#[track_caller]
25044pub fn handle_completion_request(
25045 marked_string: &str,
25046 completions: Vec<&'static str>,
25047 is_incomplete: bool,
25048 counter: Arc<AtomicUsize>,
25049 cx: &mut EditorLspTestContext,
25050) -> impl Future<Output = ()> {
25051 let complete_from_marker: TextRangeMarker = '|'.into();
25052 let replace_range_marker: TextRangeMarker = ('<', '>').into();
25053 let (_, mut marked_ranges) = marked_text_ranges_by(
25054 marked_string,
25055 vec![complete_from_marker.clone(), replace_range_marker.clone()],
25056 );
25057
25058 let complete_from_position =
25059 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
25060 let replace_range =
25061 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
25062
25063 let mut request =
25064 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
25065 let completions = completions.clone();
25066 counter.fetch_add(1, atomic::Ordering::Release);
25067 async move {
25068 assert_eq!(params.text_document_position.text_document.uri, url.clone());
25069 assert_eq!(
25070 params.text_document_position.position,
25071 complete_from_position
25072 );
25073 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
25074 is_incomplete,
25075 item_defaults: None,
25076 items: completions
25077 .iter()
25078 .map(|completion_text| lsp::CompletionItem {
25079 label: completion_text.to_string(),
25080 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
25081 range: replace_range,
25082 new_text: completion_text.to_string(),
25083 })),
25084 ..Default::default()
25085 })
25086 .collect(),
25087 })))
25088 }
25089 });
25090
25091 async move {
25092 request.next().await;
25093 }
25094}
25095
25096/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
25097/// given instead, which also contains an `insert` range.
25098///
25099/// This function uses markers to define ranges:
25100/// - `|` marks the cursor position
25101/// - `<>` marks the replace range
25102/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
25103pub fn handle_completion_request_with_insert_and_replace(
25104 cx: &mut EditorLspTestContext,
25105 marked_string: &str,
25106 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
25107 counter: Arc<AtomicUsize>,
25108) -> impl Future<Output = ()> {
25109 let complete_from_marker: TextRangeMarker = '|'.into();
25110 let replace_range_marker: TextRangeMarker = ('<', '>').into();
25111 let insert_range_marker: TextRangeMarker = ('{', '}').into();
25112
25113 let (_, mut marked_ranges) = marked_text_ranges_by(
25114 marked_string,
25115 vec![
25116 complete_from_marker.clone(),
25117 replace_range_marker.clone(),
25118 insert_range_marker.clone(),
25119 ],
25120 );
25121
25122 let complete_from_position =
25123 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
25124 let replace_range =
25125 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
25126
25127 let insert_range = match marked_ranges.remove(&insert_range_marker) {
25128 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
25129 _ => lsp::Range {
25130 start: replace_range.start,
25131 end: complete_from_position,
25132 },
25133 };
25134
25135 let mut request =
25136 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
25137 let completions = completions.clone();
25138 counter.fetch_add(1, atomic::Ordering::Release);
25139 async move {
25140 assert_eq!(params.text_document_position.text_document.uri, url.clone());
25141 assert_eq!(
25142 params.text_document_position.position, complete_from_position,
25143 "marker `|` position doesn't match",
25144 );
25145 Ok(Some(lsp::CompletionResponse::Array(
25146 completions
25147 .iter()
25148 .map(|(label, new_text)| lsp::CompletionItem {
25149 label: label.to_string(),
25150 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
25151 lsp::InsertReplaceEdit {
25152 insert: insert_range,
25153 replace: replace_range,
25154 new_text: new_text.to_string(),
25155 },
25156 )),
25157 ..Default::default()
25158 })
25159 .collect(),
25160 )))
25161 }
25162 });
25163
25164 async move {
25165 request.next().await;
25166 }
25167}
25168
25169fn handle_resolve_completion_request(
25170 cx: &mut EditorLspTestContext,
25171 edits: Option<Vec<(&'static str, &'static str)>>,
25172) -> impl Future<Output = ()> {
25173 let edits = edits.map(|edits| {
25174 edits
25175 .iter()
25176 .map(|(marked_string, new_text)| {
25177 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
25178 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
25179 lsp::TextEdit::new(replace_range, new_text.to_string())
25180 })
25181 .collect::<Vec<_>>()
25182 });
25183
25184 let mut request =
25185 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
25186 let edits = edits.clone();
25187 async move {
25188 Ok(lsp::CompletionItem {
25189 additional_text_edits: edits,
25190 ..Default::default()
25191 })
25192 }
25193 });
25194
25195 async move {
25196 request.next().await;
25197 }
25198}
25199
25200pub(crate) fn update_test_language_settings(
25201 cx: &mut TestAppContext,
25202 f: impl Fn(&mut AllLanguageSettingsContent),
25203) {
25204 cx.update(|cx| {
25205 SettingsStore::update_global(cx, |store, cx| {
25206 store.update_user_settings(cx, |settings| f(&mut settings.project.all_languages));
25207 });
25208 });
25209}
25210
25211pub(crate) fn update_test_project_settings(
25212 cx: &mut TestAppContext,
25213 f: impl Fn(&mut ProjectSettingsContent),
25214) {
25215 cx.update(|cx| {
25216 SettingsStore::update_global(cx, |store, cx| {
25217 store.update_user_settings(cx, |settings| f(&mut settings.project));
25218 });
25219 });
25220}
25221
25222pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
25223 cx.update(|cx| {
25224 assets::Assets.load_test_fonts(cx);
25225 let store = SettingsStore::test(cx);
25226 cx.set_global(store);
25227 theme::init(theme::LoadThemes::JustBase, cx);
25228 release_channel::init(SemanticVersion::default(), cx);
25229 client::init_settings(cx);
25230 language::init(cx);
25231 Project::init_settings(cx);
25232 workspace::init_settings(cx);
25233 crate::init(cx);
25234 });
25235 zlog::init_test();
25236 update_test_language_settings(cx, f);
25237}
25238
25239#[track_caller]
25240fn assert_hunk_revert(
25241 not_reverted_text_with_selections: &str,
25242 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
25243 expected_reverted_text_with_selections: &str,
25244 base_text: &str,
25245 cx: &mut EditorLspTestContext,
25246) {
25247 cx.set_state(not_reverted_text_with_selections);
25248 cx.set_head_text(base_text);
25249 cx.executor().run_until_parked();
25250
25251 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
25252 let snapshot = editor.snapshot(window, cx);
25253 let reverted_hunk_statuses = snapshot
25254 .buffer_snapshot
25255 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
25256 .map(|hunk| hunk.status().kind)
25257 .collect::<Vec<_>>();
25258
25259 editor.git_restore(&Default::default(), window, cx);
25260 reverted_hunk_statuses
25261 });
25262 cx.executor().run_until_parked();
25263 cx.assert_editor_state(expected_reverted_text_with_selections);
25264 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
25265}
25266
25267#[gpui::test(iterations = 10)]
25268async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
25269 init_test(cx, |_| {});
25270
25271 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
25272 let counter = diagnostic_requests.clone();
25273
25274 let fs = FakeFs::new(cx.executor());
25275 fs.insert_tree(
25276 path!("/a"),
25277 json!({
25278 "first.rs": "fn main() { let a = 5; }",
25279 "second.rs": "// Test file",
25280 }),
25281 )
25282 .await;
25283
25284 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25285 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25286 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25287
25288 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25289 language_registry.add(rust_lang());
25290 let mut fake_servers = language_registry.register_fake_lsp(
25291 "Rust",
25292 FakeLspAdapter {
25293 capabilities: lsp::ServerCapabilities {
25294 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
25295 lsp::DiagnosticOptions {
25296 identifier: None,
25297 inter_file_dependencies: true,
25298 workspace_diagnostics: true,
25299 work_done_progress_options: Default::default(),
25300 },
25301 )),
25302 ..Default::default()
25303 },
25304 ..Default::default()
25305 },
25306 );
25307
25308 let editor = workspace
25309 .update(cx, |workspace, window, cx| {
25310 workspace.open_abs_path(
25311 PathBuf::from(path!("/a/first.rs")),
25312 OpenOptions::default(),
25313 window,
25314 cx,
25315 )
25316 })
25317 .unwrap()
25318 .await
25319 .unwrap()
25320 .downcast::<Editor>()
25321 .unwrap();
25322 let fake_server = fake_servers.next().await.unwrap();
25323 let server_id = fake_server.server.server_id();
25324 let mut first_request = fake_server
25325 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
25326 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
25327 let result_id = Some(new_result_id.to_string());
25328 assert_eq!(
25329 params.text_document.uri,
25330 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25331 );
25332 async move {
25333 Ok(lsp::DocumentDiagnosticReportResult::Report(
25334 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
25335 related_documents: None,
25336 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
25337 items: Vec::new(),
25338 result_id,
25339 },
25340 }),
25341 ))
25342 }
25343 });
25344
25345 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
25346 project.update(cx, |project, cx| {
25347 let buffer_id = editor
25348 .read(cx)
25349 .buffer()
25350 .read(cx)
25351 .as_singleton()
25352 .expect("created a singleton buffer")
25353 .read(cx)
25354 .remote_id();
25355 let buffer_result_id = project
25356 .lsp_store()
25357 .read(cx)
25358 .result_id(server_id, buffer_id, cx);
25359 assert_eq!(expected, buffer_result_id);
25360 });
25361 };
25362
25363 ensure_result_id(None, cx);
25364 cx.executor().advance_clock(Duration::from_millis(60));
25365 cx.executor().run_until_parked();
25366 assert_eq!(
25367 diagnostic_requests.load(atomic::Ordering::Acquire),
25368 1,
25369 "Opening file should trigger diagnostic request"
25370 );
25371 first_request
25372 .next()
25373 .await
25374 .expect("should have sent the first diagnostics pull request");
25375 ensure_result_id(Some("1".to_string()), cx);
25376
25377 // Editing should trigger diagnostics
25378 editor.update_in(cx, |editor, window, cx| {
25379 editor.handle_input("2", window, cx)
25380 });
25381 cx.executor().advance_clock(Duration::from_millis(60));
25382 cx.executor().run_until_parked();
25383 assert_eq!(
25384 diagnostic_requests.load(atomic::Ordering::Acquire),
25385 2,
25386 "Editing should trigger diagnostic request"
25387 );
25388 ensure_result_id(Some("2".to_string()), cx);
25389
25390 // Moving cursor should not trigger diagnostic request
25391 editor.update_in(cx, |editor, window, cx| {
25392 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25393 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
25394 });
25395 });
25396 cx.executor().advance_clock(Duration::from_millis(60));
25397 cx.executor().run_until_parked();
25398 assert_eq!(
25399 diagnostic_requests.load(atomic::Ordering::Acquire),
25400 2,
25401 "Cursor movement should not trigger diagnostic request"
25402 );
25403 ensure_result_id(Some("2".to_string()), cx);
25404 // Multiple rapid edits should be debounced
25405 for _ in 0..5 {
25406 editor.update_in(cx, |editor, window, cx| {
25407 editor.handle_input("x", window, cx)
25408 });
25409 }
25410 cx.executor().advance_clock(Duration::from_millis(60));
25411 cx.executor().run_until_parked();
25412
25413 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
25414 assert!(
25415 final_requests <= 4,
25416 "Multiple rapid edits should be debounced (got {final_requests} requests)",
25417 );
25418 ensure_result_id(Some(final_requests.to_string()), cx);
25419}
25420
25421#[gpui::test]
25422async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
25423 // Regression test for issue #11671
25424 // Previously, adding a cursor after moving multiple cursors would reset
25425 // the cursor count instead of adding to the existing cursors.
25426 init_test(cx, |_| {});
25427 let mut cx = EditorTestContext::new(cx).await;
25428
25429 // Create a simple buffer with cursor at start
25430 cx.set_state(indoc! {"
25431 ˇaaaa
25432 bbbb
25433 cccc
25434 dddd
25435 eeee
25436 ffff
25437 gggg
25438 hhhh"});
25439
25440 // Add 2 cursors below (so we have 3 total)
25441 cx.update_editor(|editor, window, cx| {
25442 editor.add_selection_below(&Default::default(), window, cx);
25443 editor.add_selection_below(&Default::default(), window, cx);
25444 });
25445
25446 // Verify we have 3 cursors
25447 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
25448 assert_eq!(
25449 initial_count, 3,
25450 "Should have 3 cursors after adding 2 below"
25451 );
25452
25453 // Move down one line
25454 cx.update_editor(|editor, window, cx| {
25455 editor.move_down(&MoveDown, window, cx);
25456 });
25457
25458 // Add another cursor below
25459 cx.update_editor(|editor, window, cx| {
25460 editor.add_selection_below(&Default::default(), window, cx);
25461 });
25462
25463 // Should now have 4 cursors (3 original + 1 new)
25464 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
25465 assert_eq!(
25466 final_count, 4,
25467 "Should have 4 cursors after moving and adding another"
25468 );
25469}
25470
25471#[gpui::test(iterations = 10)]
25472async fn test_document_colors(cx: &mut TestAppContext) {
25473 let expected_color = Rgba {
25474 r: 0.33,
25475 g: 0.33,
25476 b: 0.33,
25477 a: 0.33,
25478 };
25479
25480 init_test(cx, |_| {});
25481
25482 let fs = FakeFs::new(cx.executor());
25483 fs.insert_tree(
25484 path!("/a"),
25485 json!({
25486 "first.rs": "fn main() { let a = 5; }",
25487 }),
25488 )
25489 .await;
25490
25491 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25492 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25493 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25494
25495 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25496 language_registry.add(rust_lang());
25497 let mut fake_servers = language_registry.register_fake_lsp(
25498 "Rust",
25499 FakeLspAdapter {
25500 capabilities: lsp::ServerCapabilities {
25501 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
25502 ..lsp::ServerCapabilities::default()
25503 },
25504 name: "rust-analyzer",
25505 ..FakeLspAdapter::default()
25506 },
25507 );
25508 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
25509 "Rust",
25510 FakeLspAdapter {
25511 capabilities: lsp::ServerCapabilities {
25512 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
25513 ..lsp::ServerCapabilities::default()
25514 },
25515 name: "not-rust-analyzer",
25516 ..FakeLspAdapter::default()
25517 },
25518 );
25519
25520 let editor = workspace
25521 .update(cx, |workspace, window, cx| {
25522 workspace.open_abs_path(
25523 PathBuf::from(path!("/a/first.rs")),
25524 OpenOptions::default(),
25525 window,
25526 cx,
25527 )
25528 })
25529 .unwrap()
25530 .await
25531 .unwrap()
25532 .downcast::<Editor>()
25533 .unwrap();
25534 let fake_language_server = fake_servers.next().await.unwrap();
25535 let fake_language_server_without_capabilities =
25536 fake_servers_without_capabilities.next().await.unwrap();
25537 let requests_made = Arc::new(AtomicUsize::new(0));
25538 let closure_requests_made = Arc::clone(&requests_made);
25539 let mut color_request_handle = fake_language_server
25540 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
25541 let requests_made = Arc::clone(&closure_requests_made);
25542 async move {
25543 assert_eq!(
25544 params.text_document.uri,
25545 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25546 );
25547 requests_made.fetch_add(1, atomic::Ordering::Release);
25548 Ok(vec![
25549 lsp::ColorInformation {
25550 range: lsp::Range {
25551 start: lsp::Position {
25552 line: 0,
25553 character: 0,
25554 },
25555 end: lsp::Position {
25556 line: 0,
25557 character: 1,
25558 },
25559 },
25560 color: lsp::Color {
25561 red: 0.33,
25562 green: 0.33,
25563 blue: 0.33,
25564 alpha: 0.33,
25565 },
25566 },
25567 lsp::ColorInformation {
25568 range: lsp::Range {
25569 start: lsp::Position {
25570 line: 0,
25571 character: 0,
25572 },
25573 end: lsp::Position {
25574 line: 0,
25575 character: 1,
25576 },
25577 },
25578 color: lsp::Color {
25579 red: 0.33,
25580 green: 0.33,
25581 blue: 0.33,
25582 alpha: 0.33,
25583 },
25584 },
25585 ])
25586 }
25587 });
25588
25589 let _handle = fake_language_server_without_capabilities
25590 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
25591 panic!("Should not be called");
25592 });
25593 cx.executor().advance_clock(Duration::from_millis(100));
25594 color_request_handle.next().await.unwrap();
25595 cx.run_until_parked();
25596 assert_eq!(
25597 1,
25598 requests_made.load(atomic::Ordering::Acquire),
25599 "Should query for colors once per editor open"
25600 );
25601 editor.update_in(cx, |editor, _, cx| {
25602 assert_eq!(
25603 vec![expected_color],
25604 extract_color_inlays(editor, cx),
25605 "Should have an initial inlay"
25606 );
25607 });
25608
25609 // opening another file in a split should not influence the LSP query counter
25610 workspace
25611 .update(cx, |workspace, window, cx| {
25612 assert_eq!(
25613 workspace.panes().len(),
25614 1,
25615 "Should have one pane with one editor"
25616 );
25617 workspace.move_item_to_pane_in_direction(
25618 &MoveItemToPaneInDirection {
25619 direction: SplitDirection::Right,
25620 focus: false,
25621 clone: true,
25622 },
25623 window,
25624 cx,
25625 );
25626 })
25627 .unwrap();
25628 cx.run_until_parked();
25629 workspace
25630 .update(cx, |workspace, _, cx| {
25631 let panes = workspace.panes();
25632 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
25633 for pane in panes {
25634 let editor = pane
25635 .read(cx)
25636 .active_item()
25637 .and_then(|item| item.downcast::<Editor>())
25638 .expect("Should have opened an editor in each split");
25639 let editor_file = editor
25640 .read(cx)
25641 .buffer()
25642 .read(cx)
25643 .as_singleton()
25644 .expect("test deals with singleton buffers")
25645 .read(cx)
25646 .file()
25647 .expect("test buffese should have a file")
25648 .path();
25649 assert_eq!(
25650 editor_file.as_ref(),
25651 rel_path("first.rs"),
25652 "Both editors should be opened for the same file"
25653 )
25654 }
25655 })
25656 .unwrap();
25657
25658 cx.executor().advance_clock(Duration::from_millis(500));
25659 let save = editor.update_in(cx, |editor, window, cx| {
25660 editor.move_to_end(&MoveToEnd, window, cx);
25661 editor.handle_input("dirty", window, cx);
25662 editor.save(
25663 SaveOptions {
25664 format: true,
25665 autosave: true,
25666 },
25667 project.clone(),
25668 window,
25669 cx,
25670 )
25671 });
25672 save.await.unwrap();
25673
25674 color_request_handle.next().await.unwrap();
25675 cx.run_until_parked();
25676 assert_eq!(
25677 3,
25678 requests_made.load(atomic::Ordering::Acquire),
25679 "Should query for colors once per save and once per formatting after save"
25680 );
25681
25682 drop(editor);
25683 let close = workspace
25684 .update(cx, |workspace, window, cx| {
25685 workspace.active_pane().update(cx, |pane, cx| {
25686 pane.close_active_item(&CloseActiveItem::default(), window, cx)
25687 })
25688 })
25689 .unwrap();
25690 close.await.unwrap();
25691 let close = workspace
25692 .update(cx, |workspace, window, cx| {
25693 workspace.active_pane().update(cx, |pane, cx| {
25694 pane.close_active_item(&CloseActiveItem::default(), window, cx)
25695 })
25696 })
25697 .unwrap();
25698 close.await.unwrap();
25699 assert_eq!(
25700 3,
25701 requests_made.load(atomic::Ordering::Acquire),
25702 "After saving and closing all editors, no extra requests should be made"
25703 );
25704 workspace
25705 .update(cx, |workspace, _, cx| {
25706 assert!(
25707 workspace.active_item(cx).is_none(),
25708 "Should close all editors"
25709 )
25710 })
25711 .unwrap();
25712
25713 workspace
25714 .update(cx, |workspace, window, cx| {
25715 workspace.active_pane().update(cx, |pane, cx| {
25716 pane.navigate_backward(&workspace::GoBack, window, cx);
25717 })
25718 })
25719 .unwrap();
25720 cx.executor().advance_clock(Duration::from_millis(100));
25721 cx.run_until_parked();
25722 let editor = workspace
25723 .update(cx, |workspace, _, cx| {
25724 workspace
25725 .active_item(cx)
25726 .expect("Should have reopened the editor again after navigating back")
25727 .downcast::<Editor>()
25728 .expect("Should be an editor")
25729 })
25730 .unwrap();
25731 color_request_handle.next().await.unwrap();
25732 assert_eq!(
25733 3,
25734 requests_made.load(atomic::Ordering::Acquire),
25735 "Cache should be reused on buffer close and reopen"
25736 );
25737 editor.update(cx, |editor, cx| {
25738 assert_eq!(
25739 vec![expected_color],
25740 extract_color_inlays(editor, cx),
25741 "Should have an initial inlay"
25742 );
25743 });
25744
25745 drop(color_request_handle);
25746 let closure_requests_made = Arc::clone(&requests_made);
25747 let mut empty_color_request_handle = fake_language_server
25748 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
25749 let requests_made = Arc::clone(&closure_requests_made);
25750 async move {
25751 assert_eq!(
25752 params.text_document.uri,
25753 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25754 );
25755 requests_made.fetch_add(1, atomic::Ordering::Release);
25756 Ok(Vec::new())
25757 }
25758 });
25759 let save = editor.update_in(cx, |editor, window, cx| {
25760 editor.move_to_end(&MoveToEnd, window, cx);
25761 editor.handle_input("dirty_again", window, cx);
25762 editor.save(
25763 SaveOptions {
25764 format: false,
25765 autosave: true,
25766 },
25767 project.clone(),
25768 window,
25769 cx,
25770 )
25771 });
25772 save.await.unwrap();
25773
25774 empty_color_request_handle.next().await.unwrap();
25775 cx.run_until_parked();
25776 assert_eq!(
25777 4,
25778 requests_made.load(atomic::Ordering::Acquire),
25779 "Should query for colors once per save only, as formatting was not requested"
25780 );
25781 editor.update(cx, |editor, cx| {
25782 assert_eq!(
25783 Vec::<Rgba>::new(),
25784 extract_color_inlays(editor, cx),
25785 "Should clear all colors when the server returns an empty response"
25786 );
25787 });
25788}
25789
25790#[gpui::test]
25791async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
25792 init_test(cx, |_| {});
25793 let (editor, cx) = cx.add_window_view(Editor::single_line);
25794 editor.update_in(cx, |editor, window, cx| {
25795 editor.set_text("oops\n\nwow\n", window, cx)
25796 });
25797 cx.run_until_parked();
25798 editor.update(cx, |editor, cx| {
25799 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
25800 });
25801 editor.update(cx, |editor, cx| editor.edit([(3..5, "")], cx));
25802 cx.run_until_parked();
25803 editor.update(cx, |editor, cx| {
25804 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
25805 });
25806}
25807
25808#[gpui::test]
25809async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
25810 init_test(cx, |_| {});
25811
25812 cx.update(|cx| {
25813 register_project_item::<Editor>(cx);
25814 });
25815
25816 let fs = FakeFs::new(cx.executor());
25817 fs.insert_tree("/root1", json!({})).await;
25818 fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
25819 .await;
25820
25821 let project = Project::test(fs, ["/root1".as_ref()], cx).await;
25822 let (workspace, cx) =
25823 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
25824
25825 let worktree_id = project.update(cx, |project, cx| {
25826 project.worktrees(cx).next().unwrap().read(cx).id()
25827 });
25828
25829 let handle = workspace
25830 .update_in(cx, |workspace, window, cx| {
25831 let project_path = (worktree_id, rel_path("one.pdf"));
25832 workspace.open_path(project_path, None, true, window, cx)
25833 })
25834 .await
25835 .unwrap();
25836
25837 assert_eq!(
25838 handle.to_any().entity_type(),
25839 TypeId::of::<InvalidBufferView>()
25840 );
25841}
25842
25843#[gpui::test]
25844async fn test_select_next_prev_syntax_node(cx: &mut TestAppContext) {
25845 init_test(cx, |_| {});
25846
25847 let language = Arc::new(Language::new(
25848 LanguageConfig::default(),
25849 Some(tree_sitter_rust::LANGUAGE.into()),
25850 ));
25851
25852 // Test hierarchical sibling navigation
25853 let text = r#"
25854 fn outer() {
25855 if condition {
25856 let a = 1;
25857 }
25858 let b = 2;
25859 }
25860
25861 fn another() {
25862 let c = 3;
25863 }
25864 "#;
25865
25866 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
25867 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
25868 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
25869
25870 // Wait for parsing to complete
25871 editor
25872 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
25873 .await;
25874
25875 editor.update_in(cx, |editor, window, cx| {
25876 // Start by selecting "let a = 1;" inside the if block
25877 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25878 s.select_display_ranges([
25879 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 26)
25880 ]);
25881 });
25882
25883 let initial_selection = editor.selections.display_ranges(cx);
25884 assert_eq!(initial_selection.len(), 1, "Should have one selection");
25885
25886 // Test select next sibling - should move up levels to find the next sibling
25887 // Since "let a = 1;" has no siblings in the if block, it should move up
25888 // to find "let b = 2;" which is a sibling of the if block
25889 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
25890 let next_selection = editor.selections.display_ranges(cx);
25891
25892 // Should have a selection and it should be different from the initial
25893 assert_eq!(
25894 next_selection.len(),
25895 1,
25896 "Should have one selection after next"
25897 );
25898 assert_ne!(
25899 next_selection[0], initial_selection[0],
25900 "Next sibling selection should be different"
25901 );
25902
25903 // Test hierarchical navigation by going to the end of the current function
25904 // and trying to navigate to the next function
25905 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25906 s.select_display_ranges([
25907 DisplayPoint::new(DisplayRow(5), 12)..DisplayPoint::new(DisplayRow(5), 22)
25908 ]);
25909 });
25910
25911 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
25912 let function_next_selection = editor.selections.display_ranges(cx);
25913
25914 // Should move to the next function
25915 assert_eq!(
25916 function_next_selection.len(),
25917 1,
25918 "Should have one selection after function next"
25919 );
25920
25921 // Test select previous sibling navigation
25922 editor.select_prev_syntax_node(&SelectPreviousSyntaxNode, window, cx);
25923 let prev_selection = editor.selections.display_ranges(cx);
25924
25925 // Should have a selection and it should be different
25926 assert_eq!(
25927 prev_selection.len(),
25928 1,
25929 "Should have one selection after prev"
25930 );
25931 assert_ne!(
25932 prev_selection[0], function_next_selection[0],
25933 "Previous sibling selection should be different from next"
25934 );
25935 });
25936}
25937
25938#[gpui::test]
25939async fn test_next_prev_document_highlight(cx: &mut TestAppContext) {
25940 init_test(cx, |_| {});
25941
25942 let mut cx = EditorTestContext::new(cx).await;
25943 cx.set_state(
25944 "let ˇvariable = 42;
25945let another = variable + 1;
25946let result = variable * 2;",
25947 );
25948
25949 // Set up document highlights manually (simulating LSP response)
25950 cx.update_editor(|editor, _window, cx| {
25951 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
25952
25953 // Create highlights for "variable" occurrences
25954 let highlight_ranges = [
25955 Point::new(0, 4)..Point::new(0, 12), // First "variable"
25956 Point::new(1, 14)..Point::new(1, 22), // Second "variable"
25957 Point::new(2, 13)..Point::new(2, 21), // Third "variable"
25958 ];
25959
25960 let anchor_ranges: Vec<_> = highlight_ranges
25961 .iter()
25962 .map(|range| range.clone().to_anchors(&buffer_snapshot))
25963 .collect();
25964
25965 editor.highlight_background::<DocumentHighlightRead>(
25966 &anchor_ranges,
25967 |theme| theme.colors().editor_document_highlight_read_background,
25968 cx,
25969 );
25970 });
25971
25972 // Go to next highlight - should move to second "variable"
25973 cx.update_editor(|editor, window, cx| {
25974 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
25975 });
25976 cx.assert_editor_state(
25977 "let variable = 42;
25978let another = ˇvariable + 1;
25979let result = variable * 2;",
25980 );
25981
25982 // Go to next highlight - should move to third "variable"
25983 cx.update_editor(|editor, window, cx| {
25984 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
25985 });
25986 cx.assert_editor_state(
25987 "let variable = 42;
25988let another = variable + 1;
25989let result = ˇvariable * 2;",
25990 );
25991
25992 // Go to next highlight - should stay at third "variable" (no wrap-around)
25993 cx.update_editor(|editor, window, cx| {
25994 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
25995 });
25996 cx.assert_editor_state(
25997 "let variable = 42;
25998let another = variable + 1;
25999let result = ˇvariable * 2;",
26000 );
26001
26002 // Now test going backwards from third position
26003 cx.update_editor(|editor, window, cx| {
26004 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26005 });
26006 cx.assert_editor_state(
26007 "let variable = 42;
26008let another = ˇvariable + 1;
26009let result = variable * 2;",
26010 );
26011
26012 // Go to previous highlight - should move to first "variable"
26013 cx.update_editor(|editor, window, cx| {
26014 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26015 });
26016 cx.assert_editor_state(
26017 "let ˇvariable = 42;
26018let another = variable + 1;
26019let result = variable * 2;",
26020 );
26021
26022 // Go to previous highlight - should stay on first "variable"
26023 cx.update_editor(|editor, window, cx| {
26024 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26025 });
26026 cx.assert_editor_state(
26027 "let ˇvariable = 42;
26028let another = variable + 1;
26029let result = variable * 2;",
26030 );
26031}
26032
26033#[gpui::test]
26034async fn test_paste_url_from_other_app_creates_markdown_link_over_selected_text(
26035 cx: &mut gpui::TestAppContext,
26036) {
26037 init_test(cx, |_| {});
26038
26039 let url = "https://zed.dev";
26040
26041 let markdown_language = Arc::new(Language::new(
26042 LanguageConfig {
26043 name: "Markdown".into(),
26044 ..LanguageConfig::default()
26045 },
26046 None,
26047 ));
26048
26049 let mut cx = EditorTestContext::new(cx).await;
26050 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26051 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)");
26052
26053 cx.update_editor(|editor, window, cx| {
26054 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26055 editor.paste(&Paste, window, cx);
26056 });
26057
26058 cx.assert_editor_state(&format!(
26059 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)"
26060 ));
26061}
26062
26063#[gpui::test]
26064async fn test_paste_url_from_zed_copy_creates_markdown_link_over_selected_text(
26065 cx: &mut gpui::TestAppContext,
26066) {
26067 init_test(cx, |_| {});
26068
26069 let url = "https://zed.dev";
26070
26071 let markdown_language = Arc::new(Language::new(
26072 LanguageConfig {
26073 name: "Markdown".into(),
26074 ..LanguageConfig::default()
26075 },
26076 None,
26077 ));
26078
26079 let mut cx = EditorTestContext::new(cx).await;
26080 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26081 cx.set_state(&format!(
26082 "Hello, editor.\nZed is great (see this link: )\n«{url}ˇ»"
26083 ));
26084
26085 cx.update_editor(|editor, window, cx| {
26086 editor.copy(&Copy, window, cx);
26087 });
26088
26089 cx.set_state(&format!(
26090 "Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)\n{url}"
26091 ));
26092
26093 cx.update_editor(|editor, window, cx| {
26094 editor.paste(&Paste, window, cx);
26095 });
26096
26097 cx.assert_editor_state(&format!(
26098 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)\n{url}"
26099 ));
26100}
26101
26102#[gpui::test]
26103async fn test_paste_url_from_other_app_replaces_existing_url_without_creating_markdown_link(
26104 cx: &mut gpui::TestAppContext,
26105) {
26106 init_test(cx, |_| {});
26107
26108 let url = "https://zed.dev";
26109
26110 let markdown_language = Arc::new(Language::new(
26111 LanguageConfig {
26112 name: "Markdown".into(),
26113 ..LanguageConfig::default()
26114 },
26115 None,
26116 ));
26117
26118 let mut cx = EditorTestContext::new(cx).await;
26119 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26120 cx.set_state("Please visit zed's homepage: «https://www.apple.comˇ»");
26121
26122 cx.update_editor(|editor, window, cx| {
26123 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26124 editor.paste(&Paste, window, cx);
26125 });
26126
26127 cx.assert_editor_state(&format!("Please visit zed's homepage: {url}ˇ"));
26128}
26129
26130#[gpui::test]
26131async fn test_paste_plain_text_from_other_app_replaces_selection_without_creating_markdown_link(
26132 cx: &mut gpui::TestAppContext,
26133) {
26134 init_test(cx, |_| {});
26135
26136 let text = "Awesome";
26137
26138 let markdown_language = Arc::new(Language::new(
26139 LanguageConfig {
26140 name: "Markdown".into(),
26141 ..LanguageConfig::default()
26142 },
26143 None,
26144 ));
26145
26146 let mut cx = EditorTestContext::new(cx).await;
26147 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26148 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat»");
26149
26150 cx.update_editor(|editor, window, cx| {
26151 cx.write_to_clipboard(ClipboardItem::new_string(text.to_string()));
26152 editor.paste(&Paste, window, cx);
26153 });
26154
26155 cx.assert_editor_state(&format!("Hello, {text}ˇ.\nZed is {text}ˇ"));
26156}
26157
26158#[gpui::test]
26159async fn test_paste_url_from_other_app_without_creating_markdown_link_in_non_markdown_language(
26160 cx: &mut gpui::TestAppContext,
26161) {
26162 init_test(cx, |_| {});
26163
26164 let url = "https://zed.dev";
26165
26166 let markdown_language = Arc::new(Language::new(
26167 LanguageConfig {
26168 name: "Rust".into(),
26169 ..LanguageConfig::default()
26170 },
26171 None,
26172 ));
26173
26174 let mut cx = EditorTestContext::new(cx).await;
26175 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26176 cx.set_state("// Hello, «editorˇ».\n// Zed is «ˇgreat» (see this link: ˇ)");
26177
26178 cx.update_editor(|editor, window, cx| {
26179 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26180 editor.paste(&Paste, window, cx);
26181 });
26182
26183 cx.assert_editor_state(&format!(
26184 "// Hello, {url}ˇ.\n// Zed is {url}ˇ (see this link: {url}ˇ)"
26185 ));
26186}
26187
26188#[gpui::test]
26189async fn test_paste_url_from_other_app_creates_markdown_link_selectively_in_multi_buffer(
26190 cx: &mut TestAppContext,
26191) {
26192 init_test(cx, |_| {});
26193
26194 let url = "https://zed.dev";
26195
26196 let markdown_language = Arc::new(Language::new(
26197 LanguageConfig {
26198 name: "Markdown".into(),
26199 ..LanguageConfig::default()
26200 },
26201 None,
26202 ));
26203
26204 let (editor, cx) = cx.add_window_view(|window, cx| {
26205 let multi_buffer = MultiBuffer::build_multi(
26206 [
26207 ("this will embed -> link", vec![Point::row_range(0..1)]),
26208 ("this will replace -> link", vec![Point::row_range(0..1)]),
26209 ],
26210 cx,
26211 );
26212 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
26213 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26214 s.select_ranges(vec![
26215 Point::new(0, 19)..Point::new(0, 23),
26216 Point::new(1, 21)..Point::new(1, 25),
26217 ])
26218 });
26219 let first_buffer_id = multi_buffer
26220 .read(cx)
26221 .excerpt_buffer_ids()
26222 .into_iter()
26223 .next()
26224 .unwrap();
26225 let first_buffer = multi_buffer.read(cx).buffer(first_buffer_id).unwrap();
26226 first_buffer.update(cx, |buffer, cx| {
26227 buffer.set_language(Some(markdown_language.clone()), cx);
26228 });
26229
26230 editor
26231 });
26232 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
26233
26234 cx.update_editor(|editor, window, cx| {
26235 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26236 editor.paste(&Paste, window, cx);
26237 });
26238
26239 cx.assert_editor_state(&format!(
26240 "this will embed -> [link]({url})ˇ\nthis will replace -> {url}ˇ"
26241 ));
26242}
26243
26244#[track_caller]
26245fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
26246 editor
26247 .all_inlays(cx)
26248 .into_iter()
26249 .filter_map(|inlay| inlay.get_color())
26250 .map(Rgba::from)
26251 .collect()
26252}