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 nlmo
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 nlmo
5984 "#
5985 ));
5986
5987 cx.update_editor(|editor, window, cx| {
5988 editor.add_selection_below(&Default::default(), window, cx);
5989 });
5990
5991 cx.assert_editor_state(indoc!(
5992 r#"abc
5993 defˇghi
5994 ˇ
5995 jkˇ
5996 nlmˇo
5997 "#
5998 ));
5999
6000 cx.update_editor(|editor, window, cx| {
6001 editor.add_selection_below(&Default::default(), window, cx);
6002 });
6003
6004 cx.assert_editor_state(indoc!(
6005 r#"abc
6006 defˇghi
6007 ˇ
6008 jkˇ
6009 nlmˇo
6010 ˇ"#
6011 ));
6012
6013 // change selections
6014 cx.set_state(indoc!(
6015 r#"abc
6016 def«ˇg»hi
6017
6018 jk
6019 nlmo
6020 "#
6021 ));
6022
6023 cx.update_editor(|editor, window, cx| {
6024 editor.add_selection_below(&Default::default(), window, cx);
6025 });
6026
6027 cx.assert_editor_state(indoc!(
6028 r#"abc
6029 def«ˇg»hi
6030
6031 jk
6032 nlm«ˇo»
6033 "#
6034 ));
6035
6036 cx.update_editor(|editor, window, cx| {
6037 editor.add_selection_below(&Default::default(), window, cx);
6038 });
6039
6040 cx.assert_editor_state(indoc!(
6041 r#"abc
6042 def«ˇg»hi
6043
6044 jk
6045 nlm«ˇo»
6046 "#
6047 ));
6048
6049 cx.update_editor(|editor, window, cx| {
6050 editor.add_selection_above(&Default::default(), window, cx);
6051 });
6052
6053 cx.assert_editor_state(indoc!(
6054 r#"abc
6055 def«ˇg»hi
6056
6057 jk
6058 nlmo
6059 "#
6060 ));
6061
6062 cx.update_editor(|editor, window, cx| {
6063 editor.add_selection_above(&Default::default(), window, cx);
6064 });
6065
6066 cx.assert_editor_state(indoc!(
6067 r#"abc
6068 def«ˇg»hi
6069
6070 jk
6071 nlmo
6072 "#
6073 ));
6074
6075 // Change selections again
6076 cx.set_state(indoc!(
6077 r#"a«bc
6078 defgˇ»hi
6079
6080 jk
6081 nlmo
6082 "#
6083 ));
6084
6085 cx.update_editor(|editor, window, cx| {
6086 editor.add_selection_below(&Default::default(), window, cx);
6087 });
6088
6089 cx.assert_editor_state(indoc!(
6090 r#"a«bcˇ»
6091 d«efgˇ»hi
6092
6093 j«kˇ»
6094 nlmo
6095 "#
6096 ));
6097
6098 cx.update_editor(|editor, window, cx| {
6099 editor.add_selection_below(&Default::default(), window, cx);
6100 });
6101 cx.assert_editor_state(indoc!(
6102 r#"a«bcˇ»
6103 d«efgˇ»hi
6104
6105 j«kˇ»
6106 n«lmoˇ»
6107 "#
6108 ));
6109 cx.update_editor(|editor, window, cx| {
6110 editor.add_selection_above(&Default::default(), window, cx);
6111 });
6112
6113 cx.assert_editor_state(indoc!(
6114 r#"a«bcˇ»
6115 d«efgˇ»hi
6116
6117 j«kˇ»
6118 nlmo
6119 "#
6120 ));
6121
6122 // Change selections again
6123 cx.set_state(indoc!(
6124 r#"abc
6125 d«ˇefghi
6126
6127 jk
6128 nlm»o
6129 "#
6130 ));
6131
6132 cx.update_editor(|editor, window, cx| {
6133 editor.add_selection_above(&Default::default(), window, cx);
6134 });
6135
6136 cx.assert_editor_state(indoc!(
6137 r#"a«ˇbc»
6138 d«ˇef»ghi
6139
6140 j«ˇk»
6141 n«ˇlm»o
6142 "#
6143 ));
6144
6145 cx.update_editor(|editor, window, cx| {
6146 editor.add_selection_below(&Default::default(), window, cx);
6147 });
6148
6149 cx.assert_editor_state(indoc!(
6150 r#"abc
6151 d«ˇef»ghi
6152
6153 j«ˇk»
6154 n«ˇlm»o
6155 "#
6156 ));
6157}
6158
6159#[gpui::test]
6160async fn test_select_next(cx: &mut TestAppContext) {
6161 init_test(cx, |_| {});
6162
6163 let mut cx = EditorTestContext::new(cx).await;
6164 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6165
6166 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6167 .unwrap();
6168 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6169
6170 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6171 .unwrap();
6172 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
6173
6174 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6175 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6176
6177 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6178 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
6179
6180 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6181 .unwrap();
6182 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6183
6184 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6185 .unwrap();
6186 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6187
6188 // Test selection direction should be preserved
6189 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
6190
6191 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6192 .unwrap();
6193 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
6194}
6195
6196#[gpui::test]
6197async fn test_select_all_matches(cx: &mut TestAppContext) {
6198 init_test(cx, |_| {});
6199
6200 let mut cx = EditorTestContext::new(cx).await;
6201
6202 // Test caret-only selections
6203 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6204 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6205 .unwrap();
6206 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6207
6208 // Test left-to-right selections
6209 cx.set_state("abc\n«abcˇ»\nabc");
6210 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6211 .unwrap();
6212 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
6213
6214 // Test right-to-left selections
6215 cx.set_state("abc\n«ˇabc»\nabc");
6216 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6217 .unwrap();
6218 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
6219
6220 // Test selecting whitespace with caret selection
6221 cx.set_state("abc\nˇ abc\nabc");
6222 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6223 .unwrap();
6224 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
6225
6226 // Test selecting whitespace with left-to-right selection
6227 cx.set_state("abc\n«ˇ »abc\nabc");
6228 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6229 .unwrap();
6230 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
6231
6232 // Test no matches with right-to-left selection
6233 cx.set_state("abc\n« ˇ»abc\nabc");
6234 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6235 .unwrap();
6236 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
6237}
6238
6239#[gpui::test]
6240async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
6241 init_test(cx, |_| {});
6242
6243 let mut cx = EditorTestContext::new(cx).await;
6244
6245 let large_body_1 = "\nd".repeat(200);
6246 let large_body_2 = "\ne".repeat(200);
6247
6248 cx.set_state(&format!(
6249 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
6250 ));
6251 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
6252 let scroll_position = editor.scroll_position(cx);
6253 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
6254 scroll_position
6255 });
6256
6257 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6258 .unwrap();
6259 cx.assert_editor_state(&format!(
6260 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
6261 ));
6262 let scroll_position_after_selection =
6263 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
6264 assert_eq!(
6265 initial_scroll_position, scroll_position_after_selection,
6266 "Scroll position should not change after selecting all matches"
6267 );
6268}
6269
6270#[gpui::test]
6271async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
6272 init_test(cx, |_| {});
6273
6274 let mut cx = EditorLspTestContext::new_rust(
6275 lsp::ServerCapabilities {
6276 document_formatting_provider: Some(lsp::OneOf::Left(true)),
6277 ..Default::default()
6278 },
6279 cx,
6280 )
6281 .await;
6282
6283 cx.set_state(indoc! {"
6284 line 1
6285 line 2
6286 linˇe 3
6287 line 4
6288 line 5
6289 "});
6290
6291 // Make an edit
6292 cx.update_editor(|editor, window, cx| {
6293 editor.handle_input("X", window, cx);
6294 });
6295
6296 // Move cursor to a different position
6297 cx.update_editor(|editor, window, cx| {
6298 editor.change_selections(None, window, cx, |s| {
6299 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
6300 });
6301 });
6302
6303 cx.assert_editor_state(indoc! {"
6304 line 1
6305 line 2
6306 linXe 3
6307 line 4
6308 liˇne 5
6309 "});
6310
6311 cx.lsp
6312 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
6313 Ok(Some(vec![lsp::TextEdit::new(
6314 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
6315 "PREFIX ".to_string(),
6316 )]))
6317 });
6318
6319 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
6320 .unwrap()
6321 .await
6322 .unwrap();
6323
6324 cx.assert_editor_state(indoc! {"
6325 PREFIX line 1
6326 line 2
6327 linXe 3
6328 line 4
6329 liˇne 5
6330 "});
6331
6332 // Undo formatting
6333 cx.update_editor(|editor, window, cx| {
6334 editor.undo(&Default::default(), window, cx);
6335 });
6336
6337 // Verify cursor moved back to position after edit
6338 cx.assert_editor_state(indoc! {"
6339 line 1
6340 line 2
6341 linXˇe 3
6342 line 4
6343 line 5
6344 "});
6345}
6346
6347#[gpui::test]
6348async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
6349 init_test(cx, |_| {});
6350
6351 let mut cx = EditorTestContext::new(cx).await;
6352 cx.set_state(
6353 r#"let foo = 2;
6354lˇet foo = 2;
6355let fooˇ = 2;
6356let foo = 2;
6357let foo = ˇ2;"#,
6358 );
6359
6360 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6361 .unwrap();
6362 cx.assert_editor_state(
6363 r#"let foo = 2;
6364«letˇ» foo = 2;
6365let «fooˇ» = 2;
6366let foo = 2;
6367let foo = «2ˇ»;"#,
6368 );
6369
6370 // noop for multiple selections with different contents
6371 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6372 .unwrap();
6373 cx.assert_editor_state(
6374 r#"let foo = 2;
6375«letˇ» foo = 2;
6376let «fooˇ» = 2;
6377let foo = 2;
6378let foo = «2ˇ»;"#,
6379 );
6380
6381 // Test last selection direction should be preserved
6382 cx.set_state(
6383 r#"let foo = 2;
6384let foo = 2;
6385let «fooˇ» = 2;
6386let «ˇfoo» = 2;
6387let foo = 2;"#,
6388 );
6389
6390 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6391 .unwrap();
6392 cx.assert_editor_state(
6393 r#"let foo = 2;
6394let foo = 2;
6395let «fooˇ» = 2;
6396let «ˇfoo» = 2;
6397let «ˇfoo» = 2;"#,
6398 );
6399}
6400
6401#[gpui::test]
6402async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
6403 init_test(cx, |_| {});
6404
6405 let mut cx =
6406 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
6407
6408 cx.assert_editor_state(indoc! {"
6409 ˇbbb
6410 ccc
6411
6412 bbb
6413 ccc
6414 "});
6415 cx.dispatch_action(SelectPrevious::default());
6416 cx.assert_editor_state(indoc! {"
6417 «bbbˇ»
6418 ccc
6419
6420 bbb
6421 ccc
6422 "});
6423 cx.dispatch_action(SelectPrevious::default());
6424 cx.assert_editor_state(indoc! {"
6425 «bbbˇ»
6426 ccc
6427
6428 «bbbˇ»
6429 ccc
6430 "});
6431}
6432
6433#[gpui::test]
6434async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
6435 init_test(cx, |_| {});
6436
6437 let mut cx = EditorTestContext::new(cx).await;
6438 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6439
6440 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6441 .unwrap();
6442 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6443
6444 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6445 .unwrap();
6446 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
6447
6448 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6449 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6450
6451 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6452 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
6453
6454 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6455 .unwrap();
6456 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
6457
6458 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6459 .unwrap();
6460 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6461}
6462
6463#[gpui::test]
6464async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
6465 init_test(cx, |_| {});
6466
6467 let mut cx = EditorTestContext::new(cx).await;
6468 cx.set_state("aˇ");
6469
6470 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6471 .unwrap();
6472 cx.assert_editor_state("«aˇ»");
6473 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6474 .unwrap();
6475 cx.assert_editor_state("«aˇ»");
6476}
6477
6478#[gpui::test]
6479async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
6480 init_test(cx, |_| {});
6481
6482 let mut cx = EditorTestContext::new(cx).await;
6483 cx.set_state(
6484 r#"let foo = 2;
6485lˇet foo = 2;
6486let fooˇ = 2;
6487let foo = 2;
6488let foo = ˇ2;"#,
6489 );
6490
6491 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6492 .unwrap();
6493 cx.assert_editor_state(
6494 r#"let foo = 2;
6495«letˇ» foo = 2;
6496let «fooˇ» = 2;
6497let foo = 2;
6498let foo = «2ˇ»;"#,
6499 );
6500
6501 // noop for multiple selections with different contents
6502 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6503 .unwrap();
6504 cx.assert_editor_state(
6505 r#"let foo = 2;
6506«letˇ» foo = 2;
6507let «fooˇ» = 2;
6508let foo = 2;
6509let foo = «2ˇ»;"#,
6510 );
6511}
6512
6513#[gpui::test]
6514async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
6515 init_test(cx, |_| {});
6516
6517 let mut cx = EditorTestContext::new(cx).await;
6518 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
6519
6520 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6521 .unwrap();
6522 // selection direction is preserved
6523 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
6524
6525 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6526 .unwrap();
6527 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
6528
6529 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6530 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
6531
6532 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6533 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
6534
6535 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6536 .unwrap();
6537 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
6538
6539 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6540 .unwrap();
6541 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
6542}
6543
6544#[gpui::test]
6545async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
6546 init_test(cx, |_| {});
6547
6548 let language = Arc::new(Language::new(
6549 LanguageConfig::default(),
6550 Some(tree_sitter_rust::LANGUAGE.into()),
6551 ));
6552
6553 let text = r#"
6554 use mod1::mod2::{mod3, mod4};
6555
6556 fn fn_1(param1: bool, param2: &str) {
6557 let var1 = "text";
6558 }
6559 "#
6560 .unindent();
6561
6562 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6563 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6564 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6565
6566 editor
6567 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6568 .await;
6569
6570 editor.update_in(cx, |editor, window, cx| {
6571 editor.change_selections(None, window, cx, |s| {
6572 s.select_display_ranges([
6573 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
6574 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
6575 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
6576 ]);
6577 });
6578 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6579 });
6580 editor.update(cx, |editor, cx| {
6581 assert_text_with_selections(
6582 editor,
6583 indoc! {r#"
6584 use mod1::mod2::{mod3, «mod4ˇ»};
6585
6586 fn fn_1«ˇ(param1: bool, param2: &str)» {
6587 let var1 = "«ˇtext»";
6588 }
6589 "#},
6590 cx,
6591 );
6592 });
6593
6594 editor.update_in(cx, |editor, window, cx| {
6595 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6596 });
6597 editor.update(cx, |editor, cx| {
6598 assert_text_with_selections(
6599 editor,
6600 indoc! {r#"
6601 use mod1::mod2::«{mod3, mod4}ˇ»;
6602
6603 «ˇfn fn_1(param1: bool, param2: &str) {
6604 let var1 = "text";
6605 }»
6606 "#},
6607 cx,
6608 );
6609 });
6610
6611 editor.update_in(cx, |editor, window, cx| {
6612 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6613 });
6614 assert_eq!(
6615 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
6616 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
6617 );
6618
6619 // Trying to expand the selected syntax node one more time has no effect.
6620 editor.update_in(cx, |editor, window, cx| {
6621 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6622 });
6623 assert_eq!(
6624 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
6625 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
6626 );
6627
6628 editor.update_in(cx, |editor, window, cx| {
6629 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6630 });
6631 editor.update(cx, |editor, cx| {
6632 assert_text_with_selections(
6633 editor,
6634 indoc! {r#"
6635 use mod1::mod2::«{mod3, mod4}ˇ»;
6636
6637 «ˇfn fn_1(param1: bool, param2: &str) {
6638 let var1 = "text";
6639 }»
6640 "#},
6641 cx,
6642 );
6643 });
6644
6645 editor.update_in(cx, |editor, window, cx| {
6646 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6647 });
6648 editor.update(cx, |editor, cx| {
6649 assert_text_with_selections(
6650 editor,
6651 indoc! {r#"
6652 use mod1::mod2::{mod3, «mod4ˇ»};
6653
6654 fn fn_1«ˇ(param1: bool, param2: &str)» {
6655 let var1 = "«ˇtext»";
6656 }
6657 "#},
6658 cx,
6659 );
6660 });
6661
6662 editor.update_in(cx, |editor, window, cx| {
6663 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6664 });
6665 editor.update(cx, |editor, cx| {
6666 assert_text_with_selections(
6667 editor,
6668 indoc! {r#"
6669 use mod1::mod2::{mod3, mo«ˇ»d4};
6670
6671 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
6672 let var1 = "te«ˇ»xt";
6673 }
6674 "#},
6675 cx,
6676 );
6677 });
6678
6679 // Trying to shrink the selected syntax node one more time has no effect.
6680 editor.update_in(cx, |editor, window, cx| {
6681 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6682 });
6683 editor.update_in(cx, |editor, _, cx| {
6684 assert_text_with_selections(
6685 editor,
6686 indoc! {r#"
6687 use mod1::mod2::{mod3, mo«ˇ»d4};
6688
6689 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
6690 let var1 = "te«ˇ»xt";
6691 }
6692 "#},
6693 cx,
6694 );
6695 });
6696
6697 // Ensure that we keep expanding the selection if the larger selection starts or ends within
6698 // a fold.
6699 editor.update_in(cx, |editor, window, cx| {
6700 editor.fold_creases(
6701 vec![
6702 Crease::simple(
6703 Point::new(0, 21)..Point::new(0, 24),
6704 FoldPlaceholder::test(),
6705 ),
6706 Crease::simple(
6707 Point::new(3, 20)..Point::new(3, 22),
6708 FoldPlaceholder::test(),
6709 ),
6710 ],
6711 true,
6712 window,
6713 cx,
6714 );
6715 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6716 });
6717 editor.update(cx, |editor, cx| {
6718 assert_text_with_selections(
6719 editor,
6720 indoc! {r#"
6721 use mod1::mod2::«{mod3, mod4}ˇ»;
6722
6723 fn fn_1«ˇ(param1: bool, param2: &str)» {
6724 let var1 = "«ˇtext»";
6725 }
6726 "#},
6727 cx,
6728 );
6729 });
6730}
6731
6732#[gpui::test]
6733async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
6734 init_test(cx, |_| {});
6735
6736 let language = Arc::new(Language::new(
6737 LanguageConfig::default(),
6738 Some(tree_sitter_rust::LANGUAGE.into()),
6739 ));
6740
6741 let text = "let a = 2;";
6742
6743 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6744 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6745 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6746
6747 editor
6748 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6749 .await;
6750
6751 // Test case 1: Cursor at end of word
6752 editor.update_in(cx, |editor, window, cx| {
6753 editor.change_selections(None, window, cx, |s| {
6754 s.select_display_ranges([
6755 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
6756 ]);
6757 });
6758 });
6759 editor.update(cx, |editor, cx| {
6760 assert_text_with_selections(editor, "let aˇ = 2;", cx);
6761 });
6762 editor.update_in(cx, |editor, window, cx| {
6763 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6764 });
6765 editor.update(cx, |editor, cx| {
6766 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
6767 });
6768 editor.update_in(cx, |editor, window, cx| {
6769 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6770 });
6771 editor.update(cx, |editor, cx| {
6772 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
6773 });
6774
6775 // Test case 2: Cursor at end of statement
6776 editor.update_in(cx, |editor, window, cx| {
6777 editor.change_selections(None, window, cx, |s| {
6778 s.select_display_ranges([
6779 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
6780 ]);
6781 });
6782 });
6783 editor.update(cx, |editor, cx| {
6784 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
6785 });
6786 editor.update_in(cx, |editor, window, cx| {
6787 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6788 });
6789 editor.update(cx, |editor, cx| {
6790 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
6791 });
6792}
6793
6794#[gpui::test]
6795async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
6796 init_test(cx, |_| {});
6797
6798 let language = Arc::new(Language::new(
6799 LanguageConfig::default(),
6800 Some(tree_sitter_rust::LANGUAGE.into()),
6801 ));
6802
6803 let text = r#"
6804 use mod1::mod2::{mod3, mod4};
6805
6806 fn fn_1(param1: bool, param2: &str) {
6807 let var1 = "hello world";
6808 }
6809 "#
6810 .unindent();
6811
6812 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6813 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6814 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6815
6816 editor
6817 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6818 .await;
6819
6820 // Test 1: Cursor on a letter of a string word
6821 editor.update_in(cx, |editor, window, cx| {
6822 editor.change_selections(None, window, cx, |s| {
6823 s.select_display_ranges([
6824 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
6825 ]);
6826 });
6827 });
6828 editor.update_in(cx, |editor, window, cx| {
6829 assert_text_with_selections(
6830 editor,
6831 indoc! {r#"
6832 use mod1::mod2::{mod3, mod4};
6833
6834 fn fn_1(param1: bool, param2: &str) {
6835 let var1 = "hˇello world";
6836 }
6837 "#},
6838 cx,
6839 );
6840 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6841 assert_text_with_selections(
6842 editor,
6843 indoc! {r#"
6844 use mod1::mod2::{mod3, mod4};
6845
6846 fn fn_1(param1: bool, param2: &str) {
6847 let var1 = "«ˇhello» world";
6848 }
6849 "#},
6850 cx,
6851 );
6852 });
6853
6854 // Test 2: Partial selection within a word
6855 editor.update_in(cx, |editor, window, cx| {
6856 editor.change_selections(None, window, cx, |s| {
6857 s.select_display_ranges([
6858 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
6859 ]);
6860 });
6861 });
6862 editor.update_in(cx, |editor, window, cx| {
6863 assert_text_with_selections(
6864 editor,
6865 indoc! {r#"
6866 use mod1::mod2::{mod3, mod4};
6867
6868 fn fn_1(param1: bool, param2: &str) {
6869 let var1 = "h«elˇ»lo world";
6870 }
6871 "#},
6872 cx,
6873 );
6874 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6875 assert_text_with_selections(
6876 editor,
6877 indoc! {r#"
6878 use mod1::mod2::{mod3, mod4};
6879
6880 fn fn_1(param1: bool, param2: &str) {
6881 let var1 = "«ˇhello» world";
6882 }
6883 "#},
6884 cx,
6885 );
6886 });
6887
6888 // Test 3: Complete word already selected
6889 editor.update_in(cx, |editor, window, cx| {
6890 editor.change_selections(None, window, cx, |s| {
6891 s.select_display_ranges([
6892 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
6893 ]);
6894 });
6895 });
6896 editor.update_in(cx, |editor, window, cx| {
6897 assert_text_with_selections(
6898 editor,
6899 indoc! {r#"
6900 use mod1::mod2::{mod3, mod4};
6901
6902 fn fn_1(param1: bool, param2: &str) {
6903 let var1 = "«helloˇ» world";
6904 }
6905 "#},
6906 cx,
6907 );
6908 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6909 assert_text_with_selections(
6910 editor,
6911 indoc! {r#"
6912 use mod1::mod2::{mod3, mod4};
6913
6914 fn fn_1(param1: bool, param2: &str) {
6915 let var1 = "«hello worldˇ»";
6916 }
6917 "#},
6918 cx,
6919 );
6920 });
6921
6922 // Test 4: Selection spanning across words
6923 editor.update_in(cx, |editor, window, cx| {
6924 editor.change_selections(None, window, cx, |s| {
6925 s.select_display_ranges([
6926 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
6927 ]);
6928 });
6929 });
6930 editor.update_in(cx, |editor, window, cx| {
6931 assert_text_with_selections(
6932 editor,
6933 indoc! {r#"
6934 use mod1::mod2::{mod3, mod4};
6935
6936 fn fn_1(param1: bool, param2: &str) {
6937 let var1 = "hel«lo woˇ»rld";
6938 }
6939 "#},
6940 cx,
6941 );
6942 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6943 assert_text_with_selections(
6944 editor,
6945 indoc! {r#"
6946 use mod1::mod2::{mod3, mod4};
6947
6948 fn fn_1(param1: bool, param2: &str) {
6949 let var1 = "«ˇhello world»";
6950 }
6951 "#},
6952 cx,
6953 );
6954 });
6955
6956 // Test 5: Expansion beyond string
6957 editor.update_in(cx, |editor, window, cx| {
6958 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6959 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6960 assert_text_with_selections(
6961 editor,
6962 indoc! {r#"
6963 use mod1::mod2::{mod3, mod4};
6964
6965 fn fn_1(param1: bool, param2: &str) {
6966 «ˇlet var1 = "hello world";»
6967 }
6968 "#},
6969 cx,
6970 );
6971 });
6972}
6973
6974#[gpui::test]
6975async fn test_fold_function_bodies(cx: &mut TestAppContext) {
6976 init_test(cx, |_| {});
6977
6978 let base_text = r#"
6979 impl A {
6980 // this is an uncommitted comment
6981
6982 fn b() {
6983 c();
6984 }
6985
6986 // this is another uncommitted comment
6987
6988 fn d() {
6989 // e
6990 // f
6991 }
6992 }
6993
6994 fn g() {
6995 // h
6996 }
6997 "#
6998 .unindent();
6999
7000 let text = r#"
7001 ˇimpl A {
7002
7003 fn b() {
7004 c();
7005 }
7006
7007 fn d() {
7008 // e
7009 // f
7010 }
7011 }
7012
7013 fn g() {
7014 // h
7015 }
7016 "#
7017 .unindent();
7018
7019 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
7020 cx.set_state(&text);
7021 cx.set_head_text(&base_text);
7022 cx.update_editor(|editor, window, cx| {
7023 editor.expand_all_diff_hunks(&Default::default(), window, cx);
7024 });
7025
7026 cx.assert_state_with_diff(
7027 "
7028 ˇimpl A {
7029 - // this is an uncommitted comment
7030
7031 fn b() {
7032 c();
7033 }
7034
7035 - // this is another uncommitted comment
7036 -
7037 fn d() {
7038 // e
7039 // f
7040 }
7041 }
7042
7043 fn g() {
7044 // h
7045 }
7046 "
7047 .unindent(),
7048 );
7049
7050 let expected_display_text = "
7051 impl A {
7052 // this is an uncommitted comment
7053
7054 fn b() {
7055 ⋯
7056 }
7057
7058 // this is another uncommitted comment
7059
7060 fn d() {
7061 ⋯
7062 }
7063 }
7064
7065 fn g() {
7066 ⋯
7067 }
7068 "
7069 .unindent();
7070
7071 cx.update_editor(|editor, window, cx| {
7072 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
7073 assert_eq!(editor.display_text(cx), expected_display_text);
7074 });
7075}
7076
7077#[gpui::test]
7078async fn test_autoindent(cx: &mut TestAppContext) {
7079 init_test(cx, |_| {});
7080
7081 let language = Arc::new(
7082 Language::new(
7083 LanguageConfig {
7084 brackets: BracketPairConfig {
7085 pairs: vec![
7086 BracketPair {
7087 start: "{".to_string(),
7088 end: "}".to_string(),
7089 close: false,
7090 surround: false,
7091 newline: true,
7092 },
7093 BracketPair {
7094 start: "(".to_string(),
7095 end: ")".to_string(),
7096 close: false,
7097 surround: false,
7098 newline: true,
7099 },
7100 ],
7101 ..Default::default()
7102 },
7103 ..Default::default()
7104 },
7105 Some(tree_sitter_rust::LANGUAGE.into()),
7106 )
7107 .with_indents_query(
7108 r#"
7109 (_ "(" ")" @end) @indent
7110 (_ "{" "}" @end) @indent
7111 "#,
7112 )
7113 .unwrap(),
7114 );
7115
7116 let text = "fn a() {}";
7117
7118 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7119 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7120 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7121 editor
7122 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7123 .await;
7124
7125 editor.update_in(cx, |editor, window, cx| {
7126 editor.change_selections(None, window, cx, |s| s.select_ranges([5..5, 8..8, 9..9]));
7127 editor.newline(&Newline, window, cx);
7128 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
7129 assert_eq!(
7130 editor.selections.ranges(cx),
7131 &[
7132 Point::new(1, 4)..Point::new(1, 4),
7133 Point::new(3, 4)..Point::new(3, 4),
7134 Point::new(5, 0)..Point::new(5, 0)
7135 ]
7136 );
7137 });
7138}
7139
7140#[gpui::test]
7141async fn test_autoindent_selections(cx: &mut TestAppContext) {
7142 init_test(cx, |_| {});
7143
7144 {
7145 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
7146 cx.set_state(indoc! {"
7147 impl A {
7148
7149 fn b() {}
7150
7151 «fn c() {
7152
7153 }ˇ»
7154 }
7155 "});
7156
7157 cx.update_editor(|editor, window, cx| {
7158 editor.autoindent(&Default::default(), window, cx);
7159 });
7160
7161 cx.assert_editor_state(indoc! {"
7162 impl A {
7163
7164 fn b() {}
7165
7166 «fn c() {
7167
7168 }ˇ»
7169 }
7170 "});
7171 }
7172
7173 {
7174 let mut cx = EditorTestContext::new_multibuffer(
7175 cx,
7176 [indoc! { "
7177 impl A {
7178 «
7179 // a
7180 fn b(){}
7181 »
7182 «
7183 }
7184 fn c(){}
7185 »
7186 "}],
7187 );
7188
7189 let buffer = cx.update_editor(|editor, _, cx| {
7190 let buffer = editor.buffer().update(cx, |buffer, _| {
7191 buffer.all_buffers().iter().next().unwrap().clone()
7192 });
7193 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7194 buffer
7195 });
7196
7197 cx.run_until_parked();
7198 cx.update_editor(|editor, window, cx| {
7199 editor.select_all(&Default::default(), window, cx);
7200 editor.autoindent(&Default::default(), window, cx)
7201 });
7202 cx.run_until_parked();
7203
7204 cx.update(|_, cx| {
7205 assert_eq!(
7206 buffer.read(cx).text(),
7207 indoc! { "
7208 impl A {
7209
7210 // a
7211 fn b(){}
7212
7213
7214 }
7215 fn c(){}
7216
7217 " }
7218 )
7219 });
7220 }
7221}
7222
7223#[gpui::test]
7224async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
7225 init_test(cx, |_| {});
7226
7227 let mut cx = EditorTestContext::new(cx).await;
7228
7229 let language = Arc::new(Language::new(
7230 LanguageConfig {
7231 brackets: BracketPairConfig {
7232 pairs: vec![
7233 BracketPair {
7234 start: "{".to_string(),
7235 end: "}".to_string(),
7236 close: true,
7237 surround: true,
7238 newline: true,
7239 },
7240 BracketPair {
7241 start: "(".to_string(),
7242 end: ")".to_string(),
7243 close: true,
7244 surround: true,
7245 newline: true,
7246 },
7247 BracketPair {
7248 start: "/*".to_string(),
7249 end: " */".to_string(),
7250 close: true,
7251 surround: true,
7252 newline: true,
7253 },
7254 BracketPair {
7255 start: "[".to_string(),
7256 end: "]".to_string(),
7257 close: false,
7258 surround: false,
7259 newline: true,
7260 },
7261 BracketPair {
7262 start: "\"".to_string(),
7263 end: "\"".to_string(),
7264 close: true,
7265 surround: true,
7266 newline: false,
7267 },
7268 BracketPair {
7269 start: "<".to_string(),
7270 end: ">".to_string(),
7271 close: false,
7272 surround: true,
7273 newline: true,
7274 },
7275 ],
7276 ..Default::default()
7277 },
7278 autoclose_before: "})]".to_string(),
7279 ..Default::default()
7280 },
7281 Some(tree_sitter_rust::LANGUAGE.into()),
7282 ));
7283
7284 cx.language_registry().add(language.clone());
7285 cx.update_buffer(|buffer, cx| {
7286 buffer.set_language(Some(language), cx);
7287 });
7288
7289 cx.set_state(
7290 &r#"
7291 🏀ˇ
7292 εˇ
7293 ❤️ˇ
7294 "#
7295 .unindent(),
7296 );
7297
7298 // autoclose multiple nested brackets at multiple cursors
7299 cx.update_editor(|editor, window, cx| {
7300 editor.handle_input("{", window, cx);
7301 editor.handle_input("{", window, cx);
7302 editor.handle_input("{", window, cx);
7303 });
7304 cx.assert_editor_state(
7305 &"
7306 🏀{{{ˇ}}}
7307 ε{{{ˇ}}}
7308 ❤️{{{ˇ}}}
7309 "
7310 .unindent(),
7311 );
7312
7313 // insert a different closing bracket
7314 cx.update_editor(|editor, window, cx| {
7315 editor.handle_input(")", window, cx);
7316 });
7317 cx.assert_editor_state(
7318 &"
7319 🏀{{{)ˇ}}}
7320 ε{{{)ˇ}}}
7321 ❤️{{{)ˇ}}}
7322 "
7323 .unindent(),
7324 );
7325
7326 // skip over the auto-closed brackets when typing a closing bracket
7327 cx.update_editor(|editor, window, cx| {
7328 editor.move_right(&MoveRight, window, cx);
7329 editor.handle_input("}", window, cx);
7330 editor.handle_input("}", window, cx);
7331 editor.handle_input("}", window, cx);
7332 });
7333 cx.assert_editor_state(
7334 &"
7335 🏀{{{)}}}}ˇ
7336 ε{{{)}}}}ˇ
7337 ❤️{{{)}}}}ˇ
7338 "
7339 .unindent(),
7340 );
7341
7342 // autoclose multi-character pairs
7343 cx.set_state(
7344 &"
7345 ˇ
7346 ˇ
7347 "
7348 .unindent(),
7349 );
7350 cx.update_editor(|editor, window, cx| {
7351 editor.handle_input("/", window, cx);
7352 editor.handle_input("*", window, cx);
7353 });
7354 cx.assert_editor_state(
7355 &"
7356 /*ˇ */
7357 /*ˇ */
7358 "
7359 .unindent(),
7360 );
7361
7362 // one cursor autocloses a multi-character pair, one cursor
7363 // does not autoclose.
7364 cx.set_state(
7365 &"
7366 /ˇ
7367 ˇ
7368 "
7369 .unindent(),
7370 );
7371 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
7372 cx.assert_editor_state(
7373 &"
7374 /*ˇ */
7375 *ˇ
7376 "
7377 .unindent(),
7378 );
7379
7380 // Don't autoclose if the next character isn't whitespace and isn't
7381 // listed in the language's "autoclose_before" section.
7382 cx.set_state("ˇa b");
7383 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7384 cx.assert_editor_state("{ˇa b");
7385
7386 // Don't autoclose if `close` is false for the bracket pair
7387 cx.set_state("ˇ");
7388 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
7389 cx.assert_editor_state("[ˇ");
7390
7391 // Surround with brackets if text is selected
7392 cx.set_state("«aˇ» b");
7393 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7394 cx.assert_editor_state("{«aˇ»} b");
7395
7396 // Autoclose when not immediately after a word character
7397 cx.set_state("a ˇ");
7398 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7399 cx.assert_editor_state("a \"ˇ\"");
7400
7401 // Autoclose pair where the start and end characters are the same
7402 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7403 cx.assert_editor_state("a \"\"ˇ");
7404
7405 // Don't autoclose when immediately after a word character
7406 cx.set_state("aˇ");
7407 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7408 cx.assert_editor_state("a\"ˇ");
7409
7410 // Do autoclose when after a non-word character
7411 cx.set_state("{ˇ");
7412 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7413 cx.assert_editor_state("{\"ˇ\"");
7414
7415 // Non identical pairs autoclose regardless of preceding character
7416 cx.set_state("aˇ");
7417 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7418 cx.assert_editor_state("a{ˇ}");
7419
7420 // Don't autoclose pair if autoclose is disabled
7421 cx.set_state("ˇ");
7422 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
7423 cx.assert_editor_state("<ˇ");
7424
7425 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
7426 cx.set_state("«aˇ» b");
7427 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
7428 cx.assert_editor_state("<«aˇ»> b");
7429}
7430
7431#[gpui::test]
7432async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
7433 init_test(cx, |settings| {
7434 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
7435 });
7436
7437 let mut cx = EditorTestContext::new(cx).await;
7438
7439 let language = Arc::new(Language::new(
7440 LanguageConfig {
7441 brackets: BracketPairConfig {
7442 pairs: vec![
7443 BracketPair {
7444 start: "{".to_string(),
7445 end: "}".to_string(),
7446 close: true,
7447 surround: true,
7448 newline: true,
7449 },
7450 BracketPair {
7451 start: "(".to_string(),
7452 end: ")".to_string(),
7453 close: true,
7454 surround: true,
7455 newline: true,
7456 },
7457 BracketPair {
7458 start: "[".to_string(),
7459 end: "]".to_string(),
7460 close: false,
7461 surround: false,
7462 newline: true,
7463 },
7464 ],
7465 ..Default::default()
7466 },
7467 autoclose_before: "})]".to_string(),
7468 ..Default::default()
7469 },
7470 Some(tree_sitter_rust::LANGUAGE.into()),
7471 ));
7472
7473 cx.language_registry().add(language.clone());
7474 cx.update_buffer(|buffer, cx| {
7475 buffer.set_language(Some(language), cx);
7476 });
7477
7478 cx.set_state(
7479 &"
7480 ˇ
7481 ˇ
7482 ˇ
7483 "
7484 .unindent(),
7485 );
7486
7487 // ensure only matching closing brackets are skipped over
7488 cx.update_editor(|editor, window, cx| {
7489 editor.handle_input("}", window, cx);
7490 editor.move_left(&MoveLeft, window, cx);
7491 editor.handle_input(")", window, cx);
7492 editor.move_left(&MoveLeft, window, cx);
7493 });
7494 cx.assert_editor_state(
7495 &"
7496 ˇ)}
7497 ˇ)}
7498 ˇ)}
7499 "
7500 .unindent(),
7501 );
7502
7503 // skip-over closing brackets at multiple cursors
7504 cx.update_editor(|editor, window, cx| {
7505 editor.handle_input(")", window, cx);
7506 editor.handle_input("}", window, cx);
7507 });
7508 cx.assert_editor_state(
7509 &"
7510 )}ˇ
7511 )}ˇ
7512 )}ˇ
7513 "
7514 .unindent(),
7515 );
7516
7517 // ignore non-close brackets
7518 cx.update_editor(|editor, window, cx| {
7519 editor.handle_input("]", window, cx);
7520 editor.move_left(&MoveLeft, window, cx);
7521 editor.handle_input("]", window, cx);
7522 });
7523 cx.assert_editor_state(
7524 &"
7525 )}]ˇ]
7526 )}]ˇ]
7527 )}]ˇ]
7528 "
7529 .unindent(),
7530 );
7531}
7532
7533#[gpui::test]
7534async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
7535 init_test(cx, |_| {});
7536
7537 let mut cx = EditorTestContext::new(cx).await;
7538
7539 let html_language = Arc::new(
7540 Language::new(
7541 LanguageConfig {
7542 name: "HTML".into(),
7543 brackets: BracketPairConfig {
7544 pairs: vec![
7545 BracketPair {
7546 start: "<".into(),
7547 end: ">".into(),
7548 close: true,
7549 ..Default::default()
7550 },
7551 BracketPair {
7552 start: "{".into(),
7553 end: "}".into(),
7554 close: true,
7555 ..Default::default()
7556 },
7557 BracketPair {
7558 start: "(".into(),
7559 end: ")".into(),
7560 close: true,
7561 ..Default::default()
7562 },
7563 ],
7564 ..Default::default()
7565 },
7566 autoclose_before: "})]>".into(),
7567 ..Default::default()
7568 },
7569 Some(tree_sitter_html::LANGUAGE.into()),
7570 )
7571 .with_injection_query(
7572 r#"
7573 (script_element
7574 (raw_text) @injection.content
7575 (#set! injection.language "javascript"))
7576 "#,
7577 )
7578 .unwrap(),
7579 );
7580
7581 let javascript_language = Arc::new(Language::new(
7582 LanguageConfig {
7583 name: "JavaScript".into(),
7584 brackets: BracketPairConfig {
7585 pairs: vec![
7586 BracketPair {
7587 start: "/*".into(),
7588 end: " */".into(),
7589 close: true,
7590 ..Default::default()
7591 },
7592 BracketPair {
7593 start: "{".into(),
7594 end: "}".into(),
7595 close: true,
7596 ..Default::default()
7597 },
7598 BracketPair {
7599 start: "(".into(),
7600 end: ")".into(),
7601 close: true,
7602 ..Default::default()
7603 },
7604 ],
7605 ..Default::default()
7606 },
7607 autoclose_before: "})]>".into(),
7608 ..Default::default()
7609 },
7610 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
7611 ));
7612
7613 cx.language_registry().add(html_language.clone());
7614 cx.language_registry().add(javascript_language.clone());
7615
7616 cx.update_buffer(|buffer, cx| {
7617 buffer.set_language(Some(html_language), cx);
7618 });
7619
7620 cx.set_state(
7621 &r#"
7622 <body>ˇ
7623 <script>
7624 var x = 1;ˇ
7625 </script>
7626 </body>ˇ
7627 "#
7628 .unindent(),
7629 );
7630
7631 // Precondition: different languages are active at different locations.
7632 cx.update_editor(|editor, window, cx| {
7633 let snapshot = editor.snapshot(window, cx);
7634 let cursors = editor.selections.ranges::<usize>(cx);
7635 let languages = cursors
7636 .iter()
7637 .map(|c| snapshot.language_at(c.start).unwrap().name())
7638 .collect::<Vec<_>>();
7639 assert_eq!(
7640 languages,
7641 &["HTML".into(), "JavaScript".into(), "HTML".into()]
7642 );
7643 });
7644
7645 // Angle brackets autoclose in HTML, but not JavaScript.
7646 cx.update_editor(|editor, window, cx| {
7647 editor.handle_input("<", window, cx);
7648 editor.handle_input("a", window, cx);
7649 });
7650 cx.assert_editor_state(
7651 &r#"
7652 <body><aˇ>
7653 <script>
7654 var x = 1;<aˇ
7655 </script>
7656 </body><aˇ>
7657 "#
7658 .unindent(),
7659 );
7660
7661 // Curly braces and parens autoclose in both HTML and JavaScript.
7662 cx.update_editor(|editor, window, cx| {
7663 editor.handle_input(" b=", window, cx);
7664 editor.handle_input("{", window, cx);
7665 editor.handle_input("c", window, cx);
7666 editor.handle_input("(", window, cx);
7667 });
7668 cx.assert_editor_state(
7669 &r#"
7670 <body><a b={c(ˇ)}>
7671 <script>
7672 var x = 1;<a b={c(ˇ)}
7673 </script>
7674 </body><a b={c(ˇ)}>
7675 "#
7676 .unindent(),
7677 );
7678
7679 // Brackets that were already autoclosed are skipped.
7680 cx.update_editor(|editor, window, cx| {
7681 editor.handle_input(")", window, cx);
7682 editor.handle_input("d", window, cx);
7683 editor.handle_input("}", window, cx);
7684 });
7685 cx.assert_editor_state(
7686 &r#"
7687 <body><a b={c()d}ˇ>
7688 <script>
7689 var x = 1;<a b={c()d}ˇ
7690 </script>
7691 </body><a b={c()d}ˇ>
7692 "#
7693 .unindent(),
7694 );
7695 cx.update_editor(|editor, window, cx| {
7696 editor.handle_input(">", window, cx);
7697 });
7698 cx.assert_editor_state(
7699 &r#"
7700 <body><a b={c()d}>ˇ
7701 <script>
7702 var x = 1;<a b={c()d}>ˇ
7703 </script>
7704 </body><a b={c()d}>ˇ
7705 "#
7706 .unindent(),
7707 );
7708
7709 // Reset
7710 cx.set_state(
7711 &r#"
7712 <body>ˇ
7713 <script>
7714 var x = 1;ˇ
7715 </script>
7716 </body>ˇ
7717 "#
7718 .unindent(),
7719 );
7720
7721 cx.update_editor(|editor, window, cx| {
7722 editor.handle_input("<", window, cx);
7723 });
7724 cx.assert_editor_state(
7725 &r#"
7726 <body><ˇ>
7727 <script>
7728 var x = 1;<ˇ
7729 </script>
7730 </body><ˇ>
7731 "#
7732 .unindent(),
7733 );
7734
7735 // When backspacing, the closing angle brackets are removed.
7736 cx.update_editor(|editor, window, cx| {
7737 editor.backspace(&Backspace, window, cx);
7738 });
7739 cx.assert_editor_state(
7740 &r#"
7741 <body>ˇ
7742 <script>
7743 var x = 1;ˇ
7744 </script>
7745 </body>ˇ
7746 "#
7747 .unindent(),
7748 );
7749
7750 // Block comments autoclose in JavaScript, but not HTML.
7751 cx.update_editor(|editor, window, cx| {
7752 editor.handle_input("/", window, cx);
7753 editor.handle_input("*", window, cx);
7754 });
7755 cx.assert_editor_state(
7756 &r#"
7757 <body>/*ˇ
7758 <script>
7759 var x = 1;/*ˇ */
7760 </script>
7761 </body>/*ˇ
7762 "#
7763 .unindent(),
7764 );
7765}
7766
7767#[gpui::test]
7768async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
7769 init_test(cx, |_| {});
7770
7771 let mut cx = EditorTestContext::new(cx).await;
7772
7773 let rust_language = Arc::new(
7774 Language::new(
7775 LanguageConfig {
7776 name: "Rust".into(),
7777 brackets: serde_json::from_value(json!([
7778 { "start": "{", "end": "}", "close": true, "newline": true },
7779 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
7780 ]))
7781 .unwrap(),
7782 autoclose_before: "})]>".into(),
7783 ..Default::default()
7784 },
7785 Some(tree_sitter_rust::LANGUAGE.into()),
7786 )
7787 .with_override_query("(string_literal) @string")
7788 .unwrap(),
7789 );
7790
7791 cx.language_registry().add(rust_language.clone());
7792 cx.update_buffer(|buffer, cx| {
7793 buffer.set_language(Some(rust_language), cx);
7794 });
7795
7796 cx.set_state(
7797 &r#"
7798 let x = ˇ
7799 "#
7800 .unindent(),
7801 );
7802
7803 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
7804 cx.update_editor(|editor, window, cx| {
7805 editor.handle_input("\"", window, cx);
7806 });
7807 cx.assert_editor_state(
7808 &r#"
7809 let x = "ˇ"
7810 "#
7811 .unindent(),
7812 );
7813
7814 // Inserting another quotation mark. The cursor moves across the existing
7815 // automatically-inserted quotation mark.
7816 cx.update_editor(|editor, window, cx| {
7817 editor.handle_input("\"", window, cx);
7818 });
7819 cx.assert_editor_state(
7820 &r#"
7821 let x = ""ˇ
7822 "#
7823 .unindent(),
7824 );
7825
7826 // Reset
7827 cx.set_state(
7828 &r#"
7829 let x = ˇ
7830 "#
7831 .unindent(),
7832 );
7833
7834 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
7835 cx.update_editor(|editor, window, cx| {
7836 editor.handle_input("\"", window, cx);
7837 editor.handle_input(" ", window, cx);
7838 editor.move_left(&Default::default(), window, cx);
7839 editor.handle_input("\\", window, cx);
7840 editor.handle_input("\"", window, cx);
7841 });
7842 cx.assert_editor_state(
7843 &r#"
7844 let x = "\"ˇ "
7845 "#
7846 .unindent(),
7847 );
7848
7849 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
7850 // mark. Nothing is inserted.
7851 cx.update_editor(|editor, window, cx| {
7852 editor.move_right(&Default::default(), window, cx);
7853 editor.handle_input("\"", window, cx);
7854 });
7855 cx.assert_editor_state(
7856 &r#"
7857 let x = "\" "ˇ
7858 "#
7859 .unindent(),
7860 );
7861}
7862
7863#[gpui::test]
7864async fn test_surround_with_pair(cx: &mut TestAppContext) {
7865 init_test(cx, |_| {});
7866
7867 let language = Arc::new(Language::new(
7868 LanguageConfig {
7869 brackets: BracketPairConfig {
7870 pairs: vec![
7871 BracketPair {
7872 start: "{".to_string(),
7873 end: "}".to_string(),
7874 close: true,
7875 surround: true,
7876 newline: true,
7877 },
7878 BracketPair {
7879 start: "/* ".to_string(),
7880 end: "*/".to_string(),
7881 close: true,
7882 surround: true,
7883 ..Default::default()
7884 },
7885 ],
7886 ..Default::default()
7887 },
7888 ..Default::default()
7889 },
7890 Some(tree_sitter_rust::LANGUAGE.into()),
7891 ));
7892
7893 let text = r#"
7894 a
7895 b
7896 c
7897 "#
7898 .unindent();
7899
7900 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7901 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7902 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7903 editor
7904 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7905 .await;
7906
7907 editor.update_in(cx, |editor, window, cx| {
7908 editor.change_selections(None, window, cx, |s| {
7909 s.select_display_ranges([
7910 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7911 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7912 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
7913 ])
7914 });
7915
7916 editor.handle_input("{", window, cx);
7917 editor.handle_input("{", window, cx);
7918 editor.handle_input("{", window, cx);
7919 assert_eq!(
7920 editor.text(cx),
7921 "
7922 {{{a}}}
7923 {{{b}}}
7924 {{{c}}}
7925 "
7926 .unindent()
7927 );
7928 assert_eq!(
7929 editor.selections.display_ranges(cx),
7930 [
7931 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
7932 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
7933 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
7934 ]
7935 );
7936
7937 editor.undo(&Undo, window, cx);
7938 editor.undo(&Undo, window, cx);
7939 editor.undo(&Undo, window, cx);
7940 assert_eq!(
7941 editor.text(cx),
7942 "
7943 a
7944 b
7945 c
7946 "
7947 .unindent()
7948 );
7949 assert_eq!(
7950 editor.selections.display_ranges(cx),
7951 [
7952 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7953 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7954 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
7955 ]
7956 );
7957
7958 // Ensure inserting the first character of a multi-byte bracket pair
7959 // doesn't surround the selections with the bracket.
7960 editor.handle_input("/", window, cx);
7961 assert_eq!(
7962 editor.text(cx),
7963 "
7964 /
7965 /
7966 /
7967 "
7968 .unindent()
7969 );
7970 assert_eq!(
7971 editor.selections.display_ranges(cx),
7972 [
7973 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
7974 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
7975 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
7976 ]
7977 );
7978
7979 editor.undo(&Undo, window, cx);
7980 assert_eq!(
7981 editor.text(cx),
7982 "
7983 a
7984 b
7985 c
7986 "
7987 .unindent()
7988 );
7989 assert_eq!(
7990 editor.selections.display_ranges(cx),
7991 [
7992 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7993 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7994 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
7995 ]
7996 );
7997
7998 // Ensure inserting the last character of a multi-byte bracket pair
7999 // doesn't surround the selections with the bracket.
8000 editor.handle_input("*", window, cx);
8001 assert_eq!(
8002 editor.text(cx),
8003 "
8004 *
8005 *
8006 *
8007 "
8008 .unindent()
8009 );
8010 assert_eq!(
8011 editor.selections.display_ranges(cx),
8012 [
8013 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
8014 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
8015 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
8016 ]
8017 );
8018 });
8019}
8020
8021#[gpui::test]
8022async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
8023 init_test(cx, |_| {});
8024
8025 let language = Arc::new(Language::new(
8026 LanguageConfig {
8027 brackets: BracketPairConfig {
8028 pairs: vec![BracketPair {
8029 start: "{".to_string(),
8030 end: "}".to_string(),
8031 close: true,
8032 surround: true,
8033 newline: true,
8034 }],
8035 ..Default::default()
8036 },
8037 autoclose_before: "}".to_string(),
8038 ..Default::default()
8039 },
8040 Some(tree_sitter_rust::LANGUAGE.into()),
8041 ));
8042
8043 let text = r#"
8044 a
8045 b
8046 c
8047 "#
8048 .unindent();
8049
8050 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8051 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8052 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8053 editor
8054 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8055 .await;
8056
8057 editor.update_in(cx, |editor, window, cx| {
8058 editor.change_selections(None, window, cx, |s| {
8059 s.select_ranges([
8060 Point::new(0, 1)..Point::new(0, 1),
8061 Point::new(1, 1)..Point::new(1, 1),
8062 Point::new(2, 1)..Point::new(2, 1),
8063 ])
8064 });
8065
8066 editor.handle_input("{", window, cx);
8067 editor.handle_input("{", window, cx);
8068 editor.handle_input("_", window, cx);
8069 assert_eq!(
8070 editor.text(cx),
8071 "
8072 a{{_}}
8073 b{{_}}
8074 c{{_}}
8075 "
8076 .unindent()
8077 );
8078 assert_eq!(
8079 editor.selections.ranges::<Point>(cx),
8080 [
8081 Point::new(0, 4)..Point::new(0, 4),
8082 Point::new(1, 4)..Point::new(1, 4),
8083 Point::new(2, 4)..Point::new(2, 4)
8084 ]
8085 );
8086
8087 editor.backspace(&Default::default(), window, cx);
8088 editor.backspace(&Default::default(), window, cx);
8089 assert_eq!(
8090 editor.text(cx),
8091 "
8092 a{}
8093 b{}
8094 c{}
8095 "
8096 .unindent()
8097 );
8098 assert_eq!(
8099 editor.selections.ranges::<Point>(cx),
8100 [
8101 Point::new(0, 2)..Point::new(0, 2),
8102 Point::new(1, 2)..Point::new(1, 2),
8103 Point::new(2, 2)..Point::new(2, 2)
8104 ]
8105 );
8106
8107 editor.delete_to_previous_word_start(&Default::default(), window, cx);
8108 assert_eq!(
8109 editor.text(cx),
8110 "
8111 a
8112 b
8113 c
8114 "
8115 .unindent()
8116 );
8117 assert_eq!(
8118 editor.selections.ranges::<Point>(cx),
8119 [
8120 Point::new(0, 1)..Point::new(0, 1),
8121 Point::new(1, 1)..Point::new(1, 1),
8122 Point::new(2, 1)..Point::new(2, 1)
8123 ]
8124 );
8125 });
8126}
8127
8128#[gpui::test]
8129async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
8130 init_test(cx, |settings| {
8131 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
8132 });
8133
8134 let mut cx = EditorTestContext::new(cx).await;
8135
8136 let language = Arc::new(Language::new(
8137 LanguageConfig {
8138 brackets: BracketPairConfig {
8139 pairs: vec![
8140 BracketPair {
8141 start: "{".to_string(),
8142 end: "}".to_string(),
8143 close: true,
8144 surround: true,
8145 newline: true,
8146 },
8147 BracketPair {
8148 start: "(".to_string(),
8149 end: ")".to_string(),
8150 close: true,
8151 surround: true,
8152 newline: true,
8153 },
8154 BracketPair {
8155 start: "[".to_string(),
8156 end: "]".to_string(),
8157 close: false,
8158 surround: true,
8159 newline: true,
8160 },
8161 ],
8162 ..Default::default()
8163 },
8164 autoclose_before: "})]".to_string(),
8165 ..Default::default()
8166 },
8167 Some(tree_sitter_rust::LANGUAGE.into()),
8168 ));
8169
8170 cx.language_registry().add(language.clone());
8171 cx.update_buffer(|buffer, cx| {
8172 buffer.set_language(Some(language), cx);
8173 });
8174
8175 cx.set_state(
8176 &"
8177 {(ˇ)}
8178 [[ˇ]]
8179 {(ˇ)}
8180 "
8181 .unindent(),
8182 );
8183
8184 cx.update_editor(|editor, window, cx| {
8185 editor.backspace(&Default::default(), window, cx);
8186 editor.backspace(&Default::default(), window, cx);
8187 });
8188
8189 cx.assert_editor_state(
8190 &"
8191 ˇ
8192 ˇ]]
8193 ˇ
8194 "
8195 .unindent(),
8196 );
8197
8198 cx.update_editor(|editor, window, cx| {
8199 editor.handle_input("{", window, cx);
8200 editor.handle_input("{", window, cx);
8201 editor.move_right(&MoveRight, window, cx);
8202 editor.move_right(&MoveRight, window, cx);
8203 editor.move_left(&MoveLeft, window, cx);
8204 editor.move_left(&MoveLeft, window, cx);
8205 editor.backspace(&Default::default(), window, cx);
8206 });
8207
8208 cx.assert_editor_state(
8209 &"
8210 {ˇ}
8211 {ˇ}]]
8212 {ˇ}
8213 "
8214 .unindent(),
8215 );
8216
8217 cx.update_editor(|editor, window, cx| {
8218 editor.backspace(&Default::default(), window, cx);
8219 });
8220
8221 cx.assert_editor_state(
8222 &"
8223 ˇ
8224 ˇ]]
8225 ˇ
8226 "
8227 .unindent(),
8228 );
8229}
8230
8231#[gpui::test]
8232async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
8233 init_test(cx, |_| {});
8234
8235 let language = Arc::new(Language::new(
8236 LanguageConfig::default(),
8237 Some(tree_sitter_rust::LANGUAGE.into()),
8238 ));
8239
8240 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
8241 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8242 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8243 editor
8244 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8245 .await;
8246
8247 editor.update_in(cx, |editor, window, cx| {
8248 editor.set_auto_replace_emoji_shortcode(true);
8249
8250 editor.handle_input("Hello ", window, cx);
8251 editor.handle_input(":wave", window, cx);
8252 assert_eq!(editor.text(cx), "Hello :wave".unindent());
8253
8254 editor.handle_input(":", window, cx);
8255 assert_eq!(editor.text(cx), "Hello 👋".unindent());
8256
8257 editor.handle_input(" :smile", window, cx);
8258 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
8259
8260 editor.handle_input(":", window, cx);
8261 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
8262
8263 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
8264 editor.handle_input(":wave", window, cx);
8265 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
8266
8267 editor.handle_input(":", window, cx);
8268 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
8269
8270 editor.handle_input(":1", window, cx);
8271 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
8272
8273 editor.handle_input(":", window, cx);
8274 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
8275
8276 // Ensure shortcode does not get replaced when it is part of a word
8277 editor.handle_input(" Test:wave", window, cx);
8278 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
8279
8280 editor.handle_input(":", window, cx);
8281 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
8282
8283 editor.set_auto_replace_emoji_shortcode(false);
8284
8285 // Ensure shortcode does not get replaced when auto replace is off
8286 editor.handle_input(" :wave", window, cx);
8287 assert_eq!(
8288 editor.text(cx),
8289 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
8290 );
8291
8292 editor.handle_input(":", window, cx);
8293 assert_eq!(
8294 editor.text(cx),
8295 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
8296 );
8297 });
8298}
8299
8300#[gpui::test]
8301async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
8302 init_test(cx, |_| {});
8303
8304 let (text, insertion_ranges) = marked_text_ranges(
8305 indoc! {"
8306 ˇ
8307 "},
8308 false,
8309 );
8310
8311 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
8312 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8313
8314 _ = editor.update_in(cx, |editor, window, cx| {
8315 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
8316
8317 editor
8318 .insert_snippet(&insertion_ranges, snippet, window, cx)
8319 .unwrap();
8320
8321 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
8322 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
8323 assert_eq!(editor.text(cx), expected_text);
8324 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
8325 }
8326
8327 assert(
8328 editor,
8329 cx,
8330 indoc! {"
8331 type «» =•
8332 "},
8333 );
8334
8335 assert!(editor.context_menu_visible(), "There should be a matches");
8336 });
8337}
8338
8339#[gpui::test]
8340async fn test_snippets(cx: &mut TestAppContext) {
8341 init_test(cx, |_| {});
8342
8343 let (text, insertion_ranges) = marked_text_ranges(
8344 indoc! {"
8345 a.ˇ b
8346 a.ˇ b
8347 a.ˇ b
8348 "},
8349 false,
8350 );
8351
8352 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
8353 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8354
8355 editor.update_in(cx, |editor, window, cx| {
8356 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
8357
8358 editor
8359 .insert_snippet(&insertion_ranges, snippet, window, cx)
8360 .unwrap();
8361
8362 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
8363 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
8364 assert_eq!(editor.text(cx), expected_text);
8365 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
8366 }
8367
8368 assert(
8369 editor,
8370 cx,
8371 indoc! {"
8372 a.f(«one», two, «three») b
8373 a.f(«one», two, «three») b
8374 a.f(«one», two, «three») b
8375 "},
8376 );
8377
8378 // Can't move earlier than the first tab stop
8379 assert!(!editor.move_to_prev_snippet_tabstop(window, cx));
8380 assert(
8381 editor,
8382 cx,
8383 indoc! {"
8384 a.f(«one», two, «three») b
8385 a.f(«one», two, «three») b
8386 a.f(«one», two, «three») b
8387 "},
8388 );
8389
8390 assert!(editor.move_to_next_snippet_tabstop(window, cx));
8391 assert(
8392 editor,
8393 cx,
8394 indoc! {"
8395 a.f(one, «two», three) b
8396 a.f(one, «two», three) b
8397 a.f(one, «two», three) b
8398 "},
8399 );
8400
8401 editor.move_to_prev_snippet_tabstop(window, cx);
8402 assert(
8403 editor,
8404 cx,
8405 indoc! {"
8406 a.f(«one», two, «three») b
8407 a.f(«one», two, «three») b
8408 a.f(«one», two, «three») b
8409 "},
8410 );
8411
8412 assert!(editor.move_to_next_snippet_tabstop(window, cx));
8413 assert(
8414 editor,
8415 cx,
8416 indoc! {"
8417 a.f(one, «two», three) b
8418 a.f(one, «two», three) b
8419 a.f(one, «two», three) b
8420 "},
8421 );
8422 assert!(editor.move_to_next_snippet_tabstop(window, cx));
8423 assert(
8424 editor,
8425 cx,
8426 indoc! {"
8427 a.f(one, two, three)ˇ b
8428 a.f(one, two, three)ˇ b
8429 a.f(one, two, three)ˇ b
8430 "},
8431 );
8432
8433 // As soon as the last tab stop is reached, snippet state is gone
8434 editor.move_to_prev_snippet_tabstop(window, cx);
8435 assert(
8436 editor,
8437 cx,
8438 indoc! {"
8439 a.f(one, two, three)ˇ b
8440 a.f(one, two, three)ˇ b
8441 a.f(one, two, three)ˇ b
8442 "},
8443 );
8444 });
8445}
8446
8447#[gpui::test]
8448async fn test_document_format_during_save(cx: &mut TestAppContext) {
8449 init_test(cx, |_| {});
8450
8451 let fs = FakeFs::new(cx.executor());
8452 fs.insert_file(path!("/file.rs"), Default::default()).await;
8453
8454 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
8455
8456 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8457 language_registry.add(rust_lang());
8458 let mut fake_servers = language_registry.register_fake_lsp(
8459 "Rust",
8460 FakeLspAdapter {
8461 capabilities: lsp::ServerCapabilities {
8462 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8463 ..Default::default()
8464 },
8465 ..Default::default()
8466 },
8467 );
8468
8469 let buffer = project
8470 .update(cx, |project, cx| {
8471 project.open_local_buffer(path!("/file.rs"), cx)
8472 })
8473 .await
8474 .unwrap();
8475
8476 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8477 let (editor, cx) = cx.add_window_view(|window, cx| {
8478 build_editor_with_project(project.clone(), buffer, window, cx)
8479 });
8480 editor.update_in(cx, |editor, window, cx| {
8481 editor.set_text("one\ntwo\nthree\n", window, cx)
8482 });
8483 assert!(cx.read(|cx| editor.is_dirty(cx)));
8484
8485 cx.executor().start_waiting();
8486 let fake_server = fake_servers.next().await.unwrap();
8487
8488 {
8489 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8490 move |params, _| async move {
8491 assert_eq!(
8492 params.text_document.uri,
8493 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8494 );
8495 assert_eq!(params.options.tab_size, 4);
8496 Ok(Some(vec![lsp::TextEdit::new(
8497 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8498 ", ".to_string(),
8499 )]))
8500 },
8501 );
8502 let save = editor
8503 .update_in(cx, |editor, window, cx| {
8504 editor.save(true, project.clone(), window, cx)
8505 })
8506 .unwrap();
8507 cx.executor().start_waiting();
8508 save.await;
8509
8510 assert_eq!(
8511 editor.update(cx, |editor, cx| editor.text(cx)),
8512 "one, two\nthree\n"
8513 );
8514 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8515 }
8516
8517 {
8518 editor.update_in(cx, |editor, window, cx| {
8519 editor.set_text("one\ntwo\nthree\n", window, cx)
8520 });
8521 assert!(cx.read(|cx| editor.is_dirty(cx)));
8522
8523 // Ensure we can still save even if formatting hangs.
8524 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8525 move |params, _| async move {
8526 assert_eq!(
8527 params.text_document.uri,
8528 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8529 );
8530 futures::future::pending::<()>().await;
8531 unreachable!()
8532 },
8533 );
8534 let save = editor
8535 .update_in(cx, |editor, window, cx| {
8536 editor.save(true, project.clone(), window, cx)
8537 })
8538 .unwrap();
8539 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8540 cx.executor().start_waiting();
8541 save.await;
8542 assert_eq!(
8543 editor.update(cx, |editor, cx| editor.text(cx)),
8544 "one\ntwo\nthree\n"
8545 );
8546 }
8547
8548 // For non-dirty buffer, no formatting request should be sent
8549 {
8550 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8551
8552 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
8553 panic!("Should not be invoked on non-dirty buffer");
8554 });
8555 let save = editor
8556 .update_in(cx, |editor, window, cx| {
8557 editor.save(true, project.clone(), window, cx)
8558 })
8559 .unwrap();
8560 cx.executor().start_waiting();
8561 save.await;
8562 }
8563
8564 // Set rust language override and assert overridden tabsize is sent to language server
8565 update_test_language_settings(cx, |settings| {
8566 settings.languages.insert(
8567 "Rust".into(),
8568 LanguageSettingsContent {
8569 tab_size: NonZeroU32::new(8),
8570 ..Default::default()
8571 },
8572 );
8573 });
8574
8575 {
8576 editor.update_in(cx, |editor, window, cx| {
8577 editor.set_text("somehting_new\n", window, cx)
8578 });
8579 assert!(cx.read(|cx| editor.is_dirty(cx)));
8580 let _formatting_request_signal = fake_server
8581 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
8582 assert_eq!(
8583 params.text_document.uri,
8584 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8585 );
8586 assert_eq!(params.options.tab_size, 8);
8587 Ok(Some(vec![]))
8588 });
8589 let save = editor
8590 .update_in(cx, |editor, window, cx| {
8591 editor.save(true, project.clone(), window, cx)
8592 })
8593 .unwrap();
8594 cx.executor().start_waiting();
8595 save.await;
8596 }
8597}
8598
8599#[gpui::test]
8600async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
8601 init_test(cx, |_| {});
8602
8603 let cols = 4;
8604 let rows = 10;
8605 let sample_text_1 = sample_text(rows, cols, 'a');
8606 assert_eq!(
8607 sample_text_1,
8608 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
8609 );
8610 let sample_text_2 = sample_text(rows, cols, 'l');
8611 assert_eq!(
8612 sample_text_2,
8613 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
8614 );
8615 let sample_text_3 = sample_text(rows, cols, 'v');
8616 assert_eq!(
8617 sample_text_3,
8618 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
8619 );
8620
8621 let fs = FakeFs::new(cx.executor());
8622 fs.insert_tree(
8623 path!("/a"),
8624 json!({
8625 "main.rs": sample_text_1,
8626 "other.rs": sample_text_2,
8627 "lib.rs": sample_text_3,
8628 }),
8629 )
8630 .await;
8631
8632 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
8633 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
8634 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
8635
8636 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8637 language_registry.add(rust_lang());
8638 let mut fake_servers = language_registry.register_fake_lsp(
8639 "Rust",
8640 FakeLspAdapter {
8641 capabilities: lsp::ServerCapabilities {
8642 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8643 ..Default::default()
8644 },
8645 ..Default::default()
8646 },
8647 );
8648
8649 let worktree = project.update(cx, |project, cx| {
8650 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
8651 assert_eq!(worktrees.len(), 1);
8652 worktrees.pop().unwrap()
8653 });
8654 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
8655
8656 let buffer_1 = project
8657 .update(cx, |project, cx| {
8658 project.open_buffer((worktree_id, "main.rs"), cx)
8659 })
8660 .await
8661 .unwrap();
8662 let buffer_2 = project
8663 .update(cx, |project, cx| {
8664 project.open_buffer((worktree_id, "other.rs"), cx)
8665 })
8666 .await
8667 .unwrap();
8668 let buffer_3 = project
8669 .update(cx, |project, cx| {
8670 project.open_buffer((worktree_id, "lib.rs"), cx)
8671 })
8672 .await
8673 .unwrap();
8674
8675 let multi_buffer = cx.new(|cx| {
8676 let mut multi_buffer = MultiBuffer::new(ReadWrite);
8677 multi_buffer.push_excerpts(
8678 buffer_1.clone(),
8679 [
8680 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8681 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8682 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8683 ],
8684 cx,
8685 );
8686 multi_buffer.push_excerpts(
8687 buffer_2.clone(),
8688 [
8689 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8690 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8691 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8692 ],
8693 cx,
8694 );
8695 multi_buffer.push_excerpts(
8696 buffer_3.clone(),
8697 [
8698 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8699 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8700 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8701 ],
8702 cx,
8703 );
8704 multi_buffer
8705 });
8706 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
8707 Editor::new(
8708 EditorMode::full(),
8709 multi_buffer,
8710 Some(project.clone()),
8711 window,
8712 cx,
8713 )
8714 });
8715
8716 multi_buffer_editor.update_in(cx, |editor, window, cx| {
8717 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
8718 s.select_ranges(Some(1..2))
8719 });
8720 editor.insert("|one|two|three|", window, cx);
8721 });
8722 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
8723 multi_buffer_editor.update_in(cx, |editor, window, cx| {
8724 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
8725 s.select_ranges(Some(60..70))
8726 });
8727 editor.insert("|four|five|six|", window, cx);
8728 });
8729 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
8730
8731 // First two buffers should be edited, but not the third one.
8732 assert_eq!(
8733 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
8734 "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}",
8735 );
8736 buffer_1.update(cx, |buffer, _| {
8737 assert!(buffer.is_dirty());
8738 assert_eq!(
8739 buffer.text(),
8740 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
8741 )
8742 });
8743 buffer_2.update(cx, |buffer, _| {
8744 assert!(buffer.is_dirty());
8745 assert_eq!(
8746 buffer.text(),
8747 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
8748 )
8749 });
8750 buffer_3.update(cx, |buffer, _| {
8751 assert!(!buffer.is_dirty());
8752 assert_eq!(buffer.text(), sample_text_3,)
8753 });
8754 cx.executor().run_until_parked();
8755
8756 cx.executor().start_waiting();
8757 let save = multi_buffer_editor
8758 .update_in(cx, |editor, window, cx| {
8759 editor.save(true, project.clone(), window, cx)
8760 })
8761 .unwrap();
8762
8763 let fake_server = fake_servers.next().await.unwrap();
8764 fake_server
8765 .server
8766 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
8767 Ok(Some(vec![lsp::TextEdit::new(
8768 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8769 format!("[{} formatted]", params.text_document.uri),
8770 )]))
8771 })
8772 .detach();
8773 save.await;
8774
8775 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
8776 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
8777 assert_eq!(
8778 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
8779 uri!(
8780 "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}"
8781 ),
8782 );
8783 buffer_1.update(cx, |buffer, _| {
8784 assert!(!buffer.is_dirty());
8785 assert_eq!(
8786 buffer.text(),
8787 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
8788 )
8789 });
8790 buffer_2.update(cx, |buffer, _| {
8791 assert!(!buffer.is_dirty());
8792 assert_eq!(
8793 buffer.text(),
8794 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
8795 )
8796 });
8797 buffer_3.update(cx, |buffer, _| {
8798 assert!(!buffer.is_dirty());
8799 assert_eq!(buffer.text(), sample_text_3,)
8800 });
8801}
8802
8803#[gpui::test]
8804async fn test_range_format_during_save(cx: &mut TestAppContext) {
8805 init_test(cx, |_| {});
8806
8807 let fs = FakeFs::new(cx.executor());
8808 fs.insert_file(path!("/file.rs"), Default::default()).await;
8809
8810 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8811
8812 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8813 language_registry.add(rust_lang());
8814 let mut fake_servers = language_registry.register_fake_lsp(
8815 "Rust",
8816 FakeLspAdapter {
8817 capabilities: lsp::ServerCapabilities {
8818 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
8819 ..Default::default()
8820 },
8821 ..Default::default()
8822 },
8823 );
8824
8825 let buffer = project
8826 .update(cx, |project, cx| {
8827 project.open_local_buffer(path!("/file.rs"), cx)
8828 })
8829 .await
8830 .unwrap();
8831
8832 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8833 let (editor, cx) = cx.add_window_view(|window, cx| {
8834 build_editor_with_project(project.clone(), buffer, window, cx)
8835 });
8836 editor.update_in(cx, |editor, window, cx| {
8837 editor.set_text("one\ntwo\nthree\n", window, cx)
8838 });
8839 assert!(cx.read(|cx| editor.is_dirty(cx)));
8840
8841 cx.executor().start_waiting();
8842 let fake_server = fake_servers.next().await.unwrap();
8843
8844 let save = editor
8845 .update_in(cx, |editor, window, cx| {
8846 editor.save(true, project.clone(), window, cx)
8847 })
8848 .unwrap();
8849 fake_server
8850 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
8851 assert_eq!(
8852 params.text_document.uri,
8853 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8854 );
8855 assert_eq!(params.options.tab_size, 4);
8856 Ok(Some(vec![lsp::TextEdit::new(
8857 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8858 ", ".to_string(),
8859 )]))
8860 })
8861 .next()
8862 .await;
8863 cx.executor().start_waiting();
8864 save.await;
8865 assert_eq!(
8866 editor.update(cx, |editor, cx| editor.text(cx)),
8867 "one, two\nthree\n"
8868 );
8869 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8870
8871 editor.update_in(cx, |editor, window, cx| {
8872 editor.set_text("one\ntwo\nthree\n", window, cx)
8873 });
8874 assert!(cx.read(|cx| editor.is_dirty(cx)));
8875
8876 // Ensure we can still save even if formatting hangs.
8877 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
8878 move |params, _| async move {
8879 assert_eq!(
8880 params.text_document.uri,
8881 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8882 );
8883 futures::future::pending::<()>().await;
8884 unreachable!()
8885 },
8886 );
8887 let save = editor
8888 .update_in(cx, |editor, window, cx| {
8889 editor.save(true, project.clone(), window, cx)
8890 })
8891 .unwrap();
8892 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8893 cx.executor().start_waiting();
8894 save.await;
8895 assert_eq!(
8896 editor.update(cx, |editor, cx| editor.text(cx)),
8897 "one\ntwo\nthree\n"
8898 );
8899 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8900
8901 // For non-dirty buffer, no formatting request should be sent
8902 let save = editor
8903 .update_in(cx, |editor, window, cx| {
8904 editor.save(true, project.clone(), window, cx)
8905 })
8906 .unwrap();
8907 let _pending_format_request = fake_server
8908 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
8909 panic!("Should not be invoked on non-dirty buffer");
8910 })
8911 .next();
8912 cx.executor().start_waiting();
8913 save.await;
8914
8915 // Set Rust language override and assert overridden tabsize is sent to language server
8916 update_test_language_settings(cx, |settings| {
8917 settings.languages.insert(
8918 "Rust".into(),
8919 LanguageSettingsContent {
8920 tab_size: NonZeroU32::new(8),
8921 ..Default::default()
8922 },
8923 );
8924 });
8925
8926 editor.update_in(cx, |editor, window, cx| {
8927 editor.set_text("somehting_new\n", window, cx)
8928 });
8929 assert!(cx.read(|cx| editor.is_dirty(cx)));
8930 let save = editor
8931 .update_in(cx, |editor, window, cx| {
8932 editor.save(true, project.clone(), window, cx)
8933 })
8934 .unwrap();
8935 fake_server
8936 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
8937 assert_eq!(
8938 params.text_document.uri,
8939 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8940 );
8941 assert_eq!(params.options.tab_size, 8);
8942 Ok(Some(vec![]))
8943 })
8944 .next()
8945 .await;
8946 cx.executor().start_waiting();
8947 save.await;
8948}
8949
8950#[gpui::test]
8951async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
8952 init_test(cx, |settings| {
8953 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
8954 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
8955 ))
8956 });
8957
8958 let fs = FakeFs::new(cx.executor());
8959 fs.insert_file(path!("/file.rs"), Default::default()).await;
8960
8961 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8962
8963 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8964 language_registry.add(Arc::new(Language::new(
8965 LanguageConfig {
8966 name: "Rust".into(),
8967 matcher: LanguageMatcher {
8968 path_suffixes: vec!["rs".to_string()],
8969 ..Default::default()
8970 },
8971 ..LanguageConfig::default()
8972 },
8973 Some(tree_sitter_rust::LANGUAGE.into()),
8974 )));
8975 update_test_language_settings(cx, |settings| {
8976 // Enable Prettier formatting for the same buffer, and ensure
8977 // LSP is called instead of Prettier.
8978 settings.defaults.prettier = Some(PrettierSettings {
8979 allowed: true,
8980 ..PrettierSettings::default()
8981 });
8982 });
8983 let mut fake_servers = language_registry.register_fake_lsp(
8984 "Rust",
8985 FakeLspAdapter {
8986 capabilities: lsp::ServerCapabilities {
8987 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8988 ..Default::default()
8989 },
8990 ..Default::default()
8991 },
8992 );
8993
8994 let buffer = project
8995 .update(cx, |project, cx| {
8996 project.open_local_buffer(path!("/file.rs"), cx)
8997 })
8998 .await
8999 .unwrap();
9000
9001 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9002 let (editor, cx) = cx.add_window_view(|window, cx| {
9003 build_editor_with_project(project.clone(), buffer, window, cx)
9004 });
9005 editor.update_in(cx, |editor, window, cx| {
9006 editor.set_text("one\ntwo\nthree\n", window, cx)
9007 });
9008
9009 cx.executor().start_waiting();
9010 let fake_server = fake_servers.next().await.unwrap();
9011
9012 let format = editor
9013 .update_in(cx, |editor, window, cx| {
9014 editor.perform_format(
9015 project.clone(),
9016 FormatTrigger::Manual,
9017 FormatTarget::Buffers,
9018 window,
9019 cx,
9020 )
9021 })
9022 .unwrap();
9023 fake_server
9024 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
9025 assert_eq!(
9026 params.text_document.uri,
9027 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9028 );
9029 assert_eq!(params.options.tab_size, 4);
9030 Ok(Some(vec![lsp::TextEdit::new(
9031 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9032 ", ".to_string(),
9033 )]))
9034 })
9035 .next()
9036 .await;
9037 cx.executor().start_waiting();
9038 format.await;
9039 assert_eq!(
9040 editor.update(cx, |editor, cx| editor.text(cx)),
9041 "one, two\nthree\n"
9042 );
9043
9044 editor.update_in(cx, |editor, window, cx| {
9045 editor.set_text("one\ntwo\nthree\n", window, cx)
9046 });
9047 // Ensure we don't lock if formatting hangs.
9048 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9049 move |params, _| async move {
9050 assert_eq!(
9051 params.text_document.uri,
9052 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9053 );
9054 futures::future::pending::<()>().await;
9055 unreachable!()
9056 },
9057 );
9058 let format = editor
9059 .update_in(cx, |editor, window, cx| {
9060 editor.perform_format(
9061 project,
9062 FormatTrigger::Manual,
9063 FormatTarget::Buffers,
9064 window,
9065 cx,
9066 )
9067 })
9068 .unwrap();
9069 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9070 cx.executor().start_waiting();
9071 format.await;
9072 assert_eq!(
9073 editor.update(cx, |editor, cx| editor.text(cx)),
9074 "one\ntwo\nthree\n"
9075 );
9076}
9077
9078#[gpui::test]
9079async fn test_multiple_formatters(cx: &mut TestAppContext) {
9080 init_test(cx, |settings| {
9081 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
9082 settings.defaults.formatter =
9083 Some(language_settings::SelectedFormatter::List(FormatterList(
9084 vec![
9085 Formatter::LanguageServer { name: None },
9086 Formatter::CodeActions(
9087 [
9088 ("code-action-1".into(), true),
9089 ("code-action-2".into(), true),
9090 ]
9091 .into_iter()
9092 .collect(),
9093 ),
9094 ]
9095 .into(),
9096 )))
9097 });
9098
9099 let fs = FakeFs::new(cx.executor());
9100 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
9101 .await;
9102
9103 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9104 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9105 language_registry.add(rust_lang());
9106
9107 let mut fake_servers = language_registry.register_fake_lsp(
9108 "Rust",
9109 FakeLspAdapter {
9110 capabilities: lsp::ServerCapabilities {
9111 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9112 execute_command_provider: Some(lsp::ExecuteCommandOptions {
9113 commands: vec!["the-command-for-code-action-1".into()],
9114 ..Default::default()
9115 }),
9116 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
9117 ..Default::default()
9118 },
9119 ..Default::default()
9120 },
9121 );
9122
9123 let buffer = project
9124 .update(cx, |project, cx| {
9125 project.open_local_buffer(path!("/file.rs"), cx)
9126 })
9127 .await
9128 .unwrap();
9129
9130 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9131 let (editor, cx) = cx.add_window_view(|window, cx| {
9132 build_editor_with_project(project.clone(), buffer, window, cx)
9133 });
9134
9135 cx.executor().start_waiting();
9136
9137 let fake_server = fake_servers.next().await.unwrap();
9138 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9139 move |_params, _| async move {
9140 Ok(Some(vec![lsp::TextEdit::new(
9141 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9142 "applied-formatting\n".to_string(),
9143 )]))
9144 },
9145 );
9146 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
9147 move |params, _| async move {
9148 assert_eq!(
9149 params.context.only,
9150 Some(vec!["code-action-1".into(), "code-action-2".into()])
9151 );
9152 let uri = lsp::Url::from_file_path(path!("/file.rs")).unwrap();
9153 Ok(Some(vec![
9154 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
9155 kind: Some("code-action-1".into()),
9156 edit: Some(lsp::WorkspaceEdit::new(
9157 [(
9158 uri.clone(),
9159 vec![lsp::TextEdit::new(
9160 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9161 "applied-code-action-1-edit\n".to_string(),
9162 )],
9163 )]
9164 .into_iter()
9165 .collect(),
9166 )),
9167 command: Some(lsp::Command {
9168 command: "the-command-for-code-action-1".into(),
9169 ..Default::default()
9170 }),
9171 ..Default::default()
9172 }),
9173 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
9174 kind: Some("code-action-2".into()),
9175 edit: Some(lsp::WorkspaceEdit::new(
9176 [(
9177 uri.clone(),
9178 vec![lsp::TextEdit::new(
9179 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9180 "applied-code-action-2-edit\n".to_string(),
9181 )],
9182 )]
9183 .into_iter()
9184 .collect(),
9185 )),
9186 ..Default::default()
9187 }),
9188 ]))
9189 },
9190 );
9191
9192 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
9193 move |params, _| async move { Ok(params) }
9194 });
9195
9196 let command_lock = Arc::new(futures::lock::Mutex::new(()));
9197 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
9198 let fake = fake_server.clone();
9199 let lock = command_lock.clone();
9200 move |params, _| {
9201 assert_eq!(params.command, "the-command-for-code-action-1");
9202 let fake = fake.clone();
9203 let lock = lock.clone();
9204 async move {
9205 lock.lock().await;
9206 fake.server
9207 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
9208 label: None,
9209 edit: lsp::WorkspaceEdit {
9210 changes: Some(
9211 [(
9212 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
9213 vec![lsp::TextEdit {
9214 range: lsp::Range::new(
9215 lsp::Position::new(0, 0),
9216 lsp::Position::new(0, 0),
9217 ),
9218 new_text: "applied-code-action-1-command\n".into(),
9219 }],
9220 )]
9221 .into_iter()
9222 .collect(),
9223 ),
9224 ..Default::default()
9225 },
9226 })
9227 .await
9228 .into_response()
9229 .unwrap();
9230 Ok(Some(json!(null)))
9231 }
9232 }
9233 });
9234
9235 cx.executor().start_waiting();
9236 editor
9237 .update_in(cx, |editor, window, cx| {
9238 editor.perform_format(
9239 project.clone(),
9240 FormatTrigger::Manual,
9241 FormatTarget::Buffers,
9242 window,
9243 cx,
9244 )
9245 })
9246 .unwrap()
9247 .await;
9248 editor.update(cx, |editor, cx| {
9249 assert_eq!(
9250 editor.text(cx),
9251 r#"
9252 applied-code-action-2-edit
9253 applied-code-action-1-command
9254 applied-code-action-1-edit
9255 applied-formatting
9256 one
9257 two
9258 three
9259 "#
9260 .unindent()
9261 );
9262 });
9263
9264 editor.update_in(cx, |editor, window, cx| {
9265 editor.undo(&Default::default(), window, cx);
9266 assert_eq!(editor.text(cx), "one \ntwo \nthree");
9267 });
9268
9269 // Perform a manual edit while waiting for an LSP command
9270 // that's being run as part of a formatting code action.
9271 let lock_guard = command_lock.lock().await;
9272 let format = editor
9273 .update_in(cx, |editor, window, cx| {
9274 editor.perform_format(
9275 project.clone(),
9276 FormatTrigger::Manual,
9277 FormatTarget::Buffers,
9278 window,
9279 cx,
9280 )
9281 })
9282 .unwrap();
9283 cx.run_until_parked();
9284 editor.update(cx, |editor, cx| {
9285 assert_eq!(
9286 editor.text(cx),
9287 r#"
9288 applied-code-action-1-edit
9289 applied-formatting
9290 one
9291 two
9292 three
9293 "#
9294 .unindent()
9295 );
9296
9297 editor.buffer.update(cx, |buffer, cx| {
9298 let ix = buffer.len(cx);
9299 buffer.edit([(ix..ix, "edited\n")], None, cx);
9300 });
9301 });
9302
9303 // Allow the LSP command to proceed. Because the buffer was edited,
9304 // the second code action will not be run.
9305 drop(lock_guard);
9306 format.await;
9307 editor.update_in(cx, |editor, window, cx| {
9308 assert_eq!(
9309 editor.text(cx),
9310 r#"
9311 applied-code-action-1-command
9312 applied-code-action-1-edit
9313 applied-formatting
9314 one
9315 two
9316 three
9317 edited
9318 "#
9319 .unindent()
9320 );
9321
9322 // The manual edit is undone first, because it is the last thing the user did
9323 // (even though the command completed afterwards).
9324 editor.undo(&Default::default(), window, cx);
9325 assert_eq!(
9326 editor.text(cx),
9327 r#"
9328 applied-code-action-1-command
9329 applied-code-action-1-edit
9330 applied-formatting
9331 one
9332 two
9333 three
9334 "#
9335 .unindent()
9336 );
9337
9338 // All the formatting (including the command, which completed after the manual edit)
9339 // is undone together.
9340 editor.undo(&Default::default(), window, cx);
9341 assert_eq!(editor.text(cx), "one \ntwo \nthree");
9342 });
9343}
9344
9345#[gpui::test]
9346async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
9347 init_test(cx, |settings| {
9348 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
9349 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
9350 ))
9351 });
9352
9353 let fs = FakeFs::new(cx.executor());
9354 fs.insert_file(path!("/file.ts"), Default::default()).await;
9355
9356 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9357
9358 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9359 language_registry.add(Arc::new(Language::new(
9360 LanguageConfig {
9361 name: "TypeScript".into(),
9362 matcher: LanguageMatcher {
9363 path_suffixes: vec!["ts".to_string()],
9364 ..Default::default()
9365 },
9366 ..LanguageConfig::default()
9367 },
9368 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
9369 )));
9370 update_test_language_settings(cx, |settings| {
9371 settings.defaults.prettier = Some(PrettierSettings {
9372 allowed: true,
9373 ..PrettierSettings::default()
9374 });
9375 });
9376 let mut fake_servers = language_registry.register_fake_lsp(
9377 "TypeScript",
9378 FakeLspAdapter {
9379 capabilities: lsp::ServerCapabilities {
9380 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
9381 ..Default::default()
9382 },
9383 ..Default::default()
9384 },
9385 );
9386
9387 let buffer = project
9388 .update(cx, |project, cx| {
9389 project.open_local_buffer(path!("/file.ts"), cx)
9390 })
9391 .await
9392 .unwrap();
9393
9394 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9395 let (editor, cx) = cx.add_window_view(|window, cx| {
9396 build_editor_with_project(project.clone(), buffer, window, cx)
9397 });
9398 editor.update_in(cx, |editor, window, cx| {
9399 editor.set_text(
9400 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
9401 window,
9402 cx,
9403 )
9404 });
9405
9406 cx.executor().start_waiting();
9407 let fake_server = fake_servers.next().await.unwrap();
9408
9409 let format = editor
9410 .update_in(cx, |editor, window, cx| {
9411 editor.perform_code_action_kind(
9412 project.clone(),
9413 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
9414 window,
9415 cx,
9416 )
9417 })
9418 .unwrap();
9419 fake_server
9420 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
9421 assert_eq!(
9422 params.text_document.uri,
9423 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
9424 );
9425 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
9426 lsp::CodeAction {
9427 title: "Organize Imports".to_string(),
9428 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
9429 edit: Some(lsp::WorkspaceEdit {
9430 changes: Some(
9431 [(
9432 params.text_document.uri.clone(),
9433 vec![lsp::TextEdit::new(
9434 lsp::Range::new(
9435 lsp::Position::new(1, 0),
9436 lsp::Position::new(2, 0),
9437 ),
9438 "".to_string(),
9439 )],
9440 )]
9441 .into_iter()
9442 .collect(),
9443 ),
9444 ..Default::default()
9445 }),
9446 ..Default::default()
9447 },
9448 )]))
9449 })
9450 .next()
9451 .await;
9452 cx.executor().start_waiting();
9453 format.await;
9454 assert_eq!(
9455 editor.update(cx, |editor, cx| editor.text(cx)),
9456 "import { a } from 'module';\n\nconst x = a;\n"
9457 );
9458
9459 editor.update_in(cx, |editor, window, cx| {
9460 editor.set_text(
9461 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
9462 window,
9463 cx,
9464 )
9465 });
9466 // Ensure we don't lock if code action hangs.
9467 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
9468 move |params, _| async move {
9469 assert_eq!(
9470 params.text_document.uri,
9471 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
9472 );
9473 futures::future::pending::<()>().await;
9474 unreachable!()
9475 },
9476 );
9477 let format = editor
9478 .update_in(cx, |editor, window, cx| {
9479 editor.perform_code_action_kind(
9480 project,
9481 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
9482 window,
9483 cx,
9484 )
9485 })
9486 .unwrap();
9487 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
9488 cx.executor().start_waiting();
9489 format.await;
9490 assert_eq!(
9491 editor.update(cx, |editor, cx| editor.text(cx)),
9492 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
9493 );
9494}
9495
9496#[gpui::test]
9497async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
9498 init_test(cx, |_| {});
9499
9500 let mut cx = EditorLspTestContext::new_rust(
9501 lsp::ServerCapabilities {
9502 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9503 ..Default::default()
9504 },
9505 cx,
9506 )
9507 .await;
9508
9509 cx.set_state(indoc! {"
9510 one.twoˇ
9511 "});
9512
9513 // The format request takes a long time. When it completes, it inserts
9514 // a newline and an indent before the `.`
9515 cx.lsp
9516 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
9517 let executor = cx.background_executor().clone();
9518 async move {
9519 executor.timer(Duration::from_millis(100)).await;
9520 Ok(Some(vec![lsp::TextEdit {
9521 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
9522 new_text: "\n ".into(),
9523 }]))
9524 }
9525 });
9526
9527 // Submit a format request.
9528 let format_1 = cx
9529 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
9530 .unwrap();
9531 cx.executor().run_until_parked();
9532
9533 // Submit a second format request.
9534 let format_2 = cx
9535 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
9536 .unwrap();
9537 cx.executor().run_until_parked();
9538
9539 // Wait for both format requests to complete
9540 cx.executor().advance_clock(Duration::from_millis(200));
9541 cx.executor().start_waiting();
9542 format_1.await.unwrap();
9543 cx.executor().start_waiting();
9544 format_2.await.unwrap();
9545
9546 // The formatting edits only happens once.
9547 cx.assert_editor_state(indoc! {"
9548 one
9549 .twoˇ
9550 "});
9551}
9552
9553#[gpui::test]
9554async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
9555 init_test(cx, |settings| {
9556 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
9557 });
9558
9559 let mut cx = EditorLspTestContext::new_rust(
9560 lsp::ServerCapabilities {
9561 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9562 ..Default::default()
9563 },
9564 cx,
9565 )
9566 .await;
9567
9568 // Set up a buffer white some trailing whitespace and no trailing newline.
9569 cx.set_state(
9570 &[
9571 "one ", //
9572 "twoˇ", //
9573 "three ", //
9574 "four", //
9575 ]
9576 .join("\n"),
9577 );
9578
9579 // Submit a format request.
9580 let format = cx
9581 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
9582 .unwrap();
9583
9584 // Record which buffer changes have been sent to the language server
9585 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
9586 cx.lsp
9587 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
9588 let buffer_changes = buffer_changes.clone();
9589 move |params, _| {
9590 buffer_changes.lock().extend(
9591 params
9592 .content_changes
9593 .into_iter()
9594 .map(|e| (e.range.unwrap(), e.text)),
9595 );
9596 }
9597 });
9598
9599 // Handle formatting requests to the language server.
9600 cx.lsp
9601 .set_request_handler::<lsp::request::Formatting, _, _>({
9602 let buffer_changes = buffer_changes.clone();
9603 move |_, _| {
9604 // When formatting is requested, trailing whitespace has already been stripped,
9605 // and the trailing newline has already been added.
9606 assert_eq!(
9607 &buffer_changes.lock()[1..],
9608 &[
9609 (
9610 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
9611 "".into()
9612 ),
9613 (
9614 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
9615 "".into()
9616 ),
9617 (
9618 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
9619 "\n".into()
9620 ),
9621 ]
9622 );
9623
9624 // Insert blank lines between each line of the buffer.
9625 async move {
9626 Ok(Some(vec![
9627 lsp::TextEdit {
9628 range: lsp::Range::new(
9629 lsp::Position::new(1, 0),
9630 lsp::Position::new(1, 0),
9631 ),
9632 new_text: "\n".into(),
9633 },
9634 lsp::TextEdit {
9635 range: lsp::Range::new(
9636 lsp::Position::new(2, 0),
9637 lsp::Position::new(2, 0),
9638 ),
9639 new_text: "\n".into(),
9640 },
9641 ]))
9642 }
9643 }
9644 });
9645
9646 // After formatting the buffer, the trailing whitespace is stripped,
9647 // a newline is appended, and the edits provided by the language server
9648 // have been applied.
9649 format.await.unwrap();
9650 cx.assert_editor_state(
9651 &[
9652 "one", //
9653 "", //
9654 "twoˇ", //
9655 "", //
9656 "three", //
9657 "four", //
9658 "", //
9659 ]
9660 .join("\n"),
9661 );
9662
9663 // Undoing the formatting undoes the trailing whitespace removal, the
9664 // trailing newline, and the LSP edits.
9665 cx.update_buffer(|buffer, cx| buffer.undo(cx));
9666 cx.assert_editor_state(
9667 &[
9668 "one ", //
9669 "twoˇ", //
9670 "three ", //
9671 "four", //
9672 ]
9673 .join("\n"),
9674 );
9675}
9676
9677#[gpui::test]
9678async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
9679 cx: &mut TestAppContext,
9680) {
9681 init_test(cx, |_| {});
9682
9683 cx.update(|cx| {
9684 cx.update_global::<SettingsStore, _>(|settings, cx| {
9685 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9686 settings.auto_signature_help = Some(true);
9687 });
9688 });
9689 });
9690
9691 let mut cx = EditorLspTestContext::new_rust(
9692 lsp::ServerCapabilities {
9693 signature_help_provider: Some(lsp::SignatureHelpOptions {
9694 ..Default::default()
9695 }),
9696 ..Default::default()
9697 },
9698 cx,
9699 )
9700 .await;
9701
9702 let language = Language::new(
9703 LanguageConfig {
9704 name: "Rust".into(),
9705 brackets: BracketPairConfig {
9706 pairs: vec![
9707 BracketPair {
9708 start: "{".to_string(),
9709 end: "}".to_string(),
9710 close: true,
9711 surround: true,
9712 newline: true,
9713 },
9714 BracketPair {
9715 start: "(".to_string(),
9716 end: ")".to_string(),
9717 close: true,
9718 surround: true,
9719 newline: true,
9720 },
9721 BracketPair {
9722 start: "/*".to_string(),
9723 end: " */".to_string(),
9724 close: true,
9725 surround: true,
9726 newline: true,
9727 },
9728 BracketPair {
9729 start: "[".to_string(),
9730 end: "]".to_string(),
9731 close: false,
9732 surround: false,
9733 newline: true,
9734 },
9735 BracketPair {
9736 start: "\"".to_string(),
9737 end: "\"".to_string(),
9738 close: true,
9739 surround: true,
9740 newline: false,
9741 },
9742 BracketPair {
9743 start: "<".to_string(),
9744 end: ">".to_string(),
9745 close: false,
9746 surround: true,
9747 newline: true,
9748 },
9749 ],
9750 ..Default::default()
9751 },
9752 autoclose_before: "})]".to_string(),
9753 ..Default::default()
9754 },
9755 Some(tree_sitter_rust::LANGUAGE.into()),
9756 );
9757 let language = Arc::new(language);
9758
9759 cx.language_registry().add(language.clone());
9760 cx.update_buffer(|buffer, cx| {
9761 buffer.set_language(Some(language), cx);
9762 });
9763
9764 cx.set_state(
9765 &r#"
9766 fn main() {
9767 sampleˇ
9768 }
9769 "#
9770 .unindent(),
9771 );
9772
9773 cx.update_editor(|editor, window, cx| {
9774 editor.handle_input("(", window, cx);
9775 });
9776 cx.assert_editor_state(
9777 &"
9778 fn main() {
9779 sample(ˇ)
9780 }
9781 "
9782 .unindent(),
9783 );
9784
9785 let mocked_response = lsp::SignatureHelp {
9786 signatures: vec![lsp::SignatureInformation {
9787 label: "fn sample(param1: u8, param2: u8)".to_string(),
9788 documentation: None,
9789 parameters: Some(vec![
9790 lsp::ParameterInformation {
9791 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9792 documentation: None,
9793 },
9794 lsp::ParameterInformation {
9795 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9796 documentation: None,
9797 },
9798 ]),
9799 active_parameter: None,
9800 }],
9801 active_signature: Some(0),
9802 active_parameter: Some(0),
9803 };
9804 handle_signature_help_request(&mut cx, mocked_response).await;
9805
9806 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9807 .await;
9808
9809 cx.editor(|editor, _, _| {
9810 let signature_help_state = editor.signature_help_state.popover().cloned();
9811 assert_eq!(
9812 signature_help_state.unwrap().label,
9813 "param1: u8, param2: u8"
9814 );
9815 });
9816}
9817
9818#[gpui::test]
9819async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
9820 init_test(cx, |_| {});
9821
9822 cx.update(|cx| {
9823 cx.update_global::<SettingsStore, _>(|settings, cx| {
9824 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9825 settings.auto_signature_help = Some(false);
9826 settings.show_signature_help_after_edits = Some(false);
9827 });
9828 });
9829 });
9830
9831 let mut cx = EditorLspTestContext::new_rust(
9832 lsp::ServerCapabilities {
9833 signature_help_provider: Some(lsp::SignatureHelpOptions {
9834 ..Default::default()
9835 }),
9836 ..Default::default()
9837 },
9838 cx,
9839 )
9840 .await;
9841
9842 let language = Language::new(
9843 LanguageConfig {
9844 name: "Rust".into(),
9845 brackets: BracketPairConfig {
9846 pairs: vec![
9847 BracketPair {
9848 start: "{".to_string(),
9849 end: "}".to_string(),
9850 close: true,
9851 surround: true,
9852 newline: true,
9853 },
9854 BracketPair {
9855 start: "(".to_string(),
9856 end: ")".to_string(),
9857 close: true,
9858 surround: true,
9859 newline: true,
9860 },
9861 BracketPair {
9862 start: "/*".to_string(),
9863 end: " */".to_string(),
9864 close: true,
9865 surround: true,
9866 newline: true,
9867 },
9868 BracketPair {
9869 start: "[".to_string(),
9870 end: "]".to_string(),
9871 close: false,
9872 surround: false,
9873 newline: true,
9874 },
9875 BracketPair {
9876 start: "\"".to_string(),
9877 end: "\"".to_string(),
9878 close: true,
9879 surround: true,
9880 newline: false,
9881 },
9882 BracketPair {
9883 start: "<".to_string(),
9884 end: ">".to_string(),
9885 close: false,
9886 surround: true,
9887 newline: true,
9888 },
9889 ],
9890 ..Default::default()
9891 },
9892 autoclose_before: "})]".to_string(),
9893 ..Default::default()
9894 },
9895 Some(tree_sitter_rust::LANGUAGE.into()),
9896 );
9897 let language = Arc::new(language);
9898
9899 cx.language_registry().add(language.clone());
9900 cx.update_buffer(|buffer, cx| {
9901 buffer.set_language(Some(language), cx);
9902 });
9903
9904 // Ensure that signature_help is not called when no signature help is enabled.
9905 cx.set_state(
9906 &r#"
9907 fn main() {
9908 sampleˇ
9909 }
9910 "#
9911 .unindent(),
9912 );
9913 cx.update_editor(|editor, window, cx| {
9914 editor.handle_input("(", window, cx);
9915 });
9916 cx.assert_editor_state(
9917 &"
9918 fn main() {
9919 sample(ˇ)
9920 }
9921 "
9922 .unindent(),
9923 );
9924 cx.editor(|editor, _, _| {
9925 assert!(editor.signature_help_state.task().is_none());
9926 });
9927
9928 let mocked_response = lsp::SignatureHelp {
9929 signatures: vec![lsp::SignatureInformation {
9930 label: "fn sample(param1: u8, param2: u8)".to_string(),
9931 documentation: None,
9932 parameters: Some(vec![
9933 lsp::ParameterInformation {
9934 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9935 documentation: None,
9936 },
9937 lsp::ParameterInformation {
9938 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9939 documentation: None,
9940 },
9941 ]),
9942 active_parameter: None,
9943 }],
9944 active_signature: Some(0),
9945 active_parameter: Some(0),
9946 };
9947
9948 // Ensure that signature_help is called when enabled afte edits
9949 cx.update(|_, cx| {
9950 cx.update_global::<SettingsStore, _>(|settings, cx| {
9951 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9952 settings.auto_signature_help = Some(false);
9953 settings.show_signature_help_after_edits = Some(true);
9954 });
9955 });
9956 });
9957 cx.set_state(
9958 &r#"
9959 fn main() {
9960 sampleˇ
9961 }
9962 "#
9963 .unindent(),
9964 );
9965 cx.update_editor(|editor, window, cx| {
9966 editor.handle_input("(", window, cx);
9967 });
9968 cx.assert_editor_state(
9969 &"
9970 fn main() {
9971 sample(ˇ)
9972 }
9973 "
9974 .unindent(),
9975 );
9976 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9977 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9978 .await;
9979 cx.update_editor(|editor, _, _| {
9980 let signature_help_state = editor.signature_help_state.popover().cloned();
9981 assert!(signature_help_state.is_some());
9982 assert_eq!(
9983 signature_help_state.unwrap().label,
9984 "param1: u8, param2: u8"
9985 );
9986 editor.signature_help_state = SignatureHelpState::default();
9987 });
9988
9989 // Ensure that signature_help is called when auto signature help override is enabled
9990 cx.update(|_, cx| {
9991 cx.update_global::<SettingsStore, _>(|settings, cx| {
9992 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9993 settings.auto_signature_help = Some(true);
9994 settings.show_signature_help_after_edits = Some(false);
9995 });
9996 });
9997 });
9998 cx.set_state(
9999 &r#"
10000 fn main() {
10001 sampleˇ
10002 }
10003 "#
10004 .unindent(),
10005 );
10006 cx.update_editor(|editor, window, cx| {
10007 editor.handle_input("(", window, cx);
10008 });
10009 cx.assert_editor_state(
10010 &"
10011 fn main() {
10012 sample(ˇ)
10013 }
10014 "
10015 .unindent(),
10016 );
10017 handle_signature_help_request(&mut cx, mocked_response).await;
10018 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10019 .await;
10020 cx.editor(|editor, _, _| {
10021 let signature_help_state = editor.signature_help_state.popover().cloned();
10022 assert!(signature_help_state.is_some());
10023 assert_eq!(
10024 signature_help_state.unwrap().label,
10025 "param1: u8, param2: u8"
10026 );
10027 });
10028}
10029
10030#[gpui::test]
10031async fn test_signature_help(cx: &mut TestAppContext) {
10032 init_test(cx, |_| {});
10033 cx.update(|cx| {
10034 cx.update_global::<SettingsStore, _>(|settings, cx| {
10035 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10036 settings.auto_signature_help = Some(true);
10037 });
10038 });
10039 });
10040
10041 let mut cx = EditorLspTestContext::new_rust(
10042 lsp::ServerCapabilities {
10043 signature_help_provider: Some(lsp::SignatureHelpOptions {
10044 ..Default::default()
10045 }),
10046 ..Default::default()
10047 },
10048 cx,
10049 )
10050 .await;
10051
10052 // A test that directly calls `show_signature_help`
10053 cx.update_editor(|editor, window, cx| {
10054 editor.show_signature_help(&ShowSignatureHelp, window, cx);
10055 });
10056
10057 let mocked_response = lsp::SignatureHelp {
10058 signatures: vec![lsp::SignatureInformation {
10059 label: "fn sample(param1: u8, param2: u8)".to_string(),
10060 documentation: None,
10061 parameters: Some(vec![
10062 lsp::ParameterInformation {
10063 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10064 documentation: None,
10065 },
10066 lsp::ParameterInformation {
10067 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10068 documentation: None,
10069 },
10070 ]),
10071 active_parameter: None,
10072 }],
10073 active_signature: Some(0),
10074 active_parameter: Some(0),
10075 };
10076 handle_signature_help_request(&mut cx, mocked_response).await;
10077
10078 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10079 .await;
10080
10081 cx.editor(|editor, _, _| {
10082 let signature_help_state = editor.signature_help_state.popover().cloned();
10083 assert!(signature_help_state.is_some());
10084 assert_eq!(
10085 signature_help_state.unwrap().label,
10086 "param1: u8, param2: u8"
10087 );
10088 });
10089
10090 // When exiting outside from inside the brackets, `signature_help` is closed.
10091 cx.set_state(indoc! {"
10092 fn main() {
10093 sample(ˇ);
10094 }
10095
10096 fn sample(param1: u8, param2: u8) {}
10097 "});
10098
10099 cx.update_editor(|editor, window, cx| {
10100 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
10101 });
10102
10103 let mocked_response = lsp::SignatureHelp {
10104 signatures: Vec::new(),
10105 active_signature: None,
10106 active_parameter: None,
10107 };
10108 handle_signature_help_request(&mut cx, mocked_response).await;
10109
10110 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
10111 .await;
10112
10113 cx.editor(|editor, _, _| {
10114 assert!(!editor.signature_help_state.is_shown());
10115 });
10116
10117 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
10118 cx.set_state(indoc! {"
10119 fn main() {
10120 sample(ˇ);
10121 }
10122
10123 fn sample(param1: u8, param2: u8) {}
10124 "});
10125
10126 let mocked_response = lsp::SignatureHelp {
10127 signatures: vec![lsp::SignatureInformation {
10128 label: "fn sample(param1: u8, param2: u8)".to_string(),
10129 documentation: None,
10130 parameters: Some(vec![
10131 lsp::ParameterInformation {
10132 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10133 documentation: None,
10134 },
10135 lsp::ParameterInformation {
10136 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10137 documentation: None,
10138 },
10139 ]),
10140 active_parameter: None,
10141 }],
10142 active_signature: Some(0),
10143 active_parameter: Some(0),
10144 };
10145 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10146 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10147 .await;
10148 cx.editor(|editor, _, _| {
10149 assert!(editor.signature_help_state.is_shown());
10150 });
10151
10152 // Restore the popover with more parameter input
10153 cx.set_state(indoc! {"
10154 fn main() {
10155 sample(param1, param2ˇ);
10156 }
10157
10158 fn sample(param1: u8, param2: u8) {}
10159 "});
10160
10161 let mocked_response = lsp::SignatureHelp {
10162 signatures: vec![lsp::SignatureInformation {
10163 label: "fn sample(param1: u8, param2: u8)".to_string(),
10164 documentation: None,
10165 parameters: Some(vec![
10166 lsp::ParameterInformation {
10167 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10168 documentation: None,
10169 },
10170 lsp::ParameterInformation {
10171 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10172 documentation: None,
10173 },
10174 ]),
10175 active_parameter: None,
10176 }],
10177 active_signature: Some(0),
10178 active_parameter: Some(1),
10179 };
10180 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10181 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10182 .await;
10183
10184 // When selecting a range, the popover is gone.
10185 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
10186 cx.update_editor(|editor, window, cx| {
10187 editor.change_selections(None, window, cx, |s| {
10188 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
10189 })
10190 });
10191 cx.assert_editor_state(indoc! {"
10192 fn main() {
10193 sample(param1, «ˇparam2»);
10194 }
10195
10196 fn sample(param1: u8, param2: u8) {}
10197 "});
10198 cx.editor(|editor, _, _| {
10199 assert!(!editor.signature_help_state.is_shown());
10200 });
10201
10202 // When unselecting again, the popover is back if within the brackets.
10203 cx.update_editor(|editor, window, cx| {
10204 editor.change_selections(None, window, cx, |s| {
10205 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
10206 })
10207 });
10208 cx.assert_editor_state(indoc! {"
10209 fn main() {
10210 sample(param1, ˇparam2);
10211 }
10212
10213 fn sample(param1: u8, param2: u8) {}
10214 "});
10215 handle_signature_help_request(&mut cx, mocked_response).await;
10216 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10217 .await;
10218 cx.editor(|editor, _, _| {
10219 assert!(editor.signature_help_state.is_shown());
10220 });
10221
10222 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
10223 cx.update_editor(|editor, window, cx| {
10224 editor.change_selections(None, window, cx, |s| {
10225 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
10226 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
10227 })
10228 });
10229 cx.assert_editor_state(indoc! {"
10230 fn main() {
10231 sample(param1, ˇparam2);
10232 }
10233
10234 fn sample(param1: u8, param2: u8) {}
10235 "});
10236
10237 let mocked_response = lsp::SignatureHelp {
10238 signatures: vec![lsp::SignatureInformation {
10239 label: "fn sample(param1: u8, param2: u8)".to_string(),
10240 documentation: None,
10241 parameters: Some(vec![
10242 lsp::ParameterInformation {
10243 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10244 documentation: None,
10245 },
10246 lsp::ParameterInformation {
10247 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10248 documentation: None,
10249 },
10250 ]),
10251 active_parameter: None,
10252 }],
10253 active_signature: Some(0),
10254 active_parameter: Some(1),
10255 };
10256 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10257 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10258 .await;
10259 cx.update_editor(|editor, _, cx| {
10260 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
10261 });
10262 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
10263 .await;
10264 cx.update_editor(|editor, window, cx| {
10265 editor.change_selections(None, window, cx, |s| {
10266 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
10267 })
10268 });
10269 cx.assert_editor_state(indoc! {"
10270 fn main() {
10271 sample(param1, «ˇparam2»);
10272 }
10273
10274 fn sample(param1: u8, param2: u8) {}
10275 "});
10276 cx.update_editor(|editor, window, cx| {
10277 editor.change_selections(None, window, cx, |s| {
10278 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
10279 })
10280 });
10281 cx.assert_editor_state(indoc! {"
10282 fn main() {
10283 sample(param1, ˇparam2);
10284 }
10285
10286 fn sample(param1: u8, param2: u8) {}
10287 "});
10288 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
10289 .await;
10290}
10291
10292#[gpui::test]
10293async fn test_completion_mode(cx: &mut TestAppContext) {
10294 init_test(cx, |_| {});
10295 let mut cx = EditorLspTestContext::new_rust(
10296 lsp::ServerCapabilities {
10297 completion_provider: Some(lsp::CompletionOptions {
10298 resolve_provider: Some(true),
10299 ..Default::default()
10300 }),
10301 ..Default::default()
10302 },
10303 cx,
10304 )
10305 .await;
10306
10307 struct Run {
10308 run_description: &'static str,
10309 initial_state: String,
10310 buffer_marked_text: String,
10311 completion_text: &'static str,
10312 expected_with_insert_mode: String,
10313 expected_with_replace_mode: String,
10314 expected_with_replace_subsequence_mode: String,
10315 expected_with_replace_suffix_mode: String,
10316 }
10317
10318 let runs = [
10319 Run {
10320 run_description: "Start of word matches completion text",
10321 initial_state: "before ediˇ after".into(),
10322 buffer_marked_text: "before <edi|> after".into(),
10323 completion_text: "editor",
10324 expected_with_insert_mode: "before editorˇ after".into(),
10325 expected_with_replace_mode: "before editorˇ after".into(),
10326 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10327 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10328 },
10329 Run {
10330 run_description: "Accept same text at the middle of the word",
10331 initial_state: "before ediˇtor after".into(),
10332 buffer_marked_text: "before <edi|tor> after".into(),
10333 completion_text: "editor",
10334 expected_with_insert_mode: "before editorˇtor after".into(),
10335 expected_with_replace_mode: "before editorˇ after".into(),
10336 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10337 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10338 },
10339 Run {
10340 run_description: "End of word matches completion text -- cursor at end",
10341 initial_state: "before torˇ after".into(),
10342 buffer_marked_text: "before <tor|> after".into(),
10343 completion_text: "editor",
10344 expected_with_insert_mode: "before editorˇ after".into(),
10345 expected_with_replace_mode: "before editorˇ after".into(),
10346 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10347 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10348 },
10349 Run {
10350 run_description: "End of word matches completion text -- cursor at start",
10351 initial_state: "before ˇtor after".into(),
10352 buffer_marked_text: "before <|tor> after".into(),
10353 completion_text: "editor",
10354 expected_with_insert_mode: "before editorˇtor after".into(),
10355 expected_with_replace_mode: "before editorˇ after".into(),
10356 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10357 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10358 },
10359 Run {
10360 run_description: "Prepend text containing whitespace",
10361 initial_state: "pˇfield: bool".into(),
10362 buffer_marked_text: "<p|field>: bool".into(),
10363 completion_text: "pub ",
10364 expected_with_insert_mode: "pub ˇfield: bool".into(),
10365 expected_with_replace_mode: "pub ˇ: bool".into(),
10366 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
10367 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
10368 },
10369 Run {
10370 run_description: "Add element to start of list",
10371 initial_state: "[element_ˇelement_2]".into(),
10372 buffer_marked_text: "[<element_|element_2>]".into(),
10373 completion_text: "element_1",
10374 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
10375 expected_with_replace_mode: "[element_1ˇ]".into(),
10376 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
10377 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
10378 },
10379 Run {
10380 run_description: "Add element to start of list -- first and second elements are equal",
10381 initial_state: "[elˇelement]".into(),
10382 buffer_marked_text: "[<el|element>]".into(),
10383 completion_text: "element",
10384 expected_with_insert_mode: "[elementˇelement]".into(),
10385 expected_with_replace_mode: "[elementˇ]".into(),
10386 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
10387 expected_with_replace_suffix_mode: "[elementˇ]".into(),
10388 },
10389 Run {
10390 run_description: "Ends with matching suffix",
10391 initial_state: "SubˇError".into(),
10392 buffer_marked_text: "<Sub|Error>".into(),
10393 completion_text: "SubscriptionError",
10394 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
10395 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10396 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10397 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
10398 },
10399 Run {
10400 run_description: "Suffix is a subsequence -- contiguous",
10401 initial_state: "SubˇErr".into(),
10402 buffer_marked_text: "<Sub|Err>".into(),
10403 completion_text: "SubscriptionError",
10404 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
10405 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10406 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10407 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
10408 },
10409 Run {
10410 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
10411 initial_state: "Suˇscrirr".into(),
10412 buffer_marked_text: "<Su|scrirr>".into(),
10413 completion_text: "SubscriptionError",
10414 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
10415 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10416 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10417 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
10418 },
10419 Run {
10420 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
10421 initial_state: "foo(indˇix)".into(),
10422 buffer_marked_text: "foo(<ind|ix>)".into(),
10423 completion_text: "node_index",
10424 expected_with_insert_mode: "foo(node_indexˇix)".into(),
10425 expected_with_replace_mode: "foo(node_indexˇ)".into(),
10426 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
10427 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
10428 },
10429 ];
10430
10431 for run in runs {
10432 let run_variations = [
10433 (LspInsertMode::Insert, run.expected_with_insert_mode),
10434 (LspInsertMode::Replace, run.expected_with_replace_mode),
10435 (
10436 LspInsertMode::ReplaceSubsequence,
10437 run.expected_with_replace_subsequence_mode,
10438 ),
10439 (
10440 LspInsertMode::ReplaceSuffix,
10441 run.expected_with_replace_suffix_mode,
10442 ),
10443 ];
10444
10445 for (lsp_insert_mode, expected_text) in run_variations {
10446 eprintln!(
10447 "run = {:?}, mode = {lsp_insert_mode:.?}",
10448 run.run_description,
10449 );
10450
10451 update_test_language_settings(&mut cx, |settings| {
10452 settings.defaults.completions = Some(CompletionSettings {
10453 lsp_insert_mode,
10454 words: WordsCompletionMode::Disabled,
10455 lsp: true,
10456 lsp_fetch_timeout_ms: 0,
10457 });
10458 });
10459
10460 cx.set_state(&run.initial_state);
10461 cx.update_editor(|editor, window, cx| {
10462 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10463 });
10464
10465 let counter = Arc::new(AtomicUsize::new(0));
10466 handle_completion_request_with_insert_and_replace(
10467 &mut cx,
10468 &run.buffer_marked_text,
10469 vec![run.completion_text],
10470 counter.clone(),
10471 )
10472 .await;
10473 cx.condition(|editor, _| editor.context_menu_visible())
10474 .await;
10475 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
10476
10477 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10478 editor
10479 .confirm_completion(&ConfirmCompletion::default(), window, cx)
10480 .unwrap()
10481 });
10482 cx.assert_editor_state(&expected_text);
10483 handle_resolve_completion_request(&mut cx, None).await;
10484 apply_additional_edits.await.unwrap();
10485 }
10486 }
10487}
10488
10489#[gpui::test]
10490async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
10491 init_test(cx, |_| {});
10492 let mut cx = EditorLspTestContext::new_rust(
10493 lsp::ServerCapabilities {
10494 completion_provider: Some(lsp::CompletionOptions {
10495 resolve_provider: Some(true),
10496 ..Default::default()
10497 }),
10498 ..Default::default()
10499 },
10500 cx,
10501 )
10502 .await;
10503
10504 let initial_state = "SubˇError";
10505 let buffer_marked_text = "<Sub|Error>";
10506 let completion_text = "SubscriptionError";
10507 let expected_with_insert_mode = "SubscriptionErrorˇError";
10508 let expected_with_replace_mode = "SubscriptionErrorˇ";
10509
10510 update_test_language_settings(&mut cx, |settings| {
10511 settings.defaults.completions = Some(CompletionSettings {
10512 words: WordsCompletionMode::Disabled,
10513 // set the opposite here to ensure that the action is overriding the default behavior
10514 lsp_insert_mode: LspInsertMode::Insert,
10515 lsp: true,
10516 lsp_fetch_timeout_ms: 0,
10517 });
10518 });
10519
10520 cx.set_state(initial_state);
10521 cx.update_editor(|editor, window, cx| {
10522 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10523 });
10524
10525 let counter = Arc::new(AtomicUsize::new(0));
10526 handle_completion_request_with_insert_and_replace(
10527 &mut cx,
10528 &buffer_marked_text,
10529 vec![completion_text],
10530 counter.clone(),
10531 )
10532 .await;
10533 cx.condition(|editor, _| editor.context_menu_visible())
10534 .await;
10535 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
10536
10537 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10538 editor
10539 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10540 .unwrap()
10541 });
10542 cx.assert_editor_state(&expected_with_replace_mode);
10543 handle_resolve_completion_request(&mut cx, None).await;
10544 apply_additional_edits.await.unwrap();
10545
10546 update_test_language_settings(&mut cx, |settings| {
10547 settings.defaults.completions = Some(CompletionSettings {
10548 words: WordsCompletionMode::Disabled,
10549 // set the opposite here to ensure that the action is overriding the default behavior
10550 lsp_insert_mode: LspInsertMode::Replace,
10551 lsp: true,
10552 lsp_fetch_timeout_ms: 0,
10553 });
10554 });
10555
10556 cx.set_state(initial_state);
10557 cx.update_editor(|editor, window, cx| {
10558 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10559 });
10560 handle_completion_request_with_insert_and_replace(
10561 &mut cx,
10562 &buffer_marked_text,
10563 vec![completion_text],
10564 counter.clone(),
10565 )
10566 .await;
10567 cx.condition(|editor, _| editor.context_menu_visible())
10568 .await;
10569 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
10570
10571 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10572 editor
10573 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
10574 .unwrap()
10575 });
10576 cx.assert_editor_state(&expected_with_insert_mode);
10577 handle_resolve_completion_request(&mut cx, None).await;
10578 apply_additional_edits.await.unwrap();
10579}
10580
10581#[gpui::test]
10582async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
10583 init_test(cx, |_| {});
10584 let mut cx = EditorLspTestContext::new_rust(
10585 lsp::ServerCapabilities {
10586 completion_provider: Some(lsp::CompletionOptions {
10587 resolve_provider: Some(true),
10588 ..Default::default()
10589 }),
10590 ..Default::default()
10591 },
10592 cx,
10593 )
10594 .await;
10595
10596 // scenario: surrounding text matches completion text
10597 let completion_text = "to_offset";
10598 let initial_state = indoc! {"
10599 1. buf.to_offˇsuffix
10600 2. buf.to_offˇsuf
10601 3. buf.to_offˇfix
10602 4. buf.to_offˇ
10603 5. into_offˇensive
10604 6. ˇsuffix
10605 7. let ˇ //
10606 8. aaˇzz
10607 9. buf.to_off«zzzzzˇ»suffix
10608 10. buf.«ˇzzzzz»suffix
10609 11. to_off«ˇzzzzz»
10610
10611 buf.to_offˇsuffix // newest cursor
10612 "};
10613 let completion_marked_buffer = indoc! {"
10614 1. buf.to_offsuffix
10615 2. buf.to_offsuf
10616 3. buf.to_offfix
10617 4. buf.to_off
10618 5. into_offensive
10619 6. suffix
10620 7. let //
10621 8. aazz
10622 9. buf.to_offzzzzzsuffix
10623 10. buf.zzzzzsuffix
10624 11. to_offzzzzz
10625
10626 buf.<to_off|suffix> // newest cursor
10627 "};
10628 let expected = indoc! {"
10629 1. buf.to_offsetˇ
10630 2. buf.to_offsetˇsuf
10631 3. buf.to_offsetˇfix
10632 4. buf.to_offsetˇ
10633 5. into_offsetˇensive
10634 6. to_offsetˇsuffix
10635 7. let to_offsetˇ //
10636 8. aato_offsetˇzz
10637 9. buf.to_offsetˇ
10638 10. buf.to_offsetˇsuffix
10639 11. to_offsetˇ
10640
10641 buf.to_offsetˇ // newest cursor
10642 "};
10643 cx.set_state(initial_state);
10644 cx.update_editor(|editor, window, cx| {
10645 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10646 });
10647 handle_completion_request_with_insert_and_replace(
10648 &mut cx,
10649 completion_marked_buffer,
10650 vec![completion_text],
10651 Arc::new(AtomicUsize::new(0)),
10652 )
10653 .await;
10654 cx.condition(|editor, _| editor.context_menu_visible())
10655 .await;
10656 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10657 editor
10658 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10659 .unwrap()
10660 });
10661 cx.assert_editor_state(expected);
10662 handle_resolve_completion_request(&mut cx, None).await;
10663 apply_additional_edits.await.unwrap();
10664
10665 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
10666 let completion_text = "foo_and_bar";
10667 let initial_state = indoc! {"
10668 1. ooanbˇ
10669 2. zooanbˇ
10670 3. ooanbˇz
10671 4. zooanbˇz
10672 5. ooanˇ
10673 6. oanbˇ
10674
10675 ooanbˇ
10676 "};
10677 let completion_marked_buffer = indoc! {"
10678 1. ooanb
10679 2. zooanb
10680 3. ooanbz
10681 4. zooanbz
10682 5. ooan
10683 6. oanb
10684
10685 <ooanb|>
10686 "};
10687 let expected = indoc! {"
10688 1. foo_and_barˇ
10689 2. zfoo_and_barˇ
10690 3. foo_and_barˇz
10691 4. zfoo_and_barˇz
10692 5. ooanfoo_and_barˇ
10693 6. oanbfoo_and_barˇ
10694
10695 foo_and_barˇ
10696 "};
10697 cx.set_state(initial_state);
10698 cx.update_editor(|editor, window, cx| {
10699 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10700 });
10701 handle_completion_request_with_insert_and_replace(
10702 &mut cx,
10703 completion_marked_buffer,
10704 vec![completion_text],
10705 Arc::new(AtomicUsize::new(0)),
10706 )
10707 .await;
10708 cx.condition(|editor, _| editor.context_menu_visible())
10709 .await;
10710 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10711 editor
10712 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10713 .unwrap()
10714 });
10715 cx.assert_editor_state(expected);
10716 handle_resolve_completion_request(&mut cx, None).await;
10717 apply_additional_edits.await.unwrap();
10718
10719 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
10720 // (expects the same as if it was inserted at the end)
10721 let completion_text = "foo_and_bar";
10722 let initial_state = indoc! {"
10723 1. ooˇanb
10724 2. zooˇanb
10725 3. ooˇanbz
10726 4. zooˇanbz
10727
10728 ooˇanb
10729 "};
10730 let completion_marked_buffer = indoc! {"
10731 1. ooanb
10732 2. zooanb
10733 3. ooanbz
10734 4. zooanbz
10735
10736 <oo|anb>
10737 "};
10738 let expected = indoc! {"
10739 1. foo_and_barˇ
10740 2. zfoo_and_barˇ
10741 3. foo_and_barˇz
10742 4. zfoo_and_barˇz
10743
10744 foo_and_barˇ
10745 "};
10746 cx.set_state(initial_state);
10747 cx.update_editor(|editor, window, cx| {
10748 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10749 });
10750 handle_completion_request_with_insert_and_replace(
10751 &mut cx,
10752 completion_marked_buffer,
10753 vec![completion_text],
10754 Arc::new(AtomicUsize::new(0)),
10755 )
10756 .await;
10757 cx.condition(|editor, _| editor.context_menu_visible())
10758 .await;
10759 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10760 editor
10761 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10762 .unwrap()
10763 });
10764 cx.assert_editor_state(expected);
10765 handle_resolve_completion_request(&mut cx, None).await;
10766 apply_additional_edits.await.unwrap();
10767}
10768
10769// This used to crash
10770#[gpui::test]
10771async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
10772 init_test(cx, |_| {});
10773
10774 let buffer_text = indoc! {"
10775 fn main() {
10776 10.satu;
10777
10778 //
10779 // separate cursors so they open in different excerpts (manually reproducible)
10780 //
10781
10782 10.satu20;
10783 }
10784 "};
10785 let multibuffer_text_with_selections = indoc! {"
10786 fn main() {
10787 10.satuˇ;
10788
10789 //
10790
10791 //
10792
10793 10.satuˇ20;
10794 }
10795 "};
10796 let expected_multibuffer = indoc! {"
10797 fn main() {
10798 10.saturating_sub()ˇ;
10799
10800 //
10801
10802 //
10803
10804 10.saturating_sub()ˇ;
10805 }
10806 "};
10807
10808 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
10809 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
10810
10811 let fs = FakeFs::new(cx.executor());
10812 fs.insert_tree(
10813 path!("/a"),
10814 json!({
10815 "main.rs": buffer_text,
10816 }),
10817 )
10818 .await;
10819
10820 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
10821 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10822 language_registry.add(rust_lang());
10823 let mut fake_servers = language_registry.register_fake_lsp(
10824 "Rust",
10825 FakeLspAdapter {
10826 capabilities: lsp::ServerCapabilities {
10827 completion_provider: Some(lsp::CompletionOptions {
10828 resolve_provider: None,
10829 ..lsp::CompletionOptions::default()
10830 }),
10831 ..lsp::ServerCapabilities::default()
10832 },
10833 ..FakeLspAdapter::default()
10834 },
10835 );
10836 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
10837 let cx = &mut VisualTestContext::from_window(*workspace, cx);
10838 let buffer = project
10839 .update(cx, |project, cx| {
10840 project.open_local_buffer(path!("/a/main.rs"), cx)
10841 })
10842 .await
10843 .unwrap();
10844
10845 let multi_buffer = cx.new(|cx| {
10846 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
10847 multi_buffer.push_excerpts(
10848 buffer.clone(),
10849 [ExcerptRange::new(0..first_excerpt_end)],
10850 cx,
10851 );
10852 multi_buffer.push_excerpts(
10853 buffer.clone(),
10854 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
10855 cx,
10856 );
10857 multi_buffer
10858 });
10859
10860 let editor = workspace
10861 .update(cx, |_, window, cx| {
10862 cx.new(|cx| {
10863 Editor::new(
10864 EditorMode::Full {
10865 scale_ui_elements_with_buffer_font_size: false,
10866 show_active_line_background: false,
10867 sized_by_content: false,
10868 },
10869 multi_buffer.clone(),
10870 Some(project.clone()),
10871 window,
10872 cx,
10873 )
10874 })
10875 })
10876 .unwrap();
10877
10878 let pane = workspace
10879 .update(cx, |workspace, _, _| workspace.active_pane().clone())
10880 .unwrap();
10881 pane.update_in(cx, |pane, window, cx| {
10882 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
10883 });
10884
10885 let fake_server = fake_servers.next().await.unwrap();
10886
10887 editor.update_in(cx, |editor, window, cx| {
10888 editor.change_selections(None, window, cx, |s| {
10889 s.select_ranges([
10890 Point::new(1, 11)..Point::new(1, 11),
10891 Point::new(7, 11)..Point::new(7, 11),
10892 ])
10893 });
10894
10895 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
10896 });
10897
10898 editor.update_in(cx, |editor, window, cx| {
10899 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10900 });
10901
10902 fake_server
10903 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
10904 let completion_item = lsp::CompletionItem {
10905 label: "saturating_sub()".into(),
10906 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
10907 lsp::InsertReplaceEdit {
10908 new_text: "saturating_sub()".to_owned(),
10909 insert: lsp::Range::new(
10910 lsp::Position::new(7, 7),
10911 lsp::Position::new(7, 11),
10912 ),
10913 replace: lsp::Range::new(
10914 lsp::Position::new(7, 7),
10915 lsp::Position::new(7, 13),
10916 ),
10917 },
10918 )),
10919 ..lsp::CompletionItem::default()
10920 };
10921
10922 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
10923 })
10924 .next()
10925 .await
10926 .unwrap();
10927
10928 cx.condition(&editor, |editor, _| editor.context_menu_visible())
10929 .await;
10930
10931 editor
10932 .update_in(cx, |editor, window, cx| {
10933 editor
10934 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10935 .unwrap()
10936 })
10937 .await
10938 .unwrap();
10939
10940 editor.update(cx, |editor, cx| {
10941 assert_text_with_selections(editor, expected_multibuffer, cx);
10942 })
10943}
10944
10945#[gpui::test]
10946async fn test_completion(cx: &mut TestAppContext) {
10947 init_test(cx, |_| {});
10948
10949 let mut cx = EditorLspTestContext::new_rust(
10950 lsp::ServerCapabilities {
10951 completion_provider: Some(lsp::CompletionOptions {
10952 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
10953 resolve_provider: Some(true),
10954 ..Default::default()
10955 }),
10956 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
10957 ..Default::default()
10958 },
10959 cx,
10960 )
10961 .await;
10962 let counter = Arc::new(AtomicUsize::new(0));
10963
10964 cx.set_state(indoc! {"
10965 oneˇ
10966 two
10967 three
10968 "});
10969 cx.simulate_keystroke(".");
10970 handle_completion_request(
10971 &mut cx,
10972 indoc! {"
10973 one.|<>
10974 two
10975 three
10976 "},
10977 vec!["first_completion", "second_completion"],
10978 counter.clone(),
10979 )
10980 .await;
10981 cx.condition(|editor, _| editor.context_menu_visible())
10982 .await;
10983 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
10984
10985 let _handler = handle_signature_help_request(
10986 &mut cx,
10987 lsp::SignatureHelp {
10988 signatures: vec![lsp::SignatureInformation {
10989 label: "test signature".to_string(),
10990 documentation: None,
10991 parameters: Some(vec![lsp::ParameterInformation {
10992 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
10993 documentation: None,
10994 }]),
10995 active_parameter: None,
10996 }],
10997 active_signature: None,
10998 active_parameter: None,
10999 },
11000 );
11001 cx.update_editor(|editor, window, cx| {
11002 assert!(
11003 !editor.signature_help_state.is_shown(),
11004 "No signature help was called for"
11005 );
11006 editor.show_signature_help(&ShowSignatureHelp, window, cx);
11007 });
11008 cx.run_until_parked();
11009 cx.update_editor(|editor, _, _| {
11010 assert!(
11011 !editor.signature_help_state.is_shown(),
11012 "No signature help should be shown when completions menu is open"
11013 );
11014 });
11015
11016 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11017 editor.context_menu_next(&Default::default(), window, cx);
11018 editor
11019 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11020 .unwrap()
11021 });
11022 cx.assert_editor_state(indoc! {"
11023 one.second_completionˇ
11024 two
11025 three
11026 "});
11027
11028 handle_resolve_completion_request(
11029 &mut cx,
11030 Some(vec![
11031 (
11032 //This overlaps with the primary completion edit which is
11033 //misbehavior from the LSP spec, test that we filter it out
11034 indoc! {"
11035 one.second_ˇcompletion
11036 two
11037 threeˇ
11038 "},
11039 "overlapping additional edit",
11040 ),
11041 (
11042 indoc! {"
11043 one.second_completion
11044 two
11045 threeˇ
11046 "},
11047 "\nadditional edit",
11048 ),
11049 ]),
11050 )
11051 .await;
11052 apply_additional_edits.await.unwrap();
11053 cx.assert_editor_state(indoc! {"
11054 one.second_completionˇ
11055 two
11056 three
11057 additional edit
11058 "});
11059
11060 cx.set_state(indoc! {"
11061 one.second_completion
11062 twoˇ
11063 threeˇ
11064 additional edit
11065 "});
11066 cx.simulate_keystroke(" ");
11067 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11068 cx.simulate_keystroke("s");
11069 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11070
11071 cx.assert_editor_state(indoc! {"
11072 one.second_completion
11073 two sˇ
11074 three sˇ
11075 additional edit
11076 "});
11077 handle_completion_request(
11078 &mut cx,
11079 indoc! {"
11080 one.second_completion
11081 two s
11082 three <s|>
11083 additional edit
11084 "},
11085 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
11086 counter.clone(),
11087 )
11088 .await;
11089 cx.condition(|editor, _| editor.context_menu_visible())
11090 .await;
11091 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
11092
11093 cx.simulate_keystroke("i");
11094
11095 handle_completion_request(
11096 &mut cx,
11097 indoc! {"
11098 one.second_completion
11099 two si
11100 three <si|>
11101 additional edit
11102 "},
11103 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
11104 counter.clone(),
11105 )
11106 .await;
11107 cx.condition(|editor, _| editor.context_menu_visible())
11108 .await;
11109 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
11110
11111 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11112 editor
11113 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11114 .unwrap()
11115 });
11116 cx.assert_editor_state(indoc! {"
11117 one.second_completion
11118 two sixth_completionˇ
11119 three sixth_completionˇ
11120 additional edit
11121 "});
11122
11123 apply_additional_edits.await.unwrap();
11124
11125 update_test_language_settings(&mut cx, |settings| {
11126 settings.defaults.show_completions_on_input = Some(false);
11127 });
11128 cx.set_state("editorˇ");
11129 cx.simulate_keystroke(".");
11130 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11131 cx.simulate_keystrokes("c l o");
11132 cx.assert_editor_state("editor.cloˇ");
11133 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11134 cx.update_editor(|editor, window, cx| {
11135 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11136 });
11137 handle_completion_request(
11138 &mut cx,
11139 "editor.<clo|>",
11140 vec!["close", "clobber"],
11141 counter.clone(),
11142 )
11143 .await;
11144 cx.condition(|editor, _| editor.context_menu_visible())
11145 .await;
11146 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
11147
11148 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11149 editor
11150 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11151 .unwrap()
11152 });
11153 cx.assert_editor_state("editor.closeˇ");
11154 handle_resolve_completion_request(&mut cx, None).await;
11155 apply_additional_edits.await.unwrap();
11156}
11157
11158#[gpui::test]
11159async fn test_word_completion(cx: &mut TestAppContext) {
11160 let lsp_fetch_timeout_ms = 10;
11161 init_test(cx, |language_settings| {
11162 language_settings.defaults.completions = Some(CompletionSettings {
11163 words: WordsCompletionMode::Fallback,
11164 lsp: true,
11165 lsp_fetch_timeout_ms: 10,
11166 lsp_insert_mode: LspInsertMode::Insert,
11167 });
11168 });
11169
11170 let mut cx = EditorLspTestContext::new_rust(
11171 lsp::ServerCapabilities {
11172 completion_provider: Some(lsp::CompletionOptions {
11173 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11174 ..lsp::CompletionOptions::default()
11175 }),
11176 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11177 ..lsp::ServerCapabilities::default()
11178 },
11179 cx,
11180 )
11181 .await;
11182
11183 let throttle_completions = Arc::new(AtomicBool::new(false));
11184
11185 let lsp_throttle_completions = throttle_completions.clone();
11186 let _completion_requests_handler =
11187 cx.lsp
11188 .server
11189 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
11190 let lsp_throttle_completions = lsp_throttle_completions.clone();
11191 let cx = cx.clone();
11192 async move {
11193 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
11194 cx.background_executor()
11195 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
11196 .await;
11197 }
11198 Ok(Some(lsp::CompletionResponse::Array(vec![
11199 lsp::CompletionItem {
11200 label: "first".into(),
11201 ..lsp::CompletionItem::default()
11202 },
11203 lsp::CompletionItem {
11204 label: "last".into(),
11205 ..lsp::CompletionItem::default()
11206 },
11207 ])))
11208 }
11209 });
11210
11211 cx.set_state(indoc! {"
11212 oneˇ
11213 two
11214 three
11215 "});
11216 cx.simulate_keystroke(".");
11217 cx.executor().run_until_parked();
11218 cx.condition(|editor, _| editor.context_menu_visible())
11219 .await;
11220 cx.update_editor(|editor, window, cx| {
11221 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11222 {
11223 assert_eq!(
11224 completion_menu_entries(&menu),
11225 &["first", "last"],
11226 "When LSP server is fast to reply, no fallback word completions are used"
11227 );
11228 } else {
11229 panic!("expected completion menu to be open");
11230 }
11231 editor.cancel(&Cancel, window, cx);
11232 });
11233 cx.executor().run_until_parked();
11234 cx.condition(|editor, _| !editor.context_menu_visible())
11235 .await;
11236
11237 throttle_completions.store(true, atomic::Ordering::Release);
11238 cx.simulate_keystroke(".");
11239 cx.executor()
11240 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
11241 cx.executor().run_until_parked();
11242 cx.condition(|editor, _| editor.context_menu_visible())
11243 .await;
11244 cx.update_editor(|editor, _, _| {
11245 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11246 {
11247 assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
11248 "When LSP server is slow, document words can be shown instead, if configured accordingly");
11249 } else {
11250 panic!("expected completion menu to be open");
11251 }
11252 });
11253}
11254
11255#[gpui::test]
11256async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
11257 init_test(cx, |language_settings| {
11258 language_settings.defaults.completions = Some(CompletionSettings {
11259 words: WordsCompletionMode::Enabled,
11260 lsp: true,
11261 lsp_fetch_timeout_ms: 0,
11262 lsp_insert_mode: LspInsertMode::Insert,
11263 });
11264 });
11265
11266 let mut cx = EditorLspTestContext::new_rust(
11267 lsp::ServerCapabilities {
11268 completion_provider: Some(lsp::CompletionOptions {
11269 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11270 ..lsp::CompletionOptions::default()
11271 }),
11272 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11273 ..lsp::ServerCapabilities::default()
11274 },
11275 cx,
11276 )
11277 .await;
11278
11279 let _completion_requests_handler =
11280 cx.lsp
11281 .server
11282 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
11283 Ok(Some(lsp::CompletionResponse::Array(vec![
11284 lsp::CompletionItem {
11285 label: "first".into(),
11286 ..lsp::CompletionItem::default()
11287 },
11288 lsp::CompletionItem {
11289 label: "last".into(),
11290 ..lsp::CompletionItem::default()
11291 },
11292 ])))
11293 });
11294
11295 cx.set_state(indoc! {"ˇ
11296 first
11297 last
11298 second
11299 "});
11300 cx.simulate_keystroke(".");
11301 cx.executor().run_until_parked();
11302 cx.condition(|editor, _| editor.context_menu_visible())
11303 .await;
11304 cx.update_editor(|editor, _, _| {
11305 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11306 {
11307 assert_eq!(
11308 completion_menu_entries(&menu),
11309 &["first", "last", "second"],
11310 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
11311 );
11312 } else {
11313 panic!("expected completion menu to be open");
11314 }
11315 });
11316}
11317
11318#[gpui::test]
11319async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
11320 init_test(cx, |language_settings| {
11321 language_settings.defaults.completions = Some(CompletionSettings {
11322 words: WordsCompletionMode::Disabled,
11323 lsp: true,
11324 lsp_fetch_timeout_ms: 0,
11325 lsp_insert_mode: LspInsertMode::Insert,
11326 });
11327 });
11328
11329 let mut cx = EditorLspTestContext::new_rust(
11330 lsp::ServerCapabilities {
11331 completion_provider: Some(lsp::CompletionOptions {
11332 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11333 ..lsp::CompletionOptions::default()
11334 }),
11335 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11336 ..lsp::ServerCapabilities::default()
11337 },
11338 cx,
11339 )
11340 .await;
11341
11342 let _completion_requests_handler =
11343 cx.lsp
11344 .server
11345 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
11346 panic!("LSP completions should not be queried when dealing with word completions")
11347 });
11348
11349 cx.set_state(indoc! {"ˇ
11350 first
11351 last
11352 second
11353 "});
11354 cx.update_editor(|editor, window, cx| {
11355 editor.show_word_completions(&ShowWordCompletions, window, cx);
11356 });
11357 cx.executor().run_until_parked();
11358 cx.condition(|editor, _| editor.context_menu_visible())
11359 .await;
11360 cx.update_editor(|editor, _, _| {
11361 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11362 {
11363 assert_eq!(
11364 completion_menu_entries(&menu),
11365 &["first", "last", "second"],
11366 "`ShowWordCompletions` action should show word completions"
11367 );
11368 } else {
11369 panic!("expected completion menu to be open");
11370 }
11371 });
11372
11373 cx.simulate_keystroke("l");
11374 cx.executor().run_until_parked();
11375 cx.condition(|editor, _| editor.context_menu_visible())
11376 .await;
11377 cx.update_editor(|editor, _, _| {
11378 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11379 {
11380 assert_eq!(
11381 completion_menu_entries(&menu),
11382 &["last"],
11383 "After showing word completions, further editing should filter them and not query the LSP"
11384 );
11385 } else {
11386 panic!("expected completion menu to be open");
11387 }
11388 });
11389}
11390
11391#[gpui::test]
11392async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
11393 init_test(cx, |language_settings| {
11394 language_settings.defaults.completions = Some(CompletionSettings {
11395 words: WordsCompletionMode::Fallback,
11396 lsp: false,
11397 lsp_fetch_timeout_ms: 0,
11398 lsp_insert_mode: LspInsertMode::Insert,
11399 });
11400 });
11401
11402 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
11403
11404 cx.set_state(indoc! {"ˇ
11405 0_usize
11406 let
11407 33
11408 4.5f32
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, window, cx| {
11417 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11418 {
11419 assert_eq!(
11420 completion_menu_entries(&menu),
11421 &["let"],
11422 "With no digits in the completion query, no digits should be in the word completions"
11423 );
11424 } else {
11425 panic!("expected completion menu to be open");
11426 }
11427 editor.cancel(&Cancel, window, cx);
11428 });
11429
11430 cx.set_state(indoc! {"3ˇ
11431 0_usize
11432 let
11433 3
11434 33.35f32
11435 "});
11436 cx.update_editor(|editor, window, cx| {
11437 editor.show_completions(&ShowCompletions::default(), window, cx);
11438 });
11439 cx.executor().run_until_parked();
11440 cx.condition(|editor, _| editor.context_menu_visible())
11441 .await;
11442 cx.update_editor(|editor, _, _| {
11443 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11444 {
11445 assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
11446 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
11447 } else {
11448 panic!("expected completion menu to be open");
11449 }
11450 });
11451}
11452
11453fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
11454 let position = || lsp::Position {
11455 line: params.text_document_position.position.line,
11456 character: params.text_document_position.position.character,
11457 };
11458 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11459 range: lsp::Range {
11460 start: position(),
11461 end: position(),
11462 },
11463 new_text: text.to_string(),
11464 }))
11465}
11466
11467#[gpui::test]
11468async fn test_multiline_completion(cx: &mut TestAppContext) {
11469 init_test(cx, |_| {});
11470
11471 let fs = FakeFs::new(cx.executor());
11472 fs.insert_tree(
11473 path!("/a"),
11474 json!({
11475 "main.ts": "a",
11476 }),
11477 )
11478 .await;
11479
11480 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11481 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11482 let typescript_language = Arc::new(Language::new(
11483 LanguageConfig {
11484 name: "TypeScript".into(),
11485 matcher: LanguageMatcher {
11486 path_suffixes: vec!["ts".to_string()],
11487 ..LanguageMatcher::default()
11488 },
11489 line_comments: vec!["// ".into()],
11490 ..LanguageConfig::default()
11491 },
11492 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
11493 ));
11494 language_registry.add(typescript_language.clone());
11495 let mut fake_servers = language_registry.register_fake_lsp(
11496 "TypeScript",
11497 FakeLspAdapter {
11498 capabilities: lsp::ServerCapabilities {
11499 completion_provider: Some(lsp::CompletionOptions {
11500 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11501 ..lsp::CompletionOptions::default()
11502 }),
11503 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11504 ..lsp::ServerCapabilities::default()
11505 },
11506 // Emulate vtsls label generation
11507 label_for_completion: Some(Box::new(|item, _| {
11508 let text = if let Some(description) = item
11509 .label_details
11510 .as_ref()
11511 .and_then(|label_details| label_details.description.as_ref())
11512 {
11513 format!("{} {}", item.label, description)
11514 } else if let Some(detail) = &item.detail {
11515 format!("{} {}", item.label, detail)
11516 } else {
11517 item.label.clone()
11518 };
11519 let len = text.len();
11520 Some(language::CodeLabel {
11521 text,
11522 runs: Vec::new(),
11523 filter_range: 0..len,
11524 })
11525 })),
11526 ..FakeLspAdapter::default()
11527 },
11528 );
11529 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11530 let cx = &mut VisualTestContext::from_window(*workspace, cx);
11531 let worktree_id = workspace
11532 .update(cx, |workspace, _window, cx| {
11533 workspace.project().update(cx, |project, cx| {
11534 project.worktrees(cx).next().unwrap().read(cx).id()
11535 })
11536 })
11537 .unwrap();
11538 let _buffer = project
11539 .update(cx, |project, cx| {
11540 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
11541 })
11542 .await
11543 .unwrap();
11544 let editor = workspace
11545 .update(cx, |workspace, window, cx| {
11546 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
11547 })
11548 .unwrap()
11549 .await
11550 .unwrap()
11551 .downcast::<Editor>()
11552 .unwrap();
11553 let fake_server = fake_servers.next().await.unwrap();
11554
11555 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
11556 let multiline_label_2 = "a\nb\nc\n";
11557 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
11558 let multiline_description = "d\ne\nf\n";
11559 let multiline_detail_2 = "g\nh\ni\n";
11560
11561 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
11562 move |params, _| async move {
11563 Ok(Some(lsp::CompletionResponse::Array(vec![
11564 lsp::CompletionItem {
11565 label: multiline_label.to_string(),
11566 text_edit: gen_text_edit(¶ms, "new_text_1"),
11567 ..lsp::CompletionItem::default()
11568 },
11569 lsp::CompletionItem {
11570 label: "single line label 1".to_string(),
11571 detail: Some(multiline_detail.to_string()),
11572 text_edit: gen_text_edit(¶ms, "new_text_2"),
11573 ..lsp::CompletionItem::default()
11574 },
11575 lsp::CompletionItem {
11576 label: "single line label 2".to_string(),
11577 label_details: Some(lsp::CompletionItemLabelDetails {
11578 description: Some(multiline_description.to_string()),
11579 detail: None,
11580 }),
11581 text_edit: gen_text_edit(¶ms, "new_text_2"),
11582 ..lsp::CompletionItem::default()
11583 },
11584 lsp::CompletionItem {
11585 label: multiline_label_2.to_string(),
11586 detail: Some(multiline_detail_2.to_string()),
11587 text_edit: gen_text_edit(¶ms, "new_text_3"),
11588 ..lsp::CompletionItem::default()
11589 },
11590 lsp::CompletionItem {
11591 label: "Label with many spaces and \t but without newlines".to_string(),
11592 detail: Some(
11593 "Details with many spaces and \t but without newlines".to_string(),
11594 ),
11595 text_edit: gen_text_edit(¶ms, "new_text_4"),
11596 ..lsp::CompletionItem::default()
11597 },
11598 ])))
11599 },
11600 );
11601
11602 editor.update_in(cx, |editor, window, cx| {
11603 cx.focus_self(window);
11604 editor.move_to_end(&MoveToEnd, window, cx);
11605 editor.handle_input(".", window, cx);
11606 });
11607 cx.run_until_parked();
11608 completion_handle.next().await.unwrap();
11609
11610 editor.update(cx, |editor, _| {
11611 assert!(editor.context_menu_visible());
11612 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11613 {
11614 let completion_labels = menu
11615 .completions
11616 .borrow()
11617 .iter()
11618 .map(|c| c.label.text.clone())
11619 .collect::<Vec<_>>();
11620 assert_eq!(
11621 completion_labels,
11622 &[
11623 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
11624 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
11625 "single line label 2 d e f ",
11626 "a b c g h i ",
11627 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
11628 ],
11629 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
11630 );
11631
11632 for completion in menu
11633 .completions
11634 .borrow()
11635 .iter() {
11636 assert_eq!(
11637 completion.label.filter_range,
11638 0..completion.label.text.len(),
11639 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
11640 );
11641 }
11642 } else {
11643 panic!("expected completion menu to be open");
11644 }
11645 });
11646}
11647
11648#[gpui::test]
11649async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
11650 init_test(cx, |_| {});
11651 let mut cx = EditorLspTestContext::new_rust(
11652 lsp::ServerCapabilities {
11653 completion_provider: Some(lsp::CompletionOptions {
11654 trigger_characters: Some(vec![".".to_string()]),
11655 ..Default::default()
11656 }),
11657 ..Default::default()
11658 },
11659 cx,
11660 )
11661 .await;
11662 cx.lsp
11663 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
11664 Ok(Some(lsp::CompletionResponse::Array(vec![
11665 lsp::CompletionItem {
11666 label: "first".into(),
11667 ..Default::default()
11668 },
11669 lsp::CompletionItem {
11670 label: "last".into(),
11671 ..Default::default()
11672 },
11673 ])))
11674 });
11675 cx.set_state("variableˇ");
11676 cx.simulate_keystroke(".");
11677 cx.executor().run_until_parked();
11678
11679 cx.update_editor(|editor, _, _| {
11680 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11681 {
11682 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
11683 } else {
11684 panic!("expected completion menu to be open");
11685 }
11686 });
11687
11688 cx.update_editor(|editor, window, cx| {
11689 editor.move_page_down(&MovePageDown::default(), window, cx);
11690 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11691 {
11692 assert!(
11693 menu.selected_item == 1,
11694 "expected PageDown to select the last item from the context menu"
11695 );
11696 } else {
11697 panic!("expected completion menu to stay open after PageDown");
11698 }
11699 });
11700
11701 cx.update_editor(|editor, window, cx| {
11702 editor.move_page_up(&MovePageUp::default(), window, cx);
11703 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11704 {
11705 assert!(
11706 menu.selected_item == 0,
11707 "expected PageUp to select the first item from the context menu"
11708 );
11709 } else {
11710 panic!("expected completion menu to stay open after PageUp");
11711 }
11712 });
11713}
11714
11715#[gpui::test]
11716async fn test_as_is_completions(cx: &mut TestAppContext) {
11717 init_test(cx, |_| {});
11718 let mut cx = EditorLspTestContext::new_rust(
11719 lsp::ServerCapabilities {
11720 completion_provider: Some(lsp::CompletionOptions {
11721 ..Default::default()
11722 }),
11723 ..Default::default()
11724 },
11725 cx,
11726 )
11727 .await;
11728 cx.lsp
11729 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
11730 Ok(Some(lsp::CompletionResponse::Array(vec![
11731 lsp::CompletionItem {
11732 label: "unsafe".into(),
11733 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11734 range: lsp::Range {
11735 start: lsp::Position {
11736 line: 1,
11737 character: 2,
11738 },
11739 end: lsp::Position {
11740 line: 1,
11741 character: 3,
11742 },
11743 },
11744 new_text: "unsafe".to_string(),
11745 })),
11746 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
11747 ..Default::default()
11748 },
11749 ])))
11750 });
11751 cx.set_state("fn a() {}\n nˇ");
11752 cx.executor().run_until_parked();
11753 cx.update_editor(|editor, window, cx| {
11754 editor.show_completions(
11755 &ShowCompletions {
11756 trigger: Some("\n".into()),
11757 },
11758 window,
11759 cx,
11760 );
11761 });
11762 cx.executor().run_until_parked();
11763
11764 cx.update_editor(|editor, window, cx| {
11765 editor.confirm_completion(&Default::default(), window, cx)
11766 });
11767 cx.executor().run_until_parked();
11768 cx.assert_editor_state("fn a() {}\n unsafeˇ");
11769}
11770
11771#[gpui::test]
11772async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
11773 init_test(cx, |_| {});
11774
11775 let mut cx = EditorLspTestContext::new_rust(
11776 lsp::ServerCapabilities {
11777 completion_provider: Some(lsp::CompletionOptions {
11778 trigger_characters: Some(vec![".".to_string()]),
11779 resolve_provider: Some(true),
11780 ..Default::default()
11781 }),
11782 ..Default::default()
11783 },
11784 cx,
11785 )
11786 .await;
11787
11788 cx.set_state("fn main() { let a = 2ˇ; }");
11789 cx.simulate_keystroke(".");
11790 let completion_item = lsp::CompletionItem {
11791 label: "Some".into(),
11792 kind: Some(lsp::CompletionItemKind::SNIPPET),
11793 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
11794 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
11795 kind: lsp::MarkupKind::Markdown,
11796 value: "```rust\nSome(2)\n```".to_string(),
11797 })),
11798 deprecated: Some(false),
11799 sort_text: Some("Some".to_string()),
11800 filter_text: Some("Some".to_string()),
11801 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
11802 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11803 range: lsp::Range {
11804 start: lsp::Position {
11805 line: 0,
11806 character: 22,
11807 },
11808 end: lsp::Position {
11809 line: 0,
11810 character: 22,
11811 },
11812 },
11813 new_text: "Some(2)".to_string(),
11814 })),
11815 additional_text_edits: Some(vec![lsp::TextEdit {
11816 range: lsp::Range {
11817 start: lsp::Position {
11818 line: 0,
11819 character: 20,
11820 },
11821 end: lsp::Position {
11822 line: 0,
11823 character: 22,
11824 },
11825 },
11826 new_text: "".to_string(),
11827 }]),
11828 ..Default::default()
11829 };
11830
11831 let closure_completion_item = completion_item.clone();
11832 let counter = Arc::new(AtomicUsize::new(0));
11833 let counter_clone = counter.clone();
11834 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
11835 let task_completion_item = closure_completion_item.clone();
11836 counter_clone.fetch_add(1, atomic::Ordering::Release);
11837 async move {
11838 Ok(Some(lsp::CompletionResponse::Array(vec![
11839 task_completion_item,
11840 ])))
11841 }
11842 });
11843
11844 cx.condition(|editor, _| editor.context_menu_visible())
11845 .await;
11846 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
11847 assert!(request.next().await.is_some());
11848 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11849
11850 cx.simulate_keystrokes("S o m");
11851 cx.condition(|editor, _| editor.context_menu_visible())
11852 .await;
11853 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
11854 assert!(request.next().await.is_some());
11855 assert!(request.next().await.is_some());
11856 assert!(request.next().await.is_some());
11857 request.close();
11858 assert!(request.next().await.is_none());
11859 assert_eq!(
11860 counter.load(atomic::Ordering::Acquire),
11861 4,
11862 "With the completions menu open, only one LSP request should happen per input"
11863 );
11864}
11865
11866#[gpui::test]
11867async fn test_toggle_comment(cx: &mut TestAppContext) {
11868 init_test(cx, |_| {});
11869 let mut cx = EditorTestContext::new(cx).await;
11870 let language = Arc::new(Language::new(
11871 LanguageConfig {
11872 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
11873 ..Default::default()
11874 },
11875 Some(tree_sitter_rust::LANGUAGE.into()),
11876 ));
11877 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
11878
11879 // If multiple selections intersect a line, the line is only toggled once.
11880 cx.set_state(indoc! {"
11881 fn a() {
11882 «//b();
11883 ˇ»// «c();
11884 //ˇ» d();
11885 }
11886 "});
11887
11888 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11889
11890 cx.assert_editor_state(indoc! {"
11891 fn a() {
11892 «b();
11893 c();
11894 ˇ» d();
11895 }
11896 "});
11897
11898 // The comment prefix is inserted at the same column for every line in a
11899 // selection.
11900 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11901
11902 cx.assert_editor_state(indoc! {"
11903 fn a() {
11904 // «b();
11905 // c();
11906 ˇ»// d();
11907 }
11908 "});
11909
11910 // If a selection ends at the beginning of a line, that line is not toggled.
11911 cx.set_selections_state(indoc! {"
11912 fn a() {
11913 // b();
11914 «// c();
11915 ˇ» // d();
11916 }
11917 "});
11918
11919 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11920
11921 cx.assert_editor_state(indoc! {"
11922 fn a() {
11923 // b();
11924 «c();
11925 ˇ» // d();
11926 }
11927 "});
11928
11929 // If a selection span a single line and is empty, the line is toggled.
11930 cx.set_state(indoc! {"
11931 fn a() {
11932 a();
11933 b();
11934 ˇ
11935 }
11936 "});
11937
11938 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11939
11940 cx.assert_editor_state(indoc! {"
11941 fn a() {
11942 a();
11943 b();
11944 //•ˇ
11945 }
11946 "});
11947
11948 // If a selection span multiple lines, empty lines are not toggled.
11949 cx.set_state(indoc! {"
11950 fn a() {
11951 «a();
11952
11953 c();ˇ»
11954 }
11955 "});
11956
11957 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11958
11959 cx.assert_editor_state(indoc! {"
11960 fn a() {
11961 // «a();
11962
11963 // c();ˇ»
11964 }
11965 "});
11966
11967 // If a selection includes multiple comment prefixes, all lines are uncommented.
11968 cx.set_state(indoc! {"
11969 fn a() {
11970 «// a();
11971 /// b();
11972 //! c();ˇ»
11973 }
11974 "});
11975
11976 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11977
11978 cx.assert_editor_state(indoc! {"
11979 fn a() {
11980 «a();
11981 b();
11982 c();ˇ»
11983 }
11984 "});
11985}
11986
11987#[gpui::test]
11988async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
11989 init_test(cx, |_| {});
11990 let mut cx = EditorTestContext::new(cx).await;
11991 let language = Arc::new(Language::new(
11992 LanguageConfig {
11993 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
11994 ..Default::default()
11995 },
11996 Some(tree_sitter_rust::LANGUAGE.into()),
11997 ));
11998 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
11999
12000 let toggle_comments = &ToggleComments {
12001 advance_downwards: false,
12002 ignore_indent: true,
12003 };
12004
12005 // If multiple selections intersect a line, the line is only toggled once.
12006 cx.set_state(indoc! {"
12007 fn a() {
12008 // «b();
12009 // c();
12010 // ˇ» d();
12011 }
12012 "});
12013
12014 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12015
12016 cx.assert_editor_state(indoc! {"
12017 fn a() {
12018 «b();
12019 c();
12020 ˇ» d();
12021 }
12022 "});
12023
12024 // The comment prefix is inserted at the beginning of each line
12025 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12026
12027 cx.assert_editor_state(indoc! {"
12028 fn a() {
12029 // «b();
12030 // c();
12031 // ˇ» d();
12032 }
12033 "});
12034
12035 // If a selection ends at the beginning of a line, that line is not toggled.
12036 cx.set_selections_state(indoc! {"
12037 fn a() {
12038 // b();
12039 // «c();
12040 ˇ»// d();
12041 }
12042 "});
12043
12044 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12045
12046 cx.assert_editor_state(indoc! {"
12047 fn a() {
12048 // b();
12049 «c();
12050 ˇ»// d();
12051 }
12052 "});
12053
12054 // If a selection span a single line and is empty, the line is toggled.
12055 cx.set_state(indoc! {"
12056 fn a() {
12057 a();
12058 b();
12059 ˇ
12060 }
12061 "});
12062
12063 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12064
12065 cx.assert_editor_state(indoc! {"
12066 fn a() {
12067 a();
12068 b();
12069 //ˇ
12070 }
12071 "});
12072
12073 // If a selection span multiple lines, empty lines are not toggled.
12074 cx.set_state(indoc! {"
12075 fn a() {
12076 «a();
12077
12078 c();ˇ»
12079 }
12080 "});
12081
12082 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12083
12084 cx.assert_editor_state(indoc! {"
12085 fn a() {
12086 // «a();
12087
12088 // c();ˇ»
12089 }
12090 "});
12091
12092 // If a selection includes multiple comment prefixes, all lines are uncommented.
12093 cx.set_state(indoc! {"
12094 fn a() {
12095 // «a();
12096 /// b();
12097 //! c();ˇ»
12098 }
12099 "});
12100
12101 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12102
12103 cx.assert_editor_state(indoc! {"
12104 fn a() {
12105 «a();
12106 b();
12107 c();ˇ»
12108 }
12109 "});
12110}
12111
12112#[gpui::test]
12113async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
12114 init_test(cx, |_| {});
12115
12116 let language = Arc::new(Language::new(
12117 LanguageConfig {
12118 line_comments: vec!["// ".into()],
12119 ..Default::default()
12120 },
12121 Some(tree_sitter_rust::LANGUAGE.into()),
12122 ));
12123
12124 let mut cx = EditorTestContext::new(cx).await;
12125
12126 cx.language_registry().add(language.clone());
12127 cx.update_buffer(|buffer, cx| {
12128 buffer.set_language(Some(language), cx);
12129 });
12130
12131 let toggle_comments = &ToggleComments {
12132 advance_downwards: true,
12133 ignore_indent: false,
12134 };
12135
12136 // Single cursor on one line -> advance
12137 // Cursor moves horizontally 3 characters as well on non-blank line
12138 cx.set_state(indoc!(
12139 "fn a() {
12140 ˇdog();
12141 cat();
12142 }"
12143 ));
12144 cx.update_editor(|editor, window, cx| {
12145 editor.toggle_comments(toggle_comments, window, cx);
12146 });
12147 cx.assert_editor_state(indoc!(
12148 "fn a() {
12149 // dog();
12150 catˇ();
12151 }"
12152 ));
12153
12154 // Single selection on one line -> don't advance
12155 cx.set_state(indoc!(
12156 "fn a() {
12157 «dog()ˇ»;
12158 cat();
12159 }"
12160 ));
12161 cx.update_editor(|editor, window, cx| {
12162 editor.toggle_comments(toggle_comments, window, cx);
12163 });
12164 cx.assert_editor_state(indoc!(
12165 "fn a() {
12166 // «dog()ˇ»;
12167 cat();
12168 }"
12169 ));
12170
12171 // Multiple cursors on one line -> advance
12172 cx.set_state(indoc!(
12173 "fn a() {
12174 ˇdˇog();
12175 cat();
12176 }"
12177 ));
12178 cx.update_editor(|editor, window, cx| {
12179 editor.toggle_comments(toggle_comments, window, cx);
12180 });
12181 cx.assert_editor_state(indoc!(
12182 "fn a() {
12183 // dog();
12184 catˇ(ˇ);
12185 }"
12186 ));
12187
12188 // Multiple cursors on one line, with selection -> don't advance
12189 cx.set_state(indoc!(
12190 "fn a() {
12191 ˇdˇog«()ˇ»;
12192 cat();
12193 }"
12194 ));
12195 cx.update_editor(|editor, window, cx| {
12196 editor.toggle_comments(toggle_comments, window, cx);
12197 });
12198 cx.assert_editor_state(indoc!(
12199 "fn a() {
12200 // ˇdˇog«()ˇ»;
12201 cat();
12202 }"
12203 ));
12204
12205 // Single cursor on one line -> advance
12206 // Cursor moves to column 0 on blank line
12207 cx.set_state(indoc!(
12208 "fn a() {
12209 ˇdog();
12210
12211 cat();
12212 }"
12213 ));
12214 cx.update_editor(|editor, window, cx| {
12215 editor.toggle_comments(toggle_comments, window, cx);
12216 });
12217 cx.assert_editor_state(indoc!(
12218 "fn a() {
12219 // dog();
12220 ˇ
12221 cat();
12222 }"
12223 ));
12224
12225 // Single cursor on one line -> advance
12226 // Cursor starts and ends at column 0
12227 cx.set_state(indoc!(
12228 "fn a() {
12229 ˇ dog();
12230 cat();
12231 }"
12232 ));
12233 cx.update_editor(|editor, window, cx| {
12234 editor.toggle_comments(toggle_comments, window, cx);
12235 });
12236 cx.assert_editor_state(indoc!(
12237 "fn a() {
12238 // dog();
12239 ˇ cat();
12240 }"
12241 ));
12242}
12243
12244#[gpui::test]
12245async fn test_toggle_block_comment(cx: &mut TestAppContext) {
12246 init_test(cx, |_| {});
12247
12248 let mut cx = EditorTestContext::new(cx).await;
12249
12250 let html_language = Arc::new(
12251 Language::new(
12252 LanguageConfig {
12253 name: "HTML".into(),
12254 block_comment: Some(("<!-- ".into(), " -->".into())),
12255 ..Default::default()
12256 },
12257 Some(tree_sitter_html::LANGUAGE.into()),
12258 )
12259 .with_injection_query(
12260 r#"
12261 (script_element
12262 (raw_text) @injection.content
12263 (#set! injection.language "javascript"))
12264 "#,
12265 )
12266 .unwrap(),
12267 );
12268
12269 let javascript_language = Arc::new(Language::new(
12270 LanguageConfig {
12271 name: "JavaScript".into(),
12272 line_comments: vec!["// ".into()],
12273 ..Default::default()
12274 },
12275 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
12276 ));
12277
12278 cx.language_registry().add(html_language.clone());
12279 cx.language_registry().add(javascript_language.clone());
12280 cx.update_buffer(|buffer, cx| {
12281 buffer.set_language(Some(html_language), cx);
12282 });
12283
12284 // Toggle comments for empty selections
12285 cx.set_state(
12286 &r#"
12287 <p>A</p>ˇ
12288 <p>B</p>ˇ
12289 <p>C</p>ˇ
12290 "#
12291 .unindent(),
12292 );
12293 cx.update_editor(|editor, window, cx| {
12294 editor.toggle_comments(&ToggleComments::default(), window, cx)
12295 });
12296 cx.assert_editor_state(
12297 &r#"
12298 <!-- <p>A</p>ˇ -->
12299 <!-- <p>B</p>ˇ -->
12300 <!-- <p>C</p>ˇ -->
12301 "#
12302 .unindent(),
12303 );
12304 cx.update_editor(|editor, window, cx| {
12305 editor.toggle_comments(&ToggleComments::default(), window, cx)
12306 });
12307 cx.assert_editor_state(
12308 &r#"
12309 <p>A</p>ˇ
12310 <p>B</p>ˇ
12311 <p>C</p>ˇ
12312 "#
12313 .unindent(),
12314 );
12315
12316 // Toggle comments for mixture of empty and non-empty selections, where
12317 // multiple selections occupy a given line.
12318 cx.set_state(
12319 &r#"
12320 <p>A«</p>
12321 <p>ˇ»B</p>ˇ
12322 <p>C«</p>
12323 <p>ˇ»D</p>ˇ
12324 "#
12325 .unindent(),
12326 );
12327
12328 cx.update_editor(|editor, window, cx| {
12329 editor.toggle_comments(&ToggleComments::default(), window, cx)
12330 });
12331 cx.assert_editor_state(
12332 &r#"
12333 <!-- <p>A«</p>
12334 <p>ˇ»B</p>ˇ -->
12335 <!-- <p>C«</p>
12336 <p>ˇ»D</p>ˇ -->
12337 "#
12338 .unindent(),
12339 );
12340 cx.update_editor(|editor, window, cx| {
12341 editor.toggle_comments(&ToggleComments::default(), window, cx)
12342 });
12343 cx.assert_editor_state(
12344 &r#"
12345 <p>A«</p>
12346 <p>ˇ»B</p>ˇ
12347 <p>C«</p>
12348 <p>ˇ»D</p>ˇ
12349 "#
12350 .unindent(),
12351 );
12352
12353 // Toggle comments when different languages are active for different
12354 // selections.
12355 cx.set_state(
12356 &r#"
12357 ˇ<script>
12358 ˇvar x = new Y();
12359 ˇ</script>
12360 "#
12361 .unindent(),
12362 );
12363 cx.executor().run_until_parked();
12364 cx.update_editor(|editor, window, cx| {
12365 editor.toggle_comments(&ToggleComments::default(), window, cx)
12366 });
12367 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
12368 // Uncommenting and commenting from this position brings in even more wrong artifacts.
12369 cx.assert_editor_state(
12370 &r#"
12371 <!-- ˇ<script> -->
12372 // ˇvar x = new Y();
12373 <!-- ˇ</script> -->
12374 "#
12375 .unindent(),
12376 );
12377}
12378
12379#[gpui::test]
12380fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
12381 init_test(cx, |_| {});
12382
12383 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
12384 let multibuffer = cx.new(|cx| {
12385 let mut multibuffer = MultiBuffer::new(ReadWrite);
12386 multibuffer.push_excerpts(
12387 buffer.clone(),
12388 [
12389 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
12390 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
12391 ],
12392 cx,
12393 );
12394 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
12395 multibuffer
12396 });
12397
12398 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
12399 editor.update_in(cx, |editor, window, cx| {
12400 assert_eq!(editor.text(cx), "aaaa\nbbbb");
12401 editor.change_selections(None, window, cx, |s| {
12402 s.select_ranges([
12403 Point::new(0, 0)..Point::new(0, 0),
12404 Point::new(1, 0)..Point::new(1, 0),
12405 ])
12406 });
12407
12408 editor.handle_input("X", window, cx);
12409 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
12410 assert_eq!(
12411 editor.selections.ranges(cx),
12412 [
12413 Point::new(0, 1)..Point::new(0, 1),
12414 Point::new(1, 1)..Point::new(1, 1),
12415 ]
12416 );
12417
12418 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
12419 editor.change_selections(None, window, cx, |s| {
12420 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
12421 });
12422 editor.backspace(&Default::default(), window, cx);
12423 assert_eq!(editor.text(cx), "Xa\nbbb");
12424 assert_eq!(
12425 editor.selections.ranges(cx),
12426 [Point::new(1, 0)..Point::new(1, 0)]
12427 );
12428
12429 editor.change_selections(None, window, cx, |s| {
12430 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
12431 });
12432 editor.backspace(&Default::default(), window, cx);
12433 assert_eq!(editor.text(cx), "X\nbb");
12434 assert_eq!(
12435 editor.selections.ranges(cx),
12436 [Point::new(0, 1)..Point::new(0, 1)]
12437 );
12438 });
12439}
12440
12441#[gpui::test]
12442fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
12443 init_test(cx, |_| {});
12444
12445 let markers = vec![('[', ']').into(), ('(', ')').into()];
12446 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
12447 indoc! {"
12448 [aaaa
12449 (bbbb]
12450 cccc)",
12451 },
12452 markers.clone(),
12453 );
12454 let excerpt_ranges = markers.into_iter().map(|marker| {
12455 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
12456 ExcerptRange::new(context.clone())
12457 });
12458 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
12459 let multibuffer = cx.new(|cx| {
12460 let mut multibuffer = MultiBuffer::new(ReadWrite);
12461 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
12462 multibuffer
12463 });
12464
12465 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
12466 editor.update_in(cx, |editor, window, cx| {
12467 let (expected_text, selection_ranges) = marked_text_ranges(
12468 indoc! {"
12469 aaaa
12470 bˇbbb
12471 bˇbbˇb
12472 cccc"
12473 },
12474 true,
12475 );
12476 assert_eq!(editor.text(cx), expected_text);
12477 editor.change_selections(None, window, cx, |s| s.select_ranges(selection_ranges));
12478
12479 editor.handle_input("X", window, cx);
12480
12481 let (expected_text, expected_selections) = marked_text_ranges(
12482 indoc! {"
12483 aaaa
12484 bXˇbbXb
12485 bXˇbbXˇb
12486 cccc"
12487 },
12488 false,
12489 );
12490 assert_eq!(editor.text(cx), expected_text);
12491 assert_eq!(editor.selections.ranges(cx), expected_selections);
12492
12493 editor.newline(&Newline, window, cx);
12494 let (expected_text, expected_selections) = marked_text_ranges(
12495 indoc! {"
12496 aaaa
12497 bX
12498 ˇbbX
12499 b
12500 bX
12501 ˇbbX
12502 ˇb
12503 cccc"
12504 },
12505 false,
12506 );
12507 assert_eq!(editor.text(cx), expected_text);
12508 assert_eq!(editor.selections.ranges(cx), expected_selections);
12509 });
12510}
12511
12512#[gpui::test]
12513fn test_refresh_selections(cx: &mut TestAppContext) {
12514 init_test(cx, |_| {});
12515
12516 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
12517 let mut excerpt1_id = None;
12518 let multibuffer = cx.new(|cx| {
12519 let mut multibuffer = MultiBuffer::new(ReadWrite);
12520 excerpt1_id = multibuffer
12521 .push_excerpts(
12522 buffer.clone(),
12523 [
12524 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
12525 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
12526 ],
12527 cx,
12528 )
12529 .into_iter()
12530 .next();
12531 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
12532 multibuffer
12533 });
12534
12535 let editor = cx.add_window(|window, cx| {
12536 let mut editor = build_editor(multibuffer.clone(), window, cx);
12537 let snapshot = editor.snapshot(window, cx);
12538 editor.change_selections(None, window, cx, |s| {
12539 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
12540 });
12541 editor.begin_selection(
12542 Point::new(2, 1).to_display_point(&snapshot),
12543 true,
12544 1,
12545 window,
12546 cx,
12547 );
12548 assert_eq!(
12549 editor.selections.ranges(cx),
12550 [
12551 Point::new(1, 3)..Point::new(1, 3),
12552 Point::new(2, 1)..Point::new(2, 1),
12553 ]
12554 );
12555 editor
12556 });
12557
12558 // Refreshing selections is a no-op when excerpts haven't changed.
12559 _ = editor.update(cx, |editor, window, cx| {
12560 editor.change_selections(None, window, cx, |s| s.refresh());
12561 assert_eq!(
12562 editor.selections.ranges(cx),
12563 [
12564 Point::new(1, 3)..Point::new(1, 3),
12565 Point::new(2, 1)..Point::new(2, 1),
12566 ]
12567 );
12568 });
12569
12570 multibuffer.update(cx, |multibuffer, cx| {
12571 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
12572 });
12573 _ = editor.update(cx, |editor, window, cx| {
12574 // Removing an excerpt causes the first selection to become degenerate.
12575 assert_eq!(
12576 editor.selections.ranges(cx),
12577 [
12578 Point::new(0, 0)..Point::new(0, 0),
12579 Point::new(0, 1)..Point::new(0, 1)
12580 ]
12581 );
12582
12583 // Refreshing selections will relocate the first selection to the original buffer
12584 // location.
12585 editor.change_selections(None, window, cx, |s| s.refresh());
12586 assert_eq!(
12587 editor.selections.ranges(cx),
12588 [
12589 Point::new(0, 1)..Point::new(0, 1),
12590 Point::new(0, 3)..Point::new(0, 3)
12591 ]
12592 );
12593 assert!(editor.selections.pending_anchor().is_some());
12594 });
12595}
12596
12597#[gpui::test]
12598fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
12599 init_test(cx, |_| {});
12600
12601 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
12602 let mut excerpt1_id = None;
12603 let multibuffer = cx.new(|cx| {
12604 let mut multibuffer = MultiBuffer::new(ReadWrite);
12605 excerpt1_id = multibuffer
12606 .push_excerpts(
12607 buffer.clone(),
12608 [
12609 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
12610 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
12611 ],
12612 cx,
12613 )
12614 .into_iter()
12615 .next();
12616 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
12617 multibuffer
12618 });
12619
12620 let editor = cx.add_window(|window, cx| {
12621 let mut editor = build_editor(multibuffer.clone(), window, cx);
12622 let snapshot = editor.snapshot(window, cx);
12623 editor.begin_selection(
12624 Point::new(1, 3).to_display_point(&snapshot),
12625 false,
12626 1,
12627 window,
12628 cx,
12629 );
12630 assert_eq!(
12631 editor.selections.ranges(cx),
12632 [Point::new(1, 3)..Point::new(1, 3)]
12633 );
12634 editor
12635 });
12636
12637 multibuffer.update(cx, |multibuffer, cx| {
12638 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
12639 });
12640 _ = editor.update(cx, |editor, window, cx| {
12641 assert_eq!(
12642 editor.selections.ranges(cx),
12643 [Point::new(0, 0)..Point::new(0, 0)]
12644 );
12645
12646 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
12647 editor.change_selections(None, window, cx, |s| s.refresh());
12648 assert_eq!(
12649 editor.selections.ranges(cx),
12650 [Point::new(0, 3)..Point::new(0, 3)]
12651 );
12652 assert!(editor.selections.pending_anchor().is_some());
12653 });
12654}
12655
12656#[gpui::test]
12657async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
12658 init_test(cx, |_| {});
12659
12660 let language = Arc::new(
12661 Language::new(
12662 LanguageConfig {
12663 brackets: BracketPairConfig {
12664 pairs: vec![
12665 BracketPair {
12666 start: "{".to_string(),
12667 end: "}".to_string(),
12668 close: true,
12669 surround: true,
12670 newline: true,
12671 },
12672 BracketPair {
12673 start: "/* ".to_string(),
12674 end: " */".to_string(),
12675 close: true,
12676 surround: true,
12677 newline: true,
12678 },
12679 ],
12680 ..Default::default()
12681 },
12682 ..Default::default()
12683 },
12684 Some(tree_sitter_rust::LANGUAGE.into()),
12685 )
12686 .with_indents_query("")
12687 .unwrap(),
12688 );
12689
12690 let text = concat!(
12691 "{ }\n", //
12692 " x\n", //
12693 " /* */\n", //
12694 "x\n", //
12695 "{{} }\n", //
12696 );
12697
12698 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
12699 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12700 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
12701 editor
12702 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
12703 .await;
12704
12705 editor.update_in(cx, |editor, window, cx| {
12706 editor.change_selections(None, window, cx, |s| {
12707 s.select_display_ranges([
12708 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
12709 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
12710 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
12711 ])
12712 });
12713 editor.newline(&Newline, window, cx);
12714
12715 assert_eq!(
12716 editor.buffer().read(cx).read(cx).text(),
12717 concat!(
12718 "{ \n", // Suppress rustfmt
12719 "\n", //
12720 "}\n", //
12721 " x\n", //
12722 " /* \n", //
12723 " \n", //
12724 " */\n", //
12725 "x\n", //
12726 "{{} \n", //
12727 "}\n", //
12728 )
12729 );
12730 });
12731}
12732
12733#[gpui::test]
12734fn test_highlighted_ranges(cx: &mut TestAppContext) {
12735 init_test(cx, |_| {});
12736
12737 let editor = cx.add_window(|window, cx| {
12738 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
12739 build_editor(buffer.clone(), window, cx)
12740 });
12741
12742 _ = editor.update(cx, |editor, window, cx| {
12743 struct Type1;
12744 struct Type2;
12745
12746 let buffer = editor.buffer.read(cx).snapshot(cx);
12747
12748 let anchor_range =
12749 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
12750
12751 editor.highlight_background::<Type1>(
12752 &[
12753 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
12754 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
12755 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
12756 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
12757 ],
12758 |_| Hsla::red(),
12759 cx,
12760 );
12761 editor.highlight_background::<Type2>(
12762 &[
12763 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
12764 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
12765 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
12766 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
12767 ],
12768 |_| Hsla::green(),
12769 cx,
12770 );
12771
12772 let snapshot = editor.snapshot(window, cx);
12773 let mut highlighted_ranges = editor.background_highlights_in_range(
12774 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
12775 &snapshot,
12776 cx.theme().colors(),
12777 );
12778 // Enforce a consistent ordering based on color without relying on the ordering of the
12779 // highlight's `TypeId` which is non-executor.
12780 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
12781 assert_eq!(
12782 highlighted_ranges,
12783 &[
12784 (
12785 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
12786 Hsla::red(),
12787 ),
12788 (
12789 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
12790 Hsla::red(),
12791 ),
12792 (
12793 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
12794 Hsla::green(),
12795 ),
12796 (
12797 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
12798 Hsla::green(),
12799 ),
12800 ]
12801 );
12802 assert_eq!(
12803 editor.background_highlights_in_range(
12804 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
12805 &snapshot,
12806 cx.theme().colors(),
12807 ),
12808 &[(
12809 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
12810 Hsla::red(),
12811 )]
12812 );
12813 });
12814}
12815
12816#[gpui::test]
12817async fn test_following(cx: &mut TestAppContext) {
12818 init_test(cx, |_| {});
12819
12820 let fs = FakeFs::new(cx.executor());
12821 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
12822
12823 let buffer = project.update(cx, |project, cx| {
12824 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
12825 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
12826 });
12827 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
12828 let follower = cx.update(|cx| {
12829 cx.open_window(
12830 WindowOptions {
12831 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
12832 gpui::Point::new(px(0.), px(0.)),
12833 gpui::Point::new(px(10.), px(80.)),
12834 ))),
12835 ..Default::default()
12836 },
12837 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
12838 )
12839 .unwrap()
12840 });
12841
12842 let is_still_following = Rc::new(RefCell::new(true));
12843 let follower_edit_event_count = Rc::new(RefCell::new(0));
12844 let pending_update = Rc::new(RefCell::new(None));
12845 let leader_entity = leader.root(cx).unwrap();
12846 let follower_entity = follower.root(cx).unwrap();
12847 _ = follower.update(cx, {
12848 let update = pending_update.clone();
12849 let is_still_following = is_still_following.clone();
12850 let follower_edit_event_count = follower_edit_event_count.clone();
12851 |_, window, cx| {
12852 cx.subscribe_in(
12853 &leader_entity,
12854 window,
12855 move |_, leader, event, window, cx| {
12856 leader.read(cx).add_event_to_update_proto(
12857 event,
12858 &mut update.borrow_mut(),
12859 window,
12860 cx,
12861 );
12862 },
12863 )
12864 .detach();
12865
12866 cx.subscribe_in(
12867 &follower_entity,
12868 window,
12869 move |_, _, event: &EditorEvent, _window, _cx| {
12870 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
12871 *is_still_following.borrow_mut() = false;
12872 }
12873
12874 if let EditorEvent::BufferEdited = event {
12875 *follower_edit_event_count.borrow_mut() += 1;
12876 }
12877 },
12878 )
12879 .detach();
12880 }
12881 });
12882
12883 // Update the selections only
12884 _ = leader.update(cx, |leader, window, cx| {
12885 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
12886 });
12887 follower
12888 .update(cx, |follower, window, cx| {
12889 follower.apply_update_proto(
12890 &project,
12891 pending_update.borrow_mut().take().unwrap(),
12892 window,
12893 cx,
12894 )
12895 })
12896 .unwrap()
12897 .await
12898 .unwrap();
12899 _ = follower.update(cx, |follower, _, cx| {
12900 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
12901 });
12902 assert!(*is_still_following.borrow());
12903 assert_eq!(*follower_edit_event_count.borrow(), 0);
12904
12905 // Update the scroll position only
12906 _ = leader.update(cx, |leader, window, cx| {
12907 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
12908 });
12909 follower
12910 .update(cx, |follower, window, cx| {
12911 follower.apply_update_proto(
12912 &project,
12913 pending_update.borrow_mut().take().unwrap(),
12914 window,
12915 cx,
12916 )
12917 })
12918 .unwrap()
12919 .await
12920 .unwrap();
12921 assert_eq!(
12922 follower
12923 .update(cx, |follower, _, cx| follower.scroll_position(cx))
12924 .unwrap(),
12925 gpui::Point::new(1.5, 3.5)
12926 );
12927 assert!(*is_still_following.borrow());
12928 assert_eq!(*follower_edit_event_count.borrow(), 0);
12929
12930 // Update the selections and scroll position. The follower's scroll position is updated
12931 // via autoscroll, not via the leader's exact scroll position.
12932 _ = leader.update(cx, |leader, window, cx| {
12933 leader.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
12934 leader.request_autoscroll(Autoscroll::newest(), cx);
12935 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
12936 });
12937 follower
12938 .update(cx, |follower, window, cx| {
12939 follower.apply_update_proto(
12940 &project,
12941 pending_update.borrow_mut().take().unwrap(),
12942 window,
12943 cx,
12944 )
12945 })
12946 .unwrap()
12947 .await
12948 .unwrap();
12949 _ = follower.update(cx, |follower, _, cx| {
12950 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
12951 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
12952 });
12953 assert!(*is_still_following.borrow());
12954
12955 // Creating a pending selection that precedes another selection
12956 _ = leader.update(cx, |leader, window, cx| {
12957 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
12958 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
12959 });
12960 follower
12961 .update(cx, |follower, window, cx| {
12962 follower.apply_update_proto(
12963 &project,
12964 pending_update.borrow_mut().take().unwrap(),
12965 window,
12966 cx,
12967 )
12968 })
12969 .unwrap()
12970 .await
12971 .unwrap();
12972 _ = follower.update(cx, |follower, _, cx| {
12973 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
12974 });
12975 assert!(*is_still_following.borrow());
12976
12977 // Extend the pending selection so that it surrounds another selection
12978 _ = leader.update(cx, |leader, window, cx| {
12979 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
12980 });
12981 follower
12982 .update(cx, |follower, window, cx| {
12983 follower.apply_update_proto(
12984 &project,
12985 pending_update.borrow_mut().take().unwrap(),
12986 window,
12987 cx,
12988 )
12989 })
12990 .unwrap()
12991 .await
12992 .unwrap();
12993 _ = follower.update(cx, |follower, _, cx| {
12994 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
12995 });
12996
12997 // Scrolling locally breaks the follow
12998 _ = follower.update(cx, |follower, window, cx| {
12999 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
13000 follower.set_scroll_anchor(
13001 ScrollAnchor {
13002 anchor: top_anchor,
13003 offset: gpui::Point::new(0.0, 0.5),
13004 },
13005 window,
13006 cx,
13007 );
13008 });
13009 assert!(!(*is_still_following.borrow()));
13010}
13011
13012#[gpui::test]
13013async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
13014 init_test(cx, |_| {});
13015
13016 let fs = FakeFs::new(cx.executor());
13017 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
13018 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13019 let pane = workspace
13020 .update(cx, |workspace, _, _| workspace.active_pane().clone())
13021 .unwrap();
13022
13023 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
13024
13025 let leader = pane.update_in(cx, |_, window, cx| {
13026 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
13027 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
13028 });
13029
13030 // Start following the editor when it has no excerpts.
13031 let mut state_message =
13032 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
13033 let workspace_entity = workspace.root(cx).unwrap();
13034 let follower_1 = cx
13035 .update_window(*workspace.deref(), |_, window, cx| {
13036 Editor::from_state_proto(
13037 workspace_entity,
13038 ViewId {
13039 creator: CollaboratorId::PeerId(PeerId::default()),
13040 id: 0,
13041 },
13042 &mut state_message,
13043 window,
13044 cx,
13045 )
13046 })
13047 .unwrap()
13048 .unwrap()
13049 .await
13050 .unwrap();
13051
13052 let update_message = Rc::new(RefCell::new(None));
13053 follower_1.update_in(cx, {
13054 let update = update_message.clone();
13055 |_, window, cx| {
13056 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
13057 leader.read(cx).add_event_to_update_proto(
13058 event,
13059 &mut update.borrow_mut(),
13060 window,
13061 cx,
13062 );
13063 })
13064 .detach();
13065 }
13066 });
13067
13068 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
13069 (
13070 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
13071 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
13072 )
13073 });
13074
13075 // Insert some excerpts.
13076 leader.update(cx, |leader, cx| {
13077 leader.buffer.update(cx, |multibuffer, cx| {
13078 multibuffer.set_excerpts_for_path(
13079 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
13080 buffer_1.clone(),
13081 vec![
13082 Point::row_range(0..3),
13083 Point::row_range(1..6),
13084 Point::row_range(12..15),
13085 ],
13086 0,
13087 cx,
13088 );
13089 multibuffer.set_excerpts_for_path(
13090 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
13091 buffer_2.clone(),
13092 vec![Point::row_range(0..6), Point::row_range(8..12)],
13093 0,
13094 cx,
13095 );
13096 });
13097 });
13098
13099 // Apply the update of adding the excerpts.
13100 follower_1
13101 .update_in(cx, |follower, window, cx| {
13102 follower.apply_update_proto(
13103 &project,
13104 update_message.borrow().clone().unwrap(),
13105 window,
13106 cx,
13107 )
13108 })
13109 .await
13110 .unwrap();
13111 assert_eq!(
13112 follower_1.update(cx, |editor, cx| editor.text(cx)),
13113 leader.update(cx, |editor, cx| editor.text(cx))
13114 );
13115 update_message.borrow_mut().take();
13116
13117 // Start following separately after it already has excerpts.
13118 let mut state_message =
13119 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
13120 let workspace_entity = workspace.root(cx).unwrap();
13121 let follower_2 = cx
13122 .update_window(*workspace.deref(), |_, window, cx| {
13123 Editor::from_state_proto(
13124 workspace_entity,
13125 ViewId {
13126 creator: CollaboratorId::PeerId(PeerId::default()),
13127 id: 0,
13128 },
13129 &mut state_message,
13130 window,
13131 cx,
13132 )
13133 })
13134 .unwrap()
13135 .unwrap()
13136 .await
13137 .unwrap();
13138 assert_eq!(
13139 follower_2.update(cx, |editor, cx| editor.text(cx)),
13140 leader.update(cx, |editor, cx| editor.text(cx))
13141 );
13142
13143 // Remove some excerpts.
13144 leader.update(cx, |leader, cx| {
13145 leader.buffer.update(cx, |multibuffer, cx| {
13146 let excerpt_ids = multibuffer.excerpt_ids();
13147 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
13148 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
13149 });
13150 });
13151
13152 // Apply the update of removing the excerpts.
13153 follower_1
13154 .update_in(cx, |follower, window, cx| {
13155 follower.apply_update_proto(
13156 &project,
13157 update_message.borrow().clone().unwrap(),
13158 window,
13159 cx,
13160 )
13161 })
13162 .await
13163 .unwrap();
13164 follower_2
13165 .update_in(cx, |follower, window, cx| {
13166 follower.apply_update_proto(
13167 &project,
13168 update_message.borrow().clone().unwrap(),
13169 window,
13170 cx,
13171 )
13172 })
13173 .await
13174 .unwrap();
13175 update_message.borrow_mut().take();
13176 assert_eq!(
13177 follower_1.update(cx, |editor, cx| editor.text(cx)),
13178 leader.update(cx, |editor, cx| editor.text(cx))
13179 );
13180}
13181
13182#[gpui::test]
13183async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
13184 init_test(cx, |_| {});
13185
13186 let mut cx = EditorTestContext::new(cx).await;
13187 let lsp_store =
13188 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
13189
13190 cx.set_state(indoc! {"
13191 ˇfn func(abc def: i32) -> u32 {
13192 }
13193 "});
13194
13195 cx.update(|_, cx| {
13196 lsp_store.update(cx, |lsp_store, cx| {
13197 lsp_store
13198 .update_diagnostics(
13199 LanguageServerId(0),
13200 lsp::PublishDiagnosticsParams {
13201 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
13202 version: None,
13203 diagnostics: vec![
13204 lsp::Diagnostic {
13205 range: lsp::Range::new(
13206 lsp::Position::new(0, 11),
13207 lsp::Position::new(0, 12),
13208 ),
13209 severity: Some(lsp::DiagnosticSeverity::ERROR),
13210 ..Default::default()
13211 },
13212 lsp::Diagnostic {
13213 range: lsp::Range::new(
13214 lsp::Position::new(0, 12),
13215 lsp::Position::new(0, 15),
13216 ),
13217 severity: Some(lsp::DiagnosticSeverity::ERROR),
13218 ..Default::default()
13219 },
13220 lsp::Diagnostic {
13221 range: lsp::Range::new(
13222 lsp::Position::new(0, 25),
13223 lsp::Position::new(0, 28),
13224 ),
13225 severity: Some(lsp::DiagnosticSeverity::ERROR),
13226 ..Default::default()
13227 },
13228 ],
13229 },
13230 &[],
13231 cx,
13232 )
13233 .unwrap()
13234 });
13235 });
13236
13237 executor.run_until_parked();
13238
13239 cx.update_editor(|editor, window, cx| {
13240 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13241 });
13242
13243 cx.assert_editor_state(indoc! {"
13244 fn func(abc def: i32) -> ˇu32 {
13245 }
13246 "});
13247
13248 cx.update_editor(|editor, window, cx| {
13249 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13250 });
13251
13252 cx.assert_editor_state(indoc! {"
13253 fn func(abc ˇdef: i32) -> u32 {
13254 }
13255 "});
13256
13257 cx.update_editor(|editor, window, cx| {
13258 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13259 });
13260
13261 cx.assert_editor_state(indoc! {"
13262 fn func(abcˇ def: i32) -> u32 {
13263 }
13264 "});
13265
13266 cx.update_editor(|editor, window, cx| {
13267 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13268 });
13269
13270 cx.assert_editor_state(indoc! {"
13271 fn func(abc def: i32) -> ˇu32 {
13272 }
13273 "});
13274}
13275
13276#[gpui::test]
13277async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
13278 init_test(cx, |_| {});
13279
13280 let mut cx = EditorTestContext::new(cx).await;
13281
13282 let diff_base = r#"
13283 use some::mod;
13284
13285 const A: u32 = 42;
13286
13287 fn main() {
13288 println!("hello");
13289
13290 println!("world");
13291 }
13292 "#
13293 .unindent();
13294
13295 // Edits are modified, removed, modified, added
13296 cx.set_state(
13297 &r#"
13298 use some::modified;
13299
13300 ˇ
13301 fn main() {
13302 println!("hello there");
13303
13304 println!("around the");
13305 println!("world");
13306 }
13307 "#
13308 .unindent(),
13309 );
13310
13311 cx.set_head_text(&diff_base);
13312 executor.run_until_parked();
13313
13314 cx.update_editor(|editor, window, cx| {
13315 //Wrap around the bottom of the buffer
13316 for _ in 0..3 {
13317 editor.go_to_next_hunk(&GoToHunk, window, cx);
13318 }
13319 });
13320
13321 cx.assert_editor_state(
13322 &r#"
13323 ˇuse some::modified;
13324
13325
13326 fn main() {
13327 println!("hello there");
13328
13329 println!("around the");
13330 println!("world");
13331 }
13332 "#
13333 .unindent(),
13334 );
13335
13336 cx.update_editor(|editor, window, cx| {
13337 //Wrap around the top of the buffer
13338 for _ in 0..2 {
13339 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13340 }
13341 });
13342
13343 cx.assert_editor_state(
13344 &r#"
13345 use some::modified;
13346
13347
13348 fn main() {
13349 ˇ println!("hello there");
13350
13351 println!("around the");
13352 println!("world");
13353 }
13354 "#
13355 .unindent(),
13356 );
13357
13358 cx.update_editor(|editor, window, cx| {
13359 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13360 });
13361
13362 cx.assert_editor_state(
13363 &r#"
13364 use some::modified;
13365
13366 ˇ
13367 fn main() {
13368 println!("hello there");
13369
13370 println!("around the");
13371 println!("world");
13372 }
13373 "#
13374 .unindent(),
13375 );
13376
13377 cx.update_editor(|editor, window, cx| {
13378 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13379 });
13380
13381 cx.assert_editor_state(
13382 &r#"
13383 ˇuse some::modified;
13384
13385
13386 fn main() {
13387 println!("hello there");
13388
13389 println!("around the");
13390 println!("world");
13391 }
13392 "#
13393 .unindent(),
13394 );
13395
13396 cx.update_editor(|editor, window, cx| {
13397 for _ in 0..2 {
13398 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13399 }
13400 });
13401
13402 cx.assert_editor_state(
13403 &r#"
13404 use some::modified;
13405
13406
13407 fn main() {
13408 ˇ println!("hello there");
13409
13410 println!("around the");
13411 println!("world");
13412 }
13413 "#
13414 .unindent(),
13415 );
13416
13417 cx.update_editor(|editor, window, cx| {
13418 editor.fold(&Fold, window, cx);
13419 });
13420
13421 cx.update_editor(|editor, window, cx| {
13422 editor.go_to_next_hunk(&GoToHunk, window, cx);
13423 });
13424
13425 cx.assert_editor_state(
13426 &r#"
13427 ˇuse some::modified;
13428
13429
13430 fn main() {
13431 println!("hello there");
13432
13433 println!("around the");
13434 println!("world");
13435 }
13436 "#
13437 .unindent(),
13438 );
13439}
13440
13441#[test]
13442fn test_split_words() {
13443 fn split(text: &str) -> Vec<&str> {
13444 split_words(text).collect()
13445 }
13446
13447 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
13448 assert_eq!(split("hello_world"), &["hello_", "world"]);
13449 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
13450 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
13451 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
13452 assert_eq!(split("helloworld"), &["helloworld"]);
13453
13454 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
13455}
13456
13457#[gpui::test]
13458async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
13459 init_test(cx, |_| {});
13460
13461 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
13462 let mut assert = |before, after| {
13463 let _state_context = cx.set_state(before);
13464 cx.run_until_parked();
13465 cx.update_editor(|editor, window, cx| {
13466 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
13467 });
13468 cx.run_until_parked();
13469 cx.assert_editor_state(after);
13470 };
13471
13472 // Outside bracket jumps to outside of matching bracket
13473 assert("console.logˇ(var);", "console.log(var)ˇ;");
13474 assert("console.log(var)ˇ;", "console.logˇ(var);");
13475
13476 // Inside bracket jumps to inside of matching bracket
13477 assert("console.log(ˇvar);", "console.log(varˇ);");
13478 assert("console.log(varˇ);", "console.log(ˇvar);");
13479
13480 // When outside a bracket and inside, favor jumping to the inside bracket
13481 assert(
13482 "console.log('foo', [1, 2, 3]ˇ);",
13483 "console.log(ˇ'foo', [1, 2, 3]);",
13484 );
13485 assert(
13486 "console.log(ˇ'foo', [1, 2, 3]);",
13487 "console.log('foo', [1, 2, 3]ˇ);",
13488 );
13489
13490 // Bias forward if two options are equally likely
13491 assert(
13492 "let result = curried_fun()ˇ();",
13493 "let result = curried_fun()()ˇ;",
13494 );
13495
13496 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
13497 assert(
13498 indoc! {"
13499 function test() {
13500 console.log('test')ˇ
13501 }"},
13502 indoc! {"
13503 function test() {
13504 console.logˇ('test')
13505 }"},
13506 );
13507}
13508
13509#[gpui::test]
13510async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
13511 init_test(cx, |_| {});
13512
13513 let fs = FakeFs::new(cx.executor());
13514 fs.insert_tree(
13515 path!("/a"),
13516 json!({
13517 "main.rs": "fn main() { let a = 5; }",
13518 "other.rs": "// Test file",
13519 }),
13520 )
13521 .await;
13522 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13523
13524 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13525 language_registry.add(Arc::new(Language::new(
13526 LanguageConfig {
13527 name: "Rust".into(),
13528 matcher: LanguageMatcher {
13529 path_suffixes: vec!["rs".to_string()],
13530 ..Default::default()
13531 },
13532 brackets: BracketPairConfig {
13533 pairs: vec![BracketPair {
13534 start: "{".to_string(),
13535 end: "}".to_string(),
13536 close: true,
13537 surround: true,
13538 newline: true,
13539 }],
13540 disabled_scopes_by_bracket_ix: Vec::new(),
13541 },
13542 ..Default::default()
13543 },
13544 Some(tree_sitter_rust::LANGUAGE.into()),
13545 )));
13546 let mut fake_servers = language_registry.register_fake_lsp(
13547 "Rust",
13548 FakeLspAdapter {
13549 capabilities: lsp::ServerCapabilities {
13550 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
13551 first_trigger_character: "{".to_string(),
13552 more_trigger_character: None,
13553 }),
13554 ..Default::default()
13555 },
13556 ..Default::default()
13557 },
13558 );
13559
13560 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13561
13562 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13563
13564 let worktree_id = workspace
13565 .update(cx, |workspace, _, cx| {
13566 workspace.project().update(cx, |project, cx| {
13567 project.worktrees(cx).next().unwrap().read(cx).id()
13568 })
13569 })
13570 .unwrap();
13571
13572 let buffer = project
13573 .update(cx, |project, cx| {
13574 project.open_local_buffer(path!("/a/main.rs"), cx)
13575 })
13576 .await
13577 .unwrap();
13578 let editor_handle = workspace
13579 .update(cx, |workspace, window, cx| {
13580 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
13581 })
13582 .unwrap()
13583 .await
13584 .unwrap()
13585 .downcast::<Editor>()
13586 .unwrap();
13587
13588 cx.executor().start_waiting();
13589 let fake_server = fake_servers.next().await.unwrap();
13590
13591 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
13592 |params, _| async move {
13593 assert_eq!(
13594 params.text_document_position.text_document.uri,
13595 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
13596 );
13597 assert_eq!(
13598 params.text_document_position.position,
13599 lsp::Position::new(0, 21),
13600 );
13601
13602 Ok(Some(vec![lsp::TextEdit {
13603 new_text: "]".to_string(),
13604 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13605 }]))
13606 },
13607 );
13608
13609 editor_handle.update_in(cx, |editor, window, cx| {
13610 window.focus(&editor.focus_handle(cx));
13611 editor.change_selections(None, window, cx, |s| {
13612 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
13613 });
13614 editor.handle_input("{", window, cx);
13615 });
13616
13617 cx.executor().run_until_parked();
13618
13619 buffer.update(cx, |buffer, _| {
13620 assert_eq!(
13621 buffer.text(),
13622 "fn main() { let a = {5}; }",
13623 "No extra braces from on type formatting should appear in the buffer"
13624 )
13625 });
13626}
13627
13628#[gpui::test]
13629async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
13630 init_test(cx, |_| {});
13631
13632 let fs = FakeFs::new(cx.executor());
13633 fs.insert_tree(
13634 path!("/a"),
13635 json!({
13636 "main.rs": "fn main() { let a = 5; }",
13637 "other.rs": "// Test file",
13638 }),
13639 )
13640 .await;
13641
13642 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13643
13644 let server_restarts = Arc::new(AtomicUsize::new(0));
13645 let closure_restarts = Arc::clone(&server_restarts);
13646 let language_server_name = "test language server";
13647 let language_name: LanguageName = "Rust".into();
13648
13649 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13650 language_registry.add(Arc::new(Language::new(
13651 LanguageConfig {
13652 name: language_name.clone(),
13653 matcher: LanguageMatcher {
13654 path_suffixes: vec!["rs".to_string()],
13655 ..Default::default()
13656 },
13657 ..Default::default()
13658 },
13659 Some(tree_sitter_rust::LANGUAGE.into()),
13660 )));
13661 let mut fake_servers = language_registry.register_fake_lsp(
13662 "Rust",
13663 FakeLspAdapter {
13664 name: language_server_name,
13665 initialization_options: Some(json!({
13666 "testOptionValue": true
13667 })),
13668 initializer: Some(Box::new(move |fake_server| {
13669 let task_restarts = Arc::clone(&closure_restarts);
13670 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
13671 task_restarts.fetch_add(1, atomic::Ordering::Release);
13672 futures::future::ready(Ok(()))
13673 });
13674 })),
13675 ..Default::default()
13676 },
13677 );
13678
13679 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13680 let _buffer = project
13681 .update(cx, |project, cx| {
13682 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
13683 })
13684 .await
13685 .unwrap();
13686 let _fake_server = fake_servers.next().await.unwrap();
13687 update_test_language_settings(cx, |language_settings| {
13688 language_settings.languages.insert(
13689 language_name.clone(),
13690 LanguageSettingsContent {
13691 tab_size: NonZeroU32::new(8),
13692 ..Default::default()
13693 },
13694 );
13695 });
13696 cx.executor().run_until_parked();
13697 assert_eq!(
13698 server_restarts.load(atomic::Ordering::Acquire),
13699 0,
13700 "Should not restart LSP server on an unrelated change"
13701 );
13702
13703 update_test_project_settings(cx, |project_settings| {
13704 project_settings.lsp.insert(
13705 "Some other server name".into(),
13706 LspSettings {
13707 binary: None,
13708 settings: None,
13709 initialization_options: Some(json!({
13710 "some other init value": false
13711 })),
13712 enable_lsp_tasks: false,
13713 },
13714 );
13715 });
13716 cx.executor().run_until_parked();
13717 assert_eq!(
13718 server_restarts.load(atomic::Ordering::Acquire),
13719 0,
13720 "Should not restart LSP server on an unrelated LSP settings change"
13721 );
13722
13723 update_test_project_settings(cx, |project_settings| {
13724 project_settings.lsp.insert(
13725 language_server_name.into(),
13726 LspSettings {
13727 binary: None,
13728 settings: None,
13729 initialization_options: Some(json!({
13730 "anotherInitValue": false
13731 })),
13732 enable_lsp_tasks: false,
13733 },
13734 );
13735 });
13736 cx.executor().run_until_parked();
13737 assert_eq!(
13738 server_restarts.load(atomic::Ordering::Acquire),
13739 1,
13740 "Should restart LSP server on a related LSP settings change"
13741 );
13742
13743 update_test_project_settings(cx, |project_settings| {
13744 project_settings.lsp.insert(
13745 language_server_name.into(),
13746 LspSettings {
13747 binary: None,
13748 settings: None,
13749 initialization_options: Some(json!({
13750 "anotherInitValue": false
13751 })),
13752 enable_lsp_tasks: false,
13753 },
13754 );
13755 });
13756 cx.executor().run_until_parked();
13757 assert_eq!(
13758 server_restarts.load(atomic::Ordering::Acquire),
13759 1,
13760 "Should not restart LSP server on a related LSP settings change that is the same"
13761 );
13762
13763 update_test_project_settings(cx, |project_settings| {
13764 project_settings.lsp.insert(
13765 language_server_name.into(),
13766 LspSettings {
13767 binary: None,
13768 settings: None,
13769 initialization_options: None,
13770 enable_lsp_tasks: false,
13771 },
13772 );
13773 });
13774 cx.executor().run_until_parked();
13775 assert_eq!(
13776 server_restarts.load(atomic::Ordering::Acquire),
13777 2,
13778 "Should restart LSP server on another related LSP settings change"
13779 );
13780}
13781
13782#[gpui::test]
13783async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
13784 init_test(cx, |_| {});
13785
13786 let mut cx = EditorLspTestContext::new_rust(
13787 lsp::ServerCapabilities {
13788 completion_provider: Some(lsp::CompletionOptions {
13789 trigger_characters: Some(vec![".".to_string()]),
13790 resolve_provider: Some(true),
13791 ..Default::default()
13792 }),
13793 ..Default::default()
13794 },
13795 cx,
13796 )
13797 .await;
13798
13799 cx.set_state("fn main() { let a = 2ˇ; }");
13800 cx.simulate_keystroke(".");
13801 let completion_item = lsp::CompletionItem {
13802 label: "some".into(),
13803 kind: Some(lsp::CompletionItemKind::SNIPPET),
13804 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
13805 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
13806 kind: lsp::MarkupKind::Markdown,
13807 value: "```rust\nSome(2)\n```".to_string(),
13808 })),
13809 deprecated: Some(false),
13810 sort_text: Some("fffffff2".to_string()),
13811 filter_text: Some("some".to_string()),
13812 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
13813 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13814 range: lsp::Range {
13815 start: lsp::Position {
13816 line: 0,
13817 character: 22,
13818 },
13819 end: lsp::Position {
13820 line: 0,
13821 character: 22,
13822 },
13823 },
13824 new_text: "Some(2)".to_string(),
13825 })),
13826 additional_text_edits: Some(vec![lsp::TextEdit {
13827 range: lsp::Range {
13828 start: lsp::Position {
13829 line: 0,
13830 character: 20,
13831 },
13832 end: lsp::Position {
13833 line: 0,
13834 character: 22,
13835 },
13836 },
13837 new_text: "".to_string(),
13838 }]),
13839 ..Default::default()
13840 };
13841
13842 let closure_completion_item = completion_item.clone();
13843 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13844 let task_completion_item = closure_completion_item.clone();
13845 async move {
13846 Ok(Some(lsp::CompletionResponse::Array(vec![
13847 task_completion_item,
13848 ])))
13849 }
13850 });
13851
13852 request.next().await;
13853
13854 cx.condition(|editor, _| editor.context_menu_visible())
13855 .await;
13856 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13857 editor
13858 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13859 .unwrap()
13860 });
13861 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
13862
13863 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
13864 let task_completion_item = completion_item.clone();
13865 async move { Ok(task_completion_item) }
13866 })
13867 .next()
13868 .await
13869 .unwrap();
13870 apply_additional_edits.await.unwrap();
13871 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
13872}
13873
13874#[gpui::test]
13875async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
13876 init_test(cx, |_| {});
13877
13878 let mut cx = EditorLspTestContext::new_rust(
13879 lsp::ServerCapabilities {
13880 completion_provider: Some(lsp::CompletionOptions {
13881 trigger_characters: Some(vec![".".to_string()]),
13882 resolve_provider: Some(true),
13883 ..Default::default()
13884 }),
13885 ..Default::default()
13886 },
13887 cx,
13888 )
13889 .await;
13890
13891 cx.set_state("fn main() { let a = 2ˇ; }");
13892 cx.simulate_keystroke(".");
13893
13894 let item1 = lsp::CompletionItem {
13895 label: "method id()".to_string(),
13896 filter_text: Some("id".to_string()),
13897 detail: None,
13898 documentation: None,
13899 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13900 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13901 new_text: ".id".to_string(),
13902 })),
13903 ..lsp::CompletionItem::default()
13904 };
13905
13906 let item2 = lsp::CompletionItem {
13907 label: "other".to_string(),
13908 filter_text: Some("other".to_string()),
13909 detail: None,
13910 documentation: None,
13911 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13912 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13913 new_text: ".other".to_string(),
13914 })),
13915 ..lsp::CompletionItem::default()
13916 };
13917
13918 let item1 = item1.clone();
13919 cx.set_request_handler::<lsp::request::Completion, _, _>({
13920 let item1 = item1.clone();
13921 move |_, _, _| {
13922 let item1 = item1.clone();
13923 let item2 = item2.clone();
13924 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
13925 }
13926 })
13927 .next()
13928 .await;
13929
13930 cx.condition(|editor, _| editor.context_menu_visible())
13931 .await;
13932 cx.update_editor(|editor, _, _| {
13933 let context_menu = editor.context_menu.borrow_mut();
13934 let context_menu = context_menu
13935 .as_ref()
13936 .expect("Should have the context menu deployed");
13937 match context_menu {
13938 CodeContextMenu::Completions(completions_menu) => {
13939 let completions = completions_menu.completions.borrow_mut();
13940 assert_eq!(
13941 completions
13942 .iter()
13943 .map(|completion| &completion.label.text)
13944 .collect::<Vec<_>>(),
13945 vec!["method id()", "other"]
13946 )
13947 }
13948 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
13949 }
13950 });
13951
13952 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
13953 let item1 = item1.clone();
13954 move |_, item_to_resolve, _| {
13955 let item1 = item1.clone();
13956 async move {
13957 if item1 == item_to_resolve {
13958 Ok(lsp::CompletionItem {
13959 label: "method id()".to_string(),
13960 filter_text: Some("id".to_string()),
13961 detail: Some("Now resolved!".to_string()),
13962 documentation: Some(lsp::Documentation::String("Docs".to_string())),
13963 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13964 range: lsp::Range::new(
13965 lsp::Position::new(0, 22),
13966 lsp::Position::new(0, 22),
13967 ),
13968 new_text: ".id".to_string(),
13969 })),
13970 ..lsp::CompletionItem::default()
13971 })
13972 } else {
13973 Ok(item_to_resolve)
13974 }
13975 }
13976 }
13977 })
13978 .next()
13979 .await
13980 .unwrap();
13981 cx.run_until_parked();
13982
13983 cx.update_editor(|editor, window, cx| {
13984 editor.context_menu_next(&Default::default(), window, cx);
13985 });
13986
13987 cx.update_editor(|editor, _, _| {
13988 let context_menu = editor.context_menu.borrow_mut();
13989 let context_menu = context_menu
13990 .as_ref()
13991 .expect("Should have the context menu deployed");
13992 match context_menu {
13993 CodeContextMenu::Completions(completions_menu) => {
13994 let completions = completions_menu.completions.borrow_mut();
13995 assert_eq!(
13996 completions
13997 .iter()
13998 .map(|completion| &completion.label.text)
13999 .collect::<Vec<_>>(),
14000 vec!["method id() Now resolved!", "other"],
14001 "Should update first completion label, but not second as the filter text did not match."
14002 );
14003 }
14004 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
14005 }
14006 });
14007}
14008
14009#[gpui::test]
14010async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
14011 init_test(cx, |_| {});
14012 let mut cx = EditorLspTestContext::new_rust(
14013 lsp::ServerCapabilities {
14014 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
14015 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
14016 completion_provider: Some(lsp::CompletionOptions {
14017 resolve_provider: Some(true),
14018 ..Default::default()
14019 }),
14020 ..Default::default()
14021 },
14022 cx,
14023 )
14024 .await;
14025 cx.set_state(indoc! {"
14026 struct TestStruct {
14027 field: i32
14028 }
14029
14030 fn mainˇ() {
14031 let unused_var = 42;
14032 let test_struct = TestStruct { field: 42 };
14033 }
14034 "});
14035 let symbol_range = cx.lsp_range(indoc! {"
14036 struct TestStruct {
14037 field: i32
14038 }
14039
14040 «fn main»() {
14041 let unused_var = 42;
14042 let test_struct = TestStruct { field: 42 };
14043 }
14044 "});
14045 let mut hover_requests =
14046 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
14047 Ok(Some(lsp::Hover {
14048 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
14049 kind: lsp::MarkupKind::Markdown,
14050 value: "Function documentation".to_string(),
14051 }),
14052 range: Some(symbol_range),
14053 }))
14054 });
14055
14056 // Case 1: Test that code action menu hide hover popover
14057 cx.dispatch_action(Hover);
14058 hover_requests.next().await;
14059 cx.condition(|editor, _| editor.hover_state.visible()).await;
14060 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
14061 move |_, _, _| async move {
14062 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
14063 lsp::CodeAction {
14064 title: "Remove unused variable".to_string(),
14065 kind: Some(CodeActionKind::QUICKFIX),
14066 edit: Some(lsp::WorkspaceEdit {
14067 changes: Some(
14068 [(
14069 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
14070 vec![lsp::TextEdit {
14071 range: lsp::Range::new(
14072 lsp::Position::new(5, 4),
14073 lsp::Position::new(5, 27),
14074 ),
14075 new_text: "".to_string(),
14076 }],
14077 )]
14078 .into_iter()
14079 .collect(),
14080 ),
14081 ..Default::default()
14082 }),
14083 ..Default::default()
14084 },
14085 )]))
14086 },
14087 );
14088 cx.update_editor(|editor, window, cx| {
14089 editor.toggle_code_actions(
14090 &ToggleCodeActions {
14091 deployed_from_indicator: None,
14092 quick_launch: false,
14093 },
14094 window,
14095 cx,
14096 );
14097 });
14098 code_action_requests.next().await;
14099 cx.run_until_parked();
14100 cx.condition(|editor, _| editor.context_menu_visible())
14101 .await;
14102 cx.update_editor(|editor, _, _| {
14103 assert!(
14104 !editor.hover_state.visible(),
14105 "Hover popover should be hidden when code action menu is shown"
14106 );
14107 // Hide code actions
14108 editor.context_menu.take();
14109 });
14110
14111 // Case 2: Test that code completions hide hover popover
14112 cx.dispatch_action(Hover);
14113 hover_requests.next().await;
14114 cx.condition(|editor, _| editor.hover_state.visible()).await;
14115 let counter = Arc::new(AtomicUsize::new(0));
14116 let mut completion_requests =
14117 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14118 let counter = counter.clone();
14119 async move {
14120 counter.fetch_add(1, atomic::Ordering::Release);
14121 Ok(Some(lsp::CompletionResponse::Array(vec![
14122 lsp::CompletionItem {
14123 label: "main".into(),
14124 kind: Some(lsp::CompletionItemKind::FUNCTION),
14125 detail: Some("() -> ()".to_string()),
14126 ..Default::default()
14127 },
14128 lsp::CompletionItem {
14129 label: "TestStruct".into(),
14130 kind: Some(lsp::CompletionItemKind::STRUCT),
14131 detail: Some("struct TestStruct".to_string()),
14132 ..Default::default()
14133 },
14134 ])))
14135 }
14136 });
14137 cx.update_editor(|editor, window, cx| {
14138 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14139 });
14140 completion_requests.next().await;
14141 cx.condition(|editor, _| editor.context_menu_visible())
14142 .await;
14143 cx.update_editor(|editor, _, _| {
14144 assert!(
14145 !editor.hover_state.visible(),
14146 "Hover popover should be hidden when completion menu is shown"
14147 );
14148 });
14149}
14150
14151#[gpui::test]
14152async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
14153 init_test(cx, |_| {});
14154
14155 let mut cx = EditorLspTestContext::new_rust(
14156 lsp::ServerCapabilities {
14157 completion_provider: Some(lsp::CompletionOptions {
14158 trigger_characters: Some(vec![".".to_string()]),
14159 resolve_provider: Some(true),
14160 ..Default::default()
14161 }),
14162 ..Default::default()
14163 },
14164 cx,
14165 )
14166 .await;
14167
14168 cx.set_state("fn main() { let a = 2ˇ; }");
14169 cx.simulate_keystroke(".");
14170
14171 let unresolved_item_1 = lsp::CompletionItem {
14172 label: "id".to_string(),
14173 filter_text: Some("id".to_string()),
14174 detail: None,
14175 documentation: None,
14176 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14177 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14178 new_text: ".id".to_string(),
14179 })),
14180 ..lsp::CompletionItem::default()
14181 };
14182 let resolved_item_1 = lsp::CompletionItem {
14183 additional_text_edits: Some(vec![lsp::TextEdit {
14184 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
14185 new_text: "!!".to_string(),
14186 }]),
14187 ..unresolved_item_1.clone()
14188 };
14189 let unresolved_item_2 = lsp::CompletionItem {
14190 label: "other".to_string(),
14191 filter_text: Some("other".to_string()),
14192 detail: None,
14193 documentation: None,
14194 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14195 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14196 new_text: ".other".to_string(),
14197 })),
14198 ..lsp::CompletionItem::default()
14199 };
14200 let resolved_item_2 = lsp::CompletionItem {
14201 additional_text_edits: Some(vec![lsp::TextEdit {
14202 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
14203 new_text: "??".to_string(),
14204 }]),
14205 ..unresolved_item_2.clone()
14206 };
14207
14208 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
14209 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
14210 cx.lsp
14211 .server
14212 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
14213 let unresolved_item_1 = unresolved_item_1.clone();
14214 let resolved_item_1 = resolved_item_1.clone();
14215 let unresolved_item_2 = unresolved_item_2.clone();
14216 let resolved_item_2 = resolved_item_2.clone();
14217 let resolve_requests_1 = resolve_requests_1.clone();
14218 let resolve_requests_2 = resolve_requests_2.clone();
14219 move |unresolved_request, _| {
14220 let unresolved_item_1 = unresolved_item_1.clone();
14221 let resolved_item_1 = resolved_item_1.clone();
14222 let unresolved_item_2 = unresolved_item_2.clone();
14223 let resolved_item_2 = resolved_item_2.clone();
14224 let resolve_requests_1 = resolve_requests_1.clone();
14225 let resolve_requests_2 = resolve_requests_2.clone();
14226 async move {
14227 if unresolved_request == unresolved_item_1 {
14228 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
14229 Ok(resolved_item_1.clone())
14230 } else if unresolved_request == unresolved_item_2 {
14231 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
14232 Ok(resolved_item_2.clone())
14233 } else {
14234 panic!("Unexpected completion item {unresolved_request:?}")
14235 }
14236 }
14237 }
14238 })
14239 .detach();
14240
14241 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14242 let unresolved_item_1 = unresolved_item_1.clone();
14243 let unresolved_item_2 = unresolved_item_2.clone();
14244 async move {
14245 Ok(Some(lsp::CompletionResponse::Array(vec![
14246 unresolved_item_1,
14247 unresolved_item_2,
14248 ])))
14249 }
14250 })
14251 .next()
14252 .await;
14253
14254 cx.condition(|editor, _| editor.context_menu_visible())
14255 .await;
14256 cx.update_editor(|editor, _, _| {
14257 let context_menu = editor.context_menu.borrow_mut();
14258 let context_menu = context_menu
14259 .as_ref()
14260 .expect("Should have the context menu deployed");
14261 match context_menu {
14262 CodeContextMenu::Completions(completions_menu) => {
14263 let completions = completions_menu.completions.borrow_mut();
14264 assert_eq!(
14265 completions
14266 .iter()
14267 .map(|completion| &completion.label.text)
14268 .collect::<Vec<_>>(),
14269 vec!["id", "other"]
14270 )
14271 }
14272 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
14273 }
14274 });
14275 cx.run_until_parked();
14276
14277 cx.update_editor(|editor, window, cx| {
14278 editor.context_menu_next(&ContextMenuNext, window, cx);
14279 });
14280 cx.run_until_parked();
14281 cx.update_editor(|editor, window, cx| {
14282 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
14283 });
14284 cx.run_until_parked();
14285 cx.update_editor(|editor, window, cx| {
14286 editor.context_menu_next(&ContextMenuNext, window, cx);
14287 });
14288 cx.run_until_parked();
14289 cx.update_editor(|editor, window, cx| {
14290 editor
14291 .compose_completion(&ComposeCompletion::default(), window, cx)
14292 .expect("No task returned")
14293 })
14294 .await
14295 .expect("Completion failed");
14296 cx.run_until_parked();
14297
14298 cx.update_editor(|editor, _, cx| {
14299 assert_eq!(
14300 resolve_requests_1.load(atomic::Ordering::Acquire),
14301 1,
14302 "Should always resolve once despite multiple selections"
14303 );
14304 assert_eq!(
14305 resolve_requests_2.load(atomic::Ordering::Acquire),
14306 1,
14307 "Should always resolve once after multiple selections and applying the completion"
14308 );
14309 assert_eq!(
14310 editor.text(cx),
14311 "fn main() { let a = ??.other; }",
14312 "Should use resolved data when applying the completion"
14313 );
14314 });
14315}
14316
14317#[gpui::test]
14318async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
14319 init_test(cx, |_| {});
14320
14321 let item_0 = lsp::CompletionItem {
14322 label: "abs".into(),
14323 insert_text: Some("abs".into()),
14324 data: Some(json!({ "very": "special"})),
14325 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
14326 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
14327 lsp::InsertReplaceEdit {
14328 new_text: "abs".to_string(),
14329 insert: lsp::Range::default(),
14330 replace: lsp::Range::default(),
14331 },
14332 )),
14333 ..lsp::CompletionItem::default()
14334 };
14335 let items = iter::once(item_0.clone())
14336 .chain((11..51).map(|i| lsp::CompletionItem {
14337 label: format!("item_{}", i),
14338 insert_text: Some(format!("item_{}", i)),
14339 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
14340 ..lsp::CompletionItem::default()
14341 }))
14342 .collect::<Vec<_>>();
14343
14344 let default_commit_characters = vec!["?".to_string()];
14345 let default_data = json!({ "default": "data"});
14346 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
14347 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
14348 let default_edit_range = lsp::Range {
14349 start: lsp::Position {
14350 line: 0,
14351 character: 5,
14352 },
14353 end: lsp::Position {
14354 line: 0,
14355 character: 5,
14356 },
14357 };
14358
14359 let mut cx = EditorLspTestContext::new_rust(
14360 lsp::ServerCapabilities {
14361 completion_provider: Some(lsp::CompletionOptions {
14362 trigger_characters: Some(vec![".".to_string()]),
14363 resolve_provider: Some(true),
14364 ..Default::default()
14365 }),
14366 ..Default::default()
14367 },
14368 cx,
14369 )
14370 .await;
14371
14372 cx.set_state("fn main() { let a = 2ˇ; }");
14373 cx.simulate_keystroke(".");
14374
14375 let completion_data = default_data.clone();
14376 let completion_characters = default_commit_characters.clone();
14377 let completion_items = items.clone();
14378 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14379 let default_data = completion_data.clone();
14380 let default_commit_characters = completion_characters.clone();
14381 let items = completion_items.clone();
14382 async move {
14383 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
14384 items,
14385 item_defaults: Some(lsp::CompletionListItemDefaults {
14386 data: Some(default_data.clone()),
14387 commit_characters: Some(default_commit_characters.clone()),
14388 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
14389 default_edit_range,
14390 )),
14391 insert_text_format: Some(default_insert_text_format),
14392 insert_text_mode: Some(default_insert_text_mode),
14393 }),
14394 ..lsp::CompletionList::default()
14395 })))
14396 }
14397 })
14398 .next()
14399 .await;
14400
14401 let resolved_items = Arc::new(Mutex::new(Vec::new()));
14402 cx.lsp
14403 .server
14404 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
14405 let closure_resolved_items = resolved_items.clone();
14406 move |item_to_resolve, _| {
14407 let closure_resolved_items = closure_resolved_items.clone();
14408 async move {
14409 closure_resolved_items.lock().push(item_to_resolve.clone());
14410 Ok(item_to_resolve)
14411 }
14412 }
14413 })
14414 .detach();
14415
14416 cx.condition(|editor, _| editor.context_menu_visible())
14417 .await;
14418 cx.run_until_parked();
14419 cx.update_editor(|editor, _, _| {
14420 let menu = editor.context_menu.borrow_mut();
14421 match menu.as_ref().expect("should have the completions menu") {
14422 CodeContextMenu::Completions(completions_menu) => {
14423 assert_eq!(
14424 completions_menu
14425 .entries
14426 .borrow()
14427 .iter()
14428 .map(|mat| mat.string.clone())
14429 .collect::<Vec<String>>(),
14430 items
14431 .iter()
14432 .map(|completion| completion.label.clone())
14433 .collect::<Vec<String>>()
14434 );
14435 }
14436 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
14437 }
14438 });
14439 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
14440 // with 4 from the end.
14441 assert_eq!(
14442 *resolved_items.lock(),
14443 [&items[0..16], &items[items.len() - 4..items.len()]]
14444 .concat()
14445 .iter()
14446 .cloned()
14447 .map(|mut item| {
14448 if item.data.is_none() {
14449 item.data = Some(default_data.clone());
14450 }
14451 item
14452 })
14453 .collect::<Vec<lsp::CompletionItem>>(),
14454 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
14455 );
14456 resolved_items.lock().clear();
14457
14458 cx.update_editor(|editor, window, cx| {
14459 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
14460 });
14461 cx.run_until_parked();
14462 // Completions that have already been resolved are skipped.
14463 assert_eq!(
14464 *resolved_items.lock(),
14465 items[items.len() - 16..items.len() - 4]
14466 .iter()
14467 .cloned()
14468 .map(|mut item| {
14469 if item.data.is_none() {
14470 item.data = Some(default_data.clone());
14471 }
14472 item
14473 })
14474 .collect::<Vec<lsp::CompletionItem>>()
14475 );
14476 resolved_items.lock().clear();
14477}
14478
14479#[gpui::test]
14480async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
14481 init_test(cx, |_| {});
14482
14483 let mut cx = EditorLspTestContext::new(
14484 Language::new(
14485 LanguageConfig {
14486 matcher: LanguageMatcher {
14487 path_suffixes: vec!["jsx".into()],
14488 ..Default::default()
14489 },
14490 overrides: [(
14491 "element".into(),
14492 LanguageConfigOverride {
14493 completion_query_characters: Override::Set(['-'].into_iter().collect()),
14494 ..Default::default()
14495 },
14496 )]
14497 .into_iter()
14498 .collect(),
14499 ..Default::default()
14500 },
14501 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
14502 )
14503 .with_override_query("(jsx_self_closing_element) @element")
14504 .unwrap(),
14505 lsp::ServerCapabilities {
14506 completion_provider: Some(lsp::CompletionOptions {
14507 trigger_characters: Some(vec![":".to_string()]),
14508 ..Default::default()
14509 }),
14510 ..Default::default()
14511 },
14512 cx,
14513 )
14514 .await;
14515
14516 cx.lsp
14517 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14518 Ok(Some(lsp::CompletionResponse::Array(vec![
14519 lsp::CompletionItem {
14520 label: "bg-blue".into(),
14521 ..Default::default()
14522 },
14523 lsp::CompletionItem {
14524 label: "bg-red".into(),
14525 ..Default::default()
14526 },
14527 lsp::CompletionItem {
14528 label: "bg-yellow".into(),
14529 ..Default::default()
14530 },
14531 ])))
14532 });
14533
14534 cx.set_state(r#"<p class="bgˇ" />"#);
14535
14536 // Trigger completion when typing a dash, because the dash is an extra
14537 // word character in the 'element' scope, which contains the cursor.
14538 cx.simulate_keystroke("-");
14539 cx.executor().run_until_parked();
14540 cx.update_editor(|editor, _, _| {
14541 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14542 {
14543 assert_eq!(
14544 completion_menu_entries(&menu),
14545 &["bg-red", "bg-blue", "bg-yellow"]
14546 );
14547 } else {
14548 panic!("expected completion menu to be open");
14549 }
14550 });
14551
14552 cx.simulate_keystroke("l");
14553 cx.executor().run_until_parked();
14554 cx.update_editor(|editor, _, _| {
14555 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14556 {
14557 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
14558 } else {
14559 panic!("expected completion menu to be open");
14560 }
14561 });
14562
14563 // When filtering completions, consider the character after the '-' to
14564 // be the start of a subword.
14565 cx.set_state(r#"<p class="yelˇ" />"#);
14566 cx.simulate_keystroke("l");
14567 cx.executor().run_until_parked();
14568 cx.update_editor(|editor, _, _| {
14569 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14570 {
14571 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
14572 } else {
14573 panic!("expected completion menu to be open");
14574 }
14575 });
14576}
14577
14578fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
14579 let entries = menu.entries.borrow();
14580 entries.iter().map(|mat| mat.string.clone()).collect()
14581}
14582
14583#[gpui::test]
14584async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
14585 init_test(cx, |settings| {
14586 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
14587 FormatterList(vec![Formatter::Prettier].into()),
14588 ))
14589 });
14590
14591 let fs = FakeFs::new(cx.executor());
14592 fs.insert_file(path!("/file.ts"), Default::default()).await;
14593
14594 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
14595 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14596
14597 language_registry.add(Arc::new(Language::new(
14598 LanguageConfig {
14599 name: "TypeScript".into(),
14600 matcher: LanguageMatcher {
14601 path_suffixes: vec!["ts".to_string()],
14602 ..Default::default()
14603 },
14604 ..Default::default()
14605 },
14606 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
14607 )));
14608 update_test_language_settings(cx, |settings| {
14609 settings.defaults.prettier = Some(PrettierSettings {
14610 allowed: true,
14611 ..PrettierSettings::default()
14612 });
14613 });
14614
14615 let test_plugin = "test_plugin";
14616 let _ = language_registry.register_fake_lsp(
14617 "TypeScript",
14618 FakeLspAdapter {
14619 prettier_plugins: vec![test_plugin],
14620 ..Default::default()
14621 },
14622 );
14623
14624 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
14625 let buffer = project
14626 .update(cx, |project, cx| {
14627 project.open_local_buffer(path!("/file.ts"), cx)
14628 })
14629 .await
14630 .unwrap();
14631
14632 let buffer_text = "one\ntwo\nthree\n";
14633 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
14634 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
14635 editor.update_in(cx, |editor, window, cx| {
14636 editor.set_text(buffer_text, window, cx)
14637 });
14638
14639 editor
14640 .update_in(cx, |editor, window, cx| {
14641 editor.perform_format(
14642 project.clone(),
14643 FormatTrigger::Manual,
14644 FormatTarget::Buffers,
14645 window,
14646 cx,
14647 )
14648 })
14649 .unwrap()
14650 .await;
14651 assert_eq!(
14652 editor.update(cx, |editor, cx| editor.text(cx)),
14653 buffer_text.to_string() + prettier_format_suffix,
14654 "Test prettier formatting was not applied to the original buffer text",
14655 );
14656
14657 update_test_language_settings(cx, |settings| {
14658 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
14659 });
14660 let format = editor.update_in(cx, |editor, window, cx| {
14661 editor.perform_format(
14662 project.clone(),
14663 FormatTrigger::Manual,
14664 FormatTarget::Buffers,
14665 window,
14666 cx,
14667 )
14668 });
14669 format.await.unwrap();
14670 assert_eq!(
14671 editor.update(cx, |editor, cx| editor.text(cx)),
14672 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
14673 "Autoformatting (via test prettier) was not applied to the original buffer text",
14674 );
14675}
14676
14677#[gpui::test]
14678async fn test_addition_reverts(cx: &mut TestAppContext) {
14679 init_test(cx, |_| {});
14680 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14681 let base_text = indoc! {r#"
14682 struct Row;
14683 struct Row1;
14684 struct Row2;
14685
14686 struct Row4;
14687 struct Row5;
14688 struct Row6;
14689
14690 struct Row8;
14691 struct Row9;
14692 struct Row10;"#};
14693
14694 // When addition hunks are not adjacent to carets, no hunk revert is performed
14695 assert_hunk_revert(
14696 indoc! {r#"struct Row;
14697 struct Row1;
14698 struct Row1.1;
14699 struct Row1.2;
14700 struct Row2;ˇ
14701
14702 struct Row4;
14703 struct Row5;
14704 struct Row6;
14705
14706 struct Row8;
14707 ˇstruct Row9;
14708 struct Row9.1;
14709 struct Row9.2;
14710 struct Row9.3;
14711 struct Row10;"#},
14712 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
14713 indoc! {r#"struct Row;
14714 struct Row1;
14715 struct Row1.1;
14716 struct Row1.2;
14717 struct Row2;ˇ
14718
14719 struct Row4;
14720 struct Row5;
14721 struct Row6;
14722
14723 struct Row8;
14724 ˇstruct Row9;
14725 struct Row9.1;
14726 struct Row9.2;
14727 struct Row9.3;
14728 struct Row10;"#},
14729 base_text,
14730 &mut cx,
14731 );
14732 // Same for selections
14733 assert_hunk_revert(
14734 indoc! {r#"struct Row;
14735 struct Row1;
14736 struct Row2;
14737 struct Row2.1;
14738 struct Row2.2;
14739 «ˇ
14740 struct Row4;
14741 struct» Row5;
14742 «struct Row6;
14743 ˇ»
14744 struct Row9.1;
14745 struct Row9.2;
14746 struct Row9.3;
14747 struct Row8;
14748 struct Row9;
14749 struct Row10;"#},
14750 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
14751 indoc! {r#"struct Row;
14752 struct Row1;
14753 struct Row2;
14754 struct Row2.1;
14755 struct Row2.2;
14756 «ˇ
14757 struct Row4;
14758 struct» Row5;
14759 «struct Row6;
14760 ˇ»
14761 struct Row9.1;
14762 struct Row9.2;
14763 struct Row9.3;
14764 struct Row8;
14765 struct Row9;
14766 struct Row10;"#},
14767 base_text,
14768 &mut cx,
14769 );
14770
14771 // When carets and selections intersect the addition hunks, those are reverted.
14772 // Adjacent carets got merged.
14773 assert_hunk_revert(
14774 indoc! {r#"struct Row;
14775 ˇ// something on the top
14776 struct Row1;
14777 struct Row2;
14778 struct Roˇw3.1;
14779 struct Row2.2;
14780 struct Row2.3;ˇ
14781
14782 struct Row4;
14783 struct ˇRow5.1;
14784 struct Row5.2;
14785 struct «Rowˇ»5.3;
14786 struct Row5;
14787 struct Row6;
14788 ˇ
14789 struct Row9.1;
14790 struct «Rowˇ»9.2;
14791 struct «ˇRow»9.3;
14792 struct Row8;
14793 struct Row9;
14794 «ˇ// something on bottom»
14795 struct Row10;"#},
14796 vec![
14797 DiffHunkStatusKind::Added,
14798 DiffHunkStatusKind::Added,
14799 DiffHunkStatusKind::Added,
14800 DiffHunkStatusKind::Added,
14801 DiffHunkStatusKind::Added,
14802 ],
14803 indoc! {r#"struct Row;
14804 ˇstruct Row1;
14805 struct Row2;
14806 ˇ
14807 struct Row4;
14808 ˇstruct Row5;
14809 struct Row6;
14810 ˇ
14811 ˇstruct Row8;
14812 struct Row9;
14813 ˇstruct Row10;"#},
14814 base_text,
14815 &mut cx,
14816 );
14817}
14818
14819#[gpui::test]
14820async fn test_modification_reverts(cx: &mut TestAppContext) {
14821 init_test(cx, |_| {});
14822 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14823 let base_text = indoc! {r#"
14824 struct Row;
14825 struct Row1;
14826 struct Row2;
14827
14828 struct Row4;
14829 struct Row5;
14830 struct Row6;
14831
14832 struct Row8;
14833 struct Row9;
14834 struct Row10;"#};
14835
14836 // Modification hunks behave the same as the addition ones.
14837 assert_hunk_revert(
14838 indoc! {r#"struct Row;
14839 struct Row1;
14840 struct Row33;
14841 ˇ
14842 struct Row4;
14843 struct Row5;
14844 struct Row6;
14845 ˇ
14846 struct Row99;
14847 struct Row9;
14848 struct Row10;"#},
14849 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
14850 indoc! {r#"struct Row;
14851 struct Row1;
14852 struct Row33;
14853 ˇ
14854 struct Row4;
14855 struct Row5;
14856 struct Row6;
14857 ˇ
14858 struct Row99;
14859 struct Row9;
14860 struct Row10;"#},
14861 base_text,
14862 &mut cx,
14863 );
14864 assert_hunk_revert(
14865 indoc! {r#"struct Row;
14866 struct Row1;
14867 struct Row33;
14868 «ˇ
14869 struct Row4;
14870 struct» Row5;
14871 «struct Row6;
14872 ˇ»
14873 struct Row99;
14874 struct Row9;
14875 struct Row10;"#},
14876 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
14877 indoc! {r#"struct Row;
14878 struct Row1;
14879 struct Row33;
14880 «ˇ
14881 struct Row4;
14882 struct» Row5;
14883 «struct Row6;
14884 ˇ»
14885 struct Row99;
14886 struct Row9;
14887 struct Row10;"#},
14888 base_text,
14889 &mut cx,
14890 );
14891
14892 assert_hunk_revert(
14893 indoc! {r#"ˇstruct Row1.1;
14894 struct Row1;
14895 «ˇstr»uct Row22;
14896
14897 struct ˇRow44;
14898 struct Row5;
14899 struct «Rˇ»ow66;ˇ
14900
14901 «struˇ»ct Row88;
14902 struct Row9;
14903 struct Row1011;ˇ"#},
14904 vec![
14905 DiffHunkStatusKind::Modified,
14906 DiffHunkStatusKind::Modified,
14907 DiffHunkStatusKind::Modified,
14908 DiffHunkStatusKind::Modified,
14909 DiffHunkStatusKind::Modified,
14910 DiffHunkStatusKind::Modified,
14911 ],
14912 indoc! {r#"struct Row;
14913 ˇstruct Row1;
14914 struct Row2;
14915 ˇ
14916 struct Row4;
14917 ˇstruct Row5;
14918 struct Row6;
14919 ˇ
14920 struct Row8;
14921 ˇstruct Row9;
14922 struct Row10;ˇ"#},
14923 base_text,
14924 &mut cx,
14925 );
14926}
14927
14928#[gpui::test]
14929async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
14930 init_test(cx, |_| {});
14931 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14932 let base_text = indoc! {r#"
14933 one
14934
14935 two
14936 three
14937 "#};
14938
14939 cx.set_head_text(base_text);
14940 cx.set_state("\nˇ\n");
14941 cx.executor().run_until_parked();
14942 cx.update_editor(|editor, _window, cx| {
14943 editor.expand_selected_diff_hunks(cx);
14944 });
14945 cx.executor().run_until_parked();
14946 cx.update_editor(|editor, window, cx| {
14947 editor.backspace(&Default::default(), window, cx);
14948 });
14949 cx.run_until_parked();
14950 cx.assert_state_with_diff(
14951 indoc! {r#"
14952
14953 - two
14954 - threeˇ
14955 +
14956 "#}
14957 .to_string(),
14958 );
14959}
14960
14961#[gpui::test]
14962async fn test_deletion_reverts(cx: &mut TestAppContext) {
14963 init_test(cx, |_| {});
14964 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14965 let base_text = indoc! {r#"struct Row;
14966struct Row1;
14967struct Row2;
14968
14969struct Row4;
14970struct Row5;
14971struct Row6;
14972
14973struct Row8;
14974struct Row9;
14975struct Row10;"#};
14976
14977 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
14978 assert_hunk_revert(
14979 indoc! {r#"struct Row;
14980 struct Row2;
14981
14982 ˇstruct Row4;
14983 struct Row5;
14984 struct Row6;
14985 ˇ
14986 struct Row8;
14987 struct Row10;"#},
14988 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
14989 indoc! {r#"struct Row;
14990 struct Row2;
14991
14992 ˇstruct Row4;
14993 struct Row5;
14994 struct Row6;
14995 ˇ
14996 struct Row8;
14997 struct Row10;"#},
14998 base_text,
14999 &mut cx,
15000 );
15001 assert_hunk_revert(
15002 indoc! {r#"struct Row;
15003 struct Row2;
15004
15005 «ˇstruct Row4;
15006 struct» Row5;
15007 «struct Row6;
15008 ˇ»
15009 struct Row8;
15010 struct Row10;"#},
15011 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
15012 indoc! {r#"struct Row;
15013 struct Row2;
15014
15015 «ˇstruct Row4;
15016 struct» Row5;
15017 «struct Row6;
15018 ˇ»
15019 struct Row8;
15020 struct Row10;"#},
15021 base_text,
15022 &mut cx,
15023 );
15024
15025 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
15026 assert_hunk_revert(
15027 indoc! {r#"struct Row;
15028 ˇstruct Row2;
15029
15030 struct Row4;
15031 struct Row5;
15032 struct Row6;
15033
15034 struct Row8;ˇ
15035 struct Row10;"#},
15036 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
15037 indoc! {r#"struct Row;
15038 struct Row1;
15039 ˇstruct Row2;
15040
15041 struct Row4;
15042 struct Row5;
15043 struct Row6;
15044
15045 struct Row8;ˇ
15046 struct Row9;
15047 struct Row10;"#},
15048 base_text,
15049 &mut cx,
15050 );
15051 assert_hunk_revert(
15052 indoc! {r#"struct Row;
15053 struct Row2«ˇ;
15054 struct Row4;
15055 struct» Row5;
15056 «struct Row6;
15057
15058 struct Row8;ˇ»
15059 struct Row10;"#},
15060 vec![
15061 DiffHunkStatusKind::Deleted,
15062 DiffHunkStatusKind::Deleted,
15063 DiffHunkStatusKind::Deleted,
15064 ],
15065 indoc! {r#"struct Row;
15066 struct Row1;
15067 struct Row2«ˇ;
15068
15069 struct Row4;
15070 struct» Row5;
15071 «struct Row6;
15072
15073 struct Row8;ˇ»
15074 struct Row9;
15075 struct Row10;"#},
15076 base_text,
15077 &mut cx,
15078 );
15079}
15080
15081#[gpui::test]
15082async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
15083 init_test(cx, |_| {});
15084
15085 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
15086 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
15087 let base_text_3 =
15088 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
15089
15090 let text_1 = edit_first_char_of_every_line(base_text_1);
15091 let text_2 = edit_first_char_of_every_line(base_text_2);
15092 let text_3 = edit_first_char_of_every_line(base_text_3);
15093
15094 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
15095 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
15096 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
15097
15098 let multibuffer = cx.new(|cx| {
15099 let mut multibuffer = MultiBuffer::new(ReadWrite);
15100 multibuffer.push_excerpts(
15101 buffer_1.clone(),
15102 [
15103 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15104 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15105 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15106 ],
15107 cx,
15108 );
15109 multibuffer.push_excerpts(
15110 buffer_2.clone(),
15111 [
15112 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15113 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15114 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15115 ],
15116 cx,
15117 );
15118 multibuffer.push_excerpts(
15119 buffer_3.clone(),
15120 [
15121 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15122 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15123 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15124 ],
15125 cx,
15126 );
15127 multibuffer
15128 });
15129
15130 let fs = FakeFs::new(cx.executor());
15131 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
15132 let (editor, cx) = cx
15133 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
15134 editor.update_in(cx, |editor, _window, cx| {
15135 for (buffer, diff_base) in [
15136 (buffer_1.clone(), base_text_1),
15137 (buffer_2.clone(), base_text_2),
15138 (buffer_3.clone(), base_text_3),
15139 ] {
15140 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
15141 editor
15142 .buffer
15143 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
15144 }
15145 });
15146 cx.executor().run_until_parked();
15147
15148 editor.update_in(cx, |editor, window, cx| {
15149 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}");
15150 editor.select_all(&SelectAll, window, cx);
15151 editor.git_restore(&Default::default(), window, cx);
15152 });
15153 cx.executor().run_until_parked();
15154
15155 // When all ranges are selected, all buffer hunks are reverted.
15156 editor.update(cx, |editor, cx| {
15157 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");
15158 });
15159 buffer_1.update(cx, |buffer, _| {
15160 assert_eq!(buffer.text(), base_text_1);
15161 });
15162 buffer_2.update(cx, |buffer, _| {
15163 assert_eq!(buffer.text(), base_text_2);
15164 });
15165 buffer_3.update(cx, |buffer, _| {
15166 assert_eq!(buffer.text(), base_text_3);
15167 });
15168
15169 editor.update_in(cx, |editor, window, cx| {
15170 editor.undo(&Default::default(), window, cx);
15171 });
15172
15173 editor.update_in(cx, |editor, window, cx| {
15174 editor.change_selections(None, window, cx, |s| {
15175 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
15176 });
15177 editor.git_restore(&Default::default(), window, cx);
15178 });
15179
15180 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
15181 // but not affect buffer_2 and its related excerpts.
15182 editor.update(cx, |editor, cx| {
15183 assert_eq!(
15184 editor.text(cx),
15185 "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}"
15186 );
15187 });
15188 buffer_1.update(cx, |buffer, _| {
15189 assert_eq!(buffer.text(), base_text_1);
15190 });
15191 buffer_2.update(cx, |buffer, _| {
15192 assert_eq!(
15193 buffer.text(),
15194 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
15195 );
15196 });
15197 buffer_3.update(cx, |buffer, _| {
15198 assert_eq!(
15199 buffer.text(),
15200 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
15201 );
15202 });
15203
15204 fn edit_first_char_of_every_line(text: &str) -> String {
15205 text.split('\n')
15206 .map(|line| format!("X{}", &line[1..]))
15207 .collect::<Vec<_>>()
15208 .join("\n")
15209 }
15210}
15211
15212#[gpui::test]
15213async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) {
15214 init_test(cx, |_| {});
15215
15216 let cols = 4;
15217 let rows = 10;
15218 let sample_text_1 = sample_text(rows, cols, 'a');
15219 assert_eq!(
15220 sample_text_1,
15221 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
15222 );
15223 let sample_text_2 = sample_text(rows, cols, 'l');
15224 assert_eq!(
15225 sample_text_2,
15226 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
15227 );
15228 let sample_text_3 = sample_text(rows, cols, 'v');
15229 assert_eq!(
15230 sample_text_3,
15231 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
15232 );
15233
15234 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
15235 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
15236 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
15237
15238 let multi_buffer = cx.new(|cx| {
15239 let mut multibuffer = MultiBuffer::new(ReadWrite);
15240 multibuffer.push_excerpts(
15241 buffer_1.clone(),
15242 [
15243 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15244 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15245 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15246 ],
15247 cx,
15248 );
15249 multibuffer.push_excerpts(
15250 buffer_2.clone(),
15251 [
15252 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15253 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15254 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15255 ],
15256 cx,
15257 );
15258 multibuffer.push_excerpts(
15259 buffer_3.clone(),
15260 [
15261 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15262 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15263 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15264 ],
15265 cx,
15266 );
15267 multibuffer
15268 });
15269
15270 let fs = FakeFs::new(cx.executor());
15271 fs.insert_tree(
15272 "/a",
15273 json!({
15274 "main.rs": sample_text_1,
15275 "other.rs": sample_text_2,
15276 "lib.rs": sample_text_3,
15277 }),
15278 )
15279 .await;
15280 let project = Project::test(fs, ["/a".as_ref()], cx).await;
15281 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15282 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
15283 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
15284 Editor::new(
15285 EditorMode::full(),
15286 multi_buffer,
15287 Some(project.clone()),
15288 window,
15289 cx,
15290 )
15291 });
15292 let multibuffer_item_id = workspace
15293 .update(cx, |workspace, window, cx| {
15294 assert!(
15295 workspace.active_item(cx).is_none(),
15296 "active item should be None before the first item is added"
15297 );
15298 workspace.add_item_to_active_pane(
15299 Box::new(multi_buffer_editor.clone()),
15300 None,
15301 true,
15302 window,
15303 cx,
15304 );
15305 let active_item = workspace
15306 .active_item(cx)
15307 .expect("should have an active item after adding the multi buffer");
15308 assert!(
15309 !active_item.is_singleton(cx),
15310 "A multi buffer was expected to active after adding"
15311 );
15312 active_item.item_id()
15313 })
15314 .unwrap();
15315 cx.executor().run_until_parked();
15316
15317 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15318 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
15319 s.select_ranges(Some(1..2))
15320 });
15321 editor.open_excerpts(&OpenExcerpts, window, cx);
15322 });
15323 cx.executor().run_until_parked();
15324 let first_item_id = workspace
15325 .update(cx, |workspace, window, cx| {
15326 let active_item = workspace
15327 .active_item(cx)
15328 .expect("should have an active item after navigating into the 1st buffer");
15329 let first_item_id = active_item.item_id();
15330 assert_ne!(
15331 first_item_id, multibuffer_item_id,
15332 "Should navigate into the 1st buffer and activate it"
15333 );
15334 assert!(
15335 active_item.is_singleton(cx),
15336 "New active item should be a singleton buffer"
15337 );
15338 assert_eq!(
15339 active_item
15340 .act_as::<Editor>(cx)
15341 .expect("should have navigated into an editor for the 1st buffer")
15342 .read(cx)
15343 .text(cx),
15344 sample_text_1
15345 );
15346
15347 workspace
15348 .go_back(workspace.active_pane().downgrade(), window, cx)
15349 .detach_and_log_err(cx);
15350
15351 first_item_id
15352 })
15353 .unwrap();
15354 cx.executor().run_until_parked();
15355 workspace
15356 .update(cx, |workspace, _, cx| {
15357 let active_item = workspace
15358 .active_item(cx)
15359 .expect("should have an active item after navigating back");
15360 assert_eq!(
15361 active_item.item_id(),
15362 multibuffer_item_id,
15363 "Should navigate back to the multi buffer"
15364 );
15365 assert!(!active_item.is_singleton(cx));
15366 })
15367 .unwrap();
15368
15369 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15370 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
15371 s.select_ranges(Some(39..40))
15372 });
15373 editor.open_excerpts(&OpenExcerpts, window, cx);
15374 });
15375 cx.executor().run_until_parked();
15376 let second_item_id = workspace
15377 .update(cx, |workspace, window, cx| {
15378 let active_item = workspace
15379 .active_item(cx)
15380 .expect("should have an active item after navigating into the 2nd buffer");
15381 let second_item_id = active_item.item_id();
15382 assert_ne!(
15383 second_item_id, multibuffer_item_id,
15384 "Should navigate away from the multibuffer"
15385 );
15386 assert_ne!(
15387 second_item_id, first_item_id,
15388 "Should navigate into the 2nd buffer and activate it"
15389 );
15390 assert!(
15391 active_item.is_singleton(cx),
15392 "New active item should be a singleton buffer"
15393 );
15394 assert_eq!(
15395 active_item
15396 .act_as::<Editor>(cx)
15397 .expect("should have navigated into an editor")
15398 .read(cx)
15399 .text(cx),
15400 sample_text_2
15401 );
15402
15403 workspace
15404 .go_back(workspace.active_pane().downgrade(), window, cx)
15405 .detach_and_log_err(cx);
15406
15407 second_item_id
15408 })
15409 .unwrap();
15410 cx.executor().run_until_parked();
15411 workspace
15412 .update(cx, |workspace, _, cx| {
15413 let active_item = workspace
15414 .active_item(cx)
15415 .expect("should have an active item after navigating back from the 2nd buffer");
15416 assert_eq!(
15417 active_item.item_id(),
15418 multibuffer_item_id,
15419 "Should navigate back from the 2nd buffer to the multi buffer"
15420 );
15421 assert!(!active_item.is_singleton(cx));
15422 })
15423 .unwrap();
15424
15425 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15426 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
15427 s.select_ranges(Some(70..70))
15428 });
15429 editor.open_excerpts(&OpenExcerpts, window, cx);
15430 });
15431 cx.executor().run_until_parked();
15432 workspace
15433 .update(cx, |workspace, window, cx| {
15434 let active_item = workspace
15435 .active_item(cx)
15436 .expect("should have an active item after navigating into the 3rd buffer");
15437 let third_item_id = active_item.item_id();
15438 assert_ne!(
15439 third_item_id, multibuffer_item_id,
15440 "Should navigate into the 3rd buffer and activate it"
15441 );
15442 assert_ne!(third_item_id, first_item_id);
15443 assert_ne!(third_item_id, second_item_id);
15444 assert!(
15445 active_item.is_singleton(cx),
15446 "New active item should be a singleton buffer"
15447 );
15448 assert_eq!(
15449 active_item
15450 .act_as::<Editor>(cx)
15451 .expect("should have navigated into an editor")
15452 .read(cx)
15453 .text(cx),
15454 sample_text_3
15455 );
15456
15457 workspace
15458 .go_back(workspace.active_pane().downgrade(), window, cx)
15459 .detach_and_log_err(cx);
15460 })
15461 .unwrap();
15462 cx.executor().run_until_parked();
15463 workspace
15464 .update(cx, |workspace, _, cx| {
15465 let active_item = workspace
15466 .active_item(cx)
15467 .expect("should have an active item after navigating back from the 3rd buffer");
15468 assert_eq!(
15469 active_item.item_id(),
15470 multibuffer_item_id,
15471 "Should navigate back from the 3rd buffer to the multi buffer"
15472 );
15473 assert!(!active_item.is_singleton(cx));
15474 })
15475 .unwrap();
15476}
15477
15478#[gpui::test]
15479async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
15480 init_test(cx, |_| {});
15481
15482 let mut cx = EditorTestContext::new(cx).await;
15483
15484 let diff_base = r#"
15485 use some::mod;
15486
15487 const A: u32 = 42;
15488
15489 fn main() {
15490 println!("hello");
15491
15492 println!("world");
15493 }
15494 "#
15495 .unindent();
15496
15497 cx.set_state(
15498 &r#"
15499 use some::modified;
15500
15501 ˇ
15502 fn main() {
15503 println!("hello there");
15504
15505 println!("around the");
15506 println!("world");
15507 }
15508 "#
15509 .unindent(),
15510 );
15511
15512 cx.set_head_text(&diff_base);
15513 executor.run_until_parked();
15514
15515 cx.update_editor(|editor, window, cx| {
15516 editor.go_to_next_hunk(&GoToHunk, window, cx);
15517 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15518 });
15519 executor.run_until_parked();
15520 cx.assert_state_with_diff(
15521 r#"
15522 use some::modified;
15523
15524
15525 fn main() {
15526 - println!("hello");
15527 + ˇ println!("hello there");
15528
15529 println!("around the");
15530 println!("world");
15531 }
15532 "#
15533 .unindent(),
15534 );
15535
15536 cx.update_editor(|editor, window, cx| {
15537 for _ in 0..2 {
15538 editor.go_to_next_hunk(&GoToHunk, window, cx);
15539 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15540 }
15541 });
15542 executor.run_until_parked();
15543 cx.assert_state_with_diff(
15544 r#"
15545 - use some::mod;
15546 + ˇuse some::modified;
15547
15548
15549 fn main() {
15550 - println!("hello");
15551 + println!("hello there");
15552
15553 + println!("around the");
15554 println!("world");
15555 }
15556 "#
15557 .unindent(),
15558 );
15559
15560 cx.update_editor(|editor, window, cx| {
15561 editor.go_to_next_hunk(&GoToHunk, window, cx);
15562 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15563 });
15564 executor.run_until_parked();
15565 cx.assert_state_with_diff(
15566 r#"
15567 - use some::mod;
15568 + use some::modified;
15569
15570 - const A: u32 = 42;
15571 ˇ
15572 fn main() {
15573 - println!("hello");
15574 + println!("hello there");
15575
15576 + println!("around the");
15577 println!("world");
15578 }
15579 "#
15580 .unindent(),
15581 );
15582
15583 cx.update_editor(|editor, window, cx| {
15584 editor.cancel(&Cancel, window, cx);
15585 });
15586
15587 cx.assert_state_with_diff(
15588 r#"
15589 use some::modified;
15590
15591 ˇ
15592 fn main() {
15593 println!("hello there");
15594
15595 println!("around the");
15596 println!("world");
15597 }
15598 "#
15599 .unindent(),
15600 );
15601}
15602
15603#[gpui::test]
15604async fn test_diff_base_change_with_expanded_diff_hunks(
15605 executor: BackgroundExecutor,
15606 cx: &mut TestAppContext,
15607) {
15608 init_test(cx, |_| {});
15609
15610 let mut cx = EditorTestContext::new(cx).await;
15611
15612 let diff_base = r#"
15613 use some::mod1;
15614 use some::mod2;
15615
15616 const A: u32 = 42;
15617 const B: u32 = 42;
15618 const C: u32 = 42;
15619
15620 fn main() {
15621 println!("hello");
15622
15623 println!("world");
15624 }
15625 "#
15626 .unindent();
15627
15628 cx.set_state(
15629 &r#"
15630 use some::mod2;
15631
15632 const A: u32 = 42;
15633 const C: u32 = 42;
15634
15635 fn main(ˇ) {
15636 //println!("hello");
15637
15638 println!("world");
15639 //
15640 //
15641 }
15642 "#
15643 .unindent(),
15644 );
15645
15646 cx.set_head_text(&diff_base);
15647 executor.run_until_parked();
15648
15649 cx.update_editor(|editor, window, cx| {
15650 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15651 });
15652 executor.run_until_parked();
15653 cx.assert_state_with_diff(
15654 r#"
15655 - use some::mod1;
15656 use some::mod2;
15657
15658 const A: u32 = 42;
15659 - const B: u32 = 42;
15660 const C: u32 = 42;
15661
15662 fn main(ˇ) {
15663 - println!("hello");
15664 + //println!("hello");
15665
15666 println!("world");
15667 + //
15668 + //
15669 }
15670 "#
15671 .unindent(),
15672 );
15673
15674 cx.set_head_text("new diff base!");
15675 executor.run_until_parked();
15676 cx.assert_state_with_diff(
15677 r#"
15678 - new diff base!
15679 + use some::mod2;
15680 +
15681 + const A: u32 = 42;
15682 + const C: u32 = 42;
15683 +
15684 + fn main(ˇ) {
15685 + //println!("hello");
15686 +
15687 + println!("world");
15688 + //
15689 + //
15690 + }
15691 "#
15692 .unindent(),
15693 );
15694}
15695
15696#[gpui::test]
15697async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
15698 init_test(cx, |_| {});
15699
15700 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
15701 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
15702 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
15703 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
15704 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
15705 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
15706
15707 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
15708 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
15709 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
15710
15711 let multi_buffer = cx.new(|cx| {
15712 let mut multibuffer = MultiBuffer::new(ReadWrite);
15713 multibuffer.push_excerpts(
15714 buffer_1.clone(),
15715 [
15716 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15717 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15718 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
15719 ],
15720 cx,
15721 );
15722 multibuffer.push_excerpts(
15723 buffer_2.clone(),
15724 [
15725 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15726 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15727 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
15728 ],
15729 cx,
15730 );
15731 multibuffer.push_excerpts(
15732 buffer_3.clone(),
15733 [
15734 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15735 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15736 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
15737 ],
15738 cx,
15739 );
15740 multibuffer
15741 });
15742
15743 let editor =
15744 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
15745 editor
15746 .update(cx, |editor, _window, cx| {
15747 for (buffer, diff_base) in [
15748 (buffer_1.clone(), file_1_old),
15749 (buffer_2.clone(), file_2_old),
15750 (buffer_3.clone(), file_3_old),
15751 ] {
15752 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
15753 editor
15754 .buffer
15755 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
15756 }
15757 })
15758 .unwrap();
15759
15760 let mut cx = EditorTestContext::for_editor(editor, cx).await;
15761 cx.run_until_parked();
15762
15763 cx.assert_editor_state(
15764 &"
15765 ˇaaa
15766 ccc
15767 ddd
15768
15769 ggg
15770 hhh
15771
15772
15773 lll
15774 mmm
15775 NNN
15776
15777 qqq
15778 rrr
15779
15780 uuu
15781 111
15782 222
15783 333
15784
15785 666
15786 777
15787
15788 000
15789 !!!"
15790 .unindent(),
15791 );
15792
15793 cx.update_editor(|editor, window, cx| {
15794 editor.select_all(&SelectAll, window, cx);
15795 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15796 });
15797 cx.executor().run_until_parked();
15798
15799 cx.assert_state_with_diff(
15800 "
15801 «aaa
15802 - bbb
15803 ccc
15804 ddd
15805
15806 ggg
15807 hhh
15808
15809
15810 lll
15811 mmm
15812 - nnn
15813 + NNN
15814
15815 qqq
15816 rrr
15817
15818 uuu
15819 111
15820 222
15821 333
15822
15823 + 666
15824 777
15825
15826 000
15827 !!!ˇ»"
15828 .unindent(),
15829 );
15830}
15831
15832#[gpui::test]
15833async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
15834 init_test(cx, |_| {});
15835
15836 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
15837 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
15838
15839 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
15840 let multi_buffer = cx.new(|cx| {
15841 let mut multibuffer = MultiBuffer::new(ReadWrite);
15842 multibuffer.push_excerpts(
15843 buffer.clone(),
15844 [
15845 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
15846 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
15847 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
15848 ],
15849 cx,
15850 );
15851 multibuffer
15852 });
15853
15854 let editor =
15855 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
15856 editor
15857 .update(cx, |editor, _window, cx| {
15858 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
15859 editor
15860 .buffer
15861 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
15862 })
15863 .unwrap();
15864
15865 let mut cx = EditorTestContext::for_editor(editor, cx).await;
15866 cx.run_until_parked();
15867
15868 cx.update_editor(|editor, window, cx| {
15869 editor.expand_all_diff_hunks(&Default::default(), window, cx)
15870 });
15871 cx.executor().run_until_parked();
15872
15873 // When the start of a hunk coincides with the start of its excerpt,
15874 // the hunk is expanded. When the start of a a hunk is earlier than
15875 // the start of its excerpt, the hunk is not expanded.
15876 cx.assert_state_with_diff(
15877 "
15878 ˇaaa
15879 - bbb
15880 + BBB
15881
15882 - ddd
15883 - eee
15884 + DDD
15885 + EEE
15886 fff
15887
15888 iii
15889 "
15890 .unindent(),
15891 );
15892}
15893
15894#[gpui::test]
15895async fn test_edits_around_expanded_insertion_hunks(
15896 executor: BackgroundExecutor,
15897 cx: &mut TestAppContext,
15898) {
15899 init_test(cx, |_| {});
15900
15901 let mut cx = EditorTestContext::new(cx).await;
15902
15903 let diff_base = r#"
15904 use some::mod1;
15905 use some::mod2;
15906
15907 const A: u32 = 42;
15908
15909 fn main() {
15910 println!("hello");
15911
15912 println!("world");
15913 }
15914 "#
15915 .unindent();
15916 executor.run_until_parked();
15917 cx.set_state(
15918 &r#"
15919 use some::mod1;
15920 use some::mod2;
15921
15922 const A: u32 = 42;
15923 const B: u32 = 42;
15924 const C: u32 = 42;
15925 ˇ
15926
15927 fn main() {
15928 println!("hello");
15929
15930 println!("world");
15931 }
15932 "#
15933 .unindent(),
15934 );
15935
15936 cx.set_head_text(&diff_base);
15937 executor.run_until_parked();
15938
15939 cx.update_editor(|editor, window, cx| {
15940 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15941 });
15942 executor.run_until_parked();
15943
15944 cx.assert_state_with_diff(
15945 r#"
15946 use some::mod1;
15947 use some::mod2;
15948
15949 const A: u32 = 42;
15950 + const B: u32 = 42;
15951 + const C: u32 = 42;
15952 + ˇ
15953
15954 fn main() {
15955 println!("hello");
15956
15957 println!("world");
15958 }
15959 "#
15960 .unindent(),
15961 );
15962
15963 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
15964 executor.run_until_parked();
15965
15966 cx.assert_state_with_diff(
15967 r#"
15968 use some::mod1;
15969 use some::mod2;
15970
15971 const A: u32 = 42;
15972 + const B: u32 = 42;
15973 + const C: u32 = 42;
15974 + const D: u32 = 42;
15975 + ˇ
15976
15977 fn main() {
15978 println!("hello");
15979
15980 println!("world");
15981 }
15982 "#
15983 .unindent(),
15984 );
15985
15986 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
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
16001 fn main() {
16002 println!("hello");
16003
16004 println!("world");
16005 }
16006 "#
16007 .unindent(),
16008 );
16009
16010 cx.update_editor(|editor, window, cx| {
16011 editor.delete_line(&DeleteLine, window, cx);
16012 });
16013 executor.run_until_parked();
16014
16015 cx.assert_state_with_diff(
16016 r#"
16017 use some::mod1;
16018 use some::mod2;
16019
16020 const A: u32 = 42;
16021 + const B: u32 = 42;
16022 + const C: u32 = 42;
16023 + const D: u32 = 42;
16024 + const E: 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.move_up(&MoveUp, window, cx);
16037 editor.delete_line(&DeleteLine, window, cx);
16038 editor.move_up(&MoveUp, window, cx);
16039 editor.delete_line(&DeleteLine, window, cx);
16040 editor.move_up(&MoveUp, window, cx);
16041 editor.delete_line(&DeleteLine, window, cx);
16042 });
16043 executor.run_until_parked();
16044 cx.assert_state_with_diff(
16045 r#"
16046 use some::mod1;
16047 use some::mod2;
16048
16049 const A: u32 = 42;
16050 + const B: u32 = 42;
16051 ˇ
16052 fn main() {
16053 println!("hello");
16054
16055 println!("world");
16056 }
16057 "#
16058 .unindent(),
16059 );
16060
16061 cx.update_editor(|editor, window, cx| {
16062 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
16063 editor.delete_line(&DeleteLine, window, cx);
16064 });
16065 executor.run_until_parked();
16066 cx.assert_state_with_diff(
16067 r#"
16068 ˇ
16069 fn main() {
16070 println!("hello");
16071
16072 println!("world");
16073 }
16074 "#
16075 .unindent(),
16076 );
16077}
16078
16079#[gpui::test]
16080async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
16081 init_test(cx, |_| {});
16082
16083 let mut cx = EditorTestContext::new(cx).await;
16084 cx.set_head_text(indoc! { "
16085 one
16086 two
16087 three
16088 four
16089 five
16090 "
16091 });
16092 cx.set_state(indoc! { "
16093 one
16094 ˇthree
16095 five
16096 "});
16097 cx.run_until_parked();
16098 cx.update_editor(|editor, window, cx| {
16099 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
16100 });
16101 cx.assert_state_with_diff(
16102 indoc! { "
16103 one
16104 - two
16105 ˇthree
16106 - four
16107 five
16108 "}
16109 .to_string(),
16110 );
16111 cx.update_editor(|editor, window, cx| {
16112 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
16113 });
16114
16115 cx.assert_state_with_diff(
16116 indoc! { "
16117 one
16118 ˇthree
16119 five
16120 "}
16121 .to_string(),
16122 );
16123
16124 cx.set_state(indoc! { "
16125 one
16126 ˇTWO
16127 three
16128 four
16129 five
16130 "});
16131 cx.run_until_parked();
16132 cx.update_editor(|editor, window, cx| {
16133 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
16134 });
16135
16136 cx.assert_state_with_diff(
16137 indoc! { "
16138 one
16139 - two
16140 + ˇTWO
16141 three
16142 four
16143 five
16144 "}
16145 .to_string(),
16146 );
16147 cx.update_editor(|editor, window, cx| {
16148 editor.move_up(&Default::default(), window, cx);
16149 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
16150 });
16151 cx.assert_state_with_diff(
16152 indoc! { "
16153 one
16154 ˇTWO
16155 three
16156 four
16157 five
16158 "}
16159 .to_string(),
16160 );
16161}
16162
16163#[gpui::test]
16164async fn test_edits_around_expanded_deletion_hunks(
16165 executor: BackgroundExecutor,
16166 cx: &mut TestAppContext,
16167) {
16168 init_test(cx, |_| {});
16169
16170 let mut cx = EditorTestContext::new(cx).await;
16171
16172 let diff_base = r#"
16173 use some::mod1;
16174 use some::mod2;
16175
16176 const A: u32 = 42;
16177 const B: u32 = 42;
16178 const C: u32 = 42;
16179
16180
16181 fn main() {
16182 println!("hello");
16183
16184 println!("world");
16185 }
16186 "#
16187 .unindent();
16188 executor.run_until_parked();
16189 cx.set_state(
16190 &r#"
16191 use some::mod1;
16192 use some::mod2;
16193
16194 ˇconst B: u32 = 42;
16195 const C: u32 = 42;
16196
16197
16198 fn main() {
16199 println!("hello");
16200
16201 println!("world");
16202 }
16203 "#
16204 .unindent(),
16205 );
16206
16207 cx.set_head_text(&diff_base);
16208 executor.run_until_parked();
16209
16210 cx.update_editor(|editor, window, cx| {
16211 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16212 });
16213 executor.run_until_parked();
16214
16215 cx.assert_state_with_diff(
16216 r#"
16217 use some::mod1;
16218 use some::mod2;
16219
16220 - const A: u32 = 42;
16221 ˇconst B: u32 = 42;
16222 const C: u32 = 42;
16223
16224
16225 fn main() {
16226 println!("hello");
16227
16228 println!("world");
16229 }
16230 "#
16231 .unindent(),
16232 );
16233
16234 cx.update_editor(|editor, window, cx| {
16235 editor.delete_line(&DeleteLine, window, cx);
16236 });
16237 executor.run_until_parked();
16238 cx.assert_state_with_diff(
16239 r#"
16240 use some::mod1;
16241 use some::mod2;
16242
16243 - const A: u32 = 42;
16244 - const B: u32 = 42;
16245 ˇconst C: u32 = 42;
16246
16247
16248 fn main() {
16249 println!("hello");
16250
16251 println!("world");
16252 }
16253 "#
16254 .unindent(),
16255 );
16256
16257 cx.update_editor(|editor, window, cx| {
16258 editor.delete_line(&DeleteLine, window, cx);
16259 });
16260 executor.run_until_parked();
16261 cx.assert_state_with_diff(
16262 r#"
16263 use some::mod1;
16264 use some::mod2;
16265
16266 - const A: u32 = 42;
16267 - const B: u32 = 42;
16268 - const C: u32 = 42;
16269 ˇ
16270
16271 fn main() {
16272 println!("hello");
16273
16274 println!("world");
16275 }
16276 "#
16277 .unindent(),
16278 );
16279
16280 cx.update_editor(|editor, window, cx| {
16281 editor.handle_input("replacement", window, cx);
16282 });
16283 executor.run_until_parked();
16284 cx.assert_state_with_diff(
16285 r#"
16286 use some::mod1;
16287 use some::mod2;
16288
16289 - const A: u32 = 42;
16290 - const B: u32 = 42;
16291 - const C: u32 = 42;
16292 -
16293 + replacementˇ
16294
16295 fn main() {
16296 println!("hello");
16297
16298 println!("world");
16299 }
16300 "#
16301 .unindent(),
16302 );
16303}
16304
16305#[gpui::test]
16306async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16307 init_test(cx, |_| {});
16308
16309 let mut cx = EditorTestContext::new(cx).await;
16310
16311 let base_text = r#"
16312 one
16313 two
16314 three
16315 four
16316 five
16317 "#
16318 .unindent();
16319 executor.run_until_parked();
16320 cx.set_state(
16321 &r#"
16322 one
16323 two
16324 fˇour
16325 five
16326 "#
16327 .unindent(),
16328 );
16329
16330 cx.set_head_text(&base_text);
16331 executor.run_until_parked();
16332
16333 cx.update_editor(|editor, window, cx| {
16334 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16335 });
16336 executor.run_until_parked();
16337
16338 cx.assert_state_with_diff(
16339 r#"
16340 one
16341 two
16342 - three
16343 fˇour
16344 five
16345 "#
16346 .unindent(),
16347 );
16348
16349 cx.update_editor(|editor, window, cx| {
16350 editor.backspace(&Backspace, window, cx);
16351 editor.backspace(&Backspace, window, cx);
16352 });
16353 executor.run_until_parked();
16354 cx.assert_state_with_diff(
16355 r#"
16356 one
16357 two
16358 - threeˇ
16359 - four
16360 + our
16361 five
16362 "#
16363 .unindent(),
16364 );
16365}
16366
16367#[gpui::test]
16368async fn test_edit_after_expanded_modification_hunk(
16369 executor: BackgroundExecutor,
16370 cx: &mut TestAppContext,
16371) {
16372 init_test(cx, |_| {});
16373
16374 let mut cx = EditorTestContext::new(cx).await;
16375
16376 let diff_base = r#"
16377 use some::mod1;
16378 use some::mod2;
16379
16380 const A: u32 = 42;
16381 const B: u32 = 42;
16382 const C: u32 = 42;
16383 const D: u32 = 42;
16384
16385
16386 fn main() {
16387 println!("hello");
16388
16389 println!("world");
16390 }"#
16391 .unindent();
16392
16393 cx.set_state(
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 = 43ˇ
16401 const D: u32 = 42;
16402
16403
16404 fn main() {
16405 println!("hello");
16406
16407 println!("world");
16408 }"#
16409 .unindent(),
16410 );
16411
16412 cx.set_head_text(&diff_base);
16413 executor.run_until_parked();
16414 cx.update_editor(|editor, window, cx| {
16415 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16416 });
16417 executor.run_until_parked();
16418
16419 cx.assert_state_with_diff(
16420 r#"
16421 use some::mod1;
16422 use some::mod2;
16423
16424 const A: u32 = 42;
16425 const B: u32 = 42;
16426 - const C: u32 = 42;
16427 + const C: u32 = 43ˇ
16428 const D: u32 = 42;
16429
16430
16431 fn main() {
16432 println!("hello");
16433
16434 println!("world");
16435 }"#
16436 .unindent(),
16437 );
16438
16439 cx.update_editor(|editor, window, cx| {
16440 editor.handle_input("\nnew_line\n", window, cx);
16441 });
16442 executor.run_until_parked();
16443
16444 cx.assert_state_with_diff(
16445 r#"
16446 use some::mod1;
16447 use some::mod2;
16448
16449 const A: u32 = 42;
16450 const B: u32 = 42;
16451 - const C: u32 = 42;
16452 + const C: u32 = 43
16453 + new_line
16454 + ˇ
16455 const D: u32 = 42;
16456
16457
16458 fn main() {
16459 println!("hello");
16460
16461 println!("world");
16462 }"#
16463 .unindent(),
16464 );
16465}
16466
16467#[gpui::test]
16468async fn test_stage_and_unstage_added_file_hunk(
16469 executor: BackgroundExecutor,
16470 cx: &mut TestAppContext,
16471) {
16472 init_test(cx, |_| {});
16473
16474 let mut cx = EditorTestContext::new(cx).await;
16475 cx.update_editor(|editor, _, cx| {
16476 editor.set_expand_all_diff_hunks(cx);
16477 });
16478
16479 let working_copy = r#"
16480 ˇfn main() {
16481 println!("hello, world!");
16482 }
16483 "#
16484 .unindent();
16485
16486 cx.set_state(&working_copy);
16487 executor.run_until_parked();
16488
16489 cx.assert_state_with_diff(
16490 r#"
16491 + ˇfn main() {
16492 + println!("hello, world!");
16493 + }
16494 "#
16495 .unindent(),
16496 );
16497 cx.assert_index_text(None);
16498
16499 cx.update_editor(|editor, window, cx| {
16500 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16501 });
16502 executor.run_until_parked();
16503 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
16504 cx.assert_state_with_diff(
16505 r#"
16506 + ˇfn main() {
16507 + println!("hello, world!");
16508 + }
16509 "#
16510 .unindent(),
16511 );
16512
16513 cx.update_editor(|editor, window, cx| {
16514 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16515 });
16516 executor.run_until_parked();
16517 cx.assert_index_text(None);
16518}
16519
16520async fn setup_indent_guides_editor(
16521 text: &str,
16522 cx: &mut TestAppContext,
16523) -> (BufferId, EditorTestContext) {
16524 init_test(cx, |_| {});
16525
16526 let mut cx = EditorTestContext::new(cx).await;
16527
16528 let buffer_id = cx.update_editor(|editor, window, cx| {
16529 editor.set_text(text, window, cx);
16530 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
16531
16532 buffer_ids[0]
16533 });
16534
16535 (buffer_id, cx)
16536}
16537
16538fn assert_indent_guides(
16539 range: Range<u32>,
16540 expected: Vec<IndentGuide>,
16541 active_indices: Option<Vec<usize>>,
16542 cx: &mut EditorTestContext,
16543) {
16544 let indent_guides = cx.update_editor(|editor, window, cx| {
16545 let snapshot = editor.snapshot(window, cx).display_snapshot;
16546 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
16547 editor,
16548 MultiBufferRow(range.start)..MultiBufferRow(range.end),
16549 true,
16550 &snapshot,
16551 cx,
16552 );
16553
16554 indent_guides.sort_by(|a, b| {
16555 a.depth.cmp(&b.depth).then(
16556 a.start_row
16557 .cmp(&b.start_row)
16558 .then(a.end_row.cmp(&b.end_row)),
16559 )
16560 });
16561 indent_guides
16562 });
16563
16564 if let Some(expected) = active_indices {
16565 let active_indices = cx.update_editor(|editor, window, cx| {
16566 let snapshot = editor.snapshot(window, cx).display_snapshot;
16567 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
16568 });
16569
16570 assert_eq!(
16571 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
16572 expected,
16573 "Active indent guide indices do not match"
16574 );
16575 }
16576
16577 assert_eq!(indent_guides, expected, "Indent guides do not match");
16578}
16579
16580fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
16581 IndentGuide {
16582 buffer_id,
16583 start_row: MultiBufferRow(start_row),
16584 end_row: MultiBufferRow(end_row),
16585 depth,
16586 tab_size: 4,
16587 settings: IndentGuideSettings {
16588 enabled: true,
16589 line_width: 1,
16590 active_line_width: 1,
16591 ..Default::default()
16592 },
16593 }
16594}
16595
16596#[gpui::test]
16597async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
16598 let (buffer_id, mut cx) = setup_indent_guides_editor(
16599 &"
16600 fn main() {
16601 let a = 1;
16602 }"
16603 .unindent(),
16604 cx,
16605 )
16606 .await;
16607
16608 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
16609}
16610
16611#[gpui::test]
16612async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
16613 let (buffer_id, mut cx) = setup_indent_guides_editor(
16614 &"
16615 fn main() {
16616 let a = 1;
16617 let b = 2;
16618 }"
16619 .unindent(),
16620 cx,
16621 )
16622 .await;
16623
16624 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
16625}
16626
16627#[gpui::test]
16628async fn test_indent_guide_nested(cx: &mut TestAppContext) {
16629 let (buffer_id, mut cx) = setup_indent_guides_editor(
16630 &"
16631 fn main() {
16632 let a = 1;
16633 if a == 3 {
16634 let b = 2;
16635 } else {
16636 let c = 3;
16637 }
16638 }"
16639 .unindent(),
16640 cx,
16641 )
16642 .await;
16643
16644 assert_indent_guides(
16645 0..8,
16646 vec![
16647 indent_guide(buffer_id, 1, 6, 0),
16648 indent_guide(buffer_id, 3, 3, 1),
16649 indent_guide(buffer_id, 5, 5, 1),
16650 ],
16651 None,
16652 &mut cx,
16653 );
16654}
16655
16656#[gpui::test]
16657async fn test_indent_guide_tab(cx: &mut TestAppContext) {
16658 let (buffer_id, mut cx) = setup_indent_guides_editor(
16659 &"
16660 fn main() {
16661 let a = 1;
16662 let b = 2;
16663 let c = 3;
16664 }"
16665 .unindent(),
16666 cx,
16667 )
16668 .await;
16669
16670 assert_indent_guides(
16671 0..5,
16672 vec![
16673 indent_guide(buffer_id, 1, 3, 0),
16674 indent_guide(buffer_id, 2, 2, 1),
16675 ],
16676 None,
16677 &mut cx,
16678 );
16679}
16680
16681#[gpui::test]
16682async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
16683 let (buffer_id, mut cx) = setup_indent_guides_editor(
16684 &"
16685 fn main() {
16686 let a = 1;
16687
16688 let c = 3;
16689 }"
16690 .unindent(),
16691 cx,
16692 )
16693 .await;
16694
16695 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
16696}
16697
16698#[gpui::test]
16699async fn test_indent_guide_complex(cx: &mut TestAppContext) {
16700 let (buffer_id, mut cx) = setup_indent_guides_editor(
16701 &"
16702 fn main() {
16703 let a = 1;
16704
16705 let c = 3;
16706
16707 if a == 3 {
16708 let b = 2;
16709 } else {
16710 let c = 3;
16711 }
16712 }"
16713 .unindent(),
16714 cx,
16715 )
16716 .await;
16717
16718 assert_indent_guides(
16719 0..11,
16720 vec![
16721 indent_guide(buffer_id, 1, 9, 0),
16722 indent_guide(buffer_id, 6, 6, 1),
16723 indent_guide(buffer_id, 8, 8, 1),
16724 ],
16725 None,
16726 &mut cx,
16727 );
16728}
16729
16730#[gpui::test]
16731async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
16732 let (buffer_id, mut cx) = setup_indent_guides_editor(
16733 &"
16734 fn main() {
16735 let a = 1;
16736
16737 let c = 3;
16738
16739 if a == 3 {
16740 let b = 2;
16741 } else {
16742 let c = 3;
16743 }
16744 }"
16745 .unindent(),
16746 cx,
16747 )
16748 .await;
16749
16750 assert_indent_guides(
16751 1..11,
16752 vec![
16753 indent_guide(buffer_id, 1, 9, 0),
16754 indent_guide(buffer_id, 6, 6, 1),
16755 indent_guide(buffer_id, 8, 8, 1),
16756 ],
16757 None,
16758 &mut cx,
16759 );
16760}
16761
16762#[gpui::test]
16763async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
16764 let (buffer_id, mut cx) = setup_indent_guides_editor(
16765 &"
16766 fn main() {
16767 let a = 1;
16768
16769 let c = 3;
16770
16771 if a == 3 {
16772 let b = 2;
16773 } else {
16774 let c = 3;
16775 }
16776 }"
16777 .unindent(),
16778 cx,
16779 )
16780 .await;
16781
16782 assert_indent_guides(
16783 1..10,
16784 vec![
16785 indent_guide(buffer_id, 1, 9, 0),
16786 indent_guide(buffer_id, 6, 6, 1),
16787 indent_guide(buffer_id, 8, 8, 1),
16788 ],
16789 None,
16790 &mut cx,
16791 );
16792}
16793
16794#[gpui::test]
16795async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
16796 let (buffer_id, mut cx) = setup_indent_guides_editor(
16797 &"
16798 block1
16799 block2
16800 block3
16801 block4
16802 block2
16803 block1
16804 block1"
16805 .unindent(),
16806 cx,
16807 )
16808 .await;
16809
16810 assert_indent_guides(
16811 1..10,
16812 vec![
16813 indent_guide(buffer_id, 1, 4, 0),
16814 indent_guide(buffer_id, 2, 3, 1),
16815 indent_guide(buffer_id, 3, 3, 2),
16816 ],
16817 None,
16818 &mut cx,
16819 );
16820}
16821
16822#[gpui::test]
16823async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
16824 let (buffer_id, mut cx) = setup_indent_guides_editor(
16825 &"
16826 block1
16827 block2
16828 block3
16829
16830 block1
16831 block1"
16832 .unindent(),
16833 cx,
16834 )
16835 .await;
16836
16837 assert_indent_guides(
16838 0..6,
16839 vec![
16840 indent_guide(buffer_id, 1, 2, 0),
16841 indent_guide(buffer_id, 2, 2, 1),
16842 ],
16843 None,
16844 &mut cx,
16845 );
16846}
16847
16848#[gpui::test]
16849async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
16850 let (buffer_id, mut cx) = setup_indent_guides_editor(
16851 &"
16852 block1
16853
16854
16855
16856 block2
16857 "
16858 .unindent(),
16859 cx,
16860 )
16861 .await;
16862
16863 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
16864}
16865
16866#[gpui::test]
16867async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
16868 let (buffer_id, mut cx) = setup_indent_guides_editor(
16869 &"
16870 def a:
16871 \tb = 3
16872 \tif True:
16873 \t\tc = 4
16874 \t\td = 5
16875 \tprint(b)
16876 "
16877 .unindent(),
16878 cx,
16879 )
16880 .await;
16881
16882 assert_indent_guides(
16883 0..6,
16884 vec![
16885 indent_guide(buffer_id, 1, 5, 0),
16886 indent_guide(buffer_id, 3, 4, 1),
16887 ],
16888 None,
16889 &mut cx,
16890 );
16891}
16892
16893#[gpui::test]
16894async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
16895 let (buffer_id, mut cx) = setup_indent_guides_editor(
16896 &"
16897 fn main() {
16898 let a = 1;
16899 }"
16900 .unindent(),
16901 cx,
16902 )
16903 .await;
16904
16905 cx.update_editor(|editor, window, cx| {
16906 editor.change_selections(None, window, cx, |s| {
16907 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
16908 });
16909 });
16910
16911 assert_indent_guides(
16912 0..3,
16913 vec![indent_guide(buffer_id, 1, 1, 0)],
16914 Some(vec![0]),
16915 &mut cx,
16916 );
16917}
16918
16919#[gpui::test]
16920async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
16921 let (buffer_id, mut cx) = setup_indent_guides_editor(
16922 &"
16923 fn main() {
16924 if 1 == 2 {
16925 let a = 1;
16926 }
16927 }"
16928 .unindent(),
16929 cx,
16930 )
16931 .await;
16932
16933 cx.update_editor(|editor, window, cx| {
16934 editor.change_selections(None, window, cx, |s| {
16935 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
16936 });
16937 });
16938
16939 assert_indent_guides(
16940 0..4,
16941 vec![
16942 indent_guide(buffer_id, 1, 3, 0),
16943 indent_guide(buffer_id, 2, 2, 1),
16944 ],
16945 Some(vec![1]),
16946 &mut cx,
16947 );
16948
16949 cx.update_editor(|editor, window, cx| {
16950 editor.change_selections(None, window, cx, |s| {
16951 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
16952 });
16953 });
16954
16955 assert_indent_guides(
16956 0..4,
16957 vec![
16958 indent_guide(buffer_id, 1, 3, 0),
16959 indent_guide(buffer_id, 2, 2, 1),
16960 ],
16961 Some(vec![1]),
16962 &mut cx,
16963 );
16964
16965 cx.update_editor(|editor, window, cx| {
16966 editor.change_selections(None, window, cx, |s| {
16967 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
16968 });
16969 });
16970
16971 assert_indent_guides(
16972 0..4,
16973 vec![
16974 indent_guide(buffer_id, 1, 3, 0),
16975 indent_guide(buffer_id, 2, 2, 1),
16976 ],
16977 Some(vec![0]),
16978 &mut cx,
16979 );
16980}
16981
16982#[gpui::test]
16983async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
16984 let (buffer_id, mut cx) = setup_indent_guides_editor(
16985 &"
16986 fn main() {
16987 let a = 1;
16988
16989 let b = 2;
16990 }"
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(2, 0)..Point::new(2, 0)])
16999 });
17000 });
17001
17002 assert_indent_guides(
17003 0..5,
17004 vec![indent_guide(buffer_id, 1, 3, 0)],
17005 Some(vec![0]),
17006 &mut cx,
17007 );
17008}
17009
17010#[gpui::test]
17011async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
17012 let (buffer_id, mut cx) = setup_indent_guides_editor(
17013 &"
17014 def m:
17015 a = 1
17016 pass"
17017 .unindent(),
17018 cx,
17019 )
17020 .await;
17021
17022 cx.update_editor(|editor, window, cx| {
17023 editor.change_selections(None, window, cx, |s| {
17024 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
17025 });
17026 });
17027
17028 assert_indent_guides(
17029 0..3,
17030 vec![indent_guide(buffer_id, 1, 2, 0)],
17031 Some(vec![0]),
17032 &mut cx,
17033 );
17034}
17035
17036#[gpui::test]
17037async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
17038 init_test(cx, |_| {});
17039 let mut cx = EditorTestContext::new(cx).await;
17040 let text = indoc! {
17041 "
17042 impl A {
17043 fn b() {
17044 0;
17045 3;
17046 5;
17047 6;
17048 7;
17049 }
17050 }
17051 "
17052 };
17053 let base_text = indoc! {
17054 "
17055 impl A {
17056 fn b() {
17057 0;
17058 1;
17059 2;
17060 3;
17061 4;
17062 }
17063 fn c() {
17064 5;
17065 6;
17066 7;
17067 }
17068 }
17069 "
17070 };
17071
17072 cx.update_editor(|editor, window, cx| {
17073 editor.set_text(text, window, cx);
17074
17075 editor.buffer().update(cx, |multibuffer, cx| {
17076 let buffer = multibuffer.as_singleton().unwrap();
17077 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
17078
17079 multibuffer.set_all_diff_hunks_expanded(cx);
17080 multibuffer.add_diff(diff, cx);
17081
17082 buffer.read(cx).remote_id()
17083 })
17084 });
17085 cx.run_until_parked();
17086
17087 cx.assert_state_with_diff(
17088 indoc! { "
17089 impl A {
17090 fn b() {
17091 0;
17092 - 1;
17093 - 2;
17094 3;
17095 - 4;
17096 - }
17097 - fn c() {
17098 5;
17099 6;
17100 7;
17101 }
17102 }
17103 ˇ"
17104 }
17105 .to_string(),
17106 );
17107
17108 let mut actual_guides = cx.update_editor(|editor, window, cx| {
17109 editor
17110 .snapshot(window, cx)
17111 .buffer_snapshot
17112 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
17113 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
17114 .collect::<Vec<_>>()
17115 });
17116 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
17117 assert_eq!(
17118 actual_guides,
17119 vec![
17120 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
17121 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
17122 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
17123 ]
17124 );
17125}
17126
17127#[gpui::test]
17128async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17129 init_test(cx, |_| {});
17130 let mut cx = EditorTestContext::new(cx).await;
17131
17132 let diff_base = r#"
17133 a
17134 b
17135 c
17136 "#
17137 .unindent();
17138
17139 cx.set_state(
17140 &r#"
17141 ˇA
17142 b
17143 C
17144 "#
17145 .unindent(),
17146 );
17147 cx.set_head_text(&diff_base);
17148 cx.update_editor(|editor, window, cx| {
17149 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17150 });
17151 executor.run_until_parked();
17152
17153 let both_hunks_expanded = r#"
17154 - a
17155 + ˇA
17156 b
17157 - c
17158 + C
17159 "#
17160 .unindent();
17161
17162 cx.assert_state_with_diff(both_hunks_expanded.clone());
17163
17164 let hunk_ranges = cx.update_editor(|editor, window, cx| {
17165 let snapshot = editor.snapshot(window, cx);
17166 let hunks = editor
17167 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
17168 .collect::<Vec<_>>();
17169 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
17170 let buffer_id = hunks[0].buffer_id;
17171 hunks
17172 .into_iter()
17173 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
17174 .collect::<Vec<_>>()
17175 });
17176 assert_eq!(hunk_ranges.len(), 2);
17177
17178 cx.update_editor(|editor, _, cx| {
17179 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
17180 });
17181 executor.run_until_parked();
17182
17183 let second_hunk_expanded = r#"
17184 ˇA
17185 b
17186 - c
17187 + C
17188 "#
17189 .unindent();
17190
17191 cx.assert_state_with_diff(second_hunk_expanded);
17192
17193 cx.update_editor(|editor, _, cx| {
17194 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
17195 });
17196 executor.run_until_parked();
17197
17198 cx.assert_state_with_diff(both_hunks_expanded.clone());
17199
17200 cx.update_editor(|editor, _, cx| {
17201 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
17202 });
17203 executor.run_until_parked();
17204
17205 let first_hunk_expanded = r#"
17206 - a
17207 + ˇA
17208 b
17209 C
17210 "#
17211 .unindent();
17212
17213 cx.assert_state_with_diff(first_hunk_expanded);
17214
17215 cx.update_editor(|editor, _, cx| {
17216 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
17217 });
17218 executor.run_until_parked();
17219
17220 cx.assert_state_with_diff(both_hunks_expanded);
17221
17222 cx.set_state(
17223 &r#"
17224 ˇA
17225 b
17226 "#
17227 .unindent(),
17228 );
17229 cx.run_until_parked();
17230
17231 // TODO this cursor position seems bad
17232 cx.assert_state_with_diff(
17233 r#"
17234 - ˇa
17235 + A
17236 b
17237 "#
17238 .unindent(),
17239 );
17240
17241 cx.update_editor(|editor, window, cx| {
17242 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17243 });
17244
17245 cx.assert_state_with_diff(
17246 r#"
17247 - ˇa
17248 + A
17249 b
17250 - c
17251 "#
17252 .unindent(),
17253 );
17254
17255 let hunk_ranges = cx.update_editor(|editor, window, cx| {
17256 let snapshot = editor.snapshot(window, cx);
17257 let hunks = editor
17258 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
17259 .collect::<Vec<_>>();
17260 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
17261 let buffer_id = hunks[0].buffer_id;
17262 hunks
17263 .into_iter()
17264 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
17265 .collect::<Vec<_>>()
17266 });
17267 assert_eq!(hunk_ranges.len(), 2);
17268
17269 cx.update_editor(|editor, _, cx| {
17270 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
17271 });
17272 executor.run_until_parked();
17273
17274 cx.assert_state_with_diff(
17275 r#"
17276 - ˇa
17277 + A
17278 b
17279 "#
17280 .unindent(),
17281 );
17282}
17283
17284#[gpui::test]
17285async fn test_toggle_deletion_hunk_at_start_of_file(
17286 executor: BackgroundExecutor,
17287 cx: &mut TestAppContext,
17288) {
17289 init_test(cx, |_| {});
17290 let mut cx = EditorTestContext::new(cx).await;
17291
17292 let diff_base = r#"
17293 a
17294 b
17295 c
17296 "#
17297 .unindent();
17298
17299 cx.set_state(
17300 &r#"
17301 ˇb
17302 c
17303 "#
17304 .unindent(),
17305 );
17306 cx.set_head_text(&diff_base);
17307 cx.update_editor(|editor, window, cx| {
17308 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17309 });
17310 executor.run_until_parked();
17311
17312 let hunk_expanded = r#"
17313 - a
17314 ˇb
17315 c
17316 "#
17317 .unindent();
17318
17319 cx.assert_state_with_diff(hunk_expanded.clone());
17320
17321 let hunk_ranges = cx.update_editor(|editor, window, cx| {
17322 let snapshot = editor.snapshot(window, cx);
17323 let hunks = editor
17324 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
17325 .collect::<Vec<_>>();
17326 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
17327 let buffer_id = hunks[0].buffer_id;
17328 hunks
17329 .into_iter()
17330 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
17331 .collect::<Vec<_>>()
17332 });
17333 assert_eq!(hunk_ranges.len(), 1);
17334
17335 cx.update_editor(|editor, _, cx| {
17336 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
17337 });
17338 executor.run_until_parked();
17339
17340 let hunk_collapsed = r#"
17341 ˇb
17342 c
17343 "#
17344 .unindent();
17345
17346 cx.assert_state_with_diff(hunk_collapsed);
17347
17348 cx.update_editor(|editor, _, cx| {
17349 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
17350 });
17351 executor.run_until_parked();
17352
17353 cx.assert_state_with_diff(hunk_expanded.clone());
17354}
17355
17356#[gpui::test]
17357async fn test_display_diff_hunks(cx: &mut TestAppContext) {
17358 init_test(cx, |_| {});
17359
17360 let fs = FakeFs::new(cx.executor());
17361 fs.insert_tree(
17362 path!("/test"),
17363 json!({
17364 ".git": {},
17365 "file-1": "ONE\n",
17366 "file-2": "TWO\n",
17367 "file-3": "THREE\n",
17368 }),
17369 )
17370 .await;
17371
17372 fs.set_head_for_repo(
17373 path!("/test/.git").as_ref(),
17374 &[
17375 ("file-1".into(), "one\n".into()),
17376 ("file-2".into(), "two\n".into()),
17377 ("file-3".into(), "three\n".into()),
17378 ],
17379 );
17380
17381 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
17382 let mut buffers = vec![];
17383 for i in 1..=3 {
17384 let buffer = project
17385 .update(cx, |project, cx| {
17386 let path = format!(path!("/test/file-{}"), i);
17387 project.open_local_buffer(path, cx)
17388 })
17389 .await
17390 .unwrap();
17391 buffers.push(buffer);
17392 }
17393
17394 let multibuffer = cx.new(|cx| {
17395 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
17396 multibuffer.set_all_diff_hunks_expanded(cx);
17397 for buffer in &buffers {
17398 let snapshot = buffer.read(cx).snapshot();
17399 multibuffer.set_excerpts_for_path(
17400 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
17401 buffer.clone(),
17402 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
17403 DEFAULT_MULTIBUFFER_CONTEXT,
17404 cx,
17405 );
17406 }
17407 multibuffer
17408 });
17409
17410 let editor = cx.add_window(|window, cx| {
17411 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
17412 });
17413 cx.run_until_parked();
17414
17415 let snapshot = editor
17416 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
17417 .unwrap();
17418 let hunks = snapshot
17419 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
17420 .map(|hunk| match hunk {
17421 DisplayDiffHunk::Unfolded {
17422 display_row_range, ..
17423 } => display_row_range,
17424 DisplayDiffHunk::Folded { .. } => unreachable!(),
17425 })
17426 .collect::<Vec<_>>();
17427 assert_eq!(
17428 hunks,
17429 [
17430 DisplayRow(2)..DisplayRow(4),
17431 DisplayRow(7)..DisplayRow(9),
17432 DisplayRow(12)..DisplayRow(14),
17433 ]
17434 );
17435}
17436
17437#[gpui::test]
17438async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
17439 init_test(cx, |_| {});
17440
17441 let mut cx = EditorTestContext::new(cx).await;
17442 cx.set_head_text(indoc! { "
17443 one
17444 two
17445 three
17446 four
17447 five
17448 "
17449 });
17450 cx.set_index_text(indoc! { "
17451 one
17452 two
17453 three
17454 four
17455 five
17456 "
17457 });
17458 cx.set_state(indoc! {"
17459 one
17460 TWO
17461 ˇTHREE
17462 FOUR
17463 five
17464 "});
17465 cx.run_until_parked();
17466 cx.update_editor(|editor, window, cx| {
17467 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
17468 });
17469 cx.run_until_parked();
17470 cx.assert_index_text(Some(indoc! {"
17471 one
17472 TWO
17473 THREE
17474 FOUR
17475 five
17476 "}));
17477 cx.set_state(indoc! { "
17478 one
17479 TWO
17480 ˇTHREE-HUNDRED
17481 FOUR
17482 five
17483 "});
17484 cx.run_until_parked();
17485 cx.update_editor(|editor, window, cx| {
17486 let snapshot = editor.snapshot(window, cx);
17487 let hunks = editor
17488 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
17489 .collect::<Vec<_>>();
17490 assert_eq!(hunks.len(), 1);
17491 assert_eq!(
17492 hunks[0].status(),
17493 DiffHunkStatus {
17494 kind: DiffHunkStatusKind::Modified,
17495 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
17496 }
17497 );
17498
17499 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
17500 });
17501 cx.run_until_parked();
17502 cx.assert_index_text(Some(indoc! {"
17503 one
17504 TWO
17505 THREE-HUNDRED
17506 FOUR
17507 five
17508 "}));
17509}
17510
17511#[gpui::test]
17512fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
17513 init_test(cx, |_| {});
17514
17515 let editor = cx.add_window(|window, cx| {
17516 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
17517 build_editor(buffer, window, cx)
17518 });
17519
17520 let render_args = Arc::new(Mutex::new(None));
17521 let snapshot = editor
17522 .update(cx, |editor, window, cx| {
17523 let snapshot = editor.buffer().read(cx).snapshot(cx);
17524 let range =
17525 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
17526
17527 struct RenderArgs {
17528 row: MultiBufferRow,
17529 folded: bool,
17530 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
17531 }
17532
17533 let crease = Crease::inline(
17534 range,
17535 FoldPlaceholder::test(),
17536 {
17537 let toggle_callback = render_args.clone();
17538 move |row, folded, callback, _window, _cx| {
17539 *toggle_callback.lock() = Some(RenderArgs {
17540 row,
17541 folded,
17542 callback,
17543 });
17544 div()
17545 }
17546 },
17547 |_row, _folded, _window, _cx| div(),
17548 );
17549
17550 editor.insert_creases(Some(crease), cx);
17551 let snapshot = editor.snapshot(window, cx);
17552 let _div = snapshot.render_crease_toggle(
17553 MultiBufferRow(1),
17554 false,
17555 cx.entity().clone(),
17556 window,
17557 cx,
17558 );
17559 snapshot
17560 })
17561 .unwrap();
17562
17563 let render_args = render_args.lock().take().unwrap();
17564 assert_eq!(render_args.row, MultiBufferRow(1));
17565 assert!(!render_args.folded);
17566 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
17567
17568 cx.update_window(*editor, |_, window, cx| {
17569 (render_args.callback)(true, window, cx)
17570 })
17571 .unwrap();
17572 let snapshot = editor
17573 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
17574 .unwrap();
17575 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
17576
17577 cx.update_window(*editor, |_, window, cx| {
17578 (render_args.callback)(false, window, cx)
17579 })
17580 .unwrap();
17581 let snapshot = editor
17582 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
17583 .unwrap();
17584 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
17585}
17586
17587#[gpui::test]
17588async fn test_input_text(cx: &mut TestAppContext) {
17589 init_test(cx, |_| {});
17590 let mut cx = EditorTestContext::new(cx).await;
17591
17592 cx.set_state(
17593 &r#"ˇone
17594 two
17595
17596 three
17597 fourˇ
17598 five
17599
17600 siˇx"#
17601 .unindent(),
17602 );
17603
17604 cx.dispatch_action(HandleInput(String::new()));
17605 cx.assert_editor_state(
17606 &r#"ˇone
17607 two
17608
17609 three
17610 fourˇ
17611 five
17612
17613 siˇx"#
17614 .unindent(),
17615 );
17616
17617 cx.dispatch_action(HandleInput("AAAA".to_string()));
17618 cx.assert_editor_state(
17619 &r#"AAAAˇone
17620 two
17621
17622 three
17623 fourAAAAˇ
17624 five
17625
17626 siAAAAˇx"#
17627 .unindent(),
17628 );
17629}
17630
17631#[gpui::test]
17632async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
17633 init_test(cx, |_| {});
17634
17635 let mut cx = EditorTestContext::new(cx).await;
17636 cx.set_state(
17637 r#"let foo = 1;
17638let foo = 2;
17639let foo = 3;
17640let fooˇ = 4;
17641let foo = 5;
17642let foo = 6;
17643let foo = 7;
17644let foo = 8;
17645let foo = 9;
17646let foo = 10;
17647let foo = 11;
17648let foo = 12;
17649let foo = 13;
17650let foo = 14;
17651let foo = 15;"#,
17652 );
17653
17654 cx.update_editor(|e, window, cx| {
17655 assert_eq!(
17656 e.next_scroll_position,
17657 NextScrollCursorCenterTopBottom::Center,
17658 "Default next scroll direction is center",
17659 );
17660
17661 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17662 assert_eq!(
17663 e.next_scroll_position,
17664 NextScrollCursorCenterTopBottom::Top,
17665 "After center, next scroll direction should be top",
17666 );
17667
17668 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17669 assert_eq!(
17670 e.next_scroll_position,
17671 NextScrollCursorCenterTopBottom::Bottom,
17672 "After top, next scroll direction should be bottom",
17673 );
17674
17675 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17676 assert_eq!(
17677 e.next_scroll_position,
17678 NextScrollCursorCenterTopBottom::Center,
17679 "After bottom, scrolling should start over",
17680 );
17681
17682 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17683 assert_eq!(
17684 e.next_scroll_position,
17685 NextScrollCursorCenterTopBottom::Top,
17686 "Scrolling continues if retriggered fast enough"
17687 );
17688 });
17689
17690 cx.executor()
17691 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
17692 cx.executor().run_until_parked();
17693 cx.update_editor(|e, _, _| {
17694 assert_eq!(
17695 e.next_scroll_position,
17696 NextScrollCursorCenterTopBottom::Center,
17697 "If scrolling is not triggered fast enough, it should reset"
17698 );
17699 });
17700}
17701
17702#[gpui::test]
17703async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
17704 init_test(cx, |_| {});
17705 let mut cx = EditorLspTestContext::new_rust(
17706 lsp::ServerCapabilities {
17707 definition_provider: Some(lsp::OneOf::Left(true)),
17708 references_provider: Some(lsp::OneOf::Left(true)),
17709 ..lsp::ServerCapabilities::default()
17710 },
17711 cx,
17712 )
17713 .await;
17714
17715 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
17716 let go_to_definition = cx
17717 .lsp
17718 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
17719 move |params, _| async move {
17720 if empty_go_to_definition {
17721 Ok(None)
17722 } else {
17723 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
17724 uri: params.text_document_position_params.text_document.uri,
17725 range: lsp::Range::new(
17726 lsp::Position::new(4, 3),
17727 lsp::Position::new(4, 6),
17728 ),
17729 })))
17730 }
17731 },
17732 );
17733 let references = cx
17734 .lsp
17735 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
17736 Ok(Some(vec![lsp::Location {
17737 uri: params.text_document_position.text_document.uri,
17738 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
17739 }]))
17740 });
17741 (go_to_definition, references)
17742 };
17743
17744 cx.set_state(
17745 &r#"fn one() {
17746 let mut a = ˇtwo();
17747 }
17748
17749 fn two() {}"#
17750 .unindent(),
17751 );
17752 set_up_lsp_handlers(false, &mut cx);
17753 let navigated = cx
17754 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
17755 .await
17756 .expect("Failed to navigate to definition");
17757 assert_eq!(
17758 navigated,
17759 Navigated::Yes,
17760 "Should have navigated to definition from the GetDefinition response"
17761 );
17762 cx.assert_editor_state(
17763 &r#"fn one() {
17764 let mut a = two();
17765 }
17766
17767 fn «twoˇ»() {}"#
17768 .unindent(),
17769 );
17770
17771 let editors = cx.update_workspace(|workspace, _, cx| {
17772 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
17773 });
17774 cx.update_editor(|_, _, test_editor_cx| {
17775 assert_eq!(
17776 editors.len(),
17777 1,
17778 "Initially, only one, test, editor should be open in the workspace"
17779 );
17780 assert_eq!(
17781 test_editor_cx.entity(),
17782 editors.last().expect("Asserted len is 1").clone()
17783 );
17784 });
17785
17786 set_up_lsp_handlers(true, &mut cx);
17787 let navigated = cx
17788 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
17789 .await
17790 .expect("Failed to navigate to lookup references");
17791 assert_eq!(
17792 navigated,
17793 Navigated::Yes,
17794 "Should have navigated to references as a fallback after empty GoToDefinition response"
17795 );
17796 // We should not change the selections in the existing file,
17797 // if opening another milti buffer with the references
17798 cx.assert_editor_state(
17799 &r#"fn one() {
17800 let mut a = two();
17801 }
17802
17803 fn «twoˇ»() {}"#
17804 .unindent(),
17805 );
17806 let editors = cx.update_workspace(|workspace, _, cx| {
17807 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
17808 });
17809 cx.update_editor(|_, _, test_editor_cx| {
17810 assert_eq!(
17811 editors.len(),
17812 2,
17813 "After falling back to references search, we open a new editor with the results"
17814 );
17815 let references_fallback_text = editors
17816 .into_iter()
17817 .find(|new_editor| *new_editor != test_editor_cx.entity())
17818 .expect("Should have one non-test editor now")
17819 .read(test_editor_cx)
17820 .text(test_editor_cx);
17821 assert_eq!(
17822 references_fallback_text, "fn one() {\n let mut a = two();\n}",
17823 "Should use the range from the references response and not the GoToDefinition one"
17824 );
17825 });
17826}
17827
17828#[gpui::test]
17829async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
17830 init_test(cx, |_| {});
17831 cx.update(|cx| {
17832 let mut editor_settings = EditorSettings::get_global(cx).clone();
17833 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
17834 EditorSettings::override_global(editor_settings, cx);
17835 });
17836 let mut cx = EditorLspTestContext::new_rust(
17837 lsp::ServerCapabilities {
17838 definition_provider: Some(lsp::OneOf::Left(true)),
17839 references_provider: Some(lsp::OneOf::Left(true)),
17840 ..lsp::ServerCapabilities::default()
17841 },
17842 cx,
17843 )
17844 .await;
17845 let original_state = r#"fn one() {
17846 let mut a = ˇtwo();
17847 }
17848
17849 fn two() {}"#
17850 .unindent();
17851 cx.set_state(&original_state);
17852
17853 let mut go_to_definition = cx
17854 .lsp
17855 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
17856 move |_, _| async move { Ok(None) },
17857 );
17858 let _references = cx
17859 .lsp
17860 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
17861 panic!("Should not call for references with no go to definition fallback")
17862 });
17863
17864 let navigated = cx
17865 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
17866 .await
17867 .expect("Failed to navigate to lookup references");
17868 go_to_definition
17869 .next()
17870 .await
17871 .expect("Should have called the go_to_definition handler");
17872
17873 assert_eq!(
17874 navigated,
17875 Navigated::No,
17876 "Should have navigated to references as a fallback after empty GoToDefinition response"
17877 );
17878 cx.assert_editor_state(&original_state);
17879 let editors = cx.update_workspace(|workspace, _, cx| {
17880 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
17881 });
17882 cx.update_editor(|_, _, _| {
17883 assert_eq!(
17884 editors.len(),
17885 1,
17886 "After unsuccessful fallback, no other editor should have been opened"
17887 );
17888 });
17889}
17890
17891#[gpui::test]
17892async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
17893 init_test(cx, |_| {});
17894
17895 let language = Arc::new(Language::new(
17896 LanguageConfig::default(),
17897 Some(tree_sitter_rust::LANGUAGE.into()),
17898 ));
17899
17900 let text = r#"
17901 #[cfg(test)]
17902 mod tests() {
17903 #[test]
17904 fn runnable_1() {
17905 let a = 1;
17906 }
17907
17908 #[test]
17909 fn runnable_2() {
17910 let a = 1;
17911 let b = 2;
17912 }
17913 }
17914 "#
17915 .unindent();
17916
17917 let fs = FakeFs::new(cx.executor());
17918 fs.insert_file("/file.rs", Default::default()).await;
17919
17920 let project = Project::test(fs, ["/a".as_ref()], cx).await;
17921 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17922 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17923 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
17924 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
17925
17926 let editor = cx.new_window_entity(|window, cx| {
17927 Editor::new(
17928 EditorMode::full(),
17929 multi_buffer,
17930 Some(project.clone()),
17931 window,
17932 cx,
17933 )
17934 });
17935
17936 editor.update_in(cx, |editor, window, cx| {
17937 let snapshot = editor.buffer().read(cx).snapshot(cx);
17938 editor.tasks.insert(
17939 (buffer.read(cx).remote_id(), 3),
17940 RunnableTasks {
17941 templates: vec![],
17942 offset: snapshot.anchor_before(43),
17943 column: 0,
17944 extra_variables: HashMap::default(),
17945 context_range: BufferOffset(43)..BufferOffset(85),
17946 },
17947 );
17948 editor.tasks.insert(
17949 (buffer.read(cx).remote_id(), 8),
17950 RunnableTasks {
17951 templates: vec![],
17952 offset: snapshot.anchor_before(86),
17953 column: 0,
17954 extra_variables: HashMap::default(),
17955 context_range: BufferOffset(86)..BufferOffset(191),
17956 },
17957 );
17958
17959 // Test finding task when cursor is inside function body
17960 editor.change_selections(None, window, cx, |s| {
17961 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
17962 });
17963 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
17964 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
17965
17966 // Test finding task when cursor is on function name
17967 editor.change_selections(None, window, cx, |s| {
17968 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
17969 });
17970 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
17971 assert_eq!(row, 8, "Should find task when cursor is on function name");
17972 });
17973}
17974
17975#[gpui::test]
17976async fn test_folding_buffers(cx: &mut TestAppContext) {
17977 init_test(cx, |_| {});
17978
17979 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
17980 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
17981 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
17982
17983 let fs = FakeFs::new(cx.executor());
17984 fs.insert_tree(
17985 path!("/a"),
17986 json!({
17987 "first.rs": sample_text_1,
17988 "second.rs": sample_text_2,
17989 "third.rs": sample_text_3,
17990 }),
17991 )
17992 .await;
17993 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17994 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17995 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17996 let worktree = project.update(cx, |project, cx| {
17997 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
17998 assert_eq!(worktrees.len(), 1);
17999 worktrees.pop().unwrap()
18000 });
18001 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
18002
18003 let buffer_1 = project
18004 .update(cx, |project, cx| {
18005 project.open_buffer((worktree_id, "first.rs"), cx)
18006 })
18007 .await
18008 .unwrap();
18009 let buffer_2 = project
18010 .update(cx, |project, cx| {
18011 project.open_buffer((worktree_id, "second.rs"), cx)
18012 })
18013 .await
18014 .unwrap();
18015 let buffer_3 = project
18016 .update(cx, |project, cx| {
18017 project.open_buffer((worktree_id, "third.rs"), cx)
18018 })
18019 .await
18020 .unwrap();
18021
18022 let multi_buffer = cx.new(|cx| {
18023 let mut multi_buffer = MultiBuffer::new(ReadWrite);
18024 multi_buffer.push_excerpts(
18025 buffer_1.clone(),
18026 [
18027 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18028 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18029 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18030 ],
18031 cx,
18032 );
18033 multi_buffer.push_excerpts(
18034 buffer_2.clone(),
18035 [
18036 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18037 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18038 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18039 ],
18040 cx,
18041 );
18042 multi_buffer.push_excerpts(
18043 buffer_3.clone(),
18044 [
18045 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18046 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18047 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18048 ],
18049 cx,
18050 );
18051 multi_buffer
18052 });
18053 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18054 Editor::new(
18055 EditorMode::full(),
18056 multi_buffer.clone(),
18057 Some(project.clone()),
18058 window,
18059 cx,
18060 )
18061 });
18062
18063 assert_eq!(
18064 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18065 "\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",
18066 );
18067
18068 multi_buffer_editor.update(cx, |editor, cx| {
18069 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
18070 });
18071 assert_eq!(
18072 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18073 "\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",
18074 "After folding the first buffer, its text should not be displayed"
18075 );
18076
18077 multi_buffer_editor.update(cx, |editor, cx| {
18078 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
18079 });
18080 assert_eq!(
18081 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18082 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
18083 "After folding the second buffer, its text should not be displayed"
18084 );
18085
18086 multi_buffer_editor.update(cx, |editor, cx| {
18087 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
18088 });
18089 assert_eq!(
18090 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18091 "\n\n\n\n\n",
18092 "After folding the third buffer, its text should not be displayed"
18093 );
18094
18095 // Emulate selection inside the fold logic, that should work
18096 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18097 editor
18098 .snapshot(window, cx)
18099 .next_line_boundary(Point::new(0, 4));
18100 });
18101
18102 multi_buffer_editor.update(cx, |editor, cx| {
18103 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
18104 });
18105 assert_eq!(
18106 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18107 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
18108 "After unfolding the second buffer, its text should be displayed"
18109 );
18110
18111 // Typing inside of buffer 1 causes that buffer to be unfolded.
18112 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18113 assert_eq!(
18114 multi_buffer
18115 .read(cx)
18116 .snapshot(cx)
18117 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
18118 .collect::<String>(),
18119 "bbbb"
18120 );
18121 editor.change_selections(None, window, cx, |selections| {
18122 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
18123 });
18124 editor.handle_input("B", window, cx);
18125 });
18126
18127 assert_eq!(
18128 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18129 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
18130 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
18131 );
18132
18133 multi_buffer_editor.update(cx, |editor, cx| {
18134 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
18135 });
18136 assert_eq!(
18137 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18138 "\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",
18139 "After unfolding the all buffers, all original text should be displayed"
18140 );
18141}
18142
18143#[gpui::test]
18144async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
18145 init_test(cx, |_| {});
18146
18147 let sample_text_1 = "1111\n2222\n3333".to_string();
18148 let sample_text_2 = "4444\n5555\n6666".to_string();
18149 let sample_text_3 = "7777\n8888\n9999".to_string();
18150
18151 let fs = FakeFs::new(cx.executor());
18152 fs.insert_tree(
18153 path!("/a"),
18154 json!({
18155 "first.rs": sample_text_1,
18156 "second.rs": sample_text_2,
18157 "third.rs": sample_text_3,
18158 }),
18159 )
18160 .await;
18161 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18162 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18163 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18164 let worktree = project.update(cx, |project, cx| {
18165 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
18166 assert_eq!(worktrees.len(), 1);
18167 worktrees.pop().unwrap()
18168 });
18169 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
18170
18171 let buffer_1 = project
18172 .update(cx, |project, cx| {
18173 project.open_buffer((worktree_id, "first.rs"), cx)
18174 })
18175 .await
18176 .unwrap();
18177 let buffer_2 = project
18178 .update(cx, |project, cx| {
18179 project.open_buffer((worktree_id, "second.rs"), cx)
18180 })
18181 .await
18182 .unwrap();
18183 let buffer_3 = project
18184 .update(cx, |project, cx| {
18185 project.open_buffer((worktree_id, "third.rs"), cx)
18186 })
18187 .await
18188 .unwrap();
18189
18190 let multi_buffer = cx.new(|cx| {
18191 let mut multi_buffer = MultiBuffer::new(ReadWrite);
18192 multi_buffer.push_excerpts(
18193 buffer_1.clone(),
18194 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
18195 cx,
18196 );
18197 multi_buffer.push_excerpts(
18198 buffer_2.clone(),
18199 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
18200 cx,
18201 );
18202 multi_buffer.push_excerpts(
18203 buffer_3.clone(),
18204 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
18205 cx,
18206 );
18207 multi_buffer
18208 });
18209
18210 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18211 Editor::new(
18212 EditorMode::full(),
18213 multi_buffer,
18214 Some(project.clone()),
18215 window,
18216 cx,
18217 )
18218 });
18219
18220 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
18221 assert_eq!(
18222 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18223 full_text,
18224 );
18225
18226 multi_buffer_editor.update(cx, |editor, cx| {
18227 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
18228 });
18229 assert_eq!(
18230 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18231 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
18232 "After folding the first buffer, its text should not be displayed"
18233 );
18234
18235 multi_buffer_editor.update(cx, |editor, cx| {
18236 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
18237 });
18238
18239 assert_eq!(
18240 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18241 "\n\n\n\n\n\n7777\n8888\n9999",
18242 "After folding the second buffer, its text should not be displayed"
18243 );
18244
18245 multi_buffer_editor.update(cx, |editor, cx| {
18246 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
18247 });
18248 assert_eq!(
18249 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18250 "\n\n\n\n\n",
18251 "After folding the third buffer, its text should not be displayed"
18252 );
18253
18254 multi_buffer_editor.update(cx, |editor, cx| {
18255 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
18256 });
18257 assert_eq!(
18258 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18259 "\n\n\n\n4444\n5555\n6666\n\n",
18260 "After unfolding the second buffer, its text should be displayed"
18261 );
18262
18263 multi_buffer_editor.update(cx, |editor, cx| {
18264 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
18265 });
18266 assert_eq!(
18267 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18268 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
18269 "After unfolding the first buffer, its text should be displayed"
18270 );
18271
18272 multi_buffer_editor.update(cx, |editor, cx| {
18273 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
18274 });
18275 assert_eq!(
18276 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18277 full_text,
18278 "After unfolding all buffers, all original text should be displayed"
18279 );
18280}
18281
18282#[gpui::test]
18283async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
18284 init_test(cx, |_| {});
18285
18286 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
18287
18288 let fs = FakeFs::new(cx.executor());
18289 fs.insert_tree(
18290 path!("/a"),
18291 json!({
18292 "main.rs": sample_text,
18293 }),
18294 )
18295 .await;
18296 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18297 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18298 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18299 let worktree = project.update(cx, |project, cx| {
18300 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
18301 assert_eq!(worktrees.len(), 1);
18302 worktrees.pop().unwrap()
18303 });
18304 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
18305
18306 let buffer_1 = project
18307 .update(cx, |project, cx| {
18308 project.open_buffer((worktree_id, "main.rs"), cx)
18309 })
18310 .await
18311 .unwrap();
18312
18313 let multi_buffer = cx.new(|cx| {
18314 let mut multi_buffer = MultiBuffer::new(ReadWrite);
18315 multi_buffer.push_excerpts(
18316 buffer_1.clone(),
18317 [ExcerptRange::new(
18318 Point::new(0, 0)
18319 ..Point::new(
18320 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
18321 0,
18322 ),
18323 )],
18324 cx,
18325 );
18326 multi_buffer
18327 });
18328 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18329 Editor::new(
18330 EditorMode::full(),
18331 multi_buffer,
18332 Some(project.clone()),
18333 window,
18334 cx,
18335 )
18336 });
18337
18338 let selection_range = Point::new(1, 0)..Point::new(2, 0);
18339 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18340 enum TestHighlight {}
18341 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
18342 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
18343 editor.highlight_text::<TestHighlight>(
18344 vec![highlight_range.clone()],
18345 HighlightStyle::color(Hsla::green()),
18346 cx,
18347 );
18348 editor.change_selections(None, window, cx, |s| s.select_ranges(Some(highlight_range)));
18349 });
18350
18351 let full_text = format!("\n\n{sample_text}");
18352 assert_eq!(
18353 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18354 full_text,
18355 );
18356}
18357
18358#[gpui::test]
18359async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
18360 init_test(cx, |_| {});
18361 cx.update(|cx| {
18362 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
18363 "keymaps/default-linux.json",
18364 cx,
18365 )
18366 .unwrap();
18367 cx.bind_keys(default_key_bindings);
18368 });
18369
18370 let (editor, cx) = cx.add_window_view(|window, cx| {
18371 let multi_buffer = MultiBuffer::build_multi(
18372 [
18373 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
18374 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
18375 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
18376 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
18377 ],
18378 cx,
18379 );
18380 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
18381
18382 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
18383 // fold all but the second buffer, so that we test navigating between two
18384 // adjacent folded buffers, as well as folded buffers at the start and
18385 // end the multibuffer
18386 editor.fold_buffer(buffer_ids[0], cx);
18387 editor.fold_buffer(buffer_ids[2], cx);
18388 editor.fold_buffer(buffer_ids[3], cx);
18389
18390 editor
18391 });
18392 cx.simulate_resize(size(px(1000.), px(1000.)));
18393
18394 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
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 cx.simulate_keystroke("down");
18434 cx.assert_excerpts_with_selections(indoc! {"
18435 [EXCERPT]
18436 [FOLDED]
18437 [EXCERPT]
18438 a1
18439 b1
18440 ˇ[EXCERPT]
18441 [FOLDED]
18442 [EXCERPT]
18443 [FOLDED]
18444 "
18445 });
18446 cx.simulate_keystroke("down");
18447 cx.assert_excerpts_with_selections(indoc! {"
18448 [EXCERPT]
18449 [FOLDED]
18450 [EXCERPT]
18451 a1
18452 b1
18453 [EXCERPT]
18454 ˇ[FOLDED]
18455 [EXCERPT]
18456 [FOLDED]
18457 "
18458 });
18459 for _ in 0..5 {
18460 cx.simulate_keystroke("down");
18461 cx.assert_excerpts_with_selections(indoc! {"
18462 [EXCERPT]
18463 [FOLDED]
18464 [EXCERPT]
18465 a1
18466 b1
18467 [EXCERPT]
18468 [FOLDED]
18469 [EXCERPT]
18470 ˇ[FOLDED]
18471 "
18472 });
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 cx.simulate_keystroke("up");
18502 cx.assert_excerpts_with_selections(indoc! {"
18503 [EXCERPT]
18504 [FOLDED]
18505 [EXCERPT]
18506 a1
18507 ˇb1
18508 [EXCERPT]
18509 [FOLDED]
18510 [EXCERPT]
18511 [FOLDED]
18512 "
18513 });
18514 cx.simulate_keystroke("up");
18515 cx.assert_excerpts_with_selections(indoc! {"
18516 [EXCERPT]
18517 [FOLDED]
18518 [EXCERPT]
18519 ˇa1
18520 b1
18521 [EXCERPT]
18522 [FOLDED]
18523 [EXCERPT]
18524 [FOLDED]
18525 "
18526 });
18527 for _ in 0..5 {
18528 cx.simulate_keystroke("up");
18529 cx.assert_excerpts_with_selections(indoc! {"
18530 [EXCERPT]
18531 ˇ[FOLDED]
18532 [EXCERPT]
18533 a1
18534 b1
18535 [EXCERPT]
18536 [FOLDED]
18537 [EXCERPT]
18538 [FOLDED]
18539 "
18540 });
18541 }
18542}
18543
18544#[gpui::test]
18545async fn test_inline_completion_text(cx: &mut TestAppContext) {
18546 init_test(cx, |_| {});
18547
18548 // Simple insertion
18549 assert_highlighted_edits(
18550 "Hello, world!",
18551 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
18552 true,
18553 cx,
18554 |highlighted_edits, cx| {
18555 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
18556 assert_eq!(highlighted_edits.highlights.len(), 1);
18557 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
18558 assert_eq!(
18559 highlighted_edits.highlights[0].1.background_color,
18560 Some(cx.theme().status().created_background)
18561 );
18562 },
18563 )
18564 .await;
18565
18566 // Replacement
18567 assert_highlighted_edits(
18568 "This is a test.",
18569 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
18570 false,
18571 cx,
18572 |highlighted_edits, cx| {
18573 assert_eq!(highlighted_edits.text, "That is a test.");
18574 assert_eq!(highlighted_edits.highlights.len(), 1);
18575 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
18576 assert_eq!(
18577 highlighted_edits.highlights[0].1.background_color,
18578 Some(cx.theme().status().created_background)
18579 );
18580 },
18581 )
18582 .await;
18583
18584 // Multiple edits
18585 assert_highlighted_edits(
18586 "Hello, world!",
18587 vec![
18588 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
18589 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
18590 ],
18591 false,
18592 cx,
18593 |highlighted_edits, cx| {
18594 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
18595 assert_eq!(highlighted_edits.highlights.len(), 2);
18596 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
18597 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
18598 assert_eq!(
18599 highlighted_edits.highlights[0].1.background_color,
18600 Some(cx.theme().status().created_background)
18601 );
18602 assert_eq!(
18603 highlighted_edits.highlights[1].1.background_color,
18604 Some(cx.theme().status().created_background)
18605 );
18606 },
18607 )
18608 .await;
18609
18610 // Multiple lines with edits
18611 assert_highlighted_edits(
18612 "First line\nSecond line\nThird line\nFourth line",
18613 vec![
18614 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
18615 (
18616 Point::new(2, 0)..Point::new(2, 10),
18617 "New third line".to_string(),
18618 ),
18619 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
18620 ],
18621 false,
18622 cx,
18623 |highlighted_edits, cx| {
18624 assert_eq!(
18625 highlighted_edits.text,
18626 "Second modified\nNew third line\nFourth updated line"
18627 );
18628 assert_eq!(highlighted_edits.highlights.len(), 3);
18629 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
18630 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
18631 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
18632 for highlight in &highlighted_edits.highlights {
18633 assert_eq!(
18634 highlight.1.background_color,
18635 Some(cx.theme().status().created_background)
18636 );
18637 }
18638 },
18639 )
18640 .await;
18641}
18642
18643#[gpui::test]
18644async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
18645 init_test(cx, |_| {});
18646
18647 // Deletion
18648 assert_highlighted_edits(
18649 "Hello, world!",
18650 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
18651 true,
18652 cx,
18653 |highlighted_edits, cx| {
18654 assert_eq!(highlighted_edits.text, "Hello, world!");
18655 assert_eq!(highlighted_edits.highlights.len(), 1);
18656 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
18657 assert_eq!(
18658 highlighted_edits.highlights[0].1.background_color,
18659 Some(cx.theme().status().deleted_background)
18660 );
18661 },
18662 )
18663 .await;
18664
18665 // Insertion
18666 assert_highlighted_edits(
18667 "Hello, world!",
18668 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
18669 true,
18670 cx,
18671 |highlighted_edits, cx| {
18672 assert_eq!(highlighted_edits.highlights.len(), 1);
18673 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
18674 assert_eq!(
18675 highlighted_edits.highlights[0].1.background_color,
18676 Some(cx.theme().status().created_background)
18677 );
18678 },
18679 )
18680 .await;
18681}
18682
18683async fn assert_highlighted_edits(
18684 text: &str,
18685 edits: Vec<(Range<Point>, String)>,
18686 include_deletions: bool,
18687 cx: &mut TestAppContext,
18688 assertion_fn: impl Fn(HighlightedText, &App),
18689) {
18690 let window = cx.add_window(|window, cx| {
18691 let buffer = MultiBuffer::build_simple(text, cx);
18692 Editor::new(EditorMode::full(), buffer, None, window, cx)
18693 });
18694 let cx = &mut VisualTestContext::from_window(*window, cx);
18695
18696 let (buffer, snapshot) = window
18697 .update(cx, |editor, _window, cx| {
18698 (
18699 editor.buffer().clone(),
18700 editor.buffer().read(cx).snapshot(cx),
18701 )
18702 })
18703 .unwrap();
18704
18705 let edits = edits
18706 .into_iter()
18707 .map(|(range, edit)| {
18708 (
18709 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
18710 edit,
18711 )
18712 })
18713 .collect::<Vec<_>>();
18714
18715 let text_anchor_edits = edits
18716 .clone()
18717 .into_iter()
18718 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
18719 .collect::<Vec<_>>();
18720
18721 let edit_preview = window
18722 .update(cx, |_, _window, cx| {
18723 buffer
18724 .read(cx)
18725 .as_singleton()
18726 .unwrap()
18727 .read(cx)
18728 .preview_edits(text_anchor_edits.into(), cx)
18729 })
18730 .unwrap()
18731 .await;
18732
18733 cx.update(|_window, cx| {
18734 let highlighted_edits = inline_completion_edit_text(
18735 &snapshot.as_singleton().unwrap().2,
18736 &edits,
18737 &edit_preview,
18738 include_deletions,
18739 cx,
18740 );
18741 assertion_fn(highlighted_edits, cx)
18742 });
18743}
18744
18745#[track_caller]
18746fn assert_breakpoint(
18747 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
18748 path: &Arc<Path>,
18749 expected: Vec<(u32, Breakpoint)>,
18750) {
18751 if expected.len() == 0usize {
18752 assert!(!breakpoints.contains_key(path), "{}", path.display());
18753 } else {
18754 let mut breakpoint = breakpoints
18755 .get(path)
18756 .unwrap()
18757 .into_iter()
18758 .map(|breakpoint| {
18759 (
18760 breakpoint.row,
18761 Breakpoint {
18762 message: breakpoint.message.clone(),
18763 state: breakpoint.state,
18764 condition: breakpoint.condition.clone(),
18765 hit_condition: breakpoint.hit_condition.clone(),
18766 },
18767 )
18768 })
18769 .collect::<Vec<_>>();
18770
18771 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
18772
18773 assert_eq!(expected, breakpoint);
18774 }
18775}
18776
18777fn add_log_breakpoint_at_cursor(
18778 editor: &mut Editor,
18779 log_message: &str,
18780 window: &mut Window,
18781 cx: &mut Context<Editor>,
18782) {
18783 let (anchor, bp) = editor
18784 .breakpoints_at_cursors(window, cx)
18785 .first()
18786 .and_then(|(anchor, bp)| {
18787 if let Some(bp) = bp {
18788 Some((*anchor, bp.clone()))
18789 } else {
18790 None
18791 }
18792 })
18793 .unwrap_or_else(|| {
18794 let cursor_position: Point = editor.selections.newest(cx).head();
18795
18796 let breakpoint_position = editor
18797 .snapshot(window, cx)
18798 .display_snapshot
18799 .buffer_snapshot
18800 .anchor_before(Point::new(cursor_position.row, 0));
18801
18802 (breakpoint_position, Breakpoint::new_log(&log_message))
18803 });
18804
18805 editor.edit_breakpoint_at_anchor(
18806 anchor,
18807 bp,
18808 BreakpointEditAction::EditLogMessage(log_message.into()),
18809 cx,
18810 );
18811}
18812
18813#[gpui::test]
18814async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
18815 init_test(cx, |_| {});
18816
18817 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
18818 let fs = FakeFs::new(cx.executor());
18819 fs.insert_tree(
18820 path!("/a"),
18821 json!({
18822 "main.rs": sample_text,
18823 }),
18824 )
18825 .await;
18826 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18827 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18828 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18829
18830 let fs = FakeFs::new(cx.executor());
18831 fs.insert_tree(
18832 path!("/a"),
18833 json!({
18834 "main.rs": sample_text,
18835 }),
18836 )
18837 .await;
18838 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18839 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18840 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18841 let worktree_id = workspace
18842 .update(cx, |workspace, _window, cx| {
18843 workspace.project().update(cx, |project, cx| {
18844 project.worktrees(cx).next().unwrap().read(cx).id()
18845 })
18846 })
18847 .unwrap();
18848
18849 let buffer = project
18850 .update(cx, |project, cx| {
18851 project.open_buffer((worktree_id, "main.rs"), cx)
18852 })
18853 .await
18854 .unwrap();
18855
18856 let (editor, cx) = cx.add_window_view(|window, cx| {
18857 Editor::new(
18858 EditorMode::full(),
18859 MultiBuffer::build_from_buffer(buffer, cx),
18860 Some(project.clone()),
18861 window,
18862 cx,
18863 )
18864 });
18865
18866 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
18867 let abs_path = project.read_with(cx, |project, cx| {
18868 project
18869 .absolute_path(&project_path, cx)
18870 .map(|path_buf| Arc::from(path_buf.to_owned()))
18871 .unwrap()
18872 });
18873
18874 // assert we can add breakpoint on the first line
18875 editor.update_in(cx, |editor, window, cx| {
18876 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18877 editor.move_to_end(&MoveToEnd, window, cx);
18878 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18879 });
18880
18881 let breakpoints = editor.update(cx, |editor, cx| {
18882 editor
18883 .breakpoint_store()
18884 .as_ref()
18885 .unwrap()
18886 .read(cx)
18887 .all_source_breakpoints(cx)
18888 .clone()
18889 });
18890
18891 assert_eq!(1, breakpoints.len());
18892 assert_breakpoint(
18893 &breakpoints,
18894 &abs_path,
18895 vec![
18896 (0, Breakpoint::new_standard()),
18897 (3, Breakpoint::new_standard()),
18898 ],
18899 );
18900
18901 editor.update_in(cx, |editor, window, cx| {
18902 editor.move_to_beginning(&MoveToBeginning, window, cx);
18903 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18904 });
18905
18906 let breakpoints = editor.update(cx, |editor, cx| {
18907 editor
18908 .breakpoint_store()
18909 .as_ref()
18910 .unwrap()
18911 .read(cx)
18912 .all_source_breakpoints(cx)
18913 .clone()
18914 });
18915
18916 assert_eq!(1, breakpoints.len());
18917 assert_breakpoint(
18918 &breakpoints,
18919 &abs_path,
18920 vec![(3, Breakpoint::new_standard())],
18921 );
18922
18923 editor.update_in(cx, |editor, window, cx| {
18924 editor.move_to_end(&MoveToEnd, window, cx);
18925 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18926 });
18927
18928 let breakpoints = editor.update(cx, |editor, cx| {
18929 editor
18930 .breakpoint_store()
18931 .as_ref()
18932 .unwrap()
18933 .read(cx)
18934 .all_source_breakpoints(cx)
18935 .clone()
18936 });
18937
18938 assert_eq!(0, breakpoints.len());
18939 assert_breakpoint(&breakpoints, &abs_path, vec![]);
18940}
18941
18942#[gpui::test]
18943async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
18944 init_test(cx, |_| {});
18945
18946 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
18947
18948 let fs = FakeFs::new(cx.executor());
18949 fs.insert_tree(
18950 path!("/a"),
18951 json!({
18952 "main.rs": sample_text,
18953 }),
18954 )
18955 .await;
18956 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18957 let (workspace, cx) =
18958 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
18959
18960 let worktree_id = workspace.update(cx, |workspace, cx| {
18961 workspace.project().update(cx, |project, cx| {
18962 project.worktrees(cx).next().unwrap().read(cx).id()
18963 })
18964 });
18965
18966 let buffer = project
18967 .update(cx, |project, cx| {
18968 project.open_buffer((worktree_id, "main.rs"), cx)
18969 })
18970 .await
18971 .unwrap();
18972
18973 let (editor, cx) = cx.add_window_view(|window, cx| {
18974 Editor::new(
18975 EditorMode::full(),
18976 MultiBuffer::build_from_buffer(buffer, cx),
18977 Some(project.clone()),
18978 window,
18979 cx,
18980 )
18981 });
18982
18983 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
18984 let abs_path = project.read_with(cx, |project, cx| {
18985 project
18986 .absolute_path(&project_path, cx)
18987 .map(|path_buf| Arc::from(path_buf.to_owned()))
18988 .unwrap()
18989 });
18990
18991 editor.update_in(cx, |editor, window, cx| {
18992 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
18993 });
18994
18995 let breakpoints = editor.update(cx, |editor, cx| {
18996 editor
18997 .breakpoint_store()
18998 .as_ref()
18999 .unwrap()
19000 .read(cx)
19001 .all_source_breakpoints(cx)
19002 .clone()
19003 });
19004
19005 assert_breakpoint(
19006 &breakpoints,
19007 &abs_path,
19008 vec![(0, Breakpoint::new_log("hello world"))],
19009 );
19010
19011 // Removing a log message from a log breakpoint should remove it
19012 editor.update_in(cx, |editor, window, cx| {
19013 add_log_breakpoint_at_cursor(editor, "", window, cx);
19014 });
19015
19016 let breakpoints = editor.update(cx, |editor, cx| {
19017 editor
19018 .breakpoint_store()
19019 .as_ref()
19020 .unwrap()
19021 .read(cx)
19022 .all_source_breakpoints(cx)
19023 .clone()
19024 });
19025
19026 assert_breakpoint(&breakpoints, &abs_path, vec![]);
19027
19028 editor.update_in(cx, |editor, window, cx| {
19029 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19030 editor.move_to_end(&MoveToEnd, window, cx);
19031 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19032 // Not adding a log message to a standard breakpoint shouldn't remove it
19033 add_log_breakpoint_at_cursor(editor, "", window, cx);
19034 });
19035
19036 let breakpoints = editor.update(cx, |editor, cx| {
19037 editor
19038 .breakpoint_store()
19039 .as_ref()
19040 .unwrap()
19041 .read(cx)
19042 .all_source_breakpoints(cx)
19043 .clone()
19044 });
19045
19046 assert_breakpoint(
19047 &breakpoints,
19048 &abs_path,
19049 vec![
19050 (0, Breakpoint::new_standard()),
19051 (3, Breakpoint::new_standard()),
19052 ],
19053 );
19054
19055 editor.update_in(cx, |editor, window, cx| {
19056 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
19057 });
19058
19059 let breakpoints = editor.update(cx, |editor, cx| {
19060 editor
19061 .breakpoint_store()
19062 .as_ref()
19063 .unwrap()
19064 .read(cx)
19065 .all_source_breakpoints(cx)
19066 .clone()
19067 });
19068
19069 assert_breakpoint(
19070 &breakpoints,
19071 &abs_path,
19072 vec![
19073 (0, Breakpoint::new_standard()),
19074 (3, Breakpoint::new_log("hello world")),
19075 ],
19076 );
19077
19078 editor.update_in(cx, |editor, window, cx| {
19079 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
19080 });
19081
19082 let breakpoints = editor.update(cx, |editor, cx| {
19083 editor
19084 .breakpoint_store()
19085 .as_ref()
19086 .unwrap()
19087 .read(cx)
19088 .all_source_breakpoints(cx)
19089 .clone()
19090 });
19091
19092 assert_breakpoint(
19093 &breakpoints,
19094 &abs_path,
19095 vec![
19096 (0, Breakpoint::new_standard()),
19097 (3, Breakpoint::new_log("hello Earth!!")),
19098 ],
19099 );
19100}
19101
19102/// This also tests that Editor::breakpoint_at_cursor_head is working properly
19103/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
19104/// or when breakpoints were placed out of order. This tests for a regression too
19105#[gpui::test]
19106async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
19107 init_test(cx, |_| {});
19108
19109 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
19110 let fs = FakeFs::new(cx.executor());
19111 fs.insert_tree(
19112 path!("/a"),
19113 json!({
19114 "main.rs": sample_text,
19115 }),
19116 )
19117 .await;
19118 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19119 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19120 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19121
19122 let fs = FakeFs::new(cx.executor());
19123 fs.insert_tree(
19124 path!("/a"),
19125 json!({
19126 "main.rs": sample_text,
19127 }),
19128 )
19129 .await;
19130 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19131 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19132 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19133 let worktree_id = workspace
19134 .update(cx, |workspace, _window, cx| {
19135 workspace.project().update(cx, |project, cx| {
19136 project.worktrees(cx).next().unwrap().read(cx).id()
19137 })
19138 })
19139 .unwrap();
19140
19141 let buffer = project
19142 .update(cx, |project, cx| {
19143 project.open_buffer((worktree_id, "main.rs"), cx)
19144 })
19145 .await
19146 .unwrap();
19147
19148 let (editor, cx) = cx.add_window_view(|window, cx| {
19149 Editor::new(
19150 EditorMode::full(),
19151 MultiBuffer::build_from_buffer(buffer, cx),
19152 Some(project.clone()),
19153 window,
19154 cx,
19155 )
19156 });
19157
19158 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
19159 let abs_path = project.read_with(cx, |project, cx| {
19160 project
19161 .absolute_path(&project_path, cx)
19162 .map(|path_buf| Arc::from(path_buf.to_owned()))
19163 .unwrap()
19164 });
19165
19166 // assert we can add breakpoint on the first line
19167 editor.update_in(cx, |editor, window, cx| {
19168 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19169 editor.move_to_end(&MoveToEnd, window, cx);
19170 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19171 editor.move_up(&MoveUp, window, cx);
19172 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19173 });
19174
19175 let breakpoints = editor.update(cx, |editor, cx| {
19176 editor
19177 .breakpoint_store()
19178 .as_ref()
19179 .unwrap()
19180 .read(cx)
19181 .all_source_breakpoints(cx)
19182 .clone()
19183 });
19184
19185 assert_eq!(1, breakpoints.len());
19186 assert_breakpoint(
19187 &breakpoints,
19188 &abs_path,
19189 vec![
19190 (0, Breakpoint::new_standard()),
19191 (2, Breakpoint::new_standard()),
19192 (3, Breakpoint::new_standard()),
19193 ],
19194 );
19195
19196 editor.update_in(cx, |editor, window, cx| {
19197 editor.move_to_beginning(&MoveToBeginning, window, cx);
19198 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
19199 editor.move_to_end(&MoveToEnd, window, cx);
19200 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
19201 // Disabling a breakpoint that doesn't exist should do nothing
19202 editor.move_up(&MoveUp, window, cx);
19203 editor.move_up(&MoveUp, window, cx);
19204 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
19205 });
19206
19207 let breakpoints = editor.update(cx, |editor, cx| {
19208 editor
19209 .breakpoint_store()
19210 .as_ref()
19211 .unwrap()
19212 .read(cx)
19213 .all_source_breakpoints(cx)
19214 .clone()
19215 });
19216
19217 let disable_breakpoint = {
19218 let mut bp = Breakpoint::new_standard();
19219 bp.state = BreakpointState::Disabled;
19220 bp
19221 };
19222
19223 assert_eq!(1, breakpoints.len());
19224 assert_breakpoint(
19225 &breakpoints,
19226 &abs_path,
19227 vec![
19228 (0, disable_breakpoint.clone()),
19229 (2, Breakpoint::new_standard()),
19230 (3, disable_breakpoint.clone()),
19231 ],
19232 );
19233
19234 editor.update_in(cx, |editor, window, cx| {
19235 editor.move_to_beginning(&MoveToBeginning, window, cx);
19236 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
19237 editor.move_to_end(&MoveToEnd, window, cx);
19238 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
19239 editor.move_up(&MoveUp, window, cx);
19240 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
19241 });
19242
19243 let breakpoints = editor.update(cx, |editor, cx| {
19244 editor
19245 .breakpoint_store()
19246 .as_ref()
19247 .unwrap()
19248 .read(cx)
19249 .all_source_breakpoints(cx)
19250 .clone()
19251 });
19252
19253 assert_eq!(1, breakpoints.len());
19254 assert_breakpoint(
19255 &breakpoints,
19256 &abs_path,
19257 vec![
19258 (0, Breakpoint::new_standard()),
19259 (2, disable_breakpoint),
19260 (3, Breakpoint::new_standard()),
19261 ],
19262 );
19263}
19264
19265#[gpui::test]
19266async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
19267 init_test(cx, |_| {});
19268 let capabilities = lsp::ServerCapabilities {
19269 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
19270 prepare_provider: Some(true),
19271 work_done_progress_options: Default::default(),
19272 })),
19273 ..Default::default()
19274 };
19275 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
19276
19277 cx.set_state(indoc! {"
19278 struct Fˇoo {}
19279 "});
19280
19281 cx.update_editor(|editor, _, cx| {
19282 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
19283 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
19284 editor.highlight_background::<DocumentHighlightRead>(
19285 &[highlight_range],
19286 |c| c.editor_document_highlight_read_background,
19287 cx,
19288 );
19289 });
19290
19291 let mut prepare_rename_handler = cx
19292 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
19293 move |_, _, _| async move {
19294 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
19295 start: lsp::Position {
19296 line: 0,
19297 character: 7,
19298 },
19299 end: lsp::Position {
19300 line: 0,
19301 character: 10,
19302 },
19303 })))
19304 },
19305 );
19306 let prepare_rename_task = cx
19307 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
19308 .expect("Prepare rename was not started");
19309 prepare_rename_handler.next().await.unwrap();
19310 prepare_rename_task.await.expect("Prepare rename failed");
19311
19312 let mut rename_handler =
19313 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
19314 let edit = lsp::TextEdit {
19315 range: lsp::Range {
19316 start: lsp::Position {
19317 line: 0,
19318 character: 7,
19319 },
19320 end: lsp::Position {
19321 line: 0,
19322 character: 10,
19323 },
19324 },
19325 new_text: "FooRenamed".to_string(),
19326 };
19327 Ok(Some(lsp::WorkspaceEdit::new(
19328 // Specify the same edit twice
19329 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
19330 )))
19331 });
19332 let rename_task = cx
19333 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
19334 .expect("Confirm rename was not started");
19335 rename_handler.next().await.unwrap();
19336 rename_task.await.expect("Confirm rename failed");
19337 cx.run_until_parked();
19338
19339 // Despite two edits, only one is actually applied as those are identical
19340 cx.assert_editor_state(indoc! {"
19341 struct FooRenamedˇ {}
19342 "});
19343}
19344
19345#[gpui::test]
19346async fn test_rename_without_prepare(cx: &mut TestAppContext) {
19347 init_test(cx, |_| {});
19348 // These capabilities indicate that the server does not support prepare rename.
19349 let capabilities = lsp::ServerCapabilities {
19350 rename_provider: Some(lsp::OneOf::Left(true)),
19351 ..Default::default()
19352 };
19353 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
19354
19355 cx.set_state(indoc! {"
19356 struct Fˇoo {}
19357 "});
19358
19359 cx.update_editor(|editor, _window, cx| {
19360 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
19361 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
19362 editor.highlight_background::<DocumentHighlightRead>(
19363 &[highlight_range],
19364 |c| c.editor_document_highlight_read_background,
19365 cx,
19366 );
19367 });
19368
19369 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
19370 .expect("Prepare rename was not started")
19371 .await
19372 .expect("Prepare rename failed");
19373
19374 let mut rename_handler =
19375 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
19376 let edit = lsp::TextEdit {
19377 range: lsp::Range {
19378 start: lsp::Position {
19379 line: 0,
19380 character: 7,
19381 },
19382 end: lsp::Position {
19383 line: 0,
19384 character: 10,
19385 },
19386 },
19387 new_text: "FooRenamed".to_string(),
19388 };
19389 Ok(Some(lsp::WorkspaceEdit::new(
19390 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
19391 )))
19392 });
19393 let rename_task = cx
19394 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
19395 .expect("Confirm rename was not started");
19396 rename_handler.next().await.unwrap();
19397 rename_task.await.expect("Confirm rename failed");
19398 cx.run_until_parked();
19399
19400 // Correct range is renamed, as `surrounding_word` is used to find it.
19401 cx.assert_editor_state(indoc! {"
19402 struct FooRenamedˇ {}
19403 "});
19404}
19405
19406#[gpui::test]
19407async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
19408 init_test(cx, |_| {});
19409 let mut cx = EditorTestContext::new(cx).await;
19410
19411 let language = Arc::new(
19412 Language::new(
19413 LanguageConfig::default(),
19414 Some(tree_sitter_html::LANGUAGE.into()),
19415 )
19416 .with_brackets_query(
19417 r#"
19418 ("<" @open "/>" @close)
19419 ("</" @open ">" @close)
19420 ("<" @open ">" @close)
19421 ("\"" @open "\"" @close)
19422 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
19423 "#,
19424 )
19425 .unwrap(),
19426 );
19427 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
19428
19429 cx.set_state(indoc! {"
19430 <span>ˇ</span>
19431 "});
19432 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
19433 cx.assert_editor_state(indoc! {"
19434 <span>
19435 ˇ
19436 </span>
19437 "});
19438
19439 cx.set_state(indoc! {"
19440 <span><span></span>ˇ</span>
19441 "});
19442 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
19443 cx.assert_editor_state(indoc! {"
19444 <span><span></span>
19445 ˇ</span>
19446 "});
19447
19448 cx.set_state(indoc! {"
19449 <span>ˇ
19450 </span>
19451 "});
19452 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
19453 cx.assert_editor_state(indoc! {"
19454 <span>
19455 ˇ
19456 </span>
19457 "});
19458}
19459
19460#[gpui::test(iterations = 10)]
19461async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
19462 init_test(cx, |_| {});
19463
19464 let fs = FakeFs::new(cx.executor());
19465 fs.insert_tree(
19466 path!("/dir"),
19467 json!({
19468 "a.ts": "a",
19469 }),
19470 )
19471 .await;
19472
19473 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
19474 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19475 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19476
19477 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
19478 language_registry.add(Arc::new(Language::new(
19479 LanguageConfig {
19480 name: "TypeScript".into(),
19481 matcher: LanguageMatcher {
19482 path_suffixes: vec!["ts".to_string()],
19483 ..Default::default()
19484 },
19485 ..Default::default()
19486 },
19487 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
19488 )));
19489 let mut fake_language_servers = language_registry.register_fake_lsp(
19490 "TypeScript",
19491 FakeLspAdapter {
19492 capabilities: lsp::ServerCapabilities {
19493 code_lens_provider: Some(lsp::CodeLensOptions {
19494 resolve_provider: Some(true),
19495 }),
19496 execute_command_provider: Some(lsp::ExecuteCommandOptions {
19497 commands: vec!["_the/command".to_string()],
19498 ..lsp::ExecuteCommandOptions::default()
19499 }),
19500 ..lsp::ServerCapabilities::default()
19501 },
19502 ..FakeLspAdapter::default()
19503 },
19504 );
19505
19506 let (buffer, _handle) = project
19507 .update(cx, |p, cx| {
19508 p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
19509 })
19510 .await
19511 .unwrap();
19512 cx.executor().run_until_parked();
19513
19514 let fake_server = fake_language_servers.next().await.unwrap();
19515
19516 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
19517 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
19518 drop(buffer_snapshot);
19519 let actions = cx
19520 .update_window(*workspace, |_, window, cx| {
19521 project.code_actions(&buffer, anchor..anchor, window, cx)
19522 })
19523 .unwrap();
19524
19525 fake_server
19526 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
19527 Ok(Some(vec![
19528 lsp::CodeLens {
19529 range: lsp::Range::default(),
19530 command: Some(lsp::Command {
19531 title: "Code lens command".to_owned(),
19532 command: "_the/command".to_owned(),
19533 arguments: None,
19534 }),
19535 data: None,
19536 },
19537 lsp::CodeLens {
19538 range: lsp::Range::default(),
19539 command: Some(lsp::Command {
19540 title: "Command not in capabilities".to_owned(),
19541 command: "not in capabilities".to_owned(),
19542 arguments: None,
19543 }),
19544 data: None,
19545 },
19546 lsp::CodeLens {
19547 range: lsp::Range {
19548 start: lsp::Position {
19549 line: 1,
19550 character: 1,
19551 },
19552 end: lsp::Position {
19553 line: 1,
19554 character: 1,
19555 },
19556 },
19557 command: Some(lsp::Command {
19558 title: "Command not in range".to_owned(),
19559 command: "_the/command".to_owned(),
19560 arguments: None,
19561 }),
19562 data: None,
19563 },
19564 ]))
19565 })
19566 .next()
19567 .await;
19568
19569 let actions = actions.await.unwrap();
19570 assert_eq!(
19571 actions.len(),
19572 1,
19573 "Should have only one valid action for the 0..0 range"
19574 );
19575 let action = actions[0].clone();
19576 let apply = project.update(cx, |project, cx| {
19577 project.apply_code_action(buffer.clone(), action, true, cx)
19578 });
19579
19580 // Resolving the code action does not populate its edits. In absence of
19581 // edits, we must execute the given command.
19582 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
19583 |mut lens, _| async move {
19584 let lens_command = lens.command.as_mut().expect("should have a command");
19585 assert_eq!(lens_command.title, "Code lens command");
19586 lens_command.arguments = Some(vec![json!("the-argument")]);
19587 Ok(lens)
19588 },
19589 );
19590
19591 // While executing the command, the language server sends the editor
19592 // a `workspaceEdit` request.
19593 fake_server
19594 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
19595 let fake = fake_server.clone();
19596 move |params, _| {
19597 assert_eq!(params.command, "_the/command");
19598 let fake = fake.clone();
19599 async move {
19600 fake.server
19601 .request::<lsp::request::ApplyWorkspaceEdit>(
19602 lsp::ApplyWorkspaceEditParams {
19603 label: None,
19604 edit: lsp::WorkspaceEdit {
19605 changes: Some(
19606 [(
19607 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
19608 vec![lsp::TextEdit {
19609 range: lsp::Range::new(
19610 lsp::Position::new(0, 0),
19611 lsp::Position::new(0, 0),
19612 ),
19613 new_text: "X".into(),
19614 }],
19615 )]
19616 .into_iter()
19617 .collect(),
19618 ),
19619 ..Default::default()
19620 },
19621 },
19622 )
19623 .await
19624 .into_response()
19625 .unwrap();
19626 Ok(Some(json!(null)))
19627 }
19628 }
19629 })
19630 .next()
19631 .await;
19632
19633 // Applying the code lens command returns a project transaction containing the edits
19634 // sent by the language server in its `workspaceEdit` request.
19635 let transaction = apply.await.unwrap();
19636 assert!(transaction.0.contains_key(&buffer));
19637 buffer.update(cx, |buffer, cx| {
19638 assert_eq!(buffer.text(), "Xa");
19639 buffer.undo(cx);
19640 assert_eq!(buffer.text(), "a");
19641 });
19642}
19643
19644#[gpui::test]
19645async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
19646 init_test(cx, |_| {});
19647
19648 let fs = FakeFs::new(cx.executor());
19649 let main_text = r#"fn main() {
19650println!("1");
19651println!("2");
19652println!("3");
19653println!("4");
19654println!("5");
19655}"#;
19656 let lib_text = "mod foo {}";
19657 fs.insert_tree(
19658 path!("/a"),
19659 json!({
19660 "lib.rs": lib_text,
19661 "main.rs": main_text,
19662 }),
19663 )
19664 .await;
19665
19666 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19667 let (workspace, cx) =
19668 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
19669 let worktree_id = workspace.update(cx, |workspace, cx| {
19670 workspace.project().update(cx, |project, cx| {
19671 project.worktrees(cx).next().unwrap().read(cx).id()
19672 })
19673 });
19674
19675 let expected_ranges = vec![
19676 Point::new(0, 0)..Point::new(0, 0),
19677 Point::new(1, 0)..Point::new(1, 1),
19678 Point::new(2, 0)..Point::new(2, 2),
19679 Point::new(3, 0)..Point::new(3, 3),
19680 ];
19681
19682 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
19683 let editor_1 = workspace
19684 .update_in(cx, |workspace, window, cx| {
19685 workspace.open_path(
19686 (worktree_id, "main.rs"),
19687 Some(pane_1.downgrade()),
19688 true,
19689 window,
19690 cx,
19691 )
19692 })
19693 .unwrap()
19694 .await
19695 .downcast::<Editor>()
19696 .unwrap();
19697 pane_1.update(cx, |pane, cx| {
19698 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19699 open_editor.update(cx, |editor, cx| {
19700 assert_eq!(
19701 editor.display_text(cx),
19702 main_text,
19703 "Original main.rs text on initial open",
19704 );
19705 assert_eq!(
19706 editor
19707 .selections
19708 .all::<Point>(cx)
19709 .into_iter()
19710 .map(|s| s.range())
19711 .collect::<Vec<_>>(),
19712 vec![Point::zero()..Point::zero()],
19713 "Default selections on initial open",
19714 );
19715 })
19716 });
19717 editor_1.update_in(cx, |editor, window, cx| {
19718 editor.change_selections(None, window, cx, |s| {
19719 s.select_ranges(expected_ranges.clone());
19720 });
19721 });
19722
19723 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
19724 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
19725 });
19726 let editor_2 = workspace
19727 .update_in(cx, |workspace, window, cx| {
19728 workspace.open_path(
19729 (worktree_id, "main.rs"),
19730 Some(pane_2.downgrade()),
19731 true,
19732 window,
19733 cx,
19734 )
19735 })
19736 .unwrap()
19737 .await
19738 .downcast::<Editor>()
19739 .unwrap();
19740 pane_2.update(cx, |pane, cx| {
19741 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19742 open_editor.update(cx, |editor, cx| {
19743 assert_eq!(
19744 editor.display_text(cx),
19745 main_text,
19746 "Original main.rs text on initial open in another panel",
19747 );
19748 assert_eq!(
19749 editor
19750 .selections
19751 .all::<Point>(cx)
19752 .into_iter()
19753 .map(|s| s.range())
19754 .collect::<Vec<_>>(),
19755 vec![Point::zero()..Point::zero()],
19756 "Default selections on initial open in another panel",
19757 );
19758 })
19759 });
19760
19761 editor_2.update_in(cx, |editor, window, cx| {
19762 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
19763 });
19764
19765 let _other_editor_1 = workspace
19766 .update_in(cx, |workspace, window, cx| {
19767 workspace.open_path(
19768 (worktree_id, "lib.rs"),
19769 Some(pane_1.downgrade()),
19770 true,
19771 window,
19772 cx,
19773 )
19774 })
19775 .unwrap()
19776 .await
19777 .downcast::<Editor>()
19778 .unwrap();
19779 pane_1
19780 .update_in(cx, |pane, window, cx| {
19781 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
19782 .unwrap()
19783 })
19784 .await
19785 .unwrap();
19786 drop(editor_1);
19787 pane_1.update(cx, |pane, cx| {
19788 pane.active_item()
19789 .unwrap()
19790 .downcast::<Editor>()
19791 .unwrap()
19792 .update(cx, |editor, cx| {
19793 assert_eq!(
19794 editor.display_text(cx),
19795 lib_text,
19796 "Other file should be open and active",
19797 );
19798 });
19799 assert_eq!(pane.items().count(), 1, "No other editors should be open");
19800 });
19801
19802 let _other_editor_2 = workspace
19803 .update_in(cx, |workspace, window, cx| {
19804 workspace.open_path(
19805 (worktree_id, "lib.rs"),
19806 Some(pane_2.downgrade()),
19807 true,
19808 window,
19809 cx,
19810 )
19811 })
19812 .unwrap()
19813 .await
19814 .downcast::<Editor>()
19815 .unwrap();
19816 pane_2
19817 .update_in(cx, |pane, window, cx| {
19818 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
19819 .unwrap()
19820 })
19821 .await
19822 .unwrap();
19823 drop(editor_2);
19824 pane_2.update(cx, |pane, cx| {
19825 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19826 open_editor.update(cx, |editor, cx| {
19827 assert_eq!(
19828 editor.display_text(cx),
19829 lib_text,
19830 "Other file should be open and active in another panel too",
19831 );
19832 });
19833 assert_eq!(
19834 pane.items().count(),
19835 1,
19836 "No other editors should be open in another pane",
19837 );
19838 });
19839
19840 let _editor_1_reopened = workspace
19841 .update_in(cx, |workspace, window, cx| {
19842 workspace.open_path(
19843 (worktree_id, "main.rs"),
19844 Some(pane_1.downgrade()),
19845 true,
19846 window,
19847 cx,
19848 )
19849 })
19850 .unwrap()
19851 .await
19852 .downcast::<Editor>()
19853 .unwrap();
19854 let _editor_2_reopened = workspace
19855 .update_in(cx, |workspace, window, cx| {
19856 workspace.open_path(
19857 (worktree_id, "main.rs"),
19858 Some(pane_2.downgrade()),
19859 true,
19860 window,
19861 cx,
19862 )
19863 })
19864 .unwrap()
19865 .await
19866 .downcast::<Editor>()
19867 .unwrap();
19868 pane_1.update(cx, |pane, cx| {
19869 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19870 open_editor.update(cx, |editor, cx| {
19871 assert_eq!(
19872 editor.display_text(cx),
19873 main_text,
19874 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
19875 );
19876 assert_eq!(
19877 editor
19878 .selections
19879 .all::<Point>(cx)
19880 .into_iter()
19881 .map(|s| s.range())
19882 .collect::<Vec<_>>(),
19883 expected_ranges,
19884 "Previous editor in the 1st panel had selections and should get them restored on reopen",
19885 );
19886 })
19887 });
19888 pane_2.update(cx, |pane, cx| {
19889 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19890 open_editor.update(cx, |editor, cx| {
19891 assert_eq!(
19892 editor.display_text(cx),
19893 r#"fn main() {
19894⋯rintln!("1");
19895⋯intln!("2");
19896⋯ntln!("3");
19897println!("4");
19898println!("5");
19899}"#,
19900 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
19901 );
19902 assert_eq!(
19903 editor
19904 .selections
19905 .all::<Point>(cx)
19906 .into_iter()
19907 .map(|s| s.range())
19908 .collect::<Vec<_>>(),
19909 vec![Point::zero()..Point::zero()],
19910 "Previous editor in the 2nd pane had no selections changed hence should restore none",
19911 );
19912 })
19913 });
19914}
19915
19916#[gpui::test]
19917async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
19918 init_test(cx, |_| {});
19919
19920 let fs = FakeFs::new(cx.executor());
19921 let main_text = r#"fn main() {
19922println!("1");
19923println!("2");
19924println!("3");
19925println!("4");
19926println!("5");
19927}"#;
19928 let lib_text = "mod foo {}";
19929 fs.insert_tree(
19930 path!("/a"),
19931 json!({
19932 "lib.rs": lib_text,
19933 "main.rs": main_text,
19934 }),
19935 )
19936 .await;
19937
19938 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19939 let (workspace, cx) =
19940 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
19941 let worktree_id = workspace.update(cx, |workspace, cx| {
19942 workspace.project().update(cx, |project, cx| {
19943 project.worktrees(cx).next().unwrap().read(cx).id()
19944 })
19945 });
19946
19947 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
19948 let editor = workspace
19949 .update_in(cx, |workspace, window, cx| {
19950 workspace.open_path(
19951 (worktree_id, "main.rs"),
19952 Some(pane.downgrade()),
19953 true,
19954 window,
19955 cx,
19956 )
19957 })
19958 .unwrap()
19959 .await
19960 .downcast::<Editor>()
19961 .unwrap();
19962 pane.update(cx, |pane, cx| {
19963 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19964 open_editor.update(cx, |editor, cx| {
19965 assert_eq!(
19966 editor.display_text(cx),
19967 main_text,
19968 "Original main.rs text on initial open",
19969 );
19970 })
19971 });
19972 editor.update_in(cx, |editor, window, cx| {
19973 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
19974 });
19975
19976 cx.update_global(|store: &mut SettingsStore, cx| {
19977 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
19978 s.restore_on_file_reopen = Some(false);
19979 });
19980 });
19981 editor.update_in(cx, |editor, window, cx| {
19982 editor.fold_ranges(
19983 vec![
19984 Point::new(1, 0)..Point::new(1, 1),
19985 Point::new(2, 0)..Point::new(2, 2),
19986 Point::new(3, 0)..Point::new(3, 3),
19987 ],
19988 false,
19989 window,
19990 cx,
19991 );
19992 });
19993 pane.update_in(cx, |pane, window, cx| {
19994 pane.close_all_items(&CloseAllItems::default(), window, cx)
19995 .unwrap()
19996 })
19997 .await
19998 .unwrap();
19999 pane.update(cx, |pane, _| {
20000 assert!(pane.active_item().is_none());
20001 });
20002 cx.update_global(|store: &mut SettingsStore, cx| {
20003 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
20004 s.restore_on_file_reopen = Some(true);
20005 });
20006 });
20007
20008 let _editor_reopened = workspace
20009 .update_in(cx, |workspace, window, cx| {
20010 workspace.open_path(
20011 (worktree_id, "main.rs"),
20012 Some(pane.downgrade()),
20013 true,
20014 window,
20015 cx,
20016 )
20017 })
20018 .unwrap()
20019 .await
20020 .downcast::<Editor>()
20021 .unwrap();
20022 pane.update(cx, |pane, cx| {
20023 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20024 open_editor.update(cx, |editor, cx| {
20025 assert_eq!(
20026 editor.display_text(cx),
20027 main_text,
20028 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
20029 );
20030 })
20031 });
20032}
20033
20034#[gpui::test]
20035async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
20036 struct EmptyModalView {
20037 focus_handle: gpui::FocusHandle,
20038 }
20039 impl EventEmitter<DismissEvent> for EmptyModalView {}
20040 impl Render for EmptyModalView {
20041 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
20042 div()
20043 }
20044 }
20045 impl Focusable for EmptyModalView {
20046 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
20047 self.focus_handle.clone()
20048 }
20049 }
20050 impl workspace::ModalView for EmptyModalView {}
20051 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
20052 EmptyModalView {
20053 focus_handle: cx.focus_handle(),
20054 }
20055 }
20056
20057 init_test(cx, |_| {});
20058
20059 let fs = FakeFs::new(cx.executor());
20060 let project = Project::test(fs, [], cx).await;
20061 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20062 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
20063 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20064 let editor = cx.new_window_entity(|window, cx| {
20065 Editor::new(
20066 EditorMode::full(),
20067 buffer,
20068 Some(project.clone()),
20069 window,
20070 cx,
20071 )
20072 });
20073 workspace
20074 .update(cx, |workspace, window, cx| {
20075 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
20076 })
20077 .unwrap();
20078 editor.update_in(cx, |editor, window, cx| {
20079 editor.open_context_menu(&OpenContextMenu, window, cx);
20080 assert!(editor.mouse_context_menu.is_some());
20081 });
20082 workspace
20083 .update(cx, |workspace, window, cx| {
20084 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
20085 })
20086 .unwrap();
20087 cx.read(|cx| {
20088 assert!(editor.read(cx).mouse_context_menu.is_none());
20089 });
20090}
20091
20092#[gpui::test]
20093async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
20094 init_test(cx, |_| {});
20095
20096 let fs = FakeFs::new(cx.executor());
20097 fs.insert_file(path!("/file.html"), Default::default())
20098 .await;
20099
20100 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
20101
20102 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
20103 let html_language = Arc::new(Language::new(
20104 LanguageConfig {
20105 name: "HTML".into(),
20106 matcher: LanguageMatcher {
20107 path_suffixes: vec!["html".to_string()],
20108 ..LanguageMatcher::default()
20109 },
20110 brackets: BracketPairConfig {
20111 pairs: vec![BracketPair {
20112 start: "<".into(),
20113 end: ">".into(),
20114 close: true,
20115 ..Default::default()
20116 }],
20117 ..Default::default()
20118 },
20119 ..Default::default()
20120 },
20121 Some(tree_sitter_html::LANGUAGE.into()),
20122 ));
20123 language_registry.add(html_language);
20124 let mut fake_servers = language_registry.register_fake_lsp(
20125 "HTML",
20126 FakeLspAdapter {
20127 capabilities: lsp::ServerCapabilities {
20128 completion_provider: Some(lsp::CompletionOptions {
20129 resolve_provider: Some(true),
20130 ..Default::default()
20131 }),
20132 ..Default::default()
20133 },
20134 ..Default::default()
20135 },
20136 );
20137
20138 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20139 let cx = &mut VisualTestContext::from_window(*workspace, cx);
20140
20141 let worktree_id = workspace
20142 .update(cx, |workspace, _window, cx| {
20143 workspace.project().update(cx, |project, cx| {
20144 project.worktrees(cx).next().unwrap().read(cx).id()
20145 })
20146 })
20147 .unwrap();
20148 project
20149 .update(cx, |project, cx| {
20150 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
20151 })
20152 .await
20153 .unwrap();
20154 let editor = workspace
20155 .update(cx, |workspace, window, cx| {
20156 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
20157 })
20158 .unwrap()
20159 .await
20160 .unwrap()
20161 .downcast::<Editor>()
20162 .unwrap();
20163
20164 let fake_server = fake_servers.next().await.unwrap();
20165 editor.update_in(cx, |editor, window, cx| {
20166 editor.set_text("<ad></ad>", window, cx);
20167 editor.change_selections(None, window, cx, |selections| {
20168 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
20169 });
20170 let Some((buffer, _)) = editor
20171 .buffer
20172 .read(cx)
20173 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
20174 else {
20175 panic!("Failed to get buffer for selection position");
20176 };
20177 let buffer = buffer.read(cx);
20178 let buffer_id = buffer.remote_id();
20179 let opening_range =
20180 buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3));
20181 let closing_range =
20182 buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
20183 let mut linked_ranges = HashMap::default();
20184 linked_ranges.insert(
20185 buffer_id,
20186 vec![(opening_range.clone(), vec![closing_range.clone()])],
20187 );
20188 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
20189 });
20190 let mut completion_handle =
20191 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
20192 Ok(Some(lsp::CompletionResponse::Array(vec![
20193 lsp::CompletionItem {
20194 label: "head".to_string(),
20195 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
20196 lsp::InsertReplaceEdit {
20197 new_text: "head".to_string(),
20198 insert: lsp::Range::new(
20199 lsp::Position::new(0, 1),
20200 lsp::Position::new(0, 3),
20201 ),
20202 replace: lsp::Range::new(
20203 lsp::Position::new(0, 1),
20204 lsp::Position::new(0, 3),
20205 ),
20206 },
20207 )),
20208 ..Default::default()
20209 },
20210 ])))
20211 });
20212 editor.update_in(cx, |editor, window, cx| {
20213 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
20214 });
20215 cx.run_until_parked();
20216 completion_handle.next().await.unwrap();
20217 editor.update(cx, |editor, _| {
20218 assert!(
20219 editor.context_menu_visible(),
20220 "Completion menu should be visible"
20221 );
20222 });
20223 editor.update_in(cx, |editor, window, cx| {
20224 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
20225 });
20226 cx.executor().run_until_parked();
20227 editor.update(cx, |editor, cx| {
20228 assert_eq!(editor.text(cx), "<head></head>");
20229 });
20230}
20231
20232#[gpui::test]
20233async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
20234 init_test(cx, |_| {});
20235
20236 let fs = FakeFs::new(cx.executor());
20237 fs.insert_tree(
20238 path!("/root"),
20239 json!({
20240 "a": {
20241 "main.rs": "fn main() {}",
20242 },
20243 "foo": {
20244 "bar": {
20245 "external_file.rs": "pub mod external {}",
20246 }
20247 }
20248 }),
20249 )
20250 .await;
20251
20252 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
20253 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
20254 language_registry.add(rust_lang());
20255 let _fake_servers = language_registry.register_fake_lsp(
20256 "Rust",
20257 FakeLspAdapter {
20258 ..FakeLspAdapter::default()
20259 },
20260 );
20261 let (workspace, cx) =
20262 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20263 let worktree_id = workspace.update(cx, |workspace, cx| {
20264 workspace.project().update(cx, |project, cx| {
20265 project.worktrees(cx).next().unwrap().read(cx).id()
20266 })
20267 });
20268
20269 let assert_language_servers_count =
20270 |expected: usize, context: &str, cx: &mut VisualTestContext| {
20271 project.update(cx, |project, cx| {
20272 let current = project
20273 .lsp_store()
20274 .read(cx)
20275 .as_local()
20276 .unwrap()
20277 .language_servers
20278 .len();
20279 assert_eq!(expected, current, "{context}");
20280 });
20281 };
20282
20283 assert_language_servers_count(
20284 0,
20285 "No servers should be running before any file is open",
20286 cx,
20287 );
20288 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
20289 let main_editor = workspace
20290 .update_in(cx, |workspace, window, cx| {
20291 workspace.open_path(
20292 (worktree_id, "main.rs"),
20293 Some(pane.downgrade()),
20294 true,
20295 window,
20296 cx,
20297 )
20298 })
20299 .unwrap()
20300 .await
20301 .downcast::<Editor>()
20302 .unwrap();
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 "fn main() {}",
20309 "Original main.rs text on initial open",
20310 );
20311 });
20312 assert_eq!(open_editor, main_editor);
20313 });
20314 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
20315
20316 let external_editor = workspace
20317 .update_in(cx, |workspace, window, cx| {
20318 workspace.open_abs_path(
20319 PathBuf::from("/root/foo/bar/external_file.rs"),
20320 OpenOptions::default(),
20321 window,
20322 cx,
20323 )
20324 })
20325 .await
20326 .expect("opening external file")
20327 .downcast::<Editor>()
20328 .expect("downcasted external file's open element to editor");
20329 pane.update(cx, |pane, cx| {
20330 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20331 open_editor.update(cx, |editor, cx| {
20332 assert_eq!(
20333 editor.display_text(cx),
20334 "pub mod external {}",
20335 "External file is open now",
20336 );
20337 });
20338 assert_eq!(open_editor, external_editor);
20339 });
20340 assert_language_servers_count(
20341 1,
20342 "Second, external, *.rs file should join the existing server",
20343 cx,
20344 );
20345
20346 pane.update_in(cx, |pane, window, cx| {
20347 pane.close_active_item(&CloseActiveItem::default(), window, cx)
20348 })
20349 .unwrap()
20350 .await
20351 .unwrap();
20352 pane.update_in(cx, |pane, window, cx| {
20353 pane.navigate_backward(window, cx);
20354 });
20355 cx.run_until_parked();
20356 pane.update(cx, |pane, cx| {
20357 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20358 open_editor.update(cx, |editor, cx| {
20359 assert_eq!(
20360 editor.display_text(cx),
20361 "pub mod external {}",
20362 "External file is open now",
20363 );
20364 });
20365 });
20366 assert_language_servers_count(
20367 1,
20368 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
20369 cx,
20370 );
20371
20372 cx.update(|_, cx| {
20373 workspace::reload(&workspace::Reload::default(), cx);
20374 });
20375 assert_language_servers_count(
20376 1,
20377 "After reloading the worktree with local and external files opened, only one project should be started",
20378 cx,
20379 );
20380}
20381
20382#[gpui::test]
20383async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
20384 init_test(cx, |_| {});
20385
20386 let mut cx = EditorTestContext::new(cx).await;
20387 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
20388 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
20389
20390 // test cursor move to start of each line on tab
20391 // for `if`, `elif`, `else`, `while`, `with` and `for`
20392 cx.set_state(indoc! {"
20393 def main():
20394 ˇ for item in items:
20395 ˇ while item.active:
20396 ˇ if item.value > 10:
20397 ˇ continue
20398 ˇ elif item.value < 0:
20399 ˇ break
20400 ˇ else:
20401 ˇ with item.context() as ctx:
20402 ˇ yield count
20403 ˇ else:
20404 ˇ log('while else')
20405 ˇ else:
20406 ˇ log('for else')
20407 "});
20408 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
20409 cx.assert_editor_state(indoc! {"
20410 def main():
20411 ˇfor item in items:
20412 ˇwhile item.active:
20413 ˇif item.value > 10:
20414 ˇcontinue
20415 ˇelif item.value < 0:
20416 ˇbreak
20417 ˇelse:
20418 ˇwith item.context() as ctx:
20419 ˇyield count
20420 ˇelse:
20421 ˇlog('while else')
20422 ˇelse:
20423 ˇlog('for else')
20424 "});
20425 // test relative indent is preserved when tab
20426 // for `if`, `elif`, `else`, `while`, `with` and `for`
20427 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
20428 cx.assert_editor_state(indoc! {"
20429 def main():
20430 ˇfor item in items:
20431 ˇwhile item.active:
20432 ˇif item.value > 10:
20433 ˇcontinue
20434 ˇelif item.value < 0:
20435 ˇbreak
20436 ˇelse:
20437 ˇwith item.context() as ctx:
20438 ˇyield count
20439 ˇelse:
20440 ˇlog('while else')
20441 ˇelse:
20442 ˇlog('for else')
20443 "});
20444
20445 // test cursor move to start of each line on tab
20446 // for `try`, `except`, `else`, `finally`, `match` and `def`
20447 cx.set_state(indoc! {"
20448 def main():
20449 ˇ try:
20450 ˇ fetch()
20451 ˇ except ValueError:
20452 ˇ handle_error()
20453 ˇ else:
20454 ˇ match value:
20455 ˇ case _:
20456 ˇ finally:
20457 ˇ def status():
20458 ˇ return 0
20459 "});
20460 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
20461 cx.assert_editor_state(indoc! {"
20462 def main():
20463 ˇtry:
20464 ˇfetch()
20465 ˇexcept ValueError:
20466 ˇhandle_error()
20467 ˇelse:
20468 ˇmatch value:
20469 ˇcase _:
20470 ˇfinally:
20471 ˇdef status():
20472 ˇreturn 0
20473 "});
20474 // test relative indent is preserved when tab
20475 // for `try`, `except`, `else`, `finally`, `match` and `def`
20476 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
20477 cx.assert_editor_state(indoc! {"
20478 def main():
20479 ˇtry:
20480 ˇfetch()
20481 ˇexcept ValueError:
20482 ˇhandle_error()
20483 ˇelse:
20484 ˇmatch value:
20485 ˇcase _:
20486 ˇfinally:
20487 ˇdef status():
20488 ˇreturn 0
20489 "});
20490}
20491
20492#[gpui::test]
20493async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
20494 init_test(cx, |_| {});
20495
20496 let mut cx = EditorTestContext::new(cx).await;
20497 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
20498 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
20499
20500 // test `else` auto outdents when typed inside `if` block
20501 cx.set_state(indoc! {"
20502 def main():
20503 if i == 2:
20504 return
20505 ˇ
20506 "});
20507 cx.update_editor(|editor, window, cx| {
20508 editor.handle_input("else:", window, cx);
20509 });
20510 cx.assert_editor_state(indoc! {"
20511 def main():
20512 if i == 2:
20513 return
20514 else:ˇ
20515 "});
20516
20517 // test `except` auto outdents when typed inside `try` block
20518 cx.set_state(indoc! {"
20519 def main():
20520 try:
20521 i = 2
20522 ˇ
20523 "});
20524 cx.update_editor(|editor, window, cx| {
20525 editor.handle_input("except:", window, cx);
20526 });
20527 cx.assert_editor_state(indoc! {"
20528 def main():
20529 try:
20530 i = 2
20531 except:ˇ
20532 "});
20533
20534 // test `else` auto outdents when typed inside `except` block
20535 cx.set_state(indoc! {"
20536 def main():
20537 try:
20538 i = 2
20539 except:
20540 j = 2
20541 ˇ
20542 "});
20543 cx.update_editor(|editor, window, cx| {
20544 editor.handle_input("else:", window, cx);
20545 });
20546 cx.assert_editor_state(indoc! {"
20547 def main():
20548 try:
20549 i = 2
20550 except:
20551 j = 2
20552 else:ˇ
20553 "});
20554
20555 // test `finally` auto outdents when typed inside `else` block
20556 cx.set_state(indoc! {"
20557 def main():
20558 try:
20559 i = 2
20560 except:
20561 j = 2
20562 else:
20563 k = 2
20564 ˇ
20565 "});
20566 cx.update_editor(|editor, window, cx| {
20567 editor.handle_input("finally:", window, cx);
20568 });
20569 cx.assert_editor_state(indoc! {"
20570 def main():
20571 try:
20572 i = 2
20573 except:
20574 j = 2
20575 else:
20576 k = 2
20577 finally:ˇ
20578 "});
20579
20580 // TODO: test `except` auto outdents when typed inside `try` block right after for block
20581 // cx.set_state(indoc! {"
20582 // def main():
20583 // try:
20584 // for i in range(n):
20585 // pass
20586 // ˇ
20587 // "});
20588 // cx.update_editor(|editor, window, cx| {
20589 // editor.handle_input("except:", window, cx);
20590 // });
20591 // cx.assert_editor_state(indoc! {"
20592 // def main():
20593 // try:
20594 // for i in range(n):
20595 // pass
20596 // except:ˇ
20597 // "});
20598
20599 // TODO: test `else` auto outdents when typed inside `except` block right after for block
20600 // cx.set_state(indoc! {"
20601 // def main():
20602 // try:
20603 // i = 2
20604 // except:
20605 // for i in range(n):
20606 // pass
20607 // ˇ
20608 // "});
20609 // cx.update_editor(|editor, window, cx| {
20610 // editor.handle_input("else:", window, cx);
20611 // });
20612 // cx.assert_editor_state(indoc! {"
20613 // def main():
20614 // try:
20615 // i = 2
20616 // except:
20617 // for i in range(n):
20618 // pass
20619 // else:ˇ
20620 // "});
20621
20622 // TODO: test `finally` auto outdents when typed inside `else` block right after for block
20623 // cx.set_state(indoc! {"
20624 // def main():
20625 // try:
20626 // i = 2
20627 // except:
20628 // j = 2
20629 // else:
20630 // for i in range(n):
20631 // pass
20632 // ˇ
20633 // "});
20634 // cx.update_editor(|editor, window, cx| {
20635 // editor.handle_input("finally:", window, cx);
20636 // });
20637 // cx.assert_editor_state(indoc! {"
20638 // def main():
20639 // try:
20640 // i = 2
20641 // except:
20642 // j = 2
20643 // else:
20644 // for i in range(n):
20645 // pass
20646 // finally:ˇ
20647 // "});
20648
20649 // test `else` stays at correct indent when typed after `for` block
20650 cx.set_state(indoc! {"
20651 def main():
20652 for i in range(10):
20653 if i == 3:
20654 break
20655 ˇ
20656 "});
20657 cx.update_editor(|editor, window, cx| {
20658 editor.handle_input("else:", window, cx);
20659 });
20660 cx.assert_editor_state(indoc! {"
20661 def main():
20662 for i in range(10):
20663 if i == 3:
20664 break
20665 else:ˇ
20666 "});
20667}
20668
20669#[gpui::test]
20670async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
20671 init_test(cx, |_| {});
20672 update_test_language_settings(cx, |settings| {
20673 settings.defaults.extend_comment_on_newline = Some(false);
20674 });
20675 let mut cx = EditorTestContext::new(cx).await;
20676 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
20677 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
20678
20679 // test correct indent after newline on comment
20680 cx.set_state(indoc! {"
20681 # COMMENT:ˇ
20682 "});
20683 cx.update_editor(|editor, window, cx| {
20684 editor.newline(&Newline, window, cx);
20685 });
20686 cx.assert_editor_state(indoc! {"
20687 # COMMENT:
20688 ˇ
20689 "});
20690
20691 // test correct indent after newline in curly brackets
20692 cx.set_state(indoc! {"
20693 {ˇ}
20694 "});
20695 cx.update_editor(|editor, window, cx| {
20696 editor.newline(&Newline, window, cx);
20697 });
20698 cx.run_until_parked();
20699 cx.assert_editor_state(indoc! {"
20700 {
20701 ˇ
20702 }
20703 "});
20704}
20705
20706fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
20707 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
20708 point..point
20709}
20710
20711fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
20712 let (text, ranges) = marked_text_ranges(marked_text, true);
20713 assert_eq!(editor.text(cx), text);
20714 assert_eq!(
20715 editor.selections.ranges(cx),
20716 ranges,
20717 "Assert selections are {}",
20718 marked_text
20719 );
20720}
20721
20722pub fn handle_signature_help_request(
20723 cx: &mut EditorLspTestContext,
20724 mocked_response: lsp::SignatureHelp,
20725) -> impl Future<Output = ()> + use<> {
20726 let mut request =
20727 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
20728 let mocked_response = mocked_response.clone();
20729 async move { Ok(Some(mocked_response)) }
20730 });
20731
20732 async move {
20733 request.next().await;
20734 }
20735}
20736
20737/// Handle completion request passing a marked string specifying where the completion
20738/// should be triggered from using '|' character, what range should be replaced, and what completions
20739/// should be returned using '<' and '>' to delimit the range.
20740///
20741/// Also see `handle_completion_request_with_insert_and_replace`.
20742#[track_caller]
20743pub fn handle_completion_request(
20744 cx: &mut EditorLspTestContext,
20745 marked_string: &str,
20746 completions: Vec<&'static str>,
20747 counter: Arc<AtomicUsize>,
20748) -> impl Future<Output = ()> {
20749 let complete_from_marker: TextRangeMarker = '|'.into();
20750 let replace_range_marker: TextRangeMarker = ('<', '>').into();
20751 let (_, mut marked_ranges) = marked_text_ranges_by(
20752 marked_string,
20753 vec![complete_from_marker.clone(), replace_range_marker.clone()],
20754 );
20755
20756 let complete_from_position =
20757 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
20758 let replace_range =
20759 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
20760
20761 let mut request =
20762 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
20763 let completions = completions.clone();
20764 counter.fetch_add(1, atomic::Ordering::Release);
20765 async move {
20766 assert_eq!(params.text_document_position.text_document.uri, url.clone());
20767 assert_eq!(
20768 params.text_document_position.position,
20769 complete_from_position
20770 );
20771 Ok(Some(lsp::CompletionResponse::Array(
20772 completions
20773 .iter()
20774 .map(|completion_text| lsp::CompletionItem {
20775 label: completion_text.to_string(),
20776 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
20777 range: replace_range,
20778 new_text: completion_text.to_string(),
20779 })),
20780 ..Default::default()
20781 })
20782 .collect(),
20783 )))
20784 }
20785 });
20786
20787 async move {
20788 request.next().await;
20789 }
20790}
20791
20792/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
20793/// given instead, which also contains an `insert` range.
20794///
20795/// This function uses the cursor position to mimic what Rust-Analyzer provides as the `insert` range,
20796/// that is, `replace_range.start..cursor_pos`.
20797pub fn handle_completion_request_with_insert_and_replace(
20798 cx: &mut EditorLspTestContext,
20799 marked_string: &str,
20800 completions: Vec<&'static str>,
20801 counter: Arc<AtomicUsize>,
20802) -> impl Future<Output = ()> {
20803 let complete_from_marker: TextRangeMarker = '|'.into();
20804 let replace_range_marker: TextRangeMarker = ('<', '>').into();
20805 let (_, mut marked_ranges) = marked_text_ranges_by(
20806 marked_string,
20807 vec![complete_from_marker.clone(), replace_range_marker.clone()],
20808 );
20809
20810 let complete_from_position =
20811 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
20812 let replace_range =
20813 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
20814
20815 let mut request =
20816 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
20817 let completions = completions.clone();
20818 counter.fetch_add(1, atomic::Ordering::Release);
20819 async move {
20820 assert_eq!(params.text_document_position.text_document.uri, url.clone());
20821 assert_eq!(
20822 params.text_document_position.position, complete_from_position,
20823 "marker `|` position doesn't match",
20824 );
20825 Ok(Some(lsp::CompletionResponse::Array(
20826 completions
20827 .iter()
20828 .map(|completion_text| lsp::CompletionItem {
20829 label: completion_text.to_string(),
20830 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
20831 lsp::InsertReplaceEdit {
20832 insert: lsp::Range {
20833 start: replace_range.start,
20834 end: complete_from_position,
20835 },
20836 replace: replace_range,
20837 new_text: completion_text.to_string(),
20838 },
20839 )),
20840 ..Default::default()
20841 })
20842 .collect(),
20843 )))
20844 }
20845 });
20846
20847 async move {
20848 request.next().await;
20849 }
20850}
20851
20852fn handle_resolve_completion_request(
20853 cx: &mut EditorLspTestContext,
20854 edits: Option<Vec<(&'static str, &'static str)>>,
20855) -> impl Future<Output = ()> {
20856 let edits = edits.map(|edits| {
20857 edits
20858 .iter()
20859 .map(|(marked_string, new_text)| {
20860 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
20861 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
20862 lsp::TextEdit::new(replace_range, new_text.to_string())
20863 })
20864 .collect::<Vec<_>>()
20865 });
20866
20867 let mut request =
20868 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
20869 let edits = edits.clone();
20870 async move {
20871 Ok(lsp::CompletionItem {
20872 additional_text_edits: edits,
20873 ..Default::default()
20874 })
20875 }
20876 });
20877
20878 async move {
20879 request.next().await;
20880 }
20881}
20882
20883pub(crate) fn update_test_language_settings(
20884 cx: &mut TestAppContext,
20885 f: impl Fn(&mut AllLanguageSettingsContent),
20886) {
20887 cx.update(|cx| {
20888 SettingsStore::update_global(cx, |store, cx| {
20889 store.update_user_settings::<AllLanguageSettings>(cx, f);
20890 });
20891 });
20892}
20893
20894pub(crate) fn update_test_project_settings(
20895 cx: &mut TestAppContext,
20896 f: impl Fn(&mut ProjectSettings),
20897) {
20898 cx.update(|cx| {
20899 SettingsStore::update_global(cx, |store, cx| {
20900 store.update_user_settings::<ProjectSettings>(cx, f);
20901 });
20902 });
20903}
20904
20905pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
20906 cx.update(|cx| {
20907 assets::Assets.load_test_fonts(cx);
20908 let store = SettingsStore::test(cx);
20909 cx.set_global(store);
20910 theme::init(theme::LoadThemes::JustBase, cx);
20911 release_channel::init(SemanticVersion::default(), cx);
20912 client::init_settings(cx);
20913 language::init(cx);
20914 Project::init_settings(cx);
20915 workspace::init_settings(cx);
20916 crate::init(cx);
20917 });
20918
20919 update_test_language_settings(cx, f);
20920}
20921
20922#[track_caller]
20923fn assert_hunk_revert(
20924 not_reverted_text_with_selections: &str,
20925 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
20926 expected_reverted_text_with_selections: &str,
20927 base_text: &str,
20928 cx: &mut EditorLspTestContext,
20929) {
20930 cx.set_state(not_reverted_text_with_selections);
20931 cx.set_head_text(base_text);
20932 cx.executor().run_until_parked();
20933
20934 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
20935 let snapshot = editor.snapshot(window, cx);
20936 let reverted_hunk_statuses = snapshot
20937 .buffer_snapshot
20938 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
20939 .map(|hunk| hunk.status().kind)
20940 .collect::<Vec<_>>();
20941
20942 editor.git_restore(&Default::default(), window, cx);
20943 reverted_hunk_statuses
20944 });
20945 cx.executor().run_until_parked();
20946 cx.assert_editor_state(expected_reverted_text_with_selections);
20947 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
20948}