1use super::*;
2use crate::{
3 JoinLines,
4 linked_editing_ranges::LinkedEditingRanges,
5 scroll::scroll_amount::ScrollAmount,
6 test::{
7 assert_text_with_selections, build_editor,
8 editor_lsp_test_context::{EditorLspTestContext, git_commit_lang},
9 editor_test_context::EditorTestContext,
10 select_ranges,
11 },
12};
13use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
14use futures::StreamExt;
15use gpui::{
16 BackgroundExecutor, DismissEvent, SemanticVersion, TestAppContext, UpdateGlobal,
17 VisualTestContext, WindowBounds, WindowOptions, div,
18};
19use indoc::indoc;
20use language::{
21 BracketPairConfig,
22 Capability::ReadWrite,
23 FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher, LanguageName,
24 Override, Point,
25 language_settings::{
26 AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings,
27 LanguageSettingsContent, LspInsertMode, PrettierSettings,
28 },
29 tree_sitter_python,
30};
31use language_settings::{Formatter, FormatterList, IndentGuideSettings};
32use lsp::CompletionParams;
33use multi_buffer::{IndentGuide, PathKey};
34use parking_lot::Mutex;
35use pretty_assertions::{assert_eq, assert_ne};
36use project::{
37 FakeFs,
38 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
39 project_settings::{LspSettings, ProjectSettings},
40};
41use serde_json::{self, json};
42use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
43use std::{
44 iter,
45 sync::atomic::{self, AtomicUsize},
46};
47use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
48use text::ToPoint as _;
49use unindent::Unindent;
50use util::{
51 assert_set_eq, path,
52 test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
53 uri,
54};
55use workspace::{
56 CloseActiveItem, CloseAllItems, CloseInactiveItems, NavigationEntry, OpenOptions, ViewId,
57 item::{FollowEvent, FollowableItem, Item, ItemHandle},
58};
59
60#[gpui::test]
61fn test_edit_events(cx: &mut TestAppContext) {
62 init_test(cx, |_| {});
63
64 let buffer = cx.new(|cx| {
65 let mut buffer = language::Buffer::local("123456", cx);
66 buffer.set_group_interval(Duration::from_secs(1));
67 buffer
68 });
69
70 let events = Rc::new(RefCell::new(Vec::new()));
71 let editor1 = cx.add_window({
72 let events = events.clone();
73 |window, cx| {
74 let entity = cx.entity().clone();
75 cx.subscribe_in(
76 &entity,
77 window,
78 move |_, _, event: &EditorEvent, _, _| match event {
79 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
80 EditorEvent::BufferEdited => {
81 events.borrow_mut().push(("editor1", "buffer edited"))
82 }
83 _ => {}
84 },
85 )
86 .detach();
87 Editor::for_buffer(buffer.clone(), None, window, cx)
88 }
89 });
90
91 let editor2 = cx.add_window({
92 let events = events.clone();
93 |window, cx| {
94 cx.subscribe_in(
95 &cx.entity().clone(),
96 window,
97 move |_, _, event: &EditorEvent, _, _| match event {
98 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
99 EditorEvent::BufferEdited => {
100 events.borrow_mut().push(("editor2", "buffer edited"))
101 }
102 _ => {}
103 },
104 )
105 .detach();
106 Editor::for_buffer(buffer.clone(), None, window, cx)
107 }
108 });
109
110 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
111
112 // Mutating editor 1 will emit an `Edited` event only for that editor.
113 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
114 assert_eq!(
115 mem::take(&mut *events.borrow_mut()),
116 [
117 ("editor1", "edited"),
118 ("editor1", "buffer edited"),
119 ("editor2", "buffer edited"),
120 ]
121 );
122
123 // Mutating editor 2 will emit an `Edited` event only for that editor.
124 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
125 assert_eq!(
126 mem::take(&mut *events.borrow_mut()),
127 [
128 ("editor2", "edited"),
129 ("editor1", "buffer edited"),
130 ("editor2", "buffer edited"),
131 ]
132 );
133
134 // Undoing on editor 1 will emit an `Edited` event only for that editor.
135 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
136 assert_eq!(
137 mem::take(&mut *events.borrow_mut()),
138 [
139 ("editor1", "edited"),
140 ("editor1", "buffer edited"),
141 ("editor2", "buffer edited"),
142 ]
143 );
144
145 // Redoing on editor 1 will emit an `Edited` event only for that editor.
146 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, 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 // Undoing on editor 2 will emit an `Edited` event only for that editor.
157 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
158 assert_eq!(
159 mem::take(&mut *events.borrow_mut()),
160 [
161 ("editor2", "edited"),
162 ("editor1", "buffer edited"),
163 ("editor2", "buffer edited"),
164 ]
165 );
166
167 // Redoing on editor 2 will emit an `Edited` event only for that editor.
168 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, 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 // No event is emitted when the mutation is a no-op.
179 _ = editor2.update(cx, |editor, window, cx| {
180 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
181
182 editor.backspace(&Backspace, window, cx);
183 });
184 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
185}
186
187#[gpui::test]
188fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
189 init_test(cx, |_| {});
190
191 let mut now = Instant::now();
192 let group_interval = Duration::from_millis(1);
193 let buffer = cx.new(|cx| {
194 let mut buf = language::Buffer::local("123456", cx);
195 buf.set_group_interval(group_interval);
196 buf
197 });
198 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
199 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
200
201 _ = editor.update(cx, |editor, window, cx| {
202 editor.start_transaction_at(now, window, cx);
203 editor.change_selections(None, window, cx, |s| s.select_ranges([2..4]));
204
205 editor.insert("cd", window, cx);
206 editor.end_transaction_at(now, cx);
207 assert_eq!(editor.text(cx), "12cd56");
208 assert_eq!(editor.selections.ranges(cx), vec![4..4]);
209
210 editor.start_transaction_at(now, window, cx);
211 editor.change_selections(None, window, cx, |s| s.select_ranges([4..5]));
212 editor.insert("e", window, cx);
213 editor.end_transaction_at(now, cx);
214 assert_eq!(editor.text(cx), "12cde6");
215 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
216
217 now += group_interval + Duration::from_millis(1);
218 editor.change_selections(None, window, cx, |s| s.select_ranges([2..2]));
219
220 // Simulate an edit in another editor
221 buffer.update(cx, |buffer, cx| {
222 buffer.start_transaction_at(now, cx);
223 buffer.edit([(0..1, "a")], None, cx);
224 buffer.edit([(1..1, "b")], None, cx);
225 buffer.end_transaction_at(now, cx);
226 });
227
228 assert_eq!(editor.text(cx), "ab2cde6");
229 assert_eq!(editor.selections.ranges(cx), vec![3..3]);
230
231 // Last transaction happened past the group interval in a different editor.
232 // Undo it individually and don't restore selections.
233 editor.undo(&Undo, window, cx);
234 assert_eq!(editor.text(cx), "12cde6");
235 assert_eq!(editor.selections.ranges(cx), vec![2..2]);
236
237 // First two transactions happened within the group interval in this editor.
238 // Undo them together and restore selections.
239 editor.undo(&Undo, window, cx);
240 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
241 assert_eq!(editor.text(cx), "123456");
242 assert_eq!(editor.selections.ranges(cx), vec![0..0]);
243
244 // Redo the first two transactions together.
245 editor.redo(&Redo, window, cx);
246 assert_eq!(editor.text(cx), "12cde6");
247 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
248
249 // Redo the last transaction on its own.
250 editor.redo(&Redo, window, cx);
251 assert_eq!(editor.text(cx), "ab2cde6");
252 assert_eq!(editor.selections.ranges(cx), vec![6..6]);
253
254 // Test empty transactions.
255 editor.start_transaction_at(now, window, cx);
256 editor.end_transaction_at(now, cx);
257 editor.undo(&Undo, window, cx);
258 assert_eq!(editor.text(cx), "12cde6");
259 });
260}
261
262#[gpui::test]
263fn test_ime_composition(cx: &mut TestAppContext) {
264 init_test(cx, |_| {});
265
266 let buffer = cx.new(|cx| {
267 let mut buffer = language::Buffer::local("abcde", cx);
268 // Ensure automatic grouping doesn't occur.
269 buffer.set_group_interval(Duration::ZERO);
270 buffer
271 });
272
273 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
274 cx.add_window(|window, cx| {
275 let mut editor = build_editor(buffer.clone(), window, cx);
276
277 // Start a new IME composition.
278 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
279 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
280 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
281 assert_eq!(editor.text(cx), "äbcde");
282 assert_eq!(
283 editor.marked_text_ranges(cx),
284 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
285 );
286
287 // Finalize IME composition.
288 editor.replace_text_in_range(None, "ā", window, cx);
289 assert_eq!(editor.text(cx), "ābcde");
290 assert_eq!(editor.marked_text_ranges(cx), None);
291
292 // IME composition edits are grouped and are undone/redone at once.
293 editor.undo(&Default::default(), window, cx);
294 assert_eq!(editor.text(cx), "abcde");
295 assert_eq!(editor.marked_text_ranges(cx), None);
296 editor.redo(&Default::default(), window, cx);
297 assert_eq!(editor.text(cx), "ābcde");
298 assert_eq!(editor.marked_text_ranges(cx), None);
299
300 // Start a new IME composition.
301 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
302 assert_eq!(
303 editor.marked_text_ranges(cx),
304 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
305 );
306
307 // Undoing during an IME composition cancels it.
308 editor.undo(&Default::default(), window, cx);
309 assert_eq!(editor.text(cx), "ābcde");
310 assert_eq!(editor.marked_text_ranges(cx), None);
311
312 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
313 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
314 assert_eq!(editor.text(cx), "ābcdè");
315 assert_eq!(
316 editor.marked_text_ranges(cx),
317 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
318 );
319
320 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
321 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
322 assert_eq!(editor.text(cx), "ābcdę");
323 assert_eq!(editor.marked_text_ranges(cx), None);
324
325 // Start a new IME composition with multiple cursors.
326 editor.change_selections(None, window, cx, |s| {
327 s.select_ranges([
328 OffsetUtf16(1)..OffsetUtf16(1),
329 OffsetUtf16(3)..OffsetUtf16(3),
330 OffsetUtf16(5)..OffsetUtf16(5),
331 ])
332 });
333 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
334 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
335 assert_eq!(
336 editor.marked_text_ranges(cx),
337 Some(vec![
338 OffsetUtf16(0)..OffsetUtf16(3),
339 OffsetUtf16(4)..OffsetUtf16(7),
340 OffsetUtf16(8)..OffsetUtf16(11)
341 ])
342 );
343
344 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
345 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
346 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
347 assert_eq!(
348 editor.marked_text_ranges(cx),
349 Some(vec![
350 OffsetUtf16(1)..OffsetUtf16(2),
351 OffsetUtf16(5)..OffsetUtf16(6),
352 OffsetUtf16(9)..OffsetUtf16(10)
353 ])
354 );
355
356 // Finalize IME composition with multiple cursors.
357 editor.replace_text_in_range(Some(9..10), "2", window, cx);
358 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
359 assert_eq!(editor.marked_text_ranges(cx), None);
360
361 editor
362 });
363}
364
365#[gpui::test]
366fn test_selection_with_mouse(cx: &mut TestAppContext) {
367 init_test(cx, |_| {});
368
369 let editor = cx.add_window(|window, cx| {
370 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
371 build_editor(buffer, window, cx)
372 });
373
374 _ = editor.update(cx, |editor, window, cx| {
375 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
376 });
377 assert_eq!(
378 editor
379 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
380 .unwrap(),
381 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
382 );
383
384 _ = editor.update(cx, |editor, window, cx| {
385 editor.update_selection(
386 DisplayPoint::new(DisplayRow(3), 3),
387 0,
388 gpui::Point::<f32>::default(),
389 window,
390 cx,
391 );
392 });
393
394 assert_eq!(
395 editor
396 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
397 .unwrap(),
398 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
399 );
400
401 _ = editor.update(cx, |editor, window, cx| {
402 editor.update_selection(
403 DisplayPoint::new(DisplayRow(1), 1),
404 0,
405 gpui::Point::<f32>::default(),
406 window,
407 cx,
408 );
409 });
410
411 assert_eq!(
412 editor
413 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
414 .unwrap(),
415 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
416 );
417
418 _ = editor.update(cx, |editor, window, cx| {
419 editor.end_selection(window, cx);
420 editor.update_selection(
421 DisplayPoint::new(DisplayRow(3), 3),
422 0,
423 gpui::Point::<f32>::default(),
424 window,
425 cx,
426 );
427 });
428
429 assert_eq!(
430 editor
431 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
432 .unwrap(),
433 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
434 );
435
436 _ = editor.update(cx, |editor, window, cx| {
437 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
438 editor.update_selection(
439 DisplayPoint::new(DisplayRow(0), 0),
440 0,
441 gpui::Point::<f32>::default(),
442 window,
443 cx,
444 );
445 });
446
447 assert_eq!(
448 editor
449 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
450 .unwrap(),
451 [
452 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
453 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
454 ]
455 );
456
457 _ = editor.update(cx, |editor, window, cx| {
458 editor.end_selection(window, cx);
459 });
460
461 assert_eq!(
462 editor
463 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
464 .unwrap(),
465 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
466 );
467}
468
469#[gpui::test]
470fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
471 init_test(cx, |_| {});
472
473 let editor = cx.add_window(|window, cx| {
474 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
475 build_editor(buffer, window, cx)
476 });
477
478 _ = editor.update(cx, |editor, window, cx| {
479 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
480 });
481
482 _ = editor.update(cx, |editor, window, cx| {
483 editor.end_selection(window, cx);
484 });
485
486 _ = editor.update(cx, |editor, window, cx| {
487 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
488 });
489
490 _ = editor.update(cx, |editor, window, cx| {
491 editor.end_selection(window, cx);
492 });
493
494 assert_eq!(
495 editor
496 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
497 .unwrap(),
498 [
499 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
500 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
501 ]
502 );
503
504 _ = editor.update(cx, |editor, window, cx| {
505 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
506 });
507
508 _ = editor.update(cx, |editor, window, cx| {
509 editor.end_selection(window, cx);
510 });
511
512 assert_eq!(
513 editor
514 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
515 .unwrap(),
516 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
517 );
518}
519
520#[gpui::test]
521fn test_canceling_pending_selection(cx: &mut TestAppContext) {
522 init_test(cx, |_| {});
523
524 let editor = cx.add_window(|window, cx| {
525 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
526 build_editor(buffer, window, cx)
527 });
528
529 _ = editor.update(cx, |editor, window, cx| {
530 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
531 assert_eq!(
532 editor.selections.display_ranges(cx),
533 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
534 );
535 });
536
537 _ = editor.update(cx, |editor, window, cx| {
538 editor.update_selection(
539 DisplayPoint::new(DisplayRow(3), 3),
540 0,
541 gpui::Point::<f32>::default(),
542 window,
543 cx,
544 );
545 assert_eq!(
546 editor.selections.display_ranges(cx),
547 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
548 );
549 });
550
551 _ = editor.update(cx, |editor, window, cx| {
552 editor.cancel(&Cancel, window, cx);
553 editor.update_selection(
554 DisplayPoint::new(DisplayRow(1), 1),
555 0,
556 gpui::Point::<f32>::default(),
557 window,
558 cx,
559 );
560 assert_eq!(
561 editor.selections.display_ranges(cx),
562 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
563 );
564 });
565}
566
567#[gpui::test]
568fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
569 init_test(cx, |_| {});
570
571 let editor = cx.add_window(|window, cx| {
572 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
573 build_editor(buffer, window, cx)
574 });
575
576 _ = editor.update(cx, |editor, window, cx| {
577 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
578 assert_eq!(
579 editor.selections.display_ranges(cx),
580 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
581 );
582
583 editor.move_down(&Default::default(), window, cx);
584 assert_eq!(
585 editor.selections.display_ranges(cx),
586 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
587 );
588
589 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
590 assert_eq!(
591 editor.selections.display_ranges(cx),
592 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
593 );
594
595 editor.move_up(&Default::default(), window, cx);
596 assert_eq!(
597 editor.selections.display_ranges(cx),
598 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
599 );
600 });
601}
602
603#[gpui::test]
604fn test_clone(cx: &mut TestAppContext) {
605 init_test(cx, |_| {});
606
607 let (text, selection_ranges) = marked_text_ranges(
608 indoc! {"
609 one
610 two
611 threeˇ
612 four
613 fiveˇ
614 "},
615 true,
616 );
617
618 let editor = cx.add_window(|window, cx| {
619 let buffer = MultiBuffer::build_simple(&text, cx);
620 build_editor(buffer, window, cx)
621 });
622
623 _ = editor.update(cx, |editor, window, cx| {
624 editor.change_selections(None, window, cx, |s| {
625 s.select_ranges(selection_ranges.clone())
626 });
627 editor.fold_creases(
628 vec![
629 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
630 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
631 ],
632 true,
633 window,
634 cx,
635 );
636 });
637
638 let cloned_editor = editor
639 .update(cx, |editor, _, cx| {
640 cx.open_window(Default::default(), |window, cx| {
641 cx.new(|cx| editor.clone(window, cx))
642 })
643 })
644 .unwrap()
645 .unwrap();
646
647 let snapshot = editor
648 .update(cx, |e, window, cx| e.snapshot(window, cx))
649 .unwrap();
650 let cloned_snapshot = cloned_editor
651 .update(cx, |e, window, cx| e.snapshot(window, cx))
652 .unwrap();
653
654 assert_eq!(
655 cloned_editor
656 .update(cx, |e, _, cx| e.display_text(cx))
657 .unwrap(),
658 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
659 );
660 assert_eq!(
661 cloned_snapshot
662 .folds_in_range(0..text.len())
663 .collect::<Vec<_>>(),
664 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
665 );
666 assert_set_eq!(
667 cloned_editor
668 .update(cx, |editor, _, cx| editor.selections.ranges::<Point>(cx))
669 .unwrap(),
670 editor
671 .update(cx, |editor, _, cx| editor.selections.ranges(cx))
672 .unwrap()
673 );
674 assert_set_eq!(
675 cloned_editor
676 .update(cx, |e, _window, cx| e.selections.display_ranges(cx))
677 .unwrap(),
678 editor
679 .update(cx, |e, _, cx| e.selections.display_ranges(cx))
680 .unwrap()
681 );
682}
683
684#[gpui::test]
685async fn test_navigation_history(cx: &mut TestAppContext) {
686 init_test(cx, |_| {});
687
688 use workspace::item::Item;
689
690 let fs = FakeFs::new(cx.executor());
691 let project = Project::test(fs, [], cx).await;
692 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
693 let pane = workspace
694 .update(cx, |workspace, _, _| workspace.active_pane().clone())
695 .unwrap();
696
697 _ = workspace.update(cx, |_v, window, cx| {
698 cx.new(|cx| {
699 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
700 let mut editor = build_editor(buffer.clone(), window, cx);
701 let handle = cx.entity();
702 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
703
704 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
705 editor.nav_history.as_mut().unwrap().pop_backward(cx)
706 }
707
708 // Move the cursor a small distance.
709 // Nothing is added to the navigation history.
710 editor.change_selections(None, window, cx, |s| {
711 s.select_display_ranges([
712 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
713 ])
714 });
715 editor.change_selections(None, window, cx, |s| {
716 s.select_display_ranges([
717 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
718 ])
719 });
720 assert!(pop_history(&mut editor, cx).is_none());
721
722 // Move the cursor a large distance.
723 // The history can jump back to the previous position.
724 editor.change_selections(None, window, cx, |s| {
725 s.select_display_ranges([
726 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
727 ])
728 });
729 let nav_entry = pop_history(&mut editor, cx).unwrap();
730 editor.navigate(nav_entry.data.unwrap(), window, cx);
731 assert_eq!(nav_entry.item.id(), cx.entity_id());
732 assert_eq!(
733 editor.selections.display_ranges(cx),
734 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
735 );
736 assert!(pop_history(&mut editor, cx).is_none());
737
738 // Move the cursor a small distance via the mouse.
739 // Nothing is added to the navigation history.
740 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
741 editor.end_selection(window, cx);
742 assert_eq!(
743 editor.selections.display_ranges(cx),
744 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
745 );
746 assert!(pop_history(&mut editor, cx).is_none());
747
748 // Move the cursor a large distance via the mouse.
749 // The history can jump back to the previous position.
750 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
751 editor.end_selection(window, cx);
752 assert_eq!(
753 editor.selections.display_ranges(cx),
754 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
755 );
756 let nav_entry = pop_history(&mut editor, cx).unwrap();
757 editor.navigate(nav_entry.data.unwrap(), window, cx);
758 assert_eq!(nav_entry.item.id(), cx.entity_id());
759 assert_eq!(
760 editor.selections.display_ranges(cx),
761 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
762 );
763 assert!(pop_history(&mut editor, cx).is_none());
764
765 // Set scroll position to check later
766 editor.set_scroll_position(gpui::Point::<f32>::new(5.5, 5.5), window, cx);
767 let original_scroll_position = editor.scroll_manager.anchor();
768
769 // Jump to the end of the document and adjust scroll
770 editor.move_to_end(&MoveToEnd, window, cx);
771 editor.set_scroll_position(gpui::Point::<f32>::new(-2.5, -0.5), window, cx);
772 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
773
774 let nav_entry = pop_history(&mut editor, cx).unwrap();
775 editor.navigate(nav_entry.data.unwrap(), window, cx);
776 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
777
778 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
779 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
780 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
781 let invalid_point = Point::new(9999, 0);
782 editor.navigate(
783 Box::new(NavigationData {
784 cursor_anchor: invalid_anchor,
785 cursor_position: invalid_point,
786 scroll_anchor: ScrollAnchor {
787 anchor: invalid_anchor,
788 offset: Default::default(),
789 },
790 scroll_top_row: invalid_point.row,
791 }),
792 window,
793 cx,
794 );
795 assert_eq!(
796 editor.selections.display_ranges(cx),
797 &[editor.max_point(cx)..editor.max_point(cx)]
798 );
799 assert_eq!(
800 editor.scroll_position(cx),
801 gpui::Point::new(0., editor.max_point(cx).row().as_f32())
802 );
803
804 editor
805 })
806 });
807}
808
809#[gpui::test]
810fn test_cancel(cx: &mut TestAppContext) {
811 init_test(cx, |_| {});
812
813 let editor = cx.add_window(|window, cx| {
814 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
815 build_editor(buffer, window, cx)
816 });
817
818 _ = editor.update(cx, |editor, window, cx| {
819 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
820 editor.update_selection(
821 DisplayPoint::new(DisplayRow(1), 1),
822 0,
823 gpui::Point::<f32>::default(),
824 window,
825 cx,
826 );
827 editor.end_selection(window, cx);
828
829 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
830 editor.update_selection(
831 DisplayPoint::new(DisplayRow(0), 3),
832 0,
833 gpui::Point::<f32>::default(),
834 window,
835 cx,
836 );
837 editor.end_selection(window, cx);
838 assert_eq!(
839 editor.selections.display_ranges(cx),
840 [
841 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
842 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
843 ]
844 );
845 });
846
847 _ = editor.update(cx, |editor, window, cx| {
848 editor.cancel(&Cancel, window, cx);
849 assert_eq!(
850 editor.selections.display_ranges(cx),
851 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
852 );
853 });
854
855 _ = editor.update(cx, |editor, window, cx| {
856 editor.cancel(&Cancel, window, cx);
857 assert_eq!(
858 editor.selections.display_ranges(cx),
859 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
860 );
861 });
862}
863
864#[gpui::test]
865fn test_fold_action(cx: &mut TestAppContext) {
866 init_test(cx, |_| {});
867
868 let editor = cx.add_window(|window, cx| {
869 let buffer = MultiBuffer::build_simple(
870 &"
871 impl Foo {
872 // Hello!
873
874 fn a() {
875 1
876 }
877
878 fn b() {
879 2
880 }
881
882 fn c() {
883 3
884 }
885 }
886 "
887 .unindent(),
888 cx,
889 );
890 build_editor(buffer.clone(), window, cx)
891 });
892
893 _ = editor.update(cx, |editor, window, cx| {
894 editor.change_selections(None, window, cx, |s| {
895 s.select_display_ranges([
896 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
897 ]);
898 });
899 editor.fold(&Fold, window, cx);
900 assert_eq!(
901 editor.display_text(cx),
902 "
903 impl Foo {
904 // Hello!
905
906 fn a() {
907 1
908 }
909
910 fn b() {⋯
911 }
912
913 fn c() {⋯
914 }
915 }
916 "
917 .unindent(),
918 );
919
920 editor.fold(&Fold, window, cx);
921 assert_eq!(
922 editor.display_text(cx),
923 "
924 impl Foo {⋯
925 }
926 "
927 .unindent(),
928 );
929
930 editor.unfold_lines(&UnfoldLines, window, cx);
931 assert_eq!(
932 editor.display_text(cx),
933 "
934 impl Foo {
935 // Hello!
936
937 fn a() {
938 1
939 }
940
941 fn b() {⋯
942 }
943
944 fn c() {⋯
945 }
946 }
947 "
948 .unindent(),
949 );
950
951 editor.unfold_lines(&UnfoldLines, window, cx);
952 assert_eq!(
953 editor.display_text(cx),
954 editor.buffer.read(cx).read(cx).text()
955 );
956 });
957}
958
959#[gpui::test]
960fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
961 init_test(cx, |_| {});
962
963 let editor = cx.add_window(|window, cx| {
964 let buffer = MultiBuffer::build_simple(
965 &"
966 class Foo:
967 # Hello!
968
969 def a():
970 print(1)
971
972 def b():
973 print(2)
974
975 def c():
976 print(3)
977 "
978 .unindent(),
979 cx,
980 );
981 build_editor(buffer.clone(), window, cx)
982 });
983
984 _ = editor.update(cx, |editor, window, cx| {
985 editor.change_selections(None, window, cx, |s| {
986 s.select_display_ranges([
987 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
988 ]);
989 });
990 editor.fold(&Fold, window, cx);
991 assert_eq!(
992 editor.display_text(cx),
993 "
994 class Foo:
995 # Hello!
996
997 def a():
998 print(1)
999
1000 def b():⋯
1001
1002 def c():⋯
1003 "
1004 .unindent(),
1005 );
1006
1007 editor.fold(&Fold, window, cx);
1008 assert_eq!(
1009 editor.display_text(cx),
1010 "
1011 class Foo:⋯
1012 "
1013 .unindent(),
1014 );
1015
1016 editor.unfold_lines(&UnfoldLines, window, cx);
1017 assert_eq!(
1018 editor.display_text(cx),
1019 "
1020 class Foo:
1021 # Hello!
1022
1023 def a():
1024 print(1)
1025
1026 def b():⋯
1027
1028 def c():⋯
1029 "
1030 .unindent(),
1031 );
1032
1033 editor.unfold_lines(&UnfoldLines, window, cx);
1034 assert_eq!(
1035 editor.display_text(cx),
1036 editor.buffer.read(cx).read(cx).text()
1037 );
1038 });
1039}
1040
1041#[gpui::test]
1042fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1043 init_test(cx, |_| {});
1044
1045 let editor = cx.add_window(|window, cx| {
1046 let buffer = MultiBuffer::build_simple(
1047 &"
1048 class Foo:
1049 # Hello!
1050
1051 def a():
1052 print(1)
1053
1054 def b():
1055 print(2)
1056
1057
1058 def c():
1059 print(3)
1060
1061
1062 "
1063 .unindent(),
1064 cx,
1065 );
1066 build_editor(buffer.clone(), window, cx)
1067 });
1068
1069 _ = editor.update(cx, |editor, window, cx| {
1070 editor.change_selections(None, window, cx, |s| {
1071 s.select_display_ranges([
1072 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1073 ]);
1074 });
1075 editor.fold(&Fold, window, cx);
1076 assert_eq!(
1077 editor.display_text(cx),
1078 "
1079 class Foo:
1080 # Hello!
1081
1082 def a():
1083 print(1)
1084
1085 def b():⋯
1086
1087
1088 def c():⋯
1089
1090
1091 "
1092 .unindent(),
1093 );
1094
1095 editor.fold(&Fold, window, cx);
1096 assert_eq!(
1097 editor.display_text(cx),
1098 "
1099 class Foo:⋯
1100
1101
1102 "
1103 .unindent(),
1104 );
1105
1106 editor.unfold_lines(&UnfoldLines, window, cx);
1107 assert_eq!(
1108 editor.display_text(cx),
1109 "
1110 class Foo:
1111 # Hello!
1112
1113 def a():
1114 print(1)
1115
1116 def b():⋯
1117
1118
1119 def c():⋯
1120
1121
1122 "
1123 .unindent(),
1124 );
1125
1126 editor.unfold_lines(&UnfoldLines, window, cx);
1127 assert_eq!(
1128 editor.display_text(cx),
1129 editor.buffer.read(cx).read(cx).text()
1130 );
1131 });
1132}
1133
1134#[gpui::test]
1135fn test_fold_at_level(cx: &mut TestAppContext) {
1136 init_test(cx, |_| {});
1137
1138 let editor = cx.add_window(|window, cx| {
1139 let buffer = MultiBuffer::build_simple(
1140 &"
1141 class Foo:
1142 # Hello!
1143
1144 def a():
1145 print(1)
1146
1147 def b():
1148 print(2)
1149
1150
1151 class Bar:
1152 # World!
1153
1154 def a():
1155 print(1)
1156
1157 def b():
1158 print(2)
1159
1160
1161 "
1162 .unindent(),
1163 cx,
1164 );
1165 build_editor(buffer.clone(), window, cx)
1166 });
1167
1168 _ = editor.update(cx, |editor, window, cx| {
1169 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1170 assert_eq!(
1171 editor.display_text(cx),
1172 "
1173 class Foo:
1174 # Hello!
1175
1176 def a():⋯
1177
1178 def b():⋯
1179
1180
1181 class Bar:
1182 # World!
1183
1184 def a():⋯
1185
1186 def b():⋯
1187
1188
1189 "
1190 .unindent(),
1191 );
1192
1193 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1194 assert_eq!(
1195 editor.display_text(cx),
1196 "
1197 class Foo:⋯
1198
1199
1200 class Bar:⋯
1201
1202
1203 "
1204 .unindent(),
1205 );
1206
1207 editor.unfold_all(&UnfoldAll, window, cx);
1208 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1209 assert_eq!(
1210 editor.display_text(cx),
1211 "
1212 class Foo:
1213 # Hello!
1214
1215 def a():
1216 print(1)
1217
1218 def b():
1219 print(2)
1220
1221
1222 class Bar:
1223 # World!
1224
1225 def a():
1226 print(1)
1227
1228 def b():
1229 print(2)
1230
1231
1232 "
1233 .unindent(),
1234 );
1235
1236 assert_eq!(
1237 editor.display_text(cx),
1238 editor.buffer.read(cx).read(cx).text()
1239 );
1240 });
1241}
1242
1243#[gpui::test]
1244fn test_move_cursor(cx: &mut TestAppContext) {
1245 init_test(cx, |_| {});
1246
1247 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1248 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1249
1250 buffer.update(cx, |buffer, cx| {
1251 buffer.edit(
1252 vec![
1253 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1254 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1255 ],
1256 None,
1257 cx,
1258 );
1259 });
1260 _ = editor.update(cx, |editor, window, cx| {
1261 assert_eq!(
1262 editor.selections.display_ranges(cx),
1263 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1264 );
1265
1266 editor.move_down(&MoveDown, window, cx);
1267 assert_eq!(
1268 editor.selections.display_ranges(cx),
1269 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1270 );
1271
1272 editor.move_right(&MoveRight, window, cx);
1273 assert_eq!(
1274 editor.selections.display_ranges(cx),
1275 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1276 );
1277
1278 editor.move_left(&MoveLeft, window, cx);
1279 assert_eq!(
1280 editor.selections.display_ranges(cx),
1281 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1282 );
1283
1284 editor.move_up(&MoveUp, window, cx);
1285 assert_eq!(
1286 editor.selections.display_ranges(cx),
1287 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1288 );
1289
1290 editor.move_to_end(&MoveToEnd, window, cx);
1291 assert_eq!(
1292 editor.selections.display_ranges(cx),
1293 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1294 );
1295
1296 editor.move_to_beginning(&MoveToBeginning, window, cx);
1297 assert_eq!(
1298 editor.selections.display_ranges(cx),
1299 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1300 );
1301
1302 editor.change_selections(None, window, cx, |s| {
1303 s.select_display_ranges([
1304 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1305 ]);
1306 });
1307 editor.select_to_beginning(&SelectToBeginning, window, cx);
1308 assert_eq!(
1309 editor.selections.display_ranges(cx),
1310 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1311 );
1312
1313 editor.select_to_end(&SelectToEnd, window, cx);
1314 assert_eq!(
1315 editor.selections.display_ranges(cx),
1316 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1317 );
1318 });
1319}
1320
1321#[gpui::test]
1322fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1323 init_test(cx, |_| {});
1324
1325 let editor = cx.add_window(|window, cx| {
1326 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1327 build_editor(buffer.clone(), window, cx)
1328 });
1329
1330 assert_eq!('🟥'.len_utf8(), 4);
1331 assert_eq!('α'.len_utf8(), 2);
1332
1333 _ = editor.update(cx, |editor, window, cx| {
1334 editor.fold_creases(
1335 vec![
1336 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1337 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1338 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1339 ],
1340 true,
1341 window,
1342 cx,
1343 );
1344 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1345
1346 editor.move_right(&MoveRight, window, cx);
1347 assert_eq!(
1348 editor.selections.display_ranges(cx),
1349 &[empty_range(0, "🟥".len())]
1350 );
1351 editor.move_right(&MoveRight, window, cx);
1352 assert_eq!(
1353 editor.selections.display_ranges(cx),
1354 &[empty_range(0, "🟥🟧".len())]
1355 );
1356 editor.move_right(&MoveRight, window, cx);
1357 assert_eq!(
1358 editor.selections.display_ranges(cx),
1359 &[empty_range(0, "🟥🟧⋯".len())]
1360 );
1361
1362 editor.move_down(&MoveDown, window, cx);
1363 assert_eq!(
1364 editor.selections.display_ranges(cx),
1365 &[empty_range(1, "ab⋯e".len())]
1366 );
1367 editor.move_left(&MoveLeft, window, cx);
1368 assert_eq!(
1369 editor.selections.display_ranges(cx),
1370 &[empty_range(1, "ab⋯".len())]
1371 );
1372 editor.move_left(&MoveLeft, window, cx);
1373 assert_eq!(
1374 editor.selections.display_ranges(cx),
1375 &[empty_range(1, "ab".len())]
1376 );
1377 editor.move_left(&MoveLeft, window, cx);
1378 assert_eq!(
1379 editor.selections.display_ranges(cx),
1380 &[empty_range(1, "a".len())]
1381 );
1382
1383 editor.move_down(&MoveDown, window, cx);
1384 assert_eq!(
1385 editor.selections.display_ranges(cx),
1386 &[empty_range(2, "α".len())]
1387 );
1388 editor.move_right(&MoveRight, window, cx);
1389 assert_eq!(
1390 editor.selections.display_ranges(cx),
1391 &[empty_range(2, "αβ".len())]
1392 );
1393 editor.move_right(&MoveRight, window, cx);
1394 assert_eq!(
1395 editor.selections.display_ranges(cx),
1396 &[empty_range(2, "αβ⋯".len())]
1397 );
1398 editor.move_right(&MoveRight, window, cx);
1399 assert_eq!(
1400 editor.selections.display_ranges(cx),
1401 &[empty_range(2, "αβ⋯ε".len())]
1402 );
1403
1404 editor.move_up(&MoveUp, window, cx);
1405 assert_eq!(
1406 editor.selections.display_ranges(cx),
1407 &[empty_range(1, "ab⋯e".len())]
1408 );
1409 editor.move_down(&MoveDown, window, cx);
1410 assert_eq!(
1411 editor.selections.display_ranges(cx),
1412 &[empty_range(2, "αβ⋯ε".len())]
1413 );
1414 editor.move_up(&MoveUp, window, cx);
1415 assert_eq!(
1416 editor.selections.display_ranges(cx),
1417 &[empty_range(1, "ab⋯e".len())]
1418 );
1419
1420 editor.move_up(&MoveUp, window, cx);
1421 assert_eq!(
1422 editor.selections.display_ranges(cx),
1423 &[empty_range(0, "🟥🟧".len())]
1424 );
1425 editor.move_left(&MoveLeft, window, cx);
1426 assert_eq!(
1427 editor.selections.display_ranges(cx),
1428 &[empty_range(0, "🟥".len())]
1429 );
1430 editor.move_left(&MoveLeft, window, cx);
1431 assert_eq!(
1432 editor.selections.display_ranges(cx),
1433 &[empty_range(0, "".len())]
1434 );
1435 });
1436}
1437
1438#[gpui::test]
1439fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1440 init_test(cx, |_| {});
1441
1442 let editor = cx.add_window(|window, cx| {
1443 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1444 build_editor(buffer.clone(), window, cx)
1445 });
1446 _ = editor.update(cx, |editor, window, cx| {
1447 editor.change_selections(None, window, cx, |s| {
1448 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1449 });
1450
1451 // moving above start of document should move selection to start of document,
1452 // but the next move down should still be at the original goal_x
1453 editor.move_up(&MoveUp, window, cx);
1454 assert_eq!(
1455 editor.selections.display_ranges(cx),
1456 &[empty_range(0, "".len())]
1457 );
1458
1459 editor.move_down(&MoveDown, window, cx);
1460 assert_eq!(
1461 editor.selections.display_ranges(cx),
1462 &[empty_range(1, "abcd".len())]
1463 );
1464
1465 editor.move_down(&MoveDown, window, cx);
1466 assert_eq!(
1467 editor.selections.display_ranges(cx),
1468 &[empty_range(2, "αβγ".len())]
1469 );
1470
1471 editor.move_down(&MoveDown, window, cx);
1472 assert_eq!(
1473 editor.selections.display_ranges(cx),
1474 &[empty_range(3, "abcd".len())]
1475 );
1476
1477 editor.move_down(&MoveDown, window, cx);
1478 assert_eq!(
1479 editor.selections.display_ranges(cx),
1480 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1481 );
1482
1483 // moving past end of document should not change goal_x
1484 editor.move_down(&MoveDown, window, cx);
1485 assert_eq!(
1486 editor.selections.display_ranges(cx),
1487 &[empty_range(5, "".len())]
1488 );
1489
1490 editor.move_down(&MoveDown, window, cx);
1491 assert_eq!(
1492 editor.selections.display_ranges(cx),
1493 &[empty_range(5, "".len())]
1494 );
1495
1496 editor.move_up(&MoveUp, window, cx);
1497 assert_eq!(
1498 editor.selections.display_ranges(cx),
1499 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1500 );
1501
1502 editor.move_up(&MoveUp, window, cx);
1503 assert_eq!(
1504 editor.selections.display_ranges(cx),
1505 &[empty_range(3, "abcd".len())]
1506 );
1507
1508 editor.move_up(&MoveUp, window, cx);
1509 assert_eq!(
1510 editor.selections.display_ranges(cx),
1511 &[empty_range(2, "αβγ".len())]
1512 );
1513 });
1514}
1515
1516#[gpui::test]
1517fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1518 init_test(cx, |_| {});
1519 let move_to_beg = MoveToBeginningOfLine {
1520 stop_at_soft_wraps: true,
1521 stop_at_indent: true,
1522 };
1523
1524 let delete_to_beg = DeleteToBeginningOfLine {
1525 stop_at_indent: false,
1526 };
1527
1528 let move_to_end = MoveToEndOfLine {
1529 stop_at_soft_wraps: true,
1530 };
1531
1532 let editor = cx.add_window(|window, cx| {
1533 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1534 build_editor(buffer, window, cx)
1535 });
1536 _ = editor.update(cx, |editor, window, cx| {
1537 editor.change_selections(None, window, cx, |s| {
1538 s.select_display_ranges([
1539 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1540 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1541 ]);
1542 });
1543 });
1544
1545 _ = editor.update(cx, |editor, window, cx| {
1546 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1547 assert_eq!(
1548 editor.selections.display_ranges(cx),
1549 &[
1550 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1551 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1552 ]
1553 );
1554 });
1555
1556 _ = editor.update(cx, |editor, window, cx| {
1557 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1558 assert_eq!(
1559 editor.selections.display_ranges(cx),
1560 &[
1561 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1562 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1563 ]
1564 );
1565 });
1566
1567 _ = editor.update(cx, |editor, window, cx| {
1568 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1569 assert_eq!(
1570 editor.selections.display_ranges(cx),
1571 &[
1572 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1573 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1574 ]
1575 );
1576 });
1577
1578 _ = editor.update(cx, |editor, window, cx| {
1579 editor.move_to_end_of_line(&move_to_end, window, cx);
1580 assert_eq!(
1581 editor.selections.display_ranges(cx),
1582 &[
1583 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1584 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1585 ]
1586 );
1587 });
1588
1589 // Moving to the end of line again is a no-op.
1590 _ = editor.update(cx, |editor, window, cx| {
1591 editor.move_to_end_of_line(&move_to_end, window, cx);
1592 assert_eq!(
1593 editor.selections.display_ranges(cx),
1594 &[
1595 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1596 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1597 ]
1598 );
1599 });
1600
1601 _ = editor.update(cx, |editor, window, cx| {
1602 editor.move_left(&MoveLeft, window, cx);
1603 editor.select_to_beginning_of_line(
1604 &SelectToBeginningOfLine {
1605 stop_at_soft_wraps: true,
1606 stop_at_indent: true,
1607 },
1608 window,
1609 cx,
1610 );
1611 assert_eq!(
1612 editor.selections.display_ranges(cx),
1613 &[
1614 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1615 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1616 ]
1617 );
1618 });
1619
1620 _ = editor.update(cx, |editor, window, cx| {
1621 editor.select_to_beginning_of_line(
1622 &SelectToBeginningOfLine {
1623 stop_at_soft_wraps: true,
1624 stop_at_indent: true,
1625 },
1626 window,
1627 cx,
1628 );
1629 assert_eq!(
1630 editor.selections.display_ranges(cx),
1631 &[
1632 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1633 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1634 ]
1635 );
1636 });
1637
1638 _ = editor.update(cx, |editor, window, cx| {
1639 editor.select_to_beginning_of_line(
1640 &SelectToBeginningOfLine {
1641 stop_at_soft_wraps: true,
1642 stop_at_indent: true,
1643 },
1644 window,
1645 cx,
1646 );
1647 assert_eq!(
1648 editor.selections.display_ranges(cx),
1649 &[
1650 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1651 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1652 ]
1653 );
1654 });
1655
1656 _ = editor.update(cx, |editor, window, cx| {
1657 editor.select_to_end_of_line(
1658 &SelectToEndOfLine {
1659 stop_at_soft_wraps: true,
1660 },
1661 window,
1662 cx,
1663 );
1664 assert_eq!(
1665 editor.selections.display_ranges(cx),
1666 &[
1667 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1668 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1669 ]
1670 );
1671 });
1672
1673 _ = editor.update(cx, |editor, window, cx| {
1674 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1675 assert_eq!(editor.display_text(cx), "ab\n de");
1676 assert_eq!(
1677 editor.selections.display_ranges(cx),
1678 &[
1679 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1680 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1681 ]
1682 );
1683 });
1684
1685 _ = editor.update(cx, |editor, window, cx| {
1686 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1687 assert_eq!(editor.display_text(cx), "\n");
1688 assert_eq!(
1689 editor.selections.display_ranges(cx),
1690 &[
1691 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1692 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1693 ]
1694 );
1695 });
1696}
1697
1698#[gpui::test]
1699fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1700 init_test(cx, |_| {});
1701 let move_to_beg = MoveToBeginningOfLine {
1702 stop_at_soft_wraps: false,
1703 stop_at_indent: false,
1704 };
1705
1706 let move_to_end = MoveToEndOfLine {
1707 stop_at_soft_wraps: false,
1708 };
1709
1710 let editor = cx.add_window(|window, cx| {
1711 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1712 build_editor(buffer, window, cx)
1713 });
1714
1715 _ = editor.update(cx, |editor, window, cx| {
1716 editor.set_wrap_width(Some(140.0.into()), cx);
1717
1718 // We expect the following lines after wrapping
1719 // ```
1720 // thequickbrownfox
1721 // jumpedoverthelazydo
1722 // gs
1723 // ```
1724 // The final `gs` was soft-wrapped onto a new line.
1725 assert_eq!(
1726 "thequickbrownfox\njumpedoverthelaz\nydogs",
1727 editor.display_text(cx),
1728 );
1729
1730 // First, let's assert behavior on the first line, that was not soft-wrapped.
1731 // Start the cursor at the `k` on the first line
1732 editor.change_selections(None, window, cx, |s| {
1733 s.select_display_ranges([
1734 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1735 ]);
1736 });
1737
1738 // Moving to the beginning of the line should put us at the beginning of the line.
1739 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1740 assert_eq!(
1741 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1742 editor.selections.display_ranges(cx)
1743 );
1744
1745 // Moving to the end of the line should put us at the end of the line.
1746 editor.move_to_end_of_line(&move_to_end, window, cx);
1747 assert_eq!(
1748 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1749 editor.selections.display_ranges(cx)
1750 );
1751
1752 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1753 // Start the cursor at the last line (`y` that was wrapped to a new line)
1754 editor.change_selections(None, window, cx, |s| {
1755 s.select_display_ranges([
1756 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1757 ]);
1758 });
1759
1760 // Moving to the beginning of the line should put us at the start of the second line of
1761 // display text, i.e., the `j`.
1762 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1763 assert_eq!(
1764 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1765 editor.selections.display_ranges(cx)
1766 );
1767
1768 // Moving to the beginning of the line again should be a no-op.
1769 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1770 assert_eq!(
1771 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1772 editor.selections.display_ranges(cx)
1773 );
1774
1775 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1776 // next display line.
1777 editor.move_to_end_of_line(&move_to_end, window, cx);
1778 assert_eq!(
1779 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1780 editor.selections.display_ranges(cx)
1781 );
1782
1783 // Moving to the end of the line again should be a no-op.
1784 editor.move_to_end_of_line(&move_to_end, window, cx);
1785 assert_eq!(
1786 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1787 editor.selections.display_ranges(cx)
1788 );
1789 });
1790}
1791
1792#[gpui::test]
1793fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1794 init_test(cx, |_| {});
1795
1796 let move_to_beg = MoveToBeginningOfLine {
1797 stop_at_soft_wraps: true,
1798 stop_at_indent: true,
1799 };
1800
1801 let select_to_beg = SelectToBeginningOfLine {
1802 stop_at_soft_wraps: true,
1803 stop_at_indent: true,
1804 };
1805
1806 let delete_to_beg = DeleteToBeginningOfLine {
1807 stop_at_indent: true,
1808 };
1809
1810 let move_to_end = MoveToEndOfLine {
1811 stop_at_soft_wraps: false,
1812 };
1813
1814 let editor = cx.add_window(|window, cx| {
1815 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1816 build_editor(buffer, window, cx)
1817 });
1818
1819 _ = editor.update(cx, |editor, window, cx| {
1820 editor.change_selections(None, window, cx, |s| {
1821 s.select_display_ranges([
1822 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1823 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1824 ]);
1825 });
1826
1827 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1828 // and the second cursor at the first non-whitespace character in the line.
1829 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1830 assert_eq!(
1831 editor.selections.display_ranges(cx),
1832 &[
1833 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1834 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1835 ]
1836 );
1837
1838 // Moving to the beginning of the line again should be a no-op for the first cursor,
1839 // and should move the second cursor to the beginning of the line.
1840 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1841 assert_eq!(
1842 editor.selections.display_ranges(cx),
1843 &[
1844 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1845 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1846 ]
1847 );
1848
1849 // Moving to the beginning of the line again should still be a no-op for the first cursor,
1850 // and should move the second cursor back to the first non-whitespace character in the line.
1851 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1852 assert_eq!(
1853 editor.selections.display_ranges(cx),
1854 &[
1855 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1856 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1857 ]
1858 );
1859
1860 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
1861 // and to the first non-whitespace character in the line for the second cursor.
1862 editor.move_to_end_of_line(&move_to_end, window, cx);
1863 editor.move_left(&MoveLeft, window, cx);
1864 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1865 assert_eq!(
1866 editor.selections.display_ranges(cx),
1867 &[
1868 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1869 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1870 ]
1871 );
1872
1873 // Selecting to the beginning of the line again should be a no-op for the first cursor,
1874 // and should select to the beginning of the line for the second cursor.
1875 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1876 assert_eq!(
1877 editor.selections.display_ranges(cx),
1878 &[
1879 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1880 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1881 ]
1882 );
1883
1884 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
1885 // and should delete to the first non-whitespace character in the line for the second cursor.
1886 editor.move_to_end_of_line(&move_to_end, window, cx);
1887 editor.move_left(&MoveLeft, window, cx);
1888 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1889 assert_eq!(editor.text(cx), "c\n f");
1890 });
1891}
1892
1893#[gpui::test]
1894fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
1895 init_test(cx, |_| {});
1896
1897 let editor = cx.add_window(|window, cx| {
1898 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
1899 build_editor(buffer, window, cx)
1900 });
1901 _ = editor.update(cx, |editor, window, cx| {
1902 editor.change_selections(None, window, cx, |s| {
1903 s.select_display_ranges([
1904 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
1905 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
1906 ])
1907 });
1908
1909 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1910 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1911
1912 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1913 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\n ˇ{baz.qux()}", editor, cx);
1914
1915 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1916 assert_selection_ranges("use ˇstd::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
1917
1918 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1919 assert_selection_ranges("ˇuse std::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1920
1921 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1922 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1923
1924 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1925 assert_selection_ranges("useˇ std::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
1926
1927 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1928 assert_selection_ranges("use stdˇ::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1929
1930 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1931 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1932
1933 editor.move_right(&MoveRight, window, cx);
1934 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1935 assert_selection_ranges(
1936 "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
1937 editor,
1938 cx,
1939 );
1940
1941 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1942 assert_selection_ranges(
1943 "use std«ˇ::s»tr::{foo, bar}\n\n «ˇ{b»az.qux()}",
1944 editor,
1945 cx,
1946 );
1947
1948 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
1949 assert_selection_ranges(
1950 "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
1951 editor,
1952 cx,
1953 );
1954 });
1955}
1956
1957#[gpui::test]
1958fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
1959 init_test(cx, |_| {});
1960
1961 let editor = cx.add_window(|window, cx| {
1962 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
1963 build_editor(buffer, window, cx)
1964 });
1965
1966 _ = editor.update(cx, |editor, window, cx| {
1967 editor.set_wrap_width(Some(140.0.into()), cx);
1968 assert_eq!(
1969 editor.display_text(cx),
1970 "use one::{\n two::three::\n four::five\n};"
1971 );
1972
1973 editor.change_selections(None, window, cx, |s| {
1974 s.select_display_ranges([
1975 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
1976 ]);
1977 });
1978
1979 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1980 assert_eq!(
1981 editor.selections.display_ranges(cx),
1982 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
1983 );
1984
1985 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1986 assert_eq!(
1987 editor.selections.display_ranges(cx),
1988 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
1989 );
1990
1991 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1992 assert_eq!(
1993 editor.selections.display_ranges(cx),
1994 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
1995 );
1996
1997 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1998 assert_eq!(
1999 editor.selections.display_ranges(cx),
2000 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2001 );
2002
2003 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2004 assert_eq!(
2005 editor.selections.display_ranges(cx),
2006 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2007 );
2008
2009 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2010 assert_eq!(
2011 editor.selections.display_ranges(cx),
2012 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2013 );
2014 });
2015}
2016
2017#[gpui::test]
2018async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2019 init_test(cx, |_| {});
2020 let mut cx = EditorTestContext::new(cx).await;
2021
2022 let line_height = cx.editor(|editor, window, _| {
2023 editor
2024 .style()
2025 .unwrap()
2026 .text
2027 .line_height_in_pixels(window.rem_size())
2028 });
2029 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2030
2031 cx.set_state(
2032 &r#"ˇone
2033 two
2034
2035 three
2036 fourˇ
2037 five
2038
2039 six"#
2040 .unindent(),
2041 );
2042
2043 cx.update_editor(|editor, window, cx| {
2044 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2045 });
2046 cx.assert_editor_state(
2047 &r#"one
2048 two
2049 ˇ
2050 three
2051 four
2052 five
2053 ˇ
2054 six"#
2055 .unindent(),
2056 );
2057
2058 cx.update_editor(|editor, window, cx| {
2059 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2060 });
2061 cx.assert_editor_state(
2062 &r#"one
2063 two
2064
2065 three
2066 four
2067 five
2068 ˇ
2069 sixˇ"#
2070 .unindent(),
2071 );
2072
2073 cx.update_editor(|editor, window, cx| {
2074 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2075 });
2076 cx.assert_editor_state(
2077 &r#"one
2078 two
2079
2080 three
2081 four
2082 five
2083
2084 sixˇ"#
2085 .unindent(),
2086 );
2087
2088 cx.update_editor(|editor, window, cx| {
2089 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2090 });
2091 cx.assert_editor_state(
2092 &r#"one
2093 two
2094
2095 three
2096 four
2097 five
2098 ˇ
2099 six"#
2100 .unindent(),
2101 );
2102
2103 cx.update_editor(|editor, window, cx| {
2104 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2105 });
2106 cx.assert_editor_state(
2107 &r#"one
2108 two
2109 ˇ
2110 three
2111 four
2112 five
2113
2114 six"#
2115 .unindent(),
2116 );
2117
2118 cx.update_editor(|editor, window, cx| {
2119 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2120 });
2121 cx.assert_editor_state(
2122 &r#"ˇone
2123 two
2124
2125 three
2126 four
2127 five
2128
2129 six"#
2130 .unindent(),
2131 );
2132}
2133
2134#[gpui::test]
2135async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2136 init_test(cx, |_| {});
2137 let mut cx = EditorTestContext::new(cx).await;
2138 let line_height = cx.editor(|editor, window, _| {
2139 editor
2140 .style()
2141 .unwrap()
2142 .text
2143 .line_height_in_pixels(window.rem_size())
2144 });
2145 let window = cx.window;
2146 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2147
2148 cx.set_state(
2149 r#"ˇone
2150 two
2151 three
2152 four
2153 five
2154 six
2155 seven
2156 eight
2157 nine
2158 ten
2159 "#,
2160 );
2161
2162 cx.update_editor(|editor, window, cx| {
2163 assert_eq!(
2164 editor.snapshot(window, cx).scroll_position(),
2165 gpui::Point::new(0., 0.)
2166 );
2167 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2168 assert_eq!(
2169 editor.snapshot(window, cx).scroll_position(),
2170 gpui::Point::new(0., 3.)
2171 );
2172 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2173 assert_eq!(
2174 editor.snapshot(window, cx).scroll_position(),
2175 gpui::Point::new(0., 6.)
2176 );
2177 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2178 assert_eq!(
2179 editor.snapshot(window, cx).scroll_position(),
2180 gpui::Point::new(0., 3.)
2181 );
2182
2183 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2184 assert_eq!(
2185 editor.snapshot(window, cx).scroll_position(),
2186 gpui::Point::new(0., 1.)
2187 );
2188 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2189 assert_eq!(
2190 editor.snapshot(window, cx).scroll_position(),
2191 gpui::Point::new(0., 3.)
2192 );
2193 });
2194}
2195
2196#[gpui::test]
2197async fn test_autoscroll(cx: &mut TestAppContext) {
2198 init_test(cx, |_| {});
2199 let mut cx = EditorTestContext::new(cx).await;
2200
2201 let line_height = cx.update_editor(|editor, window, cx| {
2202 editor.set_vertical_scroll_margin(2, cx);
2203 editor
2204 .style()
2205 .unwrap()
2206 .text
2207 .line_height_in_pixels(window.rem_size())
2208 });
2209 let window = cx.window;
2210 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2211
2212 cx.set_state(
2213 r#"ˇone
2214 two
2215 three
2216 four
2217 five
2218 six
2219 seven
2220 eight
2221 nine
2222 ten
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.0)
2229 );
2230 });
2231
2232 // Add a cursor below the visible area. Since both cursors cannot fit
2233 // on screen, the editor autoscrolls to reveal the newest cursor, and
2234 // allows the vertical scroll margin below that cursor.
2235 cx.update_editor(|editor, window, cx| {
2236 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2237 selections.select_ranges([
2238 Point::new(0, 0)..Point::new(0, 0),
2239 Point::new(6, 0)..Point::new(6, 0),
2240 ]);
2241 })
2242 });
2243 cx.update_editor(|editor, window, cx| {
2244 assert_eq!(
2245 editor.snapshot(window, cx).scroll_position(),
2246 gpui::Point::new(0., 3.0)
2247 );
2248 });
2249
2250 // Move down. The editor cursor scrolls down to track the newest cursor.
2251 cx.update_editor(|editor, window, cx| {
2252 editor.move_down(&Default::default(), window, cx);
2253 });
2254 cx.update_editor(|editor, window, cx| {
2255 assert_eq!(
2256 editor.snapshot(window, cx).scroll_position(),
2257 gpui::Point::new(0., 4.0)
2258 );
2259 });
2260
2261 // Add a cursor above the visible area. Since both cursors fit on screen,
2262 // the editor scrolls to show both.
2263 cx.update_editor(|editor, window, cx| {
2264 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2265 selections.select_ranges([
2266 Point::new(1, 0)..Point::new(1, 0),
2267 Point::new(6, 0)..Point::new(6, 0),
2268 ]);
2269 })
2270 });
2271 cx.update_editor(|editor, window, cx| {
2272 assert_eq!(
2273 editor.snapshot(window, cx).scroll_position(),
2274 gpui::Point::new(0., 1.0)
2275 );
2276 });
2277}
2278
2279#[gpui::test]
2280async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2281 init_test(cx, |_| {});
2282 let mut cx = EditorTestContext::new(cx).await;
2283
2284 let line_height = cx.editor(|editor, window, _cx| {
2285 editor
2286 .style()
2287 .unwrap()
2288 .text
2289 .line_height_in_pixels(window.rem_size())
2290 });
2291 let window = cx.window;
2292 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2293 cx.set_state(
2294 &r#"
2295 ˇone
2296 two
2297 threeˇ
2298 four
2299 five
2300 six
2301 seven
2302 eight
2303 nine
2304 ten
2305 "#
2306 .unindent(),
2307 );
2308
2309 cx.update_editor(|editor, window, cx| {
2310 editor.move_page_down(&MovePageDown::default(), window, cx)
2311 });
2312 cx.assert_editor_state(
2313 &r#"
2314 one
2315 two
2316 three
2317 ˇfour
2318 five
2319 sixˇ
2320 seven
2321 eight
2322 nine
2323 ten
2324 "#
2325 .unindent(),
2326 );
2327
2328 cx.update_editor(|editor, window, cx| {
2329 editor.move_page_down(&MovePageDown::default(), window, cx)
2330 });
2331 cx.assert_editor_state(
2332 &r#"
2333 one
2334 two
2335 three
2336 four
2337 five
2338 six
2339 ˇseven
2340 eight
2341 nineˇ
2342 ten
2343 "#
2344 .unindent(),
2345 );
2346
2347 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2348 cx.assert_editor_state(
2349 &r#"
2350 one
2351 two
2352 three
2353 ˇfour
2354 five
2355 sixˇ
2356 seven
2357 eight
2358 nine
2359 ten
2360 "#
2361 .unindent(),
2362 );
2363
2364 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2365 cx.assert_editor_state(
2366 &r#"
2367 ˇone
2368 two
2369 threeˇ
2370 four
2371 five
2372 six
2373 seven
2374 eight
2375 nine
2376 ten
2377 "#
2378 .unindent(),
2379 );
2380
2381 // Test select collapsing
2382 cx.update_editor(|editor, window, cx| {
2383 editor.move_page_down(&MovePageDown::default(), window, cx);
2384 editor.move_page_down(&MovePageDown::default(), window, cx);
2385 editor.move_page_down(&MovePageDown::default(), window, cx);
2386 });
2387 cx.assert_editor_state(
2388 &r#"
2389 one
2390 two
2391 three
2392 four
2393 five
2394 six
2395 seven
2396 eight
2397 nine
2398 ˇten
2399 ˇ"#
2400 .unindent(),
2401 );
2402}
2403
2404#[gpui::test]
2405async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2406 init_test(cx, |_| {});
2407 let mut cx = EditorTestContext::new(cx).await;
2408 cx.set_state("one «two threeˇ» four");
2409 cx.update_editor(|editor, window, cx| {
2410 editor.delete_to_beginning_of_line(
2411 &DeleteToBeginningOfLine {
2412 stop_at_indent: false,
2413 },
2414 window,
2415 cx,
2416 );
2417 assert_eq!(editor.text(cx), " four");
2418 });
2419}
2420
2421#[gpui::test]
2422fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2423 init_test(cx, |_| {});
2424
2425 let editor = cx.add_window(|window, cx| {
2426 let buffer = MultiBuffer::build_simple("one two three four", cx);
2427 build_editor(buffer.clone(), window, cx)
2428 });
2429
2430 _ = editor.update(cx, |editor, window, cx| {
2431 editor.change_selections(None, window, cx, |s| {
2432 s.select_display_ranges([
2433 // an empty selection - the preceding word fragment is deleted
2434 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2435 // characters selected - they are deleted
2436 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 12),
2437 ])
2438 });
2439 editor.delete_to_previous_word_start(
2440 &DeleteToPreviousWordStart {
2441 ignore_newlines: false,
2442 },
2443 window,
2444 cx,
2445 );
2446 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e two te four");
2447 });
2448
2449 _ = editor.update(cx, |editor, window, cx| {
2450 editor.change_selections(None, window, cx, |s| {
2451 s.select_display_ranges([
2452 // an empty selection - the following word fragment is deleted
2453 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
2454 // characters selected - they are deleted
2455 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 10),
2456 ])
2457 });
2458 editor.delete_to_next_word_end(
2459 &DeleteToNextWordEnd {
2460 ignore_newlines: false,
2461 },
2462 window,
2463 cx,
2464 );
2465 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e t te our");
2466 });
2467}
2468
2469#[gpui::test]
2470fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2471 init_test(cx, |_| {});
2472
2473 let editor = cx.add_window(|window, cx| {
2474 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2475 build_editor(buffer.clone(), window, cx)
2476 });
2477 let del_to_prev_word_start = DeleteToPreviousWordStart {
2478 ignore_newlines: false,
2479 };
2480 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2481 ignore_newlines: true,
2482 };
2483
2484 _ = editor.update(cx, |editor, window, cx| {
2485 editor.change_selections(None, window, cx, |s| {
2486 s.select_display_ranges([
2487 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2488 ])
2489 });
2490 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2491 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
2492 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2493 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
2494 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2495 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
2496 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2497 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
2498 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2499 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
2500 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2501 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2502 });
2503}
2504
2505#[gpui::test]
2506fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
2507 init_test(cx, |_| {});
2508
2509 let editor = cx.add_window(|window, cx| {
2510 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
2511 build_editor(buffer.clone(), window, cx)
2512 });
2513 let del_to_next_word_end = DeleteToNextWordEnd {
2514 ignore_newlines: false,
2515 };
2516 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
2517 ignore_newlines: true,
2518 };
2519
2520 _ = editor.update(cx, |editor, window, cx| {
2521 editor.change_selections(None, window, cx, |s| {
2522 s.select_display_ranges([
2523 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2524 ])
2525 });
2526 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2527 assert_eq!(
2528 editor.buffer.read(cx).read(cx).text(),
2529 "one\n two\nthree\n four"
2530 );
2531 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2532 assert_eq!(
2533 editor.buffer.read(cx).read(cx).text(),
2534 "\n two\nthree\n four"
2535 );
2536 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2537 assert_eq!(
2538 editor.buffer.read(cx).read(cx).text(),
2539 "two\nthree\n four"
2540 );
2541 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2542 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
2543 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2544 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
2545 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2546 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2547 });
2548}
2549
2550#[gpui::test]
2551fn test_newline(cx: &mut TestAppContext) {
2552 init_test(cx, |_| {});
2553
2554 let editor = cx.add_window(|window, cx| {
2555 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
2556 build_editor(buffer.clone(), window, cx)
2557 });
2558
2559 _ = editor.update(cx, |editor, window, cx| {
2560 editor.change_selections(None, window, cx, |s| {
2561 s.select_display_ranges([
2562 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2563 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2564 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
2565 ])
2566 });
2567
2568 editor.newline(&Newline, window, cx);
2569 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
2570 });
2571}
2572
2573#[gpui::test]
2574fn test_newline_with_old_selections(cx: &mut TestAppContext) {
2575 init_test(cx, |_| {});
2576
2577 let editor = cx.add_window(|window, cx| {
2578 let buffer = MultiBuffer::build_simple(
2579 "
2580 a
2581 b(
2582 X
2583 )
2584 c(
2585 X
2586 )
2587 "
2588 .unindent()
2589 .as_str(),
2590 cx,
2591 );
2592 let mut editor = build_editor(buffer.clone(), window, cx);
2593 editor.change_selections(None, window, cx, |s| {
2594 s.select_ranges([
2595 Point::new(2, 4)..Point::new(2, 5),
2596 Point::new(5, 4)..Point::new(5, 5),
2597 ])
2598 });
2599 editor
2600 });
2601
2602 _ = editor.update(cx, |editor, window, cx| {
2603 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2604 editor.buffer.update(cx, |buffer, cx| {
2605 buffer.edit(
2606 [
2607 (Point::new(1, 2)..Point::new(3, 0), ""),
2608 (Point::new(4, 2)..Point::new(6, 0), ""),
2609 ],
2610 None,
2611 cx,
2612 );
2613 assert_eq!(
2614 buffer.read(cx).text(),
2615 "
2616 a
2617 b()
2618 c()
2619 "
2620 .unindent()
2621 );
2622 });
2623 assert_eq!(
2624 editor.selections.ranges(cx),
2625 &[
2626 Point::new(1, 2)..Point::new(1, 2),
2627 Point::new(2, 2)..Point::new(2, 2),
2628 ],
2629 );
2630
2631 editor.newline(&Newline, window, cx);
2632 assert_eq!(
2633 editor.text(cx),
2634 "
2635 a
2636 b(
2637 )
2638 c(
2639 )
2640 "
2641 .unindent()
2642 );
2643
2644 // The selections are moved after the inserted newlines
2645 assert_eq!(
2646 editor.selections.ranges(cx),
2647 &[
2648 Point::new(2, 0)..Point::new(2, 0),
2649 Point::new(4, 0)..Point::new(4, 0),
2650 ],
2651 );
2652 });
2653}
2654
2655#[gpui::test]
2656async fn test_newline_above(cx: &mut TestAppContext) {
2657 init_test(cx, |settings| {
2658 settings.defaults.tab_size = NonZeroU32::new(4)
2659 });
2660
2661 let language = Arc::new(
2662 Language::new(
2663 LanguageConfig::default(),
2664 Some(tree_sitter_rust::LANGUAGE.into()),
2665 )
2666 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2667 .unwrap(),
2668 );
2669
2670 let mut cx = EditorTestContext::new(cx).await;
2671 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2672 cx.set_state(indoc! {"
2673 const a: ˇA = (
2674 (ˇ
2675 «const_functionˇ»(ˇ),
2676 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2677 )ˇ
2678 ˇ);ˇ
2679 "});
2680
2681 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
2682 cx.assert_editor_state(indoc! {"
2683 ˇ
2684 const a: A = (
2685 ˇ
2686 (
2687 ˇ
2688 ˇ
2689 const_function(),
2690 ˇ
2691 ˇ
2692 ˇ
2693 ˇ
2694 something_else,
2695 ˇ
2696 )
2697 ˇ
2698 ˇ
2699 );
2700 "});
2701}
2702
2703#[gpui::test]
2704async fn test_newline_below(cx: &mut TestAppContext) {
2705 init_test(cx, |settings| {
2706 settings.defaults.tab_size = NonZeroU32::new(4)
2707 });
2708
2709 let language = Arc::new(
2710 Language::new(
2711 LanguageConfig::default(),
2712 Some(tree_sitter_rust::LANGUAGE.into()),
2713 )
2714 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2715 .unwrap(),
2716 );
2717
2718 let mut cx = EditorTestContext::new(cx).await;
2719 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2720 cx.set_state(indoc! {"
2721 const a: ˇA = (
2722 (ˇ
2723 «const_functionˇ»(ˇ),
2724 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2725 )ˇ
2726 ˇ);ˇ
2727 "});
2728
2729 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
2730 cx.assert_editor_state(indoc! {"
2731 const a: A = (
2732 ˇ
2733 (
2734 ˇ
2735 const_function(),
2736 ˇ
2737 ˇ
2738 something_else,
2739 ˇ
2740 ˇ
2741 ˇ
2742 ˇ
2743 )
2744 ˇ
2745 );
2746 ˇ
2747 ˇ
2748 "});
2749}
2750
2751#[gpui::test]
2752async fn test_newline_comments(cx: &mut TestAppContext) {
2753 init_test(cx, |settings| {
2754 settings.defaults.tab_size = NonZeroU32::new(4)
2755 });
2756
2757 let language = Arc::new(Language::new(
2758 LanguageConfig {
2759 line_comments: vec!["// ".into()],
2760 ..LanguageConfig::default()
2761 },
2762 None,
2763 ));
2764 {
2765 let mut cx = EditorTestContext::new(cx).await;
2766 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2767 cx.set_state(indoc! {"
2768 // Fooˇ
2769 "});
2770
2771 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2772 cx.assert_editor_state(indoc! {"
2773 // Foo
2774 // ˇ
2775 "});
2776 // Ensure that we add comment prefix when existing line contains space
2777 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2778 cx.assert_editor_state(
2779 indoc! {"
2780 // Foo
2781 //s
2782 // ˇ
2783 "}
2784 .replace("s", " ") // s is used as space placeholder to prevent format on save
2785 .as_str(),
2786 );
2787 // Ensure that we add comment prefix when existing line does not contain space
2788 cx.set_state(indoc! {"
2789 // Foo
2790 //ˇ
2791 "});
2792 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2793 cx.assert_editor_state(indoc! {"
2794 // Foo
2795 //
2796 // ˇ
2797 "});
2798 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
2799 cx.set_state(indoc! {"
2800 ˇ// Foo
2801 "});
2802 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2803 cx.assert_editor_state(indoc! {"
2804
2805 ˇ// Foo
2806 "});
2807 }
2808 // Ensure that comment continuations can be disabled.
2809 update_test_language_settings(cx, |settings| {
2810 settings.defaults.extend_comment_on_newline = Some(false);
2811 });
2812 let mut cx = EditorTestContext::new(cx).await;
2813 cx.set_state(indoc! {"
2814 // Fooˇ
2815 "});
2816 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2817 cx.assert_editor_state(indoc! {"
2818 // Foo
2819 ˇ
2820 "});
2821}
2822
2823#[gpui::test]
2824async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
2825 init_test(cx, |settings| {
2826 settings.defaults.tab_size = NonZeroU32::new(4)
2827 });
2828
2829 let language = Arc::new(Language::new(
2830 LanguageConfig {
2831 documentation: Some(language::DocumentationConfig {
2832 start: "/**".into(),
2833 end: "*/".into(),
2834 prefix: "* ".into(),
2835 tab_size: NonZeroU32::new(1).unwrap(),
2836 }),
2837 ..LanguageConfig::default()
2838 },
2839 None,
2840 ));
2841 {
2842 let mut cx = EditorTestContext::new(cx).await;
2843 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2844 cx.set_state(indoc! {"
2845 /**ˇ
2846 "});
2847
2848 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2849 cx.assert_editor_state(indoc! {"
2850 /**
2851 * ˇ
2852 "});
2853 // Ensure that if cursor is before the comment start,
2854 // we do not actually insert a comment prefix.
2855 cx.set_state(indoc! {"
2856 ˇ/**
2857 "});
2858 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2859 cx.assert_editor_state(indoc! {"
2860
2861 ˇ/**
2862 "});
2863 // Ensure that if cursor is between it doesn't add comment prefix.
2864 cx.set_state(indoc! {"
2865 /*ˇ*
2866 "});
2867 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2868 cx.assert_editor_state(indoc! {"
2869 /*
2870 ˇ*
2871 "});
2872 // Ensure that if suffix exists on same line after cursor it adds new line.
2873 cx.set_state(indoc! {"
2874 /**ˇ*/
2875 "});
2876 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2877 cx.assert_editor_state(indoc! {"
2878 /**
2879 * ˇ
2880 */
2881 "});
2882 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2883 cx.set_state(indoc! {"
2884 /**ˇ */
2885 "});
2886 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2887 cx.assert_editor_state(indoc! {"
2888 /**
2889 * ˇ
2890 */
2891 "});
2892 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2893 cx.set_state(indoc! {"
2894 /** ˇ*/
2895 "});
2896 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2897 cx.assert_editor_state(
2898 indoc! {"
2899 /**s
2900 * ˇ
2901 */
2902 "}
2903 .replace("s", " ") // s is used as space placeholder to prevent format on save
2904 .as_str(),
2905 );
2906 // Ensure that delimiter space is preserved when newline on already
2907 // spaced delimiter.
2908 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2909 cx.assert_editor_state(
2910 indoc! {"
2911 /**s
2912 *s
2913 * ˇ
2914 */
2915 "}
2916 .replace("s", " ") // s is used as space placeholder to prevent format on save
2917 .as_str(),
2918 );
2919 // Ensure that delimiter space is preserved when space is not
2920 // on existing delimiter.
2921 cx.set_state(indoc! {"
2922 /**
2923 *ˇ
2924 */
2925 "});
2926 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2927 cx.assert_editor_state(indoc! {"
2928 /**
2929 *
2930 * ˇ
2931 */
2932 "});
2933 // Ensure that if suffix exists on same line after cursor it
2934 // doesn't add extra new line if prefix is not on same line.
2935 cx.set_state(indoc! {"
2936 /**
2937 ˇ*/
2938 "});
2939 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2940 cx.assert_editor_state(indoc! {"
2941 /**
2942
2943 ˇ*/
2944 "});
2945 // Ensure that it detects suffix after existing prefix.
2946 cx.set_state(indoc! {"
2947 /**ˇ/
2948 "});
2949 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2950 cx.assert_editor_state(indoc! {"
2951 /**
2952 ˇ/
2953 "});
2954 // Ensure that if suffix exists on same line before
2955 // cursor it does not add comment prefix.
2956 cx.set_state(indoc! {"
2957 /** */ˇ
2958 "});
2959 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2960 cx.assert_editor_state(indoc! {"
2961 /** */
2962 ˇ
2963 "});
2964 // Ensure that if suffix exists on same line before
2965 // cursor it does not add comment prefix.
2966 cx.set_state(indoc! {"
2967 /**
2968 *
2969 */ˇ
2970 "});
2971 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2972 cx.assert_editor_state(indoc! {"
2973 /**
2974 *
2975 */
2976 ˇ
2977 "});
2978 }
2979 // Ensure that comment continuations can be disabled.
2980 update_test_language_settings(cx, |settings| {
2981 settings.defaults.extend_comment_on_newline = Some(false);
2982 });
2983 let mut cx = EditorTestContext::new(cx).await;
2984 cx.set_state(indoc! {"
2985 /**ˇ
2986 "});
2987 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2988 cx.assert_editor_state(indoc! {"
2989 /**
2990 ˇ
2991 "});
2992}
2993
2994#[gpui::test]
2995fn test_insert_with_old_selections(cx: &mut TestAppContext) {
2996 init_test(cx, |_| {});
2997
2998 let editor = cx.add_window(|window, cx| {
2999 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3000 let mut editor = build_editor(buffer.clone(), window, cx);
3001 editor.change_selections(None, window, cx, |s| {
3002 s.select_ranges([3..4, 11..12, 19..20])
3003 });
3004 editor
3005 });
3006
3007 _ = editor.update(cx, |editor, window, cx| {
3008 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3009 editor.buffer.update(cx, |buffer, cx| {
3010 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
3011 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3012 });
3013 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
3014
3015 editor.insert("Z", window, cx);
3016 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3017
3018 // The selections are moved after the inserted characters
3019 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
3020 });
3021}
3022
3023#[gpui::test]
3024async fn test_tab(cx: &mut TestAppContext) {
3025 init_test(cx, |settings| {
3026 settings.defaults.tab_size = NonZeroU32::new(3)
3027 });
3028
3029 let mut cx = EditorTestContext::new(cx).await;
3030 cx.set_state(indoc! {"
3031 ˇabˇc
3032 ˇ🏀ˇ🏀ˇefg
3033 dˇ
3034 "});
3035 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3036 cx.assert_editor_state(indoc! {"
3037 ˇab ˇc
3038 ˇ🏀 ˇ🏀 ˇefg
3039 d ˇ
3040 "});
3041
3042 cx.set_state(indoc! {"
3043 a
3044 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3045 "});
3046 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3047 cx.assert_editor_state(indoc! {"
3048 a
3049 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3050 "});
3051}
3052
3053#[gpui::test]
3054async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3055 init_test(cx, |_| {});
3056
3057 let mut cx = EditorTestContext::new(cx).await;
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 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3067
3068 // test when all cursors are not at suggested indent
3069 // then simply move to their suggested indent location
3070 cx.set_state(indoc! {"
3071 const a: B = (
3072 c(
3073 ˇ
3074 ˇ )
3075 );
3076 "});
3077 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3078 cx.assert_editor_state(indoc! {"
3079 const a: B = (
3080 c(
3081 ˇ
3082 ˇ)
3083 );
3084 "});
3085
3086 // test cursor already at suggested indent not moving when
3087 // other cursors are yet to reach their suggested indents
3088 cx.set_state(indoc! {"
3089 ˇ
3090 const a: B = (
3091 c(
3092 d(
3093 ˇ
3094 )
3095 ˇ
3096 ˇ )
3097 );
3098 "});
3099 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3100 cx.assert_editor_state(indoc! {"
3101 ˇ
3102 const a: B = (
3103 c(
3104 d(
3105 ˇ
3106 )
3107 ˇ
3108 ˇ)
3109 );
3110 "});
3111 // test when all cursors are at suggested indent then tab is inserted
3112 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3113 cx.assert_editor_state(indoc! {"
3114 ˇ
3115 const a: B = (
3116 c(
3117 d(
3118 ˇ
3119 )
3120 ˇ
3121 ˇ)
3122 );
3123 "});
3124
3125 // test when current indent is less than suggested indent,
3126 // we adjust line to match suggested indent and move cursor to it
3127 //
3128 // when no other cursor is at word boundary, all of them should move
3129 cx.set_state(indoc! {"
3130 const a: B = (
3131 c(
3132 d(
3133 ˇ
3134 ˇ )
3135 ˇ )
3136 );
3137 "});
3138 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3139 cx.assert_editor_state(indoc! {"
3140 const a: B = (
3141 c(
3142 d(
3143 ˇ
3144 ˇ)
3145 ˇ)
3146 );
3147 "});
3148
3149 // test when current indent is less than suggested indent,
3150 // we adjust line to match suggested indent and move cursor to it
3151 //
3152 // when some other cursor is at word boundary, it should not move
3153 cx.set_state(indoc! {"
3154 const a: B = (
3155 c(
3156 d(
3157 ˇ
3158 ˇ )
3159 ˇ)
3160 );
3161 "});
3162 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3163 cx.assert_editor_state(indoc! {"
3164 const a: B = (
3165 c(
3166 d(
3167 ˇ
3168 ˇ)
3169 ˇ)
3170 );
3171 "});
3172
3173 // test when current indent is more than suggested indent,
3174 // we just move cursor to current indent instead of suggested indent
3175 //
3176 // when no other cursor is at word boundary, all of them should move
3177 cx.set_state(indoc! {"
3178 const a: B = (
3179 c(
3180 d(
3181 ˇ
3182 ˇ )
3183 ˇ )
3184 );
3185 "});
3186 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3187 cx.assert_editor_state(indoc! {"
3188 const a: B = (
3189 c(
3190 d(
3191 ˇ
3192 ˇ)
3193 ˇ)
3194 );
3195 "});
3196 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3197 cx.assert_editor_state(indoc! {"
3198 const a: B = (
3199 c(
3200 d(
3201 ˇ
3202 ˇ)
3203 ˇ)
3204 );
3205 "});
3206
3207 // test when current indent is more than suggested indent,
3208 // we just move cursor to current indent instead of suggested indent
3209 //
3210 // when some other cursor is at word boundary, it doesn't move
3211 cx.set_state(indoc! {"
3212 const a: B = (
3213 c(
3214 d(
3215 ˇ
3216 ˇ )
3217 ˇ)
3218 );
3219 "});
3220 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3221 cx.assert_editor_state(indoc! {"
3222 const a: B = (
3223 c(
3224 d(
3225 ˇ
3226 ˇ)
3227 ˇ)
3228 );
3229 "});
3230
3231 // handle auto-indent when there are multiple cursors on the same line
3232 cx.set_state(indoc! {"
3233 const a: B = (
3234 c(
3235 ˇ ˇ
3236 ˇ )
3237 );
3238 "});
3239 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3240 cx.assert_editor_state(indoc! {"
3241 const a: B = (
3242 c(
3243 ˇ
3244 ˇ)
3245 );
3246 "});
3247}
3248
3249#[gpui::test]
3250async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3251 init_test(cx, |settings| {
3252 settings.defaults.tab_size = NonZeroU32::new(3)
3253 });
3254
3255 let mut cx = EditorTestContext::new(cx).await;
3256 cx.set_state(indoc! {"
3257 ˇ
3258 \t ˇ
3259 \t ˇ
3260 \t ˇ
3261 \t \t\t \t \t\t \t\t \t \t ˇ
3262 "});
3263
3264 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3265 cx.assert_editor_state(indoc! {"
3266 ˇ
3267 \t ˇ
3268 \t ˇ
3269 \t ˇ
3270 \t \t\t \t \t\t \t\t \t \t ˇ
3271 "});
3272}
3273
3274#[gpui::test]
3275async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3276 init_test(cx, |settings| {
3277 settings.defaults.tab_size = NonZeroU32::new(4)
3278 });
3279
3280 let language = Arc::new(
3281 Language::new(
3282 LanguageConfig::default(),
3283 Some(tree_sitter_rust::LANGUAGE.into()),
3284 )
3285 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3286 .unwrap(),
3287 );
3288
3289 let mut cx = EditorTestContext::new(cx).await;
3290 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3291 cx.set_state(indoc! {"
3292 fn a() {
3293 if b {
3294 \t ˇc
3295 }
3296 }
3297 "});
3298
3299 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3300 cx.assert_editor_state(indoc! {"
3301 fn a() {
3302 if b {
3303 ˇc
3304 }
3305 }
3306 "});
3307}
3308
3309#[gpui::test]
3310async fn test_indent_outdent(cx: &mut TestAppContext) {
3311 init_test(cx, |settings| {
3312 settings.defaults.tab_size = NonZeroU32::new(4);
3313 });
3314
3315 let mut cx = EditorTestContext::new(cx).await;
3316
3317 cx.set_state(indoc! {"
3318 «oneˇ» «twoˇ»
3319 three
3320 four
3321 "});
3322 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3323 cx.assert_editor_state(indoc! {"
3324 «oneˇ» «twoˇ»
3325 three
3326 four
3327 "});
3328
3329 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3330 cx.assert_editor_state(indoc! {"
3331 «oneˇ» «twoˇ»
3332 three
3333 four
3334 "});
3335
3336 // select across line ending
3337 cx.set_state(indoc! {"
3338 one two
3339 t«hree
3340 ˇ» four
3341 "});
3342 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3343 cx.assert_editor_state(indoc! {"
3344 one two
3345 t«hree
3346 ˇ» four
3347 "});
3348
3349 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3350 cx.assert_editor_state(indoc! {"
3351 one two
3352 t«hree
3353 ˇ» four
3354 "});
3355
3356 // Ensure that indenting/outdenting works when the cursor is at column 0.
3357 cx.set_state(indoc! {"
3358 one two
3359 ˇthree
3360 four
3361 "});
3362 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3363 cx.assert_editor_state(indoc! {"
3364 one two
3365 ˇthree
3366 four
3367 "});
3368
3369 cx.set_state(indoc! {"
3370 one two
3371 ˇ three
3372 four
3373 "});
3374 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3375 cx.assert_editor_state(indoc! {"
3376 one two
3377 ˇthree
3378 four
3379 "});
3380}
3381
3382#[gpui::test]
3383async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
3384 init_test(cx, |settings| {
3385 settings.defaults.hard_tabs = Some(true);
3386 });
3387
3388 let mut cx = EditorTestContext::new(cx).await;
3389
3390 // select two ranges on one line
3391 cx.set_state(indoc! {"
3392 «oneˇ» «twoˇ»
3393 three
3394 four
3395 "});
3396 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3397 cx.assert_editor_state(indoc! {"
3398 \t«oneˇ» «twoˇ»
3399 three
3400 four
3401 "});
3402 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3403 cx.assert_editor_state(indoc! {"
3404 \t\t«oneˇ» «twoˇ»
3405 three
3406 four
3407 "});
3408 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3409 cx.assert_editor_state(indoc! {"
3410 \t«oneˇ» «twoˇ»
3411 three
3412 four
3413 "});
3414 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3415 cx.assert_editor_state(indoc! {"
3416 «oneˇ» «twoˇ»
3417 three
3418 four
3419 "});
3420
3421 // select across a line ending
3422 cx.set_state(indoc! {"
3423 one two
3424 t«hree
3425 ˇ»four
3426 "});
3427 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3428 cx.assert_editor_state(indoc! {"
3429 one two
3430 \tt«hree
3431 ˇ»four
3432 "});
3433 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3434 cx.assert_editor_state(indoc! {"
3435 one two
3436 \t\tt«hree
3437 ˇ»four
3438 "});
3439 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3440 cx.assert_editor_state(indoc! {"
3441 one two
3442 \tt«hree
3443 ˇ»four
3444 "});
3445 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3446 cx.assert_editor_state(indoc! {"
3447 one two
3448 t«hree
3449 ˇ»four
3450 "});
3451
3452 // Ensure that indenting/outdenting works when the cursor is at column 0.
3453 cx.set_state(indoc! {"
3454 one two
3455 ˇthree
3456 four
3457 "});
3458 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3459 cx.assert_editor_state(indoc! {"
3460 one two
3461 ˇthree
3462 four
3463 "});
3464 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3465 cx.assert_editor_state(indoc! {"
3466 one two
3467 \tˇthree
3468 four
3469 "});
3470 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3471 cx.assert_editor_state(indoc! {"
3472 one two
3473 ˇthree
3474 four
3475 "});
3476}
3477
3478#[gpui::test]
3479fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
3480 init_test(cx, |settings| {
3481 settings.languages.extend([
3482 (
3483 "TOML".into(),
3484 LanguageSettingsContent {
3485 tab_size: NonZeroU32::new(2),
3486 ..Default::default()
3487 },
3488 ),
3489 (
3490 "Rust".into(),
3491 LanguageSettingsContent {
3492 tab_size: NonZeroU32::new(4),
3493 ..Default::default()
3494 },
3495 ),
3496 ]);
3497 });
3498
3499 let toml_language = Arc::new(Language::new(
3500 LanguageConfig {
3501 name: "TOML".into(),
3502 ..Default::default()
3503 },
3504 None,
3505 ));
3506 let rust_language = Arc::new(Language::new(
3507 LanguageConfig {
3508 name: "Rust".into(),
3509 ..Default::default()
3510 },
3511 None,
3512 ));
3513
3514 let toml_buffer =
3515 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
3516 let rust_buffer =
3517 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
3518 let multibuffer = cx.new(|cx| {
3519 let mut multibuffer = MultiBuffer::new(ReadWrite);
3520 multibuffer.push_excerpts(
3521 toml_buffer.clone(),
3522 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
3523 cx,
3524 );
3525 multibuffer.push_excerpts(
3526 rust_buffer.clone(),
3527 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
3528 cx,
3529 );
3530 multibuffer
3531 });
3532
3533 cx.add_window(|window, cx| {
3534 let mut editor = build_editor(multibuffer, window, cx);
3535
3536 assert_eq!(
3537 editor.text(cx),
3538 indoc! {"
3539 a = 1
3540 b = 2
3541
3542 const c: usize = 3;
3543 "}
3544 );
3545
3546 select_ranges(
3547 &mut editor,
3548 indoc! {"
3549 «aˇ» = 1
3550 b = 2
3551
3552 «const c:ˇ» usize = 3;
3553 "},
3554 window,
3555 cx,
3556 );
3557
3558 editor.tab(&Tab, window, cx);
3559 assert_text_with_selections(
3560 &mut editor,
3561 indoc! {"
3562 «aˇ» = 1
3563 b = 2
3564
3565 «const c:ˇ» usize = 3;
3566 "},
3567 cx,
3568 );
3569 editor.backtab(&Backtab, window, cx);
3570 assert_text_with_selections(
3571 &mut editor,
3572 indoc! {"
3573 «aˇ» = 1
3574 b = 2
3575
3576 «const c:ˇ» usize = 3;
3577 "},
3578 cx,
3579 );
3580
3581 editor
3582 });
3583}
3584
3585#[gpui::test]
3586async fn test_backspace(cx: &mut TestAppContext) {
3587 init_test(cx, |_| {});
3588
3589 let mut cx = EditorTestContext::new(cx).await;
3590
3591 // Basic backspace
3592 cx.set_state(indoc! {"
3593 onˇe two three
3594 fou«rˇ» five six
3595 seven «ˇeight nine
3596 »ten
3597 "});
3598 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3599 cx.assert_editor_state(indoc! {"
3600 oˇe two three
3601 fouˇ five six
3602 seven ˇten
3603 "});
3604
3605 // Test backspace inside and around indents
3606 cx.set_state(indoc! {"
3607 zero
3608 ˇone
3609 ˇtwo
3610 ˇ ˇ ˇ three
3611 ˇ ˇ four
3612 "});
3613 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3614 cx.assert_editor_state(indoc! {"
3615 zero
3616 ˇone
3617 ˇtwo
3618 ˇ threeˇ four
3619 "});
3620}
3621
3622#[gpui::test]
3623async fn test_delete(cx: &mut TestAppContext) {
3624 init_test(cx, |_| {});
3625
3626 let mut cx = EditorTestContext::new(cx).await;
3627 cx.set_state(indoc! {"
3628 onˇe two three
3629 fou«rˇ» five six
3630 seven «ˇeight nine
3631 »ten
3632 "});
3633 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
3634 cx.assert_editor_state(indoc! {"
3635 onˇ two three
3636 fouˇ five six
3637 seven ˇten
3638 "});
3639}
3640
3641#[gpui::test]
3642fn test_delete_line(cx: &mut TestAppContext) {
3643 init_test(cx, |_| {});
3644
3645 let editor = cx.add_window(|window, cx| {
3646 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3647 build_editor(buffer, window, cx)
3648 });
3649 _ = editor.update(cx, |editor, window, cx| {
3650 editor.change_selections(None, window, cx, |s| {
3651 s.select_display_ranges([
3652 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
3653 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3654 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3655 ])
3656 });
3657 editor.delete_line(&DeleteLine, window, cx);
3658 assert_eq!(editor.display_text(cx), "ghi");
3659 assert_eq!(
3660 editor.selections.display_ranges(cx),
3661 vec![
3662 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
3663 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
3664 ]
3665 );
3666 });
3667
3668 let editor = cx.add_window(|window, cx| {
3669 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3670 build_editor(buffer, window, cx)
3671 });
3672 _ = editor.update(cx, |editor, window, cx| {
3673 editor.change_selections(None, window, cx, |s| {
3674 s.select_display_ranges([
3675 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
3676 ])
3677 });
3678 editor.delete_line(&DeleteLine, window, cx);
3679 assert_eq!(editor.display_text(cx), "ghi\n");
3680 assert_eq!(
3681 editor.selections.display_ranges(cx),
3682 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
3683 );
3684 });
3685}
3686
3687#[gpui::test]
3688fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
3689 init_test(cx, |_| {});
3690
3691 cx.add_window(|window, cx| {
3692 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3693 let mut editor = build_editor(buffer.clone(), window, cx);
3694 let buffer = buffer.read(cx).as_singleton().unwrap();
3695
3696 assert_eq!(
3697 editor.selections.ranges::<Point>(cx),
3698 &[Point::new(0, 0)..Point::new(0, 0)]
3699 );
3700
3701 // When on single line, replace newline at end by space
3702 editor.join_lines(&JoinLines, window, cx);
3703 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3704 assert_eq!(
3705 editor.selections.ranges::<Point>(cx),
3706 &[Point::new(0, 3)..Point::new(0, 3)]
3707 );
3708
3709 // When multiple lines are selected, remove newlines that are spanned by the selection
3710 editor.change_selections(None, window, cx, |s| {
3711 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
3712 });
3713 editor.join_lines(&JoinLines, window, cx);
3714 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
3715 assert_eq!(
3716 editor.selections.ranges::<Point>(cx),
3717 &[Point::new(0, 11)..Point::new(0, 11)]
3718 );
3719
3720 // Undo should be transactional
3721 editor.undo(&Undo, window, cx);
3722 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3723 assert_eq!(
3724 editor.selections.ranges::<Point>(cx),
3725 &[Point::new(0, 5)..Point::new(2, 2)]
3726 );
3727
3728 // When joining an empty line don't insert a space
3729 editor.change_selections(None, window, cx, |s| {
3730 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
3731 });
3732 editor.join_lines(&JoinLines, window, cx);
3733 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
3734 assert_eq!(
3735 editor.selections.ranges::<Point>(cx),
3736 [Point::new(2, 3)..Point::new(2, 3)]
3737 );
3738
3739 // We can remove trailing newlines
3740 editor.join_lines(&JoinLines, window, cx);
3741 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3742 assert_eq!(
3743 editor.selections.ranges::<Point>(cx),
3744 [Point::new(2, 3)..Point::new(2, 3)]
3745 );
3746
3747 // We don't blow up on the last line
3748 editor.join_lines(&JoinLines, window, cx);
3749 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3750 assert_eq!(
3751 editor.selections.ranges::<Point>(cx),
3752 [Point::new(2, 3)..Point::new(2, 3)]
3753 );
3754
3755 // reset to test indentation
3756 editor.buffer.update(cx, |buffer, cx| {
3757 buffer.edit(
3758 [
3759 (Point::new(1, 0)..Point::new(1, 2), " "),
3760 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
3761 ],
3762 None,
3763 cx,
3764 )
3765 });
3766
3767 // We remove any leading spaces
3768 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
3769 editor.change_selections(None, window, cx, |s| {
3770 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
3771 });
3772 editor.join_lines(&JoinLines, window, cx);
3773 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
3774
3775 // We don't insert a space for a line containing only spaces
3776 editor.join_lines(&JoinLines, window, cx);
3777 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
3778
3779 // We ignore any leading tabs
3780 editor.join_lines(&JoinLines, window, cx);
3781 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
3782
3783 editor
3784 });
3785}
3786
3787#[gpui::test]
3788fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
3789 init_test(cx, |_| {});
3790
3791 cx.add_window(|window, cx| {
3792 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3793 let mut editor = build_editor(buffer.clone(), window, cx);
3794 let buffer = buffer.read(cx).as_singleton().unwrap();
3795
3796 editor.change_selections(None, window, cx, |s| {
3797 s.select_ranges([
3798 Point::new(0, 2)..Point::new(1, 1),
3799 Point::new(1, 2)..Point::new(1, 2),
3800 Point::new(3, 1)..Point::new(3, 2),
3801 ])
3802 });
3803
3804 editor.join_lines(&JoinLines, window, cx);
3805 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
3806
3807 assert_eq!(
3808 editor.selections.ranges::<Point>(cx),
3809 [
3810 Point::new(0, 7)..Point::new(0, 7),
3811 Point::new(1, 3)..Point::new(1, 3)
3812 ]
3813 );
3814 editor
3815 });
3816}
3817
3818#[gpui::test]
3819async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
3820 init_test(cx, |_| {});
3821
3822 let mut cx = EditorTestContext::new(cx).await;
3823
3824 let diff_base = r#"
3825 Line 0
3826 Line 1
3827 Line 2
3828 Line 3
3829 "#
3830 .unindent();
3831
3832 cx.set_state(
3833 &r#"
3834 ˇLine 0
3835 Line 1
3836 Line 2
3837 Line 3
3838 "#
3839 .unindent(),
3840 );
3841
3842 cx.set_head_text(&diff_base);
3843 executor.run_until_parked();
3844
3845 // Join lines
3846 cx.update_editor(|editor, window, cx| {
3847 editor.join_lines(&JoinLines, window, cx);
3848 });
3849 executor.run_until_parked();
3850
3851 cx.assert_editor_state(
3852 &r#"
3853 Line 0ˇ Line 1
3854 Line 2
3855 Line 3
3856 "#
3857 .unindent(),
3858 );
3859 // Join again
3860 cx.update_editor(|editor, window, cx| {
3861 editor.join_lines(&JoinLines, window, cx);
3862 });
3863 executor.run_until_parked();
3864
3865 cx.assert_editor_state(
3866 &r#"
3867 Line 0 Line 1ˇ Line 2
3868 Line 3
3869 "#
3870 .unindent(),
3871 );
3872}
3873
3874#[gpui::test]
3875async fn test_custom_newlines_cause_no_false_positive_diffs(
3876 executor: BackgroundExecutor,
3877 cx: &mut TestAppContext,
3878) {
3879 init_test(cx, |_| {});
3880 let mut cx = EditorTestContext::new(cx).await;
3881 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
3882 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
3883 executor.run_until_parked();
3884
3885 cx.update_editor(|editor, window, cx| {
3886 let snapshot = editor.snapshot(window, cx);
3887 assert_eq!(
3888 snapshot
3889 .buffer_snapshot
3890 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
3891 .collect::<Vec<_>>(),
3892 Vec::new(),
3893 "Should not have any diffs for files with custom newlines"
3894 );
3895 });
3896}
3897
3898#[gpui::test]
3899async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) {
3900 init_test(cx, |_| {});
3901
3902 let mut cx = EditorTestContext::new(cx).await;
3903
3904 // Test sort_lines_case_insensitive()
3905 cx.set_state(indoc! {"
3906 «z
3907 y
3908 x
3909 Z
3910 Y
3911 Xˇ»
3912 "});
3913 cx.update_editor(|e, window, cx| {
3914 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
3915 });
3916 cx.assert_editor_state(indoc! {"
3917 «x
3918 X
3919 y
3920 Y
3921 z
3922 Zˇ»
3923 "});
3924
3925 // Test reverse_lines()
3926 cx.set_state(indoc! {"
3927 «5
3928 4
3929 3
3930 2
3931 1ˇ»
3932 "});
3933 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
3934 cx.assert_editor_state(indoc! {"
3935 «1
3936 2
3937 3
3938 4
3939 5ˇ»
3940 "});
3941
3942 // Skip testing shuffle_line()
3943
3944 // From here on out, test more complex cases of manipulate_lines() with a single driver method: sort_lines_case_sensitive()
3945 // Since all methods calling manipulate_lines() are doing the exact same general thing (reordering lines)
3946
3947 // Don't manipulate when cursor is on single line, but expand the selection
3948 cx.set_state(indoc! {"
3949 ddˇdd
3950 ccc
3951 bb
3952 a
3953 "});
3954 cx.update_editor(|e, window, cx| {
3955 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3956 });
3957 cx.assert_editor_state(indoc! {"
3958 «ddddˇ»
3959 ccc
3960 bb
3961 a
3962 "});
3963
3964 // Basic manipulate case
3965 // Start selection moves to column 0
3966 // End of selection shrinks to fit shorter line
3967 cx.set_state(indoc! {"
3968 dd«d
3969 ccc
3970 bb
3971 aaaaaˇ»
3972 "});
3973 cx.update_editor(|e, window, cx| {
3974 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3975 });
3976 cx.assert_editor_state(indoc! {"
3977 «aaaaa
3978 bb
3979 ccc
3980 dddˇ»
3981 "});
3982
3983 // Manipulate case with newlines
3984 cx.set_state(indoc! {"
3985 dd«d
3986 ccc
3987
3988 bb
3989 aaaaa
3990
3991 ˇ»
3992 "});
3993 cx.update_editor(|e, window, cx| {
3994 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3995 });
3996 cx.assert_editor_state(indoc! {"
3997 «
3998
3999 aaaaa
4000 bb
4001 ccc
4002 dddˇ»
4003
4004 "});
4005
4006 // Adding new line
4007 cx.set_state(indoc! {"
4008 aa«a
4009 bbˇ»b
4010 "});
4011 cx.update_editor(|e, window, cx| {
4012 e.manipulate_lines(window, cx, |lines| lines.push("added_line"))
4013 });
4014 cx.assert_editor_state(indoc! {"
4015 «aaa
4016 bbb
4017 added_lineˇ»
4018 "});
4019
4020 // Removing line
4021 cx.set_state(indoc! {"
4022 aa«a
4023 bbbˇ»
4024 "});
4025 cx.update_editor(|e, window, cx| {
4026 e.manipulate_lines(window, cx, |lines| {
4027 lines.pop();
4028 })
4029 });
4030 cx.assert_editor_state(indoc! {"
4031 «aaaˇ»
4032 "});
4033
4034 // Removing all lines
4035 cx.set_state(indoc! {"
4036 aa«a
4037 bbbˇ»
4038 "});
4039 cx.update_editor(|e, window, cx| {
4040 e.manipulate_lines(window, cx, |lines| {
4041 lines.drain(..);
4042 })
4043 });
4044 cx.assert_editor_state(indoc! {"
4045 ˇ
4046 "});
4047}
4048
4049#[gpui::test]
4050async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4051 init_test(cx, |_| {});
4052
4053 let mut cx = EditorTestContext::new(cx).await;
4054
4055 // Consider continuous selection as single selection
4056 cx.set_state(indoc! {"
4057 Aaa«aa
4058 cˇ»c«c
4059 bb
4060 aaaˇ»aa
4061 "});
4062 cx.update_editor(|e, window, cx| {
4063 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4064 });
4065 cx.assert_editor_state(indoc! {"
4066 «Aaaaa
4067 ccc
4068 bb
4069 aaaaaˇ»
4070 "});
4071
4072 cx.set_state(indoc! {"
4073 Aaa«aa
4074 cˇ»c«c
4075 bb
4076 aaaˇ»aa
4077 "});
4078 cx.update_editor(|e, window, cx| {
4079 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4080 });
4081 cx.assert_editor_state(indoc! {"
4082 «Aaaaa
4083 ccc
4084 bbˇ»
4085 "});
4086
4087 // Consider non continuous selection as distinct dedup operations
4088 cx.set_state(indoc! {"
4089 «aaaaa
4090 bb
4091 aaaaa
4092 aaaaaˇ»
4093
4094 aaa«aaˇ»
4095 "});
4096 cx.update_editor(|e, window, cx| {
4097 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4098 });
4099 cx.assert_editor_state(indoc! {"
4100 «aaaaa
4101 bbˇ»
4102
4103 «aaaaaˇ»
4104 "});
4105}
4106
4107#[gpui::test]
4108async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4109 init_test(cx, |_| {});
4110
4111 let mut cx = EditorTestContext::new(cx).await;
4112
4113 cx.set_state(indoc! {"
4114 «Aaa
4115 aAa
4116 Aaaˇ»
4117 "});
4118 cx.update_editor(|e, window, cx| {
4119 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4120 });
4121 cx.assert_editor_state(indoc! {"
4122 «Aaa
4123 aAaˇ»
4124 "});
4125
4126 cx.set_state(indoc! {"
4127 «Aaa
4128 aAa
4129 aaAˇ»
4130 "});
4131 cx.update_editor(|e, window, cx| {
4132 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4133 });
4134 cx.assert_editor_state(indoc! {"
4135 «Aaaˇ»
4136 "});
4137}
4138
4139#[gpui::test]
4140async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) {
4141 init_test(cx, |_| {});
4142
4143 let mut cx = EditorTestContext::new(cx).await;
4144
4145 // Manipulate with multiple selections on a single line
4146 cx.set_state(indoc! {"
4147 dd«dd
4148 cˇ»c«c
4149 bb
4150 aaaˇ»aa
4151 "});
4152 cx.update_editor(|e, window, cx| {
4153 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4154 });
4155 cx.assert_editor_state(indoc! {"
4156 «aaaaa
4157 bb
4158 ccc
4159 ddddˇ»
4160 "});
4161
4162 // Manipulate with multiple disjoin selections
4163 cx.set_state(indoc! {"
4164 5«
4165 4
4166 3
4167 2
4168 1ˇ»
4169
4170 dd«dd
4171 ccc
4172 bb
4173 aaaˇ»aa
4174 "});
4175 cx.update_editor(|e, window, cx| {
4176 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4177 });
4178 cx.assert_editor_state(indoc! {"
4179 «1
4180 2
4181 3
4182 4
4183 5ˇ»
4184
4185 «aaaaa
4186 bb
4187 ccc
4188 ddddˇ»
4189 "});
4190
4191 // Adding lines on each selection
4192 cx.set_state(indoc! {"
4193 2«
4194 1ˇ»
4195
4196 bb«bb
4197 aaaˇ»aa
4198 "});
4199 cx.update_editor(|e, window, cx| {
4200 e.manipulate_lines(window, cx, |lines| lines.push("added line"))
4201 });
4202 cx.assert_editor_state(indoc! {"
4203 «2
4204 1
4205 added lineˇ»
4206
4207 «bbbb
4208 aaaaa
4209 added lineˇ»
4210 "});
4211
4212 // Removing lines on each selection
4213 cx.set_state(indoc! {"
4214 2«
4215 1ˇ»
4216
4217 bb«bb
4218 aaaˇ»aa
4219 "});
4220 cx.update_editor(|e, window, cx| {
4221 e.manipulate_lines(window, cx, |lines| {
4222 lines.pop();
4223 })
4224 });
4225 cx.assert_editor_state(indoc! {"
4226 «2ˇ»
4227
4228 «bbbbˇ»
4229 "});
4230}
4231
4232#[gpui::test]
4233async fn test_toggle_case(cx: &mut TestAppContext) {
4234 init_test(cx, |_| {});
4235
4236 let mut cx = EditorTestContext::new(cx).await;
4237
4238 // If all lower case -> upper case
4239 cx.set_state(indoc! {"
4240 «hello worldˇ»
4241 "});
4242 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4243 cx.assert_editor_state(indoc! {"
4244 «HELLO WORLDˇ»
4245 "});
4246
4247 // If all upper case -> lower case
4248 cx.set_state(indoc! {"
4249 «HELLO WORLDˇ»
4250 "});
4251 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4252 cx.assert_editor_state(indoc! {"
4253 «hello worldˇ»
4254 "});
4255
4256 // If any upper case characters are identified -> lower case
4257 // This matches JetBrains IDEs
4258 cx.set_state(indoc! {"
4259 «hEllo worldˇ»
4260 "});
4261 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4262 cx.assert_editor_state(indoc! {"
4263 «hello worldˇ»
4264 "});
4265}
4266
4267#[gpui::test]
4268async fn test_manipulate_text(cx: &mut TestAppContext) {
4269 init_test(cx, |_| {});
4270
4271 let mut cx = EditorTestContext::new(cx).await;
4272
4273 // Test convert_to_upper_case()
4274 cx.set_state(indoc! {"
4275 «hello worldˇ»
4276 "});
4277 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4278 cx.assert_editor_state(indoc! {"
4279 «HELLO WORLDˇ»
4280 "});
4281
4282 // Test convert_to_lower_case()
4283 cx.set_state(indoc! {"
4284 «HELLO WORLDˇ»
4285 "});
4286 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
4287 cx.assert_editor_state(indoc! {"
4288 «hello worldˇ»
4289 "});
4290
4291 // Test multiple line, single selection case
4292 cx.set_state(indoc! {"
4293 «The quick brown
4294 fox jumps over
4295 the lazy dogˇ»
4296 "});
4297 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
4298 cx.assert_editor_state(indoc! {"
4299 «The Quick Brown
4300 Fox Jumps Over
4301 The Lazy Dogˇ»
4302 "});
4303
4304 // Test multiple line, single selection case
4305 cx.set_state(indoc! {"
4306 «The quick brown
4307 fox jumps over
4308 the lazy dogˇ»
4309 "});
4310 cx.update_editor(|e, window, cx| {
4311 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
4312 });
4313 cx.assert_editor_state(indoc! {"
4314 «TheQuickBrown
4315 FoxJumpsOver
4316 TheLazyDogˇ»
4317 "});
4318
4319 // From here on out, test more complex cases of manipulate_text()
4320
4321 // Test no selection case - should affect words cursors are in
4322 // Cursor at beginning, middle, and end of word
4323 cx.set_state(indoc! {"
4324 ˇhello big beauˇtiful worldˇ
4325 "});
4326 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4327 cx.assert_editor_state(indoc! {"
4328 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
4329 "});
4330
4331 // Test multiple selections on a single line and across multiple lines
4332 cx.set_state(indoc! {"
4333 «Theˇ» quick «brown
4334 foxˇ» jumps «overˇ»
4335 the «lazyˇ» dog
4336 "});
4337 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4338 cx.assert_editor_state(indoc! {"
4339 «THEˇ» quick «BROWN
4340 FOXˇ» jumps «OVERˇ»
4341 the «LAZYˇ» dog
4342 "});
4343
4344 // Test case where text length grows
4345 cx.set_state(indoc! {"
4346 «tschüߡ»
4347 "});
4348 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4349 cx.assert_editor_state(indoc! {"
4350 «TSCHÜSSˇ»
4351 "});
4352
4353 // Test to make sure we don't crash when text shrinks
4354 cx.set_state(indoc! {"
4355 aaa_bbbˇ
4356 "});
4357 cx.update_editor(|e, window, cx| {
4358 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4359 });
4360 cx.assert_editor_state(indoc! {"
4361 «aaaBbbˇ»
4362 "});
4363
4364 // Test to make sure we all aware of the fact that each word can grow and shrink
4365 // Final selections should be aware of this fact
4366 cx.set_state(indoc! {"
4367 aaa_bˇbb bbˇb_ccc ˇccc_ddd
4368 "});
4369 cx.update_editor(|e, window, cx| {
4370 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4371 });
4372 cx.assert_editor_state(indoc! {"
4373 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
4374 "});
4375
4376 cx.set_state(indoc! {"
4377 «hElLo, WoRld!ˇ»
4378 "});
4379 cx.update_editor(|e, window, cx| {
4380 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
4381 });
4382 cx.assert_editor_state(indoc! {"
4383 «HeLlO, wOrLD!ˇ»
4384 "});
4385}
4386
4387#[gpui::test]
4388fn test_duplicate_line(cx: &mut TestAppContext) {
4389 init_test(cx, |_| {});
4390
4391 let editor = cx.add_window(|window, cx| {
4392 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4393 build_editor(buffer, window, cx)
4394 });
4395 _ = editor.update(cx, |editor, window, cx| {
4396 editor.change_selections(None, window, cx, |s| {
4397 s.select_display_ranges([
4398 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4399 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4400 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4401 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4402 ])
4403 });
4404 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4405 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4406 assert_eq!(
4407 editor.selections.display_ranges(cx),
4408 vec![
4409 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4410 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
4411 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4412 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4413 ]
4414 );
4415 });
4416
4417 let editor = cx.add_window(|window, cx| {
4418 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4419 build_editor(buffer, window, cx)
4420 });
4421 _ = editor.update(cx, |editor, window, cx| {
4422 editor.change_selections(None, window, cx, |s| {
4423 s.select_display_ranges([
4424 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4425 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4426 ])
4427 });
4428 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4429 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4430 assert_eq!(
4431 editor.selections.display_ranges(cx),
4432 vec![
4433 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
4434 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
4435 ]
4436 );
4437 });
4438
4439 // With `move_upwards` the selections stay in place, except for
4440 // the lines inserted above them
4441 let editor = cx.add_window(|window, cx| {
4442 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4443 build_editor(buffer, window, cx)
4444 });
4445 _ = editor.update(cx, |editor, window, cx| {
4446 editor.change_selections(None, window, cx, |s| {
4447 s.select_display_ranges([
4448 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4449 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4450 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4451 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4452 ])
4453 });
4454 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4455 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4456 assert_eq!(
4457 editor.selections.display_ranges(cx),
4458 vec![
4459 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4460 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4461 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
4462 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4463 ]
4464 );
4465 });
4466
4467 let editor = cx.add_window(|window, cx| {
4468 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4469 build_editor(buffer, window, cx)
4470 });
4471 _ = editor.update(cx, |editor, window, cx| {
4472 editor.change_selections(None, window, cx, |s| {
4473 s.select_display_ranges([
4474 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4475 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4476 ])
4477 });
4478 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4479 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4480 assert_eq!(
4481 editor.selections.display_ranges(cx),
4482 vec![
4483 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4484 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4485 ]
4486 );
4487 });
4488
4489 let editor = cx.add_window(|window, cx| {
4490 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4491 build_editor(buffer, window, cx)
4492 });
4493 _ = editor.update(cx, |editor, window, cx| {
4494 editor.change_selections(None, window, cx, |s| {
4495 s.select_display_ranges([
4496 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4497 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4498 ])
4499 });
4500 editor.duplicate_selection(&DuplicateSelection, window, cx);
4501 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
4502 assert_eq!(
4503 editor.selections.display_ranges(cx),
4504 vec![
4505 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4506 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
4507 ]
4508 );
4509 });
4510}
4511
4512#[gpui::test]
4513fn test_move_line_up_down(cx: &mut TestAppContext) {
4514 init_test(cx, |_| {});
4515
4516 let editor = cx.add_window(|window, cx| {
4517 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4518 build_editor(buffer, window, cx)
4519 });
4520 _ = editor.update(cx, |editor, window, cx| {
4521 editor.fold_creases(
4522 vec![
4523 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
4524 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
4525 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
4526 ],
4527 true,
4528 window,
4529 cx,
4530 );
4531 editor.change_selections(None, window, cx, |s| {
4532 s.select_display_ranges([
4533 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4534 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4535 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4536 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
4537 ])
4538 });
4539 assert_eq!(
4540 editor.display_text(cx),
4541 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
4542 );
4543
4544 editor.move_line_up(&MoveLineUp, window, cx);
4545 assert_eq!(
4546 editor.display_text(cx),
4547 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
4548 );
4549 assert_eq!(
4550 editor.selections.display_ranges(cx),
4551 vec![
4552 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4553 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4554 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4555 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4556 ]
4557 );
4558 });
4559
4560 _ = editor.update(cx, |editor, window, cx| {
4561 editor.move_line_down(&MoveLineDown, window, cx);
4562 assert_eq!(
4563 editor.display_text(cx),
4564 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
4565 );
4566 assert_eq!(
4567 editor.selections.display_ranges(cx),
4568 vec![
4569 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4570 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4571 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4572 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4573 ]
4574 );
4575 });
4576
4577 _ = editor.update(cx, |editor, window, cx| {
4578 editor.move_line_down(&MoveLineDown, window, cx);
4579 assert_eq!(
4580 editor.display_text(cx),
4581 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
4582 );
4583 assert_eq!(
4584 editor.selections.display_ranges(cx),
4585 vec![
4586 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4587 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4588 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4589 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4590 ]
4591 );
4592 });
4593
4594 _ = editor.update(cx, |editor, window, cx| {
4595 editor.move_line_up(&MoveLineUp, window, cx);
4596 assert_eq!(
4597 editor.display_text(cx),
4598 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
4599 );
4600 assert_eq!(
4601 editor.selections.display_ranges(cx),
4602 vec![
4603 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4604 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4605 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4606 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4607 ]
4608 );
4609 });
4610}
4611
4612#[gpui::test]
4613fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
4614 init_test(cx, |_| {});
4615
4616 let editor = cx.add_window(|window, cx| {
4617 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4618 build_editor(buffer, window, cx)
4619 });
4620 _ = editor.update(cx, |editor, window, cx| {
4621 let snapshot = editor.buffer.read(cx).snapshot(cx);
4622 editor.insert_blocks(
4623 [BlockProperties {
4624 style: BlockStyle::Fixed,
4625 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
4626 height: Some(1),
4627 render: Arc::new(|_| div().into_any()),
4628 priority: 0,
4629 render_in_minimap: true,
4630 }],
4631 Some(Autoscroll::fit()),
4632 cx,
4633 );
4634 editor.change_selections(None, window, cx, |s| {
4635 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
4636 });
4637 editor.move_line_down(&MoveLineDown, window, cx);
4638 });
4639}
4640
4641#[gpui::test]
4642async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
4643 init_test(cx, |_| {});
4644
4645 let mut cx = EditorTestContext::new(cx).await;
4646 cx.set_state(
4647 &"
4648 ˇzero
4649 one
4650 two
4651 three
4652 four
4653 five
4654 "
4655 .unindent(),
4656 );
4657
4658 // Create a four-line block that replaces three lines of text.
4659 cx.update_editor(|editor, window, cx| {
4660 let snapshot = editor.snapshot(window, cx);
4661 let snapshot = &snapshot.buffer_snapshot;
4662 let placement = BlockPlacement::Replace(
4663 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
4664 );
4665 editor.insert_blocks(
4666 [BlockProperties {
4667 placement,
4668 height: Some(4),
4669 style: BlockStyle::Sticky,
4670 render: Arc::new(|_| gpui::div().into_any_element()),
4671 priority: 0,
4672 render_in_minimap: true,
4673 }],
4674 None,
4675 cx,
4676 );
4677 });
4678
4679 // Move down so that the cursor touches the block.
4680 cx.update_editor(|editor, window, cx| {
4681 editor.move_down(&Default::default(), window, cx);
4682 });
4683 cx.assert_editor_state(
4684 &"
4685 zero
4686 «one
4687 two
4688 threeˇ»
4689 four
4690 five
4691 "
4692 .unindent(),
4693 );
4694
4695 // Move down past the block.
4696 cx.update_editor(|editor, window, cx| {
4697 editor.move_down(&Default::default(), window, cx);
4698 });
4699 cx.assert_editor_state(
4700 &"
4701 zero
4702 one
4703 two
4704 three
4705 ˇfour
4706 five
4707 "
4708 .unindent(),
4709 );
4710}
4711
4712#[gpui::test]
4713fn test_transpose(cx: &mut TestAppContext) {
4714 init_test(cx, |_| {});
4715
4716 _ = cx.add_window(|window, cx| {
4717 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
4718 editor.set_style(EditorStyle::default(), window, cx);
4719 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
4720 editor.transpose(&Default::default(), window, cx);
4721 assert_eq!(editor.text(cx), "bac");
4722 assert_eq!(editor.selections.ranges(cx), [2..2]);
4723
4724 editor.transpose(&Default::default(), window, cx);
4725 assert_eq!(editor.text(cx), "bca");
4726 assert_eq!(editor.selections.ranges(cx), [3..3]);
4727
4728 editor.transpose(&Default::default(), window, cx);
4729 assert_eq!(editor.text(cx), "bac");
4730 assert_eq!(editor.selections.ranges(cx), [3..3]);
4731
4732 editor
4733 });
4734
4735 _ = cx.add_window(|window, cx| {
4736 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4737 editor.set_style(EditorStyle::default(), window, cx);
4738 editor.change_selections(None, window, cx, |s| s.select_ranges([3..3]));
4739 editor.transpose(&Default::default(), window, cx);
4740 assert_eq!(editor.text(cx), "acb\nde");
4741 assert_eq!(editor.selections.ranges(cx), [3..3]);
4742
4743 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4744 editor.transpose(&Default::default(), window, cx);
4745 assert_eq!(editor.text(cx), "acbd\ne");
4746 assert_eq!(editor.selections.ranges(cx), [5..5]);
4747
4748 editor.transpose(&Default::default(), window, cx);
4749 assert_eq!(editor.text(cx), "acbde\n");
4750 assert_eq!(editor.selections.ranges(cx), [6..6]);
4751
4752 editor.transpose(&Default::default(), window, cx);
4753 assert_eq!(editor.text(cx), "acbd\ne");
4754 assert_eq!(editor.selections.ranges(cx), [6..6]);
4755
4756 editor
4757 });
4758
4759 _ = cx.add_window(|window, cx| {
4760 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4761 editor.set_style(EditorStyle::default(), window, cx);
4762 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1, 2..2, 4..4]));
4763 editor.transpose(&Default::default(), window, cx);
4764 assert_eq!(editor.text(cx), "bacd\ne");
4765 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
4766
4767 editor.transpose(&Default::default(), window, cx);
4768 assert_eq!(editor.text(cx), "bcade\n");
4769 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
4770
4771 editor.transpose(&Default::default(), window, cx);
4772 assert_eq!(editor.text(cx), "bcda\ne");
4773 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4774
4775 editor.transpose(&Default::default(), window, cx);
4776 assert_eq!(editor.text(cx), "bcade\n");
4777 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4778
4779 editor.transpose(&Default::default(), window, cx);
4780 assert_eq!(editor.text(cx), "bcaed\n");
4781 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
4782
4783 editor
4784 });
4785
4786 _ = cx.add_window(|window, cx| {
4787 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
4788 editor.set_style(EditorStyle::default(), window, cx);
4789 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4790 editor.transpose(&Default::default(), window, cx);
4791 assert_eq!(editor.text(cx), "🏀🍐✋");
4792 assert_eq!(editor.selections.ranges(cx), [8..8]);
4793
4794 editor.transpose(&Default::default(), window, cx);
4795 assert_eq!(editor.text(cx), "🏀✋🍐");
4796 assert_eq!(editor.selections.ranges(cx), [11..11]);
4797
4798 editor.transpose(&Default::default(), window, cx);
4799 assert_eq!(editor.text(cx), "🏀🍐✋");
4800 assert_eq!(editor.selections.ranges(cx), [11..11]);
4801
4802 editor
4803 });
4804}
4805
4806#[gpui::test]
4807async fn test_rewrap(cx: &mut TestAppContext) {
4808 init_test(cx, |settings| {
4809 settings.languages.extend([
4810 (
4811 "Markdown".into(),
4812 LanguageSettingsContent {
4813 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4814 ..Default::default()
4815 },
4816 ),
4817 (
4818 "Plain Text".into(),
4819 LanguageSettingsContent {
4820 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4821 ..Default::default()
4822 },
4823 ),
4824 ])
4825 });
4826
4827 let mut cx = EditorTestContext::new(cx).await;
4828
4829 let language_with_c_comments = Arc::new(Language::new(
4830 LanguageConfig {
4831 line_comments: vec!["// ".into()],
4832 ..LanguageConfig::default()
4833 },
4834 None,
4835 ));
4836 let language_with_pound_comments = Arc::new(Language::new(
4837 LanguageConfig {
4838 line_comments: vec!["# ".into()],
4839 ..LanguageConfig::default()
4840 },
4841 None,
4842 ));
4843 let markdown_language = Arc::new(Language::new(
4844 LanguageConfig {
4845 name: "Markdown".into(),
4846 ..LanguageConfig::default()
4847 },
4848 None,
4849 ));
4850 let language_with_doc_comments = Arc::new(Language::new(
4851 LanguageConfig {
4852 line_comments: vec!["// ".into(), "/// ".into()],
4853 ..LanguageConfig::default()
4854 },
4855 Some(tree_sitter_rust::LANGUAGE.into()),
4856 ));
4857
4858 let plaintext_language = Arc::new(Language::new(
4859 LanguageConfig {
4860 name: "Plain Text".into(),
4861 ..LanguageConfig::default()
4862 },
4863 None,
4864 ));
4865
4866 assert_rewrap(
4867 indoc! {"
4868 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
4869 "},
4870 indoc! {"
4871 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4872 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4873 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4874 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4875 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4876 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4877 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4878 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4879 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4880 // porttitor id. Aliquam id accumsan eros.
4881 "},
4882 language_with_c_comments.clone(),
4883 &mut cx,
4884 );
4885
4886 // Test that rewrapping works inside of a selection
4887 assert_rewrap(
4888 indoc! {"
4889 «// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.ˇ»
4890 "},
4891 indoc! {"
4892 «// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4893 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4894 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4895 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4896 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4897 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4898 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4899 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4900 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4901 // porttitor id. Aliquam id accumsan eros.ˇ»
4902 "},
4903 language_with_c_comments.clone(),
4904 &mut cx,
4905 );
4906
4907 // Test that cursors that expand to the same region are collapsed.
4908 assert_rewrap(
4909 indoc! {"
4910 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4911 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4912 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4913 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
4914 "},
4915 indoc! {"
4916 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4917 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4918 // auctor, eu lacinia sapien scelerisque. ˇVivamus sit amet neque et quam
4919 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4920 // Pellentesque odio lectus, iaculis ac volutpat et, ˇblandit quis urna. Sed
4921 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4922 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4923 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4924 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4925 // porttitor id. Aliquam id accumsan eros.
4926 "},
4927 language_with_c_comments.clone(),
4928 &mut cx,
4929 );
4930
4931 // Test that non-contiguous selections are treated separately.
4932 assert_rewrap(
4933 indoc! {"
4934 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4935 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4936 //
4937 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4938 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
4939 "},
4940 indoc! {"
4941 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4942 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4943 // auctor, eu lacinia sapien scelerisque.
4944 //
4945 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas
4946 // tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4947 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec
4948 // molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque
4949 // nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas
4950 // porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id
4951 // vulputate turpis porttitor id. Aliquam id accumsan eros.
4952 "},
4953 language_with_c_comments.clone(),
4954 &mut cx,
4955 );
4956
4957 // Test that different comment prefixes are supported.
4958 assert_rewrap(
4959 indoc! {"
4960 # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
4961 "},
4962 indoc! {"
4963 # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4964 # purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4965 # eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4966 # hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4967 # lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit
4968 # amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet
4969 # in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur
4970 # adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis.
4971 # Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id
4972 # accumsan eros.
4973 "},
4974 language_with_pound_comments.clone(),
4975 &mut cx,
4976 );
4977
4978 // Test that rewrapping is ignored outside of comments in most languages.
4979 assert_rewrap(
4980 indoc! {"
4981 /// Adds two numbers.
4982 /// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
4983 fn add(a: u32, b: u32) -> u32 {
4984 a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + bˇ
4985 }
4986 "},
4987 indoc! {"
4988 /// Adds two numbers. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
4989 /// Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
4990 fn add(a: u32, b: u32) -> u32 {
4991 a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + bˇ
4992 }
4993 "},
4994 language_with_doc_comments.clone(),
4995 &mut cx,
4996 );
4997
4998 // Test that rewrapping works in Markdown and Plain Text languages.
4999 assert_rewrap(
5000 indoc! {"
5001 # Hello
5002
5003 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi.
5004 "},
5005 indoc! {"
5006 # Hello
5007
5008 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
5009 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
5010 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
5011 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
5012 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
5013 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
5014 Integer sit amet scelerisque nisi.
5015 "},
5016 markdown_language,
5017 &mut cx,
5018 );
5019
5020 assert_rewrap(
5021 indoc! {"
5022 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi.
5023 "},
5024 indoc! {"
5025 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
5026 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
5027 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
5028 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
5029 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
5030 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
5031 Integer sit amet scelerisque nisi.
5032 "},
5033 plaintext_language,
5034 &mut cx,
5035 );
5036
5037 // Test rewrapping unaligned comments in a selection.
5038 assert_rewrap(
5039 indoc! {"
5040 fn foo() {
5041 if true {
5042 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
5043 // Praesent semper egestas tellus id dignissim.ˇ»
5044 do_something();
5045 } else {
5046 //
5047 }
5048 }
5049 "},
5050 indoc! {"
5051 fn foo() {
5052 if true {
5053 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
5054 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
5055 // egestas tellus id dignissim.ˇ»
5056 do_something();
5057 } else {
5058 //
5059 }
5060 }
5061 "},
5062 language_with_doc_comments.clone(),
5063 &mut cx,
5064 );
5065
5066 assert_rewrap(
5067 indoc! {"
5068 fn foo() {
5069 if true {
5070 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
5071 // Praesent semper egestas tellus id dignissim.»
5072 do_something();
5073 } else {
5074 //
5075 }
5076
5077 }
5078 "},
5079 indoc! {"
5080 fn foo() {
5081 if true {
5082 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
5083 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
5084 // egestas tellus id dignissim.»
5085 do_something();
5086 } else {
5087 //
5088 }
5089
5090 }
5091 "},
5092 language_with_doc_comments.clone(),
5093 &mut cx,
5094 );
5095
5096 #[track_caller]
5097 fn assert_rewrap(
5098 unwrapped_text: &str,
5099 wrapped_text: &str,
5100 language: Arc<Language>,
5101 cx: &mut EditorTestContext,
5102 ) {
5103 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
5104 cx.set_state(unwrapped_text);
5105 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
5106 cx.assert_editor_state(wrapped_text);
5107 }
5108}
5109
5110#[gpui::test]
5111async fn test_hard_wrap(cx: &mut TestAppContext) {
5112 init_test(cx, |_| {});
5113 let mut cx = EditorTestContext::new(cx).await;
5114
5115 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
5116 cx.update_editor(|editor, _, cx| {
5117 editor.set_hard_wrap(Some(14), cx);
5118 });
5119
5120 cx.set_state(indoc!(
5121 "
5122 one two three ˇ
5123 "
5124 ));
5125 cx.simulate_input("four");
5126 cx.run_until_parked();
5127
5128 cx.assert_editor_state(indoc!(
5129 "
5130 one two three
5131 fourˇ
5132 "
5133 ));
5134
5135 cx.update_editor(|editor, window, cx| {
5136 editor.newline(&Default::default(), window, cx);
5137 });
5138 cx.run_until_parked();
5139 cx.assert_editor_state(indoc!(
5140 "
5141 one two three
5142 four
5143 ˇ
5144 "
5145 ));
5146
5147 cx.simulate_input("five");
5148 cx.run_until_parked();
5149 cx.assert_editor_state(indoc!(
5150 "
5151 one two three
5152 four
5153 fiveˇ
5154 "
5155 ));
5156
5157 cx.update_editor(|editor, window, cx| {
5158 editor.newline(&Default::default(), window, cx);
5159 });
5160 cx.run_until_parked();
5161 cx.simulate_input("# ");
5162 cx.run_until_parked();
5163 cx.assert_editor_state(indoc!(
5164 "
5165 one two three
5166 four
5167 five
5168 # ˇ
5169 "
5170 ));
5171
5172 cx.update_editor(|editor, window, cx| {
5173 editor.newline(&Default::default(), window, cx);
5174 });
5175 cx.run_until_parked();
5176 cx.assert_editor_state(indoc!(
5177 "
5178 one two three
5179 four
5180 five
5181 #\x20
5182 #ˇ
5183 "
5184 ));
5185
5186 cx.simulate_input(" 6");
5187 cx.run_until_parked();
5188 cx.assert_editor_state(indoc!(
5189 "
5190 one two three
5191 four
5192 five
5193 #
5194 # 6ˇ
5195 "
5196 ));
5197}
5198
5199#[gpui::test]
5200async fn test_clipboard(cx: &mut TestAppContext) {
5201 init_test(cx, |_| {});
5202
5203 let mut cx = EditorTestContext::new(cx).await;
5204
5205 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
5206 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5207 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
5208
5209 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
5210 cx.set_state("two ˇfour ˇsix ˇ");
5211 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5212 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
5213
5214 // Paste again but with only two cursors. Since the number of cursors doesn't
5215 // match the number of slices in the clipboard, the entire clipboard text
5216 // is pasted at each cursor.
5217 cx.set_state("ˇtwo one✅ four three six five ˇ");
5218 cx.update_editor(|e, window, cx| {
5219 e.handle_input("( ", window, cx);
5220 e.paste(&Paste, window, cx);
5221 e.handle_input(") ", window, cx);
5222 });
5223 cx.assert_editor_state(
5224 &([
5225 "( one✅ ",
5226 "three ",
5227 "five ) ˇtwo one✅ four three six five ( one✅ ",
5228 "three ",
5229 "five ) ˇ",
5230 ]
5231 .join("\n")),
5232 );
5233
5234 // Cut with three selections, one of which is full-line.
5235 cx.set_state(indoc! {"
5236 1«2ˇ»3
5237 4ˇ567
5238 «8ˇ»9"});
5239 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5240 cx.assert_editor_state(indoc! {"
5241 1ˇ3
5242 ˇ9"});
5243
5244 // Paste with three selections, noticing how the copied selection that was full-line
5245 // gets inserted before the second cursor.
5246 cx.set_state(indoc! {"
5247 1ˇ3
5248 9ˇ
5249 «oˇ»ne"});
5250 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5251 cx.assert_editor_state(indoc! {"
5252 12ˇ3
5253 4567
5254 9ˇ
5255 8ˇne"});
5256
5257 // Copy with a single cursor only, which writes the whole line into the clipboard.
5258 cx.set_state(indoc! {"
5259 The quick brown
5260 fox juˇmps over
5261 the lazy dog"});
5262 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5263 assert_eq!(
5264 cx.read_from_clipboard()
5265 .and_then(|item| item.text().as_deref().map(str::to_string)),
5266 Some("fox jumps over\n".to_string())
5267 );
5268
5269 // Paste with three selections, noticing how the copied full-line selection is inserted
5270 // before the empty selections but replaces the selection that is non-empty.
5271 cx.set_state(indoc! {"
5272 Tˇhe quick brown
5273 «foˇ»x jumps over
5274 tˇhe lazy dog"});
5275 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5276 cx.assert_editor_state(indoc! {"
5277 fox jumps over
5278 Tˇhe quick brown
5279 fox jumps over
5280 ˇx jumps over
5281 fox jumps over
5282 tˇhe lazy dog"});
5283}
5284
5285#[gpui::test]
5286async fn test_copy_trim(cx: &mut TestAppContext) {
5287 init_test(cx, |_| {});
5288
5289 let mut cx = EditorTestContext::new(cx).await;
5290 cx.set_state(
5291 r#" «for selection in selections.iter() {
5292 let mut start = selection.start;
5293 let mut end = selection.end;
5294 let is_entire_line = selection.is_empty();
5295 if is_entire_line {
5296 start = Point::new(start.row, 0);ˇ»
5297 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5298 }
5299 "#,
5300 );
5301 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5302 assert_eq!(
5303 cx.read_from_clipboard()
5304 .and_then(|item| item.text().as_deref().map(str::to_string)),
5305 Some(
5306 "for selection in selections.iter() {
5307 let mut start = selection.start;
5308 let mut end = selection.end;
5309 let is_entire_line = selection.is_empty();
5310 if is_entire_line {
5311 start = Point::new(start.row, 0);"
5312 .to_string()
5313 ),
5314 "Regular copying preserves all indentation selected",
5315 );
5316 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5317 assert_eq!(
5318 cx.read_from_clipboard()
5319 .and_then(|item| item.text().as_deref().map(str::to_string)),
5320 Some(
5321 "for selection in selections.iter() {
5322let mut start = selection.start;
5323let mut end = selection.end;
5324let is_entire_line = selection.is_empty();
5325if is_entire_line {
5326 start = Point::new(start.row, 0);"
5327 .to_string()
5328 ),
5329 "Copying with stripping should strip all leading whitespaces"
5330 );
5331
5332 cx.set_state(
5333 r#" « for selection in selections.iter() {
5334 let mut start = selection.start;
5335 let mut end = selection.end;
5336 let is_entire_line = selection.is_empty();
5337 if is_entire_line {
5338 start = Point::new(start.row, 0);ˇ»
5339 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5340 }
5341 "#,
5342 );
5343 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5344 assert_eq!(
5345 cx.read_from_clipboard()
5346 .and_then(|item| item.text().as_deref().map(str::to_string)),
5347 Some(
5348 " for selection in selections.iter() {
5349 let mut start = selection.start;
5350 let mut end = selection.end;
5351 let is_entire_line = selection.is_empty();
5352 if is_entire_line {
5353 start = Point::new(start.row, 0);"
5354 .to_string()
5355 ),
5356 "Regular copying preserves all indentation selected",
5357 );
5358 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5359 assert_eq!(
5360 cx.read_from_clipboard()
5361 .and_then(|item| item.text().as_deref().map(str::to_string)),
5362 Some(
5363 "for selection in selections.iter() {
5364let mut start = selection.start;
5365let mut end = selection.end;
5366let is_entire_line = selection.is_empty();
5367if is_entire_line {
5368 start = Point::new(start.row, 0);"
5369 .to_string()
5370 ),
5371 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
5372 );
5373
5374 cx.set_state(
5375 r#" «ˇ for selection in selections.iter() {
5376 let mut start = selection.start;
5377 let mut end = selection.end;
5378 let is_entire_line = selection.is_empty();
5379 if is_entire_line {
5380 start = Point::new(start.row, 0);»
5381 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5382 }
5383 "#,
5384 );
5385 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5386 assert_eq!(
5387 cx.read_from_clipboard()
5388 .and_then(|item| item.text().as_deref().map(str::to_string)),
5389 Some(
5390 " for selection in selections.iter() {
5391 let mut start = selection.start;
5392 let mut end = selection.end;
5393 let is_entire_line = selection.is_empty();
5394 if is_entire_line {
5395 start = Point::new(start.row, 0);"
5396 .to_string()
5397 ),
5398 "Regular copying for reverse selection works the same",
5399 );
5400 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5401 assert_eq!(
5402 cx.read_from_clipboard()
5403 .and_then(|item| item.text().as_deref().map(str::to_string)),
5404 Some(
5405 "for selection in selections.iter() {
5406let mut start = selection.start;
5407let mut end = selection.end;
5408let is_entire_line = selection.is_empty();
5409if is_entire_line {
5410 start = Point::new(start.row, 0);"
5411 .to_string()
5412 ),
5413 "Copying with stripping for reverse selection works the same"
5414 );
5415
5416 cx.set_state(
5417 r#" for selection «in selections.iter() {
5418 let mut start = selection.start;
5419 let mut end = selection.end;
5420 let is_entire_line = selection.is_empty();
5421 if is_entire_line {
5422 start = Point::new(start.row, 0);ˇ»
5423 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5424 }
5425 "#,
5426 );
5427 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5428 assert_eq!(
5429 cx.read_from_clipboard()
5430 .and_then(|item| item.text().as_deref().map(str::to_string)),
5431 Some(
5432 "in selections.iter() {
5433 let mut start = selection.start;
5434 let mut end = selection.end;
5435 let is_entire_line = selection.is_empty();
5436 if is_entire_line {
5437 start = Point::new(start.row, 0);"
5438 .to_string()
5439 ),
5440 "When selecting past the indent, the copying works as usual",
5441 );
5442 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5443 assert_eq!(
5444 cx.read_from_clipboard()
5445 .and_then(|item| item.text().as_deref().map(str::to_string)),
5446 Some(
5447 "in selections.iter() {
5448 let mut start = selection.start;
5449 let mut end = selection.end;
5450 let is_entire_line = selection.is_empty();
5451 if is_entire_line {
5452 start = Point::new(start.row, 0);"
5453 .to_string()
5454 ),
5455 "When selecting past the indent, nothing is trimmed"
5456 );
5457
5458 cx.set_state(
5459 r#" «for selection in selections.iter() {
5460 let mut start = selection.start;
5461
5462 let mut end = selection.end;
5463 let is_entire_line = selection.is_empty();
5464 if is_entire_line {
5465 start = Point::new(start.row, 0);
5466ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
5467 }
5468 "#,
5469 );
5470 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5471 assert_eq!(
5472 cx.read_from_clipboard()
5473 .and_then(|item| item.text().as_deref().map(str::to_string)),
5474 Some(
5475 "for selection in selections.iter() {
5476let mut start = selection.start;
5477
5478let mut end = selection.end;
5479let is_entire_line = selection.is_empty();
5480if is_entire_line {
5481 start = Point::new(start.row, 0);
5482"
5483 .to_string()
5484 ),
5485 "Copying with stripping should ignore empty lines"
5486 );
5487}
5488
5489#[gpui::test]
5490async fn test_paste_multiline(cx: &mut TestAppContext) {
5491 init_test(cx, |_| {});
5492
5493 let mut cx = EditorTestContext::new(cx).await;
5494 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5495
5496 // Cut an indented block, without the leading whitespace.
5497 cx.set_state(indoc! {"
5498 const a: B = (
5499 c(),
5500 «d(
5501 e,
5502 f
5503 )ˇ»
5504 );
5505 "});
5506 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5507 cx.assert_editor_state(indoc! {"
5508 const a: B = (
5509 c(),
5510 ˇ
5511 );
5512 "});
5513
5514 // Paste it at the same position.
5515 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5516 cx.assert_editor_state(indoc! {"
5517 const a: B = (
5518 c(),
5519 d(
5520 e,
5521 f
5522 )ˇ
5523 );
5524 "});
5525
5526 // Paste it at a line with a lower indent level.
5527 cx.set_state(indoc! {"
5528 ˇ
5529 const a: B = (
5530 c(),
5531 );
5532 "});
5533 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5534 cx.assert_editor_state(indoc! {"
5535 d(
5536 e,
5537 f
5538 )ˇ
5539 const a: B = (
5540 c(),
5541 );
5542 "});
5543
5544 // Cut an indented block, with the leading whitespace.
5545 cx.set_state(indoc! {"
5546 const a: B = (
5547 c(),
5548 « d(
5549 e,
5550 f
5551 )
5552 ˇ»);
5553 "});
5554 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5555 cx.assert_editor_state(indoc! {"
5556 const a: B = (
5557 c(),
5558 ˇ);
5559 "});
5560
5561 // Paste it at the same position.
5562 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5563 cx.assert_editor_state(indoc! {"
5564 const a: B = (
5565 c(),
5566 d(
5567 e,
5568 f
5569 )
5570 ˇ);
5571 "});
5572
5573 // Paste it at a line with a higher indent level.
5574 cx.set_state(indoc! {"
5575 const a: B = (
5576 c(),
5577 d(
5578 e,
5579 fˇ
5580 )
5581 );
5582 "});
5583 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5584 cx.assert_editor_state(indoc! {"
5585 const a: B = (
5586 c(),
5587 d(
5588 e,
5589 f d(
5590 e,
5591 f
5592 )
5593 ˇ
5594 )
5595 );
5596 "});
5597
5598 // Copy an indented block, starting mid-line
5599 cx.set_state(indoc! {"
5600 const a: B = (
5601 c(),
5602 somethin«g(
5603 e,
5604 f
5605 )ˇ»
5606 );
5607 "});
5608 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5609
5610 // Paste it on a line with a lower indent level
5611 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
5612 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5613 cx.assert_editor_state(indoc! {"
5614 const a: B = (
5615 c(),
5616 something(
5617 e,
5618 f
5619 )
5620 );
5621 g(
5622 e,
5623 f
5624 )ˇ"});
5625}
5626
5627#[gpui::test]
5628async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
5629 init_test(cx, |_| {});
5630
5631 cx.write_to_clipboard(ClipboardItem::new_string(
5632 " d(\n e\n );\n".into(),
5633 ));
5634
5635 let mut cx = EditorTestContext::new(cx).await;
5636 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5637
5638 cx.set_state(indoc! {"
5639 fn a() {
5640 b();
5641 if c() {
5642 ˇ
5643 }
5644 }
5645 "});
5646
5647 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5648 cx.assert_editor_state(indoc! {"
5649 fn a() {
5650 b();
5651 if c() {
5652 d(
5653 e
5654 );
5655 ˇ
5656 }
5657 }
5658 "});
5659
5660 cx.set_state(indoc! {"
5661 fn a() {
5662 b();
5663 ˇ
5664 }
5665 "});
5666
5667 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5668 cx.assert_editor_state(indoc! {"
5669 fn a() {
5670 b();
5671 d(
5672 e
5673 );
5674 ˇ
5675 }
5676 "});
5677}
5678
5679#[gpui::test]
5680fn test_select_all(cx: &mut TestAppContext) {
5681 init_test(cx, |_| {});
5682
5683 let editor = cx.add_window(|window, cx| {
5684 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
5685 build_editor(buffer, window, cx)
5686 });
5687 _ = editor.update(cx, |editor, window, cx| {
5688 editor.select_all(&SelectAll, window, cx);
5689 assert_eq!(
5690 editor.selections.display_ranges(cx),
5691 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
5692 );
5693 });
5694}
5695
5696#[gpui::test]
5697fn test_select_line(cx: &mut TestAppContext) {
5698 init_test(cx, |_| {});
5699
5700 let editor = cx.add_window(|window, cx| {
5701 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
5702 build_editor(buffer, window, cx)
5703 });
5704 _ = editor.update(cx, |editor, window, cx| {
5705 editor.change_selections(None, window, cx, |s| {
5706 s.select_display_ranges([
5707 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5708 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5709 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5710 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
5711 ])
5712 });
5713 editor.select_line(&SelectLine, window, cx);
5714 assert_eq!(
5715 editor.selections.display_ranges(cx),
5716 vec![
5717 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
5718 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
5719 ]
5720 );
5721 });
5722
5723 _ = editor.update(cx, |editor, window, cx| {
5724 editor.select_line(&SelectLine, window, cx);
5725 assert_eq!(
5726 editor.selections.display_ranges(cx),
5727 vec![
5728 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
5729 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
5730 ]
5731 );
5732 });
5733
5734 _ = editor.update(cx, |editor, window, cx| {
5735 editor.select_line(&SelectLine, window, cx);
5736 assert_eq!(
5737 editor.selections.display_ranges(cx),
5738 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
5739 );
5740 });
5741}
5742
5743#[gpui::test]
5744async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
5745 init_test(cx, |_| {});
5746 let mut cx = EditorTestContext::new(cx).await;
5747
5748 #[track_caller]
5749 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
5750 cx.set_state(initial_state);
5751 cx.update_editor(|e, window, cx| {
5752 e.split_selection_into_lines(&SplitSelectionIntoLines, window, cx)
5753 });
5754 cx.assert_editor_state(expected_state);
5755 }
5756
5757 // Selection starts and ends at the middle of lines, left-to-right
5758 test(
5759 &mut cx,
5760 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
5761 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5762 );
5763 // Same thing, right-to-left
5764 test(
5765 &mut cx,
5766 "aa\nb«b\ncc\ndd\neˇ»e\nff",
5767 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5768 );
5769
5770 // Whole buffer, left-to-right, last line *doesn't* end with newline
5771 test(
5772 &mut cx,
5773 "«ˇaa\nbb\ncc\ndd\nee\nff»",
5774 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5775 );
5776 // Same thing, right-to-left
5777 test(
5778 &mut cx,
5779 "«aa\nbb\ncc\ndd\nee\nffˇ»",
5780 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5781 );
5782
5783 // Whole buffer, left-to-right, last line ends with newline
5784 test(
5785 &mut cx,
5786 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
5787 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5788 );
5789 // Same thing, right-to-left
5790 test(
5791 &mut cx,
5792 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
5793 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5794 );
5795
5796 // Starts at the end of a line, ends at the start of another
5797 test(
5798 &mut cx,
5799 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
5800 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
5801 );
5802}
5803
5804#[gpui::test]
5805async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
5806 init_test(cx, |_| {});
5807
5808 let editor = cx.add_window(|window, cx| {
5809 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
5810 build_editor(buffer, window, cx)
5811 });
5812
5813 // setup
5814 _ = editor.update(cx, |editor, window, cx| {
5815 editor.fold_creases(
5816 vec![
5817 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5818 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5819 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5820 ],
5821 true,
5822 window,
5823 cx,
5824 );
5825 assert_eq!(
5826 editor.display_text(cx),
5827 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5828 );
5829 });
5830
5831 _ = editor.update(cx, |editor, window, cx| {
5832 editor.change_selections(None, window, cx, |s| {
5833 s.select_display_ranges([
5834 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5835 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5836 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5837 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
5838 ])
5839 });
5840 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5841 assert_eq!(
5842 editor.display_text(cx),
5843 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5844 );
5845 });
5846 EditorTestContext::for_editor(editor, cx)
5847 .await
5848 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
5849
5850 _ = editor.update(cx, |editor, window, cx| {
5851 editor.change_selections(None, window, cx, |s| {
5852 s.select_display_ranges([
5853 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
5854 ])
5855 });
5856 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5857 assert_eq!(
5858 editor.display_text(cx),
5859 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
5860 );
5861 assert_eq!(
5862 editor.selections.display_ranges(cx),
5863 [
5864 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
5865 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
5866 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
5867 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
5868 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
5869 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
5870 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
5871 ]
5872 );
5873 });
5874 EditorTestContext::for_editor(editor, cx)
5875 .await
5876 .assert_editor_state(
5877 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
5878 );
5879}
5880
5881#[gpui::test]
5882async fn test_add_selection_above_below(cx: &mut TestAppContext) {
5883 init_test(cx, |_| {});
5884
5885 let mut cx = EditorTestContext::new(cx).await;
5886
5887 cx.set_state(indoc!(
5888 r#"abc
5889 defˇghi
5890
5891 jk
5892 nlmo
5893 "#
5894 ));
5895
5896 cx.update_editor(|editor, window, cx| {
5897 editor.add_selection_above(&Default::default(), window, cx);
5898 });
5899
5900 cx.assert_editor_state(indoc!(
5901 r#"abcˇ
5902 defˇghi
5903
5904 jk
5905 nlmo
5906 "#
5907 ));
5908
5909 cx.update_editor(|editor, window, cx| {
5910 editor.add_selection_above(&Default::default(), window, cx);
5911 });
5912
5913 cx.assert_editor_state(indoc!(
5914 r#"abcˇ
5915 defˇghi
5916
5917 jk
5918 nlmo
5919 "#
5920 ));
5921
5922 cx.update_editor(|editor, window, cx| {
5923 editor.add_selection_below(&Default::default(), window, cx);
5924 });
5925
5926 cx.assert_editor_state(indoc!(
5927 r#"abc
5928 defˇghi
5929
5930 jk
5931 nlmo
5932 "#
5933 ));
5934
5935 cx.update_editor(|editor, window, cx| {
5936 editor.undo_selection(&Default::default(), window, cx);
5937 });
5938
5939 cx.assert_editor_state(indoc!(
5940 r#"abcˇ
5941 defˇghi
5942
5943 jk
5944 nlmo
5945 "#
5946 ));
5947
5948 cx.update_editor(|editor, window, cx| {
5949 editor.redo_selection(&Default::default(), window, cx);
5950 });
5951
5952 cx.assert_editor_state(indoc!(
5953 r#"abc
5954 defˇghi
5955
5956 jk
5957 nlmo
5958 "#
5959 ));
5960
5961 cx.update_editor(|editor, window, cx| {
5962 editor.add_selection_below(&Default::default(), window, cx);
5963 });
5964
5965 cx.assert_editor_state(indoc!(
5966 r#"abc
5967 defˇghi
5968
5969 jk
5970 nlmˇo
5971 "#
5972 ));
5973
5974 cx.update_editor(|editor, window, cx| {
5975 editor.add_selection_below(&Default::default(), window, cx);
5976 });
5977
5978 cx.assert_editor_state(indoc!(
5979 r#"abc
5980 defˇghi
5981
5982 jk
5983 nlmˇo
5984 "#
5985 ));
5986
5987 // change selections
5988 cx.set_state(indoc!(
5989 r#"abc
5990 def«ˇg»hi
5991
5992 jk
5993 nlmo
5994 "#
5995 ));
5996
5997 cx.update_editor(|editor, window, cx| {
5998 editor.add_selection_below(&Default::default(), window, cx);
5999 });
6000
6001 cx.assert_editor_state(indoc!(
6002 r#"abc
6003 def«ˇg»hi
6004
6005 jk
6006 nlm«ˇo»
6007 "#
6008 ));
6009
6010 cx.update_editor(|editor, window, cx| {
6011 editor.add_selection_below(&Default::default(), window, cx);
6012 });
6013
6014 cx.assert_editor_state(indoc!(
6015 r#"abc
6016 def«ˇg»hi
6017
6018 jk
6019 nlm«ˇo»
6020 "#
6021 ));
6022
6023 cx.update_editor(|editor, window, cx| {
6024 editor.add_selection_above(&Default::default(), window, cx);
6025 });
6026
6027 cx.assert_editor_state(indoc!(
6028 r#"abc
6029 def«ˇg»hi
6030
6031 jk
6032 nlmo
6033 "#
6034 ));
6035
6036 cx.update_editor(|editor, window, cx| {
6037 editor.add_selection_above(&Default::default(), window, cx);
6038 });
6039
6040 cx.assert_editor_state(indoc!(
6041 r#"abc
6042 def«ˇg»hi
6043
6044 jk
6045 nlmo
6046 "#
6047 ));
6048
6049 // Change selections again
6050 cx.set_state(indoc!(
6051 r#"a«bc
6052 defgˇ»hi
6053
6054 jk
6055 nlmo
6056 "#
6057 ));
6058
6059 cx.update_editor(|editor, window, cx| {
6060 editor.add_selection_below(&Default::default(), window, cx);
6061 });
6062
6063 cx.assert_editor_state(indoc!(
6064 r#"a«bcˇ»
6065 d«efgˇ»hi
6066
6067 j«kˇ»
6068 nlmo
6069 "#
6070 ));
6071
6072 cx.update_editor(|editor, window, cx| {
6073 editor.add_selection_below(&Default::default(), window, cx);
6074 });
6075 cx.assert_editor_state(indoc!(
6076 r#"a«bcˇ»
6077 d«efgˇ»hi
6078
6079 j«kˇ»
6080 n«lmoˇ»
6081 "#
6082 ));
6083 cx.update_editor(|editor, window, cx| {
6084 editor.add_selection_above(&Default::default(), window, cx);
6085 });
6086
6087 cx.assert_editor_state(indoc!(
6088 r#"a«bcˇ»
6089 d«efgˇ»hi
6090
6091 j«kˇ»
6092 nlmo
6093 "#
6094 ));
6095
6096 // Change selections again
6097 cx.set_state(indoc!(
6098 r#"abc
6099 d«ˇefghi
6100
6101 jk
6102 nlm»o
6103 "#
6104 ));
6105
6106 cx.update_editor(|editor, window, cx| {
6107 editor.add_selection_above(&Default::default(), window, cx);
6108 });
6109
6110 cx.assert_editor_state(indoc!(
6111 r#"a«ˇbc»
6112 d«ˇef»ghi
6113
6114 j«ˇk»
6115 n«ˇlm»o
6116 "#
6117 ));
6118
6119 cx.update_editor(|editor, window, cx| {
6120 editor.add_selection_below(&Default::default(), window, cx);
6121 });
6122
6123 cx.assert_editor_state(indoc!(
6124 r#"abc
6125 d«ˇef»ghi
6126
6127 j«ˇk»
6128 n«ˇlm»o
6129 "#
6130 ));
6131}
6132
6133#[gpui::test]
6134async fn test_select_next(cx: &mut TestAppContext) {
6135 init_test(cx, |_| {});
6136
6137 let mut cx = EditorTestContext::new(cx).await;
6138 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6139
6140 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6141 .unwrap();
6142 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6143
6144 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6145 .unwrap();
6146 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
6147
6148 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6149 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6150
6151 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6152 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
6153
6154 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6155 .unwrap();
6156 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6157
6158 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6159 .unwrap();
6160 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6161
6162 // Test selection direction should be preserved
6163 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
6164
6165 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6166 .unwrap();
6167 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
6168}
6169
6170#[gpui::test]
6171async fn test_select_all_matches(cx: &mut TestAppContext) {
6172 init_test(cx, |_| {});
6173
6174 let mut cx = EditorTestContext::new(cx).await;
6175
6176 // Test caret-only selections
6177 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6178 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6179 .unwrap();
6180 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6181
6182 // Test left-to-right selections
6183 cx.set_state("abc\n«abcˇ»\nabc");
6184 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6185 .unwrap();
6186 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
6187
6188 // Test right-to-left selections
6189 cx.set_state("abc\n«ˇabc»\nabc");
6190 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6191 .unwrap();
6192 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
6193
6194 // Test selecting whitespace with caret selection
6195 cx.set_state("abc\nˇ abc\nabc");
6196 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6197 .unwrap();
6198 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
6199
6200 // Test selecting whitespace with left-to-right selection
6201 cx.set_state("abc\n«ˇ »abc\nabc");
6202 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6203 .unwrap();
6204 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
6205
6206 // Test no matches with right-to-left selection
6207 cx.set_state("abc\n« ˇ»abc\nabc");
6208 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6209 .unwrap();
6210 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
6211}
6212
6213#[gpui::test]
6214async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
6215 init_test(cx, |_| {});
6216
6217 let mut cx = EditorTestContext::new(cx).await;
6218
6219 let large_body_1 = "\nd".repeat(200);
6220 let large_body_2 = "\ne".repeat(200);
6221
6222 cx.set_state(&format!(
6223 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
6224 ));
6225 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
6226 let scroll_position = editor.scroll_position(cx);
6227 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
6228 scroll_position
6229 });
6230
6231 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6232 .unwrap();
6233 cx.assert_editor_state(&format!(
6234 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
6235 ));
6236 let scroll_position_after_selection =
6237 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
6238 assert_eq!(
6239 initial_scroll_position, scroll_position_after_selection,
6240 "Scroll position should not change after selecting all matches"
6241 );
6242}
6243
6244#[gpui::test]
6245async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
6246 init_test(cx, |_| {});
6247
6248 let mut cx = EditorLspTestContext::new_rust(
6249 lsp::ServerCapabilities {
6250 document_formatting_provider: Some(lsp::OneOf::Left(true)),
6251 ..Default::default()
6252 },
6253 cx,
6254 )
6255 .await;
6256
6257 cx.set_state(indoc! {"
6258 line 1
6259 line 2
6260 linˇe 3
6261 line 4
6262 line 5
6263 "});
6264
6265 // Make an edit
6266 cx.update_editor(|editor, window, cx| {
6267 editor.handle_input("X", window, cx);
6268 });
6269
6270 // Move cursor to a different position
6271 cx.update_editor(|editor, window, cx| {
6272 editor.change_selections(None, window, cx, |s| {
6273 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
6274 });
6275 });
6276
6277 cx.assert_editor_state(indoc! {"
6278 line 1
6279 line 2
6280 linXe 3
6281 line 4
6282 liˇne 5
6283 "});
6284
6285 cx.lsp
6286 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
6287 Ok(Some(vec![lsp::TextEdit::new(
6288 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
6289 "PREFIX ".to_string(),
6290 )]))
6291 });
6292
6293 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
6294 .unwrap()
6295 .await
6296 .unwrap();
6297
6298 cx.assert_editor_state(indoc! {"
6299 PREFIX line 1
6300 line 2
6301 linXe 3
6302 line 4
6303 liˇne 5
6304 "});
6305
6306 // Undo formatting
6307 cx.update_editor(|editor, window, cx| {
6308 editor.undo(&Default::default(), window, cx);
6309 });
6310
6311 // Verify cursor moved back to position after edit
6312 cx.assert_editor_state(indoc! {"
6313 line 1
6314 line 2
6315 linXˇe 3
6316 line 4
6317 line 5
6318 "});
6319}
6320
6321#[gpui::test]
6322async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
6323 init_test(cx, |_| {});
6324
6325 let mut cx = EditorTestContext::new(cx).await;
6326 cx.set_state(
6327 r#"let foo = 2;
6328lˇet foo = 2;
6329let fooˇ = 2;
6330let foo = 2;
6331let foo = ˇ2;"#,
6332 );
6333
6334 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6335 .unwrap();
6336 cx.assert_editor_state(
6337 r#"let foo = 2;
6338«letˇ» foo = 2;
6339let «fooˇ» = 2;
6340let foo = 2;
6341let foo = «2ˇ»;"#,
6342 );
6343
6344 // noop for multiple selections with different contents
6345 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6346 .unwrap();
6347 cx.assert_editor_state(
6348 r#"let foo = 2;
6349«letˇ» foo = 2;
6350let «fooˇ» = 2;
6351let foo = 2;
6352let foo = «2ˇ»;"#,
6353 );
6354
6355 // Test last selection direction should be preserved
6356 cx.set_state(
6357 r#"let foo = 2;
6358let foo = 2;
6359let «fooˇ» = 2;
6360let «ˇfoo» = 2;
6361let foo = 2;"#,
6362 );
6363
6364 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6365 .unwrap();
6366 cx.assert_editor_state(
6367 r#"let foo = 2;
6368let foo = 2;
6369let «fooˇ» = 2;
6370let «ˇfoo» = 2;
6371let «ˇfoo» = 2;"#,
6372 );
6373}
6374
6375#[gpui::test]
6376async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
6377 init_test(cx, |_| {});
6378
6379 let mut cx =
6380 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
6381
6382 cx.assert_editor_state(indoc! {"
6383 ˇbbb
6384 ccc
6385
6386 bbb
6387 ccc
6388 "});
6389 cx.dispatch_action(SelectPrevious::default());
6390 cx.assert_editor_state(indoc! {"
6391 «bbbˇ»
6392 ccc
6393
6394 bbb
6395 ccc
6396 "});
6397 cx.dispatch_action(SelectPrevious::default());
6398 cx.assert_editor_state(indoc! {"
6399 «bbbˇ»
6400 ccc
6401
6402 «bbbˇ»
6403 ccc
6404 "});
6405}
6406
6407#[gpui::test]
6408async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
6409 init_test(cx, |_| {});
6410
6411 let mut cx = EditorTestContext::new(cx).await;
6412 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6413
6414 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6415 .unwrap();
6416 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6417
6418 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6419 .unwrap();
6420 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
6421
6422 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6423 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6424
6425 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6426 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
6427
6428 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6429 .unwrap();
6430 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
6431
6432 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6433 .unwrap();
6434 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6435}
6436
6437#[gpui::test]
6438async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
6439 init_test(cx, |_| {});
6440
6441 let mut cx = EditorTestContext::new(cx).await;
6442 cx.set_state("aˇ");
6443
6444 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6445 .unwrap();
6446 cx.assert_editor_state("«aˇ»");
6447 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6448 .unwrap();
6449 cx.assert_editor_state("«aˇ»");
6450}
6451
6452#[gpui::test]
6453async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
6454 init_test(cx, |_| {});
6455
6456 let mut cx = EditorTestContext::new(cx).await;
6457 cx.set_state(
6458 r#"let foo = 2;
6459lˇet foo = 2;
6460let fooˇ = 2;
6461let foo = 2;
6462let foo = ˇ2;"#,
6463 );
6464
6465 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6466 .unwrap();
6467 cx.assert_editor_state(
6468 r#"let foo = 2;
6469«letˇ» foo = 2;
6470let «fooˇ» = 2;
6471let foo = 2;
6472let foo = «2ˇ»;"#,
6473 );
6474
6475 // noop for multiple selections with different contents
6476 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6477 .unwrap();
6478 cx.assert_editor_state(
6479 r#"let foo = 2;
6480«letˇ» foo = 2;
6481let «fooˇ» = 2;
6482let foo = 2;
6483let foo = «2ˇ»;"#,
6484 );
6485}
6486
6487#[gpui::test]
6488async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
6489 init_test(cx, |_| {});
6490
6491 let mut cx = EditorTestContext::new(cx).await;
6492 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
6493
6494 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6495 .unwrap();
6496 // selection direction is preserved
6497 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
6498
6499 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6500 .unwrap();
6501 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
6502
6503 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6504 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
6505
6506 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6507 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
6508
6509 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6510 .unwrap();
6511 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
6512
6513 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6514 .unwrap();
6515 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
6516}
6517
6518#[gpui::test]
6519async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
6520 init_test(cx, |_| {});
6521
6522 let language = Arc::new(Language::new(
6523 LanguageConfig::default(),
6524 Some(tree_sitter_rust::LANGUAGE.into()),
6525 ));
6526
6527 let text = r#"
6528 use mod1::mod2::{mod3, mod4};
6529
6530 fn fn_1(param1: bool, param2: &str) {
6531 let var1 = "text";
6532 }
6533 "#
6534 .unindent();
6535
6536 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6537 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6538 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6539
6540 editor
6541 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6542 .await;
6543
6544 editor.update_in(cx, |editor, window, cx| {
6545 editor.change_selections(None, window, cx, |s| {
6546 s.select_display_ranges([
6547 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
6548 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
6549 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
6550 ]);
6551 });
6552 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6553 });
6554 editor.update(cx, |editor, cx| {
6555 assert_text_with_selections(
6556 editor,
6557 indoc! {r#"
6558 use mod1::mod2::{mod3, «mod4ˇ»};
6559
6560 fn fn_1«ˇ(param1: bool, param2: &str)» {
6561 let var1 = "«ˇtext»";
6562 }
6563 "#},
6564 cx,
6565 );
6566 });
6567
6568 editor.update_in(cx, |editor, window, cx| {
6569 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6570 });
6571 editor.update(cx, |editor, cx| {
6572 assert_text_with_selections(
6573 editor,
6574 indoc! {r#"
6575 use mod1::mod2::«{mod3, mod4}ˇ»;
6576
6577 «ˇfn fn_1(param1: bool, param2: &str) {
6578 let var1 = "text";
6579 }»
6580 "#},
6581 cx,
6582 );
6583 });
6584
6585 editor.update_in(cx, |editor, window, cx| {
6586 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6587 });
6588 assert_eq!(
6589 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
6590 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
6591 );
6592
6593 // Trying to expand the selected syntax node one more time has no effect.
6594 editor.update_in(cx, |editor, window, cx| {
6595 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6596 });
6597 assert_eq!(
6598 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
6599 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
6600 );
6601
6602 editor.update_in(cx, |editor, window, cx| {
6603 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6604 });
6605 editor.update(cx, |editor, cx| {
6606 assert_text_with_selections(
6607 editor,
6608 indoc! {r#"
6609 use mod1::mod2::«{mod3, mod4}ˇ»;
6610
6611 «ˇfn fn_1(param1: bool, param2: &str) {
6612 let var1 = "text";
6613 }»
6614 "#},
6615 cx,
6616 );
6617 });
6618
6619 editor.update_in(cx, |editor, window, cx| {
6620 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6621 });
6622 editor.update(cx, |editor, cx| {
6623 assert_text_with_selections(
6624 editor,
6625 indoc! {r#"
6626 use mod1::mod2::{mod3, «mod4ˇ»};
6627
6628 fn fn_1«ˇ(param1: bool, param2: &str)» {
6629 let var1 = "«ˇtext»";
6630 }
6631 "#},
6632 cx,
6633 );
6634 });
6635
6636 editor.update_in(cx, |editor, window, cx| {
6637 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6638 });
6639 editor.update(cx, |editor, cx| {
6640 assert_text_with_selections(
6641 editor,
6642 indoc! {r#"
6643 use mod1::mod2::{mod3, mo«ˇ»d4};
6644
6645 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
6646 let var1 = "te«ˇ»xt";
6647 }
6648 "#},
6649 cx,
6650 );
6651 });
6652
6653 // Trying to shrink the selected syntax node one more time has no effect.
6654 editor.update_in(cx, |editor, window, cx| {
6655 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6656 });
6657 editor.update_in(cx, |editor, _, cx| {
6658 assert_text_with_selections(
6659 editor,
6660 indoc! {r#"
6661 use mod1::mod2::{mod3, mo«ˇ»d4};
6662
6663 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
6664 let var1 = "te«ˇ»xt";
6665 }
6666 "#},
6667 cx,
6668 );
6669 });
6670
6671 // Ensure that we keep expanding the selection if the larger selection starts or ends within
6672 // a fold.
6673 editor.update_in(cx, |editor, window, cx| {
6674 editor.fold_creases(
6675 vec![
6676 Crease::simple(
6677 Point::new(0, 21)..Point::new(0, 24),
6678 FoldPlaceholder::test(),
6679 ),
6680 Crease::simple(
6681 Point::new(3, 20)..Point::new(3, 22),
6682 FoldPlaceholder::test(),
6683 ),
6684 ],
6685 true,
6686 window,
6687 cx,
6688 );
6689 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6690 });
6691 editor.update(cx, |editor, cx| {
6692 assert_text_with_selections(
6693 editor,
6694 indoc! {r#"
6695 use mod1::mod2::«{mod3, mod4}ˇ»;
6696
6697 fn fn_1«ˇ(param1: bool, param2: &str)» {
6698 let var1 = "«ˇtext»";
6699 }
6700 "#},
6701 cx,
6702 );
6703 });
6704}
6705
6706#[gpui::test]
6707async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
6708 init_test(cx, |_| {});
6709
6710 let language = Arc::new(Language::new(
6711 LanguageConfig::default(),
6712 Some(tree_sitter_rust::LANGUAGE.into()),
6713 ));
6714
6715 let text = "let a = 2;";
6716
6717 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6718 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6719 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6720
6721 editor
6722 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6723 .await;
6724
6725 // Test case 1: Cursor at end of word
6726 editor.update_in(cx, |editor, window, cx| {
6727 editor.change_selections(None, window, cx, |s| {
6728 s.select_display_ranges([
6729 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
6730 ]);
6731 });
6732 });
6733 editor.update(cx, |editor, cx| {
6734 assert_text_with_selections(editor, "let aˇ = 2;", cx);
6735 });
6736 editor.update_in(cx, |editor, window, cx| {
6737 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6738 });
6739 editor.update(cx, |editor, cx| {
6740 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
6741 });
6742 editor.update_in(cx, |editor, window, cx| {
6743 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6744 });
6745 editor.update(cx, |editor, cx| {
6746 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
6747 });
6748
6749 // Test case 2: Cursor at end of statement
6750 editor.update_in(cx, |editor, window, cx| {
6751 editor.change_selections(None, window, cx, |s| {
6752 s.select_display_ranges([
6753 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
6754 ]);
6755 });
6756 });
6757 editor.update(cx, |editor, cx| {
6758 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
6759 });
6760 editor.update_in(cx, |editor, window, cx| {
6761 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6762 });
6763 editor.update(cx, |editor, cx| {
6764 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
6765 });
6766}
6767
6768#[gpui::test]
6769async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
6770 init_test(cx, |_| {});
6771
6772 let language = Arc::new(Language::new(
6773 LanguageConfig::default(),
6774 Some(tree_sitter_rust::LANGUAGE.into()),
6775 ));
6776
6777 let text = r#"
6778 use mod1::mod2::{mod3, mod4};
6779
6780 fn fn_1(param1: bool, param2: &str) {
6781 let var1 = "hello world";
6782 }
6783 "#
6784 .unindent();
6785
6786 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6787 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6788 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6789
6790 editor
6791 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6792 .await;
6793
6794 // Test 1: Cursor on a letter of a string word
6795 editor.update_in(cx, |editor, window, cx| {
6796 editor.change_selections(None, window, cx, |s| {
6797 s.select_display_ranges([
6798 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
6799 ]);
6800 });
6801 });
6802 editor.update_in(cx, |editor, window, cx| {
6803 assert_text_with_selections(
6804 editor,
6805 indoc! {r#"
6806 use mod1::mod2::{mod3, mod4};
6807
6808 fn fn_1(param1: bool, param2: &str) {
6809 let var1 = "hˇello world";
6810 }
6811 "#},
6812 cx,
6813 );
6814 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6815 assert_text_with_selections(
6816 editor,
6817 indoc! {r#"
6818 use mod1::mod2::{mod3, mod4};
6819
6820 fn fn_1(param1: bool, param2: &str) {
6821 let var1 = "«ˇhello» world";
6822 }
6823 "#},
6824 cx,
6825 );
6826 });
6827
6828 // Test 2: Partial selection within a word
6829 editor.update_in(cx, |editor, window, cx| {
6830 editor.change_selections(None, window, cx, |s| {
6831 s.select_display_ranges([
6832 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
6833 ]);
6834 });
6835 });
6836 editor.update_in(cx, |editor, window, cx| {
6837 assert_text_with_selections(
6838 editor,
6839 indoc! {r#"
6840 use mod1::mod2::{mod3, mod4};
6841
6842 fn fn_1(param1: bool, param2: &str) {
6843 let var1 = "h«elˇ»lo world";
6844 }
6845 "#},
6846 cx,
6847 );
6848 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6849 assert_text_with_selections(
6850 editor,
6851 indoc! {r#"
6852 use mod1::mod2::{mod3, mod4};
6853
6854 fn fn_1(param1: bool, param2: &str) {
6855 let var1 = "«ˇhello» world";
6856 }
6857 "#},
6858 cx,
6859 );
6860 });
6861
6862 // Test 3: Complete word already selected
6863 editor.update_in(cx, |editor, window, cx| {
6864 editor.change_selections(None, window, cx, |s| {
6865 s.select_display_ranges([
6866 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
6867 ]);
6868 });
6869 });
6870 editor.update_in(cx, |editor, window, cx| {
6871 assert_text_with_selections(
6872 editor,
6873 indoc! {r#"
6874 use mod1::mod2::{mod3, mod4};
6875
6876 fn fn_1(param1: bool, param2: &str) {
6877 let var1 = "«helloˇ» world";
6878 }
6879 "#},
6880 cx,
6881 );
6882 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6883 assert_text_with_selections(
6884 editor,
6885 indoc! {r#"
6886 use mod1::mod2::{mod3, mod4};
6887
6888 fn fn_1(param1: bool, param2: &str) {
6889 let var1 = "«hello worldˇ»";
6890 }
6891 "#},
6892 cx,
6893 );
6894 });
6895
6896 // Test 4: Selection spanning across words
6897 editor.update_in(cx, |editor, window, cx| {
6898 editor.change_selections(None, window, cx, |s| {
6899 s.select_display_ranges([
6900 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
6901 ]);
6902 });
6903 });
6904 editor.update_in(cx, |editor, window, cx| {
6905 assert_text_with_selections(
6906 editor,
6907 indoc! {r#"
6908 use mod1::mod2::{mod3, mod4};
6909
6910 fn fn_1(param1: bool, param2: &str) {
6911 let var1 = "hel«lo woˇ»rld";
6912 }
6913 "#},
6914 cx,
6915 );
6916 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6917 assert_text_with_selections(
6918 editor,
6919 indoc! {r#"
6920 use mod1::mod2::{mod3, mod4};
6921
6922 fn fn_1(param1: bool, param2: &str) {
6923 let var1 = "«ˇhello world»";
6924 }
6925 "#},
6926 cx,
6927 );
6928 });
6929
6930 // Test 5: Expansion beyond string
6931 editor.update_in(cx, |editor, window, cx| {
6932 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6933 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6934 assert_text_with_selections(
6935 editor,
6936 indoc! {r#"
6937 use mod1::mod2::{mod3, mod4};
6938
6939 fn fn_1(param1: bool, param2: &str) {
6940 «ˇlet var1 = "hello world";»
6941 }
6942 "#},
6943 cx,
6944 );
6945 });
6946}
6947
6948#[gpui::test]
6949async fn test_fold_function_bodies(cx: &mut TestAppContext) {
6950 init_test(cx, |_| {});
6951
6952 let base_text = r#"
6953 impl A {
6954 // this is an uncommitted comment
6955
6956 fn b() {
6957 c();
6958 }
6959
6960 // this is another uncommitted comment
6961
6962 fn d() {
6963 // e
6964 // f
6965 }
6966 }
6967
6968 fn g() {
6969 // h
6970 }
6971 "#
6972 .unindent();
6973
6974 let text = r#"
6975 ˇimpl A {
6976
6977 fn b() {
6978 c();
6979 }
6980
6981 fn d() {
6982 // e
6983 // f
6984 }
6985 }
6986
6987 fn g() {
6988 // h
6989 }
6990 "#
6991 .unindent();
6992
6993 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
6994 cx.set_state(&text);
6995 cx.set_head_text(&base_text);
6996 cx.update_editor(|editor, window, cx| {
6997 editor.expand_all_diff_hunks(&Default::default(), window, cx);
6998 });
6999
7000 cx.assert_state_with_diff(
7001 "
7002 ˇimpl A {
7003 - // this is an uncommitted comment
7004
7005 fn b() {
7006 c();
7007 }
7008
7009 - // this is another uncommitted comment
7010 -
7011 fn d() {
7012 // e
7013 // f
7014 }
7015 }
7016
7017 fn g() {
7018 // h
7019 }
7020 "
7021 .unindent(),
7022 );
7023
7024 let expected_display_text = "
7025 impl A {
7026 // this is an uncommitted comment
7027
7028 fn b() {
7029 ⋯
7030 }
7031
7032 // this is another uncommitted comment
7033
7034 fn d() {
7035 ⋯
7036 }
7037 }
7038
7039 fn g() {
7040 ⋯
7041 }
7042 "
7043 .unindent();
7044
7045 cx.update_editor(|editor, window, cx| {
7046 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
7047 assert_eq!(editor.display_text(cx), expected_display_text);
7048 });
7049}
7050
7051#[gpui::test]
7052async fn test_autoindent(cx: &mut TestAppContext) {
7053 init_test(cx, |_| {});
7054
7055 let language = Arc::new(
7056 Language::new(
7057 LanguageConfig {
7058 brackets: BracketPairConfig {
7059 pairs: vec![
7060 BracketPair {
7061 start: "{".to_string(),
7062 end: "}".to_string(),
7063 close: false,
7064 surround: false,
7065 newline: true,
7066 },
7067 BracketPair {
7068 start: "(".to_string(),
7069 end: ")".to_string(),
7070 close: false,
7071 surround: false,
7072 newline: true,
7073 },
7074 ],
7075 ..Default::default()
7076 },
7077 ..Default::default()
7078 },
7079 Some(tree_sitter_rust::LANGUAGE.into()),
7080 )
7081 .with_indents_query(
7082 r#"
7083 (_ "(" ")" @end) @indent
7084 (_ "{" "}" @end) @indent
7085 "#,
7086 )
7087 .unwrap(),
7088 );
7089
7090 let text = "fn a() {}";
7091
7092 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7093 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7094 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7095 editor
7096 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7097 .await;
7098
7099 editor.update_in(cx, |editor, window, cx| {
7100 editor.change_selections(None, window, cx, |s| s.select_ranges([5..5, 8..8, 9..9]));
7101 editor.newline(&Newline, window, cx);
7102 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
7103 assert_eq!(
7104 editor.selections.ranges(cx),
7105 &[
7106 Point::new(1, 4)..Point::new(1, 4),
7107 Point::new(3, 4)..Point::new(3, 4),
7108 Point::new(5, 0)..Point::new(5, 0)
7109 ]
7110 );
7111 });
7112}
7113
7114#[gpui::test]
7115async fn test_autoindent_selections(cx: &mut TestAppContext) {
7116 init_test(cx, |_| {});
7117
7118 {
7119 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
7120 cx.set_state(indoc! {"
7121 impl A {
7122
7123 fn b() {}
7124
7125 «fn c() {
7126
7127 }ˇ»
7128 }
7129 "});
7130
7131 cx.update_editor(|editor, window, cx| {
7132 editor.autoindent(&Default::default(), window, cx);
7133 });
7134
7135 cx.assert_editor_state(indoc! {"
7136 impl A {
7137
7138 fn b() {}
7139
7140 «fn c() {
7141
7142 }ˇ»
7143 }
7144 "});
7145 }
7146
7147 {
7148 let mut cx = EditorTestContext::new_multibuffer(
7149 cx,
7150 [indoc! { "
7151 impl A {
7152 «
7153 // a
7154 fn b(){}
7155 »
7156 «
7157 }
7158 fn c(){}
7159 »
7160 "}],
7161 );
7162
7163 let buffer = cx.update_editor(|editor, _, cx| {
7164 let buffer = editor.buffer().update(cx, |buffer, _| {
7165 buffer.all_buffers().iter().next().unwrap().clone()
7166 });
7167 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7168 buffer
7169 });
7170
7171 cx.run_until_parked();
7172 cx.update_editor(|editor, window, cx| {
7173 editor.select_all(&Default::default(), window, cx);
7174 editor.autoindent(&Default::default(), window, cx)
7175 });
7176 cx.run_until_parked();
7177
7178 cx.update(|_, cx| {
7179 assert_eq!(
7180 buffer.read(cx).text(),
7181 indoc! { "
7182 impl A {
7183
7184 // a
7185 fn b(){}
7186
7187
7188 }
7189 fn c(){}
7190
7191 " }
7192 )
7193 });
7194 }
7195}
7196
7197#[gpui::test]
7198async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
7199 init_test(cx, |_| {});
7200
7201 let mut cx = EditorTestContext::new(cx).await;
7202
7203 let language = Arc::new(Language::new(
7204 LanguageConfig {
7205 brackets: BracketPairConfig {
7206 pairs: vec![
7207 BracketPair {
7208 start: "{".to_string(),
7209 end: "}".to_string(),
7210 close: true,
7211 surround: true,
7212 newline: true,
7213 },
7214 BracketPair {
7215 start: "(".to_string(),
7216 end: ")".to_string(),
7217 close: true,
7218 surround: true,
7219 newline: true,
7220 },
7221 BracketPair {
7222 start: "/*".to_string(),
7223 end: " */".to_string(),
7224 close: true,
7225 surround: true,
7226 newline: true,
7227 },
7228 BracketPair {
7229 start: "[".to_string(),
7230 end: "]".to_string(),
7231 close: false,
7232 surround: false,
7233 newline: true,
7234 },
7235 BracketPair {
7236 start: "\"".to_string(),
7237 end: "\"".to_string(),
7238 close: true,
7239 surround: true,
7240 newline: false,
7241 },
7242 BracketPair {
7243 start: "<".to_string(),
7244 end: ">".to_string(),
7245 close: false,
7246 surround: true,
7247 newline: true,
7248 },
7249 ],
7250 ..Default::default()
7251 },
7252 autoclose_before: "})]".to_string(),
7253 ..Default::default()
7254 },
7255 Some(tree_sitter_rust::LANGUAGE.into()),
7256 ));
7257
7258 cx.language_registry().add(language.clone());
7259 cx.update_buffer(|buffer, cx| {
7260 buffer.set_language(Some(language), cx);
7261 });
7262
7263 cx.set_state(
7264 &r#"
7265 🏀ˇ
7266 εˇ
7267 ❤️ˇ
7268 "#
7269 .unindent(),
7270 );
7271
7272 // autoclose multiple nested brackets at multiple cursors
7273 cx.update_editor(|editor, window, cx| {
7274 editor.handle_input("{", window, cx);
7275 editor.handle_input("{", window, cx);
7276 editor.handle_input("{", window, cx);
7277 });
7278 cx.assert_editor_state(
7279 &"
7280 🏀{{{ˇ}}}
7281 ε{{{ˇ}}}
7282 ❤️{{{ˇ}}}
7283 "
7284 .unindent(),
7285 );
7286
7287 // insert a different closing bracket
7288 cx.update_editor(|editor, window, cx| {
7289 editor.handle_input(")", window, cx);
7290 });
7291 cx.assert_editor_state(
7292 &"
7293 🏀{{{)ˇ}}}
7294 ε{{{)ˇ}}}
7295 ❤️{{{)ˇ}}}
7296 "
7297 .unindent(),
7298 );
7299
7300 // skip over the auto-closed brackets when typing a closing bracket
7301 cx.update_editor(|editor, window, cx| {
7302 editor.move_right(&MoveRight, window, cx);
7303 editor.handle_input("}", window, cx);
7304 editor.handle_input("}", window, cx);
7305 editor.handle_input("}", window, cx);
7306 });
7307 cx.assert_editor_state(
7308 &"
7309 🏀{{{)}}}}ˇ
7310 ε{{{)}}}}ˇ
7311 ❤️{{{)}}}}ˇ
7312 "
7313 .unindent(),
7314 );
7315
7316 // autoclose multi-character pairs
7317 cx.set_state(
7318 &"
7319 ˇ
7320 ˇ
7321 "
7322 .unindent(),
7323 );
7324 cx.update_editor(|editor, window, cx| {
7325 editor.handle_input("/", window, cx);
7326 editor.handle_input("*", window, cx);
7327 });
7328 cx.assert_editor_state(
7329 &"
7330 /*ˇ */
7331 /*ˇ */
7332 "
7333 .unindent(),
7334 );
7335
7336 // one cursor autocloses a multi-character pair, one cursor
7337 // does not autoclose.
7338 cx.set_state(
7339 &"
7340 /ˇ
7341 ˇ
7342 "
7343 .unindent(),
7344 );
7345 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
7346 cx.assert_editor_state(
7347 &"
7348 /*ˇ */
7349 *ˇ
7350 "
7351 .unindent(),
7352 );
7353
7354 // Don't autoclose if the next character isn't whitespace and isn't
7355 // listed in the language's "autoclose_before" section.
7356 cx.set_state("ˇa b");
7357 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7358 cx.assert_editor_state("{ˇa b");
7359
7360 // Don't autoclose if `close` is false for the bracket pair
7361 cx.set_state("ˇ");
7362 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
7363 cx.assert_editor_state("[ˇ");
7364
7365 // Surround with brackets if text is selected
7366 cx.set_state("«aˇ» b");
7367 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7368 cx.assert_editor_state("{«aˇ»} b");
7369
7370 // Autoclose when not immediately after a word character
7371 cx.set_state("a ˇ");
7372 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7373 cx.assert_editor_state("a \"ˇ\"");
7374
7375 // Autoclose pair where the start and end characters are the same
7376 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7377 cx.assert_editor_state("a \"\"ˇ");
7378
7379 // Don't autoclose when immediately after a word character
7380 cx.set_state("aˇ");
7381 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7382 cx.assert_editor_state("a\"ˇ");
7383
7384 // Do autoclose when after a non-word character
7385 cx.set_state("{ˇ");
7386 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7387 cx.assert_editor_state("{\"ˇ\"");
7388
7389 // Non identical pairs autoclose regardless of preceding character
7390 cx.set_state("aˇ");
7391 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7392 cx.assert_editor_state("a{ˇ}");
7393
7394 // Don't autoclose pair if autoclose is disabled
7395 cx.set_state("ˇ");
7396 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
7397 cx.assert_editor_state("<ˇ");
7398
7399 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
7400 cx.set_state("«aˇ» b");
7401 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
7402 cx.assert_editor_state("<«aˇ»> b");
7403}
7404
7405#[gpui::test]
7406async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
7407 init_test(cx, |settings| {
7408 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
7409 });
7410
7411 let mut cx = EditorTestContext::new(cx).await;
7412
7413 let language = Arc::new(Language::new(
7414 LanguageConfig {
7415 brackets: BracketPairConfig {
7416 pairs: vec![
7417 BracketPair {
7418 start: "{".to_string(),
7419 end: "}".to_string(),
7420 close: true,
7421 surround: true,
7422 newline: true,
7423 },
7424 BracketPair {
7425 start: "(".to_string(),
7426 end: ")".to_string(),
7427 close: true,
7428 surround: true,
7429 newline: true,
7430 },
7431 BracketPair {
7432 start: "[".to_string(),
7433 end: "]".to_string(),
7434 close: false,
7435 surround: false,
7436 newline: true,
7437 },
7438 ],
7439 ..Default::default()
7440 },
7441 autoclose_before: "})]".to_string(),
7442 ..Default::default()
7443 },
7444 Some(tree_sitter_rust::LANGUAGE.into()),
7445 ));
7446
7447 cx.language_registry().add(language.clone());
7448 cx.update_buffer(|buffer, cx| {
7449 buffer.set_language(Some(language), cx);
7450 });
7451
7452 cx.set_state(
7453 &"
7454 ˇ
7455 ˇ
7456 ˇ
7457 "
7458 .unindent(),
7459 );
7460
7461 // ensure only matching closing brackets are skipped over
7462 cx.update_editor(|editor, window, cx| {
7463 editor.handle_input("}", window, cx);
7464 editor.move_left(&MoveLeft, window, cx);
7465 editor.handle_input(")", window, cx);
7466 editor.move_left(&MoveLeft, window, cx);
7467 });
7468 cx.assert_editor_state(
7469 &"
7470 ˇ)}
7471 ˇ)}
7472 ˇ)}
7473 "
7474 .unindent(),
7475 );
7476
7477 // skip-over closing brackets at multiple cursors
7478 cx.update_editor(|editor, window, cx| {
7479 editor.handle_input(")", window, cx);
7480 editor.handle_input("}", window, cx);
7481 });
7482 cx.assert_editor_state(
7483 &"
7484 )}ˇ
7485 )}ˇ
7486 )}ˇ
7487 "
7488 .unindent(),
7489 );
7490
7491 // ignore non-close brackets
7492 cx.update_editor(|editor, window, cx| {
7493 editor.handle_input("]", window, cx);
7494 editor.move_left(&MoveLeft, window, cx);
7495 editor.handle_input("]", window, cx);
7496 });
7497 cx.assert_editor_state(
7498 &"
7499 )}]ˇ]
7500 )}]ˇ]
7501 )}]ˇ]
7502 "
7503 .unindent(),
7504 );
7505}
7506
7507#[gpui::test]
7508async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
7509 init_test(cx, |_| {});
7510
7511 let mut cx = EditorTestContext::new(cx).await;
7512
7513 let html_language = Arc::new(
7514 Language::new(
7515 LanguageConfig {
7516 name: "HTML".into(),
7517 brackets: BracketPairConfig {
7518 pairs: vec![
7519 BracketPair {
7520 start: "<".into(),
7521 end: ">".into(),
7522 close: true,
7523 ..Default::default()
7524 },
7525 BracketPair {
7526 start: "{".into(),
7527 end: "}".into(),
7528 close: true,
7529 ..Default::default()
7530 },
7531 BracketPair {
7532 start: "(".into(),
7533 end: ")".into(),
7534 close: true,
7535 ..Default::default()
7536 },
7537 ],
7538 ..Default::default()
7539 },
7540 autoclose_before: "})]>".into(),
7541 ..Default::default()
7542 },
7543 Some(tree_sitter_html::LANGUAGE.into()),
7544 )
7545 .with_injection_query(
7546 r#"
7547 (script_element
7548 (raw_text) @injection.content
7549 (#set! injection.language "javascript"))
7550 "#,
7551 )
7552 .unwrap(),
7553 );
7554
7555 let javascript_language = Arc::new(Language::new(
7556 LanguageConfig {
7557 name: "JavaScript".into(),
7558 brackets: BracketPairConfig {
7559 pairs: vec![
7560 BracketPair {
7561 start: "/*".into(),
7562 end: " */".into(),
7563 close: true,
7564 ..Default::default()
7565 },
7566 BracketPair {
7567 start: "{".into(),
7568 end: "}".into(),
7569 close: true,
7570 ..Default::default()
7571 },
7572 BracketPair {
7573 start: "(".into(),
7574 end: ")".into(),
7575 close: true,
7576 ..Default::default()
7577 },
7578 ],
7579 ..Default::default()
7580 },
7581 autoclose_before: "})]>".into(),
7582 ..Default::default()
7583 },
7584 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
7585 ));
7586
7587 cx.language_registry().add(html_language.clone());
7588 cx.language_registry().add(javascript_language.clone());
7589
7590 cx.update_buffer(|buffer, cx| {
7591 buffer.set_language(Some(html_language), cx);
7592 });
7593
7594 cx.set_state(
7595 &r#"
7596 <body>ˇ
7597 <script>
7598 var x = 1;ˇ
7599 </script>
7600 </body>ˇ
7601 "#
7602 .unindent(),
7603 );
7604
7605 // Precondition: different languages are active at different locations.
7606 cx.update_editor(|editor, window, cx| {
7607 let snapshot = editor.snapshot(window, cx);
7608 let cursors = editor.selections.ranges::<usize>(cx);
7609 let languages = cursors
7610 .iter()
7611 .map(|c| snapshot.language_at(c.start).unwrap().name())
7612 .collect::<Vec<_>>();
7613 assert_eq!(
7614 languages,
7615 &["HTML".into(), "JavaScript".into(), "HTML".into()]
7616 );
7617 });
7618
7619 // Angle brackets autoclose in HTML, but not JavaScript.
7620 cx.update_editor(|editor, window, cx| {
7621 editor.handle_input("<", window, cx);
7622 editor.handle_input("a", window, cx);
7623 });
7624 cx.assert_editor_state(
7625 &r#"
7626 <body><aˇ>
7627 <script>
7628 var x = 1;<aˇ
7629 </script>
7630 </body><aˇ>
7631 "#
7632 .unindent(),
7633 );
7634
7635 // Curly braces and parens autoclose in both HTML and JavaScript.
7636 cx.update_editor(|editor, window, cx| {
7637 editor.handle_input(" b=", window, cx);
7638 editor.handle_input("{", window, cx);
7639 editor.handle_input("c", window, cx);
7640 editor.handle_input("(", window, cx);
7641 });
7642 cx.assert_editor_state(
7643 &r#"
7644 <body><a b={c(ˇ)}>
7645 <script>
7646 var x = 1;<a b={c(ˇ)}
7647 </script>
7648 </body><a b={c(ˇ)}>
7649 "#
7650 .unindent(),
7651 );
7652
7653 // Brackets that were already autoclosed are skipped.
7654 cx.update_editor(|editor, window, cx| {
7655 editor.handle_input(")", window, cx);
7656 editor.handle_input("d", window, cx);
7657 editor.handle_input("}", window, cx);
7658 });
7659 cx.assert_editor_state(
7660 &r#"
7661 <body><a b={c()d}ˇ>
7662 <script>
7663 var x = 1;<a b={c()d}ˇ
7664 </script>
7665 </body><a b={c()d}ˇ>
7666 "#
7667 .unindent(),
7668 );
7669 cx.update_editor(|editor, window, cx| {
7670 editor.handle_input(">", window, cx);
7671 });
7672 cx.assert_editor_state(
7673 &r#"
7674 <body><a b={c()d}>ˇ
7675 <script>
7676 var x = 1;<a b={c()d}>ˇ
7677 </script>
7678 </body><a b={c()d}>ˇ
7679 "#
7680 .unindent(),
7681 );
7682
7683 // Reset
7684 cx.set_state(
7685 &r#"
7686 <body>ˇ
7687 <script>
7688 var x = 1;ˇ
7689 </script>
7690 </body>ˇ
7691 "#
7692 .unindent(),
7693 );
7694
7695 cx.update_editor(|editor, window, cx| {
7696 editor.handle_input("<", window, cx);
7697 });
7698 cx.assert_editor_state(
7699 &r#"
7700 <body><ˇ>
7701 <script>
7702 var x = 1;<ˇ
7703 </script>
7704 </body><ˇ>
7705 "#
7706 .unindent(),
7707 );
7708
7709 // When backspacing, the closing angle brackets are removed.
7710 cx.update_editor(|editor, window, cx| {
7711 editor.backspace(&Backspace, window, cx);
7712 });
7713 cx.assert_editor_state(
7714 &r#"
7715 <body>ˇ
7716 <script>
7717 var x = 1;ˇ
7718 </script>
7719 </body>ˇ
7720 "#
7721 .unindent(),
7722 );
7723
7724 // Block comments autoclose in JavaScript, but not HTML.
7725 cx.update_editor(|editor, window, cx| {
7726 editor.handle_input("/", window, cx);
7727 editor.handle_input("*", window, cx);
7728 });
7729 cx.assert_editor_state(
7730 &r#"
7731 <body>/*ˇ
7732 <script>
7733 var x = 1;/*ˇ */
7734 </script>
7735 </body>/*ˇ
7736 "#
7737 .unindent(),
7738 );
7739}
7740
7741#[gpui::test]
7742async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
7743 init_test(cx, |_| {});
7744
7745 let mut cx = EditorTestContext::new(cx).await;
7746
7747 let rust_language = Arc::new(
7748 Language::new(
7749 LanguageConfig {
7750 name: "Rust".into(),
7751 brackets: serde_json::from_value(json!([
7752 { "start": "{", "end": "}", "close": true, "newline": true },
7753 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
7754 ]))
7755 .unwrap(),
7756 autoclose_before: "})]>".into(),
7757 ..Default::default()
7758 },
7759 Some(tree_sitter_rust::LANGUAGE.into()),
7760 )
7761 .with_override_query("(string_literal) @string")
7762 .unwrap(),
7763 );
7764
7765 cx.language_registry().add(rust_language.clone());
7766 cx.update_buffer(|buffer, cx| {
7767 buffer.set_language(Some(rust_language), cx);
7768 });
7769
7770 cx.set_state(
7771 &r#"
7772 let x = ˇ
7773 "#
7774 .unindent(),
7775 );
7776
7777 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
7778 cx.update_editor(|editor, window, cx| {
7779 editor.handle_input("\"", window, cx);
7780 });
7781 cx.assert_editor_state(
7782 &r#"
7783 let x = "ˇ"
7784 "#
7785 .unindent(),
7786 );
7787
7788 // Inserting another quotation mark. The cursor moves across the existing
7789 // automatically-inserted quotation mark.
7790 cx.update_editor(|editor, window, cx| {
7791 editor.handle_input("\"", window, cx);
7792 });
7793 cx.assert_editor_state(
7794 &r#"
7795 let x = ""ˇ
7796 "#
7797 .unindent(),
7798 );
7799
7800 // Reset
7801 cx.set_state(
7802 &r#"
7803 let x = ˇ
7804 "#
7805 .unindent(),
7806 );
7807
7808 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
7809 cx.update_editor(|editor, window, cx| {
7810 editor.handle_input("\"", window, cx);
7811 editor.handle_input(" ", window, cx);
7812 editor.move_left(&Default::default(), window, cx);
7813 editor.handle_input("\\", window, cx);
7814 editor.handle_input("\"", window, cx);
7815 });
7816 cx.assert_editor_state(
7817 &r#"
7818 let x = "\"ˇ "
7819 "#
7820 .unindent(),
7821 );
7822
7823 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
7824 // mark. Nothing is inserted.
7825 cx.update_editor(|editor, window, cx| {
7826 editor.move_right(&Default::default(), window, cx);
7827 editor.handle_input("\"", window, cx);
7828 });
7829 cx.assert_editor_state(
7830 &r#"
7831 let x = "\" "ˇ
7832 "#
7833 .unindent(),
7834 );
7835}
7836
7837#[gpui::test]
7838async fn test_surround_with_pair(cx: &mut TestAppContext) {
7839 init_test(cx, |_| {});
7840
7841 let language = Arc::new(Language::new(
7842 LanguageConfig {
7843 brackets: BracketPairConfig {
7844 pairs: vec![
7845 BracketPair {
7846 start: "{".to_string(),
7847 end: "}".to_string(),
7848 close: true,
7849 surround: true,
7850 newline: true,
7851 },
7852 BracketPair {
7853 start: "/* ".to_string(),
7854 end: "*/".to_string(),
7855 close: true,
7856 surround: true,
7857 ..Default::default()
7858 },
7859 ],
7860 ..Default::default()
7861 },
7862 ..Default::default()
7863 },
7864 Some(tree_sitter_rust::LANGUAGE.into()),
7865 ));
7866
7867 let text = r#"
7868 a
7869 b
7870 c
7871 "#
7872 .unindent();
7873
7874 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7875 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7876 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7877 editor
7878 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7879 .await;
7880
7881 editor.update_in(cx, |editor, window, cx| {
7882 editor.change_selections(None, window, cx, |s| {
7883 s.select_display_ranges([
7884 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7885 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7886 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
7887 ])
7888 });
7889
7890 editor.handle_input("{", window, cx);
7891 editor.handle_input("{", window, cx);
7892 editor.handle_input("{", window, cx);
7893 assert_eq!(
7894 editor.text(cx),
7895 "
7896 {{{a}}}
7897 {{{b}}}
7898 {{{c}}}
7899 "
7900 .unindent()
7901 );
7902 assert_eq!(
7903 editor.selections.display_ranges(cx),
7904 [
7905 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
7906 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
7907 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
7908 ]
7909 );
7910
7911 editor.undo(&Undo, window, cx);
7912 editor.undo(&Undo, window, cx);
7913 editor.undo(&Undo, window, cx);
7914 assert_eq!(
7915 editor.text(cx),
7916 "
7917 a
7918 b
7919 c
7920 "
7921 .unindent()
7922 );
7923 assert_eq!(
7924 editor.selections.display_ranges(cx),
7925 [
7926 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7927 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7928 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
7929 ]
7930 );
7931
7932 // Ensure inserting the first character of a multi-byte bracket pair
7933 // doesn't surround the selections with the bracket.
7934 editor.handle_input("/", window, cx);
7935 assert_eq!(
7936 editor.text(cx),
7937 "
7938 /
7939 /
7940 /
7941 "
7942 .unindent()
7943 );
7944 assert_eq!(
7945 editor.selections.display_ranges(cx),
7946 [
7947 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
7948 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
7949 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
7950 ]
7951 );
7952
7953 editor.undo(&Undo, window, cx);
7954 assert_eq!(
7955 editor.text(cx),
7956 "
7957 a
7958 b
7959 c
7960 "
7961 .unindent()
7962 );
7963 assert_eq!(
7964 editor.selections.display_ranges(cx),
7965 [
7966 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7967 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7968 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
7969 ]
7970 );
7971
7972 // Ensure inserting the last character of a multi-byte bracket pair
7973 // doesn't surround the selections with the bracket.
7974 editor.handle_input("*", window, cx);
7975 assert_eq!(
7976 editor.text(cx),
7977 "
7978 *
7979 *
7980 *
7981 "
7982 .unindent()
7983 );
7984 assert_eq!(
7985 editor.selections.display_ranges(cx),
7986 [
7987 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
7988 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
7989 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
7990 ]
7991 );
7992 });
7993}
7994
7995#[gpui::test]
7996async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
7997 init_test(cx, |_| {});
7998
7999 let language = Arc::new(Language::new(
8000 LanguageConfig {
8001 brackets: BracketPairConfig {
8002 pairs: vec![BracketPair {
8003 start: "{".to_string(),
8004 end: "}".to_string(),
8005 close: true,
8006 surround: true,
8007 newline: true,
8008 }],
8009 ..Default::default()
8010 },
8011 autoclose_before: "}".to_string(),
8012 ..Default::default()
8013 },
8014 Some(tree_sitter_rust::LANGUAGE.into()),
8015 ));
8016
8017 let text = r#"
8018 a
8019 b
8020 c
8021 "#
8022 .unindent();
8023
8024 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8025 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8026 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8027 editor
8028 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8029 .await;
8030
8031 editor.update_in(cx, |editor, window, cx| {
8032 editor.change_selections(None, window, cx, |s| {
8033 s.select_ranges([
8034 Point::new(0, 1)..Point::new(0, 1),
8035 Point::new(1, 1)..Point::new(1, 1),
8036 Point::new(2, 1)..Point::new(2, 1),
8037 ])
8038 });
8039
8040 editor.handle_input("{", window, cx);
8041 editor.handle_input("{", window, cx);
8042 editor.handle_input("_", window, cx);
8043 assert_eq!(
8044 editor.text(cx),
8045 "
8046 a{{_}}
8047 b{{_}}
8048 c{{_}}
8049 "
8050 .unindent()
8051 );
8052 assert_eq!(
8053 editor.selections.ranges::<Point>(cx),
8054 [
8055 Point::new(0, 4)..Point::new(0, 4),
8056 Point::new(1, 4)..Point::new(1, 4),
8057 Point::new(2, 4)..Point::new(2, 4)
8058 ]
8059 );
8060
8061 editor.backspace(&Default::default(), window, cx);
8062 editor.backspace(&Default::default(), window, cx);
8063 assert_eq!(
8064 editor.text(cx),
8065 "
8066 a{}
8067 b{}
8068 c{}
8069 "
8070 .unindent()
8071 );
8072 assert_eq!(
8073 editor.selections.ranges::<Point>(cx),
8074 [
8075 Point::new(0, 2)..Point::new(0, 2),
8076 Point::new(1, 2)..Point::new(1, 2),
8077 Point::new(2, 2)..Point::new(2, 2)
8078 ]
8079 );
8080
8081 editor.delete_to_previous_word_start(&Default::default(), window, cx);
8082 assert_eq!(
8083 editor.text(cx),
8084 "
8085 a
8086 b
8087 c
8088 "
8089 .unindent()
8090 );
8091 assert_eq!(
8092 editor.selections.ranges::<Point>(cx),
8093 [
8094 Point::new(0, 1)..Point::new(0, 1),
8095 Point::new(1, 1)..Point::new(1, 1),
8096 Point::new(2, 1)..Point::new(2, 1)
8097 ]
8098 );
8099 });
8100}
8101
8102#[gpui::test]
8103async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
8104 init_test(cx, |settings| {
8105 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
8106 });
8107
8108 let mut cx = EditorTestContext::new(cx).await;
8109
8110 let language = Arc::new(Language::new(
8111 LanguageConfig {
8112 brackets: BracketPairConfig {
8113 pairs: vec![
8114 BracketPair {
8115 start: "{".to_string(),
8116 end: "}".to_string(),
8117 close: true,
8118 surround: true,
8119 newline: true,
8120 },
8121 BracketPair {
8122 start: "(".to_string(),
8123 end: ")".to_string(),
8124 close: true,
8125 surround: true,
8126 newline: true,
8127 },
8128 BracketPair {
8129 start: "[".to_string(),
8130 end: "]".to_string(),
8131 close: false,
8132 surround: true,
8133 newline: true,
8134 },
8135 ],
8136 ..Default::default()
8137 },
8138 autoclose_before: "})]".to_string(),
8139 ..Default::default()
8140 },
8141 Some(tree_sitter_rust::LANGUAGE.into()),
8142 ));
8143
8144 cx.language_registry().add(language.clone());
8145 cx.update_buffer(|buffer, cx| {
8146 buffer.set_language(Some(language), cx);
8147 });
8148
8149 cx.set_state(
8150 &"
8151 {(ˇ)}
8152 [[ˇ]]
8153 {(ˇ)}
8154 "
8155 .unindent(),
8156 );
8157
8158 cx.update_editor(|editor, window, cx| {
8159 editor.backspace(&Default::default(), window, cx);
8160 editor.backspace(&Default::default(), window, cx);
8161 });
8162
8163 cx.assert_editor_state(
8164 &"
8165 ˇ
8166 ˇ]]
8167 ˇ
8168 "
8169 .unindent(),
8170 );
8171
8172 cx.update_editor(|editor, window, cx| {
8173 editor.handle_input("{", window, cx);
8174 editor.handle_input("{", window, cx);
8175 editor.move_right(&MoveRight, window, cx);
8176 editor.move_right(&MoveRight, window, cx);
8177 editor.move_left(&MoveLeft, window, cx);
8178 editor.move_left(&MoveLeft, window, cx);
8179 editor.backspace(&Default::default(), window, cx);
8180 });
8181
8182 cx.assert_editor_state(
8183 &"
8184 {ˇ}
8185 {ˇ}]]
8186 {ˇ}
8187 "
8188 .unindent(),
8189 );
8190
8191 cx.update_editor(|editor, window, cx| {
8192 editor.backspace(&Default::default(), window, cx);
8193 });
8194
8195 cx.assert_editor_state(
8196 &"
8197 ˇ
8198 ˇ]]
8199 ˇ
8200 "
8201 .unindent(),
8202 );
8203}
8204
8205#[gpui::test]
8206async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
8207 init_test(cx, |_| {});
8208
8209 let language = Arc::new(Language::new(
8210 LanguageConfig::default(),
8211 Some(tree_sitter_rust::LANGUAGE.into()),
8212 ));
8213
8214 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
8215 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8216 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8217 editor
8218 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8219 .await;
8220
8221 editor.update_in(cx, |editor, window, cx| {
8222 editor.set_auto_replace_emoji_shortcode(true);
8223
8224 editor.handle_input("Hello ", window, cx);
8225 editor.handle_input(":wave", window, cx);
8226 assert_eq!(editor.text(cx), "Hello :wave".unindent());
8227
8228 editor.handle_input(":", window, cx);
8229 assert_eq!(editor.text(cx), "Hello 👋".unindent());
8230
8231 editor.handle_input(" :smile", window, cx);
8232 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
8233
8234 editor.handle_input(":", window, cx);
8235 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
8236
8237 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
8238 editor.handle_input(":wave", window, cx);
8239 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
8240
8241 editor.handle_input(":", window, cx);
8242 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
8243
8244 editor.handle_input(":1", window, cx);
8245 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
8246
8247 editor.handle_input(":", window, cx);
8248 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
8249
8250 // Ensure shortcode does not get replaced when it is part of a word
8251 editor.handle_input(" Test:wave", window, cx);
8252 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
8253
8254 editor.handle_input(":", window, cx);
8255 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
8256
8257 editor.set_auto_replace_emoji_shortcode(false);
8258
8259 // Ensure shortcode does not get replaced when auto replace is off
8260 editor.handle_input(" :wave", window, cx);
8261 assert_eq!(
8262 editor.text(cx),
8263 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
8264 );
8265
8266 editor.handle_input(":", window, cx);
8267 assert_eq!(
8268 editor.text(cx),
8269 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
8270 );
8271 });
8272}
8273
8274#[gpui::test]
8275async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
8276 init_test(cx, |_| {});
8277
8278 let (text, insertion_ranges) = marked_text_ranges(
8279 indoc! {"
8280 ˇ
8281 "},
8282 false,
8283 );
8284
8285 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
8286 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8287
8288 _ = editor.update_in(cx, |editor, window, cx| {
8289 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
8290
8291 editor
8292 .insert_snippet(&insertion_ranges, snippet, window, cx)
8293 .unwrap();
8294
8295 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
8296 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
8297 assert_eq!(editor.text(cx), expected_text);
8298 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
8299 }
8300
8301 assert(
8302 editor,
8303 cx,
8304 indoc! {"
8305 type «» =•
8306 "},
8307 );
8308
8309 assert!(editor.context_menu_visible(), "There should be a matches");
8310 });
8311}
8312
8313#[gpui::test]
8314async fn test_snippets(cx: &mut TestAppContext) {
8315 init_test(cx, |_| {});
8316
8317 let (text, insertion_ranges) = marked_text_ranges(
8318 indoc! {"
8319 a.ˇ b
8320 a.ˇ b
8321 a.ˇ b
8322 "},
8323 false,
8324 );
8325
8326 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
8327 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8328
8329 editor.update_in(cx, |editor, window, cx| {
8330 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
8331
8332 editor
8333 .insert_snippet(&insertion_ranges, snippet, window, cx)
8334 .unwrap();
8335
8336 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
8337 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
8338 assert_eq!(editor.text(cx), expected_text);
8339 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
8340 }
8341
8342 assert(
8343 editor,
8344 cx,
8345 indoc! {"
8346 a.f(«one», two, «three») b
8347 a.f(«one», two, «three») b
8348 a.f(«one», two, «three») b
8349 "},
8350 );
8351
8352 // Can't move earlier than the first tab stop
8353 assert!(!editor.move_to_prev_snippet_tabstop(window, cx));
8354 assert(
8355 editor,
8356 cx,
8357 indoc! {"
8358 a.f(«one», two, «three») b
8359 a.f(«one», two, «three») b
8360 a.f(«one», two, «three») b
8361 "},
8362 );
8363
8364 assert!(editor.move_to_next_snippet_tabstop(window, cx));
8365 assert(
8366 editor,
8367 cx,
8368 indoc! {"
8369 a.f(one, «two», three) b
8370 a.f(one, «two», three) b
8371 a.f(one, «two», three) b
8372 "},
8373 );
8374
8375 editor.move_to_prev_snippet_tabstop(window, cx);
8376 assert(
8377 editor,
8378 cx,
8379 indoc! {"
8380 a.f(«one», two, «three») b
8381 a.f(«one», two, «three») b
8382 a.f(«one», two, «three») b
8383 "},
8384 );
8385
8386 assert!(editor.move_to_next_snippet_tabstop(window, cx));
8387 assert(
8388 editor,
8389 cx,
8390 indoc! {"
8391 a.f(one, «two», three) b
8392 a.f(one, «two», three) b
8393 a.f(one, «two», three) b
8394 "},
8395 );
8396 assert!(editor.move_to_next_snippet_tabstop(window, cx));
8397 assert(
8398 editor,
8399 cx,
8400 indoc! {"
8401 a.f(one, two, three)ˇ b
8402 a.f(one, two, three)ˇ b
8403 a.f(one, two, three)ˇ b
8404 "},
8405 );
8406
8407 // As soon as the last tab stop is reached, snippet state is gone
8408 editor.move_to_prev_snippet_tabstop(window, cx);
8409 assert(
8410 editor,
8411 cx,
8412 indoc! {"
8413 a.f(one, two, three)ˇ b
8414 a.f(one, two, three)ˇ b
8415 a.f(one, two, three)ˇ b
8416 "},
8417 );
8418 });
8419}
8420
8421#[gpui::test]
8422async fn test_document_format_during_save(cx: &mut TestAppContext) {
8423 init_test(cx, |_| {});
8424
8425 let fs = FakeFs::new(cx.executor());
8426 fs.insert_file(path!("/file.rs"), Default::default()).await;
8427
8428 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
8429
8430 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8431 language_registry.add(rust_lang());
8432 let mut fake_servers = language_registry.register_fake_lsp(
8433 "Rust",
8434 FakeLspAdapter {
8435 capabilities: lsp::ServerCapabilities {
8436 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8437 ..Default::default()
8438 },
8439 ..Default::default()
8440 },
8441 );
8442
8443 let buffer = project
8444 .update(cx, |project, cx| {
8445 project.open_local_buffer(path!("/file.rs"), cx)
8446 })
8447 .await
8448 .unwrap();
8449
8450 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8451 let (editor, cx) = cx.add_window_view(|window, cx| {
8452 build_editor_with_project(project.clone(), buffer, window, cx)
8453 });
8454 editor.update_in(cx, |editor, window, cx| {
8455 editor.set_text("one\ntwo\nthree\n", window, cx)
8456 });
8457 assert!(cx.read(|cx| editor.is_dirty(cx)));
8458
8459 cx.executor().start_waiting();
8460 let fake_server = fake_servers.next().await.unwrap();
8461
8462 {
8463 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8464 move |params, _| async move {
8465 assert_eq!(
8466 params.text_document.uri,
8467 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8468 );
8469 assert_eq!(params.options.tab_size, 4);
8470 Ok(Some(vec![lsp::TextEdit::new(
8471 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8472 ", ".to_string(),
8473 )]))
8474 },
8475 );
8476 let save = editor
8477 .update_in(cx, |editor, window, cx| {
8478 editor.save(true, project.clone(), window, cx)
8479 })
8480 .unwrap();
8481 cx.executor().start_waiting();
8482 save.await;
8483
8484 assert_eq!(
8485 editor.update(cx, |editor, cx| editor.text(cx)),
8486 "one, two\nthree\n"
8487 );
8488 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8489 }
8490
8491 {
8492 editor.update_in(cx, |editor, window, cx| {
8493 editor.set_text("one\ntwo\nthree\n", window, cx)
8494 });
8495 assert!(cx.read(|cx| editor.is_dirty(cx)));
8496
8497 // Ensure we can still save even if formatting hangs.
8498 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8499 move |params, _| async move {
8500 assert_eq!(
8501 params.text_document.uri,
8502 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8503 );
8504 futures::future::pending::<()>().await;
8505 unreachable!()
8506 },
8507 );
8508 let save = editor
8509 .update_in(cx, |editor, window, cx| {
8510 editor.save(true, project.clone(), window, cx)
8511 })
8512 .unwrap();
8513 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8514 cx.executor().start_waiting();
8515 save.await;
8516 assert_eq!(
8517 editor.update(cx, |editor, cx| editor.text(cx)),
8518 "one\ntwo\nthree\n"
8519 );
8520 }
8521
8522 // For non-dirty buffer, no formatting request should be sent
8523 {
8524 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8525
8526 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
8527 panic!("Should not be invoked on non-dirty buffer");
8528 });
8529 let save = editor
8530 .update_in(cx, |editor, window, cx| {
8531 editor.save(true, project.clone(), window, cx)
8532 })
8533 .unwrap();
8534 cx.executor().start_waiting();
8535 save.await;
8536 }
8537
8538 // Set rust language override and assert overridden tabsize is sent to language server
8539 update_test_language_settings(cx, |settings| {
8540 settings.languages.insert(
8541 "Rust".into(),
8542 LanguageSettingsContent {
8543 tab_size: NonZeroU32::new(8),
8544 ..Default::default()
8545 },
8546 );
8547 });
8548
8549 {
8550 editor.update_in(cx, |editor, window, cx| {
8551 editor.set_text("somehting_new\n", window, cx)
8552 });
8553 assert!(cx.read(|cx| editor.is_dirty(cx)));
8554 let _formatting_request_signal = fake_server
8555 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
8556 assert_eq!(
8557 params.text_document.uri,
8558 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8559 );
8560 assert_eq!(params.options.tab_size, 8);
8561 Ok(Some(vec![]))
8562 });
8563 let save = editor
8564 .update_in(cx, |editor, window, cx| {
8565 editor.save(true, project.clone(), window, cx)
8566 })
8567 .unwrap();
8568 cx.executor().start_waiting();
8569 save.await;
8570 }
8571}
8572
8573#[gpui::test]
8574async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
8575 init_test(cx, |_| {});
8576
8577 let cols = 4;
8578 let rows = 10;
8579 let sample_text_1 = sample_text(rows, cols, 'a');
8580 assert_eq!(
8581 sample_text_1,
8582 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
8583 );
8584 let sample_text_2 = sample_text(rows, cols, 'l');
8585 assert_eq!(
8586 sample_text_2,
8587 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
8588 );
8589 let sample_text_3 = sample_text(rows, cols, 'v');
8590 assert_eq!(
8591 sample_text_3,
8592 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
8593 );
8594
8595 let fs = FakeFs::new(cx.executor());
8596 fs.insert_tree(
8597 path!("/a"),
8598 json!({
8599 "main.rs": sample_text_1,
8600 "other.rs": sample_text_2,
8601 "lib.rs": sample_text_3,
8602 }),
8603 )
8604 .await;
8605
8606 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
8607 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
8608 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
8609
8610 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8611 language_registry.add(rust_lang());
8612 let mut fake_servers = language_registry.register_fake_lsp(
8613 "Rust",
8614 FakeLspAdapter {
8615 capabilities: lsp::ServerCapabilities {
8616 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8617 ..Default::default()
8618 },
8619 ..Default::default()
8620 },
8621 );
8622
8623 let worktree = project.update(cx, |project, cx| {
8624 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
8625 assert_eq!(worktrees.len(), 1);
8626 worktrees.pop().unwrap()
8627 });
8628 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
8629
8630 let buffer_1 = project
8631 .update(cx, |project, cx| {
8632 project.open_buffer((worktree_id, "main.rs"), cx)
8633 })
8634 .await
8635 .unwrap();
8636 let buffer_2 = project
8637 .update(cx, |project, cx| {
8638 project.open_buffer((worktree_id, "other.rs"), cx)
8639 })
8640 .await
8641 .unwrap();
8642 let buffer_3 = project
8643 .update(cx, |project, cx| {
8644 project.open_buffer((worktree_id, "lib.rs"), cx)
8645 })
8646 .await
8647 .unwrap();
8648
8649 let multi_buffer = cx.new(|cx| {
8650 let mut multi_buffer = MultiBuffer::new(ReadWrite);
8651 multi_buffer.push_excerpts(
8652 buffer_1.clone(),
8653 [
8654 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8655 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8656 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8657 ],
8658 cx,
8659 );
8660 multi_buffer.push_excerpts(
8661 buffer_2.clone(),
8662 [
8663 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8664 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8665 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8666 ],
8667 cx,
8668 );
8669 multi_buffer.push_excerpts(
8670 buffer_3.clone(),
8671 [
8672 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8673 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8674 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8675 ],
8676 cx,
8677 );
8678 multi_buffer
8679 });
8680 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
8681 Editor::new(
8682 EditorMode::full(),
8683 multi_buffer,
8684 Some(project.clone()),
8685 window,
8686 cx,
8687 )
8688 });
8689
8690 multi_buffer_editor.update_in(cx, |editor, window, cx| {
8691 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
8692 s.select_ranges(Some(1..2))
8693 });
8694 editor.insert("|one|two|three|", window, cx);
8695 });
8696 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
8697 multi_buffer_editor.update_in(cx, |editor, window, cx| {
8698 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
8699 s.select_ranges(Some(60..70))
8700 });
8701 editor.insert("|four|five|six|", window, cx);
8702 });
8703 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
8704
8705 // First two buffers should be edited, but not the third one.
8706 assert_eq!(
8707 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
8708 "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}",
8709 );
8710 buffer_1.update(cx, |buffer, _| {
8711 assert!(buffer.is_dirty());
8712 assert_eq!(
8713 buffer.text(),
8714 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
8715 )
8716 });
8717 buffer_2.update(cx, |buffer, _| {
8718 assert!(buffer.is_dirty());
8719 assert_eq!(
8720 buffer.text(),
8721 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
8722 )
8723 });
8724 buffer_3.update(cx, |buffer, _| {
8725 assert!(!buffer.is_dirty());
8726 assert_eq!(buffer.text(), sample_text_3,)
8727 });
8728 cx.executor().run_until_parked();
8729
8730 cx.executor().start_waiting();
8731 let save = multi_buffer_editor
8732 .update_in(cx, |editor, window, cx| {
8733 editor.save(true, project.clone(), window, cx)
8734 })
8735 .unwrap();
8736
8737 let fake_server = fake_servers.next().await.unwrap();
8738 fake_server
8739 .server
8740 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
8741 Ok(Some(vec![lsp::TextEdit::new(
8742 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8743 format!("[{} formatted]", params.text_document.uri),
8744 )]))
8745 })
8746 .detach();
8747 save.await;
8748
8749 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
8750 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
8751 assert_eq!(
8752 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
8753 uri!(
8754 "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}"
8755 ),
8756 );
8757 buffer_1.update(cx, |buffer, _| {
8758 assert!(!buffer.is_dirty());
8759 assert_eq!(
8760 buffer.text(),
8761 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
8762 )
8763 });
8764 buffer_2.update(cx, |buffer, _| {
8765 assert!(!buffer.is_dirty());
8766 assert_eq!(
8767 buffer.text(),
8768 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
8769 )
8770 });
8771 buffer_3.update(cx, |buffer, _| {
8772 assert!(!buffer.is_dirty());
8773 assert_eq!(buffer.text(), sample_text_3,)
8774 });
8775}
8776
8777#[gpui::test]
8778async fn test_range_format_during_save(cx: &mut TestAppContext) {
8779 init_test(cx, |_| {});
8780
8781 let fs = FakeFs::new(cx.executor());
8782 fs.insert_file(path!("/file.rs"), Default::default()).await;
8783
8784 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8785
8786 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8787 language_registry.add(rust_lang());
8788 let mut fake_servers = language_registry.register_fake_lsp(
8789 "Rust",
8790 FakeLspAdapter {
8791 capabilities: lsp::ServerCapabilities {
8792 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
8793 ..Default::default()
8794 },
8795 ..Default::default()
8796 },
8797 );
8798
8799 let buffer = project
8800 .update(cx, |project, cx| {
8801 project.open_local_buffer(path!("/file.rs"), cx)
8802 })
8803 .await
8804 .unwrap();
8805
8806 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8807 let (editor, cx) = cx.add_window_view(|window, cx| {
8808 build_editor_with_project(project.clone(), buffer, window, cx)
8809 });
8810 editor.update_in(cx, |editor, window, cx| {
8811 editor.set_text("one\ntwo\nthree\n", window, cx)
8812 });
8813 assert!(cx.read(|cx| editor.is_dirty(cx)));
8814
8815 cx.executor().start_waiting();
8816 let fake_server = fake_servers.next().await.unwrap();
8817
8818 let save = editor
8819 .update_in(cx, |editor, window, cx| {
8820 editor.save(true, project.clone(), window, cx)
8821 })
8822 .unwrap();
8823 fake_server
8824 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
8825 assert_eq!(
8826 params.text_document.uri,
8827 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8828 );
8829 assert_eq!(params.options.tab_size, 4);
8830 Ok(Some(vec![lsp::TextEdit::new(
8831 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8832 ", ".to_string(),
8833 )]))
8834 })
8835 .next()
8836 .await;
8837 cx.executor().start_waiting();
8838 save.await;
8839 assert_eq!(
8840 editor.update(cx, |editor, cx| editor.text(cx)),
8841 "one, two\nthree\n"
8842 );
8843 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8844
8845 editor.update_in(cx, |editor, window, cx| {
8846 editor.set_text("one\ntwo\nthree\n", window, cx)
8847 });
8848 assert!(cx.read(|cx| editor.is_dirty(cx)));
8849
8850 // Ensure we can still save even if formatting hangs.
8851 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
8852 move |params, _| async move {
8853 assert_eq!(
8854 params.text_document.uri,
8855 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8856 );
8857 futures::future::pending::<()>().await;
8858 unreachable!()
8859 },
8860 );
8861 let save = editor
8862 .update_in(cx, |editor, window, cx| {
8863 editor.save(true, project.clone(), window, cx)
8864 })
8865 .unwrap();
8866 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8867 cx.executor().start_waiting();
8868 save.await;
8869 assert_eq!(
8870 editor.update(cx, |editor, cx| editor.text(cx)),
8871 "one\ntwo\nthree\n"
8872 );
8873 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8874
8875 // For non-dirty buffer, no formatting request should be sent
8876 let save = editor
8877 .update_in(cx, |editor, window, cx| {
8878 editor.save(true, project.clone(), window, cx)
8879 })
8880 .unwrap();
8881 let _pending_format_request = fake_server
8882 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
8883 panic!("Should not be invoked on non-dirty buffer");
8884 })
8885 .next();
8886 cx.executor().start_waiting();
8887 save.await;
8888
8889 // Set Rust language override and assert overridden tabsize is sent to language server
8890 update_test_language_settings(cx, |settings| {
8891 settings.languages.insert(
8892 "Rust".into(),
8893 LanguageSettingsContent {
8894 tab_size: NonZeroU32::new(8),
8895 ..Default::default()
8896 },
8897 );
8898 });
8899
8900 editor.update_in(cx, |editor, window, cx| {
8901 editor.set_text("somehting_new\n", window, cx)
8902 });
8903 assert!(cx.read(|cx| editor.is_dirty(cx)));
8904 let save = editor
8905 .update_in(cx, |editor, window, cx| {
8906 editor.save(true, project.clone(), window, cx)
8907 })
8908 .unwrap();
8909 fake_server
8910 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
8911 assert_eq!(
8912 params.text_document.uri,
8913 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8914 );
8915 assert_eq!(params.options.tab_size, 8);
8916 Ok(Some(vec![]))
8917 })
8918 .next()
8919 .await;
8920 cx.executor().start_waiting();
8921 save.await;
8922}
8923
8924#[gpui::test]
8925async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
8926 init_test(cx, |settings| {
8927 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
8928 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
8929 ))
8930 });
8931
8932 let fs = FakeFs::new(cx.executor());
8933 fs.insert_file(path!("/file.rs"), Default::default()).await;
8934
8935 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8936
8937 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8938 language_registry.add(Arc::new(Language::new(
8939 LanguageConfig {
8940 name: "Rust".into(),
8941 matcher: LanguageMatcher {
8942 path_suffixes: vec!["rs".to_string()],
8943 ..Default::default()
8944 },
8945 ..LanguageConfig::default()
8946 },
8947 Some(tree_sitter_rust::LANGUAGE.into()),
8948 )));
8949 update_test_language_settings(cx, |settings| {
8950 // Enable Prettier formatting for the same buffer, and ensure
8951 // LSP is called instead of Prettier.
8952 settings.defaults.prettier = Some(PrettierSettings {
8953 allowed: true,
8954 ..PrettierSettings::default()
8955 });
8956 });
8957 let mut fake_servers = language_registry.register_fake_lsp(
8958 "Rust",
8959 FakeLspAdapter {
8960 capabilities: lsp::ServerCapabilities {
8961 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8962 ..Default::default()
8963 },
8964 ..Default::default()
8965 },
8966 );
8967
8968 let buffer = project
8969 .update(cx, |project, cx| {
8970 project.open_local_buffer(path!("/file.rs"), cx)
8971 })
8972 .await
8973 .unwrap();
8974
8975 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8976 let (editor, cx) = cx.add_window_view(|window, cx| {
8977 build_editor_with_project(project.clone(), buffer, window, cx)
8978 });
8979 editor.update_in(cx, |editor, window, cx| {
8980 editor.set_text("one\ntwo\nthree\n", window, cx)
8981 });
8982
8983 cx.executor().start_waiting();
8984 let fake_server = fake_servers.next().await.unwrap();
8985
8986 let format = editor
8987 .update_in(cx, |editor, window, cx| {
8988 editor.perform_format(
8989 project.clone(),
8990 FormatTrigger::Manual,
8991 FormatTarget::Buffers,
8992 window,
8993 cx,
8994 )
8995 })
8996 .unwrap();
8997 fake_server
8998 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
8999 assert_eq!(
9000 params.text_document.uri,
9001 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9002 );
9003 assert_eq!(params.options.tab_size, 4);
9004 Ok(Some(vec![lsp::TextEdit::new(
9005 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9006 ", ".to_string(),
9007 )]))
9008 })
9009 .next()
9010 .await;
9011 cx.executor().start_waiting();
9012 format.await;
9013 assert_eq!(
9014 editor.update(cx, |editor, cx| editor.text(cx)),
9015 "one, two\nthree\n"
9016 );
9017
9018 editor.update_in(cx, |editor, window, cx| {
9019 editor.set_text("one\ntwo\nthree\n", window, cx)
9020 });
9021 // Ensure we don't lock if formatting hangs.
9022 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9023 move |params, _| async move {
9024 assert_eq!(
9025 params.text_document.uri,
9026 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9027 );
9028 futures::future::pending::<()>().await;
9029 unreachable!()
9030 },
9031 );
9032 let format = editor
9033 .update_in(cx, |editor, window, cx| {
9034 editor.perform_format(
9035 project,
9036 FormatTrigger::Manual,
9037 FormatTarget::Buffers,
9038 window,
9039 cx,
9040 )
9041 })
9042 .unwrap();
9043 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9044 cx.executor().start_waiting();
9045 format.await;
9046 assert_eq!(
9047 editor.update(cx, |editor, cx| editor.text(cx)),
9048 "one\ntwo\nthree\n"
9049 );
9050}
9051
9052#[gpui::test]
9053async fn test_multiple_formatters(cx: &mut TestAppContext) {
9054 init_test(cx, |settings| {
9055 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
9056 settings.defaults.formatter =
9057 Some(language_settings::SelectedFormatter::List(FormatterList(
9058 vec![
9059 Formatter::LanguageServer { name: None },
9060 Formatter::CodeActions(
9061 [
9062 ("code-action-1".into(), true),
9063 ("code-action-2".into(), true),
9064 ]
9065 .into_iter()
9066 .collect(),
9067 ),
9068 ]
9069 .into(),
9070 )))
9071 });
9072
9073 let fs = FakeFs::new(cx.executor());
9074 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
9075 .await;
9076
9077 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9078 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9079 language_registry.add(rust_lang());
9080
9081 let mut fake_servers = language_registry.register_fake_lsp(
9082 "Rust",
9083 FakeLspAdapter {
9084 capabilities: lsp::ServerCapabilities {
9085 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9086 execute_command_provider: Some(lsp::ExecuteCommandOptions {
9087 commands: vec!["the-command-for-code-action-1".into()],
9088 ..Default::default()
9089 }),
9090 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
9091 ..Default::default()
9092 },
9093 ..Default::default()
9094 },
9095 );
9096
9097 let buffer = project
9098 .update(cx, |project, cx| {
9099 project.open_local_buffer(path!("/file.rs"), cx)
9100 })
9101 .await
9102 .unwrap();
9103
9104 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9105 let (editor, cx) = cx.add_window_view(|window, cx| {
9106 build_editor_with_project(project.clone(), buffer, window, cx)
9107 });
9108
9109 cx.executor().start_waiting();
9110
9111 let fake_server = fake_servers.next().await.unwrap();
9112 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9113 move |_params, _| async move {
9114 Ok(Some(vec![lsp::TextEdit::new(
9115 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9116 "applied-formatting\n".to_string(),
9117 )]))
9118 },
9119 );
9120 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
9121 move |params, _| async move {
9122 assert_eq!(
9123 params.context.only,
9124 Some(vec!["code-action-1".into(), "code-action-2".into()])
9125 );
9126 let uri = lsp::Url::from_file_path(path!("/file.rs")).unwrap();
9127 Ok(Some(vec![
9128 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
9129 kind: Some("code-action-1".into()),
9130 edit: Some(lsp::WorkspaceEdit::new(
9131 [(
9132 uri.clone(),
9133 vec![lsp::TextEdit::new(
9134 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9135 "applied-code-action-1-edit\n".to_string(),
9136 )],
9137 )]
9138 .into_iter()
9139 .collect(),
9140 )),
9141 command: Some(lsp::Command {
9142 command: "the-command-for-code-action-1".into(),
9143 ..Default::default()
9144 }),
9145 ..Default::default()
9146 }),
9147 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
9148 kind: Some("code-action-2".into()),
9149 edit: Some(lsp::WorkspaceEdit::new(
9150 [(
9151 uri.clone(),
9152 vec![lsp::TextEdit::new(
9153 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9154 "applied-code-action-2-edit\n".to_string(),
9155 )],
9156 )]
9157 .into_iter()
9158 .collect(),
9159 )),
9160 ..Default::default()
9161 }),
9162 ]))
9163 },
9164 );
9165
9166 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
9167 move |params, _| async move { Ok(params) }
9168 });
9169
9170 let command_lock = Arc::new(futures::lock::Mutex::new(()));
9171 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
9172 let fake = fake_server.clone();
9173 let lock = command_lock.clone();
9174 move |params, _| {
9175 assert_eq!(params.command, "the-command-for-code-action-1");
9176 let fake = fake.clone();
9177 let lock = lock.clone();
9178 async move {
9179 lock.lock().await;
9180 fake.server
9181 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
9182 label: None,
9183 edit: lsp::WorkspaceEdit {
9184 changes: Some(
9185 [(
9186 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
9187 vec![lsp::TextEdit {
9188 range: lsp::Range::new(
9189 lsp::Position::new(0, 0),
9190 lsp::Position::new(0, 0),
9191 ),
9192 new_text: "applied-code-action-1-command\n".into(),
9193 }],
9194 )]
9195 .into_iter()
9196 .collect(),
9197 ),
9198 ..Default::default()
9199 },
9200 })
9201 .await
9202 .into_response()
9203 .unwrap();
9204 Ok(Some(json!(null)))
9205 }
9206 }
9207 });
9208
9209 cx.executor().start_waiting();
9210 editor
9211 .update_in(cx, |editor, window, cx| {
9212 editor.perform_format(
9213 project.clone(),
9214 FormatTrigger::Manual,
9215 FormatTarget::Buffers,
9216 window,
9217 cx,
9218 )
9219 })
9220 .unwrap()
9221 .await;
9222 editor.update(cx, |editor, cx| {
9223 assert_eq!(
9224 editor.text(cx),
9225 r#"
9226 applied-code-action-2-edit
9227 applied-code-action-1-command
9228 applied-code-action-1-edit
9229 applied-formatting
9230 one
9231 two
9232 three
9233 "#
9234 .unindent()
9235 );
9236 });
9237
9238 editor.update_in(cx, |editor, window, cx| {
9239 editor.undo(&Default::default(), window, cx);
9240 assert_eq!(editor.text(cx), "one \ntwo \nthree");
9241 });
9242
9243 // Perform a manual edit while waiting for an LSP command
9244 // that's being run as part of a formatting code action.
9245 let lock_guard = command_lock.lock().await;
9246 let format = editor
9247 .update_in(cx, |editor, window, cx| {
9248 editor.perform_format(
9249 project.clone(),
9250 FormatTrigger::Manual,
9251 FormatTarget::Buffers,
9252 window,
9253 cx,
9254 )
9255 })
9256 .unwrap();
9257 cx.run_until_parked();
9258 editor.update(cx, |editor, cx| {
9259 assert_eq!(
9260 editor.text(cx),
9261 r#"
9262 applied-code-action-1-edit
9263 applied-formatting
9264 one
9265 two
9266 three
9267 "#
9268 .unindent()
9269 );
9270
9271 editor.buffer.update(cx, |buffer, cx| {
9272 let ix = buffer.len(cx);
9273 buffer.edit([(ix..ix, "edited\n")], None, cx);
9274 });
9275 });
9276
9277 // Allow the LSP command to proceed. Because the buffer was edited,
9278 // the second code action will not be run.
9279 drop(lock_guard);
9280 format.await;
9281 editor.update_in(cx, |editor, window, cx| {
9282 assert_eq!(
9283 editor.text(cx),
9284 r#"
9285 applied-code-action-1-command
9286 applied-code-action-1-edit
9287 applied-formatting
9288 one
9289 two
9290 three
9291 edited
9292 "#
9293 .unindent()
9294 );
9295
9296 // The manual edit is undone first, because it is the last thing the user did
9297 // (even though the command completed afterwards).
9298 editor.undo(&Default::default(), window, cx);
9299 assert_eq!(
9300 editor.text(cx),
9301 r#"
9302 applied-code-action-1-command
9303 applied-code-action-1-edit
9304 applied-formatting
9305 one
9306 two
9307 three
9308 "#
9309 .unindent()
9310 );
9311
9312 // All the formatting (including the command, which completed after the manual edit)
9313 // is undone together.
9314 editor.undo(&Default::default(), window, cx);
9315 assert_eq!(editor.text(cx), "one \ntwo \nthree");
9316 });
9317}
9318
9319#[gpui::test]
9320async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
9321 init_test(cx, |settings| {
9322 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
9323 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
9324 ))
9325 });
9326
9327 let fs = FakeFs::new(cx.executor());
9328 fs.insert_file(path!("/file.ts"), Default::default()).await;
9329
9330 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9331
9332 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9333 language_registry.add(Arc::new(Language::new(
9334 LanguageConfig {
9335 name: "TypeScript".into(),
9336 matcher: LanguageMatcher {
9337 path_suffixes: vec!["ts".to_string()],
9338 ..Default::default()
9339 },
9340 ..LanguageConfig::default()
9341 },
9342 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
9343 )));
9344 update_test_language_settings(cx, |settings| {
9345 settings.defaults.prettier = Some(PrettierSettings {
9346 allowed: true,
9347 ..PrettierSettings::default()
9348 });
9349 });
9350 let mut fake_servers = language_registry.register_fake_lsp(
9351 "TypeScript",
9352 FakeLspAdapter {
9353 capabilities: lsp::ServerCapabilities {
9354 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
9355 ..Default::default()
9356 },
9357 ..Default::default()
9358 },
9359 );
9360
9361 let buffer = project
9362 .update(cx, |project, cx| {
9363 project.open_local_buffer(path!("/file.ts"), cx)
9364 })
9365 .await
9366 .unwrap();
9367
9368 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9369 let (editor, cx) = cx.add_window_view(|window, cx| {
9370 build_editor_with_project(project.clone(), buffer, window, cx)
9371 });
9372 editor.update_in(cx, |editor, window, cx| {
9373 editor.set_text(
9374 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
9375 window,
9376 cx,
9377 )
9378 });
9379
9380 cx.executor().start_waiting();
9381 let fake_server = fake_servers.next().await.unwrap();
9382
9383 let format = editor
9384 .update_in(cx, |editor, window, cx| {
9385 editor.perform_code_action_kind(
9386 project.clone(),
9387 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
9388 window,
9389 cx,
9390 )
9391 })
9392 .unwrap();
9393 fake_server
9394 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
9395 assert_eq!(
9396 params.text_document.uri,
9397 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
9398 );
9399 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
9400 lsp::CodeAction {
9401 title: "Organize Imports".to_string(),
9402 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
9403 edit: Some(lsp::WorkspaceEdit {
9404 changes: Some(
9405 [(
9406 params.text_document.uri.clone(),
9407 vec![lsp::TextEdit::new(
9408 lsp::Range::new(
9409 lsp::Position::new(1, 0),
9410 lsp::Position::new(2, 0),
9411 ),
9412 "".to_string(),
9413 )],
9414 )]
9415 .into_iter()
9416 .collect(),
9417 ),
9418 ..Default::default()
9419 }),
9420 ..Default::default()
9421 },
9422 )]))
9423 })
9424 .next()
9425 .await;
9426 cx.executor().start_waiting();
9427 format.await;
9428 assert_eq!(
9429 editor.update(cx, |editor, cx| editor.text(cx)),
9430 "import { a } from 'module';\n\nconst x = a;\n"
9431 );
9432
9433 editor.update_in(cx, |editor, window, cx| {
9434 editor.set_text(
9435 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
9436 window,
9437 cx,
9438 )
9439 });
9440 // Ensure we don't lock if code action hangs.
9441 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
9442 move |params, _| async move {
9443 assert_eq!(
9444 params.text_document.uri,
9445 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
9446 );
9447 futures::future::pending::<()>().await;
9448 unreachable!()
9449 },
9450 );
9451 let format = editor
9452 .update_in(cx, |editor, window, cx| {
9453 editor.perform_code_action_kind(
9454 project,
9455 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
9456 window,
9457 cx,
9458 )
9459 })
9460 .unwrap();
9461 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
9462 cx.executor().start_waiting();
9463 format.await;
9464 assert_eq!(
9465 editor.update(cx, |editor, cx| editor.text(cx)),
9466 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
9467 );
9468}
9469
9470#[gpui::test]
9471async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
9472 init_test(cx, |_| {});
9473
9474 let mut cx = EditorLspTestContext::new_rust(
9475 lsp::ServerCapabilities {
9476 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9477 ..Default::default()
9478 },
9479 cx,
9480 )
9481 .await;
9482
9483 cx.set_state(indoc! {"
9484 one.twoˇ
9485 "});
9486
9487 // The format request takes a long time. When it completes, it inserts
9488 // a newline and an indent before the `.`
9489 cx.lsp
9490 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
9491 let executor = cx.background_executor().clone();
9492 async move {
9493 executor.timer(Duration::from_millis(100)).await;
9494 Ok(Some(vec![lsp::TextEdit {
9495 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
9496 new_text: "\n ".into(),
9497 }]))
9498 }
9499 });
9500
9501 // Submit a format request.
9502 let format_1 = cx
9503 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
9504 .unwrap();
9505 cx.executor().run_until_parked();
9506
9507 // Submit a second format request.
9508 let format_2 = cx
9509 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
9510 .unwrap();
9511 cx.executor().run_until_parked();
9512
9513 // Wait for both format requests to complete
9514 cx.executor().advance_clock(Duration::from_millis(200));
9515 cx.executor().start_waiting();
9516 format_1.await.unwrap();
9517 cx.executor().start_waiting();
9518 format_2.await.unwrap();
9519
9520 // The formatting edits only happens once.
9521 cx.assert_editor_state(indoc! {"
9522 one
9523 .twoˇ
9524 "});
9525}
9526
9527#[gpui::test]
9528async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
9529 init_test(cx, |settings| {
9530 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
9531 });
9532
9533 let mut cx = EditorLspTestContext::new_rust(
9534 lsp::ServerCapabilities {
9535 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9536 ..Default::default()
9537 },
9538 cx,
9539 )
9540 .await;
9541
9542 // Set up a buffer white some trailing whitespace and no trailing newline.
9543 cx.set_state(
9544 &[
9545 "one ", //
9546 "twoˇ", //
9547 "three ", //
9548 "four", //
9549 ]
9550 .join("\n"),
9551 );
9552
9553 // Submit a format request.
9554 let format = cx
9555 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
9556 .unwrap();
9557
9558 // Record which buffer changes have been sent to the language server
9559 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
9560 cx.lsp
9561 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
9562 let buffer_changes = buffer_changes.clone();
9563 move |params, _| {
9564 buffer_changes.lock().extend(
9565 params
9566 .content_changes
9567 .into_iter()
9568 .map(|e| (e.range.unwrap(), e.text)),
9569 );
9570 }
9571 });
9572
9573 // Handle formatting requests to the language server.
9574 cx.lsp
9575 .set_request_handler::<lsp::request::Formatting, _, _>({
9576 let buffer_changes = buffer_changes.clone();
9577 move |_, _| {
9578 // When formatting is requested, trailing whitespace has already been stripped,
9579 // and the trailing newline has already been added.
9580 assert_eq!(
9581 &buffer_changes.lock()[1..],
9582 &[
9583 (
9584 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
9585 "".into()
9586 ),
9587 (
9588 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
9589 "".into()
9590 ),
9591 (
9592 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
9593 "\n".into()
9594 ),
9595 ]
9596 );
9597
9598 // Insert blank lines between each line of the buffer.
9599 async move {
9600 Ok(Some(vec![
9601 lsp::TextEdit {
9602 range: lsp::Range::new(
9603 lsp::Position::new(1, 0),
9604 lsp::Position::new(1, 0),
9605 ),
9606 new_text: "\n".into(),
9607 },
9608 lsp::TextEdit {
9609 range: lsp::Range::new(
9610 lsp::Position::new(2, 0),
9611 lsp::Position::new(2, 0),
9612 ),
9613 new_text: "\n".into(),
9614 },
9615 ]))
9616 }
9617 }
9618 });
9619
9620 // After formatting the buffer, the trailing whitespace is stripped,
9621 // a newline is appended, and the edits provided by the language server
9622 // have been applied.
9623 format.await.unwrap();
9624 cx.assert_editor_state(
9625 &[
9626 "one", //
9627 "", //
9628 "twoˇ", //
9629 "", //
9630 "three", //
9631 "four", //
9632 "", //
9633 ]
9634 .join("\n"),
9635 );
9636
9637 // Undoing the formatting undoes the trailing whitespace removal, the
9638 // trailing newline, and the LSP edits.
9639 cx.update_buffer(|buffer, cx| buffer.undo(cx));
9640 cx.assert_editor_state(
9641 &[
9642 "one ", //
9643 "twoˇ", //
9644 "three ", //
9645 "four", //
9646 ]
9647 .join("\n"),
9648 );
9649}
9650
9651#[gpui::test]
9652async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
9653 cx: &mut TestAppContext,
9654) {
9655 init_test(cx, |_| {});
9656
9657 cx.update(|cx| {
9658 cx.update_global::<SettingsStore, _>(|settings, cx| {
9659 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9660 settings.auto_signature_help = Some(true);
9661 });
9662 });
9663 });
9664
9665 let mut cx = EditorLspTestContext::new_rust(
9666 lsp::ServerCapabilities {
9667 signature_help_provider: Some(lsp::SignatureHelpOptions {
9668 ..Default::default()
9669 }),
9670 ..Default::default()
9671 },
9672 cx,
9673 )
9674 .await;
9675
9676 let language = Language::new(
9677 LanguageConfig {
9678 name: "Rust".into(),
9679 brackets: BracketPairConfig {
9680 pairs: vec![
9681 BracketPair {
9682 start: "{".to_string(),
9683 end: "}".to_string(),
9684 close: true,
9685 surround: true,
9686 newline: true,
9687 },
9688 BracketPair {
9689 start: "(".to_string(),
9690 end: ")".to_string(),
9691 close: true,
9692 surround: true,
9693 newline: true,
9694 },
9695 BracketPair {
9696 start: "/*".to_string(),
9697 end: " */".to_string(),
9698 close: true,
9699 surround: true,
9700 newline: true,
9701 },
9702 BracketPair {
9703 start: "[".to_string(),
9704 end: "]".to_string(),
9705 close: false,
9706 surround: false,
9707 newline: true,
9708 },
9709 BracketPair {
9710 start: "\"".to_string(),
9711 end: "\"".to_string(),
9712 close: true,
9713 surround: true,
9714 newline: false,
9715 },
9716 BracketPair {
9717 start: "<".to_string(),
9718 end: ">".to_string(),
9719 close: false,
9720 surround: true,
9721 newline: true,
9722 },
9723 ],
9724 ..Default::default()
9725 },
9726 autoclose_before: "})]".to_string(),
9727 ..Default::default()
9728 },
9729 Some(tree_sitter_rust::LANGUAGE.into()),
9730 );
9731 let language = Arc::new(language);
9732
9733 cx.language_registry().add(language.clone());
9734 cx.update_buffer(|buffer, cx| {
9735 buffer.set_language(Some(language), cx);
9736 });
9737
9738 cx.set_state(
9739 &r#"
9740 fn main() {
9741 sampleˇ
9742 }
9743 "#
9744 .unindent(),
9745 );
9746
9747 cx.update_editor(|editor, window, cx| {
9748 editor.handle_input("(", window, cx);
9749 });
9750 cx.assert_editor_state(
9751 &"
9752 fn main() {
9753 sample(ˇ)
9754 }
9755 "
9756 .unindent(),
9757 );
9758
9759 let mocked_response = lsp::SignatureHelp {
9760 signatures: vec![lsp::SignatureInformation {
9761 label: "fn sample(param1: u8, param2: u8)".to_string(),
9762 documentation: None,
9763 parameters: Some(vec![
9764 lsp::ParameterInformation {
9765 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9766 documentation: None,
9767 },
9768 lsp::ParameterInformation {
9769 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9770 documentation: None,
9771 },
9772 ]),
9773 active_parameter: None,
9774 }],
9775 active_signature: Some(0),
9776 active_parameter: Some(0),
9777 };
9778 handle_signature_help_request(&mut cx, mocked_response).await;
9779
9780 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9781 .await;
9782
9783 cx.editor(|editor, _, _| {
9784 let signature_help_state = editor.signature_help_state.popover().cloned();
9785 assert_eq!(
9786 signature_help_state.unwrap().label,
9787 "param1: u8, param2: u8"
9788 );
9789 });
9790}
9791
9792#[gpui::test]
9793async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
9794 init_test(cx, |_| {});
9795
9796 cx.update(|cx| {
9797 cx.update_global::<SettingsStore, _>(|settings, cx| {
9798 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9799 settings.auto_signature_help = Some(false);
9800 settings.show_signature_help_after_edits = Some(false);
9801 });
9802 });
9803 });
9804
9805 let mut cx = EditorLspTestContext::new_rust(
9806 lsp::ServerCapabilities {
9807 signature_help_provider: Some(lsp::SignatureHelpOptions {
9808 ..Default::default()
9809 }),
9810 ..Default::default()
9811 },
9812 cx,
9813 )
9814 .await;
9815
9816 let language = Language::new(
9817 LanguageConfig {
9818 name: "Rust".into(),
9819 brackets: BracketPairConfig {
9820 pairs: vec![
9821 BracketPair {
9822 start: "{".to_string(),
9823 end: "}".to_string(),
9824 close: true,
9825 surround: true,
9826 newline: true,
9827 },
9828 BracketPair {
9829 start: "(".to_string(),
9830 end: ")".to_string(),
9831 close: true,
9832 surround: true,
9833 newline: true,
9834 },
9835 BracketPair {
9836 start: "/*".to_string(),
9837 end: " */".to_string(),
9838 close: true,
9839 surround: true,
9840 newline: true,
9841 },
9842 BracketPair {
9843 start: "[".to_string(),
9844 end: "]".to_string(),
9845 close: false,
9846 surround: false,
9847 newline: true,
9848 },
9849 BracketPair {
9850 start: "\"".to_string(),
9851 end: "\"".to_string(),
9852 close: true,
9853 surround: true,
9854 newline: false,
9855 },
9856 BracketPair {
9857 start: "<".to_string(),
9858 end: ">".to_string(),
9859 close: false,
9860 surround: true,
9861 newline: true,
9862 },
9863 ],
9864 ..Default::default()
9865 },
9866 autoclose_before: "})]".to_string(),
9867 ..Default::default()
9868 },
9869 Some(tree_sitter_rust::LANGUAGE.into()),
9870 );
9871 let language = Arc::new(language);
9872
9873 cx.language_registry().add(language.clone());
9874 cx.update_buffer(|buffer, cx| {
9875 buffer.set_language(Some(language), cx);
9876 });
9877
9878 // Ensure that signature_help is not called when no signature help is enabled.
9879 cx.set_state(
9880 &r#"
9881 fn main() {
9882 sampleˇ
9883 }
9884 "#
9885 .unindent(),
9886 );
9887 cx.update_editor(|editor, window, cx| {
9888 editor.handle_input("(", window, cx);
9889 });
9890 cx.assert_editor_state(
9891 &"
9892 fn main() {
9893 sample(ˇ)
9894 }
9895 "
9896 .unindent(),
9897 );
9898 cx.editor(|editor, _, _| {
9899 assert!(editor.signature_help_state.task().is_none());
9900 });
9901
9902 let mocked_response = lsp::SignatureHelp {
9903 signatures: vec![lsp::SignatureInformation {
9904 label: "fn sample(param1: u8, param2: u8)".to_string(),
9905 documentation: None,
9906 parameters: Some(vec![
9907 lsp::ParameterInformation {
9908 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9909 documentation: None,
9910 },
9911 lsp::ParameterInformation {
9912 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9913 documentation: None,
9914 },
9915 ]),
9916 active_parameter: None,
9917 }],
9918 active_signature: Some(0),
9919 active_parameter: Some(0),
9920 };
9921
9922 // Ensure that signature_help is called when enabled afte edits
9923 cx.update(|_, cx| {
9924 cx.update_global::<SettingsStore, _>(|settings, cx| {
9925 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9926 settings.auto_signature_help = Some(false);
9927 settings.show_signature_help_after_edits = Some(true);
9928 });
9929 });
9930 });
9931 cx.set_state(
9932 &r#"
9933 fn main() {
9934 sampleˇ
9935 }
9936 "#
9937 .unindent(),
9938 );
9939 cx.update_editor(|editor, window, cx| {
9940 editor.handle_input("(", window, cx);
9941 });
9942 cx.assert_editor_state(
9943 &"
9944 fn main() {
9945 sample(ˇ)
9946 }
9947 "
9948 .unindent(),
9949 );
9950 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9951 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9952 .await;
9953 cx.update_editor(|editor, _, _| {
9954 let signature_help_state = editor.signature_help_state.popover().cloned();
9955 assert!(signature_help_state.is_some());
9956 assert_eq!(
9957 signature_help_state.unwrap().label,
9958 "param1: u8, param2: u8"
9959 );
9960 editor.signature_help_state = SignatureHelpState::default();
9961 });
9962
9963 // Ensure that signature_help is called when auto signature help override is enabled
9964 cx.update(|_, cx| {
9965 cx.update_global::<SettingsStore, _>(|settings, cx| {
9966 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9967 settings.auto_signature_help = Some(true);
9968 settings.show_signature_help_after_edits = Some(false);
9969 });
9970 });
9971 });
9972 cx.set_state(
9973 &r#"
9974 fn main() {
9975 sampleˇ
9976 }
9977 "#
9978 .unindent(),
9979 );
9980 cx.update_editor(|editor, window, cx| {
9981 editor.handle_input("(", window, cx);
9982 });
9983 cx.assert_editor_state(
9984 &"
9985 fn main() {
9986 sample(ˇ)
9987 }
9988 "
9989 .unindent(),
9990 );
9991 handle_signature_help_request(&mut cx, mocked_response).await;
9992 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9993 .await;
9994 cx.editor(|editor, _, _| {
9995 let signature_help_state = editor.signature_help_state.popover().cloned();
9996 assert!(signature_help_state.is_some());
9997 assert_eq!(
9998 signature_help_state.unwrap().label,
9999 "param1: u8, param2: u8"
10000 );
10001 });
10002}
10003
10004#[gpui::test]
10005async fn test_signature_help(cx: &mut TestAppContext) {
10006 init_test(cx, |_| {});
10007 cx.update(|cx| {
10008 cx.update_global::<SettingsStore, _>(|settings, cx| {
10009 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10010 settings.auto_signature_help = Some(true);
10011 });
10012 });
10013 });
10014
10015 let mut cx = EditorLspTestContext::new_rust(
10016 lsp::ServerCapabilities {
10017 signature_help_provider: Some(lsp::SignatureHelpOptions {
10018 ..Default::default()
10019 }),
10020 ..Default::default()
10021 },
10022 cx,
10023 )
10024 .await;
10025
10026 // A test that directly calls `show_signature_help`
10027 cx.update_editor(|editor, window, cx| {
10028 editor.show_signature_help(&ShowSignatureHelp, window, cx);
10029 });
10030
10031 let mocked_response = lsp::SignatureHelp {
10032 signatures: vec![lsp::SignatureInformation {
10033 label: "fn sample(param1: u8, param2: u8)".to_string(),
10034 documentation: None,
10035 parameters: Some(vec![
10036 lsp::ParameterInformation {
10037 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10038 documentation: None,
10039 },
10040 lsp::ParameterInformation {
10041 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10042 documentation: None,
10043 },
10044 ]),
10045 active_parameter: None,
10046 }],
10047 active_signature: Some(0),
10048 active_parameter: Some(0),
10049 };
10050 handle_signature_help_request(&mut cx, mocked_response).await;
10051
10052 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10053 .await;
10054
10055 cx.editor(|editor, _, _| {
10056 let signature_help_state = editor.signature_help_state.popover().cloned();
10057 assert!(signature_help_state.is_some());
10058 assert_eq!(
10059 signature_help_state.unwrap().label,
10060 "param1: u8, param2: u8"
10061 );
10062 });
10063
10064 // When exiting outside from inside the brackets, `signature_help` is closed.
10065 cx.set_state(indoc! {"
10066 fn main() {
10067 sample(ˇ);
10068 }
10069
10070 fn sample(param1: u8, param2: u8) {}
10071 "});
10072
10073 cx.update_editor(|editor, window, cx| {
10074 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
10075 });
10076
10077 let mocked_response = lsp::SignatureHelp {
10078 signatures: Vec::new(),
10079 active_signature: None,
10080 active_parameter: None,
10081 };
10082 handle_signature_help_request(&mut cx, mocked_response).await;
10083
10084 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
10085 .await;
10086
10087 cx.editor(|editor, _, _| {
10088 assert!(!editor.signature_help_state.is_shown());
10089 });
10090
10091 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
10092 cx.set_state(indoc! {"
10093 fn main() {
10094 sample(ˇ);
10095 }
10096
10097 fn sample(param1: u8, param2: u8) {}
10098 "});
10099
10100 let mocked_response = lsp::SignatureHelp {
10101 signatures: vec![lsp::SignatureInformation {
10102 label: "fn sample(param1: u8, param2: u8)".to_string(),
10103 documentation: None,
10104 parameters: Some(vec![
10105 lsp::ParameterInformation {
10106 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10107 documentation: None,
10108 },
10109 lsp::ParameterInformation {
10110 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10111 documentation: None,
10112 },
10113 ]),
10114 active_parameter: None,
10115 }],
10116 active_signature: Some(0),
10117 active_parameter: Some(0),
10118 };
10119 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10120 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10121 .await;
10122 cx.editor(|editor, _, _| {
10123 assert!(editor.signature_help_state.is_shown());
10124 });
10125
10126 // Restore the popover with more parameter input
10127 cx.set_state(indoc! {"
10128 fn main() {
10129 sample(param1, param2ˇ);
10130 }
10131
10132 fn sample(param1: u8, param2: u8) {}
10133 "});
10134
10135 let mocked_response = lsp::SignatureHelp {
10136 signatures: vec![lsp::SignatureInformation {
10137 label: "fn sample(param1: u8, param2: u8)".to_string(),
10138 documentation: None,
10139 parameters: Some(vec![
10140 lsp::ParameterInformation {
10141 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10142 documentation: None,
10143 },
10144 lsp::ParameterInformation {
10145 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10146 documentation: None,
10147 },
10148 ]),
10149 active_parameter: None,
10150 }],
10151 active_signature: Some(0),
10152 active_parameter: Some(1),
10153 };
10154 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10155 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10156 .await;
10157
10158 // When selecting a range, the popover is gone.
10159 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
10160 cx.update_editor(|editor, window, cx| {
10161 editor.change_selections(None, window, cx, |s| {
10162 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
10163 })
10164 });
10165 cx.assert_editor_state(indoc! {"
10166 fn main() {
10167 sample(param1, «ˇparam2»);
10168 }
10169
10170 fn sample(param1: u8, param2: u8) {}
10171 "});
10172 cx.editor(|editor, _, _| {
10173 assert!(!editor.signature_help_state.is_shown());
10174 });
10175
10176 // When unselecting again, the popover is back if within the brackets.
10177 cx.update_editor(|editor, window, cx| {
10178 editor.change_selections(None, window, cx, |s| {
10179 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
10180 })
10181 });
10182 cx.assert_editor_state(indoc! {"
10183 fn main() {
10184 sample(param1, ˇparam2);
10185 }
10186
10187 fn sample(param1: u8, param2: u8) {}
10188 "});
10189 handle_signature_help_request(&mut cx, mocked_response).await;
10190 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10191 .await;
10192 cx.editor(|editor, _, _| {
10193 assert!(editor.signature_help_state.is_shown());
10194 });
10195
10196 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
10197 cx.update_editor(|editor, window, cx| {
10198 editor.change_selections(None, window, cx, |s| {
10199 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
10200 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
10201 })
10202 });
10203 cx.assert_editor_state(indoc! {"
10204 fn main() {
10205 sample(param1, ˇparam2);
10206 }
10207
10208 fn sample(param1: u8, param2: u8) {}
10209 "});
10210
10211 let mocked_response = lsp::SignatureHelp {
10212 signatures: vec![lsp::SignatureInformation {
10213 label: "fn sample(param1: u8, param2: u8)".to_string(),
10214 documentation: None,
10215 parameters: Some(vec![
10216 lsp::ParameterInformation {
10217 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10218 documentation: None,
10219 },
10220 lsp::ParameterInformation {
10221 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10222 documentation: None,
10223 },
10224 ]),
10225 active_parameter: None,
10226 }],
10227 active_signature: Some(0),
10228 active_parameter: Some(1),
10229 };
10230 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10231 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10232 .await;
10233 cx.update_editor(|editor, _, cx| {
10234 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
10235 });
10236 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
10237 .await;
10238 cx.update_editor(|editor, window, cx| {
10239 editor.change_selections(None, window, cx, |s| {
10240 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
10241 })
10242 });
10243 cx.assert_editor_state(indoc! {"
10244 fn main() {
10245 sample(param1, «ˇparam2»);
10246 }
10247
10248 fn sample(param1: u8, param2: u8) {}
10249 "});
10250 cx.update_editor(|editor, window, cx| {
10251 editor.change_selections(None, window, cx, |s| {
10252 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
10253 })
10254 });
10255 cx.assert_editor_state(indoc! {"
10256 fn main() {
10257 sample(param1, ˇparam2);
10258 }
10259
10260 fn sample(param1: u8, param2: u8) {}
10261 "});
10262 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
10263 .await;
10264}
10265
10266#[gpui::test]
10267async fn test_completion_mode(cx: &mut TestAppContext) {
10268 init_test(cx, |_| {});
10269 let mut cx = EditorLspTestContext::new_rust(
10270 lsp::ServerCapabilities {
10271 completion_provider: Some(lsp::CompletionOptions {
10272 resolve_provider: Some(true),
10273 ..Default::default()
10274 }),
10275 ..Default::default()
10276 },
10277 cx,
10278 )
10279 .await;
10280
10281 struct Run {
10282 run_description: &'static str,
10283 initial_state: String,
10284 buffer_marked_text: String,
10285 completion_text: &'static str,
10286 expected_with_insert_mode: String,
10287 expected_with_replace_mode: String,
10288 expected_with_replace_subsequence_mode: String,
10289 expected_with_replace_suffix_mode: String,
10290 }
10291
10292 let runs = [
10293 Run {
10294 run_description: "Start of word matches completion text",
10295 initial_state: "before ediˇ after".into(),
10296 buffer_marked_text: "before <edi|> after".into(),
10297 completion_text: "editor",
10298 expected_with_insert_mode: "before editorˇ after".into(),
10299 expected_with_replace_mode: "before editorˇ after".into(),
10300 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10301 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10302 },
10303 Run {
10304 run_description: "Accept same text at the middle of the word",
10305 initial_state: "before ediˇtor after".into(),
10306 buffer_marked_text: "before <edi|tor> after".into(),
10307 completion_text: "editor",
10308 expected_with_insert_mode: "before editorˇtor after".into(),
10309 expected_with_replace_mode: "before editorˇ after".into(),
10310 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10311 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10312 },
10313 Run {
10314 run_description: "End of word matches completion text -- cursor at end",
10315 initial_state: "before torˇ after".into(),
10316 buffer_marked_text: "before <tor|> after".into(),
10317 completion_text: "editor",
10318 expected_with_insert_mode: "before editorˇ after".into(),
10319 expected_with_replace_mode: "before editorˇ after".into(),
10320 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10321 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10322 },
10323 Run {
10324 run_description: "End of word matches completion text -- cursor at start",
10325 initial_state: "before ˇtor after".into(),
10326 buffer_marked_text: "before <|tor> after".into(),
10327 completion_text: "editor",
10328 expected_with_insert_mode: "before editorˇtor after".into(),
10329 expected_with_replace_mode: "before editorˇ after".into(),
10330 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10331 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10332 },
10333 Run {
10334 run_description: "Prepend text containing whitespace",
10335 initial_state: "pˇfield: bool".into(),
10336 buffer_marked_text: "<p|field>: bool".into(),
10337 completion_text: "pub ",
10338 expected_with_insert_mode: "pub ˇfield: bool".into(),
10339 expected_with_replace_mode: "pub ˇ: bool".into(),
10340 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
10341 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
10342 },
10343 Run {
10344 run_description: "Add element to start of list",
10345 initial_state: "[element_ˇelement_2]".into(),
10346 buffer_marked_text: "[<element_|element_2>]".into(),
10347 completion_text: "element_1",
10348 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
10349 expected_with_replace_mode: "[element_1ˇ]".into(),
10350 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
10351 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
10352 },
10353 Run {
10354 run_description: "Add element to start of list -- first and second elements are equal",
10355 initial_state: "[elˇelement]".into(),
10356 buffer_marked_text: "[<el|element>]".into(),
10357 completion_text: "element",
10358 expected_with_insert_mode: "[elementˇelement]".into(),
10359 expected_with_replace_mode: "[elementˇ]".into(),
10360 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
10361 expected_with_replace_suffix_mode: "[elementˇ]".into(),
10362 },
10363 Run {
10364 run_description: "Ends with matching suffix",
10365 initial_state: "SubˇError".into(),
10366 buffer_marked_text: "<Sub|Error>".into(),
10367 completion_text: "SubscriptionError",
10368 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
10369 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10370 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10371 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
10372 },
10373 Run {
10374 run_description: "Suffix is a subsequence -- contiguous",
10375 initial_state: "SubˇErr".into(),
10376 buffer_marked_text: "<Sub|Err>".into(),
10377 completion_text: "SubscriptionError",
10378 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
10379 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10380 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10381 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
10382 },
10383 Run {
10384 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
10385 initial_state: "Suˇscrirr".into(),
10386 buffer_marked_text: "<Su|scrirr>".into(),
10387 completion_text: "SubscriptionError",
10388 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
10389 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10390 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10391 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
10392 },
10393 Run {
10394 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
10395 initial_state: "foo(indˇix)".into(),
10396 buffer_marked_text: "foo(<ind|ix>)".into(),
10397 completion_text: "node_index",
10398 expected_with_insert_mode: "foo(node_indexˇix)".into(),
10399 expected_with_replace_mode: "foo(node_indexˇ)".into(),
10400 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
10401 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
10402 },
10403 ];
10404
10405 for run in runs {
10406 let run_variations = [
10407 (LspInsertMode::Insert, run.expected_with_insert_mode),
10408 (LspInsertMode::Replace, run.expected_with_replace_mode),
10409 (
10410 LspInsertMode::ReplaceSubsequence,
10411 run.expected_with_replace_subsequence_mode,
10412 ),
10413 (
10414 LspInsertMode::ReplaceSuffix,
10415 run.expected_with_replace_suffix_mode,
10416 ),
10417 ];
10418
10419 for (lsp_insert_mode, expected_text) in run_variations {
10420 eprintln!(
10421 "run = {:?}, mode = {lsp_insert_mode:.?}",
10422 run.run_description,
10423 );
10424
10425 update_test_language_settings(&mut cx, |settings| {
10426 settings.defaults.completions = Some(CompletionSettings {
10427 lsp_insert_mode,
10428 words: WordsCompletionMode::Disabled,
10429 lsp: true,
10430 lsp_fetch_timeout_ms: 0,
10431 });
10432 });
10433
10434 cx.set_state(&run.initial_state);
10435 cx.update_editor(|editor, window, cx| {
10436 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10437 });
10438
10439 let counter = Arc::new(AtomicUsize::new(0));
10440 handle_completion_request_with_insert_and_replace(
10441 &mut cx,
10442 &run.buffer_marked_text,
10443 vec![run.completion_text],
10444 counter.clone(),
10445 )
10446 .await;
10447 cx.condition(|editor, _| editor.context_menu_visible())
10448 .await;
10449 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
10450
10451 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10452 editor
10453 .confirm_completion(&ConfirmCompletion::default(), window, cx)
10454 .unwrap()
10455 });
10456 cx.assert_editor_state(&expected_text);
10457 handle_resolve_completion_request(&mut cx, None).await;
10458 apply_additional_edits.await.unwrap();
10459 }
10460 }
10461}
10462
10463#[gpui::test]
10464async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
10465 init_test(cx, |_| {});
10466 let mut cx = EditorLspTestContext::new_rust(
10467 lsp::ServerCapabilities {
10468 completion_provider: Some(lsp::CompletionOptions {
10469 resolve_provider: Some(true),
10470 ..Default::default()
10471 }),
10472 ..Default::default()
10473 },
10474 cx,
10475 )
10476 .await;
10477
10478 let initial_state = "SubˇError";
10479 let buffer_marked_text = "<Sub|Error>";
10480 let completion_text = "SubscriptionError";
10481 let expected_with_insert_mode = "SubscriptionErrorˇError";
10482 let expected_with_replace_mode = "SubscriptionErrorˇ";
10483
10484 update_test_language_settings(&mut cx, |settings| {
10485 settings.defaults.completions = Some(CompletionSettings {
10486 words: WordsCompletionMode::Disabled,
10487 // set the opposite here to ensure that the action is overriding the default behavior
10488 lsp_insert_mode: LspInsertMode::Insert,
10489 lsp: true,
10490 lsp_fetch_timeout_ms: 0,
10491 });
10492 });
10493
10494 cx.set_state(initial_state);
10495 cx.update_editor(|editor, window, cx| {
10496 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10497 });
10498
10499 let counter = Arc::new(AtomicUsize::new(0));
10500 handle_completion_request_with_insert_and_replace(
10501 &mut cx,
10502 &buffer_marked_text,
10503 vec![completion_text],
10504 counter.clone(),
10505 )
10506 .await;
10507 cx.condition(|editor, _| editor.context_menu_visible())
10508 .await;
10509 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
10510
10511 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10512 editor
10513 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10514 .unwrap()
10515 });
10516 cx.assert_editor_state(&expected_with_replace_mode);
10517 handle_resolve_completion_request(&mut cx, None).await;
10518 apply_additional_edits.await.unwrap();
10519
10520 update_test_language_settings(&mut cx, |settings| {
10521 settings.defaults.completions = Some(CompletionSettings {
10522 words: WordsCompletionMode::Disabled,
10523 // set the opposite here to ensure that the action is overriding the default behavior
10524 lsp_insert_mode: LspInsertMode::Replace,
10525 lsp: true,
10526 lsp_fetch_timeout_ms: 0,
10527 });
10528 });
10529
10530 cx.set_state(initial_state);
10531 cx.update_editor(|editor, window, cx| {
10532 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10533 });
10534 handle_completion_request_with_insert_and_replace(
10535 &mut cx,
10536 &buffer_marked_text,
10537 vec![completion_text],
10538 counter.clone(),
10539 )
10540 .await;
10541 cx.condition(|editor, _| editor.context_menu_visible())
10542 .await;
10543 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
10544
10545 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10546 editor
10547 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
10548 .unwrap()
10549 });
10550 cx.assert_editor_state(&expected_with_insert_mode);
10551 handle_resolve_completion_request(&mut cx, None).await;
10552 apply_additional_edits.await.unwrap();
10553}
10554
10555#[gpui::test]
10556async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
10557 init_test(cx, |_| {});
10558 let mut cx = EditorLspTestContext::new_rust(
10559 lsp::ServerCapabilities {
10560 completion_provider: Some(lsp::CompletionOptions {
10561 resolve_provider: Some(true),
10562 ..Default::default()
10563 }),
10564 ..Default::default()
10565 },
10566 cx,
10567 )
10568 .await;
10569
10570 // scenario: surrounding text matches completion text
10571 let completion_text = "to_offset";
10572 let initial_state = indoc! {"
10573 1. buf.to_offˇsuffix
10574 2. buf.to_offˇsuf
10575 3. buf.to_offˇfix
10576 4. buf.to_offˇ
10577 5. into_offˇensive
10578 6. ˇsuffix
10579 7. let ˇ //
10580 8. aaˇzz
10581 9. buf.to_off«zzzzzˇ»suffix
10582 10. buf.«ˇzzzzz»suffix
10583 11. to_off«ˇzzzzz»
10584
10585 buf.to_offˇsuffix // newest cursor
10586 "};
10587 let completion_marked_buffer = indoc! {"
10588 1. buf.to_offsuffix
10589 2. buf.to_offsuf
10590 3. buf.to_offfix
10591 4. buf.to_off
10592 5. into_offensive
10593 6. suffix
10594 7. let //
10595 8. aazz
10596 9. buf.to_offzzzzzsuffix
10597 10. buf.zzzzzsuffix
10598 11. to_offzzzzz
10599
10600 buf.<to_off|suffix> // newest cursor
10601 "};
10602 let expected = indoc! {"
10603 1. buf.to_offsetˇ
10604 2. buf.to_offsetˇsuf
10605 3. buf.to_offsetˇfix
10606 4. buf.to_offsetˇ
10607 5. into_offsetˇensive
10608 6. to_offsetˇsuffix
10609 7. let to_offsetˇ //
10610 8. aato_offsetˇzz
10611 9. buf.to_offsetˇ
10612 10. buf.to_offsetˇsuffix
10613 11. to_offsetˇ
10614
10615 buf.to_offsetˇ // newest cursor
10616 "};
10617 cx.set_state(initial_state);
10618 cx.update_editor(|editor, window, cx| {
10619 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10620 });
10621 handle_completion_request_with_insert_and_replace(
10622 &mut cx,
10623 completion_marked_buffer,
10624 vec![completion_text],
10625 Arc::new(AtomicUsize::new(0)),
10626 )
10627 .await;
10628 cx.condition(|editor, _| editor.context_menu_visible())
10629 .await;
10630 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10631 editor
10632 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10633 .unwrap()
10634 });
10635 cx.assert_editor_state(expected);
10636 handle_resolve_completion_request(&mut cx, None).await;
10637 apply_additional_edits.await.unwrap();
10638
10639 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
10640 let completion_text = "foo_and_bar";
10641 let initial_state = indoc! {"
10642 1. ooanbˇ
10643 2. zooanbˇ
10644 3. ooanbˇz
10645 4. zooanbˇz
10646 5. ooanˇ
10647 6. oanbˇ
10648
10649 ooanbˇ
10650 "};
10651 let completion_marked_buffer = indoc! {"
10652 1. ooanb
10653 2. zooanb
10654 3. ooanbz
10655 4. zooanbz
10656 5. ooan
10657 6. oanb
10658
10659 <ooanb|>
10660 "};
10661 let expected = indoc! {"
10662 1. foo_and_barˇ
10663 2. zfoo_and_barˇ
10664 3. foo_and_barˇz
10665 4. zfoo_and_barˇz
10666 5. ooanfoo_and_barˇ
10667 6. oanbfoo_and_barˇ
10668
10669 foo_and_barˇ
10670 "};
10671 cx.set_state(initial_state);
10672 cx.update_editor(|editor, window, cx| {
10673 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10674 });
10675 handle_completion_request_with_insert_and_replace(
10676 &mut cx,
10677 completion_marked_buffer,
10678 vec![completion_text],
10679 Arc::new(AtomicUsize::new(0)),
10680 )
10681 .await;
10682 cx.condition(|editor, _| editor.context_menu_visible())
10683 .await;
10684 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10685 editor
10686 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10687 .unwrap()
10688 });
10689 cx.assert_editor_state(expected);
10690 handle_resolve_completion_request(&mut cx, None).await;
10691 apply_additional_edits.await.unwrap();
10692
10693 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
10694 // (expects the same as if it was inserted at the end)
10695 let completion_text = "foo_and_bar";
10696 let initial_state = indoc! {"
10697 1. ooˇanb
10698 2. zooˇanb
10699 3. ooˇanbz
10700 4. zooˇanbz
10701
10702 ooˇanb
10703 "};
10704 let completion_marked_buffer = indoc! {"
10705 1. ooanb
10706 2. zooanb
10707 3. ooanbz
10708 4. zooanbz
10709
10710 <oo|anb>
10711 "};
10712 let expected = indoc! {"
10713 1. foo_and_barˇ
10714 2. zfoo_and_barˇ
10715 3. foo_and_barˇz
10716 4. zfoo_and_barˇz
10717
10718 foo_and_barˇ
10719 "};
10720 cx.set_state(initial_state);
10721 cx.update_editor(|editor, window, cx| {
10722 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10723 });
10724 handle_completion_request_with_insert_and_replace(
10725 &mut cx,
10726 completion_marked_buffer,
10727 vec![completion_text],
10728 Arc::new(AtomicUsize::new(0)),
10729 )
10730 .await;
10731 cx.condition(|editor, _| editor.context_menu_visible())
10732 .await;
10733 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10734 editor
10735 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10736 .unwrap()
10737 });
10738 cx.assert_editor_state(expected);
10739 handle_resolve_completion_request(&mut cx, None).await;
10740 apply_additional_edits.await.unwrap();
10741}
10742
10743// This used to crash
10744#[gpui::test]
10745async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
10746 init_test(cx, |_| {});
10747
10748 let buffer_text = indoc! {"
10749 fn main() {
10750 10.satu;
10751
10752 //
10753 // separate cursors so they open in different excerpts (manually reproducible)
10754 //
10755
10756 10.satu20;
10757 }
10758 "};
10759 let multibuffer_text_with_selections = indoc! {"
10760 fn main() {
10761 10.satuˇ;
10762
10763 //
10764
10765 //
10766
10767 10.satuˇ20;
10768 }
10769 "};
10770 let expected_multibuffer = indoc! {"
10771 fn main() {
10772 10.saturating_sub()ˇ;
10773
10774 //
10775
10776 //
10777
10778 10.saturating_sub()ˇ;
10779 }
10780 "};
10781
10782 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
10783 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
10784
10785 let fs = FakeFs::new(cx.executor());
10786 fs.insert_tree(
10787 path!("/a"),
10788 json!({
10789 "main.rs": buffer_text,
10790 }),
10791 )
10792 .await;
10793
10794 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
10795 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10796 language_registry.add(rust_lang());
10797 let mut fake_servers = language_registry.register_fake_lsp(
10798 "Rust",
10799 FakeLspAdapter {
10800 capabilities: lsp::ServerCapabilities {
10801 completion_provider: Some(lsp::CompletionOptions {
10802 resolve_provider: None,
10803 ..lsp::CompletionOptions::default()
10804 }),
10805 ..lsp::ServerCapabilities::default()
10806 },
10807 ..FakeLspAdapter::default()
10808 },
10809 );
10810 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
10811 let cx = &mut VisualTestContext::from_window(*workspace, cx);
10812 let buffer = project
10813 .update(cx, |project, cx| {
10814 project.open_local_buffer(path!("/a/main.rs"), cx)
10815 })
10816 .await
10817 .unwrap();
10818
10819 let multi_buffer = cx.new(|cx| {
10820 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
10821 multi_buffer.push_excerpts(
10822 buffer.clone(),
10823 [ExcerptRange::new(0..first_excerpt_end)],
10824 cx,
10825 );
10826 multi_buffer.push_excerpts(
10827 buffer.clone(),
10828 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
10829 cx,
10830 );
10831 multi_buffer
10832 });
10833
10834 let editor = workspace
10835 .update(cx, |_, window, cx| {
10836 cx.new(|cx| {
10837 Editor::new(
10838 EditorMode::Full {
10839 scale_ui_elements_with_buffer_font_size: false,
10840 show_active_line_background: false,
10841 sized_by_content: false,
10842 },
10843 multi_buffer.clone(),
10844 Some(project.clone()),
10845 window,
10846 cx,
10847 )
10848 })
10849 })
10850 .unwrap();
10851
10852 let pane = workspace
10853 .update(cx, |workspace, _, _| workspace.active_pane().clone())
10854 .unwrap();
10855 pane.update_in(cx, |pane, window, cx| {
10856 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
10857 });
10858
10859 let fake_server = fake_servers.next().await.unwrap();
10860
10861 editor.update_in(cx, |editor, window, cx| {
10862 editor.change_selections(None, window, cx, |s| {
10863 s.select_ranges([
10864 Point::new(1, 11)..Point::new(1, 11),
10865 Point::new(7, 11)..Point::new(7, 11),
10866 ])
10867 });
10868
10869 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
10870 });
10871
10872 editor.update_in(cx, |editor, window, cx| {
10873 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10874 });
10875
10876 fake_server
10877 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
10878 let completion_item = lsp::CompletionItem {
10879 label: "saturating_sub()".into(),
10880 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
10881 lsp::InsertReplaceEdit {
10882 new_text: "saturating_sub()".to_owned(),
10883 insert: lsp::Range::new(
10884 lsp::Position::new(7, 7),
10885 lsp::Position::new(7, 11),
10886 ),
10887 replace: lsp::Range::new(
10888 lsp::Position::new(7, 7),
10889 lsp::Position::new(7, 13),
10890 ),
10891 },
10892 )),
10893 ..lsp::CompletionItem::default()
10894 };
10895
10896 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
10897 })
10898 .next()
10899 .await
10900 .unwrap();
10901
10902 cx.condition(&editor, |editor, _| editor.context_menu_visible())
10903 .await;
10904
10905 editor
10906 .update_in(cx, |editor, window, cx| {
10907 editor
10908 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10909 .unwrap()
10910 })
10911 .await
10912 .unwrap();
10913
10914 editor.update(cx, |editor, cx| {
10915 assert_text_with_selections(editor, expected_multibuffer, cx);
10916 })
10917}
10918
10919#[gpui::test]
10920async fn test_completion(cx: &mut TestAppContext) {
10921 init_test(cx, |_| {});
10922
10923 let mut cx = EditorLspTestContext::new_rust(
10924 lsp::ServerCapabilities {
10925 completion_provider: Some(lsp::CompletionOptions {
10926 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
10927 resolve_provider: Some(true),
10928 ..Default::default()
10929 }),
10930 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
10931 ..Default::default()
10932 },
10933 cx,
10934 )
10935 .await;
10936 let counter = Arc::new(AtomicUsize::new(0));
10937
10938 cx.set_state(indoc! {"
10939 oneˇ
10940 two
10941 three
10942 "});
10943 cx.simulate_keystroke(".");
10944 handle_completion_request(
10945 &mut cx,
10946 indoc! {"
10947 one.|<>
10948 two
10949 three
10950 "},
10951 vec!["first_completion", "second_completion"],
10952 counter.clone(),
10953 )
10954 .await;
10955 cx.condition(|editor, _| editor.context_menu_visible())
10956 .await;
10957 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
10958
10959 let _handler = handle_signature_help_request(
10960 &mut cx,
10961 lsp::SignatureHelp {
10962 signatures: vec![lsp::SignatureInformation {
10963 label: "test signature".to_string(),
10964 documentation: None,
10965 parameters: Some(vec![lsp::ParameterInformation {
10966 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
10967 documentation: None,
10968 }]),
10969 active_parameter: None,
10970 }],
10971 active_signature: None,
10972 active_parameter: None,
10973 },
10974 );
10975 cx.update_editor(|editor, window, cx| {
10976 assert!(
10977 !editor.signature_help_state.is_shown(),
10978 "No signature help was called for"
10979 );
10980 editor.show_signature_help(&ShowSignatureHelp, window, cx);
10981 });
10982 cx.run_until_parked();
10983 cx.update_editor(|editor, _, _| {
10984 assert!(
10985 !editor.signature_help_state.is_shown(),
10986 "No signature help should be shown when completions menu is open"
10987 );
10988 });
10989
10990 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10991 editor.context_menu_next(&Default::default(), window, cx);
10992 editor
10993 .confirm_completion(&ConfirmCompletion::default(), window, cx)
10994 .unwrap()
10995 });
10996 cx.assert_editor_state(indoc! {"
10997 one.second_completionˇ
10998 two
10999 three
11000 "});
11001
11002 handle_resolve_completion_request(
11003 &mut cx,
11004 Some(vec![
11005 (
11006 //This overlaps with the primary completion edit which is
11007 //misbehavior from the LSP spec, test that we filter it out
11008 indoc! {"
11009 one.second_ˇcompletion
11010 two
11011 threeˇ
11012 "},
11013 "overlapping additional edit",
11014 ),
11015 (
11016 indoc! {"
11017 one.second_completion
11018 two
11019 threeˇ
11020 "},
11021 "\nadditional edit",
11022 ),
11023 ]),
11024 )
11025 .await;
11026 apply_additional_edits.await.unwrap();
11027 cx.assert_editor_state(indoc! {"
11028 one.second_completionˇ
11029 two
11030 three
11031 additional edit
11032 "});
11033
11034 cx.set_state(indoc! {"
11035 one.second_completion
11036 twoˇ
11037 threeˇ
11038 additional edit
11039 "});
11040 cx.simulate_keystroke(" ");
11041 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11042 cx.simulate_keystroke("s");
11043 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11044
11045 cx.assert_editor_state(indoc! {"
11046 one.second_completion
11047 two sˇ
11048 three sˇ
11049 additional edit
11050 "});
11051 handle_completion_request(
11052 &mut cx,
11053 indoc! {"
11054 one.second_completion
11055 two s
11056 three <s|>
11057 additional edit
11058 "},
11059 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
11060 counter.clone(),
11061 )
11062 .await;
11063 cx.condition(|editor, _| editor.context_menu_visible())
11064 .await;
11065 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
11066
11067 cx.simulate_keystroke("i");
11068
11069 handle_completion_request(
11070 &mut cx,
11071 indoc! {"
11072 one.second_completion
11073 two si
11074 three <si|>
11075 additional edit
11076 "},
11077 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
11078 counter.clone(),
11079 )
11080 .await;
11081 cx.condition(|editor, _| editor.context_menu_visible())
11082 .await;
11083 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
11084
11085 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11086 editor
11087 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11088 .unwrap()
11089 });
11090 cx.assert_editor_state(indoc! {"
11091 one.second_completion
11092 two sixth_completionˇ
11093 three sixth_completionˇ
11094 additional edit
11095 "});
11096
11097 apply_additional_edits.await.unwrap();
11098
11099 update_test_language_settings(&mut cx, |settings| {
11100 settings.defaults.show_completions_on_input = Some(false);
11101 });
11102 cx.set_state("editorˇ");
11103 cx.simulate_keystroke(".");
11104 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11105 cx.simulate_keystrokes("c l o");
11106 cx.assert_editor_state("editor.cloˇ");
11107 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11108 cx.update_editor(|editor, window, cx| {
11109 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11110 });
11111 handle_completion_request(
11112 &mut cx,
11113 "editor.<clo|>",
11114 vec!["close", "clobber"],
11115 counter.clone(),
11116 )
11117 .await;
11118 cx.condition(|editor, _| editor.context_menu_visible())
11119 .await;
11120 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
11121
11122 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11123 editor
11124 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11125 .unwrap()
11126 });
11127 cx.assert_editor_state("editor.closeˇ");
11128 handle_resolve_completion_request(&mut cx, None).await;
11129 apply_additional_edits.await.unwrap();
11130}
11131
11132#[gpui::test]
11133async fn test_word_completion(cx: &mut TestAppContext) {
11134 let lsp_fetch_timeout_ms = 10;
11135 init_test(cx, |language_settings| {
11136 language_settings.defaults.completions = Some(CompletionSettings {
11137 words: WordsCompletionMode::Fallback,
11138 lsp: true,
11139 lsp_fetch_timeout_ms: 10,
11140 lsp_insert_mode: LspInsertMode::Insert,
11141 });
11142 });
11143
11144 let mut cx = EditorLspTestContext::new_rust(
11145 lsp::ServerCapabilities {
11146 completion_provider: Some(lsp::CompletionOptions {
11147 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11148 ..lsp::CompletionOptions::default()
11149 }),
11150 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11151 ..lsp::ServerCapabilities::default()
11152 },
11153 cx,
11154 )
11155 .await;
11156
11157 let throttle_completions = Arc::new(AtomicBool::new(false));
11158
11159 let lsp_throttle_completions = throttle_completions.clone();
11160 let _completion_requests_handler =
11161 cx.lsp
11162 .server
11163 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
11164 let lsp_throttle_completions = lsp_throttle_completions.clone();
11165 let cx = cx.clone();
11166 async move {
11167 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
11168 cx.background_executor()
11169 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
11170 .await;
11171 }
11172 Ok(Some(lsp::CompletionResponse::Array(vec![
11173 lsp::CompletionItem {
11174 label: "first".into(),
11175 ..lsp::CompletionItem::default()
11176 },
11177 lsp::CompletionItem {
11178 label: "last".into(),
11179 ..lsp::CompletionItem::default()
11180 },
11181 ])))
11182 }
11183 });
11184
11185 cx.set_state(indoc! {"
11186 oneˇ
11187 two
11188 three
11189 "});
11190 cx.simulate_keystroke(".");
11191 cx.executor().run_until_parked();
11192 cx.condition(|editor, _| editor.context_menu_visible())
11193 .await;
11194 cx.update_editor(|editor, window, cx| {
11195 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11196 {
11197 assert_eq!(
11198 completion_menu_entries(&menu),
11199 &["first", "last"],
11200 "When LSP server is fast to reply, no fallback word completions are used"
11201 );
11202 } else {
11203 panic!("expected completion menu to be open");
11204 }
11205 editor.cancel(&Cancel, window, cx);
11206 });
11207 cx.executor().run_until_parked();
11208 cx.condition(|editor, _| !editor.context_menu_visible())
11209 .await;
11210
11211 throttle_completions.store(true, atomic::Ordering::Release);
11212 cx.simulate_keystroke(".");
11213 cx.executor()
11214 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
11215 cx.executor().run_until_parked();
11216 cx.condition(|editor, _| editor.context_menu_visible())
11217 .await;
11218 cx.update_editor(|editor, _, _| {
11219 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11220 {
11221 assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
11222 "When LSP server is slow, document words can be shown instead, if configured accordingly");
11223 } else {
11224 panic!("expected completion menu to be open");
11225 }
11226 });
11227}
11228
11229#[gpui::test]
11230async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
11231 init_test(cx, |language_settings| {
11232 language_settings.defaults.completions = Some(CompletionSettings {
11233 words: WordsCompletionMode::Enabled,
11234 lsp: true,
11235 lsp_fetch_timeout_ms: 0,
11236 lsp_insert_mode: LspInsertMode::Insert,
11237 });
11238 });
11239
11240 let mut cx = EditorLspTestContext::new_rust(
11241 lsp::ServerCapabilities {
11242 completion_provider: Some(lsp::CompletionOptions {
11243 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11244 ..lsp::CompletionOptions::default()
11245 }),
11246 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11247 ..lsp::ServerCapabilities::default()
11248 },
11249 cx,
11250 )
11251 .await;
11252
11253 let _completion_requests_handler =
11254 cx.lsp
11255 .server
11256 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
11257 Ok(Some(lsp::CompletionResponse::Array(vec![
11258 lsp::CompletionItem {
11259 label: "first".into(),
11260 ..lsp::CompletionItem::default()
11261 },
11262 lsp::CompletionItem {
11263 label: "last".into(),
11264 ..lsp::CompletionItem::default()
11265 },
11266 ])))
11267 });
11268
11269 cx.set_state(indoc! {"ˇ
11270 first
11271 last
11272 second
11273 "});
11274 cx.simulate_keystroke(".");
11275 cx.executor().run_until_parked();
11276 cx.condition(|editor, _| editor.context_menu_visible())
11277 .await;
11278 cx.update_editor(|editor, _, _| {
11279 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11280 {
11281 assert_eq!(
11282 completion_menu_entries(&menu),
11283 &["first", "last", "second"],
11284 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
11285 );
11286 } else {
11287 panic!("expected completion menu to be open");
11288 }
11289 });
11290}
11291
11292#[gpui::test]
11293async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
11294 init_test(cx, |language_settings| {
11295 language_settings.defaults.completions = Some(CompletionSettings {
11296 words: WordsCompletionMode::Disabled,
11297 lsp: true,
11298 lsp_fetch_timeout_ms: 0,
11299 lsp_insert_mode: LspInsertMode::Insert,
11300 });
11301 });
11302
11303 let mut cx = EditorLspTestContext::new_rust(
11304 lsp::ServerCapabilities {
11305 completion_provider: Some(lsp::CompletionOptions {
11306 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11307 ..lsp::CompletionOptions::default()
11308 }),
11309 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11310 ..lsp::ServerCapabilities::default()
11311 },
11312 cx,
11313 )
11314 .await;
11315
11316 let _completion_requests_handler =
11317 cx.lsp
11318 .server
11319 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
11320 panic!("LSP completions should not be queried when dealing with word completions")
11321 });
11322
11323 cx.set_state(indoc! {"ˇ
11324 first
11325 last
11326 second
11327 "});
11328 cx.update_editor(|editor, window, cx| {
11329 editor.show_word_completions(&ShowWordCompletions, window, cx);
11330 });
11331 cx.executor().run_until_parked();
11332 cx.condition(|editor, _| editor.context_menu_visible())
11333 .await;
11334 cx.update_editor(|editor, _, _| {
11335 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11336 {
11337 assert_eq!(
11338 completion_menu_entries(&menu),
11339 &["first", "last", "second"],
11340 "`ShowWordCompletions` action should show word completions"
11341 );
11342 } else {
11343 panic!("expected completion menu to be open");
11344 }
11345 });
11346
11347 cx.simulate_keystroke("l");
11348 cx.executor().run_until_parked();
11349 cx.condition(|editor, _| editor.context_menu_visible())
11350 .await;
11351 cx.update_editor(|editor, _, _| {
11352 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11353 {
11354 assert_eq!(
11355 completion_menu_entries(&menu),
11356 &["last"],
11357 "After showing word completions, further editing should filter them and not query the LSP"
11358 );
11359 } else {
11360 panic!("expected completion menu to be open");
11361 }
11362 });
11363}
11364
11365#[gpui::test]
11366async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
11367 init_test(cx, |language_settings| {
11368 language_settings.defaults.completions = Some(CompletionSettings {
11369 words: WordsCompletionMode::Fallback,
11370 lsp: false,
11371 lsp_fetch_timeout_ms: 0,
11372 lsp_insert_mode: LspInsertMode::Insert,
11373 });
11374 });
11375
11376 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
11377
11378 cx.set_state(indoc! {"ˇ
11379 0_usize
11380 let
11381 33
11382 4.5f32
11383 "});
11384 cx.update_editor(|editor, window, cx| {
11385 editor.show_completions(&ShowCompletions::default(), window, cx);
11386 });
11387 cx.executor().run_until_parked();
11388 cx.condition(|editor, _| editor.context_menu_visible())
11389 .await;
11390 cx.update_editor(|editor, window, cx| {
11391 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11392 {
11393 assert_eq!(
11394 completion_menu_entries(&menu),
11395 &["let"],
11396 "With no digits in the completion query, no digits should be in the word completions"
11397 );
11398 } else {
11399 panic!("expected completion menu to be open");
11400 }
11401 editor.cancel(&Cancel, window, cx);
11402 });
11403
11404 cx.set_state(indoc! {"3ˇ
11405 0_usize
11406 let
11407 3
11408 33.35f32
11409 "});
11410 cx.update_editor(|editor, window, cx| {
11411 editor.show_completions(&ShowCompletions::default(), window, cx);
11412 });
11413 cx.executor().run_until_parked();
11414 cx.condition(|editor, _| editor.context_menu_visible())
11415 .await;
11416 cx.update_editor(|editor, _, _| {
11417 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11418 {
11419 assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
11420 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
11421 } else {
11422 panic!("expected completion menu to be open");
11423 }
11424 });
11425}
11426
11427fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
11428 let position = || lsp::Position {
11429 line: params.text_document_position.position.line,
11430 character: params.text_document_position.position.character,
11431 };
11432 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11433 range: lsp::Range {
11434 start: position(),
11435 end: position(),
11436 },
11437 new_text: text.to_string(),
11438 }))
11439}
11440
11441#[gpui::test]
11442async fn test_multiline_completion(cx: &mut TestAppContext) {
11443 init_test(cx, |_| {});
11444
11445 let fs = FakeFs::new(cx.executor());
11446 fs.insert_tree(
11447 path!("/a"),
11448 json!({
11449 "main.ts": "a",
11450 }),
11451 )
11452 .await;
11453
11454 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11455 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11456 let typescript_language = Arc::new(Language::new(
11457 LanguageConfig {
11458 name: "TypeScript".into(),
11459 matcher: LanguageMatcher {
11460 path_suffixes: vec!["ts".to_string()],
11461 ..LanguageMatcher::default()
11462 },
11463 line_comments: vec!["// ".into()],
11464 ..LanguageConfig::default()
11465 },
11466 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
11467 ));
11468 language_registry.add(typescript_language.clone());
11469 let mut fake_servers = language_registry.register_fake_lsp(
11470 "TypeScript",
11471 FakeLspAdapter {
11472 capabilities: lsp::ServerCapabilities {
11473 completion_provider: Some(lsp::CompletionOptions {
11474 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11475 ..lsp::CompletionOptions::default()
11476 }),
11477 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11478 ..lsp::ServerCapabilities::default()
11479 },
11480 // Emulate vtsls label generation
11481 label_for_completion: Some(Box::new(|item, _| {
11482 let text = if let Some(description) = item
11483 .label_details
11484 .as_ref()
11485 .and_then(|label_details| label_details.description.as_ref())
11486 {
11487 format!("{} {}", item.label, description)
11488 } else if let Some(detail) = &item.detail {
11489 format!("{} {}", item.label, detail)
11490 } else {
11491 item.label.clone()
11492 };
11493 let len = text.len();
11494 Some(language::CodeLabel {
11495 text,
11496 runs: Vec::new(),
11497 filter_range: 0..len,
11498 })
11499 })),
11500 ..FakeLspAdapter::default()
11501 },
11502 );
11503 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11504 let cx = &mut VisualTestContext::from_window(*workspace, cx);
11505 let worktree_id = workspace
11506 .update(cx, |workspace, _window, cx| {
11507 workspace.project().update(cx, |project, cx| {
11508 project.worktrees(cx).next().unwrap().read(cx).id()
11509 })
11510 })
11511 .unwrap();
11512 let _buffer = project
11513 .update(cx, |project, cx| {
11514 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
11515 })
11516 .await
11517 .unwrap();
11518 let editor = workspace
11519 .update(cx, |workspace, window, cx| {
11520 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
11521 })
11522 .unwrap()
11523 .await
11524 .unwrap()
11525 .downcast::<Editor>()
11526 .unwrap();
11527 let fake_server = fake_servers.next().await.unwrap();
11528
11529 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
11530 let multiline_label_2 = "a\nb\nc\n";
11531 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
11532 let multiline_description = "d\ne\nf\n";
11533 let multiline_detail_2 = "g\nh\ni\n";
11534
11535 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
11536 move |params, _| async move {
11537 Ok(Some(lsp::CompletionResponse::Array(vec![
11538 lsp::CompletionItem {
11539 label: multiline_label.to_string(),
11540 text_edit: gen_text_edit(¶ms, "new_text_1"),
11541 ..lsp::CompletionItem::default()
11542 },
11543 lsp::CompletionItem {
11544 label: "single line label 1".to_string(),
11545 detail: Some(multiline_detail.to_string()),
11546 text_edit: gen_text_edit(¶ms, "new_text_2"),
11547 ..lsp::CompletionItem::default()
11548 },
11549 lsp::CompletionItem {
11550 label: "single line label 2".to_string(),
11551 label_details: Some(lsp::CompletionItemLabelDetails {
11552 description: Some(multiline_description.to_string()),
11553 detail: None,
11554 }),
11555 text_edit: gen_text_edit(¶ms, "new_text_2"),
11556 ..lsp::CompletionItem::default()
11557 },
11558 lsp::CompletionItem {
11559 label: multiline_label_2.to_string(),
11560 detail: Some(multiline_detail_2.to_string()),
11561 text_edit: gen_text_edit(¶ms, "new_text_3"),
11562 ..lsp::CompletionItem::default()
11563 },
11564 lsp::CompletionItem {
11565 label: "Label with many spaces and \t but without newlines".to_string(),
11566 detail: Some(
11567 "Details with many spaces and \t but without newlines".to_string(),
11568 ),
11569 text_edit: gen_text_edit(¶ms, "new_text_4"),
11570 ..lsp::CompletionItem::default()
11571 },
11572 ])))
11573 },
11574 );
11575
11576 editor.update_in(cx, |editor, window, cx| {
11577 cx.focus_self(window);
11578 editor.move_to_end(&MoveToEnd, window, cx);
11579 editor.handle_input(".", window, cx);
11580 });
11581 cx.run_until_parked();
11582 completion_handle.next().await.unwrap();
11583
11584 editor.update(cx, |editor, _| {
11585 assert!(editor.context_menu_visible());
11586 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11587 {
11588 let completion_labels = menu
11589 .completions
11590 .borrow()
11591 .iter()
11592 .map(|c| c.label.text.clone())
11593 .collect::<Vec<_>>();
11594 assert_eq!(
11595 completion_labels,
11596 &[
11597 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
11598 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
11599 "single line label 2 d e f ",
11600 "a b c g h i ",
11601 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
11602 ],
11603 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
11604 );
11605
11606 for completion in menu
11607 .completions
11608 .borrow()
11609 .iter() {
11610 assert_eq!(
11611 completion.label.filter_range,
11612 0..completion.label.text.len(),
11613 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
11614 );
11615 }
11616 } else {
11617 panic!("expected completion menu to be open");
11618 }
11619 });
11620}
11621
11622#[gpui::test]
11623async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
11624 init_test(cx, |_| {});
11625 let mut cx = EditorLspTestContext::new_rust(
11626 lsp::ServerCapabilities {
11627 completion_provider: Some(lsp::CompletionOptions {
11628 trigger_characters: Some(vec![".".to_string()]),
11629 ..Default::default()
11630 }),
11631 ..Default::default()
11632 },
11633 cx,
11634 )
11635 .await;
11636 cx.lsp
11637 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
11638 Ok(Some(lsp::CompletionResponse::Array(vec![
11639 lsp::CompletionItem {
11640 label: "first".into(),
11641 ..Default::default()
11642 },
11643 lsp::CompletionItem {
11644 label: "last".into(),
11645 ..Default::default()
11646 },
11647 ])))
11648 });
11649 cx.set_state("variableˇ");
11650 cx.simulate_keystroke(".");
11651 cx.executor().run_until_parked();
11652
11653 cx.update_editor(|editor, _, _| {
11654 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11655 {
11656 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
11657 } else {
11658 panic!("expected completion menu to be open");
11659 }
11660 });
11661
11662 cx.update_editor(|editor, window, cx| {
11663 editor.move_page_down(&MovePageDown::default(), window, cx);
11664 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11665 {
11666 assert!(
11667 menu.selected_item == 1,
11668 "expected PageDown to select the last item from the context menu"
11669 );
11670 } else {
11671 panic!("expected completion menu to stay open after PageDown");
11672 }
11673 });
11674
11675 cx.update_editor(|editor, window, cx| {
11676 editor.move_page_up(&MovePageUp::default(), window, cx);
11677 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11678 {
11679 assert!(
11680 menu.selected_item == 0,
11681 "expected PageUp to select the first item from the context menu"
11682 );
11683 } else {
11684 panic!("expected completion menu to stay open after PageUp");
11685 }
11686 });
11687}
11688
11689#[gpui::test]
11690async fn test_as_is_completions(cx: &mut TestAppContext) {
11691 init_test(cx, |_| {});
11692 let mut cx = EditorLspTestContext::new_rust(
11693 lsp::ServerCapabilities {
11694 completion_provider: Some(lsp::CompletionOptions {
11695 ..Default::default()
11696 }),
11697 ..Default::default()
11698 },
11699 cx,
11700 )
11701 .await;
11702 cx.lsp
11703 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
11704 Ok(Some(lsp::CompletionResponse::Array(vec![
11705 lsp::CompletionItem {
11706 label: "unsafe".into(),
11707 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11708 range: lsp::Range {
11709 start: lsp::Position {
11710 line: 1,
11711 character: 2,
11712 },
11713 end: lsp::Position {
11714 line: 1,
11715 character: 3,
11716 },
11717 },
11718 new_text: "unsafe".to_string(),
11719 })),
11720 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
11721 ..Default::default()
11722 },
11723 ])))
11724 });
11725 cx.set_state("fn a() {}\n nˇ");
11726 cx.executor().run_until_parked();
11727 cx.update_editor(|editor, window, cx| {
11728 editor.show_completions(
11729 &ShowCompletions {
11730 trigger: Some("\n".into()),
11731 },
11732 window,
11733 cx,
11734 );
11735 });
11736 cx.executor().run_until_parked();
11737
11738 cx.update_editor(|editor, window, cx| {
11739 editor.confirm_completion(&Default::default(), window, cx)
11740 });
11741 cx.executor().run_until_parked();
11742 cx.assert_editor_state("fn a() {}\n unsafeˇ");
11743}
11744
11745#[gpui::test]
11746async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
11747 init_test(cx, |_| {});
11748
11749 let mut cx = EditorLspTestContext::new_rust(
11750 lsp::ServerCapabilities {
11751 completion_provider: Some(lsp::CompletionOptions {
11752 trigger_characters: Some(vec![".".to_string()]),
11753 resolve_provider: Some(true),
11754 ..Default::default()
11755 }),
11756 ..Default::default()
11757 },
11758 cx,
11759 )
11760 .await;
11761
11762 cx.set_state("fn main() { let a = 2ˇ; }");
11763 cx.simulate_keystroke(".");
11764 let completion_item = lsp::CompletionItem {
11765 label: "Some".into(),
11766 kind: Some(lsp::CompletionItemKind::SNIPPET),
11767 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
11768 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
11769 kind: lsp::MarkupKind::Markdown,
11770 value: "```rust\nSome(2)\n```".to_string(),
11771 })),
11772 deprecated: Some(false),
11773 sort_text: Some("Some".to_string()),
11774 filter_text: Some("Some".to_string()),
11775 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
11776 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11777 range: lsp::Range {
11778 start: lsp::Position {
11779 line: 0,
11780 character: 22,
11781 },
11782 end: lsp::Position {
11783 line: 0,
11784 character: 22,
11785 },
11786 },
11787 new_text: "Some(2)".to_string(),
11788 })),
11789 additional_text_edits: Some(vec![lsp::TextEdit {
11790 range: lsp::Range {
11791 start: lsp::Position {
11792 line: 0,
11793 character: 20,
11794 },
11795 end: lsp::Position {
11796 line: 0,
11797 character: 22,
11798 },
11799 },
11800 new_text: "".to_string(),
11801 }]),
11802 ..Default::default()
11803 };
11804
11805 let closure_completion_item = completion_item.clone();
11806 let counter = Arc::new(AtomicUsize::new(0));
11807 let counter_clone = counter.clone();
11808 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
11809 let task_completion_item = closure_completion_item.clone();
11810 counter_clone.fetch_add(1, atomic::Ordering::Release);
11811 async move {
11812 Ok(Some(lsp::CompletionResponse::Array(vec![
11813 task_completion_item,
11814 ])))
11815 }
11816 });
11817
11818 cx.condition(|editor, _| editor.context_menu_visible())
11819 .await;
11820 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
11821 assert!(request.next().await.is_some());
11822 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11823
11824 cx.simulate_keystrokes("S o m");
11825 cx.condition(|editor, _| editor.context_menu_visible())
11826 .await;
11827 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
11828 assert!(request.next().await.is_some());
11829 assert!(request.next().await.is_some());
11830 assert!(request.next().await.is_some());
11831 request.close();
11832 assert!(request.next().await.is_none());
11833 assert_eq!(
11834 counter.load(atomic::Ordering::Acquire),
11835 4,
11836 "With the completions menu open, only one LSP request should happen per input"
11837 );
11838}
11839
11840#[gpui::test]
11841async fn test_toggle_comment(cx: &mut TestAppContext) {
11842 init_test(cx, |_| {});
11843 let mut cx = EditorTestContext::new(cx).await;
11844 let language = Arc::new(Language::new(
11845 LanguageConfig {
11846 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
11847 ..Default::default()
11848 },
11849 Some(tree_sitter_rust::LANGUAGE.into()),
11850 ));
11851 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
11852
11853 // If multiple selections intersect a line, the line is only toggled once.
11854 cx.set_state(indoc! {"
11855 fn a() {
11856 «//b();
11857 ˇ»// «c();
11858 //ˇ» d();
11859 }
11860 "});
11861
11862 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11863
11864 cx.assert_editor_state(indoc! {"
11865 fn a() {
11866 «b();
11867 c();
11868 ˇ» d();
11869 }
11870 "});
11871
11872 // The comment prefix is inserted at the same column for every line in a
11873 // selection.
11874 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11875
11876 cx.assert_editor_state(indoc! {"
11877 fn a() {
11878 // «b();
11879 // c();
11880 ˇ»// d();
11881 }
11882 "});
11883
11884 // If a selection ends at the beginning of a line, that line is not toggled.
11885 cx.set_selections_state(indoc! {"
11886 fn a() {
11887 // b();
11888 «// c();
11889 ˇ» // d();
11890 }
11891 "});
11892
11893 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11894
11895 cx.assert_editor_state(indoc! {"
11896 fn a() {
11897 // b();
11898 «c();
11899 ˇ» // d();
11900 }
11901 "});
11902
11903 // If a selection span a single line and is empty, the line is toggled.
11904 cx.set_state(indoc! {"
11905 fn a() {
11906 a();
11907 b();
11908 ˇ
11909 }
11910 "});
11911
11912 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11913
11914 cx.assert_editor_state(indoc! {"
11915 fn a() {
11916 a();
11917 b();
11918 //•ˇ
11919 }
11920 "});
11921
11922 // If a selection span multiple lines, empty lines are not toggled.
11923 cx.set_state(indoc! {"
11924 fn a() {
11925 «a();
11926
11927 c();ˇ»
11928 }
11929 "});
11930
11931 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11932
11933 cx.assert_editor_state(indoc! {"
11934 fn a() {
11935 // «a();
11936
11937 // c();ˇ»
11938 }
11939 "});
11940
11941 // If a selection includes multiple comment prefixes, all lines are uncommented.
11942 cx.set_state(indoc! {"
11943 fn a() {
11944 «// a();
11945 /// b();
11946 //! c();ˇ»
11947 }
11948 "});
11949
11950 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11951
11952 cx.assert_editor_state(indoc! {"
11953 fn a() {
11954 «a();
11955 b();
11956 c();ˇ»
11957 }
11958 "});
11959}
11960
11961#[gpui::test]
11962async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
11963 init_test(cx, |_| {});
11964 let mut cx = EditorTestContext::new(cx).await;
11965 let language = Arc::new(Language::new(
11966 LanguageConfig {
11967 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
11968 ..Default::default()
11969 },
11970 Some(tree_sitter_rust::LANGUAGE.into()),
11971 ));
11972 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
11973
11974 let toggle_comments = &ToggleComments {
11975 advance_downwards: false,
11976 ignore_indent: true,
11977 };
11978
11979 // If multiple selections intersect a line, the line is only toggled once.
11980 cx.set_state(indoc! {"
11981 fn a() {
11982 // «b();
11983 // c();
11984 // ˇ» d();
11985 }
11986 "});
11987
11988 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11989
11990 cx.assert_editor_state(indoc! {"
11991 fn a() {
11992 «b();
11993 c();
11994 ˇ» d();
11995 }
11996 "});
11997
11998 // The comment prefix is inserted at the beginning of each line
11999 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12000
12001 cx.assert_editor_state(indoc! {"
12002 fn a() {
12003 // «b();
12004 // c();
12005 // ˇ» d();
12006 }
12007 "});
12008
12009 // If a selection ends at the beginning of a line, that line is not toggled.
12010 cx.set_selections_state(indoc! {"
12011 fn a() {
12012 // b();
12013 // «c();
12014 ˇ»// d();
12015 }
12016 "});
12017
12018 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12019
12020 cx.assert_editor_state(indoc! {"
12021 fn a() {
12022 // b();
12023 «c();
12024 ˇ»// d();
12025 }
12026 "});
12027
12028 // If a selection span a single line and is empty, the line is toggled.
12029 cx.set_state(indoc! {"
12030 fn a() {
12031 a();
12032 b();
12033 ˇ
12034 }
12035 "});
12036
12037 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12038
12039 cx.assert_editor_state(indoc! {"
12040 fn a() {
12041 a();
12042 b();
12043 //ˇ
12044 }
12045 "});
12046
12047 // If a selection span multiple lines, empty lines are not toggled.
12048 cx.set_state(indoc! {"
12049 fn a() {
12050 «a();
12051
12052 c();ˇ»
12053 }
12054 "});
12055
12056 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12057
12058 cx.assert_editor_state(indoc! {"
12059 fn a() {
12060 // «a();
12061
12062 // c();ˇ»
12063 }
12064 "});
12065
12066 // If a selection includes multiple comment prefixes, all lines are uncommented.
12067 cx.set_state(indoc! {"
12068 fn a() {
12069 // «a();
12070 /// b();
12071 //! c();ˇ»
12072 }
12073 "});
12074
12075 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12076
12077 cx.assert_editor_state(indoc! {"
12078 fn a() {
12079 «a();
12080 b();
12081 c();ˇ»
12082 }
12083 "});
12084}
12085
12086#[gpui::test]
12087async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
12088 init_test(cx, |_| {});
12089
12090 let language = Arc::new(Language::new(
12091 LanguageConfig {
12092 line_comments: vec!["// ".into()],
12093 ..Default::default()
12094 },
12095 Some(tree_sitter_rust::LANGUAGE.into()),
12096 ));
12097
12098 let mut cx = EditorTestContext::new(cx).await;
12099
12100 cx.language_registry().add(language.clone());
12101 cx.update_buffer(|buffer, cx| {
12102 buffer.set_language(Some(language), cx);
12103 });
12104
12105 let toggle_comments = &ToggleComments {
12106 advance_downwards: true,
12107 ignore_indent: false,
12108 };
12109
12110 // Single cursor on one line -> advance
12111 // Cursor moves horizontally 3 characters as well on non-blank line
12112 cx.set_state(indoc!(
12113 "fn a() {
12114 ˇdog();
12115 cat();
12116 }"
12117 ));
12118 cx.update_editor(|editor, window, cx| {
12119 editor.toggle_comments(toggle_comments, window, cx);
12120 });
12121 cx.assert_editor_state(indoc!(
12122 "fn a() {
12123 // dog();
12124 catˇ();
12125 }"
12126 ));
12127
12128 // Single selection on one line -> don't advance
12129 cx.set_state(indoc!(
12130 "fn a() {
12131 «dog()ˇ»;
12132 cat();
12133 }"
12134 ));
12135 cx.update_editor(|editor, window, cx| {
12136 editor.toggle_comments(toggle_comments, window, cx);
12137 });
12138 cx.assert_editor_state(indoc!(
12139 "fn a() {
12140 // «dog()ˇ»;
12141 cat();
12142 }"
12143 ));
12144
12145 // Multiple cursors on one line -> advance
12146 cx.set_state(indoc!(
12147 "fn a() {
12148 ˇdˇog();
12149 cat();
12150 }"
12151 ));
12152 cx.update_editor(|editor, window, cx| {
12153 editor.toggle_comments(toggle_comments, window, cx);
12154 });
12155 cx.assert_editor_state(indoc!(
12156 "fn a() {
12157 // dog();
12158 catˇ(ˇ);
12159 }"
12160 ));
12161
12162 // Multiple cursors on one line, with selection -> don't advance
12163 cx.set_state(indoc!(
12164 "fn a() {
12165 ˇdˇog«()ˇ»;
12166 cat();
12167 }"
12168 ));
12169 cx.update_editor(|editor, window, cx| {
12170 editor.toggle_comments(toggle_comments, window, cx);
12171 });
12172 cx.assert_editor_state(indoc!(
12173 "fn a() {
12174 // ˇdˇog«()ˇ»;
12175 cat();
12176 }"
12177 ));
12178
12179 // Single cursor on one line -> advance
12180 // Cursor moves to column 0 on blank line
12181 cx.set_state(indoc!(
12182 "fn a() {
12183 ˇdog();
12184
12185 cat();
12186 }"
12187 ));
12188 cx.update_editor(|editor, window, cx| {
12189 editor.toggle_comments(toggle_comments, window, cx);
12190 });
12191 cx.assert_editor_state(indoc!(
12192 "fn a() {
12193 // dog();
12194 ˇ
12195 cat();
12196 }"
12197 ));
12198
12199 // Single cursor on one line -> advance
12200 // Cursor starts and ends at column 0
12201 cx.set_state(indoc!(
12202 "fn a() {
12203 ˇ dog();
12204 cat();
12205 }"
12206 ));
12207 cx.update_editor(|editor, window, cx| {
12208 editor.toggle_comments(toggle_comments, window, cx);
12209 });
12210 cx.assert_editor_state(indoc!(
12211 "fn a() {
12212 // dog();
12213 ˇ cat();
12214 }"
12215 ));
12216}
12217
12218#[gpui::test]
12219async fn test_toggle_block_comment(cx: &mut TestAppContext) {
12220 init_test(cx, |_| {});
12221
12222 let mut cx = EditorTestContext::new(cx).await;
12223
12224 let html_language = Arc::new(
12225 Language::new(
12226 LanguageConfig {
12227 name: "HTML".into(),
12228 block_comment: Some(("<!-- ".into(), " -->".into())),
12229 ..Default::default()
12230 },
12231 Some(tree_sitter_html::LANGUAGE.into()),
12232 )
12233 .with_injection_query(
12234 r#"
12235 (script_element
12236 (raw_text) @injection.content
12237 (#set! injection.language "javascript"))
12238 "#,
12239 )
12240 .unwrap(),
12241 );
12242
12243 let javascript_language = Arc::new(Language::new(
12244 LanguageConfig {
12245 name: "JavaScript".into(),
12246 line_comments: vec!["// ".into()],
12247 ..Default::default()
12248 },
12249 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
12250 ));
12251
12252 cx.language_registry().add(html_language.clone());
12253 cx.language_registry().add(javascript_language.clone());
12254 cx.update_buffer(|buffer, cx| {
12255 buffer.set_language(Some(html_language), cx);
12256 });
12257
12258 // Toggle comments for empty selections
12259 cx.set_state(
12260 &r#"
12261 <p>A</p>ˇ
12262 <p>B</p>ˇ
12263 <p>C</p>ˇ
12264 "#
12265 .unindent(),
12266 );
12267 cx.update_editor(|editor, window, cx| {
12268 editor.toggle_comments(&ToggleComments::default(), window, cx)
12269 });
12270 cx.assert_editor_state(
12271 &r#"
12272 <!-- <p>A</p>ˇ -->
12273 <!-- <p>B</p>ˇ -->
12274 <!-- <p>C</p>ˇ -->
12275 "#
12276 .unindent(),
12277 );
12278 cx.update_editor(|editor, window, cx| {
12279 editor.toggle_comments(&ToggleComments::default(), window, cx)
12280 });
12281 cx.assert_editor_state(
12282 &r#"
12283 <p>A</p>ˇ
12284 <p>B</p>ˇ
12285 <p>C</p>ˇ
12286 "#
12287 .unindent(),
12288 );
12289
12290 // Toggle comments for mixture of empty and non-empty selections, where
12291 // multiple selections occupy a given line.
12292 cx.set_state(
12293 &r#"
12294 <p>A«</p>
12295 <p>ˇ»B</p>ˇ
12296 <p>C«</p>
12297 <p>ˇ»D</p>ˇ
12298 "#
12299 .unindent(),
12300 );
12301
12302 cx.update_editor(|editor, window, cx| {
12303 editor.toggle_comments(&ToggleComments::default(), window, cx)
12304 });
12305 cx.assert_editor_state(
12306 &r#"
12307 <!-- <p>A«</p>
12308 <p>ˇ»B</p>ˇ -->
12309 <!-- <p>C«</p>
12310 <p>ˇ»D</p>ˇ -->
12311 "#
12312 .unindent(),
12313 );
12314 cx.update_editor(|editor, window, cx| {
12315 editor.toggle_comments(&ToggleComments::default(), window, cx)
12316 });
12317 cx.assert_editor_state(
12318 &r#"
12319 <p>A«</p>
12320 <p>ˇ»B</p>ˇ
12321 <p>C«</p>
12322 <p>ˇ»D</p>ˇ
12323 "#
12324 .unindent(),
12325 );
12326
12327 // Toggle comments when different languages are active for different
12328 // selections.
12329 cx.set_state(
12330 &r#"
12331 ˇ<script>
12332 ˇvar x = new Y();
12333 ˇ</script>
12334 "#
12335 .unindent(),
12336 );
12337 cx.executor().run_until_parked();
12338 cx.update_editor(|editor, window, cx| {
12339 editor.toggle_comments(&ToggleComments::default(), window, cx)
12340 });
12341 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
12342 // Uncommenting and commenting from this position brings in even more wrong artifacts.
12343 cx.assert_editor_state(
12344 &r#"
12345 <!-- ˇ<script> -->
12346 // ˇvar x = new Y();
12347 <!-- ˇ</script> -->
12348 "#
12349 .unindent(),
12350 );
12351}
12352
12353#[gpui::test]
12354fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
12355 init_test(cx, |_| {});
12356
12357 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
12358 let multibuffer = cx.new(|cx| {
12359 let mut multibuffer = MultiBuffer::new(ReadWrite);
12360 multibuffer.push_excerpts(
12361 buffer.clone(),
12362 [
12363 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
12364 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
12365 ],
12366 cx,
12367 );
12368 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
12369 multibuffer
12370 });
12371
12372 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
12373 editor.update_in(cx, |editor, window, cx| {
12374 assert_eq!(editor.text(cx), "aaaa\nbbbb");
12375 editor.change_selections(None, window, cx, |s| {
12376 s.select_ranges([
12377 Point::new(0, 0)..Point::new(0, 0),
12378 Point::new(1, 0)..Point::new(1, 0),
12379 ])
12380 });
12381
12382 editor.handle_input("X", window, cx);
12383 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
12384 assert_eq!(
12385 editor.selections.ranges(cx),
12386 [
12387 Point::new(0, 1)..Point::new(0, 1),
12388 Point::new(1, 1)..Point::new(1, 1),
12389 ]
12390 );
12391
12392 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
12393 editor.change_selections(None, window, cx, |s| {
12394 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
12395 });
12396 editor.backspace(&Default::default(), window, cx);
12397 assert_eq!(editor.text(cx), "Xa\nbbb");
12398 assert_eq!(
12399 editor.selections.ranges(cx),
12400 [Point::new(1, 0)..Point::new(1, 0)]
12401 );
12402
12403 editor.change_selections(None, window, cx, |s| {
12404 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
12405 });
12406 editor.backspace(&Default::default(), window, cx);
12407 assert_eq!(editor.text(cx), "X\nbb");
12408 assert_eq!(
12409 editor.selections.ranges(cx),
12410 [Point::new(0, 1)..Point::new(0, 1)]
12411 );
12412 });
12413}
12414
12415#[gpui::test]
12416fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
12417 init_test(cx, |_| {});
12418
12419 let markers = vec![('[', ']').into(), ('(', ')').into()];
12420 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
12421 indoc! {"
12422 [aaaa
12423 (bbbb]
12424 cccc)",
12425 },
12426 markers.clone(),
12427 );
12428 let excerpt_ranges = markers.into_iter().map(|marker| {
12429 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
12430 ExcerptRange::new(context.clone())
12431 });
12432 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
12433 let multibuffer = cx.new(|cx| {
12434 let mut multibuffer = MultiBuffer::new(ReadWrite);
12435 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
12436 multibuffer
12437 });
12438
12439 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
12440 editor.update_in(cx, |editor, window, cx| {
12441 let (expected_text, selection_ranges) = marked_text_ranges(
12442 indoc! {"
12443 aaaa
12444 bˇbbb
12445 bˇbbˇb
12446 cccc"
12447 },
12448 true,
12449 );
12450 assert_eq!(editor.text(cx), expected_text);
12451 editor.change_selections(None, window, cx, |s| s.select_ranges(selection_ranges));
12452
12453 editor.handle_input("X", window, cx);
12454
12455 let (expected_text, expected_selections) = marked_text_ranges(
12456 indoc! {"
12457 aaaa
12458 bXˇbbXb
12459 bXˇbbXˇb
12460 cccc"
12461 },
12462 false,
12463 );
12464 assert_eq!(editor.text(cx), expected_text);
12465 assert_eq!(editor.selections.ranges(cx), expected_selections);
12466
12467 editor.newline(&Newline, window, cx);
12468 let (expected_text, expected_selections) = marked_text_ranges(
12469 indoc! {"
12470 aaaa
12471 bX
12472 ˇbbX
12473 b
12474 bX
12475 ˇbbX
12476 ˇb
12477 cccc"
12478 },
12479 false,
12480 );
12481 assert_eq!(editor.text(cx), expected_text);
12482 assert_eq!(editor.selections.ranges(cx), expected_selections);
12483 });
12484}
12485
12486#[gpui::test]
12487fn test_refresh_selections(cx: &mut TestAppContext) {
12488 init_test(cx, |_| {});
12489
12490 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
12491 let mut excerpt1_id = None;
12492 let multibuffer = cx.new(|cx| {
12493 let mut multibuffer = MultiBuffer::new(ReadWrite);
12494 excerpt1_id = multibuffer
12495 .push_excerpts(
12496 buffer.clone(),
12497 [
12498 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
12499 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
12500 ],
12501 cx,
12502 )
12503 .into_iter()
12504 .next();
12505 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
12506 multibuffer
12507 });
12508
12509 let editor = cx.add_window(|window, cx| {
12510 let mut editor = build_editor(multibuffer.clone(), window, cx);
12511 let snapshot = editor.snapshot(window, cx);
12512 editor.change_selections(None, window, cx, |s| {
12513 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
12514 });
12515 editor.begin_selection(
12516 Point::new(2, 1).to_display_point(&snapshot),
12517 true,
12518 1,
12519 window,
12520 cx,
12521 );
12522 assert_eq!(
12523 editor.selections.ranges(cx),
12524 [
12525 Point::new(1, 3)..Point::new(1, 3),
12526 Point::new(2, 1)..Point::new(2, 1),
12527 ]
12528 );
12529 editor
12530 });
12531
12532 // Refreshing selections is a no-op when excerpts haven't changed.
12533 _ = editor.update(cx, |editor, window, cx| {
12534 editor.change_selections(None, window, cx, |s| s.refresh());
12535 assert_eq!(
12536 editor.selections.ranges(cx),
12537 [
12538 Point::new(1, 3)..Point::new(1, 3),
12539 Point::new(2, 1)..Point::new(2, 1),
12540 ]
12541 );
12542 });
12543
12544 multibuffer.update(cx, |multibuffer, cx| {
12545 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
12546 });
12547 _ = editor.update(cx, |editor, window, cx| {
12548 // Removing an excerpt causes the first selection to become degenerate.
12549 assert_eq!(
12550 editor.selections.ranges(cx),
12551 [
12552 Point::new(0, 0)..Point::new(0, 0),
12553 Point::new(0, 1)..Point::new(0, 1)
12554 ]
12555 );
12556
12557 // Refreshing selections will relocate the first selection to the original buffer
12558 // location.
12559 editor.change_selections(None, window, cx, |s| s.refresh());
12560 assert_eq!(
12561 editor.selections.ranges(cx),
12562 [
12563 Point::new(0, 1)..Point::new(0, 1),
12564 Point::new(0, 3)..Point::new(0, 3)
12565 ]
12566 );
12567 assert!(editor.selections.pending_anchor().is_some());
12568 });
12569}
12570
12571#[gpui::test]
12572fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
12573 init_test(cx, |_| {});
12574
12575 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
12576 let mut excerpt1_id = None;
12577 let multibuffer = cx.new(|cx| {
12578 let mut multibuffer = MultiBuffer::new(ReadWrite);
12579 excerpt1_id = multibuffer
12580 .push_excerpts(
12581 buffer.clone(),
12582 [
12583 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
12584 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
12585 ],
12586 cx,
12587 )
12588 .into_iter()
12589 .next();
12590 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
12591 multibuffer
12592 });
12593
12594 let editor = cx.add_window(|window, cx| {
12595 let mut editor = build_editor(multibuffer.clone(), window, cx);
12596 let snapshot = editor.snapshot(window, cx);
12597 editor.begin_selection(
12598 Point::new(1, 3).to_display_point(&snapshot),
12599 false,
12600 1,
12601 window,
12602 cx,
12603 );
12604 assert_eq!(
12605 editor.selections.ranges(cx),
12606 [Point::new(1, 3)..Point::new(1, 3)]
12607 );
12608 editor
12609 });
12610
12611 multibuffer.update(cx, |multibuffer, cx| {
12612 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
12613 });
12614 _ = editor.update(cx, |editor, window, cx| {
12615 assert_eq!(
12616 editor.selections.ranges(cx),
12617 [Point::new(0, 0)..Point::new(0, 0)]
12618 );
12619
12620 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
12621 editor.change_selections(None, window, cx, |s| s.refresh());
12622 assert_eq!(
12623 editor.selections.ranges(cx),
12624 [Point::new(0, 3)..Point::new(0, 3)]
12625 );
12626 assert!(editor.selections.pending_anchor().is_some());
12627 });
12628}
12629
12630#[gpui::test]
12631async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
12632 init_test(cx, |_| {});
12633
12634 let language = Arc::new(
12635 Language::new(
12636 LanguageConfig {
12637 brackets: BracketPairConfig {
12638 pairs: vec![
12639 BracketPair {
12640 start: "{".to_string(),
12641 end: "}".to_string(),
12642 close: true,
12643 surround: true,
12644 newline: true,
12645 },
12646 BracketPair {
12647 start: "/* ".to_string(),
12648 end: " */".to_string(),
12649 close: true,
12650 surround: true,
12651 newline: true,
12652 },
12653 ],
12654 ..Default::default()
12655 },
12656 ..Default::default()
12657 },
12658 Some(tree_sitter_rust::LANGUAGE.into()),
12659 )
12660 .with_indents_query("")
12661 .unwrap(),
12662 );
12663
12664 let text = concat!(
12665 "{ }\n", //
12666 " x\n", //
12667 " /* */\n", //
12668 "x\n", //
12669 "{{} }\n", //
12670 );
12671
12672 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
12673 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12674 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
12675 editor
12676 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
12677 .await;
12678
12679 editor.update_in(cx, |editor, window, cx| {
12680 editor.change_selections(None, window, cx, |s| {
12681 s.select_display_ranges([
12682 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
12683 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
12684 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
12685 ])
12686 });
12687 editor.newline(&Newline, window, cx);
12688
12689 assert_eq!(
12690 editor.buffer().read(cx).read(cx).text(),
12691 concat!(
12692 "{ \n", // Suppress rustfmt
12693 "\n", //
12694 "}\n", //
12695 " x\n", //
12696 " /* \n", //
12697 " \n", //
12698 " */\n", //
12699 "x\n", //
12700 "{{} \n", //
12701 "}\n", //
12702 )
12703 );
12704 });
12705}
12706
12707#[gpui::test]
12708fn test_highlighted_ranges(cx: &mut TestAppContext) {
12709 init_test(cx, |_| {});
12710
12711 let editor = cx.add_window(|window, cx| {
12712 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
12713 build_editor(buffer.clone(), window, cx)
12714 });
12715
12716 _ = editor.update(cx, |editor, window, cx| {
12717 struct Type1;
12718 struct Type2;
12719
12720 let buffer = editor.buffer.read(cx).snapshot(cx);
12721
12722 let anchor_range =
12723 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
12724
12725 editor.highlight_background::<Type1>(
12726 &[
12727 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
12728 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
12729 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
12730 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
12731 ],
12732 |_| Hsla::red(),
12733 cx,
12734 );
12735 editor.highlight_background::<Type2>(
12736 &[
12737 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
12738 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
12739 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
12740 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
12741 ],
12742 |_| Hsla::green(),
12743 cx,
12744 );
12745
12746 let snapshot = editor.snapshot(window, cx);
12747 let mut highlighted_ranges = editor.background_highlights_in_range(
12748 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
12749 &snapshot,
12750 cx.theme().colors(),
12751 );
12752 // Enforce a consistent ordering based on color without relying on the ordering of the
12753 // highlight's `TypeId` which is non-executor.
12754 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
12755 assert_eq!(
12756 highlighted_ranges,
12757 &[
12758 (
12759 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
12760 Hsla::red(),
12761 ),
12762 (
12763 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
12764 Hsla::red(),
12765 ),
12766 (
12767 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
12768 Hsla::green(),
12769 ),
12770 (
12771 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
12772 Hsla::green(),
12773 ),
12774 ]
12775 );
12776 assert_eq!(
12777 editor.background_highlights_in_range(
12778 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
12779 &snapshot,
12780 cx.theme().colors(),
12781 ),
12782 &[(
12783 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
12784 Hsla::red(),
12785 )]
12786 );
12787 });
12788}
12789
12790#[gpui::test]
12791async fn test_following(cx: &mut TestAppContext) {
12792 init_test(cx, |_| {});
12793
12794 let fs = FakeFs::new(cx.executor());
12795 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
12796
12797 let buffer = project.update(cx, |project, cx| {
12798 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
12799 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
12800 });
12801 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
12802 let follower = cx.update(|cx| {
12803 cx.open_window(
12804 WindowOptions {
12805 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
12806 gpui::Point::new(px(0.), px(0.)),
12807 gpui::Point::new(px(10.), px(80.)),
12808 ))),
12809 ..Default::default()
12810 },
12811 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
12812 )
12813 .unwrap()
12814 });
12815
12816 let is_still_following = Rc::new(RefCell::new(true));
12817 let follower_edit_event_count = Rc::new(RefCell::new(0));
12818 let pending_update = Rc::new(RefCell::new(None));
12819 let leader_entity = leader.root(cx).unwrap();
12820 let follower_entity = follower.root(cx).unwrap();
12821 _ = follower.update(cx, {
12822 let update = pending_update.clone();
12823 let is_still_following = is_still_following.clone();
12824 let follower_edit_event_count = follower_edit_event_count.clone();
12825 |_, window, cx| {
12826 cx.subscribe_in(
12827 &leader_entity,
12828 window,
12829 move |_, leader, event, window, cx| {
12830 leader.read(cx).add_event_to_update_proto(
12831 event,
12832 &mut update.borrow_mut(),
12833 window,
12834 cx,
12835 );
12836 },
12837 )
12838 .detach();
12839
12840 cx.subscribe_in(
12841 &follower_entity,
12842 window,
12843 move |_, _, event: &EditorEvent, _window, _cx| {
12844 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
12845 *is_still_following.borrow_mut() = false;
12846 }
12847
12848 if let EditorEvent::BufferEdited = event {
12849 *follower_edit_event_count.borrow_mut() += 1;
12850 }
12851 },
12852 )
12853 .detach();
12854 }
12855 });
12856
12857 // Update the selections only
12858 _ = leader.update(cx, |leader, window, cx| {
12859 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
12860 });
12861 follower
12862 .update(cx, |follower, window, cx| {
12863 follower.apply_update_proto(
12864 &project,
12865 pending_update.borrow_mut().take().unwrap(),
12866 window,
12867 cx,
12868 )
12869 })
12870 .unwrap()
12871 .await
12872 .unwrap();
12873 _ = follower.update(cx, |follower, _, cx| {
12874 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
12875 });
12876 assert!(*is_still_following.borrow());
12877 assert_eq!(*follower_edit_event_count.borrow(), 0);
12878
12879 // Update the scroll position only
12880 _ = leader.update(cx, |leader, window, cx| {
12881 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
12882 });
12883 follower
12884 .update(cx, |follower, window, cx| {
12885 follower.apply_update_proto(
12886 &project,
12887 pending_update.borrow_mut().take().unwrap(),
12888 window,
12889 cx,
12890 )
12891 })
12892 .unwrap()
12893 .await
12894 .unwrap();
12895 assert_eq!(
12896 follower
12897 .update(cx, |follower, _, cx| follower.scroll_position(cx))
12898 .unwrap(),
12899 gpui::Point::new(1.5, 3.5)
12900 );
12901 assert!(*is_still_following.borrow());
12902 assert_eq!(*follower_edit_event_count.borrow(), 0);
12903
12904 // Update the selections and scroll position. The follower's scroll position is updated
12905 // via autoscroll, not via the leader's exact scroll position.
12906 _ = leader.update(cx, |leader, window, cx| {
12907 leader.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
12908 leader.request_autoscroll(Autoscroll::newest(), cx);
12909 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
12910 });
12911 follower
12912 .update(cx, |follower, window, cx| {
12913 follower.apply_update_proto(
12914 &project,
12915 pending_update.borrow_mut().take().unwrap(),
12916 window,
12917 cx,
12918 )
12919 })
12920 .unwrap()
12921 .await
12922 .unwrap();
12923 _ = follower.update(cx, |follower, _, cx| {
12924 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
12925 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
12926 });
12927 assert!(*is_still_following.borrow());
12928
12929 // Creating a pending selection that precedes another selection
12930 _ = leader.update(cx, |leader, window, cx| {
12931 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
12932 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
12933 });
12934 follower
12935 .update(cx, |follower, window, cx| {
12936 follower.apply_update_proto(
12937 &project,
12938 pending_update.borrow_mut().take().unwrap(),
12939 window,
12940 cx,
12941 )
12942 })
12943 .unwrap()
12944 .await
12945 .unwrap();
12946 _ = follower.update(cx, |follower, _, cx| {
12947 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
12948 });
12949 assert!(*is_still_following.borrow());
12950
12951 // Extend the pending selection so that it surrounds another selection
12952 _ = leader.update(cx, |leader, window, cx| {
12953 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
12954 });
12955 follower
12956 .update(cx, |follower, window, cx| {
12957 follower.apply_update_proto(
12958 &project,
12959 pending_update.borrow_mut().take().unwrap(),
12960 window,
12961 cx,
12962 )
12963 })
12964 .unwrap()
12965 .await
12966 .unwrap();
12967 _ = follower.update(cx, |follower, _, cx| {
12968 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
12969 });
12970
12971 // Scrolling locally breaks the follow
12972 _ = follower.update(cx, |follower, window, cx| {
12973 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
12974 follower.set_scroll_anchor(
12975 ScrollAnchor {
12976 anchor: top_anchor,
12977 offset: gpui::Point::new(0.0, 0.5),
12978 },
12979 window,
12980 cx,
12981 );
12982 });
12983 assert!(!(*is_still_following.borrow()));
12984}
12985
12986#[gpui::test]
12987async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
12988 init_test(cx, |_| {});
12989
12990 let fs = FakeFs::new(cx.executor());
12991 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
12992 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12993 let pane = workspace
12994 .update(cx, |workspace, _, _| workspace.active_pane().clone())
12995 .unwrap();
12996
12997 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
12998
12999 let leader = pane.update_in(cx, |_, window, cx| {
13000 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
13001 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
13002 });
13003
13004 // Start following the editor when it has no excerpts.
13005 let mut state_message =
13006 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
13007 let workspace_entity = workspace.root(cx).unwrap();
13008 let follower_1 = cx
13009 .update_window(*workspace.deref(), |_, window, cx| {
13010 Editor::from_state_proto(
13011 workspace_entity,
13012 ViewId {
13013 creator: CollaboratorId::PeerId(PeerId::default()),
13014 id: 0,
13015 },
13016 &mut state_message,
13017 window,
13018 cx,
13019 )
13020 })
13021 .unwrap()
13022 .unwrap()
13023 .await
13024 .unwrap();
13025
13026 let update_message = Rc::new(RefCell::new(None));
13027 follower_1.update_in(cx, {
13028 let update = update_message.clone();
13029 |_, window, cx| {
13030 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
13031 leader.read(cx).add_event_to_update_proto(
13032 event,
13033 &mut update.borrow_mut(),
13034 window,
13035 cx,
13036 );
13037 })
13038 .detach();
13039 }
13040 });
13041
13042 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
13043 (
13044 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
13045 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
13046 )
13047 });
13048
13049 // Insert some excerpts.
13050 leader.update(cx, |leader, cx| {
13051 leader.buffer.update(cx, |multibuffer, cx| {
13052 multibuffer.set_excerpts_for_path(
13053 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
13054 buffer_1.clone(),
13055 vec![
13056 Point::row_range(0..3),
13057 Point::row_range(1..6),
13058 Point::row_range(12..15),
13059 ],
13060 0,
13061 cx,
13062 );
13063 multibuffer.set_excerpts_for_path(
13064 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
13065 buffer_2.clone(),
13066 vec![Point::row_range(0..6), Point::row_range(8..12)],
13067 0,
13068 cx,
13069 );
13070 });
13071 });
13072
13073 // Apply the update of adding the excerpts.
13074 follower_1
13075 .update_in(cx, |follower, window, cx| {
13076 follower.apply_update_proto(
13077 &project,
13078 update_message.borrow().clone().unwrap(),
13079 window,
13080 cx,
13081 )
13082 })
13083 .await
13084 .unwrap();
13085 assert_eq!(
13086 follower_1.update(cx, |editor, cx| editor.text(cx)),
13087 leader.update(cx, |editor, cx| editor.text(cx))
13088 );
13089 update_message.borrow_mut().take();
13090
13091 // Start following separately after it already has excerpts.
13092 let mut state_message =
13093 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
13094 let workspace_entity = workspace.root(cx).unwrap();
13095 let follower_2 = cx
13096 .update_window(*workspace.deref(), |_, window, cx| {
13097 Editor::from_state_proto(
13098 workspace_entity,
13099 ViewId {
13100 creator: CollaboratorId::PeerId(PeerId::default()),
13101 id: 0,
13102 },
13103 &mut state_message,
13104 window,
13105 cx,
13106 )
13107 })
13108 .unwrap()
13109 .unwrap()
13110 .await
13111 .unwrap();
13112 assert_eq!(
13113 follower_2.update(cx, |editor, cx| editor.text(cx)),
13114 leader.update(cx, |editor, cx| editor.text(cx))
13115 );
13116
13117 // Remove some excerpts.
13118 leader.update(cx, |leader, cx| {
13119 leader.buffer.update(cx, |multibuffer, cx| {
13120 let excerpt_ids = multibuffer.excerpt_ids();
13121 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
13122 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
13123 });
13124 });
13125
13126 // Apply the update of removing the excerpts.
13127 follower_1
13128 .update_in(cx, |follower, window, cx| {
13129 follower.apply_update_proto(
13130 &project,
13131 update_message.borrow().clone().unwrap(),
13132 window,
13133 cx,
13134 )
13135 })
13136 .await
13137 .unwrap();
13138 follower_2
13139 .update_in(cx, |follower, window, cx| {
13140 follower.apply_update_proto(
13141 &project,
13142 update_message.borrow().clone().unwrap(),
13143 window,
13144 cx,
13145 )
13146 })
13147 .await
13148 .unwrap();
13149 update_message.borrow_mut().take();
13150 assert_eq!(
13151 follower_1.update(cx, |editor, cx| editor.text(cx)),
13152 leader.update(cx, |editor, cx| editor.text(cx))
13153 );
13154}
13155
13156#[gpui::test]
13157async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
13158 init_test(cx, |_| {});
13159
13160 let mut cx = EditorTestContext::new(cx).await;
13161 let lsp_store =
13162 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
13163
13164 cx.set_state(indoc! {"
13165 ˇfn func(abc def: i32) -> u32 {
13166 }
13167 "});
13168
13169 cx.update(|_, cx| {
13170 lsp_store.update(cx, |lsp_store, cx| {
13171 lsp_store
13172 .update_diagnostics(
13173 LanguageServerId(0),
13174 lsp::PublishDiagnosticsParams {
13175 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
13176 version: None,
13177 diagnostics: vec![
13178 lsp::Diagnostic {
13179 range: lsp::Range::new(
13180 lsp::Position::new(0, 11),
13181 lsp::Position::new(0, 12),
13182 ),
13183 severity: Some(lsp::DiagnosticSeverity::ERROR),
13184 ..Default::default()
13185 },
13186 lsp::Diagnostic {
13187 range: lsp::Range::new(
13188 lsp::Position::new(0, 12),
13189 lsp::Position::new(0, 15),
13190 ),
13191 severity: Some(lsp::DiagnosticSeverity::ERROR),
13192 ..Default::default()
13193 },
13194 lsp::Diagnostic {
13195 range: lsp::Range::new(
13196 lsp::Position::new(0, 25),
13197 lsp::Position::new(0, 28),
13198 ),
13199 severity: Some(lsp::DiagnosticSeverity::ERROR),
13200 ..Default::default()
13201 },
13202 ],
13203 },
13204 &[],
13205 cx,
13206 )
13207 .unwrap()
13208 });
13209 });
13210
13211 executor.run_until_parked();
13212
13213 cx.update_editor(|editor, window, cx| {
13214 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13215 });
13216
13217 cx.assert_editor_state(indoc! {"
13218 fn func(abc def: i32) -> ˇu32 {
13219 }
13220 "});
13221
13222 cx.update_editor(|editor, window, cx| {
13223 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13224 });
13225
13226 cx.assert_editor_state(indoc! {"
13227 fn func(abc ˇdef: i32) -> u32 {
13228 }
13229 "});
13230
13231 cx.update_editor(|editor, window, cx| {
13232 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13233 });
13234
13235 cx.assert_editor_state(indoc! {"
13236 fn func(abcˇ def: i32) -> u32 {
13237 }
13238 "});
13239
13240 cx.update_editor(|editor, window, cx| {
13241 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13242 });
13243
13244 cx.assert_editor_state(indoc! {"
13245 fn func(abc def: i32) -> ˇu32 {
13246 }
13247 "});
13248}
13249
13250#[gpui::test]
13251async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
13252 init_test(cx, |_| {});
13253
13254 let mut cx = EditorTestContext::new(cx).await;
13255
13256 let diff_base = r#"
13257 use some::mod;
13258
13259 const A: u32 = 42;
13260
13261 fn main() {
13262 println!("hello");
13263
13264 println!("world");
13265 }
13266 "#
13267 .unindent();
13268
13269 // Edits are modified, removed, modified, added
13270 cx.set_state(
13271 &r#"
13272 use some::modified;
13273
13274 ˇ
13275 fn main() {
13276 println!("hello there");
13277
13278 println!("around the");
13279 println!("world");
13280 }
13281 "#
13282 .unindent(),
13283 );
13284
13285 cx.set_head_text(&diff_base);
13286 executor.run_until_parked();
13287
13288 cx.update_editor(|editor, window, cx| {
13289 //Wrap around the bottom of the buffer
13290 for _ in 0..3 {
13291 editor.go_to_next_hunk(&GoToHunk, window, cx);
13292 }
13293 });
13294
13295 cx.assert_editor_state(
13296 &r#"
13297 ˇuse some::modified;
13298
13299
13300 fn main() {
13301 println!("hello there");
13302
13303 println!("around the");
13304 println!("world");
13305 }
13306 "#
13307 .unindent(),
13308 );
13309
13310 cx.update_editor(|editor, window, cx| {
13311 //Wrap around the top of the buffer
13312 for _ in 0..2 {
13313 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13314 }
13315 });
13316
13317 cx.assert_editor_state(
13318 &r#"
13319 use some::modified;
13320
13321
13322 fn main() {
13323 ˇ println!("hello there");
13324
13325 println!("around the");
13326 println!("world");
13327 }
13328 "#
13329 .unindent(),
13330 );
13331
13332 cx.update_editor(|editor, window, cx| {
13333 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13334 });
13335
13336 cx.assert_editor_state(
13337 &r#"
13338 use some::modified;
13339
13340 ˇ
13341 fn main() {
13342 println!("hello there");
13343
13344 println!("around the");
13345 println!("world");
13346 }
13347 "#
13348 .unindent(),
13349 );
13350
13351 cx.update_editor(|editor, window, cx| {
13352 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13353 });
13354
13355 cx.assert_editor_state(
13356 &r#"
13357 ˇuse some::modified;
13358
13359
13360 fn main() {
13361 println!("hello there");
13362
13363 println!("around the");
13364 println!("world");
13365 }
13366 "#
13367 .unindent(),
13368 );
13369
13370 cx.update_editor(|editor, window, cx| {
13371 for _ in 0..2 {
13372 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13373 }
13374 });
13375
13376 cx.assert_editor_state(
13377 &r#"
13378 use some::modified;
13379
13380
13381 fn main() {
13382 ˇ println!("hello there");
13383
13384 println!("around the");
13385 println!("world");
13386 }
13387 "#
13388 .unindent(),
13389 );
13390
13391 cx.update_editor(|editor, window, cx| {
13392 editor.fold(&Fold, window, cx);
13393 });
13394
13395 cx.update_editor(|editor, window, cx| {
13396 editor.go_to_next_hunk(&GoToHunk, window, cx);
13397 });
13398
13399 cx.assert_editor_state(
13400 &r#"
13401 ˇuse some::modified;
13402
13403
13404 fn main() {
13405 println!("hello there");
13406
13407 println!("around the");
13408 println!("world");
13409 }
13410 "#
13411 .unindent(),
13412 );
13413}
13414
13415#[test]
13416fn test_split_words() {
13417 fn split(text: &str) -> Vec<&str> {
13418 split_words(text).collect()
13419 }
13420
13421 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
13422 assert_eq!(split("hello_world"), &["hello_", "world"]);
13423 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
13424 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
13425 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
13426 assert_eq!(split("helloworld"), &["helloworld"]);
13427
13428 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
13429}
13430
13431#[gpui::test]
13432async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
13433 init_test(cx, |_| {});
13434
13435 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
13436 let mut assert = |before, after| {
13437 let _state_context = cx.set_state(before);
13438 cx.run_until_parked();
13439 cx.update_editor(|editor, window, cx| {
13440 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
13441 });
13442 cx.run_until_parked();
13443 cx.assert_editor_state(after);
13444 };
13445
13446 // Outside bracket jumps to outside of matching bracket
13447 assert("console.logˇ(var);", "console.log(var)ˇ;");
13448 assert("console.log(var)ˇ;", "console.logˇ(var);");
13449
13450 // Inside bracket jumps to inside of matching bracket
13451 assert("console.log(ˇvar);", "console.log(varˇ);");
13452 assert("console.log(varˇ);", "console.log(ˇvar);");
13453
13454 // When outside a bracket and inside, favor jumping to the inside bracket
13455 assert(
13456 "console.log('foo', [1, 2, 3]ˇ);",
13457 "console.log(ˇ'foo', [1, 2, 3]);",
13458 );
13459 assert(
13460 "console.log(ˇ'foo', [1, 2, 3]);",
13461 "console.log('foo', [1, 2, 3]ˇ);",
13462 );
13463
13464 // Bias forward if two options are equally likely
13465 assert(
13466 "let result = curried_fun()ˇ();",
13467 "let result = curried_fun()()ˇ;",
13468 );
13469
13470 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
13471 assert(
13472 indoc! {"
13473 function test() {
13474 console.log('test')ˇ
13475 }"},
13476 indoc! {"
13477 function test() {
13478 console.logˇ('test')
13479 }"},
13480 );
13481}
13482
13483#[gpui::test]
13484async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
13485 init_test(cx, |_| {});
13486
13487 let fs = FakeFs::new(cx.executor());
13488 fs.insert_tree(
13489 path!("/a"),
13490 json!({
13491 "main.rs": "fn main() { let a = 5; }",
13492 "other.rs": "// Test file",
13493 }),
13494 )
13495 .await;
13496 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13497
13498 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13499 language_registry.add(Arc::new(Language::new(
13500 LanguageConfig {
13501 name: "Rust".into(),
13502 matcher: LanguageMatcher {
13503 path_suffixes: vec!["rs".to_string()],
13504 ..Default::default()
13505 },
13506 brackets: BracketPairConfig {
13507 pairs: vec![BracketPair {
13508 start: "{".to_string(),
13509 end: "}".to_string(),
13510 close: true,
13511 surround: true,
13512 newline: true,
13513 }],
13514 disabled_scopes_by_bracket_ix: Vec::new(),
13515 },
13516 ..Default::default()
13517 },
13518 Some(tree_sitter_rust::LANGUAGE.into()),
13519 )));
13520 let mut fake_servers = language_registry.register_fake_lsp(
13521 "Rust",
13522 FakeLspAdapter {
13523 capabilities: lsp::ServerCapabilities {
13524 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
13525 first_trigger_character: "{".to_string(),
13526 more_trigger_character: None,
13527 }),
13528 ..Default::default()
13529 },
13530 ..Default::default()
13531 },
13532 );
13533
13534 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13535
13536 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13537
13538 let worktree_id = workspace
13539 .update(cx, |workspace, _, cx| {
13540 workspace.project().update(cx, |project, cx| {
13541 project.worktrees(cx).next().unwrap().read(cx).id()
13542 })
13543 })
13544 .unwrap();
13545
13546 let buffer = project
13547 .update(cx, |project, cx| {
13548 project.open_local_buffer(path!("/a/main.rs"), cx)
13549 })
13550 .await
13551 .unwrap();
13552 let editor_handle = workspace
13553 .update(cx, |workspace, window, cx| {
13554 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
13555 })
13556 .unwrap()
13557 .await
13558 .unwrap()
13559 .downcast::<Editor>()
13560 .unwrap();
13561
13562 cx.executor().start_waiting();
13563 let fake_server = fake_servers.next().await.unwrap();
13564
13565 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
13566 |params, _| async move {
13567 assert_eq!(
13568 params.text_document_position.text_document.uri,
13569 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
13570 );
13571 assert_eq!(
13572 params.text_document_position.position,
13573 lsp::Position::new(0, 21),
13574 );
13575
13576 Ok(Some(vec![lsp::TextEdit {
13577 new_text: "]".to_string(),
13578 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13579 }]))
13580 },
13581 );
13582
13583 editor_handle.update_in(cx, |editor, window, cx| {
13584 window.focus(&editor.focus_handle(cx));
13585 editor.change_selections(None, window, cx, |s| {
13586 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
13587 });
13588 editor.handle_input("{", window, cx);
13589 });
13590
13591 cx.executor().run_until_parked();
13592
13593 buffer.update(cx, |buffer, _| {
13594 assert_eq!(
13595 buffer.text(),
13596 "fn main() { let a = {5}; }",
13597 "No extra braces from on type formatting should appear in the buffer"
13598 )
13599 });
13600}
13601
13602#[gpui::test]
13603async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
13604 init_test(cx, |_| {});
13605
13606 let fs = FakeFs::new(cx.executor());
13607 fs.insert_tree(
13608 path!("/a"),
13609 json!({
13610 "main.rs": "fn main() { let a = 5; }",
13611 "other.rs": "// Test file",
13612 }),
13613 )
13614 .await;
13615
13616 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13617
13618 let server_restarts = Arc::new(AtomicUsize::new(0));
13619 let closure_restarts = Arc::clone(&server_restarts);
13620 let language_server_name = "test language server";
13621 let language_name: LanguageName = "Rust".into();
13622
13623 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13624 language_registry.add(Arc::new(Language::new(
13625 LanguageConfig {
13626 name: language_name.clone(),
13627 matcher: LanguageMatcher {
13628 path_suffixes: vec!["rs".to_string()],
13629 ..Default::default()
13630 },
13631 ..Default::default()
13632 },
13633 Some(tree_sitter_rust::LANGUAGE.into()),
13634 )));
13635 let mut fake_servers = language_registry.register_fake_lsp(
13636 "Rust",
13637 FakeLspAdapter {
13638 name: language_server_name,
13639 initialization_options: Some(json!({
13640 "testOptionValue": true
13641 })),
13642 initializer: Some(Box::new(move |fake_server| {
13643 let task_restarts = Arc::clone(&closure_restarts);
13644 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
13645 task_restarts.fetch_add(1, atomic::Ordering::Release);
13646 futures::future::ready(Ok(()))
13647 });
13648 })),
13649 ..Default::default()
13650 },
13651 );
13652
13653 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13654 let _buffer = project
13655 .update(cx, |project, cx| {
13656 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
13657 })
13658 .await
13659 .unwrap();
13660 let _fake_server = fake_servers.next().await.unwrap();
13661 update_test_language_settings(cx, |language_settings| {
13662 language_settings.languages.insert(
13663 language_name.clone(),
13664 LanguageSettingsContent {
13665 tab_size: NonZeroU32::new(8),
13666 ..Default::default()
13667 },
13668 );
13669 });
13670 cx.executor().run_until_parked();
13671 assert_eq!(
13672 server_restarts.load(atomic::Ordering::Acquire),
13673 0,
13674 "Should not restart LSP server on an unrelated change"
13675 );
13676
13677 update_test_project_settings(cx, |project_settings| {
13678 project_settings.lsp.insert(
13679 "Some other server name".into(),
13680 LspSettings {
13681 binary: None,
13682 settings: None,
13683 initialization_options: Some(json!({
13684 "some other init value": false
13685 })),
13686 enable_lsp_tasks: false,
13687 },
13688 );
13689 });
13690 cx.executor().run_until_parked();
13691 assert_eq!(
13692 server_restarts.load(atomic::Ordering::Acquire),
13693 0,
13694 "Should not restart LSP server on an unrelated LSP settings change"
13695 );
13696
13697 update_test_project_settings(cx, |project_settings| {
13698 project_settings.lsp.insert(
13699 language_server_name.into(),
13700 LspSettings {
13701 binary: None,
13702 settings: None,
13703 initialization_options: Some(json!({
13704 "anotherInitValue": false
13705 })),
13706 enable_lsp_tasks: false,
13707 },
13708 );
13709 });
13710 cx.executor().run_until_parked();
13711 assert_eq!(
13712 server_restarts.load(atomic::Ordering::Acquire),
13713 1,
13714 "Should restart LSP server on a related LSP settings change"
13715 );
13716
13717 update_test_project_settings(cx, |project_settings| {
13718 project_settings.lsp.insert(
13719 language_server_name.into(),
13720 LspSettings {
13721 binary: None,
13722 settings: None,
13723 initialization_options: Some(json!({
13724 "anotherInitValue": false
13725 })),
13726 enable_lsp_tasks: false,
13727 },
13728 );
13729 });
13730 cx.executor().run_until_parked();
13731 assert_eq!(
13732 server_restarts.load(atomic::Ordering::Acquire),
13733 1,
13734 "Should not restart LSP server on a related LSP settings change that is the same"
13735 );
13736
13737 update_test_project_settings(cx, |project_settings| {
13738 project_settings.lsp.insert(
13739 language_server_name.into(),
13740 LspSettings {
13741 binary: None,
13742 settings: None,
13743 initialization_options: None,
13744 enable_lsp_tasks: false,
13745 },
13746 );
13747 });
13748 cx.executor().run_until_parked();
13749 assert_eq!(
13750 server_restarts.load(atomic::Ordering::Acquire),
13751 2,
13752 "Should restart LSP server on another related LSP settings change"
13753 );
13754}
13755
13756#[gpui::test]
13757async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
13758 init_test(cx, |_| {});
13759
13760 let mut cx = EditorLspTestContext::new_rust(
13761 lsp::ServerCapabilities {
13762 completion_provider: Some(lsp::CompletionOptions {
13763 trigger_characters: Some(vec![".".to_string()]),
13764 resolve_provider: Some(true),
13765 ..Default::default()
13766 }),
13767 ..Default::default()
13768 },
13769 cx,
13770 )
13771 .await;
13772
13773 cx.set_state("fn main() { let a = 2ˇ; }");
13774 cx.simulate_keystroke(".");
13775 let completion_item = lsp::CompletionItem {
13776 label: "some".into(),
13777 kind: Some(lsp::CompletionItemKind::SNIPPET),
13778 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
13779 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
13780 kind: lsp::MarkupKind::Markdown,
13781 value: "```rust\nSome(2)\n```".to_string(),
13782 })),
13783 deprecated: Some(false),
13784 sort_text: Some("fffffff2".to_string()),
13785 filter_text: Some("some".to_string()),
13786 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
13787 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13788 range: lsp::Range {
13789 start: lsp::Position {
13790 line: 0,
13791 character: 22,
13792 },
13793 end: lsp::Position {
13794 line: 0,
13795 character: 22,
13796 },
13797 },
13798 new_text: "Some(2)".to_string(),
13799 })),
13800 additional_text_edits: Some(vec![lsp::TextEdit {
13801 range: lsp::Range {
13802 start: lsp::Position {
13803 line: 0,
13804 character: 20,
13805 },
13806 end: lsp::Position {
13807 line: 0,
13808 character: 22,
13809 },
13810 },
13811 new_text: "".to_string(),
13812 }]),
13813 ..Default::default()
13814 };
13815
13816 let closure_completion_item = completion_item.clone();
13817 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13818 let task_completion_item = closure_completion_item.clone();
13819 async move {
13820 Ok(Some(lsp::CompletionResponse::Array(vec![
13821 task_completion_item,
13822 ])))
13823 }
13824 });
13825
13826 request.next().await;
13827
13828 cx.condition(|editor, _| editor.context_menu_visible())
13829 .await;
13830 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13831 editor
13832 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13833 .unwrap()
13834 });
13835 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
13836
13837 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
13838 let task_completion_item = completion_item.clone();
13839 async move { Ok(task_completion_item) }
13840 })
13841 .next()
13842 .await
13843 .unwrap();
13844 apply_additional_edits.await.unwrap();
13845 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
13846}
13847
13848#[gpui::test]
13849async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
13850 init_test(cx, |_| {});
13851
13852 let mut cx = EditorLspTestContext::new_rust(
13853 lsp::ServerCapabilities {
13854 completion_provider: Some(lsp::CompletionOptions {
13855 trigger_characters: Some(vec![".".to_string()]),
13856 resolve_provider: Some(true),
13857 ..Default::default()
13858 }),
13859 ..Default::default()
13860 },
13861 cx,
13862 )
13863 .await;
13864
13865 cx.set_state("fn main() { let a = 2ˇ; }");
13866 cx.simulate_keystroke(".");
13867
13868 let item1 = lsp::CompletionItem {
13869 label: "method id()".to_string(),
13870 filter_text: Some("id".to_string()),
13871 detail: None,
13872 documentation: None,
13873 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13874 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13875 new_text: ".id".to_string(),
13876 })),
13877 ..lsp::CompletionItem::default()
13878 };
13879
13880 let item2 = lsp::CompletionItem {
13881 label: "other".to_string(),
13882 filter_text: Some("other".to_string()),
13883 detail: None,
13884 documentation: None,
13885 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13886 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13887 new_text: ".other".to_string(),
13888 })),
13889 ..lsp::CompletionItem::default()
13890 };
13891
13892 let item1 = item1.clone();
13893 cx.set_request_handler::<lsp::request::Completion, _, _>({
13894 let item1 = item1.clone();
13895 move |_, _, _| {
13896 let item1 = item1.clone();
13897 let item2 = item2.clone();
13898 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
13899 }
13900 })
13901 .next()
13902 .await;
13903
13904 cx.condition(|editor, _| editor.context_menu_visible())
13905 .await;
13906 cx.update_editor(|editor, _, _| {
13907 let context_menu = editor.context_menu.borrow_mut();
13908 let context_menu = context_menu
13909 .as_ref()
13910 .expect("Should have the context menu deployed");
13911 match context_menu {
13912 CodeContextMenu::Completions(completions_menu) => {
13913 let completions = completions_menu.completions.borrow_mut();
13914 assert_eq!(
13915 completions
13916 .iter()
13917 .map(|completion| &completion.label.text)
13918 .collect::<Vec<_>>(),
13919 vec!["method id()", "other"]
13920 )
13921 }
13922 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
13923 }
13924 });
13925
13926 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
13927 let item1 = item1.clone();
13928 move |_, item_to_resolve, _| {
13929 let item1 = item1.clone();
13930 async move {
13931 if item1 == item_to_resolve {
13932 Ok(lsp::CompletionItem {
13933 label: "method id()".to_string(),
13934 filter_text: Some("id".to_string()),
13935 detail: Some("Now resolved!".to_string()),
13936 documentation: Some(lsp::Documentation::String("Docs".to_string())),
13937 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13938 range: lsp::Range::new(
13939 lsp::Position::new(0, 22),
13940 lsp::Position::new(0, 22),
13941 ),
13942 new_text: ".id".to_string(),
13943 })),
13944 ..lsp::CompletionItem::default()
13945 })
13946 } else {
13947 Ok(item_to_resolve)
13948 }
13949 }
13950 }
13951 })
13952 .next()
13953 .await
13954 .unwrap();
13955 cx.run_until_parked();
13956
13957 cx.update_editor(|editor, window, cx| {
13958 editor.context_menu_next(&Default::default(), window, cx);
13959 });
13960
13961 cx.update_editor(|editor, _, _| {
13962 let context_menu = editor.context_menu.borrow_mut();
13963 let context_menu = context_menu
13964 .as_ref()
13965 .expect("Should have the context menu deployed");
13966 match context_menu {
13967 CodeContextMenu::Completions(completions_menu) => {
13968 let completions = completions_menu.completions.borrow_mut();
13969 assert_eq!(
13970 completions
13971 .iter()
13972 .map(|completion| &completion.label.text)
13973 .collect::<Vec<_>>(),
13974 vec!["method id() Now resolved!", "other"],
13975 "Should update first completion label, but not second as the filter text did not match."
13976 );
13977 }
13978 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
13979 }
13980 });
13981}
13982
13983#[gpui::test]
13984async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
13985 init_test(cx, |_| {});
13986 let mut cx = EditorLspTestContext::new_rust(
13987 lsp::ServerCapabilities {
13988 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
13989 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
13990 completion_provider: Some(lsp::CompletionOptions {
13991 resolve_provider: Some(true),
13992 ..Default::default()
13993 }),
13994 ..Default::default()
13995 },
13996 cx,
13997 )
13998 .await;
13999 cx.set_state(indoc! {"
14000 struct TestStruct {
14001 field: i32
14002 }
14003
14004 fn mainˇ() {
14005 let unused_var = 42;
14006 let test_struct = TestStruct { field: 42 };
14007 }
14008 "});
14009 let symbol_range = cx.lsp_range(indoc! {"
14010 struct TestStruct {
14011 field: i32
14012 }
14013
14014 «fn main»() {
14015 let unused_var = 42;
14016 let test_struct = TestStruct { field: 42 };
14017 }
14018 "});
14019 let mut hover_requests =
14020 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
14021 Ok(Some(lsp::Hover {
14022 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
14023 kind: lsp::MarkupKind::Markdown,
14024 value: "Function documentation".to_string(),
14025 }),
14026 range: Some(symbol_range),
14027 }))
14028 });
14029
14030 // Case 1: Test that code action menu hide hover popover
14031 cx.dispatch_action(Hover);
14032 hover_requests.next().await;
14033 cx.condition(|editor, _| editor.hover_state.visible()).await;
14034 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
14035 move |_, _, _| async move {
14036 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
14037 lsp::CodeAction {
14038 title: "Remove unused variable".to_string(),
14039 kind: Some(CodeActionKind::QUICKFIX),
14040 edit: Some(lsp::WorkspaceEdit {
14041 changes: Some(
14042 [(
14043 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
14044 vec![lsp::TextEdit {
14045 range: lsp::Range::new(
14046 lsp::Position::new(5, 4),
14047 lsp::Position::new(5, 27),
14048 ),
14049 new_text: "".to_string(),
14050 }],
14051 )]
14052 .into_iter()
14053 .collect(),
14054 ),
14055 ..Default::default()
14056 }),
14057 ..Default::default()
14058 },
14059 )]))
14060 },
14061 );
14062 cx.update_editor(|editor, window, cx| {
14063 editor.toggle_code_actions(
14064 &ToggleCodeActions {
14065 deployed_from_indicator: None,
14066 quick_launch: false,
14067 },
14068 window,
14069 cx,
14070 );
14071 });
14072 code_action_requests.next().await;
14073 cx.run_until_parked();
14074 cx.condition(|editor, _| editor.context_menu_visible())
14075 .await;
14076 cx.update_editor(|editor, _, _| {
14077 assert!(
14078 !editor.hover_state.visible(),
14079 "Hover popover should be hidden when code action menu is shown"
14080 );
14081 // Hide code actions
14082 editor.context_menu.take();
14083 });
14084
14085 // Case 2: Test that code completions hide hover popover
14086 cx.dispatch_action(Hover);
14087 hover_requests.next().await;
14088 cx.condition(|editor, _| editor.hover_state.visible()).await;
14089 let counter = Arc::new(AtomicUsize::new(0));
14090 let mut completion_requests =
14091 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14092 let counter = counter.clone();
14093 async move {
14094 counter.fetch_add(1, atomic::Ordering::Release);
14095 Ok(Some(lsp::CompletionResponse::Array(vec![
14096 lsp::CompletionItem {
14097 label: "main".into(),
14098 kind: Some(lsp::CompletionItemKind::FUNCTION),
14099 detail: Some("() -> ()".to_string()),
14100 ..Default::default()
14101 },
14102 lsp::CompletionItem {
14103 label: "TestStruct".into(),
14104 kind: Some(lsp::CompletionItemKind::STRUCT),
14105 detail: Some("struct TestStruct".to_string()),
14106 ..Default::default()
14107 },
14108 ])))
14109 }
14110 });
14111 cx.update_editor(|editor, window, cx| {
14112 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14113 });
14114 completion_requests.next().await;
14115 cx.condition(|editor, _| editor.context_menu_visible())
14116 .await;
14117 cx.update_editor(|editor, _, _| {
14118 assert!(
14119 !editor.hover_state.visible(),
14120 "Hover popover should be hidden when completion menu is shown"
14121 );
14122 });
14123}
14124
14125#[gpui::test]
14126async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
14127 init_test(cx, |_| {});
14128
14129 let mut cx = EditorLspTestContext::new_rust(
14130 lsp::ServerCapabilities {
14131 completion_provider: Some(lsp::CompletionOptions {
14132 trigger_characters: Some(vec![".".to_string()]),
14133 resolve_provider: Some(true),
14134 ..Default::default()
14135 }),
14136 ..Default::default()
14137 },
14138 cx,
14139 )
14140 .await;
14141
14142 cx.set_state("fn main() { let a = 2ˇ; }");
14143 cx.simulate_keystroke(".");
14144
14145 let unresolved_item_1 = lsp::CompletionItem {
14146 label: "id".to_string(),
14147 filter_text: Some("id".to_string()),
14148 detail: None,
14149 documentation: None,
14150 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14151 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14152 new_text: ".id".to_string(),
14153 })),
14154 ..lsp::CompletionItem::default()
14155 };
14156 let resolved_item_1 = lsp::CompletionItem {
14157 additional_text_edits: Some(vec![lsp::TextEdit {
14158 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
14159 new_text: "!!".to_string(),
14160 }]),
14161 ..unresolved_item_1.clone()
14162 };
14163 let unresolved_item_2 = lsp::CompletionItem {
14164 label: "other".to_string(),
14165 filter_text: Some("other".to_string()),
14166 detail: None,
14167 documentation: None,
14168 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14169 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14170 new_text: ".other".to_string(),
14171 })),
14172 ..lsp::CompletionItem::default()
14173 };
14174 let resolved_item_2 = lsp::CompletionItem {
14175 additional_text_edits: Some(vec![lsp::TextEdit {
14176 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
14177 new_text: "??".to_string(),
14178 }]),
14179 ..unresolved_item_2.clone()
14180 };
14181
14182 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
14183 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
14184 cx.lsp
14185 .server
14186 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
14187 let unresolved_item_1 = unresolved_item_1.clone();
14188 let resolved_item_1 = resolved_item_1.clone();
14189 let unresolved_item_2 = unresolved_item_2.clone();
14190 let resolved_item_2 = resolved_item_2.clone();
14191 let resolve_requests_1 = resolve_requests_1.clone();
14192 let resolve_requests_2 = resolve_requests_2.clone();
14193 move |unresolved_request, _| {
14194 let unresolved_item_1 = unresolved_item_1.clone();
14195 let resolved_item_1 = resolved_item_1.clone();
14196 let unresolved_item_2 = unresolved_item_2.clone();
14197 let resolved_item_2 = resolved_item_2.clone();
14198 let resolve_requests_1 = resolve_requests_1.clone();
14199 let resolve_requests_2 = resolve_requests_2.clone();
14200 async move {
14201 if unresolved_request == unresolved_item_1 {
14202 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
14203 Ok(resolved_item_1.clone())
14204 } else if unresolved_request == unresolved_item_2 {
14205 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
14206 Ok(resolved_item_2.clone())
14207 } else {
14208 panic!("Unexpected completion item {unresolved_request:?}")
14209 }
14210 }
14211 }
14212 })
14213 .detach();
14214
14215 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14216 let unresolved_item_1 = unresolved_item_1.clone();
14217 let unresolved_item_2 = unresolved_item_2.clone();
14218 async move {
14219 Ok(Some(lsp::CompletionResponse::Array(vec![
14220 unresolved_item_1,
14221 unresolved_item_2,
14222 ])))
14223 }
14224 })
14225 .next()
14226 .await;
14227
14228 cx.condition(|editor, _| editor.context_menu_visible())
14229 .await;
14230 cx.update_editor(|editor, _, _| {
14231 let context_menu = editor.context_menu.borrow_mut();
14232 let context_menu = context_menu
14233 .as_ref()
14234 .expect("Should have the context menu deployed");
14235 match context_menu {
14236 CodeContextMenu::Completions(completions_menu) => {
14237 let completions = completions_menu.completions.borrow_mut();
14238 assert_eq!(
14239 completions
14240 .iter()
14241 .map(|completion| &completion.label.text)
14242 .collect::<Vec<_>>(),
14243 vec!["id", "other"]
14244 )
14245 }
14246 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
14247 }
14248 });
14249 cx.run_until_parked();
14250
14251 cx.update_editor(|editor, window, cx| {
14252 editor.context_menu_next(&ContextMenuNext, window, cx);
14253 });
14254 cx.run_until_parked();
14255 cx.update_editor(|editor, window, cx| {
14256 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
14257 });
14258 cx.run_until_parked();
14259 cx.update_editor(|editor, window, cx| {
14260 editor.context_menu_next(&ContextMenuNext, window, cx);
14261 });
14262 cx.run_until_parked();
14263 cx.update_editor(|editor, window, cx| {
14264 editor
14265 .compose_completion(&ComposeCompletion::default(), window, cx)
14266 .expect("No task returned")
14267 })
14268 .await
14269 .expect("Completion failed");
14270 cx.run_until_parked();
14271
14272 cx.update_editor(|editor, _, cx| {
14273 assert_eq!(
14274 resolve_requests_1.load(atomic::Ordering::Acquire),
14275 1,
14276 "Should always resolve once despite multiple selections"
14277 );
14278 assert_eq!(
14279 resolve_requests_2.load(atomic::Ordering::Acquire),
14280 1,
14281 "Should always resolve once after multiple selections and applying the completion"
14282 );
14283 assert_eq!(
14284 editor.text(cx),
14285 "fn main() { let a = ??.other; }",
14286 "Should use resolved data when applying the completion"
14287 );
14288 });
14289}
14290
14291#[gpui::test]
14292async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
14293 init_test(cx, |_| {});
14294
14295 let item_0 = lsp::CompletionItem {
14296 label: "abs".into(),
14297 insert_text: Some("abs".into()),
14298 data: Some(json!({ "very": "special"})),
14299 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
14300 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
14301 lsp::InsertReplaceEdit {
14302 new_text: "abs".to_string(),
14303 insert: lsp::Range::default(),
14304 replace: lsp::Range::default(),
14305 },
14306 )),
14307 ..lsp::CompletionItem::default()
14308 };
14309 let items = iter::once(item_0.clone())
14310 .chain((11..51).map(|i| lsp::CompletionItem {
14311 label: format!("item_{}", i),
14312 insert_text: Some(format!("item_{}", i)),
14313 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
14314 ..lsp::CompletionItem::default()
14315 }))
14316 .collect::<Vec<_>>();
14317
14318 let default_commit_characters = vec!["?".to_string()];
14319 let default_data = json!({ "default": "data"});
14320 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
14321 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
14322 let default_edit_range = lsp::Range {
14323 start: lsp::Position {
14324 line: 0,
14325 character: 5,
14326 },
14327 end: lsp::Position {
14328 line: 0,
14329 character: 5,
14330 },
14331 };
14332
14333 let mut cx = EditorLspTestContext::new_rust(
14334 lsp::ServerCapabilities {
14335 completion_provider: Some(lsp::CompletionOptions {
14336 trigger_characters: Some(vec![".".to_string()]),
14337 resolve_provider: Some(true),
14338 ..Default::default()
14339 }),
14340 ..Default::default()
14341 },
14342 cx,
14343 )
14344 .await;
14345
14346 cx.set_state("fn main() { let a = 2ˇ; }");
14347 cx.simulate_keystroke(".");
14348
14349 let completion_data = default_data.clone();
14350 let completion_characters = default_commit_characters.clone();
14351 let completion_items = items.clone();
14352 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14353 let default_data = completion_data.clone();
14354 let default_commit_characters = completion_characters.clone();
14355 let items = completion_items.clone();
14356 async move {
14357 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
14358 items,
14359 item_defaults: Some(lsp::CompletionListItemDefaults {
14360 data: Some(default_data.clone()),
14361 commit_characters: Some(default_commit_characters.clone()),
14362 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
14363 default_edit_range,
14364 )),
14365 insert_text_format: Some(default_insert_text_format),
14366 insert_text_mode: Some(default_insert_text_mode),
14367 }),
14368 ..lsp::CompletionList::default()
14369 })))
14370 }
14371 })
14372 .next()
14373 .await;
14374
14375 let resolved_items = Arc::new(Mutex::new(Vec::new()));
14376 cx.lsp
14377 .server
14378 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
14379 let closure_resolved_items = resolved_items.clone();
14380 move |item_to_resolve, _| {
14381 let closure_resolved_items = closure_resolved_items.clone();
14382 async move {
14383 closure_resolved_items.lock().push(item_to_resolve.clone());
14384 Ok(item_to_resolve)
14385 }
14386 }
14387 })
14388 .detach();
14389
14390 cx.condition(|editor, _| editor.context_menu_visible())
14391 .await;
14392 cx.run_until_parked();
14393 cx.update_editor(|editor, _, _| {
14394 let menu = editor.context_menu.borrow_mut();
14395 match menu.as_ref().expect("should have the completions menu") {
14396 CodeContextMenu::Completions(completions_menu) => {
14397 assert_eq!(
14398 completions_menu
14399 .entries
14400 .borrow()
14401 .iter()
14402 .map(|mat| mat.string.clone())
14403 .collect::<Vec<String>>(),
14404 items
14405 .iter()
14406 .map(|completion| completion.label.clone())
14407 .collect::<Vec<String>>()
14408 );
14409 }
14410 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
14411 }
14412 });
14413 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
14414 // with 4 from the end.
14415 assert_eq!(
14416 *resolved_items.lock(),
14417 [&items[0..16], &items[items.len() - 4..items.len()]]
14418 .concat()
14419 .iter()
14420 .cloned()
14421 .map(|mut item| {
14422 if item.data.is_none() {
14423 item.data = Some(default_data.clone());
14424 }
14425 item
14426 })
14427 .collect::<Vec<lsp::CompletionItem>>(),
14428 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
14429 );
14430 resolved_items.lock().clear();
14431
14432 cx.update_editor(|editor, window, cx| {
14433 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
14434 });
14435 cx.run_until_parked();
14436 // Completions that have already been resolved are skipped.
14437 assert_eq!(
14438 *resolved_items.lock(),
14439 items[items.len() - 16..items.len() - 4]
14440 .iter()
14441 .cloned()
14442 .map(|mut item| {
14443 if item.data.is_none() {
14444 item.data = Some(default_data.clone());
14445 }
14446 item
14447 })
14448 .collect::<Vec<lsp::CompletionItem>>()
14449 );
14450 resolved_items.lock().clear();
14451}
14452
14453#[gpui::test]
14454async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
14455 init_test(cx, |_| {});
14456
14457 let mut cx = EditorLspTestContext::new(
14458 Language::new(
14459 LanguageConfig {
14460 matcher: LanguageMatcher {
14461 path_suffixes: vec!["jsx".into()],
14462 ..Default::default()
14463 },
14464 overrides: [(
14465 "element".into(),
14466 LanguageConfigOverride {
14467 completion_query_characters: Override::Set(['-'].into_iter().collect()),
14468 ..Default::default()
14469 },
14470 )]
14471 .into_iter()
14472 .collect(),
14473 ..Default::default()
14474 },
14475 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
14476 )
14477 .with_override_query("(jsx_self_closing_element) @element")
14478 .unwrap(),
14479 lsp::ServerCapabilities {
14480 completion_provider: Some(lsp::CompletionOptions {
14481 trigger_characters: Some(vec![":".to_string()]),
14482 ..Default::default()
14483 }),
14484 ..Default::default()
14485 },
14486 cx,
14487 )
14488 .await;
14489
14490 cx.lsp
14491 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14492 Ok(Some(lsp::CompletionResponse::Array(vec![
14493 lsp::CompletionItem {
14494 label: "bg-blue".into(),
14495 ..Default::default()
14496 },
14497 lsp::CompletionItem {
14498 label: "bg-red".into(),
14499 ..Default::default()
14500 },
14501 lsp::CompletionItem {
14502 label: "bg-yellow".into(),
14503 ..Default::default()
14504 },
14505 ])))
14506 });
14507
14508 cx.set_state(r#"<p class="bgˇ" />"#);
14509
14510 // Trigger completion when typing a dash, because the dash is an extra
14511 // word character in the 'element' scope, which contains the cursor.
14512 cx.simulate_keystroke("-");
14513 cx.executor().run_until_parked();
14514 cx.update_editor(|editor, _, _| {
14515 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14516 {
14517 assert_eq!(
14518 completion_menu_entries(&menu),
14519 &["bg-red", "bg-blue", "bg-yellow"]
14520 );
14521 } else {
14522 panic!("expected completion menu to be open");
14523 }
14524 });
14525
14526 cx.simulate_keystroke("l");
14527 cx.executor().run_until_parked();
14528 cx.update_editor(|editor, _, _| {
14529 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14530 {
14531 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
14532 } else {
14533 panic!("expected completion menu to be open");
14534 }
14535 });
14536
14537 // When filtering completions, consider the character after the '-' to
14538 // be the start of a subword.
14539 cx.set_state(r#"<p class="yelˇ" />"#);
14540 cx.simulate_keystroke("l");
14541 cx.executor().run_until_parked();
14542 cx.update_editor(|editor, _, _| {
14543 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14544 {
14545 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
14546 } else {
14547 panic!("expected completion menu to be open");
14548 }
14549 });
14550}
14551
14552fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
14553 let entries = menu.entries.borrow();
14554 entries.iter().map(|mat| mat.string.clone()).collect()
14555}
14556
14557#[gpui::test]
14558async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
14559 init_test(cx, |settings| {
14560 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
14561 FormatterList(vec![Formatter::Prettier].into()),
14562 ))
14563 });
14564
14565 let fs = FakeFs::new(cx.executor());
14566 fs.insert_file(path!("/file.ts"), Default::default()).await;
14567
14568 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
14569 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14570
14571 language_registry.add(Arc::new(Language::new(
14572 LanguageConfig {
14573 name: "TypeScript".into(),
14574 matcher: LanguageMatcher {
14575 path_suffixes: vec!["ts".to_string()],
14576 ..Default::default()
14577 },
14578 ..Default::default()
14579 },
14580 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
14581 )));
14582 update_test_language_settings(cx, |settings| {
14583 settings.defaults.prettier = Some(PrettierSettings {
14584 allowed: true,
14585 ..PrettierSettings::default()
14586 });
14587 });
14588
14589 let test_plugin = "test_plugin";
14590 let _ = language_registry.register_fake_lsp(
14591 "TypeScript",
14592 FakeLspAdapter {
14593 prettier_plugins: vec![test_plugin],
14594 ..Default::default()
14595 },
14596 );
14597
14598 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
14599 let buffer = project
14600 .update(cx, |project, cx| {
14601 project.open_local_buffer(path!("/file.ts"), cx)
14602 })
14603 .await
14604 .unwrap();
14605
14606 let buffer_text = "one\ntwo\nthree\n";
14607 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
14608 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
14609 editor.update_in(cx, |editor, window, cx| {
14610 editor.set_text(buffer_text, window, cx)
14611 });
14612
14613 editor
14614 .update_in(cx, |editor, window, cx| {
14615 editor.perform_format(
14616 project.clone(),
14617 FormatTrigger::Manual,
14618 FormatTarget::Buffers,
14619 window,
14620 cx,
14621 )
14622 })
14623 .unwrap()
14624 .await;
14625 assert_eq!(
14626 editor.update(cx, |editor, cx| editor.text(cx)),
14627 buffer_text.to_string() + prettier_format_suffix,
14628 "Test prettier formatting was not applied to the original buffer text",
14629 );
14630
14631 update_test_language_settings(cx, |settings| {
14632 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
14633 });
14634 let format = editor.update_in(cx, |editor, window, cx| {
14635 editor.perform_format(
14636 project.clone(),
14637 FormatTrigger::Manual,
14638 FormatTarget::Buffers,
14639 window,
14640 cx,
14641 )
14642 });
14643 format.await.unwrap();
14644 assert_eq!(
14645 editor.update(cx, |editor, cx| editor.text(cx)),
14646 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
14647 "Autoformatting (via test prettier) was not applied to the original buffer text",
14648 );
14649}
14650
14651#[gpui::test]
14652async fn test_addition_reverts(cx: &mut TestAppContext) {
14653 init_test(cx, |_| {});
14654 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14655 let base_text = indoc! {r#"
14656 struct Row;
14657 struct Row1;
14658 struct Row2;
14659
14660 struct Row4;
14661 struct Row5;
14662 struct Row6;
14663
14664 struct Row8;
14665 struct Row9;
14666 struct Row10;"#};
14667
14668 // When addition hunks are not adjacent to carets, no hunk revert is performed
14669 assert_hunk_revert(
14670 indoc! {r#"struct Row;
14671 struct Row1;
14672 struct Row1.1;
14673 struct Row1.2;
14674 struct Row2;ˇ
14675
14676 struct Row4;
14677 struct Row5;
14678 struct Row6;
14679
14680 struct Row8;
14681 ˇstruct Row9;
14682 struct Row9.1;
14683 struct Row9.2;
14684 struct Row9.3;
14685 struct Row10;"#},
14686 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
14687 indoc! {r#"struct Row;
14688 struct Row1;
14689 struct Row1.1;
14690 struct Row1.2;
14691 struct Row2;ˇ
14692
14693 struct Row4;
14694 struct Row5;
14695 struct Row6;
14696
14697 struct Row8;
14698 ˇstruct Row9;
14699 struct Row9.1;
14700 struct Row9.2;
14701 struct Row9.3;
14702 struct Row10;"#},
14703 base_text,
14704 &mut cx,
14705 );
14706 // Same for selections
14707 assert_hunk_revert(
14708 indoc! {r#"struct Row;
14709 struct Row1;
14710 struct Row2;
14711 struct Row2.1;
14712 struct Row2.2;
14713 «ˇ
14714 struct Row4;
14715 struct» Row5;
14716 «struct Row6;
14717 ˇ»
14718 struct Row9.1;
14719 struct Row9.2;
14720 struct Row9.3;
14721 struct Row8;
14722 struct Row9;
14723 struct Row10;"#},
14724 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
14725 indoc! {r#"struct Row;
14726 struct Row1;
14727 struct Row2;
14728 struct Row2.1;
14729 struct Row2.2;
14730 «ˇ
14731 struct Row4;
14732 struct» Row5;
14733 «struct Row6;
14734 ˇ»
14735 struct Row9.1;
14736 struct Row9.2;
14737 struct Row9.3;
14738 struct Row8;
14739 struct Row9;
14740 struct Row10;"#},
14741 base_text,
14742 &mut cx,
14743 );
14744
14745 // When carets and selections intersect the addition hunks, those are reverted.
14746 // Adjacent carets got merged.
14747 assert_hunk_revert(
14748 indoc! {r#"struct Row;
14749 ˇ// something on the top
14750 struct Row1;
14751 struct Row2;
14752 struct Roˇw3.1;
14753 struct Row2.2;
14754 struct Row2.3;ˇ
14755
14756 struct Row4;
14757 struct ˇRow5.1;
14758 struct Row5.2;
14759 struct «Rowˇ»5.3;
14760 struct Row5;
14761 struct Row6;
14762 ˇ
14763 struct Row9.1;
14764 struct «Rowˇ»9.2;
14765 struct «ˇRow»9.3;
14766 struct Row8;
14767 struct Row9;
14768 «ˇ// something on bottom»
14769 struct Row10;"#},
14770 vec![
14771 DiffHunkStatusKind::Added,
14772 DiffHunkStatusKind::Added,
14773 DiffHunkStatusKind::Added,
14774 DiffHunkStatusKind::Added,
14775 DiffHunkStatusKind::Added,
14776 ],
14777 indoc! {r#"struct Row;
14778 ˇstruct Row1;
14779 struct Row2;
14780 ˇ
14781 struct Row4;
14782 ˇstruct Row5;
14783 struct Row6;
14784 ˇ
14785 ˇstruct Row8;
14786 struct Row9;
14787 ˇstruct Row10;"#},
14788 base_text,
14789 &mut cx,
14790 );
14791}
14792
14793#[gpui::test]
14794async fn test_modification_reverts(cx: &mut TestAppContext) {
14795 init_test(cx, |_| {});
14796 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14797 let base_text = indoc! {r#"
14798 struct Row;
14799 struct Row1;
14800 struct Row2;
14801
14802 struct Row4;
14803 struct Row5;
14804 struct Row6;
14805
14806 struct Row8;
14807 struct Row9;
14808 struct Row10;"#};
14809
14810 // Modification hunks behave the same as the addition ones.
14811 assert_hunk_revert(
14812 indoc! {r#"struct Row;
14813 struct Row1;
14814 struct Row33;
14815 ˇ
14816 struct Row4;
14817 struct Row5;
14818 struct Row6;
14819 ˇ
14820 struct Row99;
14821 struct Row9;
14822 struct Row10;"#},
14823 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
14824 indoc! {r#"struct Row;
14825 struct Row1;
14826 struct Row33;
14827 ˇ
14828 struct Row4;
14829 struct Row5;
14830 struct Row6;
14831 ˇ
14832 struct Row99;
14833 struct Row9;
14834 struct Row10;"#},
14835 base_text,
14836 &mut cx,
14837 );
14838 assert_hunk_revert(
14839 indoc! {r#"struct Row;
14840 struct Row1;
14841 struct Row33;
14842 «ˇ
14843 struct Row4;
14844 struct» Row5;
14845 «struct Row6;
14846 ˇ»
14847 struct Row99;
14848 struct Row9;
14849 struct Row10;"#},
14850 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
14851 indoc! {r#"struct Row;
14852 struct Row1;
14853 struct Row33;
14854 «ˇ
14855 struct Row4;
14856 struct» Row5;
14857 «struct Row6;
14858 ˇ»
14859 struct Row99;
14860 struct Row9;
14861 struct Row10;"#},
14862 base_text,
14863 &mut cx,
14864 );
14865
14866 assert_hunk_revert(
14867 indoc! {r#"ˇstruct Row1.1;
14868 struct Row1;
14869 «ˇstr»uct Row22;
14870
14871 struct ˇRow44;
14872 struct Row5;
14873 struct «Rˇ»ow66;ˇ
14874
14875 «struˇ»ct Row88;
14876 struct Row9;
14877 struct Row1011;ˇ"#},
14878 vec![
14879 DiffHunkStatusKind::Modified,
14880 DiffHunkStatusKind::Modified,
14881 DiffHunkStatusKind::Modified,
14882 DiffHunkStatusKind::Modified,
14883 DiffHunkStatusKind::Modified,
14884 DiffHunkStatusKind::Modified,
14885 ],
14886 indoc! {r#"struct Row;
14887 ˇstruct Row1;
14888 struct Row2;
14889 ˇ
14890 struct Row4;
14891 ˇstruct Row5;
14892 struct Row6;
14893 ˇ
14894 struct Row8;
14895 ˇstruct Row9;
14896 struct Row10;ˇ"#},
14897 base_text,
14898 &mut cx,
14899 );
14900}
14901
14902#[gpui::test]
14903async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
14904 init_test(cx, |_| {});
14905 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14906 let base_text = indoc! {r#"
14907 one
14908
14909 two
14910 three
14911 "#};
14912
14913 cx.set_head_text(base_text);
14914 cx.set_state("\nˇ\n");
14915 cx.executor().run_until_parked();
14916 cx.update_editor(|editor, _window, cx| {
14917 editor.expand_selected_diff_hunks(cx);
14918 });
14919 cx.executor().run_until_parked();
14920 cx.update_editor(|editor, window, cx| {
14921 editor.backspace(&Default::default(), window, cx);
14922 });
14923 cx.run_until_parked();
14924 cx.assert_state_with_diff(
14925 indoc! {r#"
14926
14927 - two
14928 - threeˇ
14929 +
14930 "#}
14931 .to_string(),
14932 );
14933}
14934
14935#[gpui::test]
14936async fn test_deletion_reverts(cx: &mut TestAppContext) {
14937 init_test(cx, |_| {});
14938 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14939 let base_text = indoc! {r#"struct Row;
14940struct Row1;
14941struct Row2;
14942
14943struct Row4;
14944struct Row5;
14945struct Row6;
14946
14947struct Row8;
14948struct Row9;
14949struct Row10;"#};
14950
14951 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
14952 assert_hunk_revert(
14953 indoc! {r#"struct Row;
14954 struct Row2;
14955
14956 ˇstruct Row4;
14957 struct Row5;
14958 struct Row6;
14959 ˇ
14960 struct Row8;
14961 struct Row10;"#},
14962 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
14963 indoc! {r#"struct Row;
14964 struct Row2;
14965
14966 ˇstruct Row4;
14967 struct Row5;
14968 struct Row6;
14969 ˇ
14970 struct Row8;
14971 struct Row10;"#},
14972 base_text,
14973 &mut cx,
14974 );
14975 assert_hunk_revert(
14976 indoc! {r#"struct Row;
14977 struct Row2;
14978
14979 «ˇstruct Row4;
14980 struct» Row5;
14981 «struct Row6;
14982 ˇ»
14983 struct Row8;
14984 struct Row10;"#},
14985 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
14986 indoc! {r#"struct Row;
14987 struct Row2;
14988
14989 «ˇstruct Row4;
14990 struct» Row5;
14991 «struct Row6;
14992 ˇ»
14993 struct Row8;
14994 struct Row10;"#},
14995 base_text,
14996 &mut cx,
14997 );
14998
14999 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
15000 assert_hunk_revert(
15001 indoc! {r#"struct Row;
15002 ˇstruct Row2;
15003
15004 struct Row4;
15005 struct Row5;
15006 struct Row6;
15007
15008 struct Row8;ˇ
15009 struct Row10;"#},
15010 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
15011 indoc! {r#"struct Row;
15012 struct Row1;
15013 ˇstruct Row2;
15014
15015 struct Row4;
15016 struct Row5;
15017 struct Row6;
15018
15019 struct Row8;ˇ
15020 struct Row9;
15021 struct Row10;"#},
15022 base_text,
15023 &mut cx,
15024 );
15025 assert_hunk_revert(
15026 indoc! {r#"struct Row;
15027 struct Row2«ˇ;
15028 struct Row4;
15029 struct» Row5;
15030 «struct Row6;
15031
15032 struct Row8;ˇ»
15033 struct Row10;"#},
15034 vec![
15035 DiffHunkStatusKind::Deleted,
15036 DiffHunkStatusKind::Deleted,
15037 DiffHunkStatusKind::Deleted,
15038 ],
15039 indoc! {r#"struct Row;
15040 struct Row1;
15041 struct Row2«ˇ;
15042
15043 struct Row4;
15044 struct» Row5;
15045 «struct Row6;
15046
15047 struct Row8;ˇ»
15048 struct Row9;
15049 struct Row10;"#},
15050 base_text,
15051 &mut cx,
15052 );
15053}
15054
15055#[gpui::test]
15056async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
15057 init_test(cx, |_| {});
15058
15059 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
15060 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
15061 let base_text_3 =
15062 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
15063
15064 let text_1 = edit_first_char_of_every_line(base_text_1);
15065 let text_2 = edit_first_char_of_every_line(base_text_2);
15066 let text_3 = edit_first_char_of_every_line(base_text_3);
15067
15068 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
15069 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
15070 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
15071
15072 let multibuffer = cx.new(|cx| {
15073 let mut multibuffer = MultiBuffer::new(ReadWrite);
15074 multibuffer.push_excerpts(
15075 buffer_1.clone(),
15076 [
15077 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15078 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15079 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15080 ],
15081 cx,
15082 );
15083 multibuffer.push_excerpts(
15084 buffer_2.clone(),
15085 [
15086 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15087 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15088 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15089 ],
15090 cx,
15091 );
15092 multibuffer.push_excerpts(
15093 buffer_3.clone(),
15094 [
15095 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15096 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15097 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15098 ],
15099 cx,
15100 );
15101 multibuffer
15102 });
15103
15104 let fs = FakeFs::new(cx.executor());
15105 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
15106 let (editor, cx) = cx
15107 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
15108 editor.update_in(cx, |editor, _window, cx| {
15109 for (buffer, diff_base) in [
15110 (buffer_1.clone(), base_text_1),
15111 (buffer_2.clone(), base_text_2),
15112 (buffer_3.clone(), base_text_3),
15113 ] {
15114 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
15115 editor
15116 .buffer
15117 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
15118 }
15119 });
15120 cx.executor().run_until_parked();
15121
15122 editor.update_in(cx, |editor, window, cx| {
15123 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}");
15124 editor.select_all(&SelectAll, window, cx);
15125 editor.git_restore(&Default::default(), window, cx);
15126 });
15127 cx.executor().run_until_parked();
15128
15129 // When all ranges are selected, all buffer hunks are reverted.
15130 editor.update(cx, |editor, cx| {
15131 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");
15132 });
15133 buffer_1.update(cx, |buffer, _| {
15134 assert_eq!(buffer.text(), base_text_1);
15135 });
15136 buffer_2.update(cx, |buffer, _| {
15137 assert_eq!(buffer.text(), base_text_2);
15138 });
15139 buffer_3.update(cx, |buffer, _| {
15140 assert_eq!(buffer.text(), base_text_3);
15141 });
15142
15143 editor.update_in(cx, |editor, window, cx| {
15144 editor.undo(&Default::default(), window, cx);
15145 });
15146
15147 editor.update_in(cx, |editor, window, cx| {
15148 editor.change_selections(None, window, cx, |s| {
15149 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
15150 });
15151 editor.git_restore(&Default::default(), window, cx);
15152 });
15153
15154 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
15155 // but not affect buffer_2 and its related excerpts.
15156 editor.update(cx, |editor, cx| {
15157 assert_eq!(
15158 editor.text(cx),
15159 "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}"
15160 );
15161 });
15162 buffer_1.update(cx, |buffer, _| {
15163 assert_eq!(buffer.text(), base_text_1);
15164 });
15165 buffer_2.update(cx, |buffer, _| {
15166 assert_eq!(
15167 buffer.text(),
15168 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
15169 );
15170 });
15171 buffer_3.update(cx, |buffer, _| {
15172 assert_eq!(
15173 buffer.text(),
15174 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
15175 );
15176 });
15177
15178 fn edit_first_char_of_every_line(text: &str) -> String {
15179 text.split('\n')
15180 .map(|line| format!("X{}", &line[1..]))
15181 .collect::<Vec<_>>()
15182 .join("\n")
15183 }
15184}
15185
15186#[gpui::test]
15187async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) {
15188 init_test(cx, |_| {});
15189
15190 let cols = 4;
15191 let rows = 10;
15192 let sample_text_1 = sample_text(rows, cols, 'a');
15193 assert_eq!(
15194 sample_text_1,
15195 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
15196 );
15197 let sample_text_2 = sample_text(rows, cols, 'l');
15198 assert_eq!(
15199 sample_text_2,
15200 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
15201 );
15202 let sample_text_3 = sample_text(rows, cols, 'v');
15203 assert_eq!(
15204 sample_text_3,
15205 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
15206 );
15207
15208 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
15209 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
15210 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
15211
15212 let multi_buffer = cx.new(|cx| {
15213 let mut multibuffer = MultiBuffer::new(ReadWrite);
15214 multibuffer.push_excerpts(
15215 buffer_1.clone(),
15216 [
15217 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15218 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15219 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15220 ],
15221 cx,
15222 );
15223 multibuffer.push_excerpts(
15224 buffer_2.clone(),
15225 [
15226 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15227 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15228 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15229 ],
15230 cx,
15231 );
15232 multibuffer.push_excerpts(
15233 buffer_3.clone(),
15234 [
15235 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15236 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15237 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15238 ],
15239 cx,
15240 );
15241 multibuffer
15242 });
15243
15244 let fs = FakeFs::new(cx.executor());
15245 fs.insert_tree(
15246 "/a",
15247 json!({
15248 "main.rs": sample_text_1,
15249 "other.rs": sample_text_2,
15250 "lib.rs": sample_text_3,
15251 }),
15252 )
15253 .await;
15254 let project = Project::test(fs, ["/a".as_ref()], cx).await;
15255 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15256 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
15257 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
15258 Editor::new(
15259 EditorMode::full(),
15260 multi_buffer,
15261 Some(project.clone()),
15262 window,
15263 cx,
15264 )
15265 });
15266 let multibuffer_item_id = workspace
15267 .update(cx, |workspace, window, cx| {
15268 assert!(
15269 workspace.active_item(cx).is_none(),
15270 "active item should be None before the first item is added"
15271 );
15272 workspace.add_item_to_active_pane(
15273 Box::new(multi_buffer_editor.clone()),
15274 None,
15275 true,
15276 window,
15277 cx,
15278 );
15279 let active_item = workspace
15280 .active_item(cx)
15281 .expect("should have an active item after adding the multi buffer");
15282 assert!(
15283 !active_item.is_singleton(cx),
15284 "A multi buffer was expected to active after adding"
15285 );
15286 active_item.item_id()
15287 })
15288 .unwrap();
15289 cx.executor().run_until_parked();
15290
15291 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15292 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
15293 s.select_ranges(Some(1..2))
15294 });
15295 editor.open_excerpts(&OpenExcerpts, window, cx);
15296 });
15297 cx.executor().run_until_parked();
15298 let first_item_id = workspace
15299 .update(cx, |workspace, window, cx| {
15300 let active_item = workspace
15301 .active_item(cx)
15302 .expect("should have an active item after navigating into the 1st buffer");
15303 let first_item_id = active_item.item_id();
15304 assert_ne!(
15305 first_item_id, multibuffer_item_id,
15306 "Should navigate into the 1st buffer and activate it"
15307 );
15308 assert!(
15309 active_item.is_singleton(cx),
15310 "New active item should be a singleton buffer"
15311 );
15312 assert_eq!(
15313 active_item
15314 .act_as::<Editor>(cx)
15315 .expect("should have navigated into an editor for the 1st buffer")
15316 .read(cx)
15317 .text(cx),
15318 sample_text_1
15319 );
15320
15321 workspace
15322 .go_back(workspace.active_pane().downgrade(), window, cx)
15323 .detach_and_log_err(cx);
15324
15325 first_item_id
15326 })
15327 .unwrap();
15328 cx.executor().run_until_parked();
15329 workspace
15330 .update(cx, |workspace, _, cx| {
15331 let active_item = workspace
15332 .active_item(cx)
15333 .expect("should have an active item after navigating back");
15334 assert_eq!(
15335 active_item.item_id(),
15336 multibuffer_item_id,
15337 "Should navigate back to the multi buffer"
15338 );
15339 assert!(!active_item.is_singleton(cx));
15340 })
15341 .unwrap();
15342
15343 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15344 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
15345 s.select_ranges(Some(39..40))
15346 });
15347 editor.open_excerpts(&OpenExcerpts, window, cx);
15348 });
15349 cx.executor().run_until_parked();
15350 let second_item_id = workspace
15351 .update(cx, |workspace, window, cx| {
15352 let active_item = workspace
15353 .active_item(cx)
15354 .expect("should have an active item after navigating into the 2nd buffer");
15355 let second_item_id = active_item.item_id();
15356 assert_ne!(
15357 second_item_id, multibuffer_item_id,
15358 "Should navigate away from the multibuffer"
15359 );
15360 assert_ne!(
15361 second_item_id, first_item_id,
15362 "Should navigate into the 2nd buffer and activate it"
15363 );
15364 assert!(
15365 active_item.is_singleton(cx),
15366 "New active item should be a singleton buffer"
15367 );
15368 assert_eq!(
15369 active_item
15370 .act_as::<Editor>(cx)
15371 .expect("should have navigated into an editor")
15372 .read(cx)
15373 .text(cx),
15374 sample_text_2
15375 );
15376
15377 workspace
15378 .go_back(workspace.active_pane().downgrade(), window, cx)
15379 .detach_and_log_err(cx);
15380
15381 second_item_id
15382 })
15383 .unwrap();
15384 cx.executor().run_until_parked();
15385 workspace
15386 .update(cx, |workspace, _, cx| {
15387 let active_item = workspace
15388 .active_item(cx)
15389 .expect("should have an active item after navigating back from the 2nd buffer");
15390 assert_eq!(
15391 active_item.item_id(),
15392 multibuffer_item_id,
15393 "Should navigate back from the 2nd buffer to the multi buffer"
15394 );
15395 assert!(!active_item.is_singleton(cx));
15396 })
15397 .unwrap();
15398
15399 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15400 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
15401 s.select_ranges(Some(70..70))
15402 });
15403 editor.open_excerpts(&OpenExcerpts, window, cx);
15404 });
15405 cx.executor().run_until_parked();
15406 workspace
15407 .update(cx, |workspace, window, cx| {
15408 let active_item = workspace
15409 .active_item(cx)
15410 .expect("should have an active item after navigating into the 3rd buffer");
15411 let third_item_id = active_item.item_id();
15412 assert_ne!(
15413 third_item_id, multibuffer_item_id,
15414 "Should navigate into the 3rd buffer and activate it"
15415 );
15416 assert_ne!(third_item_id, first_item_id);
15417 assert_ne!(third_item_id, second_item_id);
15418 assert!(
15419 active_item.is_singleton(cx),
15420 "New active item should be a singleton buffer"
15421 );
15422 assert_eq!(
15423 active_item
15424 .act_as::<Editor>(cx)
15425 .expect("should have navigated into an editor")
15426 .read(cx)
15427 .text(cx),
15428 sample_text_3
15429 );
15430
15431 workspace
15432 .go_back(workspace.active_pane().downgrade(), window, cx)
15433 .detach_and_log_err(cx);
15434 })
15435 .unwrap();
15436 cx.executor().run_until_parked();
15437 workspace
15438 .update(cx, |workspace, _, cx| {
15439 let active_item = workspace
15440 .active_item(cx)
15441 .expect("should have an active item after navigating back from the 3rd buffer");
15442 assert_eq!(
15443 active_item.item_id(),
15444 multibuffer_item_id,
15445 "Should navigate back from the 3rd buffer to the multi buffer"
15446 );
15447 assert!(!active_item.is_singleton(cx));
15448 })
15449 .unwrap();
15450}
15451
15452#[gpui::test]
15453async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
15454 init_test(cx, |_| {});
15455
15456 let mut cx = EditorTestContext::new(cx).await;
15457
15458 let diff_base = r#"
15459 use some::mod;
15460
15461 const A: u32 = 42;
15462
15463 fn main() {
15464 println!("hello");
15465
15466 println!("world");
15467 }
15468 "#
15469 .unindent();
15470
15471 cx.set_state(
15472 &r#"
15473 use some::modified;
15474
15475 ˇ
15476 fn main() {
15477 println!("hello there");
15478
15479 println!("around the");
15480 println!("world");
15481 }
15482 "#
15483 .unindent(),
15484 );
15485
15486 cx.set_head_text(&diff_base);
15487 executor.run_until_parked();
15488
15489 cx.update_editor(|editor, window, cx| {
15490 editor.go_to_next_hunk(&GoToHunk, window, cx);
15491 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15492 });
15493 executor.run_until_parked();
15494 cx.assert_state_with_diff(
15495 r#"
15496 use some::modified;
15497
15498
15499 fn main() {
15500 - println!("hello");
15501 + ˇ println!("hello there");
15502
15503 println!("around the");
15504 println!("world");
15505 }
15506 "#
15507 .unindent(),
15508 );
15509
15510 cx.update_editor(|editor, window, cx| {
15511 for _ in 0..2 {
15512 editor.go_to_next_hunk(&GoToHunk, window, cx);
15513 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15514 }
15515 });
15516 executor.run_until_parked();
15517 cx.assert_state_with_diff(
15518 r#"
15519 - use some::mod;
15520 + ˇuse some::modified;
15521
15522
15523 fn main() {
15524 - println!("hello");
15525 + println!("hello there");
15526
15527 + println!("around the");
15528 println!("world");
15529 }
15530 "#
15531 .unindent(),
15532 );
15533
15534 cx.update_editor(|editor, window, cx| {
15535 editor.go_to_next_hunk(&GoToHunk, window, cx);
15536 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15537 });
15538 executor.run_until_parked();
15539 cx.assert_state_with_diff(
15540 r#"
15541 - use some::mod;
15542 + use some::modified;
15543
15544 - const A: u32 = 42;
15545 ˇ
15546 fn main() {
15547 - println!("hello");
15548 + println!("hello there");
15549
15550 + println!("around the");
15551 println!("world");
15552 }
15553 "#
15554 .unindent(),
15555 );
15556
15557 cx.update_editor(|editor, window, cx| {
15558 editor.cancel(&Cancel, window, cx);
15559 });
15560
15561 cx.assert_state_with_diff(
15562 r#"
15563 use some::modified;
15564
15565 ˇ
15566 fn main() {
15567 println!("hello there");
15568
15569 println!("around the");
15570 println!("world");
15571 }
15572 "#
15573 .unindent(),
15574 );
15575}
15576
15577#[gpui::test]
15578async fn test_diff_base_change_with_expanded_diff_hunks(
15579 executor: BackgroundExecutor,
15580 cx: &mut TestAppContext,
15581) {
15582 init_test(cx, |_| {});
15583
15584 let mut cx = EditorTestContext::new(cx).await;
15585
15586 let diff_base = r#"
15587 use some::mod1;
15588 use some::mod2;
15589
15590 const A: u32 = 42;
15591 const B: u32 = 42;
15592 const C: u32 = 42;
15593
15594 fn main() {
15595 println!("hello");
15596
15597 println!("world");
15598 }
15599 "#
15600 .unindent();
15601
15602 cx.set_state(
15603 &r#"
15604 use some::mod2;
15605
15606 const A: u32 = 42;
15607 const C: u32 = 42;
15608
15609 fn main(ˇ) {
15610 //println!("hello");
15611
15612 println!("world");
15613 //
15614 //
15615 }
15616 "#
15617 .unindent(),
15618 );
15619
15620 cx.set_head_text(&diff_base);
15621 executor.run_until_parked();
15622
15623 cx.update_editor(|editor, window, cx| {
15624 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15625 });
15626 executor.run_until_parked();
15627 cx.assert_state_with_diff(
15628 r#"
15629 - use some::mod1;
15630 use some::mod2;
15631
15632 const A: u32 = 42;
15633 - const B: u32 = 42;
15634 const C: u32 = 42;
15635
15636 fn main(ˇ) {
15637 - println!("hello");
15638 + //println!("hello");
15639
15640 println!("world");
15641 + //
15642 + //
15643 }
15644 "#
15645 .unindent(),
15646 );
15647
15648 cx.set_head_text("new diff base!");
15649 executor.run_until_parked();
15650 cx.assert_state_with_diff(
15651 r#"
15652 - new diff base!
15653 + use some::mod2;
15654 +
15655 + const A: u32 = 42;
15656 + const C: u32 = 42;
15657 +
15658 + fn main(ˇ) {
15659 + //println!("hello");
15660 +
15661 + println!("world");
15662 + //
15663 + //
15664 + }
15665 "#
15666 .unindent(),
15667 );
15668}
15669
15670#[gpui::test]
15671async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
15672 init_test(cx, |_| {});
15673
15674 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
15675 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
15676 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
15677 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
15678 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
15679 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
15680
15681 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
15682 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
15683 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
15684
15685 let multi_buffer = cx.new(|cx| {
15686 let mut multibuffer = MultiBuffer::new(ReadWrite);
15687 multibuffer.push_excerpts(
15688 buffer_1.clone(),
15689 [
15690 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15691 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15692 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
15693 ],
15694 cx,
15695 );
15696 multibuffer.push_excerpts(
15697 buffer_2.clone(),
15698 [
15699 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15700 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15701 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
15702 ],
15703 cx,
15704 );
15705 multibuffer.push_excerpts(
15706 buffer_3.clone(),
15707 [
15708 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15709 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15710 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
15711 ],
15712 cx,
15713 );
15714 multibuffer
15715 });
15716
15717 let editor =
15718 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
15719 editor
15720 .update(cx, |editor, _window, cx| {
15721 for (buffer, diff_base) in [
15722 (buffer_1.clone(), file_1_old),
15723 (buffer_2.clone(), file_2_old),
15724 (buffer_3.clone(), file_3_old),
15725 ] {
15726 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
15727 editor
15728 .buffer
15729 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
15730 }
15731 })
15732 .unwrap();
15733
15734 let mut cx = EditorTestContext::for_editor(editor, cx).await;
15735 cx.run_until_parked();
15736
15737 cx.assert_editor_state(
15738 &"
15739 ˇaaa
15740 ccc
15741 ddd
15742
15743 ggg
15744 hhh
15745
15746
15747 lll
15748 mmm
15749 NNN
15750
15751 qqq
15752 rrr
15753
15754 uuu
15755 111
15756 222
15757 333
15758
15759 666
15760 777
15761
15762 000
15763 !!!"
15764 .unindent(),
15765 );
15766
15767 cx.update_editor(|editor, window, cx| {
15768 editor.select_all(&SelectAll, window, cx);
15769 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15770 });
15771 cx.executor().run_until_parked();
15772
15773 cx.assert_state_with_diff(
15774 "
15775 «aaa
15776 - bbb
15777 ccc
15778 ddd
15779
15780 ggg
15781 hhh
15782
15783
15784 lll
15785 mmm
15786 - nnn
15787 + NNN
15788
15789 qqq
15790 rrr
15791
15792 uuu
15793 111
15794 222
15795 333
15796
15797 + 666
15798 777
15799
15800 000
15801 !!!ˇ»"
15802 .unindent(),
15803 );
15804}
15805
15806#[gpui::test]
15807async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
15808 init_test(cx, |_| {});
15809
15810 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
15811 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
15812
15813 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
15814 let multi_buffer = cx.new(|cx| {
15815 let mut multibuffer = MultiBuffer::new(ReadWrite);
15816 multibuffer.push_excerpts(
15817 buffer.clone(),
15818 [
15819 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
15820 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
15821 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
15822 ],
15823 cx,
15824 );
15825 multibuffer
15826 });
15827
15828 let editor =
15829 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
15830 editor
15831 .update(cx, |editor, _window, cx| {
15832 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
15833 editor
15834 .buffer
15835 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
15836 })
15837 .unwrap();
15838
15839 let mut cx = EditorTestContext::for_editor(editor, cx).await;
15840 cx.run_until_parked();
15841
15842 cx.update_editor(|editor, window, cx| {
15843 editor.expand_all_diff_hunks(&Default::default(), window, cx)
15844 });
15845 cx.executor().run_until_parked();
15846
15847 // When the start of a hunk coincides with the start of its excerpt,
15848 // the hunk is expanded. When the start of a a hunk is earlier than
15849 // the start of its excerpt, the hunk is not expanded.
15850 cx.assert_state_with_diff(
15851 "
15852 ˇaaa
15853 - bbb
15854 + BBB
15855
15856 - ddd
15857 - eee
15858 + DDD
15859 + EEE
15860 fff
15861
15862 iii
15863 "
15864 .unindent(),
15865 );
15866}
15867
15868#[gpui::test]
15869async fn test_edits_around_expanded_insertion_hunks(
15870 executor: BackgroundExecutor,
15871 cx: &mut TestAppContext,
15872) {
15873 init_test(cx, |_| {});
15874
15875 let mut cx = EditorTestContext::new(cx).await;
15876
15877 let diff_base = r#"
15878 use some::mod1;
15879 use some::mod2;
15880
15881 const A: u32 = 42;
15882
15883 fn main() {
15884 println!("hello");
15885
15886 println!("world");
15887 }
15888 "#
15889 .unindent();
15890 executor.run_until_parked();
15891 cx.set_state(
15892 &r#"
15893 use some::mod1;
15894 use some::mod2;
15895
15896 const A: u32 = 42;
15897 const B: u32 = 42;
15898 const C: u32 = 42;
15899 ˇ
15900
15901 fn main() {
15902 println!("hello");
15903
15904 println!("world");
15905 }
15906 "#
15907 .unindent(),
15908 );
15909
15910 cx.set_head_text(&diff_base);
15911 executor.run_until_parked();
15912
15913 cx.update_editor(|editor, window, cx| {
15914 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15915 });
15916 executor.run_until_parked();
15917
15918 cx.assert_state_with_diff(
15919 r#"
15920 use some::mod1;
15921 use some::mod2;
15922
15923 const A: u32 = 42;
15924 + const B: u32 = 42;
15925 + const C: u32 = 42;
15926 + ˇ
15927
15928 fn main() {
15929 println!("hello");
15930
15931 println!("world");
15932 }
15933 "#
15934 .unindent(),
15935 );
15936
15937 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
15938 executor.run_until_parked();
15939
15940 cx.assert_state_with_diff(
15941 r#"
15942 use some::mod1;
15943 use some::mod2;
15944
15945 const A: u32 = 42;
15946 + const B: u32 = 42;
15947 + const C: u32 = 42;
15948 + const D: u32 = 42;
15949 + ˇ
15950
15951 fn main() {
15952 println!("hello");
15953
15954 println!("world");
15955 }
15956 "#
15957 .unindent(),
15958 );
15959
15960 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
15961 executor.run_until_parked();
15962
15963 cx.assert_state_with_diff(
15964 r#"
15965 use some::mod1;
15966 use some::mod2;
15967
15968 const A: u32 = 42;
15969 + const B: u32 = 42;
15970 + const C: u32 = 42;
15971 + const D: u32 = 42;
15972 + const E: u32 = 42;
15973 + ˇ
15974
15975 fn main() {
15976 println!("hello");
15977
15978 println!("world");
15979 }
15980 "#
15981 .unindent(),
15982 );
15983
15984 cx.update_editor(|editor, window, cx| {
15985 editor.delete_line(&DeleteLine, window, cx);
15986 });
15987 executor.run_until_parked();
15988
15989 cx.assert_state_with_diff(
15990 r#"
15991 use some::mod1;
15992 use some::mod2;
15993
15994 const A: u32 = 42;
15995 + const B: u32 = 42;
15996 + const C: u32 = 42;
15997 + const D: u32 = 42;
15998 + const E: u32 = 42;
15999 ˇ
16000 fn main() {
16001 println!("hello");
16002
16003 println!("world");
16004 }
16005 "#
16006 .unindent(),
16007 );
16008
16009 cx.update_editor(|editor, window, cx| {
16010 editor.move_up(&MoveUp, window, cx);
16011 editor.delete_line(&DeleteLine, window, cx);
16012 editor.move_up(&MoveUp, window, cx);
16013 editor.delete_line(&DeleteLine, window, cx);
16014 editor.move_up(&MoveUp, window, cx);
16015 editor.delete_line(&DeleteLine, window, cx);
16016 });
16017 executor.run_until_parked();
16018 cx.assert_state_with_diff(
16019 r#"
16020 use some::mod1;
16021 use some::mod2;
16022
16023 const A: u32 = 42;
16024 + const B: u32 = 42;
16025 ˇ
16026 fn main() {
16027 println!("hello");
16028
16029 println!("world");
16030 }
16031 "#
16032 .unindent(),
16033 );
16034
16035 cx.update_editor(|editor, window, cx| {
16036 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
16037 editor.delete_line(&DeleteLine, window, cx);
16038 });
16039 executor.run_until_parked();
16040 cx.assert_state_with_diff(
16041 r#"
16042 ˇ
16043 fn main() {
16044 println!("hello");
16045
16046 println!("world");
16047 }
16048 "#
16049 .unindent(),
16050 );
16051}
16052
16053#[gpui::test]
16054async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
16055 init_test(cx, |_| {});
16056
16057 let mut cx = EditorTestContext::new(cx).await;
16058 cx.set_head_text(indoc! { "
16059 one
16060 two
16061 three
16062 four
16063 five
16064 "
16065 });
16066 cx.set_state(indoc! { "
16067 one
16068 ˇthree
16069 five
16070 "});
16071 cx.run_until_parked();
16072 cx.update_editor(|editor, window, cx| {
16073 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
16074 });
16075 cx.assert_state_with_diff(
16076 indoc! { "
16077 one
16078 - two
16079 ˇthree
16080 - four
16081 five
16082 "}
16083 .to_string(),
16084 );
16085 cx.update_editor(|editor, window, cx| {
16086 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
16087 });
16088
16089 cx.assert_state_with_diff(
16090 indoc! { "
16091 one
16092 ˇthree
16093 five
16094 "}
16095 .to_string(),
16096 );
16097
16098 cx.set_state(indoc! { "
16099 one
16100 ˇTWO
16101 three
16102 four
16103 five
16104 "});
16105 cx.run_until_parked();
16106 cx.update_editor(|editor, window, cx| {
16107 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
16108 });
16109
16110 cx.assert_state_with_diff(
16111 indoc! { "
16112 one
16113 - two
16114 + ˇTWO
16115 three
16116 four
16117 five
16118 "}
16119 .to_string(),
16120 );
16121 cx.update_editor(|editor, window, cx| {
16122 editor.move_up(&Default::default(), window, cx);
16123 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
16124 });
16125 cx.assert_state_with_diff(
16126 indoc! { "
16127 one
16128 ˇTWO
16129 three
16130 four
16131 five
16132 "}
16133 .to_string(),
16134 );
16135}
16136
16137#[gpui::test]
16138async fn test_edits_around_expanded_deletion_hunks(
16139 executor: BackgroundExecutor,
16140 cx: &mut TestAppContext,
16141) {
16142 init_test(cx, |_| {});
16143
16144 let mut cx = EditorTestContext::new(cx).await;
16145
16146 let diff_base = r#"
16147 use some::mod1;
16148 use some::mod2;
16149
16150 const A: u32 = 42;
16151 const B: u32 = 42;
16152 const C: u32 = 42;
16153
16154
16155 fn main() {
16156 println!("hello");
16157
16158 println!("world");
16159 }
16160 "#
16161 .unindent();
16162 executor.run_until_parked();
16163 cx.set_state(
16164 &r#"
16165 use some::mod1;
16166 use some::mod2;
16167
16168 ˇconst B: u32 = 42;
16169 const C: u32 = 42;
16170
16171
16172 fn main() {
16173 println!("hello");
16174
16175 println!("world");
16176 }
16177 "#
16178 .unindent(),
16179 );
16180
16181 cx.set_head_text(&diff_base);
16182 executor.run_until_parked();
16183
16184 cx.update_editor(|editor, window, cx| {
16185 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16186 });
16187 executor.run_until_parked();
16188
16189 cx.assert_state_with_diff(
16190 r#"
16191 use some::mod1;
16192 use some::mod2;
16193
16194 - const A: u32 = 42;
16195 ˇconst B: u32 = 42;
16196 const C: u32 = 42;
16197
16198
16199 fn main() {
16200 println!("hello");
16201
16202 println!("world");
16203 }
16204 "#
16205 .unindent(),
16206 );
16207
16208 cx.update_editor(|editor, window, cx| {
16209 editor.delete_line(&DeleteLine, window, cx);
16210 });
16211 executor.run_until_parked();
16212 cx.assert_state_with_diff(
16213 r#"
16214 use some::mod1;
16215 use some::mod2;
16216
16217 - const A: u32 = 42;
16218 - const B: u32 = 42;
16219 ˇconst C: u32 = 42;
16220
16221
16222 fn main() {
16223 println!("hello");
16224
16225 println!("world");
16226 }
16227 "#
16228 .unindent(),
16229 );
16230
16231 cx.update_editor(|editor, window, cx| {
16232 editor.delete_line(&DeleteLine, window, cx);
16233 });
16234 executor.run_until_parked();
16235 cx.assert_state_with_diff(
16236 r#"
16237 use some::mod1;
16238 use some::mod2;
16239
16240 - const A: u32 = 42;
16241 - const B: u32 = 42;
16242 - const C: u32 = 42;
16243 ˇ
16244
16245 fn main() {
16246 println!("hello");
16247
16248 println!("world");
16249 }
16250 "#
16251 .unindent(),
16252 );
16253
16254 cx.update_editor(|editor, window, cx| {
16255 editor.handle_input("replacement", window, cx);
16256 });
16257 executor.run_until_parked();
16258 cx.assert_state_with_diff(
16259 r#"
16260 use some::mod1;
16261 use some::mod2;
16262
16263 - const A: u32 = 42;
16264 - const B: u32 = 42;
16265 - const C: u32 = 42;
16266 -
16267 + replacementˇ
16268
16269 fn main() {
16270 println!("hello");
16271
16272 println!("world");
16273 }
16274 "#
16275 .unindent(),
16276 );
16277}
16278
16279#[gpui::test]
16280async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16281 init_test(cx, |_| {});
16282
16283 let mut cx = EditorTestContext::new(cx).await;
16284
16285 let base_text = r#"
16286 one
16287 two
16288 three
16289 four
16290 five
16291 "#
16292 .unindent();
16293 executor.run_until_parked();
16294 cx.set_state(
16295 &r#"
16296 one
16297 two
16298 fˇour
16299 five
16300 "#
16301 .unindent(),
16302 );
16303
16304 cx.set_head_text(&base_text);
16305 executor.run_until_parked();
16306
16307 cx.update_editor(|editor, window, cx| {
16308 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16309 });
16310 executor.run_until_parked();
16311
16312 cx.assert_state_with_diff(
16313 r#"
16314 one
16315 two
16316 - three
16317 fˇour
16318 five
16319 "#
16320 .unindent(),
16321 );
16322
16323 cx.update_editor(|editor, window, cx| {
16324 editor.backspace(&Backspace, window, cx);
16325 editor.backspace(&Backspace, window, cx);
16326 });
16327 executor.run_until_parked();
16328 cx.assert_state_with_diff(
16329 r#"
16330 one
16331 two
16332 - threeˇ
16333 - four
16334 + our
16335 five
16336 "#
16337 .unindent(),
16338 );
16339}
16340
16341#[gpui::test]
16342async fn test_edit_after_expanded_modification_hunk(
16343 executor: BackgroundExecutor,
16344 cx: &mut TestAppContext,
16345) {
16346 init_test(cx, |_| {});
16347
16348 let mut cx = EditorTestContext::new(cx).await;
16349
16350 let diff_base = r#"
16351 use some::mod1;
16352 use some::mod2;
16353
16354 const A: u32 = 42;
16355 const B: u32 = 42;
16356 const C: u32 = 42;
16357 const D: u32 = 42;
16358
16359
16360 fn main() {
16361 println!("hello");
16362
16363 println!("world");
16364 }"#
16365 .unindent();
16366
16367 cx.set_state(
16368 &r#"
16369 use some::mod1;
16370 use some::mod2;
16371
16372 const A: u32 = 42;
16373 const B: u32 = 42;
16374 const C: u32 = 43ˇ
16375 const D: u32 = 42;
16376
16377
16378 fn main() {
16379 println!("hello");
16380
16381 println!("world");
16382 }"#
16383 .unindent(),
16384 );
16385
16386 cx.set_head_text(&diff_base);
16387 executor.run_until_parked();
16388 cx.update_editor(|editor, window, cx| {
16389 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16390 });
16391 executor.run_until_parked();
16392
16393 cx.assert_state_with_diff(
16394 r#"
16395 use some::mod1;
16396 use some::mod2;
16397
16398 const A: u32 = 42;
16399 const B: u32 = 42;
16400 - const C: u32 = 42;
16401 + const C: u32 = 43ˇ
16402 const D: u32 = 42;
16403
16404
16405 fn main() {
16406 println!("hello");
16407
16408 println!("world");
16409 }"#
16410 .unindent(),
16411 );
16412
16413 cx.update_editor(|editor, window, cx| {
16414 editor.handle_input("\nnew_line\n", window, cx);
16415 });
16416 executor.run_until_parked();
16417
16418 cx.assert_state_with_diff(
16419 r#"
16420 use some::mod1;
16421 use some::mod2;
16422
16423 const A: u32 = 42;
16424 const B: u32 = 42;
16425 - const C: u32 = 42;
16426 + const C: u32 = 43
16427 + new_line
16428 + ˇ
16429 const D: u32 = 42;
16430
16431
16432 fn main() {
16433 println!("hello");
16434
16435 println!("world");
16436 }"#
16437 .unindent(),
16438 );
16439}
16440
16441#[gpui::test]
16442async fn test_stage_and_unstage_added_file_hunk(
16443 executor: BackgroundExecutor,
16444 cx: &mut TestAppContext,
16445) {
16446 init_test(cx, |_| {});
16447
16448 let mut cx = EditorTestContext::new(cx).await;
16449 cx.update_editor(|editor, _, cx| {
16450 editor.set_expand_all_diff_hunks(cx);
16451 });
16452
16453 let working_copy = r#"
16454 ˇfn main() {
16455 println!("hello, world!");
16456 }
16457 "#
16458 .unindent();
16459
16460 cx.set_state(&working_copy);
16461 executor.run_until_parked();
16462
16463 cx.assert_state_with_diff(
16464 r#"
16465 + ˇfn main() {
16466 + println!("hello, world!");
16467 + }
16468 "#
16469 .unindent(),
16470 );
16471 cx.assert_index_text(None);
16472
16473 cx.update_editor(|editor, window, cx| {
16474 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16475 });
16476 executor.run_until_parked();
16477 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
16478 cx.assert_state_with_diff(
16479 r#"
16480 + ˇfn main() {
16481 + println!("hello, world!");
16482 + }
16483 "#
16484 .unindent(),
16485 );
16486
16487 cx.update_editor(|editor, window, cx| {
16488 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16489 });
16490 executor.run_until_parked();
16491 cx.assert_index_text(None);
16492}
16493
16494async fn setup_indent_guides_editor(
16495 text: &str,
16496 cx: &mut TestAppContext,
16497) -> (BufferId, EditorTestContext) {
16498 init_test(cx, |_| {});
16499
16500 let mut cx = EditorTestContext::new(cx).await;
16501
16502 let buffer_id = cx.update_editor(|editor, window, cx| {
16503 editor.set_text(text, window, cx);
16504 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
16505
16506 buffer_ids[0]
16507 });
16508
16509 (buffer_id, cx)
16510}
16511
16512fn assert_indent_guides(
16513 range: Range<u32>,
16514 expected: Vec<IndentGuide>,
16515 active_indices: Option<Vec<usize>>,
16516 cx: &mut EditorTestContext,
16517) {
16518 let indent_guides = cx.update_editor(|editor, window, cx| {
16519 let snapshot = editor.snapshot(window, cx).display_snapshot;
16520 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
16521 editor,
16522 MultiBufferRow(range.start)..MultiBufferRow(range.end),
16523 true,
16524 &snapshot,
16525 cx,
16526 );
16527
16528 indent_guides.sort_by(|a, b| {
16529 a.depth.cmp(&b.depth).then(
16530 a.start_row
16531 .cmp(&b.start_row)
16532 .then(a.end_row.cmp(&b.end_row)),
16533 )
16534 });
16535 indent_guides
16536 });
16537
16538 if let Some(expected) = active_indices {
16539 let active_indices = cx.update_editor(|editor, window, cx| {
16540 let snapshot = editor.snapshot(window, cx).display_snapshot;
16541 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
16542 });
16543
16544 assert_eq!(
16545 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
16546 expected,
16547 "Active indent guide indices do not match"
16548 );
16549 }
16550
16551 assert_eq!(indent_guides, expected, "Indent guides do not match");
16552}
16553
16554fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
16555 IndentGuide {
16556 buffer_id,
16557 start_row: MultiBufferRow(start_row),
16558 end_row: MultiBufferRow(end_row),
16559 depth,
16560 tab_size: 4,
16561 settings: IndentGuideSettings {
16562 enabled: true,
16563 line_width: 1,
16564 active_line_width: 1,
16565 ..Default::default()
16566 },
16567 }
16568}
16569
16570#[gpui::test]
16571async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
16572 let (buffer_id, mut cx) = setup_indent_guides_editor(
16573 &"
16574 fn main() {
16575 let a = 1;
16576 }"
16577 .unindent(),
16578 cx,
16579 )
16580 .await;
16581
16582 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
16583}
16584
16585#[gpui::test]
16586async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
16587 let (buffer_id, mut cx) = setup_indent_guides_editor(
16588 &"
16589 fn main() {
16590 let a = 1;
16591 let b = 2;
16592 }"
16593 .unindent(),
16594 cx,
16595 )
16596 .await;
16597
16598 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
16599}
16600
16601#[gpui::test]
16602async fn test_indent_guide_nested(cx: &mut TestAppContext) {
16603 let (buffer_id, mut cx) = setup_indent_guides_editor(
16604 &"
16605 fn main() {
16606 let a = 1;
16607 if a == 3 {
16608 let b = 2;
16609 } else {
16610 let c = 3;
16611 }
16612 }"
16613 .unindent(),
16614 cx,
16615 )
16616 .await;
16617
16618 assert_indent_guides(
16619 0..8,
16620 vec![
16621 indent_guide(buffer_id, 1, 6, 0),
16622 indent_guide(buffer_id, 3, 3, 1),
16623 indent_guide(buffer_id, 5, 5, 1),
16624 ],
16625 None,
16626 &mut cx,
16627 );
16628}
16629
16630#[gpui::test]
16631async fn test_indent_guide_tab(cx: &mut TestAppContext) {
16632 let (buffer_id, mut cx) = setup_indent_guides_editor(
16633 &"
16634 fn main() {
16635 let a = 1;
16636 let b = 2;
16637 let c = 3;
16638 }"
16639 .unindent(),
16640 cx,
16641 )
16642 .await;
16643
16644 assert_indent_guides(
16645 0..5,
16646 vec![
16647 indent_guide(buffer_id, 1, 3, 0),
16648 indent_guide(buffer_id, 2, 2, 1),
16649 ],
16650 None,
16651 &mut cx,
16652 );
16653}
16654
16655#[gpui::test]
16656async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
16657 let (buffer_id, mut cx) = setup_indent_guides_editor(
16658 &"
16659 fn main() {
16660 let a = 1;
16661
16662 let c = 3;
16663 }"
16664 .unindent(),
16665 cx,
16666 )
16667 .await;
16668
16669 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
16670}
16671
16672#[gpui::test]
16673async fn test_indent_guide_complex(cx: &mut TestAppContext) {
16674 let (buffer_id, mut cx) = setup_indent_guides_editor(
16675 &"
16676 fn main() {
16677 let a = 1;
16678
16679 let c = 3;
16680
16681 if a == 3 {
16682 let b = 2;
16683 } else {
16684 let c = 3;
16685 }
16686 }"
16687 .unindent(),
16688 cx,
16689 )
16690 .await;
16691
16692 assert_indent_guides(
16693 0..11,
16694 vec![
16695 indent_guide(buffer_id, 1, 9, 0),
16696 indent_guide(buffer_id, 6, 6, 1),
16697 indent_guide(buffer_id, 8, 8, 1),
16698 ],
16699 None,
16700 &mut cx,
16701 );
16702}
16703
16704#[gpui::test]
16705async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
16706 let (buffer_id, mut cx) = setup_indent_guides_editor(
16707 &"
16708 fn main() {
16709 let a = 1;
16710
16711 let c = 3;
16712
16713 if a == 3 {
16714 let b = 2;
16715 } else {
16716 let c = 3;
16717 }
16718 }"
16719 .unindent(),
16720 cx,
16721 )
16722 .await;
16723
16724 assert_indent_guides(
16725 1..11,
16726 vec![
16727 indent_guide(buffer_id, 1, 9, 0),
16728 indent_guide(buffer_id, 6, 6, 1),
16729 indent_guide(buffer_id, 8, 8, 1),
16730 ],
16731 None,
16732 &mut cx,
16733 );
16734}
16735
16736#[gpui::test]
16737async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
16738 let (buffer_id, mut cx) = setup_indent_guides_editor(
16739 &"
16740 fn main() {
16741 let a = 1;
16742
16743 let c = 3;
16744
16745 if a == 3 {
16746 let b = 2;
16747 } else {
16748 let c = 3;
16749 }
16750 }"
16751 .unindent(),
16752 cx,
16753 )
16754 .await;
16755
16756 assert_indent_guides(
16757 1..10,
16758 vec![
16759 indent_guide(buffer_id, 1, 9, 0),
16760 indent_guide(buffer_id, 6, 6, 1),
16761 indent_guide(buffer_id, 8, 8, 1),
16762 ],
16763 None,
16764 &mut cx,
16765 );
16766}
16767
16768#[gpui::test]
16769async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
16770 let (buffer_id, mut cx) = setup_indent_guides_editor(
16771 &"
16772 block1
16773 block2
16774 block3
16775 block4
16776 block2
16777 block1
16778 block1"
16779 .unindent(),
16780 cx,
16781 )
16782 .await;
16783
16784 assert_indent_guides(
16785 1..10,
16786 vec![
16787 indent_guide(buffer_id, 1, 4, 0),
16788 indent_guide(buffer_id, 2, 3, 1),
16789 indent_guide(buffer_id, 3, 3, 2),
16790 ],
16791 None,
16792 &mut cx,
16793 );
16794}
16795
16796#[gpui::test]
16797async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
16798 let (buffer_id, mut cx) = setup_indent_guides_editor(
16799 &"
16800 block1
16801 block2
16802 block3
16803
16804 block1
16805 block1"
16806 .unindent(),
16807 cx,
16808 )
16809 .await;
16810
16811 assert_indent_guides(
16812 0..6,
16813 vec![
16814 indent_guide(buffer_id, 1, 2, 0),
16815 indent_guide(buffer_id, 2, 2, 1),
16816 ],
16817 None,
16818 &mut cx,
16819 );
16820}
16821
16822#[gpui::test]
16823async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
16824 let (buffer_id, mut cx) = setup_indent_guides_editor(
16825 &"
16826 block1
16827
16828
16829
16830 block2
16831 "
16832 .unindent(),
16833 cx,
16834 )
16835 .await;
16836
16837 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
16838}
16839
16840#[gpui::test]
16841async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
16842 let (buffer_id, mut cx) = setup_indent_guides_editor(
16843 &"
16844 def a:
16845 \tb = 3
16846 \tif True:
16847 \t\tc = 4
16848 \t\td = 5
16849 \tprint(b)
16850 "
16851 .unindent(),
16852 cx,
16853 )
16854 .await;
16855
16856 assert_indent_guides(
16857 0..6,
16858 vec![
16859 indent_guide(buffer_id, 1, 5, 0),
16860 indent_guide(buffer_id, 3, 4, 1),
16861 ],
16862 None,
16863 &mut cx,
16864 );
16865}
16866
16867#[gpui::test]
16868async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
16869 let (buffer_id, mut cx) = setup_indent_guides_editor(
16870 &"
16871 fn main() {
16872 let a = 1;
16873 }"
16874 .unindent(),
16875 cx,
16876 )
16877 .await;
16878
16879 cx.update_editor(|editor, window, cx| {
16880 editor.change_selections(None, window, cx, |s| {
16881 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
16882 });
16883 });
16884
16885 assert_indent_guides(
16886 0..3,
16887 vec![indent_guide(buffer_id, 1, 1, 0)],
16888 Some(vec![0]),
16889 &mut cx,
16890 );
16891}
16892
16893#[gpui::test]
16894async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
16895 let (buffer_id, mut cx) = setup_indent_guides_editor(
16896 &"
16897 fn main() {
16898 if 1 == 2 {
16899 let a = 1;
16900 }
16901 }"
16902 .unindent(),
16903 cx,
16904 )
16905 .await;
16906
16907 cx.update_editor(|editor, window, cx| {
16908 editor.change_selections(None, window, cx, |s| {
16909 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
16910 });
16911 });
16912
16913 assert_indent_guides(
16914 0..4,
16915 vec![
16916 indent_guide(buffer_id, 1, 3, 0),
16917 indent_guide(buffer_id, 2, 2, 1),
16918 ],
16919 Some(vec![1]),
16920 &mut cx,
16921 );
16922
16923 cx.update_editor(|editor, window, cx| {
16924 editor.change_selections(None, window, cx, |s| {
16925 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
16926 });
16927 });
16928
16929 assert_indent_guides(
16930 0..4,
16931 vec![
16932 indent_guide(buffer_id, 1, 3, 0),
16933 indent_guide(buffer_id, 2, 2, 1),
16934 ],
16935 Some(vec![1]),
16936 &mut cx,
16937 );
16938
16939 cx.update_editor(|editor, window, cx| {
16940 editor.change_selections(None, window, cx, |s| {
16941 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
16942 });
16943 });
16944
16945 assert_indent_guides(
16946 0..4,
16947 vec![
16948 indent_guide(buffer_id, 1, 3, 0),
16949 indent_guide(buffer_id, 2, 2, 1),
16950 ],
16951 Some(vec![0]),
16952 &mut cx,
16953 );
16954}
16955
16956#[gpui::test]
16957async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
16958 let (buffer_id, mut cx) = setup_indent_guides_editor(
16959 &"
16960 fn main() {
16961 let a = 1;
16962
16963 let b = 2;
16964 }"
16965 .unindent(),
16966 cx,
16967 )
16968 .await;
16969
16970 cx.update_editor(|editor, window, cx| {
16971 editor.change_selections(None, window, cx, |s| {
16972 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
16973 });
16974 });
16975
16976 assert_indent_guides(
16977 0..5,
16978 vec![indent_guide(buffer_id, 1, 3, 0)],
16979 Some(vec![0]),
16980 &mut cx,
16981 );
16982}
16983
16984#[gpui::test]
16985async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
16986 let (buffer_id, mut cx) = setup_indent_guides_editor(
16987 &"
16988 def m:
16989 a = 1
16990 pass"
16991 .unindent(),
16992 cx,
16993 )
16994 .await;
16995
16996 cx.update_editor(|editor, window, cx| {
16997 editor.change_selections(None, window, cx, |s| {
16998 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
16999 });
17000 });
17001
17002 assert_indent_guides(
17003 0..3,
17004 vec![indent_guide(buffer_id, 1, 2, 0)],
17005 Some(vec![0]),
17006 &mut cx,
17007 );
17008}
17009
17010#[gpui::test]
17011async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
17012 init_test(cx, |_| {});
17013 let mut cx = EditorTestContext::new(cx).await;
17014 let text = indoc! {
17015 "
17016 impl A {
17017 fn b() {
17018 0;
17019 3;
17020 5;
17021 6;
17022 7;
17023 }
17024 }
17025 "
17026 };
17027 let base_text = indoc! {
17028 "
17029 impl A {
17030 fn b() {
17031 0;
17032 1;
17033 2;
17034 3;
17035 4;
17036 }
17037 fn c() {
17038 5;
17039 6;
17040 7;
17041 }
17042 }
17043 "
17044 };
17045
17046 cx.update_editor(|editor, window, cx| {
17047 editor.set_text(text, window, cx);
17048
17049 editor.buffer().update(cx, |multibuffer, cx| {
17050 let buffer = multibuffer.as_singleton().unwrap();
17051 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
17052
17053 multibuffer.set_all_diff_hunks_expanded(cx);
17054 multibuffer.add_diff(diff, cx);
17055
17056 buffer.read(cx).remote_id()
17057 })
17058 });
17059 cx.run_until_parked();
17060
17061 cx.assert_state_with_diff(
17062 indoc! { "
17063 impl A {
17064 fn b() {
17065 0;
17066 - 1;
17067 - 2;
17068 3;
17069 - 4;
17070 - }
17071 - fn c() {
17072 5;
17073 6;
17074 7;
17075 }
17076 }
17077 ˇ"
17078 }
17079 .to_string(),
17080 );
17081
17082 let mut actual_guides = cx.update_editor(|editor, window, cx| {
17083 editor
17084 .snapshot(window, cx)
17085 .buffer_snapshot
17086 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
17087 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
17088 .collect::<Vec<_>>()
17089 });
17090 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
17091 assert_eq!(
17092 actual_guides,
17093 vec![
17094 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
17095 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
17096 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
17097 ]
17098 );
17099}
17100
17101#[gpui::test]
17102async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17103 init_test(cx, |_| {});
17104 let mut cx = EditorTestContext::new(cx).await;
17105
17106 let diff_base = r#"
17107 a
17108 b
17109 c
17110 "#
17111 .unindent();
17112
17113 cx.set_state(
17114 &r#"
17115 ˇA
17116 b
17117 C
17118 "#
17119 .unindent(),
17120 );
17121 cx.set_head_text(&diff_base);
17122 cx.update_editor(|editor, window, cx| {
17123 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17124 });
17125 executor.run_until_parked();
17126
17127 let both_hunks_expanded = r#"
17128 - a
17129 + ˇA
17130 b
17131 - c
17132 + C
17133 "#
17134 .unindent();
17135
17136 cx.assert_state_with_diff(both_hunks_expanded.clone());
17137
17138 let hunk_ranges = cx.update_editor(|editor, window, cx| {
17139 let snapshot = editor.snapshot(window, cx);
17140 let hunks = editor
17141 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
17142 .collect::<Vec<_>>();
17143 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
17144 let buffer_id = hunks[0].buffer_id;
17145 hunks
17146 .into_iter()
17147 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
17148 .collect::<Vec<_>>()
17149 });
17150 assert_eq!(hunk_ranges.len(), 2);
17151
17152 cx.update_editor(|editor, _, cx| {
17153 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
17154 });
17155 executor.run_until_parked();
17156
17157 let second_hunk_expanded = r#"
17158 ˇA
17159 b
17160 - c
17161 + C
17162 "#
17163 .unindent();
17164
17165 cx.assert_state_with_diff(second_hunk_expanded);
17166
17167 cx.update_editor(|editor, _, cx| {
17168 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
17169 });
17170 executor.run_until_parked();
17171
17172 cx.assert_state_with_diff(both_hunks_expanded.clone());
17173
17174 cx.update_editor(|editor, _, cx| {
17175 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
17176 });
17177 executor.run_until_parked();
17178
17179 let first_hunk_expanded = r#"
17180 - a
17181 + ˇA
17182 b
17183 C
17184 "#
17185 .unindent();
17186
17187 cx.assert_state_with_diff(first_hunk_expanded);
17188
17189 cx.update_editor(|editor, _, cx| {
17190 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
17191 });
17192 executor.run_until_parked();
17193
17194 cx.assert_state_with_diff(both_hunks_expanded);
17195
17196 cx.set_state(
17197 &r#"
17198 ˇA
17199 b
17200 "#
17201 .unindent(),
17202 );
17203 cx.run_until_parked();
17204
17205 // TODO this cursor position seems bad
17206 cx.assert_state_with_diff(
17207 r#"
17208 - ˇa
17209 + A
17210 b
17211 "#
17212 .unindent(),
17213 );
17214
17215 cx.update_editor(|editor, window, cx| {
17216 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17217 });
17218
17219 cx.assert_state_with_diff(
17220 r#"
17221 - ˇa
17222 + A
17223 b
17224 - c
17225 "#
17226 .unindent(),
17227 );
17228
17229 let hunk_ranges = cx.update_editor(|editor, window, cx| {
17230 let snapshot = editor.snapshot(window, cx);
17231 let hunks = editor
17232 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
17233 .collect::<Vec<_>>();
17234 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
17235 let buffer_id = hunks[0].buffer_id;
17236 hunks
17237 .into_iter()
17238 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
17239 .collect::<Vec<_>>()
17240 });
17241 assert_eq!(hunk_ranges.len(), 2);
17242
17243 cx.update_editor(|editor, _, cx| {
17244 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
17245 });
17246 executor.run_until_parked();
17247
17248 cx.assert_state_with_diff(
17249 r#"
17250 - ˇa
17251 + A
17252 b
17253 "#
17254 .unindent(),
17255 );
17256}
17257
17258#[gpui::test]
17259async fn test_toggle_deletion_hunk_at_start_of_file(
17260 executor: BackgroundExecutor,
17261 cx: &mut TestAppContext,
17262) {
17263 init_test(cx, |_| {});
17264 let mut cx = EditorTestContext::new(cx).await;
17265
17266 let diff_base = r#"
17267 a
17268 b
17269 c
17270 "#
17271 .unindent();
17272
17273 cx.set_state(
17274 &r#"
17275 ˇb
17276 c
17277 "#
17278 .unindent(),
17279 );
17280 cx.set_head_text(&diff_base);
17281 cx.update_editor(|editor, window, cx| {
17282 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17283 });
17284 executor.run_until_parked();
17285
17286 let hunk_expanded = r#"
17287 - a
17288 ˇb
17289 c
17290 "#
17291 .unindent();
17292
17293 cx.assert_state_with_diff(hunk_expanded.clone());
17294
17295 let hunk_ranges = cx.update_editor(|editor, window, cx| {
17296 let snapshot = editor.snapshot(window, cx);
17297 let hunks = editor
17298 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
17299 .collect::<Vec<_>>();
17300 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
17301 let buffer_id = hunks[0].buffer_id;
17302 hunks
17303 .into_iter()
17304 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
17305 .collect::<Vec<_>>()
17306 });
17307 assert_eq!(hunk_ranges.len(), 1);
17308
17309 cx.update_editor(|editor, _, cx| {
17310 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
17311 });
17312 executor.run_until_parked();
17313
17314 let hunk_collapsed = r#"
17315 ˇb
17316 c
17317 "#
17318 .unindent();
17319
17320 cx.assert_state_with_diff(hunk_collapsed);
17321
17322 cx.update_editor(|editor, _, cx| {
17323 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
17324 });
17325 executor.run_until_parked();
17326
17327 cx.assert_state_with_diff(hunk_expanded.clone());
17328}
17329
17330#[gpui::test]
17331async fn test_display_diff_hunks(cx: &mut TestAppContext) {
17332 init_test(cx, |_| {});
17333
17334 let fs = FakeFs::new(cx.executor());
17335 fs.insert_tree(
17336 path!("/test"),
17337 json!({
17338 ".git": {},
17339 "file-1": "ONE\n",
17340 "file-2": "TWO\n",
17341 "file-3": "THREE\n",
17342 }),
17343 )
17344 .await;
17345
17346 fs.set_head_for_repo(
17347 path!("/test/.git").as_ref(),
17348 &[
17349 ("file-1".into(), "one\n".into()),
17350 ("file-2".into(), "two\n".into()),
17351 ("file-3".into(), "three\n".into()),
17352 ],
17353 );
17354
17355 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
17356 let mut buffers = vec![];
17357 for i in 1..=3 {
17358 let buffer = project
17359 .update(cx, |project, cx| {
17360 let path = format!(path!("/test/file-{}"), i);
17361 project.open_local_buffer(path, cx)
17362 })
17363 .await
17364 .unwrap();
17365 buffers.push(buffer);
17366 }
17367
17368 let multibuffer = cx.new(|cx| {
17369 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
17370 multibuffer.set_all_diff_hunks_expanded(cx);
17371 for buffer in &buffers {
17372 let snapshot = buffer.read(cx).snapshot();
17373 multibuffer.set_excerpts_for_path(
17374 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
17375 buffer.clone(),
17376 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
17377 DEFAULT_MULTIBUFFER_CONTEXT,
17378 cx,
17379 );
17380 }
17381 multibuffer
17382 });
17383
17384 let editor = cx.add_window(|window, cx| {
17385 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
17386 });
17387 cx.run_until_parked();
17388
17389 let snapshot = editor
17390 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
17391 .unwrap();
17392 let hunks = snapshot
17393 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
17394 .map(|hunk| match hunk {
17395 DisplayDiffHunk::Unfolded {
17396 display_row_range, ..
17397 } => display_row_range,
17398 DisplayDiffHunk::Folded { .. } => unreachable!(),
17399 })
17400 .collect::<Vec<_>>();
17401 assert_eq!(
17402 hunks,
17403 [
17404 DisplayRow(2)..DisplayRow(4),
17405 DisplayRow(7)..DisplayRow(9),
17406 DisplayRow(12)..DisplayRow(14),
17407 ]
17408 );
17409}
17410
17411#[gpui::test]
17412async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
17413 init_test(cx, |_| {});
17414
17415 let mut cx = EditorTestContext::new(cx).await;
17416 cx.set_head_text(indoc! { "
17417 one
17418 two
17419 three
17420 four
17421 five
17422 "
17423 });
17424 cx.set_index_text(indoc! { "
17425 one
17426 two
17427 three
17428 four
17429 five
17430 "
17431 });
17432 cx.set_state(indoc! {"
17433 one
17434 TWO
17435 ˇTHREE
17436 FOUR
17437 five
17438 "});
17439 cx.run_until_parked();
17440 cx.update_editor(|editor, window, cx| {
17441 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
17442 });
17443 cx.run_until_parked();
17444 cx.assert_index_text(Some(indoc! {"
17445 one
17446 TWO
17447 THREE
17448 FOUR
17449 five
17450 "}));
17451 cx.set_state(indoc! { "
17452 one
17453 TWO
17454 ˇTHREE-HUNDRED
17455 FOUR
17456 five
17457 "});
17458 cx.run_until_parked();
17459 cx.update_editor(|editor, window, cx| {
17460 let snapshot = editor.snapshot(window, cx);
17461 let hunks = editor
17462 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
17463 .collect::<Vec<_>>();
17464 assert_eq!(hunks.len(), 1);
17465 assert_eq!(
17466 hunks[0].status(),
17467 DiffHunkStatus {
17468 kind: DiffHunkStatusKind::Modified,
17469 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
17470 }
17471 );
17472
17473 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
17474 });
17475 cx.run_until_parked();
17476 cx.assert_index_text(Some(indoc! {"
17477 one
17478 TWO
17479 THREE-HUNDRED
17480 FOUR
17481 five
17482 "}));
17483}
17484
17485#[gpui::test]
17486fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
17487 init_test(cx, |_| {});
17488
17489 let editor = cx.add_window(|window, cx| {
17490 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
17491 build_editor(buffer, window, cx)
17492 });
17493
17494 let render_args = Arc::new(Mutex::new(None));
17495 let snapshot = editor
17496 .update(cx, |editor, window, cx| {
17497 let snapshot = editor.buffer().read(cx).snapshot(cx);
17498 let range =
17499 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
17500
17501 struct RenderArgs {
17502 row: MultiBufferRow,
17503 folded: bool,
17504 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
17505 }
17506
17507 let crease = Crease::inline(
17508 range,
17509 FoldPlaceholder::test(),
17510 {
17511 let toggle_callback = render_args.clone();
17512 move |row, folded, callback, _window, _cx| {
17513 *toggle_callback.lock() = Some(RenderArgs {
17514 row,
17515 folded,
17516 callback,
17517 });
17518 div()
17519 }
17520 },
17521 |_row, _folded, _window, _cx| div(),
17522 );
17523
17524 editor.insert_creases(Some(crease), cx);
17525 let snapshot = editor.snapshot(window, cx);
17526 let _div = snapshot.render_crease_toggle(
17527 MultiBufferRow(1),
17528 false,
17529 cx.entity().clone(),
17530 window,
17531 cx,
17532 );
17533 snapshot
17534 })
17535 .unwrap();
17536
17537 let render_args = render_args.lock().take().unwrap();
17538 assert_eq!(render_args.row, MultiBufferRow(1));
17539 assert!(!render_args.folded);
17540 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
17541
17542 cx.update_window(*editor, |_, window, cx| {
17543 (render_args.callback)(true, window, cx)
17544 })
17545 .unwrap();
17546 let snapshot = editor
17547 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
17548 .unwrap();
17549 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
17550
17551 cx.update_window(*editor, |_, window, cx| {
17552 (render_args.callback)(false, window, cx)
17553 })
17554 .unwrap();
17555 let snapshot = editor
17556 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
17557 .unwrap();
17558 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
17559}
17560
17561#[gpui::test]
17562async fn test_input_text(cx: &mut TestAppContext) {
17563 init_test(cx, |_| {});
17564 let mut cx = EditorTestContext::new(cx).await;
17565
17566 cx.set_state(
17567 &r#"ˇone
17568 two
17569
17570 three
17571 fourˇ
17572 five
17573
17574 siˇx"#
17575 .unindent(),
17576 );
17577
17578 cx.dispatch_action(HandleInput(String::new()));
17579 cx.assert_editor_state(
17580 &r#"ˇone
17581 two
17582
17583 three
17584 fourˇ
17585 five
17586
17587 siˇx"#
17588 .unindent(),
17589 );
17590
17591 cx.dispatch_action(HandleInput("AAAA".to_string()));
17592 cx.assert_editor_state(
17593 &r#"AAAAˇone
17594 two
17595
17596 three
17597 fourAAAAˇ
17598 five
17599
17600 siAAAAˇx"#
17601 .unindent(),
17602 );
17603}
17604
17605#[gpui::test]
17606async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
17607 init_test(cx, |_| {});
17608
17609 let mut cx = EditorTestContext::new(cx).await;
17610 cx.set_state(
17611 r#"let foo = 1;
17612let foo = 2;
17613let foo = 3;
17614let fooˇ = 4;
17615let foo = 5;
17616let foo = 6;
17617let foo = 7;
17618let foo = 8;
17619let foo = 9;
17620let foo = 10;
17621let foo = 11;
17622let foo = 12;
17623let foo = 13;
17624let foo = 14;
17625let foo = 15;"#,
17626 );
17627
17628 cx.update_editor(|e, window, cx| {
17629 assert_eq!(
17630 e.next_scroll_position,
17631 NextScrollCursorCenterTopBottom::Center,
17632 "Default next scroll direction is center",
17633 );
17634
17635 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17636 assert_eq!(
17637 e.next_scroll_position,
17638 NextScrollCursorCenterTopBottom::Top,
17639 "After center, next scroll direction should be top",
17640 );
17641
17642 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17643 assert_eq!(
17644 e.next_scroll_position,
17645 NextScrollCursorCenterTopBottom::Bottom,
17646 "After top, next scroll direction should be bottom",
17647 );
17648
17649 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17650 assert_eq!(
17651 e.next_scroll_position,
17652 NextScrollCursorCenterTopBottom::Center,
17653 "After bottom, scrolling should start over",
17654 );
17655
17656 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17657 assert_eq!(
17658 e.next_scroll_position,
17659 NextScrollCursorCenterTopBottom::Top,
17660 "Scrolling continues if retriggered fast enough"
17661 );
17662 });
17663
17664 cx.executor()
17665 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
17666 cx.executor().run_until_parked();
17667 cx.update_editor(|e, _, _| {
17668 assert_eq!(
17669 e.next_scroll_position,
17670 NextScrollCursorCenterTopBottom::Center,
17671 "If scrolling is not triggered fast enough, it should reset"
17672 );
17673 });
17674}
17675
17676#[gpui::test]
17677async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
17678 init_test(cx, |_| {});
17679 let mut cx = EditorLspTestContext::new_rust(
17680 lsp::ServerCapabilities {
17681 definition_provider: Some(lsp::OneOf::Left(true)),
17682 references_provider: Some(lsp::OneOf::Left(true)),
17683 ..lsp::ServerCapabilities::default()
17684 },
17685 cx,
17686 )
17687 .await;
17688
17689 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
17690 let go_to_definition = cx
17691 .lsp
17692 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
17693 move |params, _| async move {
17694 if empty_go_to_definition {
17695 Ok(None)
17696 } else {
17697 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
17698 uri: params.text_document_position_params.text_document.uri,
17699 range: lsp::Range::new(
17700 lsp::Position::new(4, 3),
17701 lsp::Position::new(4, 6),
17702 ),
17703 })))
17704 }
17705 },
17706 );
17707 let references = cx
17708 .lsp
17709 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
17710 Ok(Some(vec![lsp::Location {
17711 uri: params.text_document_position.text_document.uri,
17712 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
17713 }]))
17714 });
17715 (go_to_definition, references)
17716 };
17717
17718 cx.set_state(
17719 &r#"fn one() {
17720 let mut a = ˇtwo();
17721 }
17722
17723 fn two() {}"#
17724 .unindent(),
17725 );
17726 set_up_lsp_handlers(false, &mut cx);
17727 let navigated = cx
17728 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
17729 .await
17730 .expect("Failed to navigate to definition");
17731 assert_eq!(
17732 navigated,
17733 Navigated::Yes,
17734 "Should have navigated to definition from the GetDefinition response"
17735 );
17736 cx.assert_editor_state(
17737 &r#"fn one() {
17738 let mut a = two();
17739 }
17740
17741 fn «twoˇ»() {}"#
17742 .unindent(),
17743 );
17744
17745 let editors = cx.update_workspace(|workspace, _, cx| {
17746 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
17747 });
17748 cx.update_editor(|_, _, test_editor_cx| {
17749 assert_eq!(
17750 editors.len(),
17751 1,
17752 "Initially, only one, test, editor should be open in the workspace"
17753 );
17754 assert_eq!(
17755 test_editor_cx.entity(),
17756 editors.last().expect("Asserted len is 1").clone()
17757 );
17758 });
17759
17760 set_up_lsp_handlers(true, &mut cx);
17761 let navigated = cx
17762 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
17763 .await
17764 .expect("Failed to navigate to lookup references");
17765 assert_eq!(
17766 navigated,
17767 Navigated::Yes,
17768 "Should have navigated to references as a fallback after empty GoToDefinition response"
17769 );
17770 // We should not change the selections in the existing file,
17771 // if opening another milti buffer with the references
17772 cx.assert_editor_state(
17773 &r#"fn one() {
17774 let mut a = two();
17775 }
17776
17777 fn «twoˇ»() {}"#
17778 .unindent(),
17779 );
17780 let editors = cx.update_workspace(|workspace, _, cx| {
17781 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
17782 });
17783 cx.update_editor(|_, _, test_editor_cx| {
17784 assert_eq!(
17785 editors.len(),
17786 2,
17787 "After falling back to references search, we open a new editor with the results"
17788 );
17789 let references_fallback_text = editors
17790 .into_iter()
17791 .find(|new_editor| *new_editor != test_editor_cx.entity())
17792 .expect("Should have one non-test editor now")
17793 .read(test_editor_cx)
17794 .text(test_editor_cx);
17795 assert_eq!(
17796 references_fallback_text, "fn one() {\n let mut a = two();\n}",
17797 "Should use the range from the references response and not the GoToDefinition one"
17798 );
17799 });
17800}
17801
17802#[gpui::test]
17803async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
17804 init_test(cx, |_| {});
17805 cx.update(|cx| {
17806 let mut editor_settings = EditorSettings::get_global(cx).clone();
17807 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
17808 EditorSettings::override_global(editor_settings, cx);
17809 });
17810 let mut cx = EditorLspTestContext::new_rust(
17811 lsp::ServerCapabilities {
17812 definition_provider: Some(lsp::OneOf::Left(true)),
17813 references_provider: Some(lsp::OneOf::Left(true)),
17814 ..lsp::ServerCapabilities::default()
17815 },
17816 cx,
17817 )
17818 .await;
17819 let original_state = r#"fn one() {
17820 let mut a = ˇtwo();
17821 }
17822
17823 fn two() {}"#
17824 .unindent();
17825 cx.set_state(&original_state);
17826
17827 let mut go_to_definition = cx
17828 .lsp
17829 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
17830 move |_, _| async move { Ok(None) },
17831 );
17832 let _references = cx
17833 .lsp
17834 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
17835 panic!("Should not call for references with no go to definition fallback")
17836 });
17837
17838 let navigated = cx
17839 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
17840 .await
17841 .expect("Failed to navigate to lookup references");
17842 go_to_definition
17843 .next()
17844 .await
17845 .expect("Should have called the go_to_definition handler");
17846
17847 assert_eq!(
17848 navigated,
17849 Navigated::No,
17850 "Should have navigated to references as a fallback after empty GoToDefinition response"
17851 );
17852 cx.assert_editor_state(&original_state);
17853 let editors = cx.update_workspace(|workspace, _, cx| {
17854 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
17855 });
17856 cx.update_editor(|_, _, _| {
17857 assert_eq!(
17858 editors.len(),
17859 1,
17860 "After unsuccessful fallback, no other editor should have been opened"
17861 );
17862 });
17863}
17864
17865#[gpui::test]
17866async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
17867 init_test(cx, |_| {});
17868
17869 let language = Arc::new(Language::new(
17870 LanguageConfig::default(),
17871 Some(tree_sitter_rust::LANGUAGE.into()),
17872 ));
17873
17874 let text = r#"
17875 #[cfg(test)]
17876 mod tests() {
17877 #[test]
17878 fn runnable_1() {
17879 let a = 1;
17880 }
17881
17882 #[test]
17883 fn runnable_2() {
17884 let a = 1;
17885 let b = 2;
17886 }
17887 }
17888 "#
17889 .unindent();
17890
17891 let fs = FakeFs::new(cx.executor());
17892 fs.insert_file("/file.rs", Default::default()).await;
17893
17894 let project = Project::test(fs, ["/a".as_ref()], cx).await;
17895 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17896 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17897 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
17898 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
17899
17900 let editor = cx.new_window_entity(|window, cx| {
17901 Editor::new(
17902 EditorMode::full(),
17903 multi_buffer,
17904 Some(project.clone()),
17905 window,
17906 cx,
17907 )
17908 });
17909
17910 editor.update_in(cx, |editor, window, cx| {
17911 let snapshot = editor.buffer().read(cx).snapshot(cx);
17912 editor.tasks.insert(
17913 (buffer.read(cx).remote_id(), 3),
17914 RunnableTasks {
17915 templates: vec![],
17916 offset: snapshot.anchor_before(43),
17917 column: 0,
17918 extra_variables: HashMap::default(),
17919 context_range: BufferOffset(43)..BufferOffset(85),
17920 },
17921 );
17922 editor.tasks.insert(
17923 (buffer.read(cx).remote_id(), 8),
17924 RunnableTasks {
17925 templates: vec![],
17926 offset: snapshot.anchor_before(86),
17927 column: 0,
17928 extra_variables: HashMap::default(),
17929 context_range: BufferOffset(86)..BufferOffset(191),
17930 },
17931 );
17932
17933 // Test finding task when cursor is inside function body
17934 editor.change_selections(None, window, cx, |s| {
17935 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
17936 });
17937 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
17938 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
17939
17940 // Test finding task when cursor is on function name
17941 editor.change_selections(None, window, cx, |s| {
17942 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
17943 });
17944 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
17945 assert_eq!(row, 8, "Should find task when cursor is on function name");
17946 });
17947}
17948
17949#[gpui::test]
17950async fn test_folding_buffers(cx: &mut TestAppContext) {
17951 init_test(cx, |_| {});
17952
17953 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
17954 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
17955 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
17956
17957 let fs = FakeFs::new(cx.executor());
17958 fs.insert_tree(
17959 path!("/a"),
17960 json!({
17961 "first.rs": sample_text_1,
17962 "second.rs": sample_text_2,
17963 "third.rs": sample_text_3,
17964 }),
17965 )
17966 .await;
17967 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17968 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17969 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17970 let worktree = project.update(cx, |project, cx| {
17971 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
17972 assert_eq!(worktrees.len(), 1);
17973 worktrees.pop().unwrap()
17974 });
17975 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
17976
17977 let buffer_1 = project
17978 .update(cx, |project, cx| {
17979 project.open_buffer((worktree_id, "first.rs"), cx)
17980 })
17981 .await
17982 .unwrap();
17983 let buffer_2 = project
17984 .update(cx, |project, cx| {
17985 project.open_buffer((worktree_id, "second.rs"), cx)
17986 })
17987 .await
17988 .unwrap();
17989 let buffer_3 = project
17990 .update(cx, |project, cx| {
17991 project.open_buffer((worktree_id, "third.rs"), cx)
17992 })
17993 .await
17994 .unwrap();
17995
17996 let multi_buffer = cx.new(|cx| {
17997 let mut multi_buffer = MultiBuffer::new(ReadWrite);
17998 multi_buffer.push_excerpts(
17999 buffer_1.clone(),
18000 [
18001 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18002 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18003 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18004 ],
18005 cx,
18006 );
18007 multi_buffer.push_excerpts(
18008 buffer_2.clone(),
18009 [
18010 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18011 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18012 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18013 ],
18014 cx,
18015 );
18016 multi_buffer.push_excerpts(
18017 buffer_3.clone(),
18018 [
18019 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18020 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18021 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18022 ],
18023 cx,
18024 );
18025 multi_buffer
18026 });
18027 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18028 Editor::new(
18029 EditorMode::full(),
18030 multi_buffer.clone(),
18031 Some(project.clone()),
18032 window,
18033 cx,
18034 )
18035 });
18036
18037 assert_eq!(
18038 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18039 "\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",
18040 );
18041
18042 multi_buffer_editor.update(cx, |editor, cx| {
18043 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
18044 });
18045 assert_eq!(
18046 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18047 "\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",
18048 "After folding the first buffer, its text should not be displayed"
18049 );
18050
18051 multi_buffer_editor.update(cx, |editor, cx| {
18052 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
18053 });
18054 assert_eq!(
18055 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18056 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
18057 "After folding the second buffer, its text should not be displayed"
18058 );
18059
18060 multi_buffer_editor.update(cx, |editor, cx| {
18061 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
18062 });
18063 assert_eq!(
18064 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18065 "\n\n\n\n\n",
18066 "After folding the third buffer, its text should not be displayed"
18067 );
18068
18069 // Emulate selection inside the fold logic, that should work
18070 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18071 editor
18072 .snapshot(window, cx)
18073 .next_line_boundary(Point::new(0, 4));
18074 });
18075
18076 multi_buffer_editor.update(cx, |editor, cx| {
18077 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
18078 });
18079 assert_eq!(
18080 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18081 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
18082 "After unfolding the second buffer, its text should be displayed"
18083 );
18084
18085 // Typing inside of buffer 1 causes that buffer to be unfolded.
18086 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18087 assert_eq!(
18088 multi_buffer
18089 .read(cx)
18090 .snapshot(cx)
18091 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
18092 .collect::<String>(),
18093 "bbbb"
18094 );
18095 editor.change_selections(None, window, cx, |selections| {
18096 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
18097 });
18098 editor.handle_input("B", window, cx);
18099 });
18100
18101 assert_eq!(
18102 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18103 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
18104 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
18105 );
18106
18107 multi_buffer_editor.update(cx, |editor, cx| {
18108 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
18109 });
18110 assert_eq!(
18111 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18112 "\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",
18113 "After unfolding the all buffers, all original text should be displayed"
18114 );
18115}
18116
18117#[gpui::test]
18118async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
18119 init_test(cx, |_| {});
18120
18121 let sample_text_1 = "1111\n2222\n3333".to_string();
18122 let sample_text_2 = "4444\n5555\n6666".to_string();
18123 let sample_text_3 = "7777\n8888\n9999".to_string();
18124
18125 let fs = FakeFs::new(cx.executor());
18126 fs.insert_tree(
18127 path!("/a"),
18128 json!({
18129 "first.rs": sample_text_1,
18130 "second.rs": sample_text_2,
18131 "third.rs": sample_text_3,
18132 }),
18133 )
18134 .await;
18135 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18136 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18137 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18138 let worktree = project.update(cx, |project, cx| {
18139 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
18140 assert_eq!(worktrees.len(), 1);
18141 worktrees.pop().unwrap()
18142 });
18143 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
18144
18145 let buffer_1 = project
18146 .update(cx, |project, cx| {
18147 project.open_buffer((worktree_id, "first.rs"), cx)
18148 })
18149 .await
18150 .unwrap();
18151 let buffer_2 = project
18152 .update(cx, |project, cx| {
18153 project.open_buffer((worktree_id, "second.rs"), cx)
18154 })
18155 .await
18156 .unwrap();
18157 let buffer_3 = project
18158 .update(cx, |project, cx| {
18159 project.open_buffer((worktree_id, "third.rs"), cx)
18160 })
18161 .await
18162 .unwrap();
18163
18164 let multi_buffer = cx.new(|cx| {
18165 let mut multi_buffer = MultiBuffer::new(ReadWrite);
18166 multi_buffer.push_excerpts(
18167 buffer_1.clone(),
18168 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
18169 cx,
18170 );
18171 multi_buffer.push_excerpts(
18172 buffer_2.clone(),
18173 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
18174 cx,
18175 );
18176 multi_buffer.push_excerpts(
18177 buffer_3.clone(),
18178 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
18179 cx,
18180 );
18181 multi_buffer
18182 });
18183
18184 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18185 Editor::new(
18186 EditorMode::full(),
18187 multi_buffer,
18188 Some(project.clone()),
18189 window,
18190 cx,
18191 )
18192 });
18193
18194 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
18195 assert_eq!(
18196 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18197 full_text,
18198 );
18199
18200 multi_buffer_editor.update(cx, |editor, cx| {
18201 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
18202 });
18203 assert_eq!(
18204 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18205 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
18206 "After folding the first buffer, its text should not be displayed"
18207 );
18208
18209 multi_buffer_editor.update(cx, |editor, cx| {
18210 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
18211 });
18212
18213 assert_eq!(
18214 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18215 "\n\n\n\n\n\n7777\n8888\n9999",
18216 "After folding the second buffer, its text should not be displayed"
18217 );
18218
18219 multi_buffer_editor.update(cx, |editor, cx| {
18220 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
18221 });
18222 assert_eq!(
18223 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18224 "\n\n\n\n\n",
18225 "After folding the third buffer, its text should not be displayed"
18226 );
18227
18228 multi_buffer_editor.update(cx, |editor, cx| {
18229 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
18230 });
18231 assert_eq!(
18232 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18233 "\n\n\n\n4444\n5555\n6666\n\n",
18234 "After unfolding the second buffer, its text should be displayed"
18235 );
18236
18237 multi_buffer_editor.update(cx, |editor, cx| {
18238 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
18239 });
18240 assert_eq!(
18241 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18242 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
18243 "After unfolding the first buffer, its text should be displayed"
18244 );
18245
18246 multi_buffer_editor.update(cx, |editor, cx| {
18247 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
18248 });
18249 assert_eq!(
18250 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18251 full_text,
18252 "After unfolding all buffers, all original text should be displayed"
18253 );
18254}
18255
18256#[gpui::test]
18257async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
18258 init_test(cx, |_| {});
18259
18260 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
18261
18262 let fs = FakeFs::new(cx.executor());
18263 fs.insert_tree(
18264 path!("/a"),
18265 json!({
18266 "main.rs": sample_text,
18267 }),
18268 )
18269 .await;
18270 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18271 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18272 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18273 let worktree = project.update(cx, |project, cx| {
18274 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
18275 assert_eq!(worktrees.len(), 1);
18276 worktrees.pop().unwrap()
18277 });
18278 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
18279
18280 let buffer_1 = project
18281 .update(cx, |project, cx| {
18282 project.open_buffer((worktree_id, "main.rs"), cx)
18283 })
18284 .await
18285 .unwrap();
18286
18287 let multi_buffer = cx.new(|cx| {
18288 let mut multi_buffer = MultiBuffer::new(ReadWrite);
18289 multi_buffer.push_excerpts(
18290 buffer_1.clone(),
18291 [ExcerptRange::new(
18292 Point::new(0, 0)
18293 ..Point::new(
18294 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
18295 0,
18296 ),
18297 )],
18298 cx,
18299 );
18300 multi_buffer
18301 });
18302 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18303 Editor::new(
18304 EditorMode::full(),
18305 multi_buffer,
18306 Some(project.clone()),
18307 window,
18308 cx,
18309 )
18310 });
18311
18312 let selection_range = Point::new(1, 0)..Point::new(2, 0);
18313 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18314 enum TestHighlight {}
18315 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
18316 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
18317 editor.highlight_text::<TestHighlight>(
18318 vec![highlight_range.clone()],
18319 HighlightStyle::color(Hsla::green()),
18320 cx,
18321 );
18322 editor.change_selections(None, window, cx, |s| s.select_ranges(Some(highlight_range)));
18323 });
18324
18325 let full_text = format!("\n\n{sample_text}");
18326 assert_eq!(
18327 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18328 full_text,
18329 );
18330}
18331
18332#[gpui::test]
18333async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
18334 init_test(cx, |_| {});
18335 cx.update(|cx| {
18336 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
18337 "keymaps/default-linux.json",
18338 cx,
18339 )
18340 .unwrap();
18341 cx.bind_keys(default_key_bindings);
18342 });
18343
18344 let (editor, cx) = cx.add_window_view(|window, cx| {
18345 let multi_buffer = MultiBuffer::build_multi(
18346 [
18347 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
18348 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
18349 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
18350 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
18351 ],
18352 cx,
18353 );
18354 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
18355
18356 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
18357 // fold all but the second buffer, so that we test navigating between two
18358 // adjacent folded buffers, as well as folded buffers at the start and
18359 // end the multibuffer
18360 editor.fold_buffer(buffer_ids[0], cx);
18361 editor.fold_buffer(buffer_ids[2], cx);
18362 editor.fold_buffer(buffer_ids[3], cx);
18363
18364 editor
18365 });
18366 cx.simulate_resize(size(px(1000.), px(1000.)));
18367
18368 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
18369 cx.assert_excerpts_with_selections(indoc! {"
18370 [EXCERPT]
18371 ˇ[FOLDED]
18372 [EXCERPT]
18373 a1
18374 b1
18375 [EXCERPT]
18376 [FOLDED]
18377 [EXCERPT]
18378 [FOLDED]
18379 "
18380 });
18381 cx.simulate_keystroke("down");
18382 cx.assert_excerpts_with_selections(indoc! {"
18383 [EXCERPT]
18384 [FOLDED]
18385 [EXCERPT]
18386 ˇa1
18387 b1
18388 [EXCERPT]
18389 [FOLDED]
18390 [EXCERPT]
18391 [FOLDED]
18392 "
18393 });
18394 cx.simulate_keystroke("down");
18395 cx.assert_excerpts_with_selections(indoc! {"
18396 [EXCERPT]
18397 [FOLDED]
18398 [EXCERPT]
18399 a1
18400 ˇb1
18401 [EXCERPT]
18402 [FOLDED]
18403 [EXCERPT]
18404 [FOLDED]
18405 "
18406 });
18407 cx.simulate_keystroke("down");
18408 cx.assert_excerpts_with_selections(indoc! {"
18409 [EXCERPT]
18410 [FOLDED]
18411 [EXCERPT]
18412 a1
18413 b1
18414 ˇ[EXCERPT]
18415 [FOLDED]
18416 [EXCERPT]
18417 [FOLDED]
18418 "
18419 });
18420 cx.simulate_keystroke("down");
18421 cx.assert_excerpts_with_selections(indoc! {"
18422 [EXCERPT]
18423 [FOLDED]
18424 [EXCERPT]
18425 a1
18426 b1
18427 [EXCERPT]
18428 ˇ[FOLDED]
18429 [EXCERPT]
18430 [FOLDED]
18431 "
18432 });
18433 for _ in 0..5 {
18434 cx.simulate_keystroke("down");
18435 cx.assert_excerpts_with_selections(indoc! {"
18436 [EXCERPT]
18437 [FOLDED]
18438 [EXCERPT]
18439 a1
18440 b1
18441 [EXCERPT]
18442 [FOLDED]
18443 [EXCERPT]
18444 ˇ[FOLDED]
18445 "
18446 });
18447 }
18448
18449 cx.simulate_keystroke("up");
18450 cx.assert_excerpts_with_selections(indoc! {"
18451 [EXCERPT]
18452 [FOLDED]
18453 [EXCERPT]
18454 a1
18455 b1
18456 [EXCERPT]
18457 ˇ[FOLDED]
18458 [EXCERPT]
18459 [FOLDED]
18460 "
18461 });
18462 cx.simulate_keystroke("up");
18463 cx.assert_excerpts_with_selections(indoc! {"
18464 [EXCERPT]
18465 [FOLDED]
18466 [EXCERPT]
18467 a1
18468 b1
18469 ˇ[EXCERPT]
18470 [FOLDED]
18471 [EXCERPT]
18472 [FOLDED]
18473 "
18474 });
18475 cx.simulate_keystroke("up");
18476 cx.assert_excerpts_with_selections(indoc! {"
18477 [EXCERPT]
18478 [FOLDED]
18479 [EXCERPT]
18480 a1
18481 ˇb1
18482 [EXCERPT]
18483 [FOLDED]
18484 [EXCERPT]
18485 [FOLDED]
18486 "
18487 });
18488 cx.simulate_keystroke("up");
18489 cx.assert_excerpts_with_selections(indoc! {"
18490 [EXCERPT]
18491 [FOLDED]
18492 [EXCERPT]
18493 ˇa1
18494 b1
18495 [EXCERPT]
18496 [FOLDED]
18497 [EXCERPT]
18498 [FOLDED]
18499 "
18500 });
18501 for _ in 0..5 {
18502 cx.simulate_keystroke("up");
18503 cx.assert_excerpts_with_selections(indoc! {"
18504 [EXCERPT]
18505 ˇ[FOLDED]
18506 [EXCERPT]
18507 a1
18508 b1
18509 [EXCERPT]
18510 [FOLDED]
18511 [EXCERPT]
18512 [FOLDED]
18513 "
18514 });
18515 }
18516}
18517
18518#[gpui::test]
18519async fn test_inline_completion_text(cx: &mut TestAppContext) {
18520 init_test(cx, |_| {});
18521
18522 // Simple insertion
18523 assert_highlighted_edits(
18524 "Hello, world!",
18525 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
18526 true,
18527 cx,
18528 |highlighted_edits, cx| {
18529 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
18530 assert_eq!(highlighted_edits.highlights.len(), 1);
18531 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
18532 assert_eq!(
18533 highlighted_edits.highlights[0].1.background_color,
18534 Some(cx.theme().status().created_background)
18535 );
18536 },
18537 )
18538 .await;
18539
18540 // Replacement
18541 assert_highlighted_edits(
18542 "This is a test.",
18543 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
18544 false,
18545 cx,
18546 |highlighted_edits, cx| {
18547 assert_eq!(highlighted_edits.text, "That is a test.");
18548 assert_eq!(highlighted_edits.highlights.len(), 1);
18549 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
18550 assert_eq!(
18551 highlighted_edits.highlights[0].1.background_color,
18552 Some(cx.theme().status().created_background)
18553 );
18554 },
18555 )
18556 .await;
18557
18558 // Multiple edits
18559 assert_highlighted_edits(
18560 "Hello, world!",
18561 vec![
18562 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
18563 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
18564 ],
18565 false,
18566 cx,
18567 |highlighted_edits, cx| {
18568 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
18569 assert_eq!(highlighted_edits.highlights.len(), 2);
18570 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
18571 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
18572 assert_eq!(
18573 highlighted_edits.highlights[0].1.background_color,
18574 Some(cx.theme().status().created_background)
18575 );
18576 assert_eq!(
18577 highlighted_edits.highlights[1].1.background_color,
18578 Some(cx.theme().status().created_background)
18579 );
18580 },
18581 )
18582 .await;
18583
18584 // Multiple lines with edits
18585 assert_highlighted_edits(
18586 "First line\nSecond line\nThird line\nFourth line",
18587 vec![
18588 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
18589 (
18590 Point::new(2, 0)..Point::new(2, 10),
18591 "New third line".to_string(),
18592 ),
18593 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
18594 ],
18595 false,
18596 cx,
18597 |highlighted_edits, cx| {
18598 assert_eq!(
18599 highlighted_edits.text,
18600 "Second modified\nNew third line\nFourth updated line"
18601 );
18602 assert_eq!(highlighted_edits.highlights.len(), 3);
18603 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
18604 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
18605 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
18606 for highlight in &highlighted_edits.highlights {
18607 assert_eq!(
18608 highlight.1.background_color,
18609 Some(cx.theme().status().created_background)
18610 );
18611 }
18612 },
18613 )
18614 .await;
18615}
18616
18617#[gpui::test]
18618async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
18619 init_test(cx, |_| {});
18620
18621 // Deletion
18622 assert_highlighted_edits(
18623 "Hello, world!",
18624 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
18625 true,
18626 cx,
18627 |highlighted_edits, cx| {
18628 assert_eq!(highlighted_edits.text, "Hello, world!");
18629 assert_eq!(highlighted_edits.highlights.len(), 1);
18630 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
18631 assert_eq!(
18632 highlighted_edits.highlights[0].1.background_color,
18633 Some(cx.theme().status().deleted_background)
18634 );
18635 },
18636 )
18637 .await;
18638
18639 // Insertion
18640 assert_highlighted_edits(
18641 "Hello, world!",
18642 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
18643 true,
18644 cx,
18645 |highlighted_edits, cx| {
18646 assert_eq!(highlighted_edits.highlights.len(), 1);
18647 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
18648 assert_eq!(
18649 highlighted_edits.highlights[0].1.background_color,
18650 Some(cx.theme().status().created_background)
18651 );
18652 },
18653 )
18654 .await;
18655}
18656
18657async fn assert_highlighted_edits(
18658 text: &str,
18659 edits: Vec<(Range<Point>, String)>,
18660 include_deletions: bool,
18661 cx: &mut TestAppContext,
18662 assertion_fn: impl Fn(HighlightedText, &App),
18663) {
18664 let window = cx.add_window(|window, cx| {
18665 let buffer = MultiBuffer::build_simple(text, cx);
18666 Editor::new(EditorMode::full(), buffer, None, window, cx)
18667 });
18668 let cx = &mut VisualTestContext::from_window(*window, cx);
18669
18670 let (buffer, snapshot) = window
18671 .update(cx, |editor, _window, cx| {
18672 (
18673 editor.buffer().clone(),
18674 editor.buffer().read(cx).snapshot(cx),
18675 )
18676 })
18677 .unwrap();
18678
18679 let edits = edits
18680 .into_iter()
18681 .map(|(range, edit)| {
18682 (
18683 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
18684 edit,
18685 )
18686 })
18687 .collect::<Vec<_>>();
18688
18689 let text_anchor_edits = edits
18690 .clone()
18691 .into_iter()
18692 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
18693 .collect::<Vec<_>>();
18694
18695 let edit_preview = window
18696 .update(cx, |_, _window, cx| {
18697 buffer
18698 .read(cx)
18699 .as_singleton()
18700 .unwrap()
18701 .read(cx)
18702 .preview_edits(text_anchor_edits.into(), cx)
18703 })
18704 .unwrap()
18705 .await;
18706
18707 cx.update(|_window, cx| {
18708 let highlighted_edits = inline_completion_edit_text(
18709 &snapshot.as_singleton().unwrap().2,
18710 &edits,
18711 &edit_preview,
18712 include_deletions,
18713 cx,
18714 );
18715 assertion_fn(highlighted_edits, cx)
18716 });
18717}
18718
18719#[track_caller]
18720fn assert_breakpoint(
18721 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
18722 path: &Arc<Path>,
18723 expected: Vec<(u32, Breakpoint)>,
18724) {
18725 if expected.len() == 0usize {
18726 assert!(!breakpoints.contains_key(path), "{}", path.display());
18727 } else {
18728 let mut breakpoint = breakpoints
18729 .get(path)
18730 .unwrap()
18731 .into_iter()
18732 .map(|breakpoint| {
18733 (
18734 breakpoint.row,
18735 Breakpoint {
18736 message: breakpoint.message.clone(),
18737 state: breakpoint.state,
18738 condition: breakpoint.condition.clone(),
18739 hit_condition: breakpoint.hit_condition.clone(),
18740 },
18741 )
18742 })
18743 .collect::<Vec<_>>();
18744
18745 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
18746
18747 assert_eq!(expected, breakpoint);
18748 }
18749}
18750
18751fn add_log_breakpoint_at_cursor(
18752 editor: &mut Editor,
18753 log_message: &str,
18754 window: &mut Window,
18755 cx: &mut Context<Editor>,
18756) {
18757 let (anchor, bp) = editor
18758 .breakpoints_at_cursors(window, cx)
18759 .first()
18760 .and_then(|(anchor, bp)| {
18761 if let Some(bp) = bp {
18762 Some((*anchor, bp.clone()))
18763 } else {
18764 None
18765 }
18766 })
18767 .unwrap_or_else(|| {
18768 let cursor_position: Point = editor.selections.newest(cx).head();
18769
18770 let breakpoint_position = editor
18771 .snapshot(window, cx)
18772 .display_snapshot
18773 .buffer_snapshot
18774 .anchor_before(Point::new(cursor_position.row, 0));
18775
18776 (breakpoint_position, Breakpoint::new_log(&log_message))
18777 });
18778
18779 editor.edit_breakpoint_at_anchor(
18780 anchor,
18781 bp,
18782 BreakpointEditAction::EditLogMessage(log_message.into()),
18783 cx,
18784 );
18785}
18786
18787#[gpui::test]
18788async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
18789 init_test(cx, |_| {});
18790
18791 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
18792 let fs = FakeFs::new(cx.executor());
18793 fs.insert_tree(
18794 path!("/a"),
18795 json!({
18796 "main.rs": sample_text,
18797 }),
18798 )
18799 .await;
18800 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18801 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18802 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18803
18804 let fs = FakeFs::new(cx.executor());
18805 fs.insert_tree(
18806 path!("/a"),
18807 json!({
18808 "main.rs": sample_text,
18809 }),
18810 )
18811 .await;
18812 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18813 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18814 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18815 let worktree_id = workspace
18816 .update(cx, |workspace, _window, cx| {
18817 workspace.project().update(cx, |project, cx| {
18818 project.worktrees(cx).next().unwrap().read(cx).id()
18819 })
18820 })
18821 .unwrap();
18822
18823 let buffer = project
18824 .update(cx, |project, cx| {
18825 project.open_buffer((worktree_id, "main.rs"), cx)
18826 })
18827 .await
18828 .unwrap();
18829
18830 let (editor, cx) = cx.add_window_view(|window, cx| {
18831 Editor::new(
18832 EditorMode::full(),
18833 MultiBuffer::build_from_buffer(buffer, cx),
18834 Some(project.clone()),
18835 window,
18836 cx,
18837 )
18838 });
18839
18840 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
18841 let abs_path = project.read_with(cx, |project, cx| {
18842 project
18843 .absolute_path(&project_path, cx)
18844 .map(|path_buf| Arc::from(path_buf.to_owned()))
18845 .unwrap()
18846 });
18847
18848 // assert we can add breakpoint on the first line
18849 editor.update_in(cx, |editor, window, cx| {
18850 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18851 editor.move_to_end(&MoveToEnd, window, cx);
18852 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18853 });
18854
18855 let breakpoints = editor.update(cx, |editor, cx| {
18856 editor
18857 .breakpoint_store()
18858 .as_ref()
18859 .unwrap()
18860 .read(cx)
18861 .all_source_breakpoints(cx)
18862 .clone()
18863 });
18864
18865 assert_eq!(1, breakpoints.len());
18866 assert_breakpoint(
18867 &breakpoints,
18868 &abs_path,
18869 vec![
18870 (0, Breakpoint::new_standard()),
18871 (3, Breakpoint::new_standard()),
18872 ],
18873 );
18874
18875 editor.update_in(cx, |editor, window, cx| {
18876 editor.move_to_beginning(&MoveToBeginning, window, cx);
18877 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18878 });
18879
18880 let breakpoints = editor.update(cx, |editor, cx| {
18881 editor
18882 .breakpoint_store()
18883 .as_ref()
18884 .unwrap()
18885 .read(cx)
18886 .all_source_breakpoints(cx)
18887 .clone()
18888 });
18889
18890 assert_eq!(1, breakpoints.len());
18891 assert_breakpoint(
18892 &breakpoints,
18893 &abs_path,
18894 vec![(3, Breakpoint::new_standard())],
18895 );
18896
18897 editor.update_in(cx, |editor, window, cx| {
18898 editor.move_to_end(&MoveToEnd, window, cx);
18899 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18900 });
18901
18902 let breakpoints = editor.update(cx, |editor, cx| {
18903 editor
18904 .breakpoint_store()
18905 .as_ref()
18906 .unwrap()
18907 .read(cx)
18908 .all_source_breakpoints(cx)
18909 .clone()
18910 });
18911
18912 assert_eq!(0, breakpoints.len());
18913 assert_breakpoint(&breakpoints, &abs_path, vec![]);
18914}
18915
18916#[gpui::test]
18917async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
18918 init_test(cx, |_| {});
18919
18920 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
18921
18922 let fs = FakeFs::new(cx.executor());
18923 fs.insert_tree(
18924 path!("/a"),
18925 json!({
18926 "main.rs": sample_text,
18927 }),
18928 )
18929 .await;
18930 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18931 let (workspace, cx) =
18932 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
18933
18934 let worktree_id = workspace.update(cx, |workspace, cx| {
18935 workspace.project().update(cx, |project, cx| {
18936 project.worktrees(cx).next().unwrap().read(cx).id()
18937 })
18938 });
18939
18940 let buffer = project
18941 .update(cx, |project, cx| {
18942 project.open_buffer((worktree_id, "main.rs"), cx)
18943 })
18944 .await
18945 .unwrap();
18946
18947 let (editor, cx) = cx.add_window_view(|window, cx| {
18948 Editor::new(
18949 EditorMode::full(),
18950 MultiBuffer::build_from_buffer(buffer, cx),
18951 Some(project.clone()),
18952 window,
18953 cx,
18954 )
18955 });
18956
18957 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
18958 let abs_path = project.read_with(cx, |project, cx| {
18959 project
18960 .absolute_path(&project_path, cx)
18961 .map(|path_buf| Arc::from(path_buf.to_owned()))
18962 .unwrap()
18963 });
18964
18965 editor.update_in(cx, |editor, window, cx| {
18966 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
18967 });
18968
18969 let breakpoints = editor.update(cx, |editor, cx| {
18970 editor
18971 .breakpoint_store()
18972 .as_ref()
18973 .unwrap()
18974 .read(cx)
18975 .all_source_breakpoints(cx)
18976 .clone()
18977 });
18978
18979 assert_breakpoint(
18980 &breakpoints,
18981 &abs_path,
18982 vec![(0, Breakpoint::new_log("hello world"))],
18983 );
18984
18985 // Removing a log message from a log breakpoint should remove it
18986 editor.update_in(cx, |editor, window, cx| {
18987 add_log_breakpoint_at_cursor(editor, "", window, cx);
18988 });
18989
18990 let breakpoints = editor.update(cx, |editor, cx| {
18991 editor
18992 .breakpoint_store()
18993 .as_ref()
18994 .unwrap()
18995 .read(cx)
18996 .all_source_breakpoints(cx)
18997 .clone()
18998 });
18999
19000 assert_breakpoint(&breakpoints, &abs_path, vec![]);
19001
19002 editor.update_in(cx, |editor, window, cx| {
19003 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19004 editor.move_to_end(&MoveToEnd, window, cx);
19005 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19006 // Not adding a log message to a standard breakpoint shouldn't remove it
19007 add_log_breakpoint_at_cursor(editor, "", window, cx);
19008 });
19009
19010 let breakpoints = editor.update(cx, |editor, cx| {
19011 editor
19012 .breakpoint_store()
19013 .as_ref()
19014 .unwrap()
19015 .read(cx)
19016 .all_source_breakpoints(cx)
19017 .clone()
19018 });
19019
19020 assert_breakpoint(
19021 &breakpoints,
19022 &abs_path,
19023 vec![
19024 (0, Breakpoint::new_standard()),
19025 (3, Breakpoint::new_standard()),
19026 ],
19027 );
19028
19029 editor.update_in(cx, |editor, window, cx| {
19030 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
19031 });
19032
19033 let breakpoints = editor.update(cx, |editor, cx| {
19034 editor
19035 .breakpoint_store()
19036 .as_ref()
19037 .unwrap()
19038 .read(cx)
19039 .all_source_breakpoints(cx)
19040 .clone()
19041 });
19042
19043 assert_breakpoint(
19044 &breakpoints,
19045 &abs_path,
19046 vec![
19047 (0, Breakpoint::new_standard()),
19048 (3, Breakpoint::new_log("hello world")),
19049 ],
19050 );
19051
19052 editor.update_in(cx, |editor, window, cx| {
19053 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
19054 });
19055
19056 let breakpoints = editor.update(cx, |editor, cx| {
19057 editor
19058 .breakpoint_store()
19059 .as_ref()
19060 .unwrap()
19061 .read(cx)
19062 .all_source_breakpoints(cx)
19063 .clone()
19064 });
19065
19066 assert_breakpoint(
19067 &breakpoints,
19068 &abs_path,
19069 vec![
19070 (0, Breakpoint::new_standard()),
19071 (3, Breakpoint::new_log("hello Earth!!")),
19072 ],
19073 );
19074}
19075
19076/// This also tests that Editor::breakpoint_at_cursor_head is working properly
19077/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
19078/// or when breakpoints were placed out of order. This tests for a regression too
19079#[gpui::test]
19080async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
19081 init_test(cx, |_| {});
19082
19083 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
19084 let fs = FakeFs::new(cx.executor());
19085 fs.insert_tree(
19086 path!("/a"),
19087 json!({
19088 "main.rs": sample_text,
19089 }),
19090 )
19091 .await;
19092 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19093 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19094 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19095
19096 let fs = FakeFs::new(cx.executor());
19097 fs.insert_tree(
19098 path!("/a"),
19099 json!({
19100 "main.rs": sample_text,
19101 }),
19102 )
19103 .await;
19104 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19105 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19106 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19107 let worktree_id = workspace
19108 .update(cx, |workspace, _window, cx| {
19109 workspace.project().update(cx, |project, cx| {
19110 project.worktrees(cx).next().unwrap().read(cx).id()
19111 })
19112 })
19113 .unwrap();
19114
19115 let buffer = project
19116 .update(cx, |project, cx| {
19117 project.open_buffer((worktree_id, "main.rs"), cx)
19118 })
19119 .await
19120 .unwrap();
19121
19122 let (editor, cx) = cx.add_window_view(|window, cx| {
19123 Editor::new(
19124 EditorMode::full(),
19125 MultiBuffer::build_from_buffer(buffer, cx),
19126 Some(project.clone()),
19127 window,
19128 cx,
19129 )
19130 });
19131
19132 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
19133 let abs_path = project.read_with(cx, |project, cx| {
19134 project
19135 .absolute_path(&project_path, cx)
19136 .map(|path_buf| Arc::from(path_buf.to_owned()))
19137 .unwrap()
19138 });
19139
19140 // assert we can add breakpoint on the first line
19141 editor.update_in(cx, |editor, window, cx| {
19142 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19143 editor.move_to_end(&MoveToEnd, window, cx);
19144 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19145 editor.move_up(&MoveUp, window, cx);
19146 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19147 });
19148
19149 let breakpoints = editor.update(cx, |editor, cx| {
19150 editor
19151 .breakpoint_store()
19152 .as_ref()
19153 .unwrap()
19154 .read(cx)
19155 .all_source_breakpoints(cx)
19156 .clone()
19157 });
19158
19159 assert_eq!(1, breakpoints.len());
19160 assert_breakpoint(
19161 &breakpoints,
19162 &abs_path,
19163 vec![
19164 (0, Breakpoint::new_standard()),
19165 (2, Breakpoint::new_standard()),
19166 (3, Breakpoint::new_standard()),
19167 ],
19168 );
19169
19170 editor.update_in(cx, |editor, window, cx| {
19171 editor.move_to_beginning(&MoveToBeginning, window, cx);
19172 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
19173 editor.move_to_end(&MoveToEnd, window, cx);
19174 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
19175 // Disabling a breakpoint that doesn't exist should do nothing
19176 editor.move_up(&MoveUp, window, cx);
19177 editor.move_up(&MoveUp, window, cx);
19178 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
19179 });
19180
19181 let breakpoints = editor.update(cx, |editor, cx| {
19182 editor
19183 .breakpoint_store()
19184 .as_ref()
19185 .unwrap()
19186 .read(cx)
19187 .all_source_breakpoints(cx)
19188 .clone()
19189 });
19190
19191 let disable_breakpoint = {
19192 let mut bp = Breakpoint::new_standard();
19193 bp.state = BreakpointState::Disabled;
19194 bp
19195 };
19196
19197 assert_eq!(1, breakpoints.len());
19198 assert_breakpoint(
19199 &breakpoints,
19200 &abs_path,
19201 vec![
19202 (0, disable_breakpoint.clone()),
19203 (2, Breakpoint::new_standard()),
19204 (3, disable_breakpoint.clone()),
19205 ],
19206 );
19207
19208 editor.update_in(cx, |editor, window, cx| {
19209 editor.move_to_beginning(&MoveToBeginning, window, cx);
19210 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
19211 editor.move_to_end(&MoveToEnd, window, cx);
19212 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
19213 editor.move_up(&MoveUp, window, cx);
19214 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
19215 });
19216
19217 let breakpoints = editor.update(cx, |editor, cx| {
19218 editor
19219 .breakpoint_store()
19220 .as_ref()
19221 .unwrap()
19222 .read(cx)
19223 .all_source_breakpoints(cx)
19224 .clone()
19225 });
19226
19227 assert_eq!(1, breakpoints.len());
19228 assert_breakpoint(
19229 &breakpoints,
19230 &abs_path,
19231 vec![
19232 (0, Breakpoint::new_standard()),
19233 (2, disable_breakpoint),
19234 (3, Breakpoint::new_standard()),
19235 ],
19236 );
19237}
19238
19239#[gpui::test]
19240async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
19241 init_test(cx, |_| {});
19242 let capabilities = lsp::ServerCapabilities {
19243 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
19244 prepare_provider: Some(true),
19245 work_done_progress_options: Default::default(),
19246 })),
19247 ..Default::default()
19248 };
19249 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
19250
19251 cx.set_state(indoc! {"
19252 struct Fˇoo {}
19253 "});
19254
19255 cx.update_editor(|editor, _, cx| {
19256 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
19257 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
19258 editor.highlight_background::<DocumentHighlightRead>(
19259 &[highlight_range],
19260 |c| c.editor_document_highlight_read_background,
19261 cx,
19262 );
19263 });
19264
19265 let mut prepare_rename_handler = cx
19266 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
19267 move |_, _, _| async move {
19268 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
19269 start: lsp::Position {
19270 line: 0,
19271 character: 7,
19272 },
19273 end: lsp::Position {
19274 line: 0,
19275 character: 10,
19276 },
19277 })))
19278 },
19279 );
19280 let prepare_rename_task = cx
19281 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
19282 .expect("Prepare rename was not started");
19283 prepare_rename_handler.next().await.unwrap();
19284 prepare_rename_task.await.expect("Prepare rename failed");
19285
19286 let mut rename_handler =
19287 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
19288 let edit = lsp::TextEdit {
19289 range: lsp::Range {
19290 start: lsp::Position {
19291 line: 0,
19292 character: 7,
19293 },
19294 end: lsp::Position {
19295 line: 0,
19296 character: 10,
19297 },
19298 },
19299 new_text: "FooRenamed".to_string(),
19300 };
19301 Ok(Some(lsp::WorkspaceEdit::new(
19302 // Specify the same edit twice
19303 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
19304 )))
19305 });
19306 let rename_task = cx
19307 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
19308 .expect("Confirm rename was not started");
19309 rename_handler.next().await.unwrap();
19310 rename_task.await.expect("Confirm rename failed");
19311 cx.run_until_parked();
19312
19313 // Despite two edits, only one is actually applied as those are identical
19314 cx.assert_editor_state(indoc! {"
19315 struct FooRenamedˇ {}
19316 "});
19317}
19318
19319#[gpui::test]
19320async fn test_rename_without_prepare(cx: &mut TestAppContext) {
19321 init_test(cx, |_| {});
19322 // These capabilities indicate that the server does not support prepare rename.
19323 let capabilities = lsp::ServerCapabilities {
19324 rename_provider: Some(lsp::OneOf::Left(true)),
19325 ..Default::default()
19326 };
19327 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
19328
19329 cx.set_state(indoc! {"
19330 struct Fˇoo {}
19331 "});
19332
19333 cx.update_editor(|editor, _window, cx| {
19334 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
19335 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
19336 editor.highlight_background::<DocumentHighlightRead>(
19337 &[highlight_range],
19338 |c| c.editor_document_highlight_read_background,
19339 cx,
19340 );
19341 });
19342
19343 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
19344 .expect("Prepare rename was not started")
19345 .await
19346 .expect("Prepare rename failed");
19347
19348 let mut rename_handler =
19349 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
19350 let edit = lsp::TextEdit {
19351 range: lsp::Range {
19352 start: lsp::Position {
19353 line: 0,
19354 character: 7,
19355 },
19356 end: lsp::Position {
19357 line: 0,
19358 character: 10,
19359 },
19360 },
19361 new_text: "FooRenamed".to_string(),
19362 };
19363 Ok(Some(lsp::WorkspaceEdit::new(
19364 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
19365 )))
19366 });
19367 let rename_task = cx
19368 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
19369 .expect("Confirm rename was not started");
19370 rename_handler.next().await.unwrap();
19371 rename_task.await.expect("Confirm rename failed");
19372 cx.run_until_parked();
19373
19374 // Correct range is renamed, as `surrounding_word` is used to find it.
19375 cx.assert_editor_state(indoc! {"
19376 struct FooRenamedˇ {}
19377 "});
19378}
19379
19380#[gpui::test]
19381async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
19382 init_test(cx, |_| {});
19383 let mut cx = EditorTestContext::new(cx).await;
19384
19385 let language = Arc::new(
19386 Language::new(
19387 LanguageConfig::default(),
19388 Some(tree_sitter_html::LANGUAGE.into()),
19389 )
19390 .with_brackets_query(
19391 r#"
19392 ("<" @open "/>" @close)
19393 ("</" @open ">" @close)
19394 ("<" @open ">" @close)
19395 ("\"" @open "\"" @close)
19396 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
19397 "#,
19398 )
19399 .unwrap(),
19400 );
19401 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
19402
19403 cx.set_state(indoc! {"
19404 <span>ˇ</span>
19405 "});
19406 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
19407 cx.assert_editor_state(indoc! {"
19408 <span>
19409 ˇ
19410 </span>
19411 "});
19412
19413 cx.set_state(indoc! {"
19414 <span><span></span>ˇ</span>
19415 "});
19416 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
19417 cx.assert_editor_state(indoc! {"
19418 <span><span></span>
19419 ˇ</span>
19420 "});
19421
19422 cx.set_state(indoc! {"
19423 <span>ˇ
19424 </span>
19425 "});
19426 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
19427 cx.assert_editor_state(indoc! {"
19428 <span>
19429 ˇ
19430 </span>
19431 "});
19432}
19433
19434#[gpui::test(iterations = 10)]
19435async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
19436 init_test(cx, |_| {});
19437
19438 let fs = FakeFs::new(cx.executor());
19439 fs.insert_tree(
19440 path!("/dir"),
19441 json!({
19442 "a.ts": "a",
19443 }),
19444 )
19445 .await;
19446
19447 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
19448 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19449 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19450
19451 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
19452 language_registry.add(Arc::new(Language::new(
19453 LanguageConfig {
19454 name: "TypeScript".into(),
19455 matcher: LanguageMatcher {
19456 path_suffixes: vec!["ts".to_string()],
19457 ..Default::default()
19458 },
19459 ..Default::default()
19460 },
19461 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
19462 )));
19463 let mut fake_language_servers = language_registry.register_fake_lsp(
19464 "TypeScript",
19465 FakeLspAdapter {
19466 capabilities: lsp::ServerCapabilities {
19467 code_lens_provider: Some(lsp::CodeLensOptions {
19468 resolve_provider: Some(true),
19469 }),
19470 execute_command_provider: Some(lsp::ExecuteCommandOptions {
19471 commands: vec!["_the/command".to_string()],
19472 ..lsp::ExecuteCommandOptions::default()
19473 }),
19474 ..lsp::ServerCapabilities::default()
19475 },
19476 ..FakeLspAdapter::default()
19477 },
19478 );
19479
19480 let (buffer, _handle) = project
19481 .update(cx, |p, cx| {
19482 p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
19483 })
19484 .await
19485 .unwrap();
19486 cx.executor().run_until_parked();
19487
19488 let fake_server = fake_language_servers.next().await.unwrap();
19489
19490 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
19491 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
19492 drop(buffer_snapshot);
19493 let actions = cx
19494 .update_window(*workspace, |_, window, cx| {
19495 project.code_actions(&buffer, anchor..anchor, window, cx)
19496 })
19497 .unwrap();
19498
19499 fake_server
19500 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
19501 Ok(Some(vec![
19502 lsp::CodeLens {
19503 range: lsp::Range::default(),
19504 command: Some(lsp::Command {
19505 title: "Code lens command".to_owned(),
19506 command: "_the/command".to_owned(),
19507 arguments: None,
19508 }),
19509 data: None,
19510 },
19511 lsp::CodeLens {
19512 range: lsp::Range::default(),
19513 command: Some(lsp::Command {
19514 title: "Command not in capabilities".to_owned(),
19515 command: "not in capabilities".to_owned(),
19516 arguments: None,
19517 }),
19518 data: None,
19519 },
19520 lsp::CodeLens {
19521 range: lsp::Range {
19522 start: lsp::Position {
19523 line: 1,
19524 character: 1,
19525 },
19526 end: lsp::Position {
19527 line: 1,
19528 character: 1,
19529 },
19530 },
19531 command: Some(lsp::Command {
19532 title: "Command not in range".to_owned(),
19533 command: "_the/command".to_owned(),
19534 arguments: None,
19535 }),
19536 data: None,
19537 },
19538 ]))
19539 })
19540 .next()
19541 .await;
19542
19543 let actions = actions.await.unwrap();
19544 assert_eq!(
19545 actions.len(),
19546 1,
19547 "Should have only one valid action for the 0..0 range"
19548 );
19549 let action = actions[0].clone();
19550 let apply = project.update(cx, |project, cx| {
19551 project.apply_code_action(buffer.clone(), action, true, cx)
19552 });
19553
19554 // Resolving the code action does not populate its edits. In absence of
19555 // edits, we must execute the given command.
19556 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
19557 |mut lens, _| async move {
19558 let lens_command = lens.command.as_mut().expect("should have a command");
19559 assert_eq!(lens_command.title, "Code lens command");
19560 lens_command.arguments = Some(vec![json!("the-argument")]);
19561 Ok(lens)
19562 },
19563 );
19564
19565 // While executing the command, the language server sends the editor
19566 // a `workspaceEdit` request.
19567 fake_server
19568 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
19569 let fake = fake_server.clone();
19570 move |params, _| {
19571 assert_eq!(params.command, "_the/command");
19572 let fake = fake.clone();
19573 async move {
19574 fake.server
19575 .request::<lsp::request::ApplyWorkspaceEdit>(
19576 lsp::ApplyWorkspaceEditParams {
19577 label: None,
19578 edit: lsp::WorkspaceEdit {
19579 changes: Some(
19580 [(
19581 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
19582 vec![lsp::TextEdit {
19583 range: lsp::Range::new(
19584 lsp::Position::new(0, 0),
19585 lsp::Position::new(0, 0),
19586 ),
19587 new_text: "X".into(),
19588 }],
19589 )]
19590 .into_iter()
19591 .collect(),
19592 ),
19593 ..Default::default()
19594 },
19595 },
19596 )
19597 .await
19598 .into_response()
19599 .unwrap();
19600 Ok(Some(json!(null)))
19601 }
19602 }
19603 })
19604 .next()
19605 .await;
19606
19607 // Applying the code lens command returns a project transaction containing the edits
19608 // sent by the language server in its `workspaceEdit` request.
19609 let transaction = apply.await.unwrap();
19610 assert!(transaction.0.contains_key(&buffer));
19611 buffer.update(cx, |buffer, cx| {
19612 assert_eq!(buffer.text(), "Xa");
19613 buffer.undo(cx);
19614 assert_eq!(buffer.text(), "a");
19615 });
19616}
19617
19618#[gpui::test]
19619async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
19620 init_test(cx, |_| {});
19621
19622 let fs = FakeFs::new(cx.executor());
19623 let main_text = r#"fn main() {
19624println!("1");
19625println!("2");
19626println!("3");
19627println!("4");
19628println!("5");
19629}"#;
19630 let lib_text = "mod foo {}";
19631 fs.insert_tree(
19632 path!("/a"),
19633 json!({
19634 "lib.rs": lib_text,
19635 "main.rs": main_text,
19636 }),
19637 )
19638 .await;
19639
19640 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19641 let (workspace, cx) =
19642 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
19643 let worktree_id = workspace.update(cx, |workspace, cx| {
19644 workspace.project().update(cx, |project, cx| {
19645 project.worktrees(cx).next().unwrap().read(cx).id()
19646 })
19647 });
19648
19649 let expected_ranges = vec![
19650 Point::new(0, 0)..Point::new(0, 0),
19651 Point::new(1, 0)..Point::new(1, 1),
19652 Point::new(2, 0)..Point::new(2, 2),
19653 Point::new(3, 0)..Point::new(3, 3),
19654 ];
19655
19656 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
19657 let editor_1 = workspace
19658 .update_in(cx, |workspace, window, cx| {
19659 workspace.open_path(
19660 (worktree_id, "main.rs"),
19661 Some(pane_1.downgrade()),
19662 true,
19663 window,
19664 cx,
19665 )
19666 })
19667 .unwrap()
19668 .await
19669 .downcast::<Editor>()
19670 .unwrap();
19671 pane_1.update(cx, |pane, cx| {
19672 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19673 open_editor.update(cx, |editor, cx| {
19674 assert_eq!(
19675 editor.display_text(cx),
19676 main_text,
19677 "Original main.rs text on initial open",
19678 );
19679 assert_eq!(
19680 editor
19681 .selections
19682 .all::<Point>(cx)
19683 .into_iter()
19684 .map(|s| s.range())
19685 .collect::<Vec<_>>(),
19686 vec![Point::zero()..Point::zero()],
19687 "Default selections on initial open",
19688 );
19689 })
19690 });
19691 editor_1.update_in(cx, |editor, window, cx| {
19692 editor.change_selections(None, window, cx, |s| {
19693 s.select_ranges(expected_ranges.clone());
19694 });
19695 });
19696
19697 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
19698 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
19699 });
19700 let editor_2 = workspace
19701 .update_in(cx, |workspace, window, cx| {
19702 workspace.open_path(
19703 (worktree_id, "main.rs"),
19704 Some(pane_2.downgrade()),
19705 true,
19706 window,
19707 cx,
19708 )
19709 })
19710 .unwrap()
19711 .await
19712 .downcast::<Editor>()
19713 .unwrap();
19714 pane_2.update(cx, |pane, cx| {
19715 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19716 open_editor.update(cx, |editor, cx| {
19717 assert_eq!(
19718 editor.display_text(cx),
19719 main_text,
19720 "Original main.rs text on initial open in another panel",
19721 );
19722 assert_eq!(
19723 editor
19724 .selections
19725 .all::<Point>(cx)
19726 .into_iter()
19727 .map(|s| s.range())
19728 .collect::<Vec<_>>(),
19729 vec![Point::zero()..Point::zero()],
19730 "Default selections on initial open in another panel",
19731 );
19732 })
19733 });
19734
19735 editor_2.update_in(cx, |editor, window, cx| {
19736 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
19737 });
19738
19739 let _other_editor_1 = workspace
19740 .update_in(cx, |workspace, window, cx| {
19741 workspace.open_path(
19742 (worktree_id, "lib.rs"),
19743 Some(pane_1.downgrade()),
19744 true,
19745 window,
19746 cx,
19747 )
19748 })
19749 .unwrap()
19750 .await
19751 .downcast::<Editor>()
19752 .unwrap();
19753 pane_1
19754 .update_in(cx, |pane, window, cx| {
19755 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
19756 .unwrap()
19757 })
19758 .await
19759 .unwrap();
19760 drop(editor_1);
19761 pane_1.update(cx, |pane, cx| {
19762 pane.active_item()
19763 .unwrap()
19764 .downcast::<Editor>()
19765 .unwrap()
19766 .update(cx, |editor, cx| {
19767 assert_eq!(
19768 editor.display_text(cx),
19769 lib_text,
19770 "Other file should be open and active",
19771 );
19772 });
19773 assert_eq!(pane.items().count(), 1, "No other editors should be open");
19774 });
19775
19776 let _other_editor_2 = workspace
19777 .update_in(cx, |workspace, window, cx| {
19778 workspace.open_path(
19779 (worktree_id, "lib.rs"),
19780 Some(pane_2.downgrade()),
19781 true,
19782 window,
19783 cx,
19784 )
19785 })
19786 .unwrap()
19787 .await
19788 .downcast::<Editor>()
19789 .unwrap();
19790 pane_2
19791 .update_in(cx, |pane, window, cx| {
19792 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
19793 .unwrap()
19794 })
19795 .await
19796 .unwrap();
19797 drop(editor_2);
19798 pane_2.update(cx, |pane, cx| {
19799 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19800 open_editor.update(cx, |editor, cx| {
19801 assert_eq!(
19802 editor.display_text(cx),
19803 lib_text,
19804 "Other file should be open and active in another panel too",
19805 );
19806 });
19807 assert_eq!(
19808 pane.items().count(),
19809 1,
19810 "No other editors should be open in another pane",
19811 );
19812 });
19813
19814 let _editor_1_reopened = workspace
19815 .update_in(cx, |workspace, window, cx| {
19816 workspace.open_path(
19817 (worktree_id, "main.rs"),
19818 Some(pane_1.downgrade()),
19819 true,
19820 window,
19821 cx,
19822 )
19823 })
19824 .unwrap()
19825 .await
19826 .downcast::<Editor>()
19827 .unwrap();
19828 let _editor_2_reopened = workspace
19829 .update_in(cx, |workspace, window, cx| {
19830 workspace.open_path(
19831 (worktree_id, "main.rs"),
19832 Some(pane_2.downgrade()),
19833 true,
19834 window,
19835 cx,
19836 )
19837 })
19838 .unwrap()
19839 .await
19840 .downcast::<Editor>()
19841 .unwrap();
19842 pane_1.update(cx, |pane, cx| {
19843 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19844 open_editor.update(cx, |editor, cx| {
19845 assert_eq!(
19846 editor.display_text(cx),
19847 main_text,
19848 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
19849 );
19850 assert_eq!(
19851 editor
19852 .selections
19853 .all::<Point>(cx)
19854 .into_iter()
19855 .map(|s| s.range())
19856 .collect::<Vec<_>>(),
19857 expected_ranges,
19858 "Previous editor in the 1st panel had selections and should get them restored on reopen",
19859 );
19860 })
19861 });
19862 pane_2.update(cx, |pane, cx| {
19863 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19864 open_editor.update(cx, |editor, cx| {
19865 assert_eq!(
19866 editor.display_text(cx),
19867 r#"fn main() {
19868⋯rintln!("1");
19869⋯intln!("2");
19870⋯ntln!("3");
19871println!("4");
19872println!("5");
19873}"#,
19874 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
19875 );
19876 assert_eq!(
19877 editor
19878 .selections
19879 .all::<Point>(cx)
19880 .into_iter()
19881 .map(|s| s.range())
19882 .collect::<Vec<_>>(),
19883 vec![Point::zero()..Point::zero()],
19884 "Previous editor in the 2nd pane had no selections changed hence should restore none",
19885 );
19886 })
19887 });
19888}
19889
19890#[gpui::test]
19891async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
19892 init_test(cx, |_| {});
19893
19894 let fs = FakeFs::new(cx.executor());
19895 let main_text = r#"fn main() {
19896println!("1");
19897println!("2");
19898println!("3");
19899println!("4");
19900println!("5");
19901}"#;
19902 let lib_text = "mod foo {}";
19903 fs.insert_tree(
19904 path!("/a"),
19905 json!({
19906 "lib.rs": lib_text,
19907 "main.rs": main_text,
19908 }),
19909 )
19910 .await;
19911
19912 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19913 let (workspace, cx) =
19914 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
19915 let worktree_id = workspace.update(cx, |workspace, cx| {
19916 workspace.project().update(cx, |project, cx| {
19917 project.worktrees(cx).next().unwrap().read(cx).id()
19918 })
19919 });
19920
19921 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
19922 let editor = workspace
19923 .update_in(cx, |workspace, window, cx| {
19924 workspace.open_path(
19925 (worktree_id, "main.rs"),
19926 Some(pane.downgrade()),
19927 true,
19928 window,
19929 cx,
19930 )
19931 })
19932 .unwrap()
19933 .await
19934 .downcast::<Editor>()
19935 .unwrap();
19936 pane.update(cx, |pane, cx| {
19937 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19938 open_editor.update(cx, |editor, cx| {
19939 assert_eq!(
19940 editor.display_text(cx),
19941 main_text,
19942 "Original main.rs text on initial open",
19943 );
19944 })
19945 });
19946 editor.update_in(cx, |editor, window, cx| {
19947 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
19948 });
19949
19950 cx.update_global(|store: &mut SettingsStore, cx| {
19951 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
19952 s.restore_on_file_reopen = Some(false);
19953 });
19954 });
19955 editor.update_in(cx, |editor, window, cx| {
19956 editor.fold_ranges(
19957 vec![
19958 Point::new(1, 0)..Point::new(1, 1),
19959 Point::new(2, 0)..Point::new(2, 2),
19960 Point::new(3, 0)..Point::new(3, 3),
19961 ],
19962 false,
19963 window,
19964 cx,
19965 );
19966 });
19967 pane.update_in(cx, |pane, window, cx| {
19968 pane.close_all_items(&CloseAllItems::default(), window, cx)
19969 .unwrap()
19970 })
19971 .await
19972 .unwrap();
19973 pane.update(cx, |pane, _| {
19974 assert!(pane.active_item().is_none());
19975 });
19976 cx.update_global(|store: &mut SettingsStore, cx| {
19977 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
19978 s.restore_on_file_reopen = Some(true);
19979 });
19980 });
19981
19982 let _editor_reopened = workspace
19983 .update_in(cx, |workspace, window, cx| {
19984 workspace.open_path(
19985 (worktree_id, "main.rs"),
19986 Some(pane.downgrade()),
19987 true,
19988 window,
19989 cx,
19990 )
19991 })
19992 .unwrap()
19993 .await
19994 .downcast::<Editor>()
19995 .unwrap();
19996 pane.update(cx, |pane, cx| {
19997 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19998 open_editor.update(cx, |editor, cx| {
19999 assert_eq!(
20000 editor.display_text(cx),
20001 main_text,
20002 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
20003 );
20004 })
20005 });
20006}
20007
20008#[gpui::test]
20009async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
20010 struct EmptyModalView {
20011 focus_handle: gpui::FocusHandle,
20012 }
20013 impl EventEmitter<DismissEvent> for EmptyModalView {}
20014 impl Render for EmptyModalView {
20015 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
20016 div()
20017 }
20018 }
20019 impl Focusable for EmptyModalView {
20020 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
20021 self.focus_handle.clone()
20022 }
20023 }
20024 impl workspace::ModalView for EmptyModalView {}
20025 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
20026 EmptyModalView {
20027 focus_handle: cx.focus_handle(),
20028 }
20029 }
20030
20031 init_test(cx, |_| {});
20032
20033 let fs = FakeFs::new(cx.executor());
20034 let project = Project::test(fs, [], cx).await;
20035 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20036 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
20037 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20038 let editor = cx.new_window_entity(|window, cx| {
20039 Editor::new(
20040 EditorMode::full(),
20041 buffer,
20042 Some(project.clone()),
20043 window,
20044 cx,
20045 )
20046 });
20047 workspace
20048 .update(cx, |workspace, window, cx| {
20049 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
20050 })
20051 .unwrap();
20052 editor.update_in(cx, |editor, window, cx| {
20053 editor.open_context_menu(&OpenContextMenu, window, cx);
20054 assert!(editor.mouse_context_menu.is_some());
20055 });
20056 workspace
20057 .update(cx, |workspace, window, cx| {
20058 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
20059 })
20060 .unwrap();
20061 cx.read(|cx| {
20062 assert!(editor.read(cx).mouse_context_menu.is_none());
20063 });
20064}
20065
20066#[gpui::test]
20067async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
20068 init_test(cx, |_| {});
20069
20070 let fs = FakeFs::new(cx.executor());
20071 fs.insert_file(path!("/file.html"), Default::default())
20072 .await;
20073
20074 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
20075
20076 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
20077 let html_language = Arc::new(Language::new(
20078 LanguageConfig {
20079 name: "HTML".into(),
20080 matcher: LanguageMatcher {
20081 path_suffixes: vec!["html".to_string()],
20082 ..LanguageMatcher::default()
20083 },
20084 brackets: BracketPairConfig {
20085 pairs: vec![BracketPair {
20086 start: "<".into(),
20087 end: ">".into(),
20088 close: true,
20089 ..Default::default()
20090 }],
20091 ..Default::default()
20092 },
20093 ..Default::default()
20094 },
20095 Some(tree_sitter_html::LANGUAGE.into()),
20096 ));
20097 language_registry.add(html_language);
20098 let mut fake_servers = language_registry.register_fake_lsp(
20099 "HTML",
20100 FakeLspAdapter {
20101 capabilities: lsp::ServerCapabilities {
20102 completion_provider: Some(lsp::CompletionOptions {
20103 resolve_provider: Some(true),
20104 ..Default::default()
20105 }),
20106 ..Default::default()
20107 },
20108 ..Default::default()
20109 },
20110 );
20111
20112 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20113 let cx = &mut VisualTestContext::from_window(*workspace, cx);
20114
20115 let worktree_id = workspace
20116 .update(cx, |workspace, _window, cx| {
20117 workspace.project().update(cx, |project, cx| {
20118 project.worktrees(cx).next().unwrap().read(cx).id()
20119 })
20120 })
20121 .unwrap();
20122 project
20123 .update(cx, |project, cx| {
20124 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
20125 })
20126 .await
20127 .unwrap();
20128 let editor = workspace
20129 .update(cx, |workspace, window, cx| {
20130 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
20131 })
20132 .unwrap()
20133 .await
20134 .unwrap()
20135 .downcast::<Editor>()
20136 .unwrap();
20137
20138 let fake_server = fake_servers.next().await.unwrap();
20139 editor.update_in(cx, |editor, window, cx| {
20140 editor.set_text("<ad></ad>", window, cx);
20141 editor.change_selections(None, window, cx, |selections| {
20142 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
20143 });
20144 let Some((buffer, _)) = editor
20145 .buffer
20146 .read(cx)
20147 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
20148 else {
20149 panic!("Failed to get buffer for selection position");
20150 };
20151 let buffer = buffer.read(cx);
20152 let buffer_id = buffer.remote_id();
20153 let opening_range =
20154 buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3));
20155 let closing_range =
20156 buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
20157 let mut linked_ranges = HashMap::default();
20158 linked_ranges.insert(
20159 buffer_id,
20160 vec![(opening_range.clone(), vec![closing_range.clone()])],
20161 );
20162 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
20163 });
20164 let mut completion_handle =
20165 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
20166 Ok(Some(lsp::CompletionResponse::Array(vec![
20167 lsp::CompletionItem {
20168 label: "head".to_string(),
20169 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
20170 lsp::InsertReplaceEdit {
20171 new_text: "head".to_string(),
20172 insert: lsp::Range::new(
20173 lsp::Position::new(0, 1),
20174 lsp::Position::new(0, 3),
20175 ),
20176 replace: lsp::Range::new(
20177 lsp::Position::new(0, 1),
20178 lsp::Position::new(0, 3),
20179 ),
20180 },
20181 )),
20182 ..Default::default()
20183 },
20184 ])))
20185 });
20186 editor.update_in(cx, |editor, window, cx| {
20187 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
20188 });
20189 cx.run_until_parked();
20190 completion_handle.next().await.unwrap();
20191 editor.update(cx, |editor, _| {
20192 assert!(
20193 editor.context_menu_visible(),
20194 "Completion menu should be visible"
20195 );
20196 });
20197 editor.update_in(cx, |editor, window, cx| {
20198 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
20199 });
20200 cx.executor().run_until_parked();
20201 editor.update(cx, |editor, cx| {
20202 assert_eq!(editor.text(cx), "<head></head>");
20203 });
20204}
20205
20206#[gpui::test]
20207async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
20208 init_test(cx, |_| {});
20209
20210 let fs = FakeFs::new(cx.executor());
20211 fs.insert_tree(
20212 path!("/root"),
20213 json!({
20214 "a": {
20215 "main.rs": "fn main() {}",
20216 },
20217 "foo": {
20218 "bar": {
20219 "external_file.rs": "pub mod external {}",
20220 }
20221 }
20222 }),
20223 )
20224 .await;
20225
20226 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
20227 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
20228 language_registry.add(rust_lang());
20229 let _fake_servers = language_registry.register_fake_lsp(
20230 "Rust",
20231 FakeLspAdapter {
20232 ..FakeLspAdapter::default()
20233 },
20234 );
20235 let (workspace, cx) =
20236 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20237 let worktree_id = workspace.update(cx, |workspace, cx| {
20238 workspace.project().update(cx, |project, cx| {
20239 project.worktrees(cx).next().unwrap().read(cx).id()
20240 })
20241 });
20242
20243 let assert_language_servers_count =
20244 |expected: usize, context: &str, cx: &mut VisualTestContext| {
20245 project.update(cx, |project, cx| {
20246 let current = project
20247 .lsp_store()
20248 .read(cx)
20249 .as_local()
20250 .unwrap()
20251 .language_servers
20252 .len();
20253 assert_eq!(expected, current, "{context}");
20254 });
20255 };
20256
20257 assert_language_servers_count(
20258 0,
20259 "No servers should be running before any file is open",
20260 cx,
20261 );
20262 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
20263 let main_editor = workspace
20264 .update_in(cx, |workspace, window, cx| {
20265 workspace.open_path(
20266 (worktree_id, "main.rs"),
20267 Some(pane.downgrade()),
20268 true,
20269 window,
20270 cx,
20271 )
20272 })
20273 .unwrap()
20274 .await
20275 .downcast::<Editor>()
20276 .unwrap();
20277 pane.update(cx, |pane, cx| {
20278 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20279 open_editor.update(cx, |editor, cx| {
20280 assert_eq!(
20281 editor.display_text(cx),
20282 "fn main() {}",
20283 "Original main.rs text on initial open",
20284 );
20285 });
20286 assert_eq!(open_editor, main_editor);
20287 });
20288 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
20289
20290 let external_editor = workspace
20291 .update_in(cx, |workspace, window, cx| {
20292 workspace.open_abs_path(
20293 PathBuf::from("/root/foo/bar/external_file.rs"),
20294 OpenOptions::default(),
20295 window,
20296 cx,
20297 )
20298 })
20299 .await
20300 .expect("opening external file")
20301 .downcast::<Editor>()
20302 .expect("downcasted external file's open element to editor");
20303 pane.update(cx, |pane, cx| {
20304 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20305 open_editor.update(cx, |editor, cx| {
20306 assert_eq!(
20307 editor.display_text(cx),
20308 "pub mod external {}",
20309 "External file is open now",
20310 );
20311 });
20312 assert_eq!(open_editor, external_editor);
20313 });
20314 assert_language_servers_count(
20315 1,
20316 "Second, external, *.rs file should join the existing server",
20317 cx,
20318 );
20319
20320 pane.update_in(cx, |pane, window, cx| {
20321 pane.close_active_item(&CloseActiveItem::default(), window, cx)
20322 })
20323 .unwrap()
20324 .await
20325 .unwrap();
20326 pane.update_in(cx, |pane, window, cx| {
20327 pane.navigate_backward(window, cx);
20328 });
20329 cx.run_until_parked();
20330 pane.update(cx, |pane, cx| {
20331 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20332 open_editor.update(cx, |editor, cx| {
20333 assert_eq!(
20334 editor.display_text(cx),
20335 "pub mod external {}",
20336 "External file is open now",
20337 );
20338 });
20339 });
20340 assert_language_servers_count(
20341 1,
20342 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
20343 cx,
20344 );
20345
20346 cx.update(|_, cx| {
20347 workspace::reload(&workspace::Reload::default(), cx);
20348 });
20349 assert_language_servers_count(
20350 1,
20351 "After reloading the worktree with local and external files opened, only one project should be started",
20352 cx,
20353 );
20354}
20355
20356#[gpui::test]
20357async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
20358 init_test(cx, |_| {});
20359
20360 let mut cx = EditorTestContext::new(cx).await;
20361 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
20362 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
20363
20364 // test cursor move to start of each line on tab
20365 // for `if`, `elif`, `else`, `while`, `with` and `for`
20366 cx.set_state(indoc! {"
20367 def main():
20368 ˇ for item in items:
20369 ˇ while item.active:
20370 ˇ if item.value > 10:
20371 ˇ continue
20372 ˇ elif item.value < 0:
20373 ˇ break
20374 ˇ else:
20375 ˇ with item.context() as ctx:
20376 ˇ yield count
20377 ˇ else:
20378 ˇ log('while else')
20379 ˇ else:
20380 ˇ log('for else')
20381 "});
20382 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
20383 cx.assert_editor_state(indoc! {"
20384 def main():
20385 ˇfor item in items:
20386 ˇwhile item.active:
20387 ˇif item.value > 10:
20388 ˇcontinue
20389 ˇelif item.value < 0:
20390 ˇbreak
20391 ˇelse:
20392 ˇwith item.context() as ctx:
20393 ˇyield count
20394 ˇelse:
20395 ˇlog('while else')
20396 ˇelse:
20397 ˇlog('for else')
20398 "});
20399 // test relative indent is preserved when tab
20400 // for `if`, `elif`, `else`, `while`, `with` and `for`
20401 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
20402 cx.assert_editor_state(indoc! {"
20403 def main():
20404 ˇfor item in items:
20405 ˇwhile item.active:
20406 ˇif item.value > 10:
20407 ˇcontinue
20408 ˇelif item.value < 0:
20409 ˇbreak
20410 ˇelse:
20411 ˇwith item.context() as ctx:
20412 ˇyield count
20413 ˇelse:
20414 ˇlog('while else')
20415 ˇelse:
20416 ˇlog('for else')
20417 "});
20418
20419 // test cursor move to start of each line on tab
20420 // for `try`, `except`, `else`, `finally`, `match` and `def`
20421 cx.set_state(indoc! {"
20422 def main():
20423 ˇ try:
20424 ˇ fetch()
20425 ˇ except ValueError:
20426 ˇ handle_error()
20427 ˇ else:
20428 ˇ match value:
20429 ˇ case _:
20430 ˇ finally:
20431 ˇ def status():
20432 ˇ return 0
20433 "});
20434 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
20435 cx.assert_editor_state(indoc! {"
20436 def main():
20437 ˇtry:
20438 ˇfetch()
20439 ˇexcept ValueError:
20440 ˇhandle_error()
20441 ˇelse:
20442 ˇmatch value:
20443 ˇcase _:
20444 ˇfinally:
20445 ˇdef status():
20446 ˇreturn 0
20447 "});
20448 // test relative indent is preserved when tab
20449 // for `try`, `except`, `else`, `finally`, `match` and `def`
20450 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
20451 cx.assert_editor_state(indoc! {"
20452 def main():
20453 ˇtry:
20454 ˇfetch()
20455 ˇexcept ValueError:
20456 ˇhandle_error()
20457 ˇelse:
20458 ˇmatch value:
20459 ˇcase _:
20460 ˇfinally:
20461 ˇdef status():
20462 ˇreturn 0
20463 "});
20464}
20465
20466#[gpui::test]
20467async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
20468 init_test(cx, |_| {});
20469
20470 let mut cx = EditorTestContext::new(cx).await;
20471 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
20472 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
20473
20474 // test `else` auto outdents when typed inside `if` block
20475 cx.set_state(indoc! {"
20476 def main():
20477 if i == 2:
20478 return
20479 ˇ
20480 "});
20481 cx.update_editor(|editor, window, cx| {
20482 editor.handle_input("else:", window, cx);
20483 });
20484 cx.assert_editor_state(indoc! {"
20485 def main():
20486 if i == 2:
20487 return
20488 else:ˇ
20489 "});
20490
20491 // test `except` auto outdents when typed inside `try` block
20492 cx.set_state(indoc! {"
20493 def main():
20494 try:
20495 i = 2
20496 ˇ
20497 "});
20498 cx.update_editor(|editor, window, cx| {
20499 editor.handle_input("except:", window, cx);
20500 });
20501 cx.assert_editor_state(indoc! {"
20502 def main():
20503 try:
20504 i = 2
20505 except:ˇ
20506 "});
20507
20508 // test `else` auto outdents when typed inside `except` block
20509 cx.set_state(indoc! {"
20510 def main():
20511 try:
20512 i = 2
20513 except:
20514 j = 2
20515 ˇ
20516 "});
20517 cx.update_editor(|editor, window, cx| {
20518 editor.handle_input("else:", window, cx);
20519 });
20520 cx.assert_editor_state(indoc! {"
20521 def main():
20522 try:
20523 i = 2
20524 except:
20525 j = 2
20526 else:ˇ
20527 "});
20528
20529 // test `finally` auto outdents when typed inside `else` block
20530 cx.set_state(indoc! {"
20531 def main():
20532 try:
20533 i = 2
20534 except:
20535 j = 2
20536 else:
20537 k = 2
20538 ˇ
20539 "});
20540 cx.update_editor(|editor, window, cx| {
20541 editor.handle_input("finally:", window, cx);
20542 });
20543 cx.assert_editor_state(indoc! {"
20544 def main():
20545 try:
20546 i = 2
20547 except:
20548 j = 2
20549 else:
20550 k = 2
20551 finally:ˇ
20552 "});
20553
20554 // TODO: test `except` auto outdents when typed inside `try` block right after for block
20555 // cx.set_state(indoc! {"
20556 // def main():
20557 // try:
20558 // for i in range(n):
20559 // pass
20560 // ˇ
20561 // "});
20562 // cx.update_editor(|editor, window, cx| {
20563 // editor.handle_input("except:", window, cx);
20564 // });
20565 // cx.assert_editor_state(indoc! {"
20566 // def main():
20567 // try:
20568 // for i in range(n):
20569 // pass
20570 // except:ˇ
20571 // "});
20572
20573 // TODO: test `else` auto outdents when typed inside `except` block right after for block
20574 // cx.set_state(indoc! {"
20575 // def main():
20576 // try:
20577 // i = 2
20578 // except:
20579 // for i in range(n):
20580 // pass
20581 // ˇ
20582 // "});
20583 // cx.update_editor(|editor, window, cx| {
20584 // editor.handle_input("else:", window, cx);
20585 // });
20586 // cx.assert_editor_state(indoc! {"
20587 // def main():
20588 // try:
20589 // i = 2
20590 // except:
20591 // for i in range(n):
20592 // pass
20593 // else:ˇ
20594 // "});
20595
20596 // TODO: test `finally` auto outdents when typed inside `else` block right after for block
20597 // cx.set_state(indoc! {"
20598 // def main():
20599 // try:
20600 // i = 2
20601 // except:
20602 // j = 2
20603 // else:
20604 // for i in range(n):
20605 // pass
20606 // ˇ
20607 // "});
20608 // cx.update_editor(|editor, window, cx| {
20609 // editor.handle_input("finally:", window, cx);
20610 // });
20611 // cx.assert_editor_state(indoc! {"
20612 // def main():
20613 // try:
20614 // i = 2
20615 // except:
20616 // j = 2
20617 // else:
20618 // for i in range(n):
20619 // pass
20620 // finally:ˇ
20621 // "});
20622
20623 // test `else` stays at correct indent when typed after `for` block
20624 cx.set_state(indoc! {"
20625 def main():
20626 for i in range(10):
20627 if i == 3:
20628 break
20629 ˇ
20630 "});
20631 cx.update_editor(|editor, window, cx| {
20632 editor.handle_input("else:", window, cx);
20633 });
20634 cx.assert_editor_state(indoc! {"
20635 def main():
20636 for i in range(10):
20637 if i == 3:
20638 break
20639 else:ˇ
20640 "});
20641}
20642
20643#[gpui::test]
20644async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
20645 init_test(cx, |_| {});
20646 update_test_language_settings(cx, |settings| {
20647 settings.defaults.extend_comment_on_newline = Some(false);
20648 });
20649 let mut cx = EditorTestContext::new(cx).await;
20650 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
20651 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
20652
20653 // test correct indent after newline on comment
20654 cx.set_state(indoc! {"
20655 # COMMENT:ˇ
20656 "});
20657 cx.update_editor(|editor, window, cx| {
20658 editor.newline(&Newline, window, cx);
20659 });
20660 cx.assert_editor_state(indoc! {"
20661 # COMMENT:
20662 ˇ
20663 "});
20664
20665 // test correct indent after newline in curly brackets
20666 cx.set_state(indoc! {"
20667 {ˇ}
20668 "});
20669 cx.update_editor(|editor, window, cx| {
20670 editor.newline(&Newline, window, cx);
20671 });
20672 cx.run_until_parked();
20673 cx.assert_editor_state(indoc! {"
20674 {
20675 ˇ
20676 }
20677 "});
20678}
20679
20680fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
20681 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
20682 point..point
20683}
20684
20685fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
20686 let (text, ranges) = marked_text_ranges(marked_text, true);
20687 assert_eq!(editor.text(cx), text);
20688 assert_eq!(
20689 editor.selections.ranges(cx),
20690 ranges,
20691 "Assert selections are {}",
20692 marked_text
20693 );
20694}
20695
20696pub fn handle_signature_help_request(
20697 cx: &mut EditorLspTestContext,
20698 mocked_response: lsp::SignatureHelp,
20699) -> impl Future<Output = ()> + use<> {
20700 let mut request =
20701 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
20702 let mocked_response = mocked_response.clone();
20703 async move { Ok(Some(mocked_response)) }
20704 });
20705
20706 async move {
20707 request.next().await;
20708 }
20709}
20710
20711/// Handle completion request passing a marked string specifying where the completion
20712/// should be triggered from using '|' character, what range should be replaced, and what completions
20713/// should be returned using '<' and '>' to delimit the range.
20714///
20715/// Also see `handle_completion_request_with_insert_and_replace`.
20716#[track_caller]
20717pub fn handle_completion_request(
20718 cx: &mut EditorLspTestContext,
20719 marked_string: &str,
20720 completions: Vec<&'static str>,
20721 counter: Arc<AtomicUsize>,
20722) -> impl Future<Output = ()> {
20723 let complete_from_marker: TextRangeMarker = '|'.into();
20724 let replace_range_marker: TextRangeMarker = ('<', '>').into();
20725 let (_, mut marked_ranges) = marked_text_ranges_by(
20726 marked_string,
20727 vec![complete_from_marker.clone(), replace_range_marker.clone()],
20728 );
20729
20730 let complete_from_position =
20731 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
20732 let replace_range =
20733 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
20734
20735 let mut request =
20736 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
20737 let completions = completions.clone();
20738 counter.fetch_add(1, atomic::Ordering::Release);
20739 async move {
20740 assert_eq!(params.text_document_position.text_document.uri, url.clone());
20741 assert_eq!(
20742 params.text_document_position.position,
20743 complete_from_position
20744 );
20745 Ok(Some(lsp::CompletionResponse::Array(
20746 completions
20747 .iter()
20748 .map(|completion_text| lsp::CompletionItem {
20749 label: completion_text.to_string(),
20750 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
20751 range: replace_range,
20752 new_text: completion_text.to_string(),
20753 })),
20754 ..Default::default()
20755 })
20756 .collect(),
20757 )))
20758 }
20759 });
20760
20761 async move {
20762 request.next().await;
20763 }
20764}
20765
20766/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
20767/// given instead, which also contains an `insert` range.
20768///
20769/// This function uses the cursor position to mimic what Rust-Analyzer provides as the `insert` range,
20770/// that is, `replace_range.start..cursor_pos`.
20771pub fn handle_completion_request_with_insert_and_replace(
20772 cx: &mut EditorLspTestContext,
20773 marked_string: &str,
20774 completions: Vec<&'static str>,
20775 counter: Arc<AtomicUsize>,
20776) -> impl Future<Output = ()> {
20777 let complete_from_marker: TextRangeMarker = '|'.into();
20778 let replace_range_marker: TextRangeMarker = ('<', '>').into();
20779 let (_, mut marked_ranges) = marked_text_ranges_by(
20780 marked_string,
20781 vec![complete_from_marker.clone(), replace_range_marker.clone()],
20782 );
20783
20784 let complete_from_position =
20785 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
20786 let replace_range =
20787 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
20788
20789 let mut request =
20790 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
20791 let completions = completions.clone();
20792 counter.fetch_add(1, atomic::Ordering::Release);
20793 async move {
20794 assert_eq!(params.text_document_position.text_document.uri, url.clone());
20795 assert_eq!(
20796 params.text_document_position.position, complete_from_position,
20797 "marker `|` position doesn't match",
20798 );
20799 Ok(Some(lsp::CompletionResponse::Array(
20800 completions
20801 .iter()
20802 .map(|completion_text| lsp::CompletionItem {
20803 label: completion_text.to_string(),
20804 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
20805 lsp::InsertReplaceEdit {
20806 insert: lsp::Range {
20807 start: replace_range.start,
20808 end: complete_from_position,
20809 },
20810 replace: replace_range,
20811 new_text: completion_text.to_string(),
20812 },
20813 )),
20814 ..Default::default()
20815 })
20816 .collect(),
20817 )))
20818 }
20819 });
20820
20821 async move {
20822 request.next().await;
20823 }
20824}
20825
20826fn handle_resolve_completion_request(
20827 cx: &mut EditorLspTestContext,
20828 edits: Option<Vec<(&'static str, &'static str)>>,
20829) -> impl Future<Output = ()> {
20830 let edits = edits.map(|edits| {
20831 edits
20832 .iter()
20833 .map(|(marked_string, new_text)| {
20834 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
20835 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
20836 lsp::TextEdit::new(replace_range, new_text.to_string())
20837 })
20838 .collect::<Vec<_>>()
20839 });
20840
20841 let mut request =
20842 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
20843 let edits = edits.clone();
20844 async move {
20845 Ok(lsp::CompletionItem {
20846 additional_text_edits: edits,
20847 ..Default::default()
20848 })
20849 }
20850 });
20851
20852 async move {
20853 request.next().await;
20854 }
20855}
20856
20857pub(crate) fn update_test_language_settings(
20858 cx: &mut TestAppContext,
20859 f: impl Fn(&mut AllLanguageSettingsContent),
20860) {
20861 cx.update(|cx| {
20862 SettingsStore::update_global(cx, |store, cx| {
20863 store.update_user_settings::<AllLanguageSettings>(cx, f);
20864 });
20865 });
20866}
20867
20868pub(crate) fn update_test_project_settings(
20869 cx: &mut TestAppContext,
20870 f: impl Fn(&mut ProjectSettings),
20871) {
20872 cx.update(|cx| {
20873 SettingsStore::update_global(cx, |store, cx| {
20874 store.update_user_settings::<ProjectSettings>(cx, f);
20875 });
20876 });
20877}
20878
20879pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
20880 cx.update(|cx| {
20881 assets::Assets.load_test_fonts(cx);
20882 let store = SettingsStore::test(cx);
20883 cx.set_global(store);
20884 theme::init(theme::LoadThemes::JustBase, cx);
20885 release_channel::init(SemanticVersion::default(), cx);
20886 client::init_settings(cx);
20887 language::init(cx);
20888 Project::init_settings(cx);
20889 workspace::init_settings(cx);
20890 crate::init(cx);
20891 });
20892
20893 update_test_language_settings(cx, f);
20894}
20895
20896#[track_caller]
20897fn assert_hunk_revert(
20898 not_reverted_text_with_selections: &str,
20899 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
20900 expected_reverted_text_with_selections: &str,
20901 base_text: &str,
20902 cx: &mut EditorLspTestContext,
20903) {
20904 cx.set_state(not_reverted_text_with_selections);
20905 cx.set_head_text(base_text);
20906 cx.executor().run_until_parked();
20907
20908 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
20909 let snapshot = editor.snapshot(window, cx);
20910 let reverted_hunk_statuses = snapshot
20911 .buffer_snapshot
20912 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
20913 .map(|hunk| hunk.status().kind)
20914 .collect::<Vec<_>>();
20915
20916 editor.git_restore(&Default::default(), window, cx);
20917 reverted_hunk_statuses
20918 });
20919 cx.executor().run_until_parked();
20920 cx.assert_editor_state(expected_reverted_text_with_selections);
20921 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
20922}