1use super::*;
2use crate::{
3 JoinLines,
4 linked_editing_ranges::LinkedEditingRanges,
5 scroll::scroll_amount::ScrollAmount,
6 test::{
7 assert_text_with_selections, build_editor,
8 editor_lsp_test_context::{EditorLspTestContext, git_commit_lang},
9 editor_test_context::EditorTestContext,
10 select_ranges,
11 },
12};
13use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
14use futures::StreamExt;
15use gpui::{
16 BackgroundExecutor, DismissEvent, SemanticVersion, TestAppContext, UpdateGlobal,
17 VisualTestContext, WindowBounds, WindowOptions, div,
18};
19use indoc::indoc;
20use language::{
21 BracketPairConfig,
22 Capability::ReadWrite,
23 FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher, LanguageName,
24 Override, Point,
25 language_settings::{
26 AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings,
27 LanguageSettingsContent, LspInsertMode, PrettierSettings,
28 },
29 tree_sitter_python,
30};
31use language_settings::{Formatter, FormatterList, IndentGuideSettings};
32use lsp::CompletionParams;
33use multi_buffer::{IndentGuide, PathKey};
34use parking_lot::Mutex;
35use pretty_assertions::{assert_eq, assert_ne};
36use project::{
37 FakeFs,
38 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
39 project_settings::{LspSettings, ProjectSettings},
40};
41use serde_json::{self, json};
42use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
43use std::{
44 iter,
45 sync::atomic::{self, AtomicUsize},
46};
47use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
48use text::ToPoint as _;
49use unindent::Unindent;
50use util::{
51 assert_set_eq, path,
52 test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
53 uri,
54};
55use workspace::{
56 CloseActiveItem, CloseAllItems, CloseInactiveItems, NavigationEntry, OpenOptions, ViewId,
57 item::{FollowEvent, FollowableItem, Item, ItemHandle},
58};
59
60#[gpui::test]
61fn test_edit_events(cx: &mut TestAppContext) {
62 init_test(cx, |_| {});
63
64 let buffer = cx.new(|cx| {
65 let mut buffer = language::Buffer::local("123456", cx);
66 buffer.set_group_interval(Duration::from_secs(1));
67 buffer
68 });
69
70 let events = Rc::new(RefCell::new(Vec::new()));
71 let editor1 = cx.add_window({
72 let events = events.clone();
73 |window, cx| {
74 let entity = cx.entity().clone();
75 cx.subscribe_in(
76 &entity,
77 window,
78 move |_, _, event: &EditorEvent, _, _| match event {
79 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
80 EditorEvent::BufferEdited => {
81 events.borrow_mut().push(("editor1", "buffer edited"))
82 }
83 _ => {}
84 },
85 )
86 .detach();
87 Editor::for_buffer(buffer.clone(), None, window, cx)
88 }
89 });
90
91 let editor2 = cx.add_window({
92 let events = events.clone();
93 |window, cx| {
94 cx.subscribe_in(
95 &cx.entity().clone(),
96 window,
97 move |_, _, event: &EditorEvent, _, _| match event {
98 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
99 EditorEvent::BufferEdited => {
100 events.borrow_mut().push(("editor2", "buffer edited"))
101 }
102 _ => {}
103 },
104 )
105 .detach();
106 Editor::for_buffer(buffer.clone(), None, window, cx)
107 }
108 });
109
110 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
111
112 // Mutating editor 1 will emit an `Edited` event only for that editor.
113 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
114 assert_eq!(
115 mem::take(&mut *events.borrow_mut()),
116 [
117 ("editor1", "edited"),
118 ("editor1", "buffer edited"),
119 ("editor2", "buffer edited"),
120 ]
121 );
122
123 // Mutating editor 2 will emit an `Edited` event only for that editor.
124 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
125 assert_eq!(
126 mem::take(&mut *events.borrow_mut()),
127 [
128 ("editor2", "edited"),
129 ("editor1", "buffer edited"),
130 ("editor2", "buffer edited"),
131 ]
132 );
133
134 // Undoing on editor 1 will emit an `Edited` event only for that editor.
135 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
136 assert_eq!(
137 mem::take(&mut *events.borrow_mut()),
138 [
139 ("editor1", "edited"),
140 ("editor1", "buffer edited"),
141 ("editor2", "buffer edited"),
142 ]
143 );
144
145 // Redoing on editor 1 will emit an `Edited` event only for that editor.
146 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
147 assert_eq!(
148 mem::take(&mut *events.borrow_mut()),
149 [
150 ("editor1", "edited"),
151 ("editor1", "buffer edited"),
152 ("editor2", "buffer edited"),
153 ]
154 );
155
156 // Undoing on editor 2 will emit an `Edited` event only for that editor.
157 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
158 assert_eq!(
159 mem::take(&mut *events.borrow_mut()),
160 [
161 ("editor2", "edited"),
162 ("editor1", "buffer edited"),
163 ("editor2", "buffer edited"),
164 ]
165 );
166
167 // Redoing on editor 2 will emit an `Edited` event only for that editor.
168 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
169 assert_eq!(
170 mem::take(&mut *events.borrow_mut()),
171 [
172 ("editor2", "edited"),
173 ("editor1", "buffer edited"),
174 ("editor2", "buffer edited"),
175 ]
176 );
177
178 // No event is emitted when the mutation is a no-op.
179 _ = editor2.update(cx, |editor, window, cx| {
180 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
181
182 editor.backspace(&Backspace, window, cx);
183 });
184 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
185}
186
187#[gpui::test]
188fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
189 init_test(cx, |_| {});
190
191 let mut now = Instant::now();
192 let group_interval = Duration::from_millis(1);
193 let buffer = cx.new(|cx| {
194 let mut buf = language::Buffer::local("123456", cx);
195 buf.set_group_interval(group_interval);
196 buf
197 });
198 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
199 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
200
201 _ = editor.update(cx, |editor, window, cx| {
202 editor.start_transaction_at(now, window, cx);
203 editor.change_selections(None, window, cx, |s| s.select_ranges([2..4]));
204
205 editor.insert("cd", window, cx);
206 editor.end_transaction_at(now, cx);
207 assert_eq!(editor.text(cx), "12cd56");
208 assert_eq!(editor.selections.ranges(cx), vec![4..4]);
209
210 editor.start_transaction_at(now, window, cx);
211 editor.change_selections(None, window, cx, |s| s.select_ranges([4..5]));
212 editor.insert("e", window, cx);
213 editor.end_transaction_at(now, cx);
214 assert_eq!(editor.text(cx), "12cde6");
215 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
216
217 now += group_interval + Duration::from_millis(1);
218 editor.change_selections(None, window, cx, |s| s.select_ranges([2..2]));
219
220 // Simulate an edit in another editor
221 buffer.update(cx, |buffer, cx| {
222 buffer.start_transaction_at(now, cx);
223 buffer.edit([(0..1, "a")], None, cx);
224 buffer.edit([(1..1, "b")], None, cx);
225 buffer.end_transaction_at(now, cx);
226 });
227
228 assert_eq!(editor.text(cx), "ab2cde6");
229 assert_eq!(editor.selections.ranges(cx), vec![3..3]);
230
231 // Last transaction happened past the group interval in a different editor.
232 // Undo it individually and don't restore selections.
233 editor.undo(&Undo, window, cx);
234 assert_eq!(editor.text(cx), "12cde6");
235 assert_eq!(editor.selections.ranges(cx), vec![2..2]);
236
237 // First two transactions happened within the group interval in this editor.
238 // Undo them together and restore selections.
239 editor.undo(&Undo, window, cx);
240 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
241 assert_eq!(editor.text(cx), "123456");
242 assert_eq!(editor.selections.ranges(cx), vec![0..0]);
243
244 // Redo the first two transactions together.
245 editor.redo(&Redo, window, cx);
246 assert_eq!(editor.text(cx), "12cde6");
247 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
248
249 // Redo the last transaction on its own.
250 editor.redo(&Redo, window, cx);
251 assert_eq!(editor.text(cx), "ab2cde6");
252 assert_eq!(editor.selections.ranges(cx), vec![6..6]);
253
254 // Test empty transactions.
255 editor.start_transaction_at(now, window, cx);
256 editor.end_transaction_at(now, cx);
257 editor.undo(&Undo, window, cx);
258 assert_eq!(editor.text(cx), "12cde6");
259 });
260}
261
262#[gpui::test]
263fn test_ime_composition(cx: &mut TestAppContext) {
264 init_test(cx, |_| {});
265
266 let buffer = cx.new(|cx| {
267 let mut buffer = language::Buffer::local("abcde", cx);
268 // Ensure automatic grouping doesn't occur.
269 buffer.set_group_interval(Duration::ZERO);
270 buffer
271 });
272
273 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
274 cx.add_window(|window, cx| {
275 let mut editor = build_editor(buffer.clone(), window, cx);
276
277 // Start a new IME composition.
278 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
279 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
280 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
281 assert_eq!(editor.text(cx), "äbcde");
282 assert_eq!(
283 editor.marked_text_ranges(cx),
284 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
285 );
286
287 // Finalize IME composition.
288 editor.replace_text_in_range(None, "ā", window, cx);
289 assert_eq!(editor.text(cx), "ābcde");
290 assert_eq!(editor.marked_text_ranges(cx), None);
291
292 // IME composition edits are grouped and are undone/redone at once.
293 editor.undo(&Default::default(), window, cx);
294 assert_eq!(editor.text(cx), "abcde");
295 assert_eq!(editor.marked_text_ranges(cx), None);
296 editor.redo(&Default::default(), window, cx);
297 assert_eq!(editor.text(cx), "ābcde");
298 assert_eq!(editor.marked_text_ranges(cx), None);
299
300 // Start a new IME composition.
301 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
302 assert_eq!(
303 editor.marked_text_ranges(cx),
304 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
305 );
306
307 // Undoing during an IME composition cancels it.
308 editor.undo(&Default::default(), window, cx);
309 assert_eq!(editor.text(cx), "ābcde");
310 assert_eq!(editor.marked_text_ranges(cx), None);
311
312 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
313 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
314 assert_eq!(editor.text(cx), "ābcdè");
315 assert_eq!(
316 editor.marked_text_ranges(cx),
317 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
318 );
319
320 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
321 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
322 assert_eq!(editor.text(cx), "ābcdę");
323 assert_eq!(editor.marked_text_ranges(cx), None);
324
325 // Start a new IME composition with multiple cursors.
326 editor.change_selections(None, window, cx, |s| {
327 s.select_ranges([
328 OffsetUtf16(1)..OffsetUtf16(1),
329 OffsetUtf16(3)..OffsetUtf16(3),
330 OffsetUtf16(5)..OffsetUtf16(5),
331 ])
332 });
333 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
334 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
335 assert_eq!(
336 editor.marked_text_ranges(cx),
337 Some(vec![
338 OffsetUtf16(0)..OffsetUtf16(3),
339 OffsetUtf16(4)..OffsetUtf16(7),
340 OffsetUtf16(8)..OffsetUtf16(11)
341 ])
342 );
343
344 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
345 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
346 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
347 assert_eq!(
348 editor.marked_text_ranges(cx),
349 Some(vec![
350 OffsetUtf16(1)..OffsetUtf16(2),
351 OffsetUtf16(5)..OffsetUtf16(6),
352 OffsetUtf16(9)..OffsetUtf16(10)
353 ])
354 );
355
356 // Finalize IME composition with multiple cursors.
357 editor.replace_text_in_range(Some(9..10), "2", window, cx);
358 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
359 assert_eq!(editor.marked_text_ranges(cx), None);
360
361 editor
362 });
363}
364
365#[gpui::test]
366fn test_selection_with_mouse(cx: &mut TestAppContext) {
367 init_test(cx, |_| {});
368
369 let editor = cx.add_window(|window, cx| {
370 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
371 build_editor(buffer, window, cx)
372 });
373
374 _ = editor.update(cx, |editor, window, cx| {
375 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
376 });
377 assert_eq!(
378 editor
379 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
380 .unwrap(),
381 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
382 );
383
384 _ = editor.update(cx, |editor, window, cx| {
385 editor.update_selection(
386 DisplayPoint::new(DisplayRow(3), 3),
387 0,
388 gpui::Point::<f32>::default(),
389 window,
390 cx,
391 );
392 });
393
394 assert_eq!(
395 editor
396 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
397 .unwrap(),
398 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
399 );
400
401 _ = editor.update(cx, |editor, window, cx| {
402 editor.update_selection(
403 DisplayPoint::new(DisplayRow(1), 1),
404 0,
405 gpui::Point::<f32>::default(),
406 window,
407 cx,
408 );
409 });
410
411 assert_eq!(
412 editor
413 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
414 .unwrap(),
415 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
416 );
417
418 _ = editor.update(cx, |editor, window, cx| {
419 editor.end_selection(window, cx);
420 editor.update_selection(
421 DisplayPoint::new(DisplayRow(3), 3),
422 0,
423 gpui::Point::<f32>::default(),
424 window,
425 cx,
426 );
427 });
428
429 assert_eq!(
430 editor
431 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
432 .unwrap(),
433 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
434 );
435
436 _ = editor.update(cx, |editor, window, cx| {
437 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
438 editor.update_selection(
439 DisplayPoint::new(DisplayRow(0), 0),
440 0,
441 gpui::Point::<f32>::default(),
442 window,
443 cx,
444 );
445 });
446
447 assert_eq!(
448 editor
449 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
450 .unwrap(),
451 [
452 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
453 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
454 ]
455 );
456
457 _ = editor.update(cx, |editor, window, cx| {
458 editor.end_selection(window, cx);
459 });
460
461 assert_eq!(
462 editor
463 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
464 .unwrap(),
465 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
466 );
467}
468
469#[gpui::test]
470fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
471 init_test(cx, |_| {});
472
473 let editor = cx.add_window(|window, cx| {
474 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
475 build_editor(buffer, window, cx)
476 });
477
478 _ = editor.update(cx, |editor, window, cx| {
479 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
480 });
481
482 _ = editor.update(cx, |editor, window, cx| {
483 editor.end_selection(window, cx);
484 });
485
486 _ = editor.update(cx, |editor, window, cx| {
487 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
488 });
489
490 _ = editor.update(cx, |editor, window, cx| {
491 editor.end_selection(window, cx);
492 });
493
494 assert_eq!(
495 editor
496 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
497 .unwrap(),
498 [
499 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
500 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
501 ]
502 );
503
504 _ = editor.update(cx, |editor, window, cx| {
505 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
506 });
507
508 _ = editor.update(cx, |editor, window, cx| {
509 editor.end_selection(window, cx);
510 });
511
512 assert_eq!(
513 editor
514 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
515 .unwrap(),
516 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
517 );
518}
519
520#[gpui::test]
521fn test_canceling_pending_selection(cx: &mut TestAppContext) {
522 init_test(cx, |_| {});
523
524 let editor = cx.add_window(|window, cx| {
525 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
526 build_editor(buffer, window, cx)
527 });
528
529 _ = editor.update(cx, |editor, window, cx| {
530 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
531 assert_eq!(
532 editor.selections.display_ranges(cx),
533 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
534 );
535 });
536
537 _ = editor.update(cx, |editor, window, cx| {
538 editor.update_selection(
539 DisplayPoint::new(DisplayRow(3), 3),
540 0,
541 gpui::Point::<f32>::default(),
542 window,
543 cx,
544 );
545 assert_eq!(
546 editor.selections.display_ranges(cx),
547 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
548 );
549 });
550
551 _ = editor.update(cx, |editor, window, cx| {
552 editor.cancel(&Cancel, window, cx);
553 editor.update_selection(
554 DisplayPoint::new(DisplayRow(1), 1),
555 0,
556 gpui::Point::<f32>::default(),
557 window,
558 cx,
559 );
560 assert_eq!(
561 editor.selections.display_ranges(cx),
562 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
563 );
564 });
565}
566
567#[gpui::test]
568fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
569 init_test(cx, |_| {});
570
571 let editor = cx.add_window(|window, cx| {
572 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
573 build_editor(buffer, window, cx)
574 });
575
576 _ = editor.update(cx, |editor, window, cx| {
577 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
578 assert_eq!(
579 editor.selections.display_ranges(cx),
580 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
581 );
582
583 editor.move_down(&Default::default(), window, cx);
584 assert_eq!(
585 editor.selections.display_ranges(cx),
586 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
587 );
588
589 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
590 assert_eq!(
591 editor.selections.display_ranges(cx),
592 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
593 );
594
595 editor.move_up(&Default::default(), window, cx);
596 assert_eq!(
597 editor.selections.display_ranges(cx),
598 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
599 );
600 });
601}
602
603#[gpui::test]
604fn test_clone(cx: &mut TestAppContext) {
605 init_test(cx, |_| {});
606
607 let (text, selection_ranges) = marked_text_ranges(
608 indoc! {"
609 one
610 two
611 threeˇ
612 four
613 fiveˇ
614 "},
615 true,
616 );
617
618 let editor = cx.add_window(|window, cx| {
619 let buffer = MultiBuffer::build_simple(&text, cx);
620 build_editor(buffer, window, cx)
621 });
622
623 _ = editor.update(cx, |editor, window, cx| {
624 editor.change_selections(None, window, cx, |s| {
625 s.select_ranges(selection_ranges.clone())
626 });
627 editor.fold_creases(
628 vec![
629 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
630 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
631 ],
632 true,
633 window,
634 cx,
635 );
636 });
637
638 let cloned_editor = editor
639 .update(cx, |editor, _, cx| {
640 cx.open_window(Default::default(), |window, cx| {
641 cx.new(|cx| editor.clone(window, cx))
642 })
643 })
644 .unwrap()
645 .unwrap();
646
647 let snapshot = editor
648 .update(cx, |e, window, cx| e.snapshot(window, cx))
649 .unwrap();
650 let cloned_snapshot = cloned_editor
651 .update(cx, |e, window, cx| e.snapshot(window, cx))
652 .unwrap();
653
654 assert_eq!(
655 cloned_editor
656 .update(cx, |e, _, cx| e.display_text(cx))
657 .unwrap(),
658 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
659 );
660 assert_eq!(
661 cloned_snapshot
662 .folds_in_range(0..text.len())
663 .collect::<Vec<_>>(),
664 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
665 );
666 assert_set_eq!(
667 cloned_editor
668 .update(cx, |editor, _, cx| editor.selections.ranges::<Point>(cx))
669 .unwrap(),
670 editor
671 .update(cx, |editor, _, cx| editor.selections.ranges(cx))
672 .unwrap()
673 );
674 assert_set_eq!(
675 cloned_editor
676 .update(cx, |e, _window, cx| e.selections.display_ranges(cx))
677 .unwrap(),
678 editor
679 .update(cx, |e, _, cx| e.selections.display_ranges(cx))
680 .unwrap()
681 );
682}
683
684#[gpui::test]
685async fn test_navigation_history(cx: &mut TestAppContext) {
686 init_test(cx, |_| {});
687
688 use workspace::item::Item;
689
690 let fs = FakeFs::new(cx.executor());
691 let project = Project::test(fs, [], cx).await;
692 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
693 let pane = workspace
694 .update(cx, |workspace, _, _| workspace.active_pane().clone())
695 .unwrap();
696
697 _ = workspace.update(cx, |_v, window, cx| {
698 cx.new(|cx| {
699 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
700 let mut editor = build_editor(buffer.clone(), window, cx);
701 let handle = cx.entity();
702 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
703
704 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
705 editor.nav_history.as_mut().unwrap().pop_backward(cx)
706 }
707
708 // Move the cursor a small distance.
709 // Nothing is added to the navigation history.
710 editor.change_selections(None, window, cx, |s| {
711 s.select_display_ranges([
712 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
713 ])
714 });
715 editor.change_selections(None, window, cx, |s| {
716 s.select_display_ranges([
717 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
718 ])
719 });
720 assert!(pop_history(&mut editor, cx).is_none());
721
722 // Move the cursor a large distance.
723 // The history can jump back to the previous position.
724 editor.change_selections(None, window, cx, |s| {
725 s.select_display_ranges([
726 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
727 ])
728 });
729 let nav_entry = pop_history(&mut editor, cx).unwrap();
730 editor.navigate(nav_entry.data.unwrap(), window, cx);
731 assert_eq!(nav_entry.item.id(), cx.entity_id());
732 assert_eq!(
733 editor.selections.display_ranges(cx),
734 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
735 );
736 assert!(pop_history(&mut editor, cx).is_none());
737
738 // Move the cursor a small distance via the mouse.
739 // Nothing is added to the navigation history.
740 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
741 editor.end_selection(window, cx);
742 assert_eq!(
743 editor.selections.display_ranges(cx),
744 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
745 );
746 assert!(pop_history(&mut editor, cx).is_none());
747
748 // Move the cursor a large distance via the mouse.
749 // The history can jump back to the previous position.
750 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
751 editor.end_selection(window, cx);
752 assert_eq!(
753 editor.selections.display_ranges(cx),
754 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
755 );
756 let nav_entry = pop_history(&mut editor, cx).unwrap();
757 editor.navigate(nav_entry.data.unwrap(), window, cx);
758 assert_eq!(nav_entry.item.id(), cx.entity_id());
759 assert_eq!(
760 editor.selections.display_ranges(cx),
761 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
762 );
763 assert!(pop_history(&mut editor, cx).is_none());
764
765 // Set scroll position to check later
766 editor.set_scroll_position(gpui::Point::<f32>::new(5.5, 5.5), window, cx);
767 let original_scroll_position = editor.scroll_manager.anchor();
768
769 // Jump to the end of the document and adjust scroll
770 editor.move_to_end(&MoveToEnd, window, cx);
771 editor.set_scroll_position(gpui::Point::<f32>::new(-2.5, -0.5), window, cx);
772 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
773
774 let nav_entry = pop_history(&mut editor, cx).unwrap();
775 editor.navigate(nav_entry.data.unwrap(), window, cx);
776 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
777
778 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
779 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
780 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
781 let invalid_point = Point::new(9999, 0);
782 editor.navigate(
783 Box::new(NavigationData {
784 cursor_anchor: invalid_anchor,
785 cursor_position: invalid_point,
786 scroll_anchor: ScrollAnchor {
787 anchor: invalid_anchor,
788 offset: Default::default(),
789 },
790 scroll_top_row: invalid_point.row,
791 }),
792 window,
793 cx,
794 );
795 assert_eq!(
796 editor.selections.display_ranges(cx),
797 &[editor.max_point(cx)..editor.max_point(cx)]
798 );
799 assert_eq!(
800 editor.scroll_position(cx),
801 gpui::Point::new(0., editor.max_point(cx).row().as_f32())
802 );
803
804 editor
805 })
806 });
807}
808
809#[gpui::test]
810fn test_cancel(cx: &mut TestAppContext) {
811 init_test(cx, |_| {});
812
813 let editor = cx.add_window(|window, cx| {
814 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
815 build_editor(buffer, window, cx)
816 });
817
818 _ = editor.update(cx, |editor, window, cx| {
819 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
820 editor.update_selection(
821 DisplayPoint::new(DisplayRow(1), 1),
822 0,
823 gpui::Point::<f32>::default(),
824 window,
825 cx,
826 );
827 editor.end_selection(window, cx);
828
829 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
830 editor.update_selection(
831 DisplayPoint::new(DisplayRow(0), 3),
832 0,
833 gpui::Point::<f32>::default(),
834 window,
835 cx,
836 );
837 editor.end_selection(window, cx);
838 assert_eq!(
839 editor.selections.display_ranges(cx),
840 [
841 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
842 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
843 ]
844 );
845 });
846
847 _ = editor.update(cx, |editor, window, cx| {
848 editor.cancel(&Cancel, window, cx);
849 assert_eq!(
850 editor.selections.display_ranges(cx),
851 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
852 );
853 });
854
855 _ = editor.update(cx, |editor, window, cx| {
856 editor.cancel(&Cancel, window, cx);
857 assert_eq!(
858 editor.selections.display_ranges(cx),
859 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
860 );
861 });
862}
863
864#[gpui::test]
865fn test_fold_action(cx: &mut TestAppContext) {
866 init_test(cx, |_| {});
867
868 let editor = cx.add_window(|window, cx| {
869 let buffer = MultiBuffer::build_simple(
870 &"
871 impl Foo {
872 // Hello!
873
874 fn a() {
875 1
876 }
877
878 fn b() {
879 2
880 }
881
882 fn c() {
883 3
884 }
885 }
886 "
887 .unindent(),
888 cx,
889 );
890 build_editor(buffer.clone(), window, cx)
891 });
892
893 _ = editor.update(cx, |editor, window, cx| {
894 editor.change_selections(None, window, cx, |s| {
895 s.select_display_ranges([
896 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
897 ]);
898 });
899 editor.fold(&Fold, window, cx);
900 assert_eq!(
901 editor.display_text(cx),
902 "
903 impl Foo {
904 // Hello!
905
906 fn a() {
907 1
908 }
909
910 fn b() {⋯
911 }
912
913 fn c() {⋯
914 }
915 }
916 "
917 .unindent(),
918 );
919
920 editor.fold(&Fold, window, cx);
921 assert_eq!(
922 editor.display_text(cx),
923 "
924 impl Foo {⋯
925 }
926 "
927 .unindent(),
928 );
929
930 editor.unfold_lines(&UnfoldLines, window, cx);
931 assert_eq!(
932 editor.display_text(cx),
933 "
934 impl Foo {
935 // Hello!
936
937 fn a() {
938 1
939 }
940
941 fn b() {⋯
942 }
943
944 fn c() {⋯
945 }
946 }
947 "
948 .unindent(),
949 );
950
951 editor.unfold_lines(&UnfoldLines, window, cx);
952 assert_eq!(
953 editor.display_text(cx),
954 editor.buffer.read(cx).read(cx).text()
955 );
956 });
957}
958
959#[gpui::test]
960fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
961 init_test(cx, |_| {});
962
963 let editor = cx.add_window(|window, cx| {
964 let buffer = MultiBuffer::build_simple(
965 &"
966 class Foo:
967 # Hello!
968
969 def a():
970 print(1)
971
972 def b():
973 print(2)
974
975 def c():
976 print(3)
977 "
978 .unindent(),
979 cx,
980 );
981 build_editor(buffer.clone(), window, cx)
982 });
983
984 _ = editor.update(cx, |editor, window, cx| {
985 editor.change_selections(None, window, cx, |s| {
986 s.select_display_ranges([
987 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
988 ]);
989 });
990 editor.fold(&Fold, window, cx);
991 assert_eq!(
992 editor.display_text(cx),
993 "
994 class Foo:
995 # Hello!
996
997 def a():
998 print(1)
999
1000 def b():⋯
1001
1002 def c():⋯
1003 "
1004 .unindent(),
1005 );
1006
1007 editor.fold(&Fold, window, cx);
1008 assert_eq!(
1009 editor.display_text(cx),
1010 "
1011 class Foo:⋯
1012 "
1013 .unindent(),
1014 );
1015
1016 editor.unfold_lines(&UnfoldLines, window, cx);
1017 assert_eq!(
1018 editor.display_text(cx),
1019 "
1020 class Foo:
1021 # Hello!
1022
1023 def a():
1024 print(1)
1025
1026 def b():⋯
1027
1028 def c():⋯
1029 "
1030 .unindent(),
1031 );
1032
1033 editor.unfold_lines(&UnfoldLines, window, cx);
1034 assert_eq!(
1035 editor.display_text(cx),
1036 editor.buffer.read(cx).read(cx).text()
1037 );
1038 });
1039}
1040
1041#[gpui::test]
1042fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1043 init_test(cx, |_| {});
1044
1045 let editor = cx.add_window(|window, cx| {
1046 let buffer = MultiBuffer::build_simple(
1047 &"
1048 class Foo:
1049 # Hello!
1050
1051 def a():
1052 print(1)
1053
1054 def b():
1055 print(2)
1056
1057
1058 def c():
1059 print(3)
1060
1061
1062 "
1063 .unindent(),
1064 cx,
1065 );
1066 build_editor(buffer.clone(), window, cx)
1067 });
1068
1069 _ = editor.update(cx, |editor, window, cx| {
1070 editor.change_selections(None, window, cx, |s| {
1071 s.select_display_ranges([
1072 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1073 ]);
1074 });
1075 editor.fold(&Fold, window, cx);
1076 assert_eq!(
1077 editor.display_text(cx),
1078 "
1079 class Foo:
1080 # Hello!
1081
1082 def a():
1083 print(1)
1084
1085 def b():⋯
1086
1087
1088 def c():⋯
1089
1090
1091 "
1092 .unindent(),
1093 );
1094
1095 editor.fold(&Fold, window, cx);
1096 assert_eq!(
1097 editor.display_text(cx),
1098 "
1099 class Foo:⋯
1100
1101
1102 "
1103 .unindent(),
1104 );
1105
1106 editor.unfold_lines(&UnfoldLines, window, cx);
1107 assert_eq!(
1108 editor.display_text(cx),
1109 "
1110 class Foo:
1111 # Hello!
1112
1113 def a():
1114 print(1)
1115
1116 def b():⋯
1117
1118
1119 def c():⋯
1120
1121
1122 "
1123 .unindent(),
1124 );
1125
1126 editor.unfold_lines(&UnfoldLines, window, cx);
1127 assert_eq!(
1128 editor.display_text(cx),
1129 editor.buffer.read(cx).read(cx).text()
1130 );
1131 });
1132}
1133
1134#[gpui::test]
1135fn test_fold_at_level(cx: &mut TestAppContext) {
1136 init_test(cx, |_| {});
1137
1138 let editor = cx.add_window(|window, cx| {
1139 let buffer = MultiBuffer::build_simple(
1140 &"
1141 class Foo:
1142 # Hello!
1143
1144 def a():
1145 print(1)
1146
1147 def b():
1148 print(2)
1149
1150
1151 class Bar:
1152 # World!
1153
1154 def a():
1155 print(1)
1156
1157 def b():
1158 print(2)
1159
1160
1161 "
1162 .unindent(),
1163 cx,
1164 );
1165 build_editor(buffer.clone(), window, cx)
1166 });
1167
1168 _ = editor.update(cx, |editor, window, cx| {
1169 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1170 assert_eq!(
1171 editor.display_text(cx),
1172 "
1173 class Foo:
1174 # Hello!
1175
1176 def a():⋯
1177
1178 def b():⋯
1179
1180
1181 class Bar:
1182 # World!
1183
1184 def a():⋯
1185
1186 def b():⋯
1187
1188
1189 "
1190 .unindent(),
1191 );
1192
1193 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1194 assert_eq!(
1195 editor.display_text(cx),
1196 "
1197 class Foo:⋯
1198
1199
1200 class Bar:⋯
1201
1202
1203 "
1204 .unindent(),
1205 );
1206
1207 editor.unfold_all(&UnfoldAll, window, cx);
1208 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1209 assert_eq!(
1210 editor.display_text(cx),
1211 "
1212 class Foo:
1213 # Hello!
1214
1215 def a():
1216 print(1)
1217
1218 def b():
1219 print(2)
1220
1221
1222 class Bar:
1223 # World!
1224
1225 def a():
1226 print(1)
1227
1228 def b():
1229 print(2)
1230
1231
1232 "
1233 .unindent(),
1234 );
1235
1236 assert_eq!(
1237 editor.display_text(cx),
1238 editor.buffer.read(cx).read(cx).text()
1239 );
1240 });
1241}
1242
1243#[gpui::test]
1244fn test_move_cursor(cx: &mut TestAppContext) {
1245 init_test(cx, |_| {});
1246
1247 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1248 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1249
1250 buffer.update(cx, |buffer, cx| {
1251 buffer.edit(
1252 vec![
1253 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1254 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1255 ],
1256 None,
1257 cx,
1258 );
1259 });
1260 _ = editor.update(cx, |editor, window, cx| {
1261 assert_eq!(
1262 editor.selections.display_ranges(cx),
1263 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1264 );
1265
1266 editor.move_down(&MoveDown, window, cx);
1267 assert_eq!(
1268 editor.selections.display_ranges(cx),
1269 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1270 );
1271
1272 editor.move_right(&MoveRight, window, cx);
1273 assert_eq!(
1274 editor.selections.display_ranges(cx),
1275 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1276 );
1277
1278 editor.move_left(&MoveLeft, window, cx);
1279 assert_eq!(
1280 editor.selections.display_ranges(cx),
1281 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1282 );
1283
1284 editor.move_up(&MoveUp, window, cx);
1285 assert_eq!(
1286 editor.selections.display_ranges(cx),
1287 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1288 );
1289
1290 editor.move_to_end(&MoveToEnd, window, cx);
1291 assert_eq!(
1292 editor.selections.display_ranges(cx),
1293 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1294 );
1295
1296 editor.move_to_beginning(&MoveToBeginning, window, cx);
1297 assert_eq!(
1298 editor.selections.display_ranges(cx),
1299 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1300 );
1301
1302 editor.change_selections(None, window, cx, |s| {
1303 s.select_display_ranges([
1304 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1305 ]);
1306 });
1307 editor.select_to_beginning(&SelectToBeginning, window, cx);
1308 assert_eq!(
1309 editor.selections.display_ranges(cx),
1310 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1311 );
1312
1313 editor.select_to_end(&SelectToEnd, window, cx);
1314 assert_eq!(
1315 editor.selections.display_ranges(cx),
1316 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1317 );
1318 });
1319}
1320
1321#[gpui::test]
1322fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1323 init_test(cx, |_| {});
1324
1325 let editor = cx.add_window(|window, cx| {
1326 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1327 build_editor(buffer.clone(), window, cx)
1328 });
1329
1330 assert_eq!('🟥'.len_utf8(), 4);
1331 assert_eq!('α'.len_utf8(), 2);
1332
1333 _ = editor.update(cx, |editor, window, cx| {
1334 editor.fold_creases(
1335 vec![
1336 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1337 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1338 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1339 ],
1340 true,
1341 window,
1342 cx,
1343 );
1344 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1345
1346 editor.move_right(&MoveRight, window, cx);
1347 assert_eq!(
1348 editor.selections.display_ranges(cx),
1349 &[empty_range(0, "🟥".len())]
1350 );
1351 editor.move_right(&MoveRight, window, cx);
1352 assert_eq!(
1353 editor.selections.display_ranges(cx),
1354 &[empty_range(0, "🟥🟧".len())]
1355 );
1356 editor.move_right(&MoveRight, window, cx);
1357 assert_eq!(
1358 editor.selections.display_ranges(cx),
1359 &[empty_range(0, "🟥🟧⋯".len())]
1360 );
1361
1362 editor.move_down(&MoveDown, window, cx);
1363 assert_eq!(
1364 editor.selections.display_ranges(cx),
1365 &[empty_range(1, "ab⋯e".len())]
1366 );
1367 editor.move_left(&MoveLeft, window, cx);
1368 assert_eq!(
1369 editor.selections.display_ranges(cx),
1370 &[empty_range(1, "ab⋯".len())]
1371 );
1372 editor.move_left(&MoveLeft, window, cx);
1373 assert_eq!(
1374 editor.selections.display_ranges(cx),
1375 &[empty_range(1, "ab".len())]
1376 );
1377 editor.move_left(&MoveLeft, window, cx);
1378 assert_eq!(
1379 editor.selections.display_ranges(cx),
1380 &[empty_range(1, "a".len())]
1381 );
1382
1383 editor.move_down(&MoveDown, window, cx);
1384 assert_eq!(
1385 editor.selections.display_ranges(cx),
1386 &[empty_range(2, "α".len())]
1387 );
1388 editor.move_right(&MoveRight, window, cx);
1389 assert_eq!(
1390 editor.selections.display_ranges(cx),
1391 &[empty_range(2, "αβ".len())]
1392 );
1393 editor.move_right(&MoveRight, window, cx);
1394 assert_eq!(
1395 editor.selections.display_ranges(cx),
1396 &[empty_range(2, "αβ⋯".len())]
1397 );
1398 editor.move_right(&MoveRight, window, cx);
1399 assert_eq!(
1400 editor.selections.display_ranges(cx),
1401 &[empty_range(2, "αβ⋯ε".len())]
1402 );
1403
1404 editor.move_up(&MoveUp, window, cx);
1405 assert_eq!(
1406 editor.selections.display_ranges(cx),
1407 &[empty_range(1, "ab⋯e".len())]
1408 );
1409 editor.move_down(&MoveDown, window, cx);
1410 assert_eq!(
1411 editor.selections.display_ranges(cx),
1412 &[empty_range(2, "αβ⋯ε".len())]
1413 );
1414 editor.move_up(&MoveUp, window, cx);
1415 assert_eq!(
1416 editor.selections.display_ranges(cx),
1417 &[empty_range(1, "ab⋯e".len())]
1418 );
1419
1420 editor.move_up(&MoveUp, window, cx);
1421 assert_eq!(
1422 editor.selections.display_ranges(cx),
1423 &[empty_range(0, "🟥🟧".len())]
1424 );
1425 editor.move_left(&MoveLeft, window, cx);
1426 assert_eq!(
1427 editor.selections.display_ranges(cx),
1428 &[empty_range(0, "🟥".len())]
1429 );
1430 editor.move_left(&MoveLeft, window, cx);
1431 assert_eq!(
1432 editor.selections.display_ranges(cx),
1433 &[empty_range(0, "".len())]
1434 );
1435 });
1436}
1437
1438#[gpui::test]
1439fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1440 init_test(cx, |_| {});
1441
1442 let editor = cx.add_window(|window, cx| {
1443 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1444 build_editor(buffer.clone(), window, cx)
1445 });
1446 _ = editor.update(cx, |editor, window, cx| {
1447 editor.change_selections(None, window, cx, |s| {
1448 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1449 });
1450
1451 // moving above start of document should move selection to start of document,
1452 // but the next move down should still be at the original goal_x
1453 editor.move_up(&MoveUp, window, cx);
1454 assert_eq!(
1455 editor.selections.display_ranges(cx),
1456 &[empty_range(0, "".len())]
1457 );
1458
1459 editor.move_down(&MoveDown, window, cx);
1460 assert_eq!(
1461 editor.selections.display_ranges(cx),
1462 &[empty_range(1, "abcd".len())]
1463 );
1464
1465 editor.move_down(&MoveDown, window, cx);
1466 assert_eq!(
1467 editor.selections.display_ranges(cx),
1468 &[empty_range(2, "αβγ".len())]
1469 );
1470
1471 editor.move_down(&MoveDown, window, cx);
1472 assert_eq!(
1473 editor.selections.display_ranges(cx),
1474 &[empty_range(3, "abcd".len())]
1475 );
1476
1477 editor.move_down(&MoveDown, window, cx);
1478 assert_eq!(
1479 editor.selections.display_ranges(cx),
1480 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1481 );
1482
1483 // moving past end of document should not change goal_x
1484 editor.move_down(&MoveDown, window, cx);
1485 assert_eq!(
1486 editor.selections.display_ranges(cx),
1487 &[empty_range(5, "".len())]
1488 );
1489
1490 editor.move_down(&MoveDown, window, cx);
1491 assert_eq!(
1492 editor.selections.display_ranges(cx),
1493 &[empty_range(5, "".len())]
1494 );
1495
1496 editor.move_up(&MoveUp, window, cx);
1497 assert_eq!(
1498 editor.selections.display_ranges(cx),
1499 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1500 );
1501
1502 editor.move_up(&MoveUp, window, cx);
1503 assert_eq!(
1504 editor.selections.display_ranges(cx),
1505 &[empty_range(3, "abcd".len())]
1506 );
1507
1508 editor.move_up(&MoveUp, window, cx);
1509 assert_eq!(
1510 editor.selections.display_ranges(cx),
1511 &[empty_range(2, "αβγ".len())]
1512 );
1513 });
1514}
1515
1516#[gpui::test]
1517fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1518 init_test(cx, |_| {});
1519 let move_to_beg = MoveToBeginningOfLine {
1520 stop_at_soft_wraps: true,
1521 stop_at_indent: true,
1522 };
1523
1524 let delete_to_beg = DeleteToBeginningOfLine {
1525 stop_at_indent: false,
1526 };
1527
1528 let move_to_end = MoveToEndOfLine {
1529 stop_at_soft_wraps: true,
1530 };
1531
1532 let editor = cx.add_window(|window, cx| {
1533 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1534 build_editor(buffer, window, cx)
1535 });
1536 _ = editor.update(cx, |editor, window, cx| {
1537 editor.change_selections(None, window, cx, |s| {
1538 s.select_display_ranges([
1539 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1540 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1541 ]);
1542 });
1543 });
1544
1545 _ = editor.update(cx, |editor, window, cx| {
1546 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1547 assert_eq!(
1548 editor.selections.display_ranges(cx),
1549 &[
1550 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1551 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1552 ]
1553 );
1554 });
1555
1556 _ = editor.update(cx, |editor, window, cx| {
1557 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1558 assert_eq!(
1559 editor.selections.display_ranges(cx),
1560 &[
1561 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1562 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1563 ]
1564 );
1565 });
1566
1567 _ = editor.update(cx, |editor, window, cx| {
1568 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1569 assert_eq!(
1570 editor.selections.display_ranges(cx),
1571 &[
1572 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1573 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1574 ]
1575 );
1576 });
1577
1578 _ = editor.update(cx, |editor, window, cx| {
1579 editor.move_to_end_of_line(&move_to_end, window, cx);
1580 assert_eq!(
1581 editor.selections.display_ranges(cx),
1582 &[
1583 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1584 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1585 ]
1586 );
1587 });
1588
1589 // Moving to the end of line again is a no-op.
1590 _ = editor.update(cx, |editor, window, cx| {
1591 editor.move_to_end_of_line(&move_to_end, window, cx);
1592 assert_eq!(
1593 editor.selections.display_ranges(cx),
1594 &[
1595 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1596 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1597 ]
1598 );
1599 });
1600
1601 _ = editor.update(cx, |editor, window, cx| {
1602 editor.move_left(&MoveLeft, window, cx);
1603 editor.select_to_beginning_of_line(
1604 &SelectToBeginningOfLine {
1605 stop_at_soft_wraps: true,
1606 stop_at_indent: true,
1607 },
1608 window,
1609 cx,
1610 );
1611 assert_eq!(
1612 editor.selections.display_ranges(cx),
1613 &[
1614 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1615 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1616 ]
1617 );
1618 });
1619
1620 _ = editor.update(cx, |editor, window, cx| {
1621 editor.select_to_beginning_of_line(
1622 &SelectToBeginningOfLine {
1623 stop_at_soft_wraps: true,
1624 stop_at_indent: true,
1625 },
1626 window,
1627 cx,
1628 );
1629 assert_eq!(
1630 editor.selections.display_ranges(cx),
1631 &[
1632 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1633 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1634 ]
1635 );
1636 });
1637
1638 _ = editor.update(cx, |editor, window, cx| {
1639 editor.select_to_beginning_of_line(
1640 &SelectToBeginningOfLine {
1641 stop_at_soft_wraps: true,
1642 stop_at_indent: true,
1643 },
1644 window,
1645 cx,
1646 );
1647 assert_eq!(
1648 editor.selections.display_ranges(cx),
1649 &[
1650 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1651 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1652 ]
1653 );
1654 });
1655
1656 _ = editor.update(cx, |editor, window, cx| {
1657 editor.select_to_end_of_line(
1658 &SelectToEndOfLine {
1659 stop_at_soft_wraps: true,
1660 },
1661 window,
1662 cx,
1663 );
1664 assert_eq!(
1665 editor.selections.display_ranges(cx),
1666 &[
1667 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1668 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1669 ]
1670 );
1671 });
1672
1673 _ = editor.update(cx, |editor, window, cx| {
1674 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1675 assert_eq!(editor.display_text(cx), "ab\n de");
1676 assert_eq!(
1677 editor.selections.display_ranges(cx),
1678 &[
1679 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1680 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1681 ]
1682 );
1683 });
1684
1685 _ = editor.update(cx, |editor, window, cx| {
1686 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1687 assert_eq!(editor.display_text(cx), "\n");
1688 assert_eq!(
1689 editor.selections.display_ranges(cx),
1690 &[
1691 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1692 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1693 ]
1694 );
1695 });
1696}
1697
1698#[gpui::test]
1699fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1700 init_test(cx, |_| {});
1701 let move_to_beg = MoveToBeginningOfLine {
1702 stop_at_soft_wraps: false,
1703 stop_at_indent: false,
1704 };
1705
1706 let move_to_end = MoveToEndOfLine {
1707 stop_at_soft_wraps: false,
1708 };
1709
1710 let editor = cx.add_window(|window, cx| {
1711 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1712 build_editor(buffer, window, cx)
1713 });
1714
1715 _ = editor.update(cx, |editor, window, cx| {
1716 editor.set_wrap_width(Some(140.0.into()), cx);
1717
1718 // We expect the following lines after wrapping
1719 // ```
1720 // thequickbrownfox
1721 // jumpedoverthelazydo
1722 // gs
1723 // ```
1724 // The final `gs` was soft-wrapped onto a new line.
1725 assert_eq!(
1726 "thequickbrownfox\njumpedoverthelaz\nydogs",
1727 editor.display_text(cx),
1728 );
1729
1730 // First, let's assert behavior on the first line, that was not soft-wrapped.
1731 // Start the cursor at the `k` on the first line
1732 editor.change_selections(None, window, cx, |s| {
1733 s.select_display_ranges([
1734 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1735 ]);
1736 });
1737
1738 // Moving to the beginning of the line should put us at the beginning of the line.
1739 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1740 assert_eq!(
1741 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1742 editor.selections.display_ranges(cx)
1743 );
1744
1745 // Moving to the end of the line should put us at the end of the line.
1746 editor.move_to_end_of_line(&move_to_end, window, cx);
1747 assert_eq!(
1748 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1749 editor.selections.display_ranges(cx)
1750 );
1751
1752 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1753 // Start the cursor at the last line (`y` that was wrapped to a new line)
1754 editor.change_selections(None, window, cx, |s| {
1755 s.select_display_ranges([
1756 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1757 ]);
1758 });
1759
1760 // Moving to the beginning of the line should put us at the start of the second line of
1761 // display text, i.e., the `j`.
1762 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1763 assert_eq!(
1764 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1765 editor.selections.display_ranges(cx)
1766 );
1767
1768 // Moving to the beginning of the line again should be a no-op.
1769 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1770 assert_eq!(
1771 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1772 editor.selections.display_ranges(cx)
1773 );
1774
1775 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1776 // next display line.
1777 editor.move_to_end_of_line(&move_to_end, window, cx);
1778 assert_eq!(
1779 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1780 editor.selections.display_ranges(cx)
1781 );
1782
1783 // Moving to the end of the line again should be a no-op.
1784 editor.move_to_end_of_line(&move_to_end, window, cx);
1785 assert_eq!(
1786 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1787 editor.selections.display_ranges(cx)
1788 );
1789 });
1790}
1791
1792#[gpui::test]
1793fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1794 init_test(cx, |_| {});
1795
1796 let move_to_beg = MoveToBeginningOfLine {
1797 stop_at_soft_wraps: true,
1798 stop_at_indent: true,
1799 };
1800
1801 let select_to_beg = SelectToBeginningOfLine {
1802 stop_at_soft_wraps: true,
1803 stop_at_indent: true,
1804 };
1805
1806 let delete_to_beg = DeleteToBeginningOfLine {
1807 stop_at_indent: true,
1808 };
1809
1810 let move_to_end = MoveToEndOfLine {
1811 stop_at_soft_wraps: false,
1812 };
1813
1814 let editor = cx.add_window(|window, cx| {
1815 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1816 build_editor(buffer, window, cx)
1817 });
1818
1819 _ = editor.update(cx, |editor, window, cx| {
1820 editor.change_selections(None, window, cx, |s| {
1821 s.select_display_ranges([
1822 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1823 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1824 ]);
1825 });
1826
1827 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1828 // and the second cursor at the first non-whitespace character in the line.
1829 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1830 assert_eq!(
1831 editor.selections.display_ranges(cx),
1832 &[
1833 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1834 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1835 ]
1836 );
1837
1838 // Moving to the beginning of the line again should be a no-op for the first cursor,
1839 // and should move the second cursor to the beginning of the line.
1840 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1841 assert_eq!(
1842 editor.selections.display_ranges(cx),
1843 &[
1844 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1845 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1846 ]
1847 );
1848
1849 // Moving to the beginning of the line again should still be a no-op for the first cursor,
1850 // and should move the second cursor back to the first non-whitespace character in the line.
1851 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1852 assert_eq!(
1853 editor.selections.display_ranges(cx),
1854 &[
1855 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1856 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1857 ]
1858 );
1859
1860 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
1861 // and to the first non-whitespace character in the line for the second cursor.
1862 editor.move_to_end_of_line(&move_to_end, window, cx);
1863 editor.move_left(&MoveLeft, window, cx);
1864 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1865 assert_eq!(
1866 editor.selections.display_ranges(cx),
1867 &[
1868 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1869 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1870 ]
1871 );
1872
1873 // Selecting to the beginning of the line again should be a no-op for the first cursor,
1874 // and should select to the beginning of the line for the second cursor.
1875 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1876 assert_eq!(
1877 editor.selections.display_ranges(cx),
1878 &[
1879 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1880 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1881 ]
1882 );
1883
1884 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
1885 // and should delete to the first non-whitespace character in the line for the second cursor.
1886 editor.move_to_end_of_line(&move_to_end, window, cx);
1887 editor.move_left(&MoveLeft, window, cx);
1888 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1889 assert_eq!(editor.text(cx), "c\n f");
1890 });
1891}
1892
1893#[gpui::test]
1894fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
1895 init_test(cx, |_| {});
1896
1897 let editor = cx.add_window(|window, cx| {
1898 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
1899 build_editor(buffer, window, cx)
1900 });
1901 _ = editor.update(cx, |editor, window, cx| {
1902 editor.change_selections(None, window, cx, |s| {
1903 s.select_display_ranges([
1904 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
1905 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
1906 ])
1907 });
1908
1909 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1910 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1911
1912 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1913 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\n ˇ{baz.qux()}", editor, cx);
1914
1915 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1916 assert_selection_ranges("use ˇstd::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
1917
1918 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1919 assert_selection_ranges("ˇuse std::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1920
1921 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1922 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1923
1924 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1925 assert_selection_ranges("useˇ std::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
1926
1927 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1928 assert_selection_ranges("use stdˇ::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1929
1930 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1931 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1932
1933 editor.move_right(&MoveRight, window, cx);
1934 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1935 assert_selection_ranges(
1936 "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
1937 editor,
1938 cx,
1939 );
1940
1941 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1942 assert_selection_ranges(
1943 "use std«ˇ::s»tr::{foo, bar}\n\n «ˇ{b»az.qux()}",
1944 editor,
1945 cx,
1946 );
1947
1948 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
1949 assert_selection_ranges(
1950 "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
1951 editor,
1952 cx,
1953 );
1954 });
1955}
1956
1957#[gpui::test]
1958fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
1959 init_test(cx, |_| {});
1960
1961 let editor = cx.add_window(|window, cx| {
1962 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
1963 build_editor(buffer, window, cx)
1964 });
1965
1966 _ = editor.update(cx, |editor, window, cx| {
1967 editor.set_wrap_width(Some(140.0.into()), cx);
1968 assert_eq!(
1969 editor.display_text(cx),
1970 "use one::{\n two::three::\n four::five\n};"
1971 );
1972
1973 editor.change_selections(None, window, cx, |s| {
1974 s.select_display_ranges([
1975 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
1976 ]);
1977 });
1978
1979 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1980 assert_eq!(
1981 editor.selections.display_ranges(cx),
1982 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
1983 );
1984
1985 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1986 assert_eq!(
1987 editor.selections.display_ranges(cx),
1988 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
1989 );
1990
1991 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1992 assert_eq!(
1993 editor.selections.display_ranges(cx),
1994 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
1995 );
1996
1997 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1998 assert_eq!(
1999 editor.selections.display_ranges(cx),
2000 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2001 );
2002
2003 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2004 assert_eq!(
2005 editor.selections.display_ranges(cx),
2006 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2007 );
2008
2009 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2010 assert_eq!(
2011 editor.selections.display_ranges(cx),
2012 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2013 );
2014 });
2015}
2016
2017#[gpui::test]
2018async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2019 init_test(cx, |_| {});
2020 let mut cx = EditorTestContext::new(cx).await;
2021
2022 let line_height = cx.editor(|editor, window, _| {
2023 editor
2024 .style()
2025 .unwrap()
2026 .text
2027 .line_height_in_pixels(window.rem_size())
2028 });
2029 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2030
2031 cx.set_state(
2032 &r#"ˇone
2033 two
2034
2035 three
2036 fourˇ
2037 five
2038
2039 six"#
2040 .unindent(),
2041 );
2042
2043 cx.update_editor(|editor, window, cx| {
2044 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2045 });
2046 cx.assert_editor_state(
2047 &r#"one
2048 two
2049 ˇ
2050 three
2051 four
2052 five
2053 ˇ
2054 six"#
2055 .unindent(),
2056 );
2057
2058 cx.update_editor(|editor, window, cx| {
2059 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2060 });
2061 cx.assert_editor_state(
2062 &r#"one
2063 two
2064
2065 three
2066 four
2067 five
2068 ˇ
2069 sixˇ"#
2070 .unindent(),
2071 );
2072
2073 cx.update_editor(|editor, window, cx| {
2074 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2075 });
2076 cx.assert_editor_state(
2077 &r#"one
2078 two
2079
2080 three
2081 four
2082 five
2083
2084 sixˇ"#
2085 .unindent(),
2086 );
2087
2088 cx.update_editor(|editor, window, cx| {
2089 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2090 });
2091 cx.assert_editor_state(
2092 &r#"one
2093 two
2094
2095 three
2096 four
2097 five
2098 ˇ
2099 six"#
2100 .unindent(),
2101 );
2102
2103 cx.update_editor(|editor, window, cx| {
2104 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2105 });
2106 cx.assert_editor_state(
2107 &r#"one
2108 two
2109 ˇ
2110 three
2111 four
2112 five
2113
2114 six"#
2115 .unindent(),
2116 );
2117
2118 cx.update_editor(|editor, window, cx| {
2119 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2120 });
2121 cx.assert_editor_state(
2122 &r#"ˇone
2123 two
2124
2125 three
2126 four
2127 five
2128
2129 six"#
2130 .unindent(),
2131 );
2132}
2133
2134#[gpui::test]
2135async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2136 init_test(cx, |_| {});
2137 let mut cx = EditorTestContext::new(cx).await;
2138 let line_height = cx.editor(|editor, window, _| {
2139 editor
2140 .style()
2141 .unwrap()
2142 .text
2143 .line_height_in_pixels(window.rem_size())
2144 });
2145 let window = cx.window;
2146 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2147
2148 cx.set_state(
2149 r#"ˇone
2150 two
2151 three
2152 four
2153 five
2154 six
2155 seven
2156 eight
2157 nine
2158 ten
2159 "#,
2160 );
2161
2162 cx.update_editor(|editor, window, cx| {
2163 assert_eq!(
2164 editor.snapshot(window, cx).scroll_position(),
2165 gpui::Point::new(0., 0.)
2166 );
2167 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2168 assert_eq!(
2169 editor.snapshot(window, cx).scroll_position(),
2170 gpui::Point::new(0., 3.)
2171 );
2172 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2173 assert_eq!(
2174 editor.snapshot(window, cx).scroll_position(),
2175 gpui::Point::new(0., 6.)
2176 );
2177 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2178 assert_eq!(
2179 editor.snapshot(window, cx).scroll_position(),
2180 gpui::Point::new(0., 3.)
2181 );
2182
2183 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2184 assert_eq!(
2185 editor.snapshot(window, cx).scroll_position(),
2186 gpui::Point::new(0., 1.)
2187 );
2188 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2189 assert_eq!(
2190 editor.snapshot(window, cx).scroll_position(),
2191 gpui::Point::new(0., 3.)
2192 );
2193 });
2194}
2195
2196#[gpui::test]
2197async fn test_autoscroll(cx: &mut TestAppContext) {
2198 init_test(cx, |_| {});
2199 let mut cx = EditorTestContext::new(cx).await;
2200
2201 let line_height = cx.update_editor(|editor, window, cx| {
2202 editor.set_vertical_scroll_margin(2, cx);
2203 editor
2204 .style()
2205 .unwrap()
2206 .text
2207 .line_height_in_pixels(window.rem_size())
2208 });
2209 let window = cx.window;
2210 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2211
2212 cx.set_state(
2213 r#"ˇone
2214 two
2215 three
2216 four
2217 five
2218 six
2219 seven
2220 eight
2221 nine
2222 ten
2223 "#,
2224 );
2225 cx.update_editor(|editor, window, cx| {
2226 assert_eq!(
2227 editor.snapshot(window, cx).scroll_position(),
2228 gpui::Point::new(0., 0.0)
2229 );
2230 });
2231
2232 // Add a cursor below the visible area. Since both cursors cannot fit
2233 // on screen, the editor autoscrolls to reveal the newest cursor, and
2234 // allows the vertical scroll margin below that cursor.
2235 cx.update_editor(|editor, window, cx| {
2236 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2237 selections.select_ranges([
2238 Point::new(0, 0)..Point::new(0, 0),
2239 Point::new(6, 0)..Point::new(6, 0),
2240 ]);
2241 })
2242 });
2243 cx.update_editor(|editor, window, cx| {
2244 assert_eq!(
2245 editor.snapshot(window, cx).scroll_position(),
2246 gpui::Point::new(0., 3.0)
2247 );
2248 });
2249
2250 // Move down. The editor cursor scrolls down to track the newest cursor.
2251 cx.update_editor(|editor, window, cx| {
2252 editor.move_down(&Default::default(), window, cx);
2253 });
2254 cx.update_editor(|editor, window, cx| {
2255 assert_eq!(
2256 editor.snapshot(window, cx).scroll_position(),
2257 gpui::Point::new(0., 4.0)
2258 );
2259 });
2260
2261 // Add a cursor above the visible area. Since both cursors fit on screen,
2262 // the editor scrolls to show both.
2263 cx.update_editor(|editor, window, cx| {
2264 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2265 selections.select_ranges([
2266 Point::new(1, 0)..Point::new(1, 0),
2267 Point::new(6, 0)..Point::new(6, 0),
2268 ]);
2269 })
2270 });
2271 cx.update_editor(|editor, window, cx| {
2272 assert_eq!(
2273 editor.snapshot(window, cx).scroll_position(),
2274 gpui::Point::new(0., 1.0)
2275 );
2276 });
2277}
2278
2279#[gpui::test]
2280async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2281 init_test(cx, |_| {});
2282 let mut cx = EditorTestContext::new(cx).await;
2283
2284 let line_height = cx.editor(|editor, window, _cx| {
2285 editor
2286 .style()
2287 .unwrap()
2288 .text
2289 .line_height_in_pixels(window.rem_size())
2290 });
2291 let window = cx.window;
2292 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2293 cx.set_state(
2294 &r#"
2295 ˇone
2296 two
2297 threeˇ
2298 four
2299 five
2300 six
2301 seven
2302 eight
2303 nine
2304 ten
2305 "#
2306 .unindent(),
2307 );
2308
2309 cx.update_editor(|editor, window, cx| {
2310 editor.move_page_down(&MovePageDown::default(), window, cx)
2311 });
2312 cx.assert_editor_state(
2313 &r#"
2314 one
2315 two
2316 three
2317 ˇfour
2318 five
2319 sixˇ
2320 seven
2321 eight
2322 nine
2323 ten
2324 "#
2325 .unindent(),
2326 );
2327
2328 cx.update_editor(|editor, window, cx| {
2329 editor.move_page_down(&MovePageDown::default(), window, cx)
2330 });
2331 cx.assert_editor_state(
2332 &r#"
2333 one
2334 two
2335 three
2336 four
2337 five
2338 six
2339 ˇseven
2340 eight
2341 nineˇ
2342 ten
2343 "#
2344 .unindent(),
2345 );
2346
2347 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2348 cx.assert_editor_state(
2349 &r#"
2350 one
2351 two
2352 three
2353 ˇfour
2354 five
2355 sixˇ
2356 seven
2357 eight
2358 nine
2359 ten
2360 "#
2361 .unindent(),
2362 );
2363
2364 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2365 cx.assert_editor_state(
2366 &r#"
2367 ˇone
2368 two
2369 threeˇ
2370 four
2371 five
2372 six
2373 seven
2374 eight
2375 nine
2376 ten
2377 "#
2378 .unindent(),
2379 );
2380
2381 // Test select collapsing
2382 cx.update_editor(|editor, window, cx| {
2383 editor.move_page_down(&MovePageDown::default(), window, cx);
2384 editor.move_page_down(&MovePageDown::default(), window, cx);
2385 editor.move_page_down(&MovePageDown::default(), window, cx);
2386 });
2387 cx.assert_editor_state(
2388 &r#"
2389 one
2390 two
2391 three
2392 four
2393 five
2394 six
2395 seven
2396 eight
2397 nine
2398 ˇten
2399 ˇ"#
2400 .unindent(),
2401 );
2402}
2403
2404#[gpui::test]
2405async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2406 init_test(cx, |_| {});
2407 let mut cx = EditorTestContext::new(cx).await;
2408 cx.set_state("one «two threeˇ» four");
2409 cx.update_editor(|editor, window, cx| {
2410 editor.delete_to_beginning_of_line(
2411 &DeleteToBeginningOfLine {
2412 stop_at_indent: false,
2413 },
2414 window,
2415 cx,
2416 );
2417 assert_eq!(editor.text(cx), " four");
2418 });
2419}
2420
2421#[gpui::test]
2422fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2423 init_test(cx, |_| {});
2424
2425 let editor = cx.add_window(|window, cx| {
2426 let buffer = MultiBuffer::build_simple("one two three four", cx);
2427 build_editor(buffer.clone(), window, cx)
2428 });
2429
2430 _ = editor.update(cx, |editor, window, cx| {
2431 editor.change_selections(None, window, cx, |s| {
2432 s.select_display_ranges([
2433 // an empty selection - the preceding word fragment is deleted
2434 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2435 // characters selected - they are deleted
2436 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 12),
2437 ])
2438 });
2439 editor.delete_to_previous_word_start(
2440 &DeleteToPreviousWordStart {
2441 ignore_newlines: false,
2442 },
2443 window,
2444 cx,
2445 );
2446 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e two te four");
2447 });
2448
2449 _ = editor.update(cx, |editor, window, cx| {
2450 editor.change_selections(None, window, cx, |s| {
2451 s.select_display_ranges([
2452 // an empty selection - the following word fragment is deleted
2453 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
2454 // characters selected - they are deleted
2455 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 10),
2456 ])
2457 });
2458 editor.delete_to_next_word_end(
2459 &DeleteToNextWordEnd {
2460 ignore_newlines: false,
2461 },
2462 window,
2463 cx,
2464 );
2465 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e t te our");
2466 });
2467}
2468
2469#[gpui::test]
2470fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2471 init_test(cx, |_| {});
2472
2473 let editor = cx.add_window(|window, cx| {
2474 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2475 build_editor(buffer.clone(), window, cx)
2476 });
2477 let del_to_prev_word_start = DeleteToPreviousWordStart {
2478 ignore_newlines: false,
2479 };
2480 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2481 ignore_newlines: true,
2482 };
2483
2484 _ = editor.update(cx, |editor, window, cx| {
2485 editor.change_selections(None, window, cx, |s| {
2486 s.select_display_ranges([
2487 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2488 ])
2489 });
2490 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2491 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
2492 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2493 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
2494 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2495 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
2496 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2497 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
2498 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2499 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
2500 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2501 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2502 });
2503}
2504
2505#[gpui::test]
2506fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
2507 init_test(cx, |_| {});
2508
2509 let editor = cx.add_window(|window, cx| {
2510 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
2511 build_editor(buffer.clone(), window, cx)
2512 });
2513 let del_to_next_word_end = DeleteToNextWordEnd {
2514 ignore_newlines: false,
2515 };
2516 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
2517 ignore_newlines: true,
2518 };
2519
2520 _ = editor.update(cx, |editor, window, cx| {
2521 editor.change_selections(None, window, cx, |s| {
2522 s.select_display_ranges([
2523 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2524 ])
2525 });
2526 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2527 assert_eq!(
2528 editor.buffer.read(cx).read(cx).text(),
2529 "one\n two\nthree\n four"
2530 );
2531 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2532 assert_eq!(
2533 editor.buffer.read(cx).read(cx).text(),
2534 "\n two\nthree\n four"
2535 );
2536 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2537 assert_eq!(
2538 editor.buffer.read(cx).read(cx).text(),
2539 "two\nthree\n four"
2540 );
2541 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2542 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
2543 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2544 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
2545 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2546 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2547 });
2548}
2549
2550#[gpui::test]
2551fn test_newline(cx: &mut TestAppContext) {
2552 init_test(cx, |_| {});
2553
2554 let editor = cx.add_window(|window, cx| {
2555 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
2556 build_editor(buffer.clone(), window, cx)
2557 });
2558
2559 _ = editor.update(cx, |editor, window, cx| {
2560 editor.change_selections(None, window, cx, |s| {
2561 s.select_display_ranges([
2562 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2563 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2564 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
2565 ])
2566 });
2567
2568 editor.newline(&Newline, window, cx);
2569 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
2570 });
2571}
2572
2573#[gpui::test]
2574fn test_newline_with_old_selections(cx: &mut TestAppContext) {
2575 init_test(cx, |_| {});
2576
2577 let editor = cx.add_window(|window, cx| {
2578 let buffer = MultiBuffer::build_simple(
2579 "
2580 a
2581 b(
2582 X
2583 )
2584 c(
2585 X
2586 )
2587 "
2588 .unindent()
2589 .as_str(),
2590 cx,
2591 );
2592 let mut editor = build_editor(buffer.clone(), window, cx);
2593 editor.change_selections(None, window, cx, |s| {
2594 s.select_ranges([
2595 Point::new(2, 4)..Point::new(2, 5),
2596 Point::new(5, 4)..Point::new(5, 5),
2597 ])
2598 });
2599 editor
2600 });
2601
2602 _ = editor.update(cx, |editor, window, cx| {
2603 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2604 editor.buffer.update(cx, |buffer, cx| {
2605 buffer.edit(
2606 [
2607 (Point::new(1, 2)..Point::new(3, 0), ""),
2608 (Point::new(4, 2)..Point::new(6, 0), ""),
2609 ],
2610 None,
2611 cx,
2612 );
2613 assert_eq!(
2614 buffer.read(cx).text(),
2615 "
2616 a
2617 b()
2618 c()
2619 "
2620 .unindent()
2621 );
2622 });
2623 assert_eq!(
2624 editor.selections.ranges(cx),
2625 &[
2626 Point::new(1, 2)..Point::new(1, 2),
2627 Point::new(2, 2)..Point::new(2, 2),
2628 ],
2629 );
2630
2631 editor.newline(&Newline, window, cx);
2632 assert_eq!(
2633 editor.text(cx),
2634 "
2635 a
2636 b(
2637 )
2638 c(
2639 )
2640 "
2641 .unindent()
2642 );
2643
2644 // The selections are moved after the inserted newlines
2645 assert_eq!(
2646 editor.selections.ranges(cx),
2647 &[
2648 Point::new(2, 0)..Point::new(2, 0),
2649 Point::new(4, 0)..Point::new(4, 0),
2650 ],
2651 );
2652 });
2653}
2654
2655#[gpui::test]
2656async fn test_newline_above(cx: &mut TestAppContext) {
2657 init_test(cx, |settings| {
2658 settings.defaults.tab_size = NonZeroU32::new(4)
2659 });
2660
2661 let language = Arc::new(
2662 Language::new(
2663 LanguageConfig::default(),
2664 Some(tree_sitter_rust::LANGUAGE.into()),
2665 )
2666 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2667 .unwrap(),
2668 );
2669
2670 let mut cx = EditorTestContext::new(cx).await;
2671 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2672 cx.set_state(indoc! {"
2673 const a: ˇA = (
2674 (ˇ
2675 «const_functionˇ»(ˇ),
2676 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2677 )ˇ
2678 ˇ);ˇ
2679 "});
2680
2681 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
2682 cx.assert_editor_state(indoc! {"
2683 ˇ
2684 const a: A = (
2685 ˇ
2686 (
2687 ˇ
2688 ˇ
2689 const_function(),
2690 ˇ
2691 ˇ
2692 ˇ
2693 ˇ
2694 something_else,
2695 ˇ
2696 )
2697 ˇ
2698 ˇ
2699 );
2700 "});
2701}
2702
2703#[gpui::test]
2704async fn test_newline_below(cx: &mut TestAppContext) {
2705 init_test(cx, |settings| {
2706 settings.defaults.tab_size = NonZeroU32::new(4)
2707 });
2708
2709 let language = Arc::new(
2710 Language::new(
2711 LanguageConfig::default(),
2712 Some(tree_sitter_rust::LANGUAGE.into()),
2713 )
2714 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2715 .unwrap(),
2716 );
2717
2718 let mut cx = EditorTestContext::new(cx).await;
2719 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2720 cx.set_state(indoc! {"
2721 const a: ˇA = (
2722 (ˇ
2723 «const_functionˇ»(ˇ),
2724 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2725 )ˇ
2726 ˇ);ˇ
2727 "});
2728
2729 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
2730 cx.assert_editor_state(indoc! {"
2731 const a: A = (
2732 ˇ
2733 (
2734 ˇ
2735 const_function(),
2736 ˇ
2737 ˇ
2738 something_else,
2739 ˇ
2740 ˇ
2741 ˇ
2742 ˇ
2743 )
2744 ˇ
2745 );
2746 ˇ
2747 ˇ
2748 "});
2749}
2750
2751#[gpui::test]
2752async fn test_newline_comments(cx: &mut TestAppContext) {
2753 init_test(cx, |settings| {
2754 settings.defaults.tab_size = NonZeroU32::new(4)
2755 });
2756
2757 let language = Arc::new(Language::new(
2758 LanguageConfig {
2759 line_comments: vec!["// ".into()],
2760 ..LanguageConfig::default()
2761 },
2762 None,
2763 ));
2764 {
2765 let mut cx = EditorTestContext::new(cx).await;
2766 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2767 cx.set_state(indoc! {"
2768 // Fooˇ
2769 "});
2770
2771 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2772 cx.assert_editor_state(indoc! {"
2773 // Foo
2774 // ˇ
2775 "});
2776 // Ensure that we add comment prefix when existing line contains space
2777 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2778 cx.assert_editor_state(
2779 indoc! {"
2780 // Foo
2781 //s
2782 // ˇ
2783 "}
2784 .replace("s", " ") // s is used as space placeholder to prevent format on save
2785 .as_str(),
2786 );
2787 // Ensure that we add comment prefix when existing line does not contain space
2788 cx.set_state(indoc! {"
2789 // Foo
2790 //ˇ
2791 "});
2792 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2793 cx.assert_editor_state(indoc! {"
2794 // Foo
2795 //
2796 // ˇ
2797 "});
2798 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
2799 cx.set_state(indoc! {"
2800 ˇ// Foo
2801 "});
2802 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2803 cx.assert_editor_state(indoc! {"
2804
2805 ˇ// Foo
2806 "});
2807 }
2808 // Ensure that comment continuations can be disabled.
2809 update_test_language_settings(cx, |settings| {
2810 settings.defaults.extend_comment_on_newline = Some(false);
2811 });
2812 let mut cx = EditorTestContext::new(cx).await;
2813 cx.set_state(indoc! {"
2814 // Fooˇ
2815 "});
2816 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2817 cx.assert_editor_state(indoc! {"
2818 // Foo
2819 ˇ
2820 "});
2821}
2822
2823#[gpui::test]
2824async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
2825 init_test(cx, |settings| {
2826 settings.defaults.tab_size = NonZeroU32::new(4)
2827 });
2828
2829 let language = Arc::new(Language::new(
2830 LanguageConfig {
2831 documentation: Some(language::DocumentationConfig {
2832 start: "/**".into(),
2833 end: "*/".into(),
2834 prefix: "* ".into(),
2835 tab_size: NonZeroU32::new(1).unwrap(),
2836 }),
2837 ..LanguageConfig::default()
2838 },
2839 None,
2840 ));
2841 {
2842 let mut cx = EditorTestContext::new(cx).await;
2843 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2844 cx.set_state(indoc! {"
2845 /**ˇ
2846 "});
2847
2848 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2849 cx.assert_editor_state(indoc! {"
2850 /**
2851 * ˇ
2852 "});
2853 // Ensure that if cursor is before the comment start,
2854 // we do not actually insert a comment prefix.
2855 cx.set_state(indoc! {"
2856 ˇ/**
2857 "});
2858 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2859 cx.assert_editor_state(indoc! {"
2860
2861 ˇ/**
2862 "});
2863 // Ensure that if cursor is between it doesn't add comment prefix.
2864 cx.set_state(indoc! {"
2865 /*ˇ*
2866 "});
2867 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2868 cx.assert_editor_state(indoc! {"
2869 /*
2870 ˇ*
2871 "});
2872 // Ensure that if suffix exists on same line after cursor it adds new line.
2873 cx.set_state(indoc! {"
2874 /**ˇ*/
2875 "});
2876 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2877 cx.assert_editor_state(indoc! {"
2878 /**
2879 * ˇ
2880 */
2881 "});
2882 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2883 cx.set_state(indoc! {"
2884 /**ˇ */
2885 "});
2886 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2887 cx.assert_editor_state(indoc! {"
2888 /**
2889 * ˇ
2890 */
2891 "});
2892 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2893 cx.set_state(indoc! {"
2894 /** ˇ*/
2895 "});
2896 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2897 cx.assert_editor_state(
2898 indoc! {"
2899 /**s
2900 * ˇ
2901 */
2902 "}
2903 .replace("s", " ") // s is used as space placeholder to prevent format on save
2904 .as_str(),
2905 );
2906 // Ensure that delimiter space is preserved when newline on already
2907 // spaced delimiter.
2908 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2909 cx.assert_editor_state(
2910 indoc! {"
2911 /**s
2912 *s
2913 * ˇ
2914 */
2915 "}
2916 .replace("s", " ") // s is used as space placeholder to prevent format on save
2917 .as_str(),
2918 );
2919 // Ensure that delimiter space is preserved when space is not
2920 // on existing delimiter.
2921 cx.set_state(indoc! {"
2922 /**
2923 *ˇ
2924 */
2925 "});
2926 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2927 cx.assert_editor_state(indoc! {"
2928 /**
2929 *
2930 * ˇ
2931 */
2932 "});
2933 // Ensure that if suffix exists on same line after cursor it
2934 // doesn't add extra new line if prefix is not on same line.
2935 cx.set_state(indoc! {"
2936 /**
2937 ˇ*/
2938 "});
2939 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2940 cx.assert_editor_state(indoc! {"
2941 /**
2942
2943 ˇ*/
2944 "});
2945 // Ensure that it detects suffix after existing prefix.
2946 cx.set_state(indoc! {"
2947 /**ˇ/
2948 "});
2949 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2950 cx.assert_editor_state(indoc! {"
2951 /**
2952 ˇ/
2953 "});
2954 // Ensure that if suffix exists on same line before
2955 // cursor it does not add comment prefix.
2956 cx.set_state(indoc! {"
2957 /** */ˇ
2958 "});
2959 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2960 cx.assert_editor_state(indoc! {"
2961 /** */
2962 ˇ
2963 "});
2964 // Ensure that if suffix exists on same line before
2965 // cursor it does not add comment prefix.
2966 cx.set_state(indoc! {"
2967 /**
2968 *
2969 */ˇ
2970 "});
2971 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2972 cx.assert_editor_state(indoc! {"
2973 /**
2974 *
2975 */
2976 ˇ
2977 "});
2978 }
2979 // Ensure that comment continuations can be disabled.
2980 update_test_language_settings(cx, |settings| {
2981 settings.defaults.extend_comment_on_newline = Some(false);
2982 });
2983 let mut cx = EditorTestContext::new(cx).await;
2984 cx.set_state(indoc! {"
2985 /**ˇ
2986 "});
2987 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2988 cx.assert_editor_state(indoc! {"
2989 /**
2990 ˇ
2991 "});
2992}
2993
2994#[gpui::test]
2995fn test_insert_with_old_selections(cx: &mut TestAppContext) {
2996 init_test(cx, |_| {});
2997
2998 let editor = cx.add_window(|window, cx| {
2999 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3000 let mut editor = build_editor(buffer.clone(), window, cx);
3001 editor.change_selections(None, window, cx, |s| {
3002 s.select_ranges([3..4, 11..12, 19..20])
3003 });
3004 editor
3005 });
3006
3007 _ = editor.update(cx, |editor, window, cx| {
3008 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3009 editor.buffer.update(cx, |buffer, cx| {
3010 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
3011 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3012 });
3013 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
3014
3015 editor.insert("Z", window, cx);
3016 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3017
3018 // The selections are moved after the inserted characters
3019 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
3020 });
3021}
3022
3023#[gpui::test]
3024async fn test_tab(cx: &mut TestAppContext) {
3025 init_test(cx, |settings| {
3026 settings.defaults.tab_size = NonZeroU32::new(3)
3027 });
3028
3029 let mut cx = EditorTestContext::new(cx).await;
3030 cx.set_state(indoc! {"
3031 ˇabˇc
3032 ˇ🏀ˇ🏀ˇefg
3033 dˇ
3034 "});
3035 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3036 cx.assert_editor_state(indoc! {"
3037 ˇab ˇc
3038 ˇ🏀 ˇ🏀 ˇefg
3039 d ˇ
3040 "});
3041
3042 cx.set_state(indoc! {"
3043 a
3044 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3045 "});
3046 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3047 cx.assert_editor_state(indoc! {"
3048 a
3049 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3050 "});
3051}
3052
3053#[gpui::test]
3054async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3055 init_test(cx, |_| {});
3056
3057 let mut cx = EditorTestContext::new(cx).await;
3058 let language = Arc::new(
3059 Language::new(
3060 LanguageConfig::default(),
3061 Some(tree_sitter_rust::LANGUAGE.into()),
3062 )
3063 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3064 .unwrap(),
3065 );
3066 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3067
3068 // test when all cursors are not at suggested indent
3069 // then simply move to their suggested indent location
3070 cx.set_state(indoc! {"
3071 const a: B = (
3072 c(
3073 ˇ
3074 ˇ )
3075 );
3076 "});
3077 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3078 cx.assert_editor_state(indoc! {"
3079 const a: B = (
3080 c(
3081 ˇ
3082 ˇ)
3083 );
3084 "});
3085
3086 // test cursor already at suggested indent not moving when
3087 // other cursors are yet to reach their suggested indents
3088 cx.set_state(indoc! {"
3089 ˇ
3090 const a: B = (
3091 c(
3092 d(
3093 ˇ
3094 )
3095 ˇ
3096 ˇ )
3097 );
3098 "});
3099 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3100 cx.assert_editor_state(indoc! {"
3101 ˇ
3102 const a: B = (
3103 c(
3104 d(
3105 ˇ
3106 )
3107 ˇ
3108 ˇ)
3109 );
3110 "});
3111 // test when all cursors are at suggested indent then tab is inserted
3112 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3113 cx.assert_editor_state(indoc! {"
3114 ˇ
3115 const a: B = (
3116 c(
3117 d(
3118 ˇ
3119 )
3120 ˇ
3121 ˇ)
3122 );
3123 "});
3124
3125 // test when current indent is less than suggested indent,
3126 // we adjust line to match suggested indent and move cursor to it
3127 //
3128 // when no other cursor is at word boundary, all of them should move
3129 cx.set_state(indoc! {"
3130 const a: B = (
3131 c(
3132 d(
3133 ˇ
3134 ˇ )
3135 ˇ )
3136 );
3137 "});
3138 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3139 cx.assert_editor_state(indoc! {"
3140 const a: B = (
3141 c(
3142 d(
3143 ˇ
3144 ˇ)
3145 ˇ)
3146 );
3147 "});
3148
3149 // test when current indent is less than suggested indent,
3150 // we adjust line to match suggested indent and move cursor to it
3151 //
3152 // when some other cursor is at word boundary, it should not move
3153 cx.set_state(indoc! {"
3154 const a: B = (
3155 c(
3156 d(
3157 ˇ
3158 ˇ )
3159 ˇ)
3160 );
3161 "});
3162 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3163 cx.assert_editor_state(indoc! {"
3164 const a: B = (
3165 c(
3166 d(
3167 ˇ
3168 ˇ)
3169 ˇ)
3170 );
3171 "});
3172
3173 // test when current indent is more than suggested indent,
3174 // we just move cursor to current indent instead of suggested indent
3175 //
3176 // when no other cursor is at word boundary, all of them should move
3177 cx.set_state(indoc! {"
3178 const a: B = (
3179 c(
3180 d(
3181 ˇ
3182 ˇ )
3183 ˇ )
3184 );
3185 "});
3186 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3187 cx.assert_editor_state(indoc! {"
3188 const a: B = (
3189 c(
3190 d(
3191 ˇ
3192 ˇ)
3193 ˇ)
3194 );
3195 "});
3196 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3197 cx.assert_editor_state(indoc! {"
3198 const a: B = (
3199 c(
3200 d(
3201 ˇ
3202 ˇ)
3203 ˇ)
3204 );
3205 "});
3206
3207 // test when current indent is more than suggested indent,
3208 // we just move cursor to current indent instead of suggested indent
3209 //
3210 // when some other cursor is at word boundary, it doesn't move
3211 cx.set_state(indoc! {"
3212 const a: B = (
3213 c(
3214 d(
3215 ˇ
3216 ˇ )
3217 ˇ)
3218 );
3219 "});
3220 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3221 cx.assert_editor_state(indoc! {"
3222 const a: B = (
3223 c(
3224 d(
3225 ˇ
3226 ˇ)
3227 ˇ)
3228 );
3229 "});
3230
3231 // handle auto-indent when there are multiple cursors on the same line
3232 cx.set_state(indoc! {"
3233 const a: B = (
3234 c(
3235 ˇ ˇ
3236 ˇ )
3237 );
3238 "});
3239 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3240 cx.assert_editor_state(indoc! {"
3241 const a: B = (
3242 c(
3243 ˇ
3244 ˇ)
3245 );
3246 "});
3247}
3248
3249#[gpui::test]
3250async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3251 init_test(cx, |settings| {
3252 settings.defaults.tab_size = NonZeroU32::new(3)
3253 });
3254
3255 let mut cx = EditorTestContext::new(cx).await;
3256 cx.set_state(indoc! {"
3257 ˇ
3258 \t ˇ
3259 \t ˇ
3260 \t ˇ
3261 \t \t\t \t \t\t \t\t \t \t ˇ
3262 "});
3263
3264 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3265 cx.assert_editor_state(indoc! {"
3266 ˇ
3267 \t ˇ
3268 \t ˇ
3269 \t ˇ
3270 \t \t\t \t \t\t \t\t \t \t ˇ
3271 "});
3272}
3273
3274#[gpui::test]
3275async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3276 init_test(cx, |settings| {
3277 settings.defaults.tab_size = NonZeroU32::new(4)
3278 });
3279
3280 let language = Arc::new(
3281 Language::new(
3282 LanguageConfig::default(),
3283 Some(tree_sitter_rust::LANGUAGE.into()),
3284 )
3285 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3286 .unwrap(),
3287 );
3288
3289 let mut cx = EditorTestContext::new(cx).await;
3290 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3291 cx.set_state(indoc! {"
3292 fn a() {
3293 if b {
3294 \t ˇc
3295 }
3296 }
3297 "});
3298
3299 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3300 cx.assert_editor_state(indoc! {"
3301 fn a() {
3302 if b {
3303 ˇc
3304 }
3305 }
3306 "});
3307}
3308
3309#[gpui::test]
3310async fn test_indent_outdent(cx: &mut TestAppContext) {
3311 init_test(cx, |settings| {
3312 settings.defaults.tab_size = NonZeroU32::new(4);
3313 });
3314
3315 let mut cx = EditorTestContext::new(cx).await;
3316
3317 cx.set_state(indoc! {"
3318 «oneˇ» «twoˇ»
3319 three
3320 four
3321 "});
3322 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3323 cx.assert_editor_state(indoc! {"
3324 «oneˇ» «twoˇ»
3325 three
3326 four
3327 "});
3328
3329 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3330 cx.assert_editor_state(indoc! {"
3331 «oneˇ» «twoˇ»
3332 three
3333 four
3334 "});
3335
3336 // select across line ending
3337 cx.set_state(indoc! {"
3338 one two
3339 t«hree
3340 ˇ» four
3341 "});
3342 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3343 cx.assert_editor_state(indoc! {"
3344 one two
3345 t«hree
3346 ˇ» four
3347 "});
3348
3349 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3350 cx.assert_editor_state(indoc! {"
3351 one two
3352 t«hree
3353 ˇ» four
3354 "});
3355
3356 // Ensure that indenting/outdenting works when the cursor is at column 0.
3357 cx.set_state(indoc! {"
3358 one two
3359 ˇthree
3360 four
3361 "});
3362 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3363 cx.assert_editor_state(indoc! {"
3364 one two
3365 ˇthree
3366 four
3367 "});
3368
3369 cx.set_state(indoc! {"
3370 one two
3371 ˇ three
3372 four
3373 "});
3374 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3375 cx.assert_editor_state(indoc! {"
3376 one two
3377 ˇthree
3378 four
3379 "});
3380}
3381
3382#[gpui::test]
3383async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
3384 init_test(cx, |settings| {
3385 settings.defaults.hard_tabs = Some(true);
3386 });
3387
3388 let mut cx = EditorTestContext::new(cx).await;
3389
3390 // select two ranges on one line
3391 cx.set_state(indoc! {"
3392 «oneˇ» «twoˇ»
3393 three
3394 four
3395 "});
3396 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3397 cx.assert_editor_state(indoc! {"
3398 \t«oneˇ» «twoˇ»
3399 three
3400 four
3401 "});
3402 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3403 cx.assert_editor_state(indoc! {"
3404 \t\t«oneˇ» «twoˇ»
3405 three
3406 four
3407 "});
3408 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3409 cx.assert_editor_state(indoc! {"
3410 \t«oneˇ» «twoˇ»
3411 three
3412 four
3413 "});
3414 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3415 cx.assert_editor_state(indoc! {"
3416 «oneˇ» «twoˇ»
3417 three
3418 four
3419 "});
3420
3421 // select across a line ending
3422 cx.set_state(indoc! {"
3423 one two
3424 t«hree
3425 ˇ»four
3426 "});
3427 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3428 cx.assert_editor_state(indoc! {"
3429 one two
3430 \tt«hree
3431 ˇ»four
3432 "});
3433 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3434 cx.assert_editor_state(indoc! {"
3435 one two
3436 \t\tt«hree
3437 ˇ»four
3438 "});
3439 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3440 cx.assert_editor_state(indoc! {"
3441 one two
3442 \tt«hree
3443 ˇ»four
3444 "});
3445 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3446 cx.assert_editor_state(indoc! {"
3447 one two
3448 t«hree
3449 ˇ»four
3450 "});
3451
3452 // Ensure that indenting/outdenting works when the cursor is at column 0.
3453 cx.set_state(indoc! {"
3454 one two
3455 ˇthree
3456 four
3457 "});
3458 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3459 cx.assert_editor_state(indoc! {"
3460 one two
3461 ˇthree
3462 four
3463 "});
3464 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3465 cx.assert_editor_state(indoc! {"
3466 one two
3467 \tˇthree
3468 four
3469 "});
3470 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3471 cx.assert_editor_state(indoc! {"
3472 one two
3473 ˇthree
3474 four
3475 "});
3476}
3477
3478#[gpui::test]
3479fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
3480 init_test(cx, |settings| {
3481 settings.languages.extend([
3482 (
3483 "TOML".into(),
3484 LanguageSettingsContent {
3485 tab_size: NonZeroU32::new(2),
3486 ..Default::default()
3487 },
3488 ),
3489 (
3490 "Rust".into(),
3491 LanguageSettingsContent {
3492 tab_size: NonZeroU32::new(4),
3493 ..Default::default()
3494 },
3495 ),
3496 ]);
3497 });
3498
3499 let toml_language = Arc::new(Language::new(
3500 LanguageConfig {
3501 name: "TOML".into(),
3502 ..Default::default()
3503 },
3504 None,
3505 ));
3506 let rust_language = Arc::new(Language::new(
3507 LanguageConfig {
3508 name: "Rust".into(),
3509 ..Default::default()
3510 },
3511 None,
3512 ));
3513
3514 let toml_buffer =
3515 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
3516 let rust_buffer =
3517 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
3518 let multibuffer = cx.new(|cx| {
3519 let mut multibuffer = MultiBuffer::new(ReadWrite);
3520 multibuffer.push_excerpts(
3521 toml_buffer.clone(),
3522 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
3523 cx,
3524 );
3525 multibuffer.push_excerpts(
3526 rust_buffer.clone(),
3527 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
3528 cx,
3529 );
3530 multibuffer
3531 });
3532
3533 cx.add_window(|window, cx| {
3534 let mut editor = build_editor(multibuffer, window, cx);
3535
3536 assert_eq!(
3537 editor.text(cx),
3538 indoc! {"
3539 a = 1
3540 b = 2
3541
3542 const c: usize = 3;
3543 "}
3544 );
3545
3546 select_ranges(
3547 &mut editor,
3548 indoc! {"
3549 «aˇ» = 1
3550 b = 2
3551
3552 «const c:ˇ» usize = 3;
3553 "},
3554 window,
3555 cx,
3556 );
3557
3558 editor.tab(&Tab, window, cx);
3559 assert_text_with_selections(
3560 &mut editor,
3561 indoc! {"
3562 «aˇ» = 1
3563 b = 2
3564
3565 «const c:ˇ» usize = 3;
3566 "},
3567 cx,
3568 );
3569 editor.backtab(&Backtab, window, cx);
3570 assert_text_with_selections(
3571 &mut editor,
3572 indoc! {"
3573 «aˇ» = 1
3574 b = 2
3575
3576 «const c:ˇ» usize = 3;
3577 "},
3578 cx,
3579 );
3580
3581 editor
3582 });
3583}
3584
3585#[gpui::test]
3586async fn test_backspace(cx: &mut TestAppContext) {
3587 init_test(cx, |_| {});
3588
3589 let mut cx = EditorTestContext::new(cx).await;
3590
3591 // Basic backspace
3592 cx.set_state(indoc! {"
3593 onˇe two three
3594 fou«rˇ» five six
3595 seven «ˇeight nine
3596 »ten
3597 "});
3598 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3599 cx.assert_editor_state(indoc! {"
3600 oˇe two three
3601 fouˇ five six
3602 seven ˇten
3603 "});
3604
3605 // Test backspace inside and around indents
3606 cx.set_state(indoc! {"
3607 zero
3608 ˇone
3609 ˇtwo
3610 ˇ ˇ ˇ three
3611 ˇ ˇ four
3612 "});
3613 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3614 cx.assert_editor_state(indoc! {"
3615 zero
3616 ˇone
3617 ˇtwo
3618 ˇ threeˇ four
3619 "});
3620}
3621
3622#[gpui::test]
3623async fn test_delete(cx: &mut TestAppContext) {
3624 init_test(cx, |_| {});
3625
3626 let mut cx = EditorTestContext::new(cx).await;
3627 cx.set_state(indoc! {"
3628 onˇe two three
3629 fou«rˇ» five six
3630 seven «ˇeight nine
3631 »ten
3632 "});
3633 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
3634 cx.assert_editor_state(indoc! {"
3635 onˇ two three
3636 fouˇ five six
3637 seven ˇten
3638 "});
3639}
3640
3641#[gpui::test]
3642fn test_delete_line(cx: &mut TestAppContext) {
3643 init_test(cx, |_| {});
3644
3645 let editor = cx.add_window(|window, cx| {
3646 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3647 build_editor(buffer, window, cx)
3648 });
3649 _ = editor.update(cx, |editor, window, cx| {
3650 editor.change_selections(None, window, cx, |s| {
3651 s.select_display_ranges([
3652 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
3653 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3654 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3655 ])
3656 });
3657 editor.delete_line(&DeleteLine, window, cx);
3658 assert_eq!(editor.display_text(cx), "ghi");
3659 assert_eq!(
3660 editor.selections.display_ranges(cx),
3661 vec![
3662 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
3663 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
3664 ]
3665 );
3666 });
3667
3668 let editor = cx.add_window(|window, cx| {
3669 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3670 build_editor(buffer, window, cx)
3671 });
3672 _ = editor.update(cx, |editor, window, cx| {
3673 editor.change_selections(None, window, cx, |s| {
3674 s.select_display_ranges([
3675 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
3676 ])
3677 });
3678 editor.delete_line(&DeleteLine, window, cx);
3679 assert_eq!(editor.display_text(cx), "ghi\n");
3680 assert_eq!(
3681 editor.selections.display_ranges(cx),
3682 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
3683 );
3684 });
3685}
3686
3687#[gpui::test]
3688fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
3689 init_test(cx, |_| {});
3690
3691 cx.add_window(|window, cx| {
3692 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3693 let mut editor = build_editor(buffer.clone(), window, cx);
3694 let buffer = buffer.read(cx).as_singleton().unwrap();
3695
3696 assert_eq!(
3697 editor.selections.ranges::<Point>(cx),
3698 &[Point::new(0, 0)..Point::new(0, 0)]
3699 );
3700
3701 // When on single line, replace newline at end by space
3702 editor.join_lines(&JoinLines, window, cx);
3703 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3704 assert_eq!(
3705 editor.selections.ranges::<Point>(cx),
3706 &[Point::new(0, 3)..Point::new(0, 3)]
3707 );
3708
3709 // When multiple lines are selected, remove newlines that are spanned by the selection
3710 editor.change_selections(None, window, cx, |s| {
3711 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
3712 });
3713 editor.join_lines(&JoinLines, window, cx);
3714 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
3715 assert_eq!(
3716 editor.selections.ranges::<Point>(cx),
3717 &[Point::new(0, 11)..Point::new(0, 11)]
3718 );
3719
3720 // Undo should be transactional
3721 editor.undo(&Undo, window, cx);
3722 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3723 assert_eq!(
3724 editor.selections.ranges::<Point>(cx),
3725 &[Point::new(0, 5)..Point::new(2, 2)]
3726 );
3727
3728 // When joining an empty line don't insert a space
3729 editor.change_selections(None, window, cx, |s| {
3730 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
3731 });
3732 editor.join_lines(&JoinLines, window, cx);
3733 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
3734 assert_eq!(
3735 editor.selections.ranges::<Point>(cx),
3736 [Point::new(2, 3)..Point::new(2, 3)]
3737 );
3738
3739 // We can remove trailing newlines
3740 editor.join_lines(&JoinLines, window, cx);
3741 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3742 assert_eq!(
3743 editor.selections.ranges::<Point>(cx),
3744 [Point::new(2, 3)..Point::new(2, 3)]
3745 );
3746
3747 // We don't blow up on the last line
3748 editor.join_lines(&JoinLines, window, cx);
3749 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3750 assert_eq!(
3751 editor.selections.ranges::<Point>(cx),
3752 [Point::new(2, 3)..Point::new(2, 3)]
3753 );
3754
3755 // reset to test indentation
3756 editor.buffer.update(cx, |buffer, cx| {
3757 buffer.edit(
3758 [
3759 (Point::new(1, 0)..Point::new(1, 2), " "),
3760 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
3761 ],
3762 None,
3763 cx,
3764 )
3765 });
3766
3767 // We remove any leading spaces
3768 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
3769 editor.change_selections(None, window, cx, |s| {
3770 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
3771 });
3772 editor.join_lines(&JoinLines, window, cx);
3773 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
3774
3775 // We don't insert a space for a line containing only spaces
3776 editor.join_lines(&JoinLines, window, cx);
3777 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
3778
3779 // We ignore any leading tabs
3780 editor.join_lines(&JoinLines, window, cx);
3781 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
3782
3783 editor
3784 });
3785}
3786
3787#[gpui::test]
3788fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
3789 init_test(cx, |_| {});
3790
3791 cx.add_window(|window, cx| {
3792 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3793 let mut editor = build_editor(buffer.clone(), window, cx);
3794 let buffer = buffer.read(cx).as_singleton().unwrap();
3795
3796 editor.change_selections(None, window, cx, |s| {
3797 s.select_ranges([
3798 Point::new(0, 2)..Point::new(1, 1),
3799 Point::new(1, 2)..Point::new(1, 2),
3800 Point::new(3, 1)..Point::new(3, 2),
3801 ])
3802 });
3803
3804 editor.join_lines(&JoinLines, window, cx);
3805 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
3806
3807 assert_eq!(
3808 editor.selections.ranges::<Point>(cx),
3809 [
3810 Point::new(0, 7)..Point::new(0, 7),
3811 Point::new(1, 3)..Point::new(1, 3)
3812 ]
3813 );
3814 editor
3815 });
3816}
3817
3818#[gpui::test]
3819async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
3820 init_test(cx, |_| {});
3821
3822 let mut cx = EditorTestContext::new(cx).await;
3823
3824 let diff_base = r#"
3825 Line 0
3826 Line 1
3827 Line 2
3828 Line 3
3829 "#
3830 .unindent();
3831
3832 cx.set_state(
3833 &r#"
3834 ˇLine 0
3835 Line 1
3836 Line 2
3837 Line 3
3838 "#
3839 .unindent(),
3840 );
3841
3842 cx.set_head_text(&diff_base);
3843 executor.run_until_parked();
3844
3845 // Join lines
3846 cx.update_editor(|editor, window, cx| {
3847 editor.join_lines(&JoinLines, window, cx);
3848 });
3849 executor.run_until_parked();
3850
3851 cx.assert_editor_state(
3852 &r#"
3853 Line 0ˇ Line 1
3854 Line 2
3855 Line 3
3856 "#
3857 .unindent(),
3858 );
3859 // Join again
3860 cx.update_editor(|editor, window, cx| {
3861 editor.join_lines(&JoinLines, window, cx);
3862 });
3863 executor.run_until_parked();
3864
3865 cx.assert_editor_state(
3866 &r#"
3867 Line 0 Line 1ˇ Line 2
3868 Line 3
3869 "#
3870 .unindent(),
3871 );
3872}
3873
3874#[gpui::test]
3875async fn test_custom_newlines_cause_no_false_positive_diffs(
3876 executor: BackgroundExecutor,
3877 cx: &mut TestAppContext,
3878) {
3879 init_test(cx, |_| {});
3880 let mut cx = EditorTestContext::new(cx).await;
3881 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
3882 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
3883 executor.run_until_parked();
3884
3885 cx.update_editor(|editor, window, cx| {
3886 let snapshot = editor.snapshot(window, cx);
3887 assert_eq!(
3888 snapshot
3889 .buffer_snapshot
3890 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
3891 .collect::<Vec<_>>(),
3892 Vec::new(),
3893 "Should not have any diffs for files with custom newlines"
3894 );
3895 });
3896}
3897
3898#[gpui::test]
3899async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) {
3900 init_test(cx, |_| {});
3901
3902 let mut cx = EditorTestContext::new(cx).await;
3903
3904 // Test sort_lines_case_insensitive()
3905 cx.set_state(indoc! {"
3906 «z
3907 y
3908 x
3909 Z
3910 Y
3911 Xˇ»
3912 "});
3913 cx.update_editor(|e, window, cx| {
3914 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
3915 });
3916 cx.assert_editor_state(indoc! {"
3917 «x
3918 X
3919 y
3920 Y
3921 z
3922 Zˇ»
3923 "});
3924
3925 // Test reverse_lines()
3926 cx.set_state(indoc! {"
3927 «5
3928 4
3929 3
3930 2
3931 1ˇ»
3932 "});
3933 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
3934 cx.assert_editor_state(indoc! {"
3935 «1
3936 2
3937 3
3938 4
3939 5ˇ»
3940 "});
3941
3942 // Skip testing shuffle_line()
3943
3944 // From here on out, test more complex cases of manipulate_lines() with a single driver method: sort_lines_case_sensitive()
3945 // Since all methods calling manipulate_lines() are doing the exact same general thing (reordering lines)
3946
3947 // Don't manipulate when cursor is on single line, but expand the selection
3948 cx.set_state(indoc! {"
3949 ddˇdd
3950 ccc
3951 bb
3952 a
3953 "});
3954 cx.update_editor(|e, window, cx| {
3955 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3956 });
3957 cx.assert_editor_state(indoc! {"
3958 «ddddˇ»
3959 ccc
3960 bb
3961 a
3962 "});
3963
3964 // Basic manipulate case
3965 // Start selection moves to column 0
3966 // End of selection shrinks to fit shorter line
3967 cx.set_state(indoc! {"
3968 dd«d
3969 ccc
3970 bb
3971 aaaaaˇ»
3972 "});
3973 cx.update_editor(|e, window, cx| {
3974 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3975 });
3976 cx.assert_editor_state(indoc! {"
3977 «aaaaa
3978 bb
3979 ccc
3980 dddˇ»
3981 "});
3982
3983 // Manipulate case with newlines
3984 cx.set_state(indoc! {"
3985 dd«d
3986 ccc
3987
3988 bb
3989 aaaaa
3990
3991 ˇ»
3992 "});
3993 cx.update_editor(|e, window, cx| {
3994 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3995 });
3996 cx.assert_editor_state(indoc! {"
3997 «
3998
3999 aaaaa
4000 bb
4001 ccc
4002 dddˇ»
4003
4004 "});
4005
4006 // Adding new line
4007 cx.set_state(indoc! {"
4008 aa«a
4009 bbˇ»b
4010 "});
4011 cx.update_editor(|e, window, cx| {
4012 e.manipulate_lines(window, cx, |lines| lines.push("added_line"))
4013 });
4014 cx.assert_editor_state(indoc! {"
4015 «aaa
4016 bbb
4017 added_lineˇ»
4018 "});
4019
4020 // Removing line
4021 cx.set_state(indoc! {"
4022 aa«a
4023 bbbˇ»
4024 "});
4025 cx.update_editor(|e, window, cx| {
4026 e.manipulate_lines(window, cx, |lines| {
4027 lines.pop();
4028 })
4029 });
4030 cx.assert_editor_state(indoc! {"
4031 «aaaˇ»
4032 "});
4033
4034 // Removing all lines
4035 cx.set_state(indoc! {"
4036 aa«a
4037 bbbˇ»
4038 "});
4039 cx.update_editor(|e, window, cx| {
4040 e.manipulate_lines(window, cx, |lines| {
4041 lines.drain(..);
4042 })
4043 });
4044 cx.assert_editor_state(indoc! {"
4045 ˇ
4046 "});
4047}
4048
4049#[gpui::test]
4050async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4051 init_test(cx, |_| {});
4052
4053 let mut cx = EditorTestContext::new(cx).await;
4054
4055 // Consider continuous selection as single selection
4056 cx.set_state(indoc! {"
4057 Aaa«aa
4058 cˇ»c«c
4059 bb
4060 aaaˇ»aa
4061 "});
4062 cx.update_editor(|e, window, cx| {
4063 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4064 });
4065 cx.assert_editor_state(indoc! {"
4066 «Aaaaa
4067 ccc
4068 bb
4069 aaaaaˇ»
4070 "});
4071
4072 cx.set_state(indoc! {"
4073 Aaa«aa
4074 cˇ»c«c
4075 bb
4076 aaaˇ»aa
4077 "});
4078 cx.update_editor(|e, window, cx| {
4079 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4080 });
4081 cx.assert_editor_state(indoc! {"
4082 «Aaaaa
4083 ccc
4084 bbˇ»
4085 "});
4086
4087 // Consider non continuous selection as distinct dedup operations
4088 cx.set_state(indoc! {"
4089 «aaaaa
4090 bb
4091 aaaaa
4092 aaaaaˇ»
4093
4094 aaa«aaˇ»
4095 "});
4096 cx.update_editor(|e, window, cx| {
4097 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4098 });
4099 cx.assert_editor_state(indoc! {"
4100 «aaaaa
4101 bbˇ»
4102
4103 «aaaaaˇ»
4104 "});
4105}
4106
4107#[gpui::test]
4108async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4109 init_test(cx, |_| {});
4110
4111 let mut cx = EditorTestContext::new(cx).await;
4112
4113 cx.set_state(indoc! {"
4114 «Aaa
4115 aAa
4116 Aaaˇ»
4117 "});
4118 cx.update_editor(|e, window, cx| {
4119 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4120 });
4121 cx.assert_editor_state(indoc! {"
4122 «Aaa
4123 aAaˇ»
4124 "});
4125
4126 cx.set_state(indoc! {"
4127 «Aaa
4128 aAa
4129 aaAˇ»
4130 "});
4131 cx.update_editor(|e, window, cx| {
4132 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4133 });
4134 cx.assert_editor_state(indoc! {"
4135 «Aaaˇ»
4136 "});
4137}
4138
4139#[gpui::test]
4140async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) {
4141 init_test(cx, |_| {});
4142
4143 let mut cx = EditorTestContext::new(cx).await;
4144
4145 // Manipulate with multiple selections on a single line
4146 cx.set_state(indoc! {"
4147 dd«dd
4148 cˇ»c«c
4149 bb
4150 aaaˇ»aa
4151 "});
4152 cx.update_editor(|e, window, cx| {
4153 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4154 });
4155 cx.assert_editor_state(indoc! {"
4156 «aaaaa
4157 bb
4158 ccc
4159 ddddˇ»
4160 "});
4161
4162 // Manipulate with multiple disjoin selections
4163 cx.set_state(indoc! {"
4164 5«
4165 4
4166 3
4167 2
4168 1ˇ»
4169
4170 dd«dd
4171 ccc
4172 bb
4173 aaaˇ»aa
4174 "});
4175 cx.update_editor(|e, window, cx| {
4176 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4177 });
4178 cx.assert_editor_state(indoc! {"
4179 «1
4180 2
4181 3
4182 4
4183 5ˇ»
4184
4185 «aaaaa
4186 bb
4187 ccc
4188 ddddˇ»
4189 "});
4190
4191 // Adding lines on each selection
4192 cx.set_state(indoc! {"
4193 2«
4194 1ˇ»
4195
4196 bb«bb
4197 aaaˇ»aa
4198 "});
4199 cx.update_editor(|e, window, cx| {
4200 e.manipulate_lines(window, cx, |lines| lines.push("added line"))
4201 });
4202 cx.assert_editor_state(indoc! {"
4203 «2
4204 1
4205 added lineˇ»
4206
4207 «bbbb
4208 aaaaa
4209 added lineˇ»
4210 "});
4211
4212 // Removing lines on each selection
4213 cx.set_state(indoc! {"
4214 2«
4215 1ˇ»
4216
4217 bb«bb
4218 aaaˇ»aa
4219 "});
4220 cx.update_editor(|e, window, cx| {
4221 e.manipulate_lines(window, cx, |lines| {
4222 lines.pop();
4223 })
4224 });
4225 cx.assert_editor_state(indoc! {"
4226 «2ˇ»
4227
4228 «bbbbˇ»
4229 "});
4230}
4231
4232#[gpui::test]
4233async fn test_toggle_case(cx: &mut TestAppContext) {
4234 init_test(cx, |_| {});
4235
4236 let mut cx = EditorTestContext::new(cx).await;
4237
4238 // If all lower case -> upper case
4239 cx.set_state(indoc! {"
4240 «hello worldˇ»
4241 "});
4242 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4243 cx.assert_editor_state(indoc! {"
4244 «HELLO WORLDˇ»
4245 "});
4246
4247 // If all upper case -> lower case
4248 cx.set_state(indoc! {"
4249 «HELLO WORLDˇ»
4250 "});
4251 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4252 cx.assert_editor_state(indoc! {"
4253 «hello worldˇ»
4254 "});
4255
4256 // If any upper case characters are identified -> lower case
4257 // This matches JetBrains IDEs
4258 cx.set_state(indoc! {"
4259 «hEllo worldˇ»
4260 "});
4261 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4262 cx.assert_editor_state(indoc! {"
4263 «hello worldˇ»
4264 "});
4265}
4266
4267#[gpui::test]
4268async fn test_manipulate_text(cx: &mut TestAppContext) {
4269 init_test(cx, |_| {});
4270
4271 let mut cx = EditorTestContext::new(cx).await;
4272
4273 // Test convert_to_upper_case()
4274 cx.set_state(indoc! {"
4275 «hello worldˇ»
4276 "});
4277 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4278 cx.assert_editor_state(indoc! {"
4279 «HELLO WORLDˇ»
4280 "});
4281
4282 // Test convert_to_lower_case()
4283 cx.set_state(indoc! {"
4284 «HELLO WORLDˇ»
4285 "});
4286 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
4287 cx.assert_editor_state(indoc! {"
4288 «hello worldˇ»
4289 "});
4290
4291 // Test multiple line, single selection case
4292 cx.set_state(indoc! {"
4293 «The quick brown
4294 fox jumps over
4295 the lazy dogˇ»
4296 "});
4297 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
4298 cx.assert_editor_state(indoc! {"
4299 «The Quick Brown
4300 Fox Jumps Over
4301 The Lazy Dogˇ»
4302 "});
4303
4304 // Test multiple line, single selection case
4305 cx.set_state(indoc! {"
4306 «The quick brown
4307 fox jumps over
4308 the lazy dogˇ»
4309 "});
4310 cx.update_editor(|e, window, cx| {
4311 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
4312 });
4313 cx.assert_editor_state(indoc! {"
4314 «TheQuickBrown
4315 FoxJumpsOver
4316 TheLazyDogˇ»
4317 "});
4318
4319 // From here on out, test more complex cases of manipulate_text()
4320
4321 // Test no selection case - should affect words cursors are in
4322 // Cursor at beginning, middle, and end of word
4323 cx.set_state(indoc! {"
4324 ˇhello big beauˇtiful worldˇ
4325 "});
4326 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4327 cx.assert_editor_state(indoc! {"
4328 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
4329 "});
4330
4331 // Test multiple selections on a single line and across multiple lines
4332 cx.set_state(indoc! {"
4333 «Theˇ» quick «brown
4334 foxˇ» jumps «overˇ»
4335 the «lazyˇ» dog
4336 "});
4337 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4338 cx.assert_editor_state(indoc! {"
4339 «THEˇ» quick «BROWN
4340 FOXˇ» jumps «OVERˇ»
4341 the «LAZYˇ» dog
4342 "});
4343
4344 // Test case where text length grows
4345 cx.set_state(indoc! {"
4346 «tschüߡ»
4347 "});
4348 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4349 cx.assert_editor_state(indoc! {"
4350 «TSCHÜSSˇ»
4351 "});
4352
4353 // Test to make sure we don't crash when text shrinks
4354 cx.set_state(indoc! {"
4355 aaa_bbbˇ
4356 "});
4357 cx.update_editor(|e, window, cx| {
4358 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4359 });
4360 cx.assert_editor_state(indoc! {"
4361 «aaaBbbˇ»
4362 "});
4363
4364 // Test to make sure we all aware of the fact that each word can grow and shrink
4365 // Final selections should be aware of this fact
4366 cx.set_state(indoc! {"
4367 aaa_bˇbb bbˇb_ccc ˇccc_ddd
4368 "});
4369 cx.update_editor(|e, window, cx| {
4370 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4371 });
4372 cx.assert_editor_state(indoc! {"
4373 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
4374 "});
4375
4376 cx.set_state(indoc! {"
4377 «hElLo, WoRld!ˇ»
4378 "});
4379 cx.update_editor(|e, window, cx| {
4380 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
4381 });
4382 cx.assert_editor_state(indoc! {"
4383 «HeLlO, wOrLD!ˇ»
4384 "});
4385}
4386
4387#[gpui::test]
4388fn test_duplicate_line(cx: &mut TestAppContext) {
4389 init_test(cx, |_| {});
4390
4391 let editor = cx.add_window(|window, cx| {
4392 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4393 build_editor(buffer, window, cx)
4394 });
4395 _ = editor.update(cx, |editor, window, cx| {
4396 editor.change_selections(None, window, cx, |s| {
4397 s.select_display_ranges([
4398 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4399 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4400 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4401 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4402 ])
4403 });
4404 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4405 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4406 assert_eq!(
4407 editor.selections.display_ranges(cx),
4408 vec![
4409 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4410 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
4411 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4412 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4413 ]
4414 );
4415 });
4416
4417 let editor = cx.add_window(|window, cx| {
4418 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4419 build_editor(buffer, window, cx)
4420 });
4421 _ = editor.update(cx, |editor, window, cx| {
4422 editor.change_selections(None, window, cx, |s| {
4423 s.select_display_ranges([
4424 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4425 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4426 ])
4427 });
4428 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4429 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4430 assert_eq!(
4431 editor.selections.display_ranges(cx),
4432 vec![
4433 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
4434 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
4435 ]
4436 );
4437 });
4438
4439 // With `move_upwards` the selections stay in place, except for
4440 // the lines inserted above them
4441 let editor = cx.add_window(|window, cx| {
4442 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4443 build_editor(buffer, window, cx)
4444 });
4445 _ = editor.update(cx, |editor, window, cx| {
4446 editor.change_selections(None, window, cx, |s| {
4447 s.select_display_ranges([
4448 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4449 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4450 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4451 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4452 ])
4453 });
4454 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4455 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4456 assert_eq!(
4457 editor.selections.display_ranges(cx),
4458 vec![
4459 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4460 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4461 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
4462 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4463 ]
4464 );
4465 });
4466
4467 let editor = cx.add_window(|window, cx| {
4468 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4469 build_editor(buffer, window, cx)
4470 });
4471 _ = editor.update(cx, |editor, window, cx| {
4472 editor.change_selections(None, window, cx, |s| {
4473 s.select_display_ranges([
4474 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4475 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4476 ])
4477 });
4478 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4479 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4480 assert_eq!(
4481 editor.selections.display_ranges(cx),
4482 vec![
4483 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4484 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4485 ]
4486 );
4487 });
4488
4489 let editor = cx.add_window(|window, cx| {
4490 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4491 build_editor(buffer, window, cx)
4492 });
4493 _ = editor.update(cx, |editor, window, cx| {
4494 editor.change_selections(None, window, cx, |s| {
4495 s.select_display_ranges([
4496 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4497 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4498 ])
4499 });
4500 editor.duplicate_selection(&DuplicateSelection, window, cx);
4501 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
4502 assert_eq!(
4503 editor.selections.display_ranges(cx),
4504 vec![
4505 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4506 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
4507 ]
4508 );
4509 });
4510}
4511
4512#[gpui::test]
4513fn test_move_line_up_down(cx: &mut TestAppContext) {
4514 init_test(cx, |_| {});
4515
4516 let editor = cx.add_window(|window, cx| {
4517 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4518 build_editor(buffer, window, cx)
4519 });
4520 _ = editor.update(cx, |editor, window, cx| {
4521 editor.fold_creases(
4522 vec![
4523 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
4524 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
4525 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
4526 ],
4527 true,
4528 window,
4529 cx,
4530 );
4531 editor.change_selections(None, window, cx, |s| {
4532 s.select_display_ranges([
4533 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4534 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4535 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4536 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
4537 ])
4538 });
4539 assert_eq!(
4540 editor.display_text(cx),
4541 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
4542 );
4543
4544 editor.move_line_up(&MoveLineUp, window, cx);
4545 assert_eq!(
4546 editor.display_text(cx),
4547 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
4548 );
4549 assert_eq!(
4550 editor.selections.display_ranges(cx),
4551 vec![
4552 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4553 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4554 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4555 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4556 ]
4557 );
4558 });
4559
4560 _ = editor.update(cx, |editor, window, cx| {
4561 editor.move_line_down(&MoveLineDown, window, cx);
4562 assert_eq!(
4563 editor.display_text(cx),
4564 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
4565 );
4566 assert_eq!(
4567 editor.selections.display_ranges(cx),
4568 vec![
4569 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4570 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4571 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4572 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4573 ]
4574 );
4575 });
4576
4577 _ = editor.update(cx, |editor, window, cx| {
4578 editor.move_line_down(&MoveLineDown, window, cx);
4579 assert_eq!(
4580 editor.display_text(cx),
4581 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
4582 );
4583 assert_eq!(
4584 editor.selections.display_ranges(cx),
4585 vec![
4586 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4587 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4588 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4589 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4590 ]
4591 );
4592 });
4593
4594 _ = editor.update(cx, |editor, window, cx| {
4595 editor.move_line_up(&MoveLineUp, window, cx);
4596 assert_eq!(
4597 editor.display_text(cx),
4598 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
4599 );
4600 assert_eq!(
4601 editor.selections.display_ranges(cx),
4602 vec![
4603 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4604 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4605 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4606 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4607 ]
4608 );
4609 });
4610}
4611
4612#[gpui::test]
4613fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
4614 init_test(cx, |_| {});
4615
4616 let editor = cx.add_window(|window, cx| {
4617 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4618 build_editor(buffer, window, cx)
4619 });
4620 _ = editor.update(cx, |editor, window, cx| {
4621 let snapshot = editor.buffer.read(cx).snapshot(cx);
4622 editor.insert_blocks(
4623 [BlockProperties {
4624 style: BlockStyle::Fixed,
4625 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
4626 height: Some(1),
4627 render: Arc::new(|_| div().into_any()),
4628 priority: 0,
4629 render_in_minimap: true,
4630 }],
4631 Some(Autoscroll::fit()),
4632 cx,
4633 );
4634 editor.change_selections(None, window, cx, |s| {
4635 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
4636 });
4637 editor.move_line_down(&MoveLineDown, window, cx);
4638 });
4639}
4640
4641#[gpui::test]
4642async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
4643 init_test(cx, |_| {});
4644
4645 let mut cx = EditorTestContext::new(cx).await;
4646 cx.set_state(
4647 &"
4648 ˇzero
4649 one
4650 two
4651 three
4652 four
4653 five
4654 "
4655 .unindent(),
4656 );
4657
4658 // Create a four-line block that replaces three lines of text.
4659 cx.update_editor(|editor, window, cx| {
4660 let snapshot = editor.snapshot(window, cx);
4661 let snapshot = &snapshot.buffer_snapshot;
4662 let placement = BlockPlacement::Replace(
4663 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
4664 );
4665 editor.insert_blocks(
4666 [BlockProperties {
4667 placement,
4668 height: Some(4),
4669 style: BlockStyle::Sticky,
4670 render: Arc::new(|_| gpui::div().into_any_element()),
4671 priority: 0,
4672 render_in_minimap: true,
4673 }],
4674 None,
4675 cx,
4676 );
4677 });
4678
4679 // Move down so that the cursor touches the block.
4680 cx.update_editor(|editor, window, cx| {
4681 editor.move_down(&Default::default(), window, cx);
4682 });
4683 cx.assert_editor_state(
4684 &"
4685 zero
4686 «one
4687 two
4688 threeˇ»
4689 four
4690 five
4691 "
4692 .unindent(),
4693 );
4694
4695 // Move down past the block.
4696 cx.update_editor(|editor, window, cx| {
4697 editor.move_down(&Default::default(), window, cx);
4698 });
4699 cx.assert_editor_state(
4700 &"
4701 zero
4702 one
4703 two
4704 three
4705 ˇfour
4706 five
4707 "
4708 .unindent(),
4709 );
4710}
4711
4712#[gpui::test]
4713fn test_transpose(cx: &mut TestAppContext) {
4714 init_test(cx, |_| {});
4715
4716 _ = cx.add_window(|window, cx| {
4717 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
4718 editor.set_style(EditorStyle::default(), window, cx);
4719 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
4720 editor.transpose(&Default::default(), window, cx);
4721 assert_eq!(editor.text(cx), "bac");
4722 assert_eq!(editor.selections.ranges(cx), [2..2]);
4723
4724 editor.transpose(&Default::default(), window, cx);
4725 assert_eq!(editor.text(cx), "bca");
4726 assert_eq!(editor.selections.ranges(cx), [3..3]);
4727
4728 editor.transpose(&Default::default(), window, cx);
4729 assert_eq!(editor.text(cx), "bac");
4730 assert_eq!(editor.selections.ranges(cx), [3..3]);
4731
4732 editor
4733 });
4734
4735 _ = cx.add_window(|window, cx| {
4736 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4737 editor.set_style(EditorStyle::default(), window, cx);
4738 editor.change_selections(None, window, cx, |s| s.select_ranges([3..3]));
4739 editor.transpose(&Default::default(), window, cx);
4740 assert_eq!(editor.text(cx), "acb\nde");
4741 assert_eq!(editor.selections.ranges(cx), [3..3]);
4742
4743 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4744 editor.transpose(&Default::default(), window, cx);
4745 assert_eq!(editor.text(cx), "acbd\ne");
4746 assert_eq!(editor.selections.ranges(cx), [5..5]);
4747
4748 editor.transpose(&Default::default(), window, cx);
4749 assert_eq!(editor.text(cx), "acbde\n");
4750 assert_eq!(editor.selections.ranges(cx), [6..6]);
4751
4752 editor.transpose(&Default::default(), window, cx);
4753 assert_eq!(editor.text(cx), "acbd\ne");
4754 assert_eq!(editor.selections.ranges(cx), [6..6]);
4755
4756 editor
4757 });
4758
4759 _ = cx.add_window(|window, cx| {
4760 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4761 editor.set_style(EditorStyle::default(), window, cx);
4762 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1, 2..2, 4..4]));
4763 editor.transpose(&Default::default(), window, cx);
4764 assert_eq!(editor.text(cx), "bacd\ne");
4765 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
4766
4767 editor.transpose(&Default::default(), window, cx);
4768 assert_eq!(editor.text(cx), "bcade\n");
4769 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
4770
4771 editor.transpose(&Default::default(), window, cx);
4772 assert_eq!(editor.text(cx), "bcda\ne");
4773 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4774
4775 editor.transpose(&Default::default(), window, cx);
4776 assert_eq!(editor.text(cx), "bcade\n");
4777 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4778
4779 editor.transpose(&Default::default(), window, cx);
4780 assert_eq!(editor.text(cx), "bcaed\n");
4781 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
4782
4783 editor
4784 });
4785
4786 _ = cx.add_window(|window, cx| {
4787 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
4788 editor.set_style(EditorStyle::default(), window, cx);
4789 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4790 editor.transpose(&Default::default(), window, cx);
4791 assert_eq!(editor.text(cx), "🏀🍐✋");
4792 assert_eq!(editor.selections.ranges(cx), [8..8]);
4793
4794 editor.transpose(&Default::default(), window, cx);
4795 assert_eq!(editor.text(cx), "🏀✋🍐");
4796 assert_eq!(editor.selections.ranges(cx), [11..11]);
4797
4798 editor.transpose(&Default::default(), window, cx);
4799 assert_eq!(editor.text(cx), "🏀🍐✋");
4800 assert_eq!(editor.selections.ranges(cx), [11..11]);
4801
4802 editor
4803 });
4804}
4805
4806#[gpui::test]
4807async fn test_rewrap(cx: &mut TestAppContext) {
4808 init_test(cx, |settings| {
4809 settings.languages.extend([
4810 (
4811 "Markdown".into(),
4812 LanguageSettingsContent {
4813 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4814 ..Default::default()
4815 },
4816 ),
4817 (
4818 "Plain Text".into(),
4819 LanguageSettingsContent {
4820 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4821 ..Default::default()
4822 },
4823 ),
4824 ])
4825 });
4826
4827 let mut cx = EditorTestContext::new(cx).await;
4828
4829 let language_with_c_comments = Arc::new(Language::new(
4830 LanguageConfig {
4831 line_comments: vec!["// ".into()],
4832 ..LanguageConfig::default()
4833 },
4834 None,
4835 ));
4836 let language_with_pound_comments = Arc::new(Language::new(
4837 LanguageConfig {
4838 line_comments: vec!["# ".into()],
4839 ..LanguageConfig::default()
4840 },
4841 None,
4842 ));
4843 let markdown_language = Arc::new(Language::new(
4844 LanguageConfig {
4845 name: "Markdown".into(),
4846 ..LanguageConfig::default()
4847 },
4848 None,
4849 ));
4850 let language_with_doc_comments = Arc::new(Language::new(
4851 LanguageConfig {
4852 line_comments: vec!["// ".into(), "/// ".into()],
4853 ..LanguageConfig::default()
4854 },
4855 Some(tree_sitter_rust::LANGUAGE.into()),
4856 ));
4857
4858 let plaintext_language = Arc::new(Language::new(
4859 LanguageConfig {
4860 name: "Plain Text".into(),
4861 ..LanguageConfig::default()
4862 },
4863 None,
4864 ));
4865
4866 assert_rewrap(
4867 indoc! {"
4868 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
4869 "},
4870 indoc! {"
4871 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4872 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4873 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4874 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4875 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4876 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4877 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4878 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4879 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4880 // porttitor id. Aliquam id accumsan eros.
4881 "},
4882 language_with_c_comments.clone(),
4883 &mut cx,
4884 );
4885
4886 // Test that rewrapping works inside of a selection
4887 assert_rewrap(
4888 indoc! {"
4889 «// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.ˇ»
4890 "},
4891 indoc! {"
4892 «// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4893 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4894 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4895 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4896 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4897 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4898 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4899 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4900 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4901 // porttitor id. Aliquam id accumsan eros.ˇ»
4902 "},
4903 language_with_c_comments.clone(),
4904 &mut cx,
4905 );
4906
4907 // Test that cursors that expand to the same region are collapsed.
4908 assert_rewrap(
4909 indoc! {"
4910 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4911 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4912 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4913 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
4914 "},
4915 indoc! {"
4916 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4917 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4918 // auctor, eu lacinia sapien scelerisque. ˇVivamus sit amet neque et quam
4919 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4920 // Pellentesque odio lectus, iaculis ac volutpat et, ˇblandit quis urna. Sed
4921 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4922 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4923 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4924 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4925 // porttitor id. Aliquam id accumsan eros.
4926 "},
4927 language_with_c_comments.clone(),
4928 &mut cx,
4929 );
4930
4931 // Test that non-contiguous selections are treated separately.
4932 assert_rewrap(
4933 indoc! {"
4934 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4935 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4936 //
4937 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4938 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
4939 "},
4940 indoc! {"
4941 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4942 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4943 // auctor, eu lacinia sapien scelerisque.
4944 //
4945 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas
4946 // tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4947 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec
4948 // molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque
4949 // nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas
4950 // porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id
4951 // vulputate turpis porttitor id. Aliquam id accumsan eros.
4952 "},
4953 language_with_c_comments.clone(),
4954 &mut cx,
4955 );
4956
4957 // Test that different comment prefixes are supported.
4958 assert_rewrap(
4959 indoc! {"
4960 # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
4961 "},
4962 indoc! {"
4963 # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4964 # purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4965 # eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4966 # hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4967 # lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit
4968 # amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet
4969 # in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur
4970 # adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis.
4971 # Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id
4972 # accumsan eros.
4973 "},
4974 language_with_pound_comments.clone(),
4975 &mut cx,
4976 );
4977
4978 // Test that rewrapping is ignored outside of comments in most languages.
4979 assert_rewrap(
4980 indoc! {"
4981 /// Adds two numbers.
4982 /// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
4983 fn add(a: u32, b: u32) -> u32 {
4984 a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + bˇ
4985 }
4986 "},
4987 indoc! {"
4988 /// Adds two numbers. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
4989 /// Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
4990 fn add(a: u32, b: u32) -> u32 {
4991 a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + bˇ
4992 }
4993 "},
4994 language_with_doc_comments.clone(),
4995 &mut cx,
4996 );
4997
4998 // Test that rewrapping works in Markdown and Plain Text languages.
4999 assert_rewrap(
5000 indoc! {"
5001 # Hello
5002
5003 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi.
5004 "},
5005 indoc! {"
5006 # Hello
5007
5008 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
5009 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
5010 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
5011 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
5012 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
5013 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
5014 Integer sit amet scelerisque nisi.
5015 "},
5016 markdown_language,
5017 &mut cx,
5018 );
5019
5020 assert_rewrap(
5021 indoc! {"
5022 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi.
5023 "},
5024 indoc! {"
5025 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
5026 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
5027 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
5028 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
5029 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
5030 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
5031 Integer sit amet scelerisque nisi.
5032 "},
5033 plaintext_language,
5034 &mut cx,
5035 );
5036
5037 // Test rewrapping unaligned comments in a selection.
5038 assert_rewrap(
5039 indoc! {"
5040 fn foo() {
5041 if true {
5042 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
5043 // Praesent semper egestas tellus id dignissim.ˇ»
5044 do_something();
5045 } else {
5046 //
5047 }
5048 }
5049 "},
5050 indoc! {"
5051 fn foo() {
5052 if true {
5053 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
5054 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
5055 // egestas tellus id dignissim.ˇ»
5056 do_something();
5057 } else {
5058 //
5059 }
5060 }
5061 "},
5062 language_with_doc_comments.clone(),
5063 &mut cx,
5064 );
5065
5066 assert_rewrap(
5067 indoc! {"
5068 fn foo() {
5069 if true {
5070 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
5071 // Praesent semper egestas tellus id dignissim.»
5072 do_something();
5073 } else {
5074 //
5075 }
5076
5077 }
5078 "},
5079 indoc! {"
5080 fn foo() {
5081 if true {
5082 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
5083 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
5084 // egestas tellus id dignissim.»
5085 do_something();
5086 } else {
5087 //
5088 }
5089
5090 }
5091 "},
5092 language_with_doc_comments.clone(),
5093 &mut cx,
5094 );
5095
5096 #[track_caller]
5097 fn assert_rewrap(
5098 unwrapped_text: &str,
5099 wrapped_text: &str,
5100 language: Arc<Language>,
5101 cx: &mut EditorTestContext,
5102 ) {
5103 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
5104 cx.set_state(unwrapped_text);
5105 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
5106 cx.assert_editor_state(wrapped_text);
5107 }
5108}
5109
5110#[gpui::test]
5111async fn test_hard_wrap(cx: &mut TestAppContext) {
5112 init_test(cx, |_| {});
5113 let mut cx = EditorTestContext::new(cx).await;
5114
5115 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
5116 cx.update_editor(|editor, _, cx| {
5117 editor.set_hard_wrap(Some(14), cx);
5118 });
5119
5120 cx.set_state(indoc!(
5121 "
5122 one two three ˇ
5123 "
5124 ));
5125 cx.simulate_input("four");
5126 cx.run_until_parked();
5127
5128 cx.assert_editor_state(indoc!(
5129 "
5130 one two three
5131 fourˇ
5132 "
5133 ));
5134
5135 cx.update_editor(|editor, window, cx| {
5136 editor.newline(&Default::default(), window, cx);
5137 });
5138 cx.run_until_parked();
5139 cx.assert_editor_state(indoc!(
5140 "
5141 one two three
5142 four
5143 ˇ
5144 "
5145 ));
5146
5147 cx.simulate_input("five");
5148 cx.run_until_parked();
5149 cx.assert_editor_state(indoc!(
5150 "
5151 one two three
5152 four
5153 fiveˇ
5154 "
5155 ));
5156
5157 cx.update_editor(|editor, window, cx| {
5158 editor.newline(&Default::default(), window, cx);
5159 });
5160 cx.run_until_parked();
5161 cx.simulate_input("# ");
5162 cx.run_until_parked();
5163 cx.assert_editor_state(indoc!(
5164 "
5165 one two three
5166 four
5167 five
5168 # ˇ
5169 "
5170 ));
5171
5172 cx.update_editor(|editor, window, cx| {
5173 editor.newline(&Default::default(), window, cx);
5174 });
5175 cx.run_until_parked();
5176 cx.assert_editor_state(indoc!(
5177 "
5178 one two three
5179 four
5180 five
5181 #\x20
5182 #ˇ
5183 "
5184 ));
5185
5186 cx.simulate_input(" 6");
5187 cx.run_until_parked();
5188 cx.assert_editor_state(indoc!(
5189 "
5190 one two three
5191 four
5192 five
5193 #
5194 # 6ˇ
5195 "
5196 ));
5197}
5198
5199#[gpui::test]
5200async fn test_clipboard(cx: &mut TestAppContext) {
5201 init_test(cx, |_| {});
5202
5203 let mut cx = EditorTestContext::new(cx).await;
5204
5205 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
5206 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5207 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
5208
5209 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
5210 cx.set_state("two ˇfour ˇsix ˇ");
5211 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5212 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
5213
5214 // Paste again but with only two cursors. Since the number of cursors doesn't
5215 // match the number of slices in the clipboard, the entire clipboard text
5216 // is pasted at each cursor.
5217 cx.set_state("ˇtwo one✅ four three six five ˇ");
5218 cx.update_editor(|e, window, cx| {
5219 e.handle_input("( ", window, cx);
5220 e.paste(&Paste, window, cx);
5221 e.handle_input(") ", window, cx);
5222 });
5223 cx.assert_editor_state(
5224 &([
5225 "( one✅ ",
5226 "three ",
5227 "five ) ˇtwo one✅ four three six five ( one✅ ",
5228 "three ",
5229 "five ) ˇ",
5230 ]
5231 .join("\n")),
5232 );
5233
5234 // Cut with three selections, one of which is full-line.
5235 cx.set_state(indoc! {"
5236 1«2ˇ»3
5237 4ˇ567
5238 «8ˇ»9"});
5239 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5240 cx.assert_editor_state(indoc! {"
5241 1ˇ3
5242 ˇ9"});
5243
5244 // Paste with three selections, noticing how the copied selection that was full-line
5245 // gets inserted before the second cursor.
5246 cx.set_state(indoc! {"
5247 1ˇ3
5248 9ˇ
5249 «oˇ»ne"});
5250 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5251 cx.assert_editor_state(indoc! {"
5252 12ˇ3
5253 4567
5254 9ˇ
5255 8ˇne"});
5256
5257 // Copy with a single cursor only, which writes the whole line into the clipboard.
5258 cx.set_state(indoc! {"
5259 The quick brown
5260 fox juˇmps over
5261 the lazy dog"});
5262 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5263 assert_eq!(
5264 cx.read_from_clipboard()
5265 .and_then(|item| item.text().as_deref().map(str::to_string)),
5266 Some("fox jumps over\n".to_string())
5267 );
5268
5269 // Paste with three selections, noticing how the copied full-line selection is inserted
5270 // before the empty selections but replaces the selection that is non-empty.
5271 cx.set_state(indoc! {"
5272 Tˇhe quick brown
5273 «foˇ»x jumps over
5274 tˇhe lazy dog"});
5275 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5276 cx.assert_editor_state(indoc! {"
5277 fox jumps over
5278 Tˇhe quick brown
5279 fox jumps over
5280 ˇx jumps over
5281 fox jumps over
5282 tˇhe lazy dog"});
5283}
5284
5285#[gpui::test]
5286async fn test_copy_trim(cx: &mut TestAppContext) {
5287 init_test(cx, |_| {});
5288
5289 let mut cx = EditorTestContext::new(cx).await;
5290 cx.set_state(
5291 r#" «for selection in selections.iter() {
5292 let mut start = selection.start;
5293 let mut end = selection.end;
5294 let is_entire_line = selection.is_empty();
5295 if is_entire_line {
5296 start = Point::new(start.row, 0);ˇ»
5297 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5298 }
5299 "#,
5300 );
5301 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5302 assert_eq!(
5303 cx.read_from_clipboard()
5304 .and_then(|item| item.text().as_deref().map(str::to_string)),
5305 Some(
5306 "for selection in selections.iter() {
5307 let mut start = selection.start;
5308 let mut end = selection.end;
5309 let is_entire_line = selection.is_empty();
5310 if is_entire_line {
5311 start = Point::new(start.row, 0);"
5312 .to_string()
5313 ),
5314 "Regular copying preserves all indentation selected",
5315 );
5316 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5317 assert_eq!(
5318 cx.read_from_clipboard()
5319 .and_then(|item| item.text().as_deref().map(str::to_string)),
5320 Some(
5321 "for selection in selections.iter() {
5322let mut start = selection.start;
5323let mut end = selection.end;
5324let is_entire_line = selection.is_empty();
5325if is_entire_line {
5326 start = Point::new(start.row, 0);"
5327 .to_string()
5328 ),
5329 "Copying with stripping should strip all leading whitespaces"
5330 );
5331
5332 cx.set_state(
5333 r#" « for selection in selections.iter() {
5334 let mut start = selection.start;
5335 let mut end = selection.end;
5336 let is_entire_line = selection.is_empty();
5337 if is_entire_line {
5338 start = Point::new(start.row, 0);ˇ»
5339 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5340 }
5341 "#,
5342 );
5343 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5344 assert_eq!(
5345 cx.read_from_clipboard()
5346 .and_then(|item| item.text().as_deref().map(str::to_string)),
5347 Some(
5348 " for selection in selections.iter() {
5349 let mut start = selection.start;
5350 let mut end = selection.end;
5351 let is_entire_line = selection.is_empty();
5352 if is_entire_line {
5353 start = Point::new(start.row, 0);"
5354 .to_string()
5355 ),
5356 "Regular copying preserves all indentation selected",
5357 );
5358 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5359 assert_eq!(
5360 cx.read_from_clipboard()
5361 .and_then(|item| item.text().as_deref().map(str::to_string)),
5362 Some(
5363 "for selection in selections.iter() {
5364let mut start = selection.start;
5365let mut end = selection.end;
5366let is_entire_line = selection.is_empty();
5367if is_entire_line {
5368 start = Point::new(start.row, 0);"
5369 .to_string()
5370 ),
5371 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
5372 );
5373
5374 cx.set_state(
5375 r#" «ˇ for selection in selections.iter() {
5376 let mut start = selection.start;
5377 let mut end = selection.end;
5378 let is_entire_line = selection.is_empty();
5379 if is_entire_line {
5380 start = Point::new(start.row, 0);»
5381 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5382 }
5383 "#,
5384 );
5385 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5386 assert_eq!(
5387 cx.read_from_clipboard()
5388 .and_then(|item| item.text().as_deref().map(str::to_string)),
5389 Some(
5390 " for selection in selections.iter() {
5391 let mut start = selection.start;
5392 let mut end = selection.end;
5393 let is_entire_line = selection.is_empty();
5394 if is_entire_line {
5395 start = Point::new(start.row, 0);"
5396 .to_string()
5397 ),
5398 "Regular copying for reverse selection works the same",
5399 );
5400 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5401 assert_eq!(
5402 cx.read_from_clipboard()
5403 .and_then(|item| item.text().as_deref().map(str::to_string)),
5404 Some(
5405 "for selection in selections.iter() {
5406let mut start = selection.start;
5407let mut end = selection.end;
5408let is_entire_line = selection.is_empty();
5409if is_entire_line {
5410 start = Point::new(start.row, 0);"
5411 .to_string()
5412 ),
5413 "Copying with stripping for reverse selection works the same"
5414 );
5415
5416 cx.set_state(
5417 r#" for selection «in selections.iter() {
5418 let mut start = selection.start;
5419 let mut end = selection.end;
5420 let is_entire_line = selection.is_empty();
5421 if is_entire_line {
5422 start = Point::new(start.row, 0);ˇ»
5423 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5424 }
5425 "#,
5426 );
5427 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5428 assert_eq!(
5429 cx.read_from_clipboard()
5430 .and_then(|item| item.text().as_deref().map(str::to_string)),
5431 Some(
5432 "in selections.iter() {
5433 let mut start = selection.start;
5434 let mut end = selection.end;
5435 let is_entire_line = selection.is_empty();
5436 if is_entire_line {
5437 start = Point::new(start.row, 0);"
5438 .to_string()
5439 ),
5440 "When selecting past the indent, the copying works as usual",
5441 );
5442 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5443 assert_eq!(
5444 cx.read_from_clipboard()
5445 .and_then(|item| item.text().as_deref().map(str::to_string)),
5446 Some(
5447 "in selections.iter() {
5448 let mut start = selection.start;
5449 let mut end = selection.end;
5450 let is_entire_line = selection.is_empty();
5451 if is_entire_line {
5452 start = Point::new(start.row, 0);"
5453 .to_string()
5454 ),
5455 "When selecting past the indent, nothing is trimmed"
5456 );
5457
5458 cx.set_state(
5459 r#" «for selection in selections.iter() {
5460 let mut start = selection.start;
5461
5462 let mut end = selection.end;
5463 let is_entire_line = selection.is_empty();
5464 if is_entire_line {
5465 start = Point::new(start.row, 0);
5466ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
5467 }
5468 "#,
5469 );
5470 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5471 assert_eq!(
5472 cx.read_from_clipboard()
5473 .and_then(|item| item.text().as_deref().map(str::to_string)),
5474 Some(
5475 "for selection in selections.iter() {
5476let mut start = selection.start;
5477
5478let mut end = selection.end;
5479let is_entire_line = selection.is_empty();
5480if is_entire_line {
5481 start = Point::new(start.row, 0);
5482"
5483 .to_string()
5484 ),
5485 "Copying with stripping should ignore empty lines"
5486 );
5487}
5488
5489#[gpui::test]
5490async fn test_paste_multiline(cx: &mut TestAppContext) {
5491 init_test(cx, |_| {});
5492
5493 let mut cx = EditorTestContext::new(cx).await;
5494 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5495
5496 // Cut an indented block, without the leading whitespace.
5497 cx.set_state(indoc! {"
5498 const a: B = (
5499 c(),
5500 «d(
5501 e,
5502 f
5503 )ˇ»
5504 );
5505 "});
5506 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5507 cx.assert_editor_state(indoc! {"
5508 const a: B = (
5509 c(),
5510 ˇ
5511 );
5512 "});
5513
5514 // Paste it at the same position.
5515 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5516 cx.assert_editor_state(indoc! {"
5517 const a: B = (
5518 c(),
5519 d(
5520 e,
5521 f
5522 )ˇ
5523 );
5524 "});
5525
5526 // Paste it at a line with a lower indent level.
5527 cx.set_state(indoc! {"
5528 ˇ
5529 const a: B = (
5530 c(),
5531 );
5532 "});
5533 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5534 cx.assert_editor_state(indoc! {"
5535 d(
5536 e,
5537 f
5538 )ˇ
5539 const a: B = (
5540 c(),
5541 );
5542 "});
5543
5544 // Cut an indented block, with the leading whitespace.
5545 cx.set_state(indoc! {"
5546 const a: B = (
5547 c(),
5548 « d(
5549 e,
5550 f
5551 )
5552 ˇ»);
5553 "});
5554 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5555 cx.assert_editor_state(indoc! {"
5556 const a: B = (
5557 c(),
5558 ˇ);
5559 "});
5560
5561 // Paste it at the same position.
5562 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5563 cx.assert_editor_state(indoc! {"
5564 const a: B = (
5565 c(),
5566 d(
5567 e,
5568 f
5569 )
5570 ˇ);
5571 "});
5572
5573 // Paste it at a line with a higher indent level.
5574 cx.set_state(indoc! {"
5575 const a: B = (
5576 c(),
5577 d(
5578 e,
5579 fˇ
5580 )
5581 );
5582 "});
5583 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5584 cx.assert_editor_state(indoc! {"
5585 const a: B = (
5586 c(),
5587 d(
5588 e,
5589 f d(
5590 e,
5591 f
5592 )
5593 ˇ
5594 )
5595 );
5596 "});
5597
5598 // Copy an indented block, starting mid-line
5599 cx.set_state(indoc! {"
5600 const a: B = (
5601 c(),
5602 somethin«g(
5603 e,
5604 f
5605 )ˇ»
5606 );
5607 "});
5608 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5609
5610 // Paste it on a line with a lower indent level
5611 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
5612 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5613 cx.assert_editor_state(indoc! {"
5614 const a: B = (
5615 c(),
5616 something(
5617 e,
5618 f
5619 )
5620 );
5621 g(
5622 e,
5623 f
5624 )ˇ"});
5625}
5626
5627#[gpui::test]
5628async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
5629 init_test(cx, |_| {});
5630
5631 cx.write_to_clipboard(ClipboardItem::new_string(
5632 " d(\n e\n );\n".into(),
5633 ));
5634
5635 let mut cx = EditorTestContext::new(cx).await;
5636 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5637
5638 cx.set_state(indoc! {"
5639 fn a() {
5640 b();
5641 if c() {
5642 ˇ
5643 }
5644 }
5645 "});
5646
5647 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5648 cx.assert_editor_state(indoc! {"
5649 fn a() {
5650 b();
5651 if c() {
5652 d(
5653 e
5654 );
5655 ˇ
5656 }
5657 }
5658 "});
5659
5660 cx.set_state(indoc! {"
5661 fn a() {
5662 b();
5663 ˇ
5664 }
5665 "});
5666
5667 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5668 cx.assert_editor_state(indoc! {"
5669 fn a() {
5670 b();
5671 d(
5672 e
5673 );
5674 ˇ
5675 }
5676 "});
5677}
5678
5679#[gpui::test]
5680fn test_select_all(cx: &mut TestAppContext) {
5681 init_test(cx, |_| {});
5682
5683 let editor = cx.add_window(|window, cx| {
5684 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
5685 build_editor(buffer, window, cx)
5686 });
5687 _ = editor.update(cx, |editor, window, cx| {
5688 editor.select_all(&SelectAll, window, cx);
5689 assert_eq!(
5690 editor.selections.display_ranges(cx),
5691 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
5692 );
5693 });
5694}
5695
5696#[gpui::test]
5697fn test_select_line(cx: &mut TestAppContext) {
5698 init_test(cx, |_| {});
5699
5700 let editor = cx.add_window(|window, cx| {
5701 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
5702 build_editor(buffer, window, cx)
5703 });
5704 _ = editor.update(cx, |editor, window, cx| {
5705 editor.change_selections(None, window, cx, |s| {
5706 s.select_display_ranges([
5707 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5708 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5709 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5710 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
5711 ])
5712 });
5713 editor.select_line(&SelectLine, window, cx);
5714 assert_eq!(
5715 editor.selections.display_ranges(cx),
5716 vec![
5717 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
5718 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
5719 ]
5720 );
5721 });
5722
5723 _ = editor.update(cx, |editor, window, cx| {
5724 editor.select_line(&SelectLine, window, cx);
5725 assert_eq!(
5726 editor.selections.display_ranges(cx),
5727 vec![
5728 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
5729 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
5730 ]
5731 );
5732 });
5733
5734 _ = editor.update(cx, |editor, window, cx| {
5735 editor.select_line(&SelectLine, window, cx);
5736 assert_eq!(
5737 editor.selections.display_ranges(cx),
5738 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
5739 );
5740 });
5741}
5742
5743#[gpui::test]
5744async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
5745 init_test(cx, |_| {});
5746 let mut cx = EditorTestContext::new(cx).await;
5747
5748 #[track_caller]
5749 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
5750 cx.set_state(initial_state);
5751 cx.update_editor(|e, window, cx| {
5752 e.split_selection_into_lines(&SplitSelectionIntoLines, window, cx)
5753 });
5754 cx.assert_editor_state(expected_state);
5755 }
5756
5757 // Selection starts and ends at the middle of lines, left-to-right
5758 test(
5759 &mut cx,
5760 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
5761 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5762 );
5763 // Same thing, right-to-left
5764 test(
5765 &mut cx,
5766 "aa\nb«b\ncc\ndd\neˇ»e\nff",
5767 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5768 );
5769
5770 // Whole buffer, left-to-right, last line *doesn't* end with newline
5771 test(
5772 &mut cx,
5773 "«ˇaa\nbb\ncc\ndd\nee\nff»",
5774 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5775 );
5776 // Same thing, right-to-left
5777 test(
5778 &mut cx,
5779 "«aa\nbb\ncc\ndd\nee\nffˇ»",
5780 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5781 );
5782
5783 // Whole buffer, left-to-right, last line ends with newline
5784 test(
5785 &mut cx,
5786 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
5787 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5788 );
5789 // Same thing, right-to-left
5790 test(
5791 &mut cx,
5792 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
5793 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5794 );
5795
5796 // Starts at the end of a line, ends at the start of another
5797 test(
5798 &mut cx,
5799 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
5800 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
5801 );
5802}
5803
5804#[gpui::test]
5805async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
5806 init_test(cx, |_| {});
5807
5808 let editor = cx.add_window(|window, cx| {
5809 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
5810 build_editor(buffer, window, cx)
5811 });
5812
5813 // setup
5814 _ = editor.update(cx, |editor, window, cx| {
5815 editor.fold_creases(
5816 vec![
5817 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5818 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5819 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5820 ],
5821 true,
5822 window,
5823 cx,
5824 );
5825 assert_eq!(
5826 editor.display_text(cx),
5827 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5828 );
5829 });
5830
5831 _ = editor.update(cx, |editor, window, cx| {
5832 editor.change_selections(None, window, cx, |s| {
5833 s.select_display_ranges([
5834 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5835 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5836 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5837 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
5838 ])
5839 });
5840 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5841 assert_eq!(
5842 editor.display_text(cx),
5843 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5844 );
5845 });
5846 EditorTestContext::for_editor(editor, cx)
5847 .await
5848 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
5849
5850 _ = editor.update(cx, |editor, window, cx| {
5851 editor.change_selections(None, window, cx, |s| {
5852 s.select_display_ranges([
5853 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
5854 ])
5855 });
5856 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5857 assert_eq!(
5858 editor.display_text(cx),
5859 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
5860 );
5861 assert_eq!(
5862 editor.selections.display_ranges(cx),
5863 [
5864 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
5865 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
5866 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
5867 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
5868 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
5869 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
5870 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
5871 ]
5872 );
5873 });
5874 EditorTestContext::for_editor(editor, cx)
5875 .await
5876 .assert_editor_state(
5877 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
5878 );
5879}
5880
5881#[gpui::test]
5882async fn test_add_selection_above_below(cx: &mut TestAppContext) {
5883 init_test(cx, |_| {});
5884
5885 let mut cx = EditorTestContext::new(cx).await;
5886
5887 cx.set_state(indoc!(
5888 r#"abc
5889 defˇghi
5890
5891 jk
5892 nlmo
5893 "#
5894 ));
5895
5896 cx.update_editor(|editor, window, cx| {
5897 editor.add_selection_above(&Default::default(), window, cx);
5898 });
5899
5900 cx.assert_editor_state(indoc!(
5901 r#"abcˇ
5902 defˇghi
5903
5904 jk
5905 nlmo
5906 "#
5907 ));
5908
5909 cx.update_editor(|editor, window, cx| {
5910 editor.add_selection_above(&Default::default(), window, cx);
5911 });
5912
5913 cx.assert_editor_state(indoc!(
5914 r#"abcˇ
5915 defˇghi
5916
5917 jk
5918 nlmo
5919 "#
5920 ));
5921
5922 cx.update_editor(|editor, window, cx| {
5923 editor.add_selection_below(&Default::default(), window, cx);
5924 });
5925
5926 cx.assert_editor_state(indoc!(
5927 r#"abc
5928 defˇghi
5929
5930 jk
5931 nlmo
5932 "#
5933 ));
5934
5935 cx.update_editor(|editor, window, cx| {
5936 editor.undo_selection(&Default::default(), window, cx);
5937 });
5938
5939 cx.assert_editor_state(indoc!(
5940 r#"abcˇ
5941 defˇghi
5942
5943 jk
5944 nlmo
5945 "#
5946 ));
5947
5948 cx.update_editor(|editor, window, cx| {
5949 editor.redo_selection(&Default::default(), window, cx);
5950 });
5951
5952 cx.assert_editor_state(indoc!(
5953 r#"abc
5954 defˇghi
5955
5956 jk
5957 nlmo
5958 "#
5959 ));
5960
5961 cx.update_editor(|editor, window, cx| {
5962 editor.add_selection_below(&Default::default(), window, cx);
5963 });
5964
5965 cx.assert_editor_state(indoc!(
5966 r#"abc
5967 defˇghi
5968
5969 jk
5970 nlmˇo
5971 "#
5972 ));
5973
5974 cx.update_editor(|editor, window, cx| {
5975 editor.add_selection_below(&Default::default(), window, cx);
5976 });
5977
5978 cx.assert_editor_state(indoc!(
5979 r#"abc
5980 defˇghi
5981
5982 jk
5983 nlmˇo
5984 "#
5985 ));
5986
5987 // change selections
5988 cx.set_state(indoc!(
5989 r#"abc
5990 def«ˇg»hi
5991
5992 jk
5993 nlmo
5994 "#
5995 ));
5996
5997 cx.update_editor(|editor, window, cx| {
5998 editor.add_selection_below(&Default::default(), window, cx);
5999 });
6000
6001 cx.assert_editor_state(indoc!(
6002 r#"abc
6003 def«ˇg»hi
6004
6005 jk
6006 nlm«ˇo»
6007 "#
6008 ));
6009
6010 cx.update_editor(|editor, window, cx| {
6011 editor.add_selection_below(&Default::default(), window, cx);
6012 });
6013
6014 cx.assert_editor_state(indoc!(
6015 r#"abc
6016 def«ˇg»hi
6017
6018 jk
6019 nlm«ˇo»
6020 "#
6021 ));
6022
6023 cx.update_editor(|editor, window, cx| {
6024 editor.add_selection_above(&Default::default(), window, cx);
6025 });
6026
6027 cx.assert_editor_state(indoc!(
6028 r#"abc
6029 def«ˇg»hi
6030
6031 jk
6032 nlmo
6033 "#
6034 ));
6035
6036 cx.update_editor(|editor, window, cx| {
6037 editor.add_selection_above(&Default::default(), window, cx);
6038 });
6039
6040 cx.assert_editor_state(indoc!(
6041 r#"abc
6042 def«ˇg»hi
6043
6044 jk
6045 nlmo
6046 "#
6047 ));
6048
6049 // Change selections again
6050 cx.set_state(indoc!(
6051 r#"a«bc
6052 defgˇ»hi
6053
6054 jk
6055 nlmo
6056 "#
6057 ));
6058
6059 cx.update_editor(|editor, window, cx| {
6060 editor.add_selection_below(&Default::default(), window, cx);
6061 });
6062
6063 cx.assert_editor_state(indoc!(
6064 r#"a«bcˇ»
6065 d«efgˇ»hi
6066
6067 j«kˇ»
6068 nlmo
6069 "#
6070 ));
6071
6072 cx.update_editor(|editor, window, cx| {
6073 editor.add_selection_below(&Default::default(), window, cx);
6074 });
6075 cx.assert_editor_state(indoc!(
6076 r#"a«bcˇ»
6077 d«efgˇ»hi
6078
6079 j«kˇ»
6080 n«lmoˇ»
6081 "#
6082 ));
6083 cx.update_editor(|editor, window, cx| {
6084 editor.add_selection_above(&Default::default(), window, cx);
6085 });
6086
6087 cx.assert_editor_state(indoc!(
6088 r#"a«bcˇ»
6089 d«efgˇ»hi
6090
6091 j«kˇ»
6092 nlmo
6093 "#
6094 ));
6095
6096 // Change selections again
6097 cx.set_state(indoc!(
6098 r#"abc
6099 d«ˇefghi
6100
6101 jk
6102 nlm»o
6103 "#
6104 ));
6105
6106 cx.update_editor(|editor, window, cx| {
6107 editor.add_selection_above(&Default::default(), window, cx);
6108 });
6109
6110 cx.assert_editor_state(indoc!(
6111 r#"a«ˇbc»
6112 d«ˇef»ghi
6113
6114 j«ˇk»
6115 n«ˇlm»o
6116 "#
6117 ));
6118
6119 cx.update_editor(|editor, window, cx| {
6120 editor.add_selection_below(&Default::default(), window, cx);
6121 });
6122
6123 cx.assert_editor_state(indoc!(
6124 r#"abc
6125 d«ˇef»ghi
6126
6127 j«ˇk»
6128 n«ˇlm»o
6129 "#
6130 ));
6131}
6132
6133#[gpui::test]
6134async fn test_select_next(cx: &mut TestAppContext) {
6135 init_test(cx, |_| {});
6136
6137 let mut cx = EditorTestContext::new(cx).await;
6138 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6139
6140 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6141 .unwrap();
6142 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6143
6144 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6145 .unwrap();
6146 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
6147
6148 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6149 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6150
6151 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6152 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
6153
6154 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6155 .unwrap();
6156 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6157
6158 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6159 .unwrap();
6160 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6161
6162 // Test selection direction should be preserved
6163 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
6164
6165 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6166 .unwrap();
6167 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
6168}
6169
6170#[gpui::test]
6171async fn test_select_all_matches(cx: &mut TestAppContext) {
6172 init_test(cx, |_| {});
6173
6174 let mut cx = EditorTestContext::new(cx).await;
6175
6176 // Test caret-only selections
6177 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6178 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6179 .unwrap();
6180 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6181
6182 // Test left-to-right selections
6183 cx.set_state("abc\n«abcˇ»\nabc");
6184 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6185 .unwrap();
6186 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
6187
6188 // Test right-to-left selections
6189 cx.set_state("abc\n«ˇabc»\nabc");
6190 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6191 .unwrap();
6192 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
6193
6194 // Test selecting whitespace with caret selection
6195 cx.set_state("abc\nˇ abc\nabc");
6196 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6197 .unwrap();
6198 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
6199
6200 // Test selecting whitespace with left-to-right selection
6201 cx.set_state("abc\n«ˇ »abc\nabc");
6202 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6203 .unwrap();
6204 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
6205
6206 // Test no matches with right-to-left selection
6207 cx.set_state("abc\n« ˇ»abc\nabc");
6208 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6209 .unwrap();
6210 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
6211}
6212
6213#[gpui::test]
6214async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
6215 init_test(cx, |_| {});
6216
6217 let mut cx = EditorTestContext::new(cx).await;
6218
6219 let large_body_1 = "\nd".repeat(200);
6220 let large_body_2 = "\ne".repeat(200);
6221
6222 cx.set_state(&format!(
6223 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
6224 ));
6225 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
6226 let scroll_position = editor.scroll_position(cx);
6227 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
6228 scroll_position
6229 });
6230
6231 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6232 .unwrap();
6233 cx.assert_editor_state(&format!(
6234 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
6235 ));
6236 let scroll_position_after_selection =
6237 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
6238 assert_eq!(
6239 initial_scroll_position, scroll_position_after_selection,
6240 "Scroll position should not change after selecting all matches"
6241 );
6242}
6243
6244#[gpui::test]
6245async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
6246 init_test(cx, |_| {});
6247
6248 let mut cx = EditorLspTestContext::new_rust(
6249 lsp::ServerCapabilities {
6250 document_formatting_provider: Some(lsp::OneOf::Left(true)),
6251 ..Default::default()
6252 },
6253 cx,
6254 )
6255 .await;
6256
6257 cx.set_state(indoc! {"
6258 line 1
6259 line 2
6260 linˇe 3
6261 line 4
6262 line 5
6263 "});
6264
6265 // Make an edit
6266 cx.update_editor(|editor, window, cx| {
6267 editor.handle_input("X", window, cx);
6268 });
6269
6270 // Move cursor to a different position
6271 cx.update_editor(|editor, window, cx| {
6272 editor.change_selections(None, window, cx, |s| {
6273 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
6274 });
6275 });
6276
6277 cx.assert_editor_state(indoc! {"
6278 line 1
6279 line 2
6280 linXe 3
6281 line 4
6282 liˇne 5
6283 "});
6284
6285 cx.lsp
6286 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
6287 Ok(Some(vec![lsp::TextEdit::new(
6288 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
6289 "PREFIX ".to_string(),
6290 )]))
6291 });
6292
6293 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
6294 .unwrap()
6295 .await
6296 .unwrap();
6297
6298 cx.assert_editor_state(indoc! {"
6299 PREFIX line 1
6300 line 2
6301 linXe 3
6302 line 4
6303 liˇne 5
6304 "});
6305
6306 // Undo formatting
6307 cx.update_editor(|editor, window, cx| {
6308 editor.undo(&Default::default(), window, cx);
6309 });
6310
6311 // Verify cursor moved back to position after edit
6312 cx.assert_editor_state(indoc! {"
6313 line 1
6314 line 2
6315 linXˇe 3
6316 line 4
6317 line 5
6318 "});
6319}
6320
6321#[gpui::test]
6322async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
6323 init_test(cx, |_| {});
6324
6325 let mut cx = EditorTestContext::new(cx).await;
6326 cx.set_state(
6327 r#"let foo = 2;
6328lˇet foo = 2;
6329let fooˇ = 2;
6330let foo = 2;
6331let foo = ˇ2;"#,
6332 );
6333
6334 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6335 .unwrap();
6336 cx.assert_editor_state(
6337 r#"let foo = 2;
6338«letˇ» foo = 2;
6339let «fooˇ» = 2;
6340let foo = 2;
6341let foo = «2ˇ»;"#,
6342 );
6343
6344 // noop for multiple selections with different contents
6345 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6346 .unwrap();
6347 cx.assert_editor_state(
6348 r#"let foo = 2;
6349«letˇ» foo = 2;
6350let «fooˇ» = 2;
6351let foo = 2;
6352let foo = «2ˇ»;"#,
6353 );
6354
6355 // Test last selection direction should be preserved
6356 cx.set_state(
6357 r#"let foo = 2;
6358let foo = 2;
6359let «fooˇ» = 2;
6360let «ˇfoo» = 2;
6361let foo = 2;"#,
6362 );
6363
6364 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6365 .unwrap();
6366 cx.assert_editor_state(
6367 r#"let foo = 2;
6368let foo = 2;
6369let «fooˇ» = 2;
6370let «ˇfoo» = 2;
6371let «ˇfoo» = 2;"#,
6372 );
6373}
6374
6375#[gpui::test]
6376async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
6377 init_test(cx, |_| {});
6378
6379 let mut cx =
6380 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
6381
6382 cx.assert_editor_state(indoc! {"
6383 ˇbbb
6384 ccc
6385
6386 bbb
6387 ccc
6388 "});
6389 cx.dispatch_action(SelectPrevious::default());
6390 cx.assert_editor_state(indoc! {"
6391 «bbbˇ»
6392 ccc
6393
6394 bbb
6395 ccc
6396 "});
6397 cx.dispatch_action(SelectPrevious::default());
6398 cx.assert_editor_state(indoc! {"
6399 «bbbˇ»
6400 ccc
6401
6402 «bbbˇ»
6403 ccc
6404 "});
6405}
6406
6407#[gpui::test]
6408async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
6409 init_test(cx, |_| {});
6410
6411 let mut cx = EditorTestContext::new(cx).await;
6412 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6413
6414 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6415 .unwrap();
6416 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6417
6418 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6419 .unwrap();
6420 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
6421
6422 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6423 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6424
6425 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6426 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
6427
6428 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6429 .unwrap();
6430 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
6431
6432 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6433 .unwrap();
6434 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6435}
6436
6437#[gpui::test]
6438async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
6439 init_test(cx, |_| {});
6440
6441 let mut cx = EditorTestContext::new(cx).await;
6442 cx.set_state("aˇ");
6443
6444 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6445 .unwrap();
6446 cx.assert_editor_state("«aˇ»");
6447 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6448 .unwrap();
6449 cx.assert_editor_state("«aˇ»");
6450}
6451
6452#[gpui::test]
6453async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
6454 init_test(cx, |_| {});
6455
6456 let mut cx = EditorTestContext::new(cx).await;
6457 cx.set_state(
6458 r#"let foo = 2;
6459lˇet foo = 2;
6460let fooˇ = 2;
6461let foo = 2;
6462let foo = ˇ2;"#,
6463 );
6464
6465 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6466 .unwrap();
6467 cx.assert_editor_state(
6468 r#"let foo = 2;
6469«letˇ» foo = 2;
6470let «fooˇ» = 2;
6471let foo = 2;
6472let foo = «2ˇ»;"#,
6473 );
6474
6475 // noop for multiple selections with different contents
6476 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6477 .unwrap();
6478 cx.assert_editor_state(
6479 r#"let foo = 2;
6480«letˇ» foo = 2;
6481let «fooˇ» = 2;
6482let foo = 2;
6483let foo = «2ˇ»;"#,
6484 );
6485}
6486
6487#[gpui::test]
6488async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
6489 init_test(cx, |_| {});
6490
6491 let mut cx = EditorTestContext::new(cx).await;
6492 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
6493
6494 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6495 .unwrap();
6496 // selection direction is preserved
6497 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
6498
6499 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6500 .unwrap();
6501 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
6502
6503 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6504 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
6505
6506 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6507 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
6508
6509 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6510 .unwrap();
6511 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
6512
6513 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6514 .unwrap();
6515 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
6516}
6517
6518#[gpui::test]
6519async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
6520 init_test(cx, |_| {});
6521
6522 let language = Arc::new(Language::new(
6523 LanguageConfig::default(),
6524 Some(tree_sitter_rust::LANGUAGE.into()),
6525 ));
6526
6527 let text = r#"
6528 use mod1::mod2::{mod3, mod4};
6529
6530 fn fn_1(param1: bool, param2: &str) {
6531 let var1 = "text";
6532 }
6533 "#
6534 .unindent();
6535
6536 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6537 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6538 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6539
6540 editor
6541 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6542 .await;
6543
6544 editor.update_in(cx, |editor, window, cx| {
6545 editor.change_selections(None, window, cx, |s| {
6546 s.select_display_ranges([
6547 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
6548 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
6549 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
6550 ]);
6551 });
6552 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6553 });
6554 editor.update(cx, |editor, cx| {
6555 assert_text_with_selections(
6556 editor,
6557 indoc! {r#"
6558 use mod1::mod2::{mod3, «mod4ˇ»};
6559
6560 fn fn_1«ˇ(param1: bool, param2: &str)» {
6561 let var1 = "«ˇtext»";
6562 }
6563 "#},
6564 cx,
6565 );
6566 });
6567
6568 editor.update_in(cx, |editor, window, cx| {
6569 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6570 });
6571 editor.update(cx, |editor, cx| {
6572 assert_text_with_selections(
6573 editor,
6574 indoc! {r#"
6575 use mod1::mod2::«{mod3, mod4}ˇ»;
6576
6577 «ˇfn fn_1(param1: bool, param2: &str) {
6578 let var1 = "text";
6579 }»
6580 "#},
6581 cx,
6582 );
6583 });
6584
6585 editor.update_in(cx, |editor, window, cx| {
6586 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6587 });
6588 assert_eq!(
6589 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
6590 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
6591 );
6592
6593 // Trying to expand the selected syntax node one more time has no effect.
6594 editor.update_in(cx, |editor, window, cx| {
6595 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6596 });
6597 assert_eq!(
6598 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
6599 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
6600 );
6601
6602 editor.update_in(cx, |editor, window, cx| {
6603 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6604 });
6605 editor.update(cx, |editor, cx| {
6606 assert_text_with_selections(
6607 editor,
6608 indoc! {r#"
6609 use mod1::mod2::«{mod3, mod4}ˇ»;
6610
6611 «ˇfn fn_1(param1: bool, param2: &str) {
6612 let var1 = "text";
6613 }»
6614 "#},
6615 cx,
6616 );
6617 });
6618
6619 editor.update_in(cx, |editor, window, cx| {
6620 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6621 });
6622 editor.update(cx, |editor, cx| {
6623 assert_text_with_selections(
6624 editor,
6625 indoc! {r#"
6626 use mod1::mod2::{mod3, «mod4ˇ»};
6627
6628 fn fn_1«ˇ(param1: bool, param2: &str)» {
6629 let var1 = "«ˇtext»";
6630 }
6631 "#},
6632 cx,
6633 );
6634 });
6635
6636 editor.update_in(cx, |editor, window, cx| {
6637 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6638 });
6639 editor.update(cx, |editor, cx| {
6640 assert_text_with_selections(
6641 editor,
6642 indoc! {r#"
6643 use mod1::mod2::{mod3, mo«ˇ»d4};
6644
6645 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
6646 let var1 = "te«ˇ»xt";
6647 }
6648 "#},
6649 cx,
6650 );
6651 });
6652
6653 // Trying to shrink the selected syntax node one more time has no effect.
6654 editor.update_in(cx, |editor, window, cx| {
6655 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6656 });
6657 editor.update_in(cx, |editor, _, cx| {
6658 assert_text_with_selections(
6659 editor,
6660 indoc! {r#"
6661 use mod1::mod2::{mod3, mo«ˇ»d4};
6662
6663 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
6664 let var1 = "te«ˇ»xt";
6665 }
6666 "#},
6667 cx,
6668 );
6669 });
6670
6671 // Ensure that we keep expanding the selection if the larger selection starts or ends within
6672 // a fold.
6673 editor.update_in(cx, |editor, window, cx| {
6674 editor.fold_creases(
6675 vec![
6676 Crease::simple(
6677 Point::new(0, 21)..Point::new(0, 24),
6678 FoldPlaceholder::test(),
6679 ),
6680 Crease::simple(
6681 Point::new(3, 20)..Point::new(3, 22),
6682 FoldPlaceholder::test(),
6683 ),
6684 ],
6685 true,
6686 window,
6687 cx,
6688 );
6689 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6690 });
6691 editor.update(cx, |editor, cx| {
6692 assert_text_with_selections(
6693 editor,
6694 indoc! {r#"
6695 use mod1::mod2::«{mod3, mod4}ˇ»;
6696
6697 fn fn_1«ˇ(param1: bool, param2: &str)» {
6698 let var1 = "«ˇtext»";
6699 }
6700 "#},
6701 cx,
6702 );
6703 });
6704}
6705
6706#[gpui::test]
6707async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
6708 init_test(cx, |_| {});
6709
6710 let language = Arc::new(Language::new(
6711 LanguageConfig::default(),
6712 Some(tree_sitter_rust::LANGUAGE.into()),
6713 ));
6714
6715 let text = "let a = 2;";
6716
6717 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6718 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6719 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6720
6721 editor
6722 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6723 .await;
6724
6725 // Test case 1: Cursor at end of word
6726 editor.update_in(cx, |editor, window, cx| {
6727 editor.change_selections(None, window, cx, |s| {
6728 s.select_display_ranges([
6729 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
6730 ]);
6731 });
6732 });
6733 editor.update(cx, |editor, cx| {
6734 assert_text_with_selections(editor, "let aˇ = 2;", cx);
6735 });
6736 editor.update_in(cx, |editor, window, cx| {
6737 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6738 });
6739 editor.update(cx, |editor, cx| {
6740 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
6741 });
6742 editor.update_in(cx, |editor, window, cx| {
6743 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6744 });
6745 editor.update(cx, |editor, cx| {
6746 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
6747 });
6748
6749 // Test case 2: Cursor at end of statement
6750 editor.update_in(cx, |editor, window, cx| {
6751 editor.change_selections(None, window, cx, |s| {
6752 s.select_display_ranges([
6753 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
6754 ]);
6755 });
6756 });
6757 editor.update(cx, |editor, cx| {
6758 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
6759 });
6760 editor.update_in(cx, |editor, window, cx| {
6761 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6762 });
6763 editor.update(cx, |editor, cx| {
6764 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
6765 });
6766}
6767
6768#[gpui::test]
6769async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
6770 init_test(cx, |_| {});
6771
6772 let language = Arc::new(Language::new(
6773 LanguageConfig::default(),
6774 Some(tree_sitter_rust::LANGUAGE.into()),
6775 ));
6776
6777 let text = r#"
6778 use mod1::mod2::{mod3, mod4};
6779
6780 fn fn_1(param1: bool, param2: &str) {
6781 let var1 = "hello world";
6782 }
6783 "#
6784 .unindent();
6785
6786 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6787 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6788 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6789
6790 editor
6791 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6792 .await;
6793
6794 // Test 1: Cursor on a letter of a string word
6795 editor.update_in(cx, |editor, window, cx| {
6796 editor.change_selections(None, window, cx, |s| {
6797 s.select_display_ranges([
6798 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
6799 ]);
6800 });
6801 });
6802 editor.update_in(cx, |editor, window, cx| {
6803 assert_text_with_selections(
6804 editor,
6805 indoc! {r#"
6806 use mod1::mod2::{mod3, mod4};
6807
6808 fn fn_1(param1: bool, param2: &str) {
6809 let var1 = "hˇello world";
6810 }
6811 "#},
6812 cx,
6813 );
6814 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6815 assert_text_with_selections(
6816 editor,
6817 indoc! {r#"
6818 use mod1::mod2::{mod3, mod4};
6819
6820 fn fn_1(param1: bool, param2: &str) {
6821 let var1 = "«ˇhello» world";
6822 }
6823 "#},
6824 cx,
6825 );
6826 });
6827
6828 // Test 2: Partial selection within a word
6829 editor.update_in(cx, |editor, window, cx| {
6830 editor.change_selections(None, window, cx, |s| {
6831 s.select_display_ranges([
6832 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
6833 ]);
6834 });
6835 });
6836 editor.update_in(cx, |editor, window, cx| {
6837 assert_text_with_selections(
6838 editor,
6839 indoc! {r#"
6840 use mod1::mod2::{mod3, mod4};
6841
6842 fn fn_1(param1: bool, param2: &str) {
6843 let var1 = "h«elˇ»lo world";
6844 }
6845 "#},
6846 cx,
6847 );
6848 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6849 assert_text_with_selections(
6850 editor,
6851 indoc! {r#"
6852 use mod1::mod2::{mod3, mod4};
6853
6854 fn fn_1(param1: bool, param2: &str) {
6855 let var1 = "«ˇhello» world";
6856 }
6857 "#},
6858 cx,
6859 );
6860 });
6861
6862 // Test 3: Complete word already selected
6863 editor.update_in(cx, |editor, window, cx| {
6864 editor.change_selections(None, window, cx, |s| {
6865 s.select_display_ranges([
6866 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
6867 ]);
6868 });
6869 });
6870 editor.update_in(cx, |editor, window, cx| {
6871 assert_text_with_selections(
6872 editor,
6873 indoc! {r#"
6874 use mod1::mod2::{mod3, mod4};
6875
6876 fn fn_1(param1: bool, param2: &str) {
6877 let var1 = "«helloˇ» world";
6878 }
6879 "#},
6880 cx,
6881 );
6882 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6883 assert_text_with_selections(
6884 editor,
6885 indoc! {r#"
6886 use mod1::mod2::{mod3, mod4};
6887
6888 fn fn_1(param1: bool, param2: &str) {
6889 let var1 = "«hello worldˇ»";
6890 }
6891 "#},
6892 cx,
6893 );
6894 });
6895
6896 // Test 4: Selection spanning across words
6897 editor.update_in(cx, |editor, window, cx| {
6898 editor.change_selections(None, window, cx, |s| {
6899 s.select_display_ranges([
6900 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
6901 ]);
6902 });
6903 });
6904 editor.update_in(cx, |editor, window, cx| {
6905 assert_text_with_selections(
6906 editor,
6907 indoc! {r#"
6908 use mod1::mod2::{mod3, mod4};
6909
6910 fn fn_1(param1: bool, param2: &str) {
6911 let var1 = "hel«lo woˇ»rld";
6912 }
6913 "#},
6914 cx,
6915 );
6916 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6917 assert_text_with_selections(
6918 editor,
6919 indoc! {r#"
6920 use mod1::mod2::{mod3, mod4};
6921
6922 fn fn_1(param1: bool, param2: &str) {
6923 let var1 = "«ˇhello world»";
6924 }
6925 "#},
6926 cx,
6927 );
6928 });
6929
6930 // Test 5: Expansion beyond string
6931 editor.update_in(cx, |editor, window, cx| {
6932 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6933 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6934 assert_text_with_selections(
6935 editor,
6936 indoc! {r#"
6937 use mod1::mod2::{mod3, mod4};
6938
6939 fn fn_1(param1: bool, param2: &str) {
6940 «ˇlet var1 = "hello world";»
6941 }
6942 "#},
6943 cx,
6944 );
6945 });
6946}
6947
6948#[gpui::test]
6949async fn test_fold_function_bodies(cx: &mut TestAppContext) {
6950 init_test(cx, |_| {});
6951
6952 let base_text = r#"
6953 impl A {
6954 // this is an uncommitted comment
6955
6956 fn b() {
6957 c();
6958 }
6959
6960 // this is another uncommitted comment
6961
6962 fn d() {
6963 // e
6964 // f
6965 }
6966 }
6967
6968 fn g() {
6969 // h
6970 }
6971 "#
6972 .unindent();
6973
6974 let text = r#"
6975 ˇimpl A {
6976
6977 fn b() {
6978 c();
6979 }
6980
6981 fn d() {
6982 // e
6983 // f
6984 }
6985 }
6986
6987 fn g() {
6988 // h
6989 }
6990 "#
6991 .unindent();
6992
6993 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
6994 cx.set_state(&text);
6995 cx.set_head_text(&base_text);
6996 cx.update_editor(|editor, window, cx| {
6997 editor.expand_all_diff_hunks(&Default::default(), window, cx);
6998 });
6999
7000 cx.assert_state_with_diff(
7001 "
7002 ˇimpl A {
7003 - // this is an uncommitted comment
7004
7005 fn b() {
7006 c();
7007 }
7008
7009 - // this is another uncommitted comment
7010 -
7011 fn d() {
7012 // e
7013 // f
7014 }
7015 }
7016
7017 fn g() {
7018 // h
7019 }
7020 "
7021 .unindent(),
7022 );
7023
7024 let expected_display_text = "
7025 impl A {
7026 // this is an uncommitted comment
7027
7028 fn b() {
7029 ⋯
7030 }
7031
7032 // this is another uncommitted comment
7033
7034 fn d() {
7035 ⋯
7036 }
7037 }
7038
7039 fn g() {
7040 ⋯
7041 }
7042 "
7043 .unindent();
7044
7045 cx.update_editor(|editor, window, cx| {
7046 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
7047 assert_eq!(editor.display_text(cx), expected_display_text);
7048 });
7049}
7050
7051#[gpui::test]
7052async fn test_autoindent(cx: &mut TestAppContext) {
7053 init_test(cx, |_| {});
7054
7055 let language = Arc::new(
7056 Language::new(
7057 LanguageConfig {
7058 brackets: BracketPairConfig {
7059 pairs: vec![
7060 BracketPair {
7061 start: "{".to_string(),
7062 end: "}".to_string(),
7063 close: false,
7064 surround: false,
7065 newline: true,
7066 },
7067 BracketPair {
7068 start: "(".to_string(),
7069 end: ")".to_string(),
7070 close: false,
7071 surround: false,
7072 newline: true,
7073 },
7074 ],
7075 ..Default::default()
7076 },
7077 ..Default::default()
7078 },
7079 Some(tree_sitter_rust::LANGUAGE.into()),
7080 )
7081 .with_indents_query(
7082 r#"
7083 (_ "(" ")" @end) @indent
7084 (_ "{" "}" @end) @indent
7085 "#,
7086 )
7087 .unwrap(),
7088 );
7089
7090 let text = "fn a() {}";
7091
7092 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7093 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7094 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7095 editor
7096 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7097 .await;
7098
7099 editor.update_in(cx, |editor, window, cx| {
7100 editor.change_selections(None, window, cx, |s| s.select_ranges([5..5, 8..8, 9..9]));
7101 editor.newline(&Newline, window, cx);
7102 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
7103 assert_eq!(
7104 editor.selections.ranges(cx),
7105 &[
7106 Point::new(1, 4)..Point::new(1, 4),
7107 Point::new(3, 4)..Point::new(3, 4),
7108 Point::new(5, 0)..Point::new(5, 0)
7109 ]
7110 );
7111 });
7112}
7113
7114#[gpui::test]
7115async fn test_autoindent_selections(cx: &mut TestAppContext) {
7116 init_test(cx, |_| {});
7117
7118 {
7119 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
7120 cx.set_state(indoc! {"
7121 impl A {
7122
7123 fn b() {}
7124
7125 «fn c() {
7126
7127 }ˇ»
7128 }
7129 "});
7130
7131 cx.update_editor(|editor, window, cx| {
7132 editor.autoindent(&Default::default(), window, cx);
7133 });
7134
7135 cx.assert_editor_state(indoc! {"
7136 impl A {
7137
7138 fn b() {}
7139
7140 «fn c() {
7141
7142 }ˇ»
7143 }
7144 "});
7145 }
7146
7147 {
7148 let mut cx = EditorTestContext::new_multibuffer(
7149 cx,
7150 [indoc! { "
7151 impl A {
7152 «
7153 // a
7154 fn b(){}
7155 »
7156 «
7157 }
7158 fn c(){}
7159 »
7160 "}],
7161 );
7162
7163 let buffer = cx.update_editor(|editor, _, cx| {
7164 let buffer = editor.buffer().update(cx, |buffer, _| {
7165 buffer.all_buffers().iter().next().unwrap().clone()
7166 });
7167 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7168 buffer
7169 });
7170
7171 cx.run_until_parked();
7172 cx.update_editor(|editor, window, cx| {
7173 editor.select_all(&Default::default(), window, cx);
7174 editor.autoindent(&Default::default(), window, cx)
7175 });
7176 cx.run_until_parked();
7177
7178 cx.update(|_, cx| {
7179 assert_eq!(
7180 buffer.read(cx).text(),
7181 indoc! { "
7182 impl A {
7183
7184 // a
7185 fn b(){}
7186
7187
7188 }
7189 fn c(){}
7190
7191 " }
7192 )
7193 });
7194 }
7195}
7196
7197#[gpui::test]
7198async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
7199 init_test(cx, |_| {});
7200
7201 let mut cx = EditorTestContext::new(cx).await;
7202
7203 let language = Arc::new(Language::new(
7204 LanguageConfig {
7205 brackets: BracketPairConfig {
7206 pairs: vec![
7207 BracketPair {
7208 start: "{".to_string(),
7209 end: "}".to_string(),
7210 close: true,
7211 surround: true,
7212 newline: true,
7213 },
7214 BracketPair {
7215 start: "(".to_string(),
7216 end: ")".to_string(),
7217 close: true,
7218 surround: true,
7219 newline: true,
7220 },
7221 BracketPair {
7222 start: "/*".to_string(),
7223 end: " */".to_string(),
7224 close: true,
7225 surround: true,
7226 newline: true,
7227 },
7228 BracketPair {
7229 start: "[".to_string(),
7230 end: "]".to_string(),
7231 close: false,
7232 surround: false,
7233 newline: true,
7234 },
7235 BracketPair {
7236 start: "\"".to_string(),
7237 end: "\"".to_string(),
7238 close: true,
7239 surround: true,
7240 newline: false,
7241 },
7242 BracketPair {
7243 start: "<".to_string(),
7244 end: ">".to_string(),
7245 close: false,
7246 surround: true,
7247 newline: true,
7248 },
7249 ],
7250 ..Default::default()
7251 },
7252 autoclose_before: "})]".to_string(),
7253 ..Default::default()
7254 },
7255 Some(tree_sitter_rust::LANGUAGE.into()),
7256 ));
7257
7258 cx.language_registry().add(language.clone());
7259 cx.update_buffer(|buffer, cx| {
7260 buffer.set_language(Some(language), cx);
7261 });
7262
7263 cx.set_state(
7264 &r#"
7265 🏀ˇ
7266 εˇ
7267 ❤️ˇ
7268 "#
7269 .unindent(),
7270 );
7271
7272 // autoclose multiple nested brackets at multiple cursors
7273 cx.update_editor(|editor, window, cx| {
7274 editor.handle_input("{", window, cx);
7275 editor.handle_input("{", window, cx);
7276 editor.handle_input("{", window, cx);
7277 });
7278 cx.assert_editor_state(
7279 &"
7280 🏀{{{ˇ}}}
7281 ε{{{ˇ}}}
7282 ❤️{{{ˇ}}}
7283 "
7284 .unindent(),
7285 );
7286
7287 // insert a different closing bracket
7288 cx.update_editor(|editor, window, cx| {
7289 editor.handle_input(")", window, cx);
7290 });
7291 cx.assert_editor_state(
7292 &"
7293 🏀{{{)ˇ}}}
7294 ε{{{)ˇ}}}
7295 ❤️{{{)ˇ}}}
7296 "
7297 .unindent(),
7298 );
7299
7300 // skip over the auto-closed brackets when typing a closing bracket
7301 cx.update_editor(|editor, window, cx| {
7302 editor.move_right(&MoveRight, window, cx);
7303 editor.handle_input("}", window, cx);
7304 editor.handle_input("}", window, cx);
7305 editor.handle_input("}", window, cx);
7306 });
7307 cx.assert_editor_state(
7308 &"
7309 🏀{{{)}}}}ˇ
7310 ε{{{)}}}}ˇ
7311 ❤️{{{)}}}}ˇ
7312 "
7313 .unindent(),
7314 );
7315
7316 // autoclose multi-character pairs
7317 cx.set_state(
7318 &"
7319 ˇ
7320 ˇ
7321 "
7322 .unindent(),
7323 );
7324 cx.update_editor(|editor, window, cx| {
7325 editor.handle_input("/", window, cx);
7326 editor.handle_input("*", window, cx);
7327 });
7328 cx.assert_editor_state(
7329 &"
7330 /*ˇ */
7331 /*ˇ */
7332 "
7333 .unindent(),
7334 );
7335
7336 // one cursor autocloses a multi-character pair, one cursor
7337 // does not autoclose.
7338 cx.set_state(
7339 &"
7340 /ˇ
7341 ˇ
7342 "
7343 .unindent(),
7344 );
7345 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
7346 cx.assert_editor_state(
7347 &"
7348 /*ˇ */
7349 *ˇ
7350 "
7351 .unindent(),
7352 );
7353
7354 // Don't autoclose if the next character isn't whitespace and isn't
7355 // listed in the language's "autoclose_before" section.
7356 cx.set_state("ˇa b");
7357 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7358 cx.assert_editor_state("{ˇa b");
7359
7360 // Don't autoclose if `close` is false for the bracket pair
7361 cx.set_state("ˇ");
7362 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
7363 cx.assert_editor_state("[ˇ");
7364
7365 // Surround with brackets if text is selected
7366 cx.set_state("«aˇ» b");
7367 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7368 cx.assert_editor_state("{«aˇ»} b");
7369
7370 // Autoclose when not immediately after a word character
7371 cx.set_state("a ˇ");
7372 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7373 cx.assert_editor_state("a \"ˇ\"");
7374
7375 // Autoclose pair where the start and end characters are the same
7376 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7377 cx.assert_editor_state("a \"\"ˇ");
7378
7379 // Don't autoclose when immediately after a word character
7380 cx.set_state("aˇ");
7381 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7382 cx.assert_editor_state("a\"ˇ");
7383
7384 // Do autoclose when after a non-word character
7385 cx.set_state("{ˇ");
7386 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7387 cx.assert_editor_state("{\"ˇ\"");
7388
7389 // Non identical pairs autoclose regardless of preceding character
7390 cx.set_state("aˇ");
7391 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7392 cx.assert_editor_state("a{ˇ}");
7393
7394 // Don't autoclose pair if autoclose is disabled
7395 cx.set_state("ˇ");
7396 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
7397 cx.assert_editor_state("<ˇ");
7398
7399 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
7400 cx.set_state("«aˇ» b");
7401 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
7402 cx.assert_editor_state("<«aˇ»> b");
7403}
7404
7405#[gpui::test]
7406async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
7407 init_test(cx, |settings| {
7408 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
7409 });
7410
7411 let mut cx = EditorTestContext::new(cx).await;
7412
7413 let language = Arc::new(Language::new(
7414 LanguageConfig {
7415 brackets: BracketPairConfig {
7416 pairs: vec![
7417 BracketPair {
7418 start: "{".to_string(),
7419 end: "}".to_string(),
7420 close: true,
7421 surround: true,
7422 newline: true,
7423 },
7424 BracketPair {
7425 start: "(".to_string(),
7426 end: ")".to_string(),
7427 close: true,
7428 surround: true,
7429 newline: true,
7430 },
7431 BracketPair {
7432 start: "[".to_string(),
7433 end: "]".to_string(),
7434 close: false,
7435 surround: false,
7436 newline: true,
7437 },
7438 ],
7439 ..Default::default()
7440 },
7441 autoclose_before: "})]".to_string(),
7442 ..Default::default()
7443 },
7444 Some(tree_sitter_rust::LANGUAGE.into()),
7445 ));
7446
7447 cx.language_registry().add(language.clone());
7448 cx.update_buffer(|buffer, cx| {
7449 buffer.set_language(Some(language), cx);
7450 });
7451
7452 cx.set_state(
7453 &"
7454 ˇ
7455 ˇ
7456 ˇ
7457 "
7458 .unindent(),
7459 );
7460
7461 // ensure only matching closing brackets are skipped over
7462 cx.update_editor(|editor, window, cx| {
7463 editor.handle_input("}", window, cx);
7464 editor.move_left(&MoveLeft, window, cx);
7465 editor.handle_input(")", window, cx);
7466 editor.move_left(&MoveLeft, window, cx);
7467 });
7468 cx.assert_editor_state(
7469 &"
7470 ˇ)}
7471 ˇ)}
7472 ˇ)}
7473 "
7474 .unindent(),
7475 );
7476
7477 // skip-over closing brackets at multiple cursors
7478 cx.update_editor(|editor, window, cx| {
7479 editor.handle_input(")", window, cx);
7480 editor.handle_input("}", window, cx);
7481 });
7482 cx.assert_editor_state(
7483 &"
7484 )}ˇ
7485 )}ˇ
7486 )}ˇ
7487 "
7488 .unindent(),
7489 );
7490
7491 // ignore non-close brackets
7492 cx.update_editor(|editor, window, cx| {
7493 editor.handle_input("]", window, cx);
7494 editor.move_left(&MoveLeft, window, cx);
7495 editor.handle_input("]", window, cx);
7496 });
7497 cx.assert_editor_state(
7498 &"
7499 )}]ˇ]
7500 )}]ˇ]
7501 )}]ˇ]
7502 "
7503 .unindent(),
7504 );
7505}
7506
7507#[gpui::test]
7508async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
7509 init_test(cx, |_| {});
7510
7511 let mut cx = EditorTestContext::new(cx).await;
7512
7513 let html_language = Arc::new(
7514 Language::new(
7515 LanguageConfig {
7516 name: "HTML".into(),
7517 brackets: BracketPairConfig {
7518 pairs: vec![
7519 BracketPair {
7520 start: "<".into(),
7521 end: ">".into(),
7522 close: true,
7523 ..Default::default()
7524 },
7525 BracketPair {
7526 start: "{".into(),
7527 end: "}".into(),
7528 close: true,
7529 ..Default::default()
7530 },
7531 BracketPair {
7532 start: "(".into(),
7533 end: ")".into(),
7534 close: true,
7535 ..Default::default()
7536 },
7537 ],
7538 ..Default::default()
7539 },
7540 autoclose_before: "})]>".into(),
7541 ..Default::default()
7542 },
7543 Some(tree_sitter_html::LANGUAGE.into()),
7544 )
7545 .with_injection_query(
7546 r#"
7547 (script_element
7548 (raw_text) @injection.content
7549 (#set! injection.language "javascript"))
7550 "#,
7551 )
7552 .unwrap(),
7553 );
7554
7555 let javascript_language = Arc::new(Language::new(
7556 LanguageConfig {
7557 name: "JavaScript".into(),
7558 brackets: BracketPairConfig {
7559 pairs: vec![
7560 BracketPair {
7561 start: "/*".into(),
7562 end: " */".into(),
7563 close: true,
7564 ..Default::default()
7565 },
7566 BracketPair {
7567 start: "{".into(),
7568 end: "}".into(),
7569 close: true,
7570 ..Default::default()
7571 },
7572 BracketPair {
7573 start: "(".into(),
7574 end: ")".into(),
7575 close: true,
7576 ..Default::default()
7577 },
7578 ],
7579 ..Default::default()
7580 },
7581 autoclose_before: "})]>".into(),
7582 ..Default::default()
7583 },
7584 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
7585 ));
7586
7587 cx.language_registry().add(html_language.clone());
7588 cx.language_registry().add(javascript_language.clone());
7589
7590 cx.update_buffer(|buffer, cx| {
7591 buffer.set_language(Some(html_language), cx);
7592 });
7593
7594 cx.set_state(
7595 &r#"
7596 <body>ˇ
7597 <script>
7598 var x = 1;ˇ
7599 </script>
7600 </body>ˇ
7601 "#
7602 .unindent(),
7603 );
7604
7605 // Precondition: different languages are active at different locations.
7606 cx.update_editor(|editor, window, cx| {
7607 let snapshot = editor.snapshot(window, cx);
7608 let cursors = editor.selections.ranges::<usize>(cx);
7609 let languages = cursors
7610 .iter()
7611 .map(|c| snapshot.language_at(c.start).unwrap().name())
7612 .collect::<Vec<_>>();
7613 assert_eq!(
7614 languages,
7615 &["HTML".into(), "JavaScript".into(), "HTML".into()]
7616 );
7617 });
7618
7619 // Angle brackets autoclose in HTML, but not JavaScript.
7620 cx.update_editor(|editor, window, cx| {
7621 editor.handle_input("<", window, cx);
7622 editor.handle_input("a", window, cx);
7623 });
7624 cx.assert_editor_state(
7625 &r#"
7626 <body><aˇ>
7627 <script>
7628 var x = 1;<aˇ
7629 </script>
7630 </body><aˇ>
7631 "#
7632 .unindent(),
7633 );
7634
7635 // Curly braces and parens autoclose in both HTML and JavaScript.
7636 cx.update_editor(|editor, window, cx| {
7637 editor.handle_input(" b=", window, cx);
7638 editor.handle_input("{", window, cx);
7639 editor.handle_input("c", window, cx);
7640 editor.handle_input("(", window, cx);
7641 });
7642 cx.assert_editor_state(
7643 &r#"
7644 <body><a b={c(ˇ)}>
7645 <script>
7646 var x = 1;<a b={c(ˇ)}
7647 </script>
7648 </body><a b={c(ˇ)}>
7649 "#
7650 .unindent(),
7651 );
7652
7653 // Brackets that were already autoclosed are skipped.
7654 cx.update_editor(|editor, window, cx| {
7655 editor.handle_input(")", window, cx);
7656 editor.handle_input("d", window, cx);
7657 editor.handle_input("}", window, cx);
7658 });
7659 cx.assert_editor_state(
7660 &r#"
7661 <body><a b={c()d}ˇ>
7662 <script>
7663 var x = 1;<a b={c()d}ˇ
7664 </script>
7665 </body><a b={c()d}ˇ>
7666 "#
7667 .unindent(),
7668 );
7669 cx.update_editor(|editor, window, cx| {
7670 editor.handle_input(">", window, cx);
7671 });
7672 cx.assert_editor_state(
7673 &r#"
7674 <body><a b={c()d}>ˇ
7675 <script>
7676 var x = 1;<a b={c()d}>ˇ
7677 </script>
7678 </body><a b={c()d}>ˇ
7679 "#
7680 .unindent(),
7681 );
7682
7683 // Reset
7684 cx.set_state(
7685 &r#"
7686 <body>ˇ
7687 <script>
7688 var x = 1;ˇ
7689 </script>
7690 </body>ˇ
7691 "#
7692 .unindent(),
7693 );
7694
7695 cx.update_editor(|editor, window, cx| {
7696 editor.handle_input("<", window, cx);
7697 });
7698 cx.assert_editor_state(
7699 &r#"
7700 <body><ˇ>
7701 <script>
7702 var x = 1;<ˇ
7703 </script>
7704 </body><ˇ>
7705 "#
7706 .unindent(),
7707 );
7708
7709 // When backspacing, the closing angle brackets are removed.
7710 cx.update_editor(|editor, window, cx| {
7711 editor.backspace(&Backspace, window, cx);
7712 });
7713 cx.assert_editor_state(
7714 &r#"
7715 <body>ˇ
7716 <script>
7717 var x = 1;ˇ
7718 </script>
7719 </body>ˇ
7720 "#
7721 .unindent(),
7722 );
7723
7724 // Block comments autoclose in JavaScript, but not HTML.
7725 cx.update_editor(|editor, window, cx| {
7726 editor.handle_input("/", window, cx);
7727 editor.handle_input("*", window, cx);
7728 });
7729 cx.assert_editor_state(
7730 &r#"
7731 <body>/*ˇ
7732 <script>
7733 var x = 1;/*ˇ */
7734 </script>
7735 </body>/*ˇ
7736 "#
7737 .unindent(),
7738 );
7739}
7740
7741#[gpui::test]
7742async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
7743 init_test(cx, |_| {});
7744
7745 let mut cx = EditorTestContext::new(cx).await;
7746
7747 let rust_language = Arc::new(
7748 Language::new(
7749 LanguageConfig {
7750 name: "Rust".into(),
7751 brackets: serde_json::from_value(json!([
7752 { "start": "{", "end": "}", "close": true, "newline": true },
7753 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
7754 ]))
7755 .unwrap(),
7756 autoclose_before: "})]>".into(),
7757 ..Default::default()
7758 },
7759 Some(tree_sitter_rust::LANGUAGE.into()),
7760 )
7761 .with_override_query("(string_literal) @string")
7762 .unwrap(),
7763 );
7764
7765 cx.language_registry().add(rust_language.clone());
7766 cx.update_buffer(|buffer, cx| {
7767 buffer.set_language(Some(rust_language), cx);
7768 });
7769
7770 cx.set_state(
7771 &r#"
7772 let x = ˇ
7773 "#
7774 .unindent(),
7775 );
7776
7777 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
7778 cx.update_editor(|editor, window, cx| {
7779 editor.handle_input("\"", window, cx);
7780 });
7781 cx.assert_editor_state(
7782 &r#"
7783 let x = "ˇ"
7784 "#
7785 .unindent(),
7786 );
7787
7788 // Inserting another quotation mark. The cursor moves across the existing
7789 // automatically-inserted quotation mark.
7790 cx.update_editor(|editor, window, cx| {
7791 editor.handle_input("\"", window, cx);
7792 });
7793 cx.assert_editor_state(
7794 &r#"
7795 let x = ""ˇ
7796 "#
7797 .unindent(),
7798 );
7799
7800 // Reset
7801 cx.set_state(
7802 &r#"
7803 let x = ˇ
7804 "#
7805 .unindent(),
7806 );
7807
7808 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
7809 cx.update_editor(|editor, window, cx| {
7810 editor.handle_input("\"", window, cx);
7811 editor.handle_input(" ", window, cx);
7812 editor.move_left(&Default::default(), window, cx);
7813 editor.handle_input("\\", window, cx);
7814 editor.handle_input("\"", window, cx);
7815 });
7816 cx.assert_editor_state(
7817 &r#"
7818 let x = "\"ˇ "
7819 "#
7820 .unindent(),
7821 );
7822
7823 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
7824 // mark. Nothing is inserted.
7825 cx.update_editor(|editor, window, cx| {
7826 editor.move_right(&Default::default(), window, cx);
7827 editor.handle_input("\"", window, cx);
7828 });
7829 cx.assert_editor_state(
7830 &r#"
7831 let x = "\" "ˇ
7832 "#
7833 .unindent(),
7834 );
7835}
7836
7837#[gpui::test]
7838async fn test_surround_with_pair(cx: &mut TestAppContext) {
7839 init_test(cx, |_| {});
7840
7841 let language = Arc::new(Language::new(
7842 LanguageConfig {
7843 brackets: BracketPairConfig {
7844 pairs: vec![
7845 BracketPair {
7846 start: "{".to_string(),
7847 end: "}".to_string(),
7848 close: true,
7849 surround: true,
7850 newline: true,
7851 },
7852 BracketPair {
7853 start: "/* ".to_string(),
7854 end: "*/".to_string(),
7855 close: true,
7856 surround: true,
7857 ..Default::default()
7858 },
7859 ],
7860 ..Default::default()
7861 },
7862 ..Default::default()
7863 },
7864 Some(tree_sitter_rust::LANGUAGE.into()),
7865 ));
7866
7867 let text = r#"
7868 a
7869 b
7870 c
7871 "#
7872 .unindent();
7873
7874 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7875 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7876 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7877 editor
7878 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7879 .await;
7880
7881 editor.update_in(cx, |editor, window, cx| {
7882 editor.change_selections(None, window, cx, |s| {
7883 s.select_display_ranges([
7884 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7885 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7886 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
7887 ])
7888 });
7889
7890 editor.handle_input("{", window, cx);
7891 editor.handle_input("{", window, cx);
7892 editor.handle_input("{", window, cx);
7893 assert_eq!(
7894 editor.text(cx),
7895 "
7896 {{{a}}}
7897 {{{b}}}
7898 {{{c}}}
7899 "
7900 .unindent()
7901 );
7902 assert_eq!(
7903 editor.selections.display_ranges(cx),
7904 [
7905 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
7906 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
7907 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
7908 ]
7909 );
7910
7911 editor.undo(&Undo, window, cx);
7912 editor.undo(&Undo, window, cx);
7913 editor.undo(&Undo, window, cx);
7914 assert_eq!(
7915 editor.text(cx),
7916 "
7917 a
7918 b
7919 c
7920 "
7921 .unindent()
7922 );
7923 assert_eq!(
7924 editor.selections.display_ranges(cx),
7925 [
7926 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7927 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7928 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
7929 ]
7930 );
7931
7932 // Ensure inserting the first character of a multi-byte bracket pair
7933 // doesn't surround the selections with the bracket.
7934 editor.handle_input("/", window, cx);
7935 assert_eq!(
7936 editor.text(cx),
7937 "
7938 /
7939 /
7940 /
7941 "
7942 .unindent()
7943 );
7944 assert_eq!(
7945 editor.selections.display_ranges(cx),
7946 [
7947 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
7948 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
7949 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
7950 ]
7951 );
7952
7953 editor.undo(&Undo, window, cx);
7954 assert_eq!(
7955 editor.text(cx),
7956 "
7957 a
7958 b
7959 c
7960 "
7961 .unindent()
7962 );
7963 assert_eq!(
7964 editor.selections.display_ranges(cx),
7965 [
7966 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7967 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7968 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
7969 ]
7970 );
7971
7972 // Ensure inserting the last character of a multi-byte bracket pair
7973 // doesn't surround the selections with the bracket.
7974 editor.handle_input("*", window, cx);
7975 assert_eq!(
7976 editor.text(cx),
7977 "
7978 *
7979 *
7980 *
7981 "
7982 .unindent()
7983 );
7984 assert_eq!(
7985 editor.selections.display_ranges(cx),
7986 [
7987 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
7988 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
7989 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
7990 ]
7991 );
7992 });
7993}
7994
7995#[gpui::test]
7996async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
7997 init_test(cx, |_| {});
7998
7999 let language = Arc::new(Language::new(
8000 LanguageConfig {
8001 brackets: BracketPairConfig {
8002 pairs: vec![BracketPair {
8003 start: "{".to_string(),
8004 end: "}".to_string(),
8005 close: true,
8006 surround: true,
8007 newline: true,
8008 }],
8009 ..Default::default()
8010 },
8011 autoclose_before: "}".to_string(),
8012 ..Default::default()
8013 },
8014 Some(tree_sitter_rust::LANGUAGE.into()),
8015 ));
8016
8017 let text = r#"
8018 a
8019 b
8020 c
8021 "#
8022 .unindent();
8023
8024 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8025 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8026 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8027 editor
8028 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8029 .await;
8030
8031 editor.update_in(cx, |editor, window, cx| {
8032 editor.change_selections(None, window, cx, |s| {
8033 s.select_ranges([
8034 Point::new(0, 1)..Point::new(0, 1),
8035 Point::new(1, 1)..Point::new(1, 1),
8036 Point::new(2, 1)..Point::new(2, 1),
8037 ])
8038 });
8039
8040 editor.handle_input("{", window, cx);
8041 editor.handle_input("{", window, cx);
8042 editor.handle_input("_", window, cx);
8043 assert_eq!(
8044 editor.text(cx),
8045 "
8046 a{{_}}
8047 b{{_}}
8048 c{{_}}
8049 "
8050 .unindent()
8051 );
8052 assert_eq!(
8053 editor.selections.ranges::<Point>(cx),
8054 [
8055 Point::new(0, 4)..Point::new(0, 4),
8056 Point::new(1, 4)..Point::new(1, 4),
8057 Point::new(2, 4)..Point::new(2, 4)
8058 ]
8059 );
8060
8061 editor.backspace(&Default::default(), window, cx);
8062 editor.backspace(&Default::default(), window, cx);
8063 assert_eq!(
8064 editor.text(cx),
8065 "
8066 a{}
8067 b{}
8068 c{}
8069 "
8070 .unindent()
8071 );
8072 assert_eq!(
8073 editor.selections.ranges::<Point>(cx),
8074 [
8075 Point::new(0, 2)..Point::new(0, 2),
8076 Point::new(1, 2)..Point::new(1, 2),
8077 Point::new(2, 2)..Point::new(2, 2)
8078 ]
8079 );
8080
8081 editor.delete_to_previous_word_start(&Default::default(), window, cx);
8082 assert_eq!(
8083 editor.text(cx),
8084 "
8085 a
8086 b
8087 c
8088 "
8089 .unindent()
8090 );
8091 assert_eq!(
8092 editor.selections.ranges::<Point>(cx),
8093 [
8094 Point::new(0, 1)..Point::new(0, 1),
8095 Point::new(1, 1)..Point::new(1, 1),
8096 Point::new(2, 1)..Point::new(2, 1)
8097 ]
8098 );
8099 });
8100}
8101
8102#[gpui::test]
8103async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
8104 init_test(cx, |settings| {
8105 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
8106 });
8107
8108 let mut cx = EditorTestContext::new(cx).await;
8109
8110 let language = Arc::new(Language::new(
8111 LanguageConfig {
8112 brackets: BracketPairConfig {
8113 pairs: vec![
8114 BracketPair {
8115 start: "{".to_string(),
8116 end: "}".to_string(),
8117 close: true,
8118 surround: true,
8119 newline: true,
8120 },
8121 BracketPair {
8122 start: "(".to_string(),
8123 end: ")".to_string(),
8124 close: true,
8125 surround: true,
8126 newline: true,
8127 },
8128 BracketPair {
8129 start: "[".to_string(),
8130 end: "]".to_string(),
8131 close: false,
8132 surround: true,
8133 newline: true,
8134 },
8135 ],
8136 ..Default::default()
8137 },
8138 autoclose_before: "})]".to_string(),
8139 ..Default::default()
8140 },
8141 Some(tree_sitter_rust::LANGUAGE.into()),
8142 ));
8143
8144 cx.language_registry().add(language.clone());
8145 cx.update_buffer(|buffer, cx| {
8146 buffer.set_language(Some(language), cx);
8147 });
8148
8149 cx.set_state(
8150 &"
8151 {(ˇ)}
8152 [[ˇ]]
8153 {(ˇ)}
8154 "
8155 .unindent(),
8156 );
8157
8158 cx.update_editor(|editor, window, cx| {
8159 editor.backspace(&Default::default(), window, cx);
8160 editor.backspace(&Default::default(), window, cx);
8161 });
8162
8163 cx.assert_editor_state(
8164 &"
8165 ˇ
8166 ˇ]]
8167 ˇ
8168 "
8169 .unindent(),
8170 );
8171
8172 cx.update_editor(|editor, window, cx| {
8173 editor.handle_input("{", window, cx);
8174 editor.handle_input("{", window, cx);
8175 editor.move_right(&MoveRight, window, cx);
8176 editor.move_right(&MoveRight, window, cx);
8177 editor.move_left(&MoveLeft, window, cx);
8178 editor.move_left(&MoveLeft, window, cx);
8179 editor.backspace(&Default::default(), window, cx);
8180 });
8181
8182 cx.assert_editor_state(
8183 &"
8184 {ˇ}
8185 {ˇ}]]
8186 {ˇ}
8187 "
8188 .unindent(),
8189 );
8190
8191 cx.update_editor(|editor, window, cx| {
8192 editor.backspace(&Default::default(), window, cx);
8193 });
8194
8195 cx.assert_editor_state(
8196 &"
8197 ˇ
8198 ˇ]]
8199 ˇ
8200 "
8201 .unindent(),
8202 );
8203}
8204
8205#[gpui::test]
8206async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
8207 init_test(cx, |_| {});
8208
8209 let language = Arc::new(Language::new(
8210 LanguageConfig::default(),
8211 Some(tree_sitter_rust::LANGUAGE.into()),
8212 ));
8213
8214 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
8215 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8216 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8217 editor
8218 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8219 .await;
8220
8221 editor.update_in(cx, |editor, window, cx| {
8222 editor.set_auto_replace_emoji_shortcode(true);
8223
8224 editor.handle_input("Hello ", window, cx);
8225 editor.handle_input(":wave", window, cx);
8226 assert_eq!(editor.text(cx), "Hello :wave".unindent());
8227
8228 editor.handle_input(":", window, cx);
8229 assert_eq!(editor.text(cx), "Hello 👋".unindent());
8230
8231 editor.handle_input(" :smile", window, cx);
8232 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
8233
8234 editor.handle_input(":", window, cx);
8235 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
8236
8237 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
8238 editor.handle_input(":wave", window, cx);
8239 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
8240
8241 editor.handle_input(":", window, cx);
8242 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
8243
8244 editor.handle_input(":1", window, cx);
8245 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
8246
8247 editor.handle_input(":", window, cx);
8248 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
8249
8250 // Ensure shortcode does not get replaced when it is part of a word
8251 editor.handle_input(" Test:wave", window, cx);
8252 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
8253
8254 editor.handle_input(":", window, cx);
8255 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
8256
8257 editor.set_auto_replace_emoji_shortcode(false);
8258
8259 // Ensure shortcode does not get replaced when auto replace is off
8260 editor.handle_input(" :wave", window, cx);
8261 assert_eq!(
8262 editor.text(cx),
8263 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
8264 );
8265
8266 editor.handle_input(":", window, cx);
8267 assert_eq!(
8268 editor.text(cx),
8269 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
8270 );
8271 });
8272}
8273
8274#[gpui::test]
8275async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
8276 init_test(cx, |_| {});
8277
8278 let (text, insertion_ranges) = marked_text_ranges(
8279 indoc! {"
8280 ˇ
8281 "},
8282 false,
8283 );
8284
8285 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
8286 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8287
8288 _ = editor.update_in(cx, |editor, window, cx| {
8289 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
8290
8291 editor
8292 .insert_snippet(&insertion_ranges, snippet, window, cx)
8293 .unwrap();
8294
8295 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
8296 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
8297 assert_eq!(editor.text(cx), expected_text);
8298 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
8299 }
8300
8301 assert(
8302 editor,
8303 cx,
8304 indoc! {"
8305 type «» =•
8306 "},
8307 );
8308
8309 assert!(editor.context_menu_visible(), "There should be a matches");
8310 });
8311}
8312
8313#[gpui::test]
8314async fn test_snippets(cx: &mut TestAppContext) {
8315 init_test(cx, |_| {});
8316
8317 let (text, insertion_ranges) = marked_text_ranges(
8318 indoc! {"
8319 a.ˇ b
8320 a.ˇ b
8321 a.ˇ b
8322 "},
8323 false,
8324 );
8325
8326 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
8327 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8328
8329 editor.update_in(cx, |editor, window, cx| {
8330 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
8331
8332 editor
8333 .insert_snippet(&insertion_ranges, snippet, window, cx)
8334 .unwrap();
8335
8336 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
8337 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
8338 assert_eq!(editor.text(cx), expected_text);
8339 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
8340 }
8341
8342 assert(
8343 editor,
8344 cx,
8345 indoc! {"
8346 a.f(«one», two, «three») b
8347 a.f(«one», two, «three») b
8348 a.f(«one», two, «three») b
8349 "},
8350 );
8351
8352 // Can't move earlier than the first tab stop
8353 assert!(!editor.move_to_prev_snippet_tabstop(window, cx));
8354 assert(
8355 editor,
8356 cx,
8357 indoc! {"
8358 a.f(«one», two, «three») b
8359 a.f(«one», two, «three») b
8360 a.f(«one», two, «three») b
8361 "},
8362 );
8363
8364 assert!(editor.move_to_next_snippet_tabstop(window, cx));
8365 assert(
8366 editor,
8367 cx,
8368 indoc! {"
8369 a.f(one, «two», three) b
8370 a.f(one, «two», three) b
8371 a.f(one, «two», three) b
8372 "},
8373 );
8374
8375 editor.move_to_prev_snippet_tabstop(window, cx);
8376 assert(
8377 editor,
8378 cx,
8379 indoc! {"
8380 a.f(«one», two, «three») b
8381 a.f(«one», two, «three») b
8382 a.f(«one», two, «three») b
8383 "},
8384 );
8385
8386 assert!(editor.move_to_next_snippet_tabstop(window, cx));
8387 assert(
8388 editor,
8389 cx,
8390 indoc! {"
8391 a.f(one, «two», three) b
8392 a.f(one, «two», three) b
8393 a.f(one, «two», three) b
8394 "},
8395 );
8396 assert!(editor.move_to_next_snippet_tabstop(window, cx));
8397 assert(
8398 editor,
8399 cx,
8400 indoc! {"
8401 a.f(one, two, three)ˇ b
8402 a.f(one, two, three)ˇ b
8403 a.f(one, two, three)ˇ b
8404 "},
8405 );
8406
8407 // As soon as the last tab stop is reached, snippet state is gone
8408 editor.move_to_prev_snippet_tabstop(window, cx);
8409 assert(
8410 editor,
8411 cx,
8412 indoc! {"
8413 a.f(one, two, three)ˇ b
8414 a.f(one, two, three)ˇ b
8415 a.f(one, two, three)ˇ b
8416 "},
8417 );
8418 });
8419}
8420
8421#[gpui::test]
8422async fn test_document_format_during_save(cx: &mut TestAppContext) {
8423 init_test(cx, |_| {});
8424
8425 let fs = FakeFs::new(cx.executor());
8426 fs.insert_file(path!("/file.rs"), Default::default()).await;
8427
8428 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
8429
8430 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8431 language_registry.add(rust_lang());
8432 let mut fake_servers = language_registry.register_fake_lsp(
8433 "Rust",
8434 FakeLspAdapter {
8435 capabilities: lsp::ServerCapabilities {
8436 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8437 ..Default::default()
8438 },
8439 ..Default::default()
8440 },
8441 );
8442
8443 let buffer = project
8444 .update(cx, |project, cx| {
8445 project.open_local_buffer(path!("/file.rs"), cx)
8446 })
8447 .await
8448 .unwrap();
8449
8450 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8451 let (editor, cx) = cx.add_window_view(|window, cx| {
8452 build_editor_with_project(project.clone(), buffer, window, cx)
8453 });
8454 editor.update_in(cx, |editor, window, cx| {
8455 editor.set_text("one\ntwo\nthree\n", window, cx)
8456 });
8457 assert!(cx.read(|cx| editor.is_dirty(cx)));
8458
8459 cx.executor().start_waiting();
8460 let fake_server = fake_servers.next().await.unwrap();
8461
8462 {
8463 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8464 move |params, _| async move {
8465 assert_eq!(
8466 params.text_document.uri,
8467 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8468 );
8469 assert_eq!(params.options.tab_size, 4);
8470 Ok(Some(vec![lsp::TextEdit::new(
8471 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8472 ", ".to_string(),
8473 )]))
8474 },
8475 );
8476 let save = editor
8477 .update_in(cx, |editor, window, cx| {
8478 editor.save(true, project.clone(), window, cx)
8479 })
8480 .unwrap();
8481 cx.executor().start_waiting();
8482 save.await;
8483
8484 assert_eq!(
8485 editor.update(cx, |editor, cx| editor.text(cx)),
8486 "one, two\nthree\n"
8487 );
8488 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8489 }
8490
8491 {
8492 editor.update_in(cx, |editor, window, cx| {
8493 editor.set_text("one\ntwo\nthree\n", window, cx)
8494 });
8495 assert!(cx.read(|cx| editor.is_dirty(cx)));
8496
8497 // Ensure we can still save even if formatting hangs.
8498 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8499 move |params, _| async move {
8500 assert_eq!(
8501 params.text_document.uri,
8502 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8503 );
8504 futures::future::pending::<()>().await;
8505 unreachable!()
8506 },
8507 );
8508 let save = editor
8509 .update_in(cx, |editor, window, cx| {
8510 editor.save(true, project.clone(), window, cx)
8511 })
8512 .unwrap();
8513 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8514 cx.executor().start_waiting();
8515 save.await;
8516 assert_eq!(
8517 editor.update(cx, |editor, cx| editor.text(cx)),
8518 "one\ntwo\nthree\n"
8519 );
8520 }
8521
8522 // For non-dirty buffer, no formatting request should be sent
8523 {
8524 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8525
8526 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
8527 panic!("Should not be invoked on non-dirty buffer");
8528 });
8529 let save = editor
8530 .update_in(cx, |editor, window, cx| {
8531 editor.save(true, project.clone(), window, cx)
8532 })
8533 .unwrap();
8534 cx.executor().start_waiting();
8535 save.await;
8536 }
8537
8538 // Set rust language override and assert overridden tabsize is sent to language server
8539 update_test_language_settings(cx, |settings| {
8540 settings.languages.insert(
8541 "Rust".into(),
8542 LanguageSettingsContent {
8543 tab_size: NonZeroU32::new(8),
8544 ..Default::default()
8545 },
8546 );
8547 });
8548
8549 {
8550 editor.update_in(cx, |editor, window, cx| {
8551 editor.set_text("somehting_new\n", window, cx)
8552 });
8553 assert!(cx.read(|cx| editor.is_dirty(cx)));
8554 let _formatting_request_signal = fake_server
8555 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
8556 assert_eq!(
8557 params.text_document.uri,
8558 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8559 );
8560 assert_eq!(params.options.tab_size, 8);
8561 Ok(Some(vec![]))
8562 });
8563 let save = editor
8564 .update_in(cx, |editor, window, cx| {
8565 editor.save(true, project.clone(), window, cx)
8566 })
8567 .unwrap();
8568 cx.executor().start_waiting();
8569 save.await;
8570 }
8571}
8572
8573#[gpui::test]
8574async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
8575 init_test(cx, |_| {});
8576
8577 let cols = 4;
8578 let rows = 10;
8579 let sample_text_1 = sample_text(rows, cols, 'a');
8580 assert_eq!(
8581 sample_text_1,
8582 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
8583 );
8584 let sample_text_2 = sample_text(rows, cols, 'l');
8585 assert_eq!(
8586 sample_text_2,
8587 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
8588 );
8589 let sample_text_3 = sample_text(rows, cols, 'v');
8590 assert_eq!(
8591 sample_text_3,
8592 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
8593 );
8594
8595 let fs = FakeFs::new(cx.executor());
8596 fs.insert_tree(
8597 path!("/a"),
8598 json!({
8599 "main.rs": sample_text_1,
8600 "other.rs": sample_text_2,
8601 "lib.rs": sample_text_3,
8602 }),
8603 )
8604 .await;
8605
8606 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
8607 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
8608 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
8609
8610 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8611 language_registry.add(rust_lang());
8612 let mut fake_servers = language_registry.register_fake_lsp(
8613 "Rust",
8614 FakeLspAdapter {
8615 capabilities: lsp::ServerCapabilities {
8616 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8617 ..Default::default()
8618 },
8619 ..Default::default()
8620 },
8621 );
8622
8623 let worktree = project.update(cx, |project, cx| {
8624 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
8625 assert_eq!(worktrees.len(), 1);
8626 worktrees.pop().unwrap()
8627 });
8628 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
8629
8630 let buffer_1 = project
8631 .update(cx, |project, cx| {
8632 project.open_buffer((worktree_id, "main.rs"), cx)
8633 })
8634 .await
8635 .unwrap();
8636 let buffer_2 = project
8637 .update(cx, |project, cx| {
8638 project.open_buffer((worktree_id, "other.rs"), cx)
8639 })
8640 .await
8641 .unwrap();
8642 let buffer_3 = project
8643 .update(cx, |project, cx| {
8644 project.open_buffer((worktree_id, "lib.rs"), cx)
8645 })
8646 .await
8647 .unwrap();
8648
8649 let multi_buffer = cx.new(|cx| {
8650 let mut multi_buffer = MultiBuffer::new(ReadWrite);
8651 multi_buffer.push_excerpts(
8652 buffer_1.clone(),
8653 [
8654 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8655 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8656 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8657 ],
8658 cx,
8659 );
8660 multi_buffer.push_excerpts(
8661 buffer_2.clone(),
8662 [
8663 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8664 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8665 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8666 ],
8667 cx,
8668 );
8669 multi_buffer.push_excerpts(
8670 buffer_3.clone(),
8671 [
8672 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8673 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8674 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8675 ],
8676 cx,
8677 );
8678 multi_buffer
8679 });
8680 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
8681 Editor::new(
8682 EditorMode::full(),
8683 multi_buffer,
8684 Some(project.clone()),
8685 window,
8686 cx,
8687 )
8688 });
8689
8690 multi_buffer_editor.update_in(cx, |editor, window, cx| {
8691 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
8692 s.select_ranges(Some(1..2))
8693 });
8694 editor.insert("|one|two|three|", window, cx);
8695 });
8696 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
8697 multi_buffer_editor.update_in(cx, |editor, window, cx| {
8698 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
8699 s.select_ranges(Some(60..70))
8700 });
8701 editor.insert("|four|five|six|", window, cx);
8702 });
8703 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
8704
8705 // First two buffers should be edited, but not the third one.
8706 assert_eq!(
8707 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
8708 "a|one|two|three|aa\nbbbb\ncccc\n\nffff\ngggg\n\njjjj\nllll\nmmmm\nnnnn|four|five|six|\nr\n\nuuuu\nvvvv\nwwww\nxxxx\n\n{{{{\n||||\n\n\u{7f}\u{7f}\u{7f}\u{7f}",
8709 );
8710 buffer_1.update(cx, |buffer, _| {
8711 assert!(buffer.is_dirty());
8712 assert_eq!(
8713 buffer.text(),
8714 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
8715 )
8716 });
8717 buffer_2.update(cx, |buffer, _| {
8718 assert!(buffer.is_dirty());
8719 assert_eq!(
8720 buffer.text(),
8721 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
8722 )
8723 });
8724 buffer_3.update(cx, |buffer, _| {
8725 assert!(!buffer.is_dirty());
8726 assert_eq!(buffer.text(), sample_text_3,)
8727 });
8728 cx.executor().run_until_parked();
8729
8730 cx.executor().start_waiting();
8731 let save = multi_buffer_editor
8732 .update_in(cx, |editor, window, cx| {
8733 editor.save(true, project.clone(), window, cx)
8734 })
8735 .unwrap();
8736
8737 let fake_server = fake_servers.next().await.unwrap();
8738 fake_server
8739 .server
8740 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
8741 Ok(Some(vec![lsp::TextEdit::new(
8742 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8743 format!("[{} formatted]", params.text_document.uri),
8744 )]))
8745 })
8746 .detach();
8747 save.await;
8748
8749 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
8750 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
8751 assert_eq!(
8752 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
8753 uri!(
8754 "a|o[file:///a/main.rs formatted]bbbb\ncccc\n\nffff\ngggg\n\njjjj\n\nlll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|\nr\n\nuuuu\n\nvvvv\nwwww\nxxxx\n\n{{{{\n||||\n\n\u{7f}\u{7f}\u{7f}\u{7f}"
8755 ),
8756 );
8757 buffer_1.update(cx, |buffer, _| {
8758 assert!(!buffer.is_dirty());
8759 assert_eq!(
8760 buffer.text(),
8761 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
8762 )
8763 });
8764 buffer_2.update(cx, |buffer, _| {
8765 assert!(!buffer.is_dirty());
8766 assert_eq!(
8767 buffer.text(),
8768 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
8769 )
8770 });
8771 buffer_3.update(cx, |buffer, _| {
8772 assert!(!buffer.is_dirty());
8773 assert_eq!(buffer.text(), sample_text_3,)
8774 });
8775}
8776
8777#[gpui::test]
8778async fn test_range_format_during_save(cx: &mut TestAppContext) {
8779 init_test(cx, |_| {});
8780
8781 let fs = FakeFs::new(cx.executor());
8782 fs.insert_file(path!("/file.rs"), Default::default()).await;
8783
8784 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8785
8786 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8787 language_registry.add(rust_lang());
8788 let mut fake_servers = language_registry.register_fake_lsp(
8789 "Rust",
8790 FakeLspAdapter {
8791 capabilities: lsp::ServerCapabilities {
8792 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
8793 ..Default::default()
8794 },
8795 ..Default::default()
8796 },
8797 );
8798
8799 let buffer = project
8800 .update(cx, |project, cx| {
8801 project.open_local_buffer(path!("/file.rs"), cx)
8802 })
8803 .await
8804 .unwrap();
8805
8806 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8807 let (editor, cx) = cx.add_window_view(|window, cx| {
8808 build_editor_with_project(project.clone(), buffer, window, cx)
8809 });
8810 editor.update_in(cx, |editor, window, cx| {
8811 editor.set_text("one\ntwo\nthree\n", window, cx)
8812 });
8813 assert!(cx.read(|cx| editor.is_dirty(cx)));
8814
8815 cx.executor().start_waiting();
8816 let fake_server = fake_servers.next().await.unwrap();
8817
8818 let save = editor
8819 .update_in(cx, |editor, window, cx| {
8820 editor.save(true, project.clone(), window, cx)
8821 })
8822 .unwrap();
8823 fake_server
8824 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
8825 assert_eq!(
8826 params.text_document.uri,
8827 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8828 );
8829 assert_eq!(params.options.tab_size, 4);
8830 Ok(Some(vec![lsp::TextEdit::new(
8831 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8832 ", ".to_string(),
8833 )]))
8834 })
8835 .next()
8836 .await;
8837 cx.executor().start_waiting();
8838 save.await;
8839 assert_eq!(
8840 editor.update(cx, |editor, cx| editor.text(cx)),
8841 "one, two\nthree\n"
8842 );
8843 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8844
8845 editor.update_in(cx, |editor, window, cx| {
8846 editor.set_text("one\ntwo\nthree\n", window, cx)
8847 });
8848 assert!(cx.read(|cx| editor.is_dirty(cx)));
8849
8850 // Ensure we can still save even if formatting hangs.
8851 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
8852 move |params, _| async move {
8853 assert_eq!(
8854 params.text_document.uri,
8855 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8856 );
8857 futures::future::pending::<()>().await;
8858 unreachable!()
8859 },
8860 );
8861 let save = editor
8862 .update_in(cx, |editor, window, cx| {
8863 editor.save(true, project.clone(), window, cx)
8864 })
8865 .unwrap();
8866 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8867 cx.executor().start_waiting();
8868 save.await;
8869 assert_eq!(
8870 editor.update(cx, |editor, cx| editor.text(cx)),
8871 "one\ntwo\nthree\n"
8872 );
8873 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8874
8875 // For non-dirty buffer, no formatting request should be sent
8876 let save = editor
8877 .update_in(cx, |editor, window, cx| {
8878 editor.save(true, project.clone(), window, cx)
8879 })
8880 .unwrap();
8881 let _pending_format_request = fake_server
8882 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
8883 panic!("Should not be invoked on non-dirty buffer");
8884 })
8885 .next();
8886 cx.executor().start_waiting();
8887 save.await;
8888
8889 // Set Rust language override and assert overridden tabsize is sent to language server
8890 update_test_language_settings(cx, |settings| {
8891 settings.languages.insert(
8892 "Rust".into(),
8893 LanguageSettingsContent {
8894 tab_size: NonZeroU32::new(8),
8895 ..Default::default()
8896 },
8897 );
8898 });
8899
8900 editor.update_in(cx, |editor, window, cx| {
8901 editor.set_text("somehting_new\n", window, cx)
8902 });
8903 assert!(cx.read(|cx| editor.is_dirty(cx)));
8904 let save = editor
8905 .update_in(cx, |editor, window, cx| {
8906 editor.save(true, project.clone(), window, cx)
8907 })
8908 .unwrap();
8909 fake_server
8910 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
8911 assert_eq!(
8912 params.text_document.uri,
8913 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8914 );
8915 assert_eq!(params.options.tab_size, 8);
8916 Ok(Some(vec![]))
8917 })
8918 .next()
8919 .await;
8920 cx.executor().start_waiting();
8921 save.await;
8922}
8923
8924#[gpui::test]
8925async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
8926 init_test(cx, |settings| {
8927 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
8928 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
8929 ))
8930 });
8931
8932 let fs = FakeFs::new(cx.executor());
8933 fs.insert_file(path!("/file.rs"), Default::default()).await;
8934
8935 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8936
8937 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8938 language_registry.add(Arc::new(Language::new(
8939 LanguageConfig {
8940 name: "Rust".into(),
8941 matcher: LanguageMatcher {
8942 path_suffixes: vec!["rs".to_string()],
8943 ..Default::default()
8944 },
8945 ..LanguageConfig::default()
8946 },
8947 Some(tree_sitter_rust::LANGUAGE.into()),
8948 )));
8949 update_test_language_settings(cx, |settings| {
8950 // Enable Prettier formatting for the same buffer, and ensure
8951 // LSP is called instead of Prettier.
8952 settings.defaults.prettier = Some(PrettierSettings {
8953 allowed: true,
8954 ..PrettierSettings::default()
8955 });
8956 });
8957 let mut fake_servers = language_registry.register_fake_lsp(
8958 "Rust",
8959 FakeLspAdapter {
8960 capabilities: lsp::ServerCapabilities {
8961 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8962 ..Default::default()
8963 },
8964 ..Default::default()
8965 },
8966 );
8967
8968 let buffer = project
8969 .update(cx, |project, cx| {
8970 project.open_local_buffer(path!("/file.rs"), cx)
8971 })
8972 .await
8973 .unwrap();
8974
8975 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8976 let (editor, cx) = cx.add_window_view(|window, cx| {
8977 build_editor_with_project(project.clone(), buffer, window, cx)
8978 });
8979 editor.update_in(cx, |editor, window, cx| {
8980 editor.set_text("one\ntwo\nthree\n", window, cx)
8981 });
8982
8983 cx.executor().start_waiting();
8984 let fake_server = fake_servers.next().await.unwrap();
8985
8986 let format = editor
8987 .update_in(cx, |editor, window, cx| {
8988 editor.perform_format(
8989 project.clone(),
8990 FormatTrigger::Manual,
8991 FormatTarget::Buffers,
8992 window,
8993 cx,
8994 )
8995 })
8996 .unwrap();
8997 fake_server
8998 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
8999 assert_eq!(
9000 params.text_document.uri,
9001 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9002 );
9003 assert_eq!(params.options.tab_size, 4);
9004 Ok(Some(vec![lsp::TextEdit::new(
9005 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9006 ", ".to_string(),
9007 )]))
9008 })
9009 .next()
9010 .await;
9011 cx.executor().start_waiting();
9012 format.await;
9013 assert_eq!(
9014 editor.update(cx, |editor, cx| editor.text(cx)),
9015 "one, two\nthree\n"
9016 );
9017
9018 editor.update_in(cx, |editor, window, cx| {
9019 editor.set_text("one\ntwo\nthree\n", window, cx)
9020 });
9021 // Ensure we don't lock if formatting hangs.
9022 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9023 move |params, _| async move {
9024 assert_eq!(
9025 params.text_document.uri,
9026 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9027 );
9028 futures::future::pending::<()>().await;
9029 unreachable!()
9030 },
9031 );
9032 let format = editor
9033 .update_in(cx, |editor, window, cx| {
9034 editor.perform_format(
9035 project,
9036 FormatTrigger::Manual,
9037 FormatTarget::Buffers,
9038 window,
9039 cx,
9040 )
9041 })
9042 .unwrap();
9043 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9044 cx.executor().start_waiting();
9045 format.await;
9046 assert_eq!(
9047 editor.update(cx, |editor, cx| editor.text(cx)),
9048 "one\ntwo\nthree\n"
9049 );
9050}
9051
9052#[gpui::test]
9053async fn test_multiple_formatters(cx: &mut TestAppContext) {
9054 init_test(cx, |settings| {
9055 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
9056 settings.defaults.formatter =
9057 Some(language_settings::SelectedFormatter::List(FormatterList(
9058 vec![
9059 Formatter::LanguageServer { name: None },
9060 Formatter::CodeActions(
9061 [
9062 ("code-action-1".into(), true),
9063 ("code-action-2".into(), true),
9064 ]
9065 .into_iter()
9066 .collect(),
9067 ),
9068 ]
9069 .into(),
9070 )))
9071 });
9072
9073 let fs = FakeFs::new(cx.executor());
9074 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
9075 .await;
9076
9077 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9078 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9079 language_registry.add(rust_lang());
9080
9081 let mut fake_servers = language_registry.register_fake_lsp(
9082 "Rust",
9083 FakeLspAdapter {
9084 capabilities: lsp::ServerCapabilities {
9085 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9086 execute_command_provider: Some(lsp::ExecuteCommandOptions {
9087 commands: vec!["the-command-for-code-action-1".into()],
9088 ..Default::default()
9089 }),
9090 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
9091 ..Default::default()
9092 },
9093 ..Default::default()
9094 },
9095 );
9096
9097 let buffer = project
9098 .update(cx, |project, cx| {
9099 project.open_local_buffer(path!("/file.rs"), cx)
9100 })
9101 .await
9102 .unwrap();
9103
9104 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9105 let (editor, cx) = cx.add_window_view(|window, cx| {
9106 build_editor_with_project(project.clone(), buffer, window, cx)
9107 });
9108
9109 cx.executor().start_waiting();
9110
9111 let fake_server = fake_servers.next().await.unwrap();
9112 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9113 move |_params, _| async move {
9114 Ok(Some(vec![lsp::TextEdit::new(
9115 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9116 "applied-formatting\n".to_string(),
9117 )]))
9118 },
9119 );
9120 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
9121 move |params, _| async move {
9122 assert_eq!(
9123 params.context.only,
9124 Some(vec!["code-action-1".into(), "code-action-2".into()])
9125 );
9126 let uri = lsp::Url::from_file_path(path!("/file.rs")).unwrap();
9127 Ok(Some(vec![
9128 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
9129 kind: Some("code-action-1".into()),
9130 edit: Some(lsp::WorkspaceEdit::new(
9131 [(
9132 uri.clone(),
9133 vec![lsp::TextEdit::new(
9134 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9135 "applied-code-action-1-edit\n".to_string(),
9136 )],
9137 )]
9138 .into_iter()
9139 .collect(),
9140 )),
9141 command: Some(lsp::Command {
9142 command: "the-command-for-code-action-1".into(),
9143 ..Default::default()
9144 }),
9145 ..Default::default()
9146 }),
9147 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
9148 kind: Some("code-action-2".into()),
9149 edit: Some(lsp::WorkspaceEdit::new(
9150 [(
9151 uri.clone(),
9152 vec![lsp::TextEdit::new(
9153 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9154 "applied-code-action-2-edit\n".to_string(),
9155 )],
9156 )]
9157 .into_iter()
9158 .collect(),
9159 )),
9160 ..Default::default()
9161 }),
9162 ]))
9163 },
9164 );
9165
9166 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
9167 move |params, _| async move { Ok(params) }
9168 });
9169
9170 let command_lock = Arc::new(futures::lock::Mutex::new(()));
9171 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
9172 let fake = fake_server.clone();
9173 let lock = command_lock.clone();
9174 move |params, _| {
9175 assert_eq!(params.command, "the-command-for-code-action-1");
9176 let fake = fake.clone();
9177 let lock = lock.clone();
9178 async move {
9179 lock.lock().await;
9180 fake.server
9181 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
9182 label: None,
9183 edit: lsp::WorkspaceEdit {
9184 changes: Some(
9185 [(
9186 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
9187 vec![lsp::TextEdit {
9188 range: lsp::Range::new(
9189 lsp::Position::new(0, 0),
9190 lsp::Position::new(0, 0),
9191 ),
9192 new_text: "applied-code-action-1-command\n".into(),
9193 }],
9194 )]
9195 .into_iter()
9196 .collect(),
9197 ),
9198 ..Default::default()
9199 },
9200 })
9201 .await
9202 .into_response()
9203 .unwrap();
9204 Ok(Some(json!(null)))
9205 }
9206 }
9207 });
9208
9209 cx.executor().start_waiting();
9210 editor
9211 .update_in(cx, |editor, window, cx| {
9212 editor.perform_format(
9213 project.clone(),
9214 FormatTrigger::Manual,
9215 FormatTarget::Buffers,
9216 window,
9217 cx,
9218 )
9219 })
9220 .unwrap()
9221 .await;
9222 editor.update(cx, |editor, cx| {
9223 assert_eq!(
9224 editor.text(cx),
9225 r#"
9226 applied-code-action-2-edit
9227 applied-code-action-1-command
9228 applied-code-action-1-edit
9229 applied-formatting
9230 one
9231 two
9232 three
9233 "#
9234 .unindent()
9235 );
9236 });
9237
9238 editor.update_in(cx, |editor, window, cx| {
9239 editor.undo(&Default::default(), window, cx);
9240 assert_eq!(editor.text(cx), "one \ntwo \nthree");
9241 });
9242
9243 // Perform a manual edit while waiting for an LSP command
9244 // that's being run as part of a formatting code action.
9245 let lock_guard = command_lock.lock().await;
9246 let format = editor
9247 .update_in(cx, |editor, window, cx| {
9248 editor.perform_format(
9249 project.clone(),
9250 FormatTrigger::Manual,
9251 FormatTarget::Buffers,
9252 window,
9253 cx,
9254 )
9255 })
9256 .unwrap();
9257 cx.run_until_parked();
9258 editor.update(cx, |editor, cx| {
9259 assert_eq!(
9260 editor.text(cx),
9261 r#"
9262 applied-code-action-1-edit
9263 applied-formatting
9264 one
9265 two
9266 three
9267 "#
9268 .unindent()
9269 );
9270
9271 editor.buffer.update(cx, |buffer, cx| {
9272 let ix = buffer.len(cx);
9273 buffer.edit([(ix..ix, "edited\n")], None, cx);
9274 });
9275 });
9276
9277 // Allow the LSP command to proceed. Because the buffer was edited,
9278 // the second code action will not be run.
9279 drop(lock_guard);
9280 format.await;
9281 editor.update_in(cx, |editor, window, cx| {
9282 assert_eq!(
9283 editor.text(cx),
9284 r#"
9285 applied-code-action-1-command
9286 applied-code-action-1-edit
9287 applied-formatting
9288 one
9289 two
9290 three
9291 edited
9292 "#
9293 .unindent()
9294 );
9295
9296 // The manual edit is undone first, because it is the last thing the user did
9297 // (even though the command completed afterwards).
9298 editor.undo(&Default::default(), window, cx);
9299 assert_eq!(
9300 editor.text(cx),
9301 r#"
9302 applied-code-action-1-command
9303 applied-code-action-1-edit
9304 applied-formatting
9305 one
9306 two
9307 three
9308 "#
9309 .unindent()
9310 );
9311
9312 // All the formatting (including the command, which completed after the manual edit)
9313 // is undone together.
9314 editor.undo(&Default::default(), window, cx);
9315 assert_eq!(editor.text(cx), "one \ntwo \nthree");
9316 });
9317}
9318
9319#[gpui::test]
9320async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
9321 init_test(cx, |settings| {
9322 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
9323 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
9324 ))
9325 });
9326
9327 let fs = FakeFs::new(cx.executor());
9328 fs.insert_file(path!("/file.ts"), Default::default()).await;
9329
9330 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9331
9332 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9333 language_registry.add(Arc::new(Language::new(
9334 LanguageConfig {
9335 name: "TypeScript".into(),
9336 matcher: LanguageMatcher {
9337 path_suffixes: vec!["ts".to_string()],
9338 ..Default::default()
9339 },
9340 ..LanguageConfig::default()
9341 },
9342 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
9343 )));
9344 update_test_language_settings(cx, |settings| {
9345 settings.defaults.prettier = Some(PrettierSettings {
9346 allowed: true,
9347 ..PrettierSettings::default()
9348 });
9349 });
9350 let mut fake_servers = language_registry.register_fake_lsp(
9351 "TypeScript",
9352 FakeLspAdapter {
9353 capabilities: lsp::ServerCapabilities {
9354 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
9355 ..Default::default()
9356 },
9357 ..Default::default()
9358 },
9359 );
9360
9361 let buffer = project
9362 .update(cx, |project, cx| {
9363 project.open_local_buffer(path!("/file.ts"), cx)
9364 })
9365 .await
9366 .unwrap();
9367
9368 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9369 let (editor, cx) = cx.add_window_view(|window, cx| {
9370 build_editor_with_project(project.clone(), buffer, window, cx)
9371 });
9372 editor.update_in(cx, |editor, window, cx| {
9373 editor.set_text(
9374 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
9375 window,
9376 cx,
9377 )
9378 });
9379
9380 cx.executor().start_waiting();
9381 let fake_server = fake_servers.next().await.unwrap();
9382
9383 let format = editor
9384 .update_in(cx, |editor, window, cx| {
9385 editor.perform_code_action_kind(
9386 project.clone(),
9387 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
9388 window,
9389 cx,
9390 )
9391 })
9392 .unwrap();
9393 fake_server
9394 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
9395 assert_eq!(
9396 params.text_document.uri,
9397 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
9398 );
9399 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
9400 lsp::CodeAction {
9401 title: "Organize Imports".to_string(),
9402 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
9403 edit: Some(lsp::WorkspaceEdit {
9404 changes: Some(
9405 [(
9406 params.text_document.uri.clone(),
9407 vec![lsp::TextEdit::new(
9408 lsp::Range::new(
9409 lsp::Position::new(1, 0),
9410 lsp::Position::new(2, 0),
9411 ),
9412 "".to_string(),
9413 )],
9414 )]
9415 .into_iter()
9416 .collect(),
9417 ),
9418 ..Default::default()
9419 }),
9420 ..Default::default()
9421 },
9422 )]))
9423 })
9424 .next()
9425 .await;
9426 cx.executor().start_waiting();
9427 format.await;
9428 assert_eq!(
9429 editor.update(cx, |editor, cx| editor.text(cx)),
9430 "import { a } from 'module';\n\nconst x = a;\n"
9431 );
9432
9433 editor.update_in(cx, |editor, window, cx| {
9434 editor.set_text(
9435 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
9436 window,
9437 cx,
9438 )
9439 });
9440 // Ensure we don't lock if code action hangs.
9441 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
9442 move |params, _| async move {
9443 assert_eq!(
9444 params.text_document.uri,
9445 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
9446 );
9447 futures::future::pending::<()>().await;
9448 unreachable!()
9449 },
9450 );
9451 let format = editor
9452 .update_in(cx, |editor, window, cx| {
9453 editor.perform_code_action_kind(
9454 project,
9455 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
9456 window,
9457 cx,
9458 )
9459 })
9460 .unwrap();
9461 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
9462 cx.executor().start_waiting();
9463 format.await;
9464 assert_eq!(
9465 editor.update(cx, |editor, cx| editor.text(cx)),
9466 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
9467 );
9468}
9469
9470#[gpui::test]
9471async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
9472 init_test(cx, |_| {});
9473
9474 let mut cx = EditorLspTestContext::new_rust(
9475 lsp::ServerCapabilities {
9476 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9477 ..Default::default()
9478 },
9479 cx,
9480 )
9481 .await;
9482
9483 cx.set_state(indoc! {"
9484 one.twoˇ
9485 "});
9486
9487 // The format request takes a long time. When it completes, it inserts
9488 // a newline and an indent before the `.`
9489 cx.lsp
9490 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
9491 let executor = cx.background_executor().clone();
9492 async move {
9493 executor.timer(Duration::from_millis(100)).await;
9494 Ok(Some(vec![lsp::TextEdit {
9495 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
9496 new_text: "\n ".into(),
9497 }]))
9498 }
9499 });
9500
9501 // Submit a format request.
9502 let format_1 = cx
9503 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
9504 .unwrap();
9505 cx.executor().run_until_parked();
9506
9507 // Submit a second format request.
9508 let format_2 = cx
9509 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
9510 .unwrap();
9511 cx.executor().run_until_parked();
9512
9513 // Wait for both format requests to complete
9514 cx.executor().advance_clock(Duration::from_millis(200));
9515 cx.executor().start_waiting();
9516 format_1.await.unwrap();
9517 cx.executor().start_waiting();
9518 format_2.await.unwrap();
9519
9520 // The formatting edits only happens once.
9521 cx.assert_editor_state(indoc! {"
9522 one
9523 .twoˇ
9524 "});
9525}
9526
9527#[gpui::test]
9528async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
9529 init_test(cx, |settings| {
9530 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
9531 });
9532
9533 let mut cx = EditorLspTestContext::new_rust(
9534 lsp::ServerCapabilities {
9535 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9536 ..Default::default()
9537 },
9538 cx,
9539 )
9540 .await;
9541
9542 // Set up a buffer white some trailing whitespace and no trailing newline.
9543 cx.set_state(
9544 &[
9545 "one ", //
9546 "twoˇ", //
9547 "three ", //
9548 "four", //
9549 ]
9550 .join("\n"),
9551 );
9552
9553 // Submit a format request.
9554 let format = cx
9555 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
9556 .unwrap();
9557
9558 // Record which buffer changes have been sent to the language server
9559 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
9560 cx.lsp
9561 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
9562 let buffer_changes = buffer_changes.clone();
9563 move |params, _| {
9564 buffer_changes.lock().extend(
9565 params
9566 .content_changes
9567 .into_iter()
9568 .map(|e| (e.range.unwrap(), e.text)),
9569 );
9570 }
9571 });
9572
9573 // Handle formatting requests to the language server.
9574 cx.lsp
9575 .set_request_handler::<lsp::request::Formatting, _, _>({
9576 let buffer_changes = buffer_changes.clone();
9577 move |_, _| {
9578 // When formatting is requested, trailing whitespace has already been stripped,
9579 // and the trailing newline has already been added.
9580 assert_eq!(
9581 &buffer_changes.lock()[1..],
9582 &[
9583 (
9584 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
9585 "".into()
9586 ),
9587 (
9588 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
9589 "".into()
9590 ),
9591 (
9592 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
9593 "\n".into()
9594 ),
9595 ]
9596 );
9597
9598 // Insert blank lines between each line of the buffer.
9599 async move {
9600 Ok(Some(vec![
9601 lsp::TextEdit {
9602 range: lsp::Range::new(
9603 lsp::Position::new(1, 0),
9604 lsp::Position::new(1, 0),
9605 ),
9606 new_text: "\n".into(),
9607 },
9608 lsp::TextEdit {
9609 range: lsp::Range::new(
9610 lsp::Position::new(2, 0),
9611 lsp::Position::new(2, 0),
9612 ),
9613 new_text: "\n".into(),
9614 },
9615 ]))
9616 }
9617 }
9618 });
9619
9620 // After formatting the buffer, the trailing whitespace is stripped,
9621 // a newline is appended, and the edits provided by the language server
9622 // have been applied.
9623 format.await.unwrap();
9624 cx.assert_editor_state(
9625 &[
9626 "one", //
9627 "", //
9628 "twoˇ", //
9629 "", //
9630 "three", //
9631 "four", //
9632 "", //
9633 ]
9634 .join("\n"),
9635 );
9636
9637 // Undoing the formatting undoes the trailing whitespace removal, the
9638 // trailing newline, and the LSP edits.
9639 cx.update_buffer(|buffer, cx| buffer.undo(cx));
9640 cx.assert_editor_state(
9641 &[
9642 "one ", //
9643 "twoˇ", //
9644 "three ", //
9645 "four", //
9646 ]
9647 .join("\n"),
9648 );
9649}
9650
9651#[gpui::test]
9652async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
9653 cx: &mut TestAppContext,
9654) {
9655 init_test(cx, |_| {});
9656
9657 cx.update(|cx| {
9658 cx.update_global::<SettingsStore, _>(|settings, cx| {
9659 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9660 settings.auto_signature_help = Some(true);
9661 });
9662 });
9663 });
9664
9665 let mut cx = EditorLspTestContext::new_rust(
9666 lsp::ServerCapabilities {
9667 signature_help_provider: Some(lsp::SignatureHelpOptions {
9668 ..Default::default()
9669 }),
9670 ..Default::default()
9671 },
9672 cx,
9673 )
9674 .await;
9675
9676 let language = Language::new(
9677 LanguageConfig {
9678 name: "Rust".into(),
9679 brackets: BracketPairConfig {
9680 pairs: vec![
9681 BracketPair {
9682 start: "{".to_string(),
9683 end: "}".to_string(),
9684 close: true,
9685 surround: true,
9686 newline: true,
9687 },
9688 BracketPair {
9689 start: "(".to_string(),
9690 end: ")".to_string(),
9691 close: true,
9692 surround: true,
9693 newline: true,
9694 },
9695 BracketPair {
9696 start: "/*".to_string(),
9697 end: " */".to_string(),
9698 close: true,
9699 surround: true,
9700 newline: true,
9701 },
9702 BracketPair {
9703 start: "[".to_string(),
9704 end: "]".to_string(),
9705 close: false,
9706 surround: false,
9707 newline: true,
9708 },
9709 BracketPair {
9710 start: "\"".to_string(),
9711 end: "\"".to_string(),
9712 close: true,
9713 surround: true,
9714 newline: false,
9715 },
9716 BracketPair {
9717 start: "<".to_string(),
9718 end: ">".to_string(),
9719 close: false,
9720 surround: true,
9721 newline: true,
9722 },
9723 ],
9724 ..Default::default()
9725 },
9726 autoclose_before: "})]".to_string(),
9727 ..Default::default()
9728 },
9729 Some(tree_sitter_rust::LANGUAGE.into()),
9730 );
9731 let language = Arc::new(language);
9732
9733 cx.language_registry().add(language.clone());
9734 cx.update_buffer(|buffer, cx| {
9735 buffer.set_language(Some(language), cx);
9736 });
9737
9738 cx.set_state(
9739 &r#"
9740 fn main() {
9741 sampleˇ
9742 }
9743 "#
9744 .unindent(),
9745 );
9746
9747 cx.update_editor(|editor, window, cx| {
9748 editor.handle_input("(", window, cx);
9749 });
9750 cx.assert_editor_state(
9751 &"
9752 fn main() {
9753 sample(ˇ)
9754 }
9755 "
9756 .unindent(),
9757 );
9758
9759 let mocked_response = lsp::SignatureHelp {
9760 signatures: vec![lsp::SignatureInformation {
9761 label: "fn sample(param1: u8, param2: u8)".to_string(),
9762 documentation: None,
9763 parameters: Some(vec![
9764 lsp::ParameterInformation {
9765 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9766 documentation: None,
9767 },
9768 lsp::ParameterInformation {
9769 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9770 documentation: None,
9771 },
9772 ]),
9773 active_parameter: None,
9774 }],
9775 active_signature: Some(0),
9776 active_parameter: Some(0),
9777 };
9778 handle_signature_help_request(&mut cx, mocked_response).await;
9779
9780 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9781 .await;
9782
9783 cx.editor(|editor, _, _| {
9784 let signature_help_state = editor.signature_help_state.popover().cloned();
9785 assert_eq!(
9786 signature_help_state.unwrap().label,
9787 "param1: u8, param2: u8"
9788 );
9789 });
9790}
9791
9792#[gpui::test]
9793async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
9794 init_test(cx, |_| {});
9795
9796 cx.update(|cx| {
9797 cx.update_global::<SettingsStore, _>(|settings, cx| {
9798 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9799 settings.auto_signature_help = Some(false);
9800 settings.show_signature_help_after_edits = Some(false);
9801 });
9802 });
9803 });
9804
9805 let mut cx = EditorLspTestContext::new_rust(
9806 lsp::ServerCapabilities {
9807 signature_help_provider: Some(lsp::SignatureHelpOptions {
9808 ..Default::default()
9809 }),
9810 ..Default::default()
9811 },
9812 cx,
9813 )
9814 .await;
9815
9816 let language = Language::new(
9817 LanguageConfig {
9818 name: "Rust".into(),
9819 brackets: BracketPairConfig {
9820 pairs: vec![
9821 BracketPair {
9822 start: "{".to_string(),
9823 end: "}".to_string(),
9824 close: true,
9825 surround: true,
9826 newline: true,
9827 },
9828 BracketPair {
9829 start: "(".to_string(),
9830 end: ")".to_string(),
9831 close: true,
9832 surround: true,
9833 newline: true,
9834 },
9835 BracketPair {
9836 start: "/*".to_string(),
9837 end: " */".to_string(),
9838 close: true,
9839 surround: true,
9840 newline: true,
9841 },
9842 BracketPair {
9843 start: "[".to_string(),
9844 end: "]".to_string(),
9845 close: false,
9846 surround: false,
9847 newline: true,
9848 },
9849 BracketPair {
9850 start: "\"".to_string(),
9851 end: "\"".to_string(),
9852 close: true,
9853 surround: true,
9854 newline: false,
9855 },
9856 BracketPair {
9857 start: "<".to_string(),
9858 end: ">".to_string(),
9859 close: false,
9860 surround: true,
9861 newline: true,
9862 },
9863 ],
9864 ..Default::default()
9865 },
9866 autoclose_before: "})]".to_string(),
9867 ..Default::default()
9868 },
9869 Some(tree_sitter_rust::LANGUAGE.into()),
9870 );
9871 let language = Arc::new(language);
9872
9873 cx.language_registry().add(language.clone());
9874 cx.update_buffer(|buffer, cx| {
9875 buffer.set_language(Some(language), cx);
9876 });
9877
9878 // Ensure that signature_help is not called when no signature help is enabled.
9879 cx.set_state(
9880 &r#"
9881 fn main() {
9882 sampleˇ
9883 }
9884 "#
9885 .unindent(),
9886 );
9887 cx.update_editor(|editor, window, cx| {
9888 editor.handle_input("(", window, cx);
9889 });
9890 cx.assert_editor_state(
9891 &"
9892 fn main() {
9893 sample(ˇ)
9894 }
9895 "
9896 .unindent(),
9897 );
9898 cx.editor(|editor, _, _| {
9899 assert!(editor.signature_help_state.task().is_none());
9900 });
9901
9902 let mocked_response = lsp::SignatureHelp {
9903 signatures: vec![lsp::SignatureInformation {
9904 label: "fn sample(param1: u8, param2: u8)".to_string(),
9905 documentation: None,
9906 parameters: Some(vec![
9907 lsp::ParameterInformation {
9908 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9909 documentation: None,
9910 },
9911 lsp::ParameterInformation {
9912 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9913 documentation: None,
9914 },
9915 ]),
9916 active_parameter: None,
9917 }],
9918 active_signature: Some(0),
9919 active_parameter: Some(0),
9920 };
9921
9922 // Ensure that signature_help is called when enabled afte edits
9923 cx.update(|_, cx| {
9924 cx.update_global::<SettingsStore, _>(|settings, cx| {
9925 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9926 settings.auto_signature_help = Some(false);
9927 settings.show_signature_help_after_edits = Some(true);
9928 });
9929 });
9930 });
9931 cx.set_state(
9932 &r#"
9933 fn main() {
9934 sampleˇ
9935 }
9936 "#
9937 .unindent(),
9938 );
9939 cx.update_editor(|editor, window, cx| {
9940 editor.handle_input("(", window, cx);
9941 });
9942 cx.assert_editor_state(
9943 &"
9944 fn main() {
9945 sample(ˇ)
9946 }
9947 "
9948 .unindent(),
9949 );
9950 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9951 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9952 .await;
9953 cx.update_editor(|editor, _, _| {
9954 let signature_help_state = editor.signature_help_state.popover().cloned();
9955 assert!(signature_help_state.is_some());
9956 assert_eq!(
9957 signature_help_state.unwrap().label,
9958 "param1: u8, param2: u8"
9959 );
9960 editor.signature_help_state = SignatureHelpState::default();
9961 });
9962
9963 // Ensure that signature_help is called when auto signature help override is enabled
9964 cx.update(|_, cx| {
9965 cx.update_global::<SettingsStore, _>(|settings, cx| {
9966 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9967 settings.auto_signature_help = Some(true);
9968 settings.show_signature_help_after_edits = Some(false);
9969 });
9970 });
9971 });
9972 cx.set_state(
9973 &r#"
9974 fn main() {
9975 sampleˇ
9976 }
9977 "#
9978 .unindent(),
9979 );
9980 cx.update_editor(|editor, window, cx| {
9981 editor.handle_input("(", window, cx);
9982 });
9983 cx.assert_editor_state(
9984 &"
9985 fn main() {
9986 sample(ˇ)
9987 }
9988 "
9989 .unindent(),
9990 );
9991 handle_signature_help_request(&mut cx, mocked_response).await;
9992 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9993 .await;
9994 cx.editor(|editor, _, _| {
9995 let signature_help_state = editor.signature_help_state.popover().cloned();
9996 assert!(signature_help_state.is_some());
9997 assert_eq!(
9998 signature_help_state.unwrap().label,
9999 "param1: u8, param2: u8"
10000 );
10001 });
10002}
10003
10004#[gpui::test]
10005async fn test_signature_help(cx: &mut TestAppContext) {
10006 init_test(cx, |_| {});
10007 cx.update(|cx| {
10008 cx.update_global::<SettingsStore, _>(|settings, cx| {
10009 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10010 settings.auto_signature_help = Some(true);
10011 });
10012 });
10013 });
10014
10015 let mut cx = EditorLspTestContext::new_rust(
10016 lsp::ServerCapabilities {
10017 signature_help_provider: Some(lsp::SignatureHelpOptions {
10018 ..Default::default()
10019 }),
10020 ..Default::default()
10021 },
10022 cx,
10023 )
10024 .await;
10025
10026 // A test that directly calls `show_signature_help`
10027 cx.update_editor(|editor, window, cx| {
10028 editor.show_signature_help(&ShowSignatureHelp, window, cx);
10029 });
10030
10031 let mocked_response = lsp::SignatureHelp {
10032 signatures: vec![lsp::SignatureInformation {
10033 label: "fn sample(param1: u8, param2: u8)".to_string(),
10034 documentation: None,
10035 parameters: Some(vec![
10036 lsp::ParameterInformation {
10037 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10038 documentation: None,
10039 },
10040 lsp::ParameterInformation {
10041 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10042 documentation: None,
10043 },
10044 ]),
10045 active_parameter: None,
10046 }],
10047 active_signature: Some(0),
10048 active_parameter: Some(0),
10049 };
10050 handle_signature_help_request(&mut cx, mocked_response).await;
10051
10052 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10053 .await;
10054
10055 cx.editor(|editor, _, _| {
10056 let signature_help_state = editor.signature_help_state.popover().cloned();
10057 assert!(signature_help_state.is_some());
10058 assert_eq!(
10059 signature_help_state.unwrap().label,
10060 "param1: u8, param2: u8"
10061 );
10062 });
10063
10064 // When exiting outside from inside the brackets, `signature_help` is closed.
10065 cx.set_state(indoc! {"
10066 fn main() {
10067 sample(ˇ);
10068 }
10069
10070 fn sample(param1: u8, param2: u8) {}
10071 "});
10072
10073 cx.update_editor(|editor, window, cx| {
10074 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
10075 });
10076
10077 let mocked_response = lsp::SignatureHelp {
10078 signatures: Vec::new(),
10079 active_signature: None,
10080 active_parameter: None,
10081 };
10082 handle_signature_help_request(&mut cx, mocked_response).await;
10083
10084 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
10085 .await;
10086
10087 cx.editor(|editor, _, _| {
10088 assert!(!editor.signature_help_state.is_shown());
10089 });
10090
10091 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
10092 cx.set_state(indoc! {"
10093 fn main() {
10094 sample(ˇ);
10095 }
10096
10097 fn sample(param1: u8, param2: u8) {}
10098 "});
10099
10100 let mocked_response = lsp::SignatureHelp {
10101 signatures: vec![lsp::SignatureInformation {
10102 label: "fn sample(param1: u8, param2: u8)".to_string(),
10103 documentation: None,
10104 parameters: Some(vec![
10105 lsp::ParameterInformation {
10106 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10107 documentation: None,
10108 },
10109 lsp::ParameterInformation {
10110 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10111 documentation: None,
10112 },
10113 ]),
10114 active_parameter: None,
10115 }],
10116 active_signature: Some(0),
10117 active_parameter: Some(0),
10118 };
10119 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10120 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10121 .await;
10122 cx.editor(|editor, _, _| {
10123 assert!(editor.signature_help_state.is_shown());
10124 });
10125
10126 // Restore the popover with more parameter input
10127 cx.set_state(indoc! {"
10128 fn main() {
10129 sample(param1, param2ˇ);
10130 }
10131
10132 fn sample(param1: u8, param2: u8) {}
10133 "});
10134
10135 let mocked_response = lsp::SignatureHelp {
10136 signatures: vec![lsp::SignatureInformation {
10137 label: "fn sample(param1: u8, param2: u8)".to_string(),
10138 documentation: None,
10139 parameters: Some(vec![
10140 lsp::ParameterInformation {
10141 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10142 documentation: None,
10143 },
10144 lsp::ParameterInformation {
10145 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10146 documentation: None,
10147 },
10148 ]),
10149 active_parameter: None,
10150 }],
10151 active_signature: Some(0),
10152 active_parameter: Some(1),
10153 };
10154 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10155 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10156 .await;
10157
10158 // When selecting a range, the popover is gone.
10159 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
10160 cx.update_editor(|editor, window, cx| {
10161 editor.change_selections(None, window, cx, |s| {
10162 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
10163 })
10164 });
10165 cx.assert_editor_state(indoc! {"
10166 fn main() {
10167 sample(param1, «ˇparam2»);
10168 }
10169
10170 fn sample(param1: u8, param2: u8) {}
10171 "});
10172 cx.editor(|editor, _, _| {
10173 assert!(!editor.signature_help_state.is_shown());
10174 });
10175
10176 // When unselecting again, the popover is back if within the brackets.
10177 cx.update_editor(|editor, window, cx| {
10178 editor.change_selections(None, window, cx, |s| {
10179 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
10180 })
10181 });
10182 cx.assert_editor_state(indoc! {"
10183 fn main() {
10184 sample(param1, ˇparam2);
10185 }
10186
10187 fn sample(param1: u8, param2: u8) {}
10188 "});
10189 handle_signature_help_request(&mut cx, mocked_response).await;
10190 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10191 .await;
10192 cx.editor(|editor, _, _| {
10193 assert!(editor.signature_help_state.is_shown());
10194 });
10195
10196 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
10197 cx.update_editor(|editor, window, cx| {
10198 editor.change_selections(None, window, cx, |s| {
10199 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
10200 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
10201 })
10202 });
10203 cx.assert_editor_state(indoc! {"
10204 fn main() {
10205 sample(param1, ˇparam2);
10206 }
10207
10208 fn sample(param1: u8, param2: u8) {}
10209 "});
10210
10211 let mocked_response = lsp::SignatureHelp {
10212 signatures: vec![lsp::SignatureInformation {
10213 label: "fn sample(param1: u8, param2: u8)".to_string(),
10214 documentation: None,
10215 parameters: Some(vec![
10216 lsp::ParameterInformation {
10217 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10218 documentation: None,
10219 },
10220 lsp::ParameterInformation {
10221 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10222 documentation: None,
10223 },
10224 ]),
10225 active_parameter: None,
10226 }],
10227 active_signature: Some(0),
10228 active_parameter: Some(1),
10229 };
10230 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10231 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10232 .await;
10233 cx.update_editor(|editor, _, cx| {
10234 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
10235 });
10236 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
10237 .await;
10238 cx.update_editor(|editor, window, cx| {
10239 editor.change_selections(None, window, cx, |s| {
10240 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
10241 })
10242 });
10243 cx.assert_editor_state(indoc! {"
10244 fn main() {
10245 sample(param1, «ˇparam2»);
10246 }
10247
10248 fn sample(param1: u8, param2: u8) {}
10249 "});
10250 cx.update_editor(|editor, window, cx| {
10251 editor.change_selections(None, window, cx, |s| {
10252 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
10253 })
10254 });
10255 cx.assert_editor_state(indoc! {"
10256 fn main() {
10257 sample(param1, ˇparam2);
10258 }
10259
10260 fn sample(param1: u8, param2: u8) {}
10261 "});
10262 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
10263 .await;
10264}
10265
10266#[gpui::test]
10267async fn test_completion_mode(cx: &mut TestAppContext) {
10268 init_test(cx, |_| {});
10269 let mut cx = EditorLspTestContext::new_rust(
10270 lsp::ServerCapabilities {
10271 completion_provider: Some(lsp::CompletionOptions {
10272 resolve_provider: Some(true),
10273 ..Default::default()
10274 }),
10275 ..Default::default()
10276 },
10277 cx,
10278 )
10279 .await;
10280
10281 struct Run {
10282 run_description: &'static str,
10283 initial_state: String,
10284 buffer_marked_text: String,
10285 completion_text: &'static str,
10286 expected_with_insert_mode: String,
10287 expected_with_replace_mode: String,
10288 expected_with_replace_subsequence_mode: String,
10289 expected_with_replace_suffix_mode: String,
10290 }
10291
10292 let runs = [
10293 Run {
10294 run_description: "Start of word matches completion text",
10295 initial_state: "before ediˇ after".into(),
10296 buffer_marked_text: "before <edi|> after".into(),
10297 completion_text: "editor",
10298 expected_with_insert_mode: "before editorˇ after".into(),
10299 expected_with_replace_mode: "before editorˇ after".into(),
10300 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10301 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10302 },
10303 Run {
10304 run_description: "Accept same text at the middle of the word",
10305 initial_state: "before ediˇtor after".into(),
10306 buffer_marked_text: "before <edi|tor> after".into(),
10307 completion_text: "editor",
10308 expected_with_insert_mode: "before editorˇtor after".into(),
10309 expected_with_replace_mode: "before editorˇ after".into(),
10310 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10311 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10312 },
10313 Run {
10314 run_description: "End of word matches completion text -- cursor at end",
10315 initial_state: "before torˇ after".into(),
10316 buffer_marked_text: "before <tor|> after".into(),
10317 completion_text: "editor",
10318 expected_with_insert_mode: "before editorˇ after".into(),
10319 expected_with_replace_mode: "before editorˇ after".into(),
10320 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10321 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10322 },
10323 Run {
10324 run_description: "End of word matches completion text -- cursor at start",
10325 initial_state: "before ˇtor after".into(),
10326 buffer_marked_text: "before <|tor> after".into(),
10327 completion_text: "editor",
10328 expected_with_insert_mode: "before editorˇtor after".into(),
10329 expected_with_replace_mode: "before editorˇ after".into(),
10330 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10331 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10332 },
10333 Run {
10334 run_description: "Prepend text containing whitespace",
10335 initial_state: "pˇfield: bool".into(),
10336 buffer_marked_text: "<p|field>: bool".into(),
10337 completion_text: "pub ",
10338 expected_with_insert_mode: "pub ˇfield: bool".into(),
10339 expected_with_replace_mode: "pub ˇ: bool".into(),
10340 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
10341 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
10342 },
10343 Run {
10344 run_description: "Add element to start of list",
10345 initial_state: "[element_ˇelement_2]".into(),
10346 buffer_marked_text: "[<element_|element_2>]".into(),
10347 completion_text: "element_1",
10348 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
10349 expected_with_replace_mode: "[element_1ˇ]".into(),
10350 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
10351 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
10352 },
10353 Run {
10354 run_description: "Add element to start of list -- first and second elements are equal",
10355 initial_state: "[elˇelement]".into(),
10356 buffer_marked_text: "[<el|element>]".into(),
10357 completion_text: "element",
10358 expected_with_insert_mode: "[elementˇelement]".into(),
10359 expected_with_replace_mode: "[elementˇ]".into(),
10360 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
10361 expected_with_replace_suffix_mode: "[elementˇ]".into(),
10362 },
10363 Run {
10364 run_description: "Ends with matching suffix",
10365 initial_state: "SubˇError".into(),
10366 buffer_marked_text: "<Sub|Error>".into(),
10367 completion_text: "SubscriptionError",
10368 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
10369 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10370 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10371 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
10372 },
10373 Run {
10374 run_description: "Suffix is a subsequence -- contiguous",
10375 initial_state: "SubˇErr".into(),
10376 buffer_marked_text: "<Sub|Err>".into(),
10377 completion_text: "SubscriptionError",
10378 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
10379 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10380 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10381 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
10382 },
10383 Run {
10384 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
10385 initial_state: "Suˇscrirr".into(),
10386 buffer_marked_text: "<Su|scrirr>".into(),
10387 completion_text: "SubscriptionError",
10388 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
10389 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10390 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10391 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
10392 },
10393 Run {
10394 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
10395 initial_state: "foo(indˇix)".into(),
10396 buffer_marked_text: "foo(<ind|ix>)".into(),
10397 completion_text: "node_index",
10398 expected_with_insert_mode: "foo(node_indexˇix)".into(),
10399 expected_with_replace_mode: "foo(node_indexˇ)".into(),
10400 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
10401 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
10402 },
10403 ];
10404
10405 for run in runs {
10406 let run_variations = [
10407 (LspInsertMode::Insert, run.expected_with_insert_mode),
10408 (LspInsertMode::Replace, run.expected_with_replace_mode),
10409 (
10410 LspInsertMode::ReplaceSubsequence,
10411 run.expected_with_replace_subsequence_mode,
10412 ),
10413 (
10414 LspInsertMode::ReplaceSuffix,
10415 run.expected_with_replace_suffix_mode,
10416 ),
10417 ];
10418
10419 for (lsp_insert_mode, expected_text) in run_variations {
10420 eprintln!(
10421 "run = {:?}, mode = {lsp_insert_mode:.?}",
10422 run.run_description,
10423 );
10424
10425 update_test_language_settings(&mut cx, |settings| {
10426 settings.defaults.completions = Some(CompletionSettings {
10427 lsp_insert_mode,
10428 words: WordsCompletionMode::Disabled,
10429 lsp: true,
10430 lsp_fetch_timeout_ms: 0,
10431 });
10432 });
10433
10434 cx.set_state(&run.initial_state);
10435 cx.update_editor(|editor, window, cx| {
10436 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10437 });
10438
10439 let counter = Arc::new(AtomicUsize::new(0));
10440 handle_completion_request_with_insert_and_replace(
10441 &mut cx,
10442 &run.buffer_marked_text,
10443 vec![run.completion_text],
10444 counter.clone(),
10445 )
10446 .await;
10447 cx.condition(|editor, _| editor.context_menu_visible())
10448 .await;
10449 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
10450
10451 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10452 editor
10453 .confirm_completion(&ConfirmCompletion::default(), window, cx)
10454 .unwrap()
10455 });
10456 cx.assert_editor_state(&expected_text);
10457 handle_resolve_completion_request(&mut cx, None).await;
10458 apply_additional_edits.await.unwrap();
10459 }
10460 }
10461}
10462
10463#[gpui::test]
10464async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
10465 init_test(cx, |_| {});
10466 let mut cx = EditorLspTestContext::new_rust(
10467 lsp::ServerCapabilities {
10468 completion_provider: Some(lsp::CompletionOptions {
10469 resolve_provider: Some(true),
10470 ..Default::default()
10471 }),
10472 ..Default::default()
10473 },
10474 cx,
10475 )
10476 .await;
10477
10478 let initial_state = "SubˇError";
10479 let buffer_marked_text = "<Sub|Error>";
10480 let completion_text = "SubscriptionError";
10481 let expected_with_insert_mode = "SubscriptionErrorˇError";
10482 let expected_with_replace_mode = "SubscriptionErrorˇ";
10483
10484 update_test_language_settings(&mut cx, |settings| {
10485 settings.defaults.completions = Some(CompletionSettings {
10486 words: WordsCompletionMode::Disabled,
10487 // set the opposite here to ensure that the action is overriding the default behavior
10488 lsp_insert_mode: LspInsertMode::Insert,
10489 lsp: true,
10490 lsp_fetch_timeout_ms: 0,
10491 });
10492 });
10493
10494 cx.set_state(initial_state);
10495 cx.update_editor(|editor, window, cx| {
10496 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10497 });
10498
10499 let counter = Arc::new(AtomicUsize::new(0));
10500 handle_completion_request_with_insert_and_replace(
10501 &mut cx,
10502 &buffer_marked_text,
10503 vec![completion_text],
10504 counter.clone(),
10505 )
10506 .await;
10507 cx.condition(|editor, _| editor.context_menu_visible())
10508 .await;
10509 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
10510
10511 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10512 editor
10513 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10514 .unwrap()
10515 });
10516 cx.assert_editor_state(&expected_with_replace_mode);
10517 handle_resolve_completion_request(&mut cx, None).await;
10518 apply_additional_edits.await.unwrap();
10519
10520 update_test_language_settings(&mut cx, |settings| {
10521 settings.defaults.completions = Some(CompletionSettings {
10522 words: WordsCompletionMode::Disabled,
10523 // set the opposite here to ensure that the action is overriding the default behavior
10524 lsp_insert_mode: LspInsertMode::Replace,
10525 lsp: true,
10526 lsp_fetch_timeout_ms: 0,
10527 });
10528 });
10529
10530 cx.set_state(initial_state);
10531 cx.update_editor(|editor, window, cx| {
10532 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10533 });
10534 handle_completion_request_with_insert_and_replace(
10535 &mut cx,
10536 &buffer_marked_text,
10537 vec![completion_text],
10538 counter.clone(),
10539 )
10540 .await;
10541 cx.condition(|editor, _| editor.context_menu_visible())
10542 .await;
10543 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
10544
10545 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10546 editor
10547 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
10548 .unwrap()
10549 });
10550 cx.assert_editor_state(&expected_with_insert_mode);
10551 handle_resolve_completion_request(&mut cx, None).await;
10552 apply_additional_edits.await.unwrap();
10553}
10554
10555#[gpui::test]
10556async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
10557 init_test(cx, |_| {});
10558 let mut cx = EditorLspTestContext::new_rust(
10559 lsp::ServerCapabilities {
10560 completion_provider: Some(lsp::CompletionOptions {
10561 resolve_provider: Some(true),
10562 ..Default::default()
10563 }),
10564 ..Default::default()
10565 },
10566 cx,
10567 )
10568 .await;
10569
10570 // scenario: surrounding text matches completion text
10571 let completion_text = "to_offset";
10572 let initial_state = indoc! {"
10573 1. buf.to_offˇsuffix
10574 2. buf.to_offˇsuf
10575 3. buf.to_offˇfix
10576 4. buf.to_offˇ
10577 5. into_offˇensive
10578 6. ˇsuffix
10579 7. let ˇ //
10580 8. aaˇzz
10581 9. buf.to_off«zzzzzˇ»suffix
10582 10. buf.«ˇzzzzz»suffix
10583 11. to_off«ˇzzzzz»
10584
10585 buf.to_offˇsuffix // newest cursor
10586 "};
10587 let completion_marked_buffer = indoc! {"
10588 1. buf.to_offsuffix
10589 2. buf.to_offsuf
10590 3. buf.to_offfix
10591 4. buf.to_off
10592 5. into_offensive
10593 6. suffix
10594 7. let //
10595 8. aazz
10596 9. buf.to_offzzzzzsuffix
10597 10. buf.zzzzzsuffix
10598 11. to_offzzzzz
10599
10600 buf.<to_off|suffix> // newest cursor
10601 "};
10602 let expected = indoc! {"
10603 1. buf.to_offsetˇ
10604 2. buf.to_offsetˇsuf
10605 3. buf.to_offsetˇfix
10606 4. buf.to_offsetˇ
10607 5. into_offsetˇensive
10608 6. to_offsetˇsuffix
10609 7. let to_offsetˇ //
10610 8. aato_offsetˇzz
10611 9. buf.to_offsetˇ
10612 10. buf.to_offsetˇsuffix
10613 11. to_offsetˇ
10614
10615 buf.to_offsetˇ // newest cursor
10616 "};
10617 cx.set_state(initial_state);
10618 cx.update_editor(|editor, window, cx| {
10619 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10620 });
10621 handle_completion_request_with_insert_and_replace(
10622 &mut cx,
10623 completion_marked_buffer,
10624 vec![completion_text],
10625 Arc::new(AtomicUsize::new(0)),
10626 )
10627 .await;
10628 cx.condition(|editor, _| editor.context_menu_visible())
10629 .await;
10630 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10631 editor
10632 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10633 .unwrap()
10634 });
10635 cx.assert_editor_state(expected);
10636 handle_resolve_completion_request(&mut cx, None).await;
10637 apply_additional_edits.await.unwrap();
10638
10639 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
10640 let completion_text = "foo_and_bar";
10641 let initial_state = indoc! {"
10642 1. ooanbˇ
10643 2. zooanbˇ
10644 3. ooanbˇz
10645 4. zooanbˇz
10646 5. ooanˇ
10647 6. oanbˇ
10648
10649 ooanbˇ
10650 "};
10651 let completion_marked_buffer = indoc! {"
10652 1. ooanb
10653 2. zooanb
10654 3. ooanbz
10655 4. zooanbz
10656 5. ooan
10657 6. oanb
10658
10659 <ooanb|>
10660 "};
10661 let expected = indoc! {"
10662 1. foo_and_barˇ
10663 2. zfoo_and_barˇ
10664 3. foo_and_barˇz
10665 4. zfoo_and_barˇz
10666 5. ooanfoo_and_barˇ
10667 6. oanbfoo_and_barˇ
10668
10669 foo_and_barˇ
10670 "};
10671 cx.set_state(initial_state);
10672 cx.update_editor(|editor, window, cx| {
10673 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10674 });
10675 handle_completion_request_with_insert_and_replace(
10676 &mut cx,
10677 completion_marked_buffer,
10678 vec![completion_text],
10679 Arc::new(AtomicUsize::new(0)),
10680 )
10681 .await;
10682 cx.condition(|editor, _| editor.context_menu_visible())
10683 .await;
10684 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10685 editor
10686 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10687 .unwrap()
10688 });
10689 cx.assert_editor_state(expected);
10690 handle_resolve_completion_request(&mut cx, None).await;
10691 apply_additional_edits.await.unwrap();
10692
10693 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
10694 // (expects the same as if it was inserted at the end)
10695 let completion_text = "foo_and_bar";
10696 let initial_state = indoc! {"
10697 1. ooˇanb
10698 2. zooˇanb
10699 3. ooˇanbz
10700 4. zooˇanbz
10701
10702 ooˇanb
10703 "};
10704 let completion_marked_buffer = indoc! {"
10705 1. ooanb
10706 2. zooanb
10707 3. ooanbz
10708 4. zooanbz
10709
10710 <oo|anb>
10711 "};
10712 let expected = indoc! {"
10713 1. foo_and_barˇ
10714 2. zfoo_and_barˇ
10715 3. foo_and_barˇz
10716 4. zfoo_and_barˇz
10717
10718 foo_and_barˇ
10719 "};
10720 cx.set_state(initial_state);
10721 cx.update_editor(|editor, window, cx| {
10722 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10723 });
10724 handle_completion_request_with_insert_and_replace(
10725 &mut cx,
10726 completion_marked_buffer,
10727 vec![completion_text],
10728 Arc::new(AtomicUsize::new(0)),
10729 )
10730 .await;
10731 cx.condition(|editor, _| editor.context_menu_visible())
10732 .await;
10733 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10734 editor
10735 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10736 .unwrap()
10737 });
10738 cx.assert_editor_state(expected);
10739 handle_resolve_completion_request(&mut cx, None).await;
10740 apply_additional_edits.await.unwrap();
10741}
10742
10743// This used to crash
10744#[gpui::test]
10745async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
10746 init_test(cx, |_| {});
10747
10748 let buffer_text = indoc! {"
10749 fn main() {
10750 10.satu;
10751
10752 //
10753 // separate cursors so they open in different excerpts (manually reproducible)
10754 //
10755
10756 10.satu20;
10757 }
10758 "};
10759 let multibuffer_text_with_selections = indoc! {"
10760 fn main() {
10761 10.satuˇ;
10762
10763 //
10764
10765 //
10766
10767 10.satuˇ20;
10768 }
10769 "};
10770 let expected_multibuffer = indoc! {"
10771 fn main() {
10772 10.saturating_sub()ˇ;
10773
10774 //
10775
10776 //
10777
10778 10.saturating_sub()ˇ;
10779 }
10780 "};
10781
10782 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
10783 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
10784
10785 let fs = FakeFs::new(cx.executor());
10786 fs.insert_tree(
10787 path!("/a"),
10788 json!({
10789 "main.rs": buffer_text,
10790 }),
10791 )
10792 .await;
10793
10794 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
10795 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10796 language_registry.add(rust_lang());
10797 let mut fake_servers = language_registry.register_fake_lsp(
10798 "Rust",
10799 FakeLspAdapter {
10800 capabilities: lsp::ServerCapabilities {
10801 completion_provider: Some(lsp::CompletionOptions {
10802 resolve_provider: None,
10803 ..lsp::CompletionOptions::default()
10804 }),
10805 ..lsp::ServerCapabilities::default()
10806 },
10807 ..FakeLspAdapter::default()
10808 },
10809 );
10810 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
10811 let cx = &mut VisualTestContext::from_window(*workspace, cx);
10812 let buffer = project
10813 .update(cx, |project, cx| {
10814 project.open_local_buffer(path!("/a/main.rs"), cx)
10815 })
10816 .await
10817 .unwrap();
10818
10819 let multi_buffer = cx.new(|cx| {
10820 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
10821 multi_buffer.push_excerpts(
10822 buffer.clone(),
10823 [ExcerptRange::new(0..first_excerpt_end)],
10824 cx,
10825 );
10826 multi_buffer.push_excerpts(
10827 buffer.clone(),
10828 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
10829 cx,
10830 );
10831 multi_buffer
10832 });
10833
10834 let editor = workspace
10835 .update(cx, |_, window, cx| {
10836 cx.new(|cx| {
10837 Editor::new(
10838 EditorMode::Full {
10839 scale_ui_elements_with_buffer_font_size: false,
10840 show_active_line_background: false,
10841 sized_by_content: false,
10842 },
10843 multi_buffer.clone(),
10844 Some(project.clone()),
10845 window,
10846 cx,
10847 )
10848 })
10849 })
10850 .unwrap();
10851
10852 let pane = workspace
10853 .update(cx, |workspace, _, _| workspace.active_pane().clone())
10854 .unwrap();
10855 pane.update_in(cx, |pane, window, cx| {
10856 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
10857 });
10858
10859 let fake_server = fake_servers.next().await.unwrap();
10860
10861 editor.update_in(cx, |editor, window, cx| {
10862 editor.change_selections(None, window, cx, |s| {
10863 s.select_ranges([
10864 Point::new(1, 11)..Point::new(1, 11),
10865 Point::new(7, 11)..Point::new(7, 11),
10866 ])
10867 });
10868
10869 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
10870 });
10871
10872 editor.update_in(cx, |editor, window, cx| {
10873 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10874 });
10875
10876 fake_server
10877 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
10878 let completion_item = lsp::CompletionItem {
10879 label: "saturating_sub()".into(),
10880 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
10881 lsp::InsertReplaceEdit {
10882 new_text: "saturating_sub()".to_owned(),
10883 insert: lsp::Range::new(
10884 lsp::Position::new(7, 7),
10885 lsp::Position::new(7, 11),
10886 ),
10887 replace: lsp::Range::new(
10888 lsp::Position::new(7, 7),
10889 lsp::Position::new(7, 13),
10890 ),
10891 },
10892 )),
10893 ..lsp::CompletionItem::default()
10894 };
10895
10896 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
10897 })
10898 .next()
10899 .await
10900 .unwrap();
10901
10902 cx.condition(&editor, |editor, _| editor.context_menu_visible())
10903 .await;
10904
10905 editor
10906 .update_in(cx, |editor, window, cx| {
10907 editor
10908 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10909 .unwrap()
10910 })
10911 .await
10912 .unwrap();
10913
10914 editor.update(cx, |editor, cx| {
10915 assert_text_with_selections(editor, expected_multibuffer, cx);
10916 })
10917}
10918
10919#[gpui::test]
10920async fn test_completion(cx: &mut TestAppContext) {
10921 init_test(cx, |_| {});
10922
10923 let mut cx = EditorLspTestContext::new_rust(
10924 lsp::ServerCapabilities {
10925 completion_provider: Some(lsp::CompletionOptions {
10926 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
10927 resolve_provider: Some(true),
10928 ..Default::default()
10929 }),
10930 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
10931 ..Default::default()
10932 },
10933 cx,
10934 )
10935 .await;
10936 let counter = Arc::new(AtomicUsize::new(0));
10937
10938 cx.set_state(indoc! {"
10939 oneˇ
10940 two
10941 three
10942 "});
10943 cx.simulate_keystroke(".");
10944 handle_completion_request(
10945 &mut cx,
10946 indoc! {"
10947 one.|<>
10948 two
10949 three
10950 "},
10951 vec!["first_completion", "second_completion"],
10952 counter.clone(),
10953 )
10954 .await;
10955 cx.condition(|editor, _| editor.context_menu_visible())
10956 .await;
10957 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
10958
10959 let _handler = handle_signature_help_request(
10960 &mut cx,
10961 lsp::SignatureHelp {
10962 signatures: vec![lsp::SignatureInformation {
10963 label: "test signature".to_string(),
10964 documentation: None,
10965 parameters: Some(vec![lsp::ParameterInformation {
10966 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
10967 documentation: None,
10968 }]),
10969 active_parameter: None,
10970 }],
10971 active_signature: None,
10972 active_parameter: None,
10973 },
10974 );
10975 cx.update_editor(|editor, window, cx| {
10976 assert!(
10977 !editor.signature_help_state.is_shown(),
10978 "No signature help was called for"
10979 );
10980 editor.show_signature_help(&ShowSignatureHelp, window, cx);
10981 });
10982 cx.run_until_parked();
10983 cx.update_editor(|editor, _, _| {
10984 assert!(
10985 !editor.signature_help_state.is_shown(),
10986 "No signature help should be shown when completions menu is open"
10987 );
10988 });
10989
10990 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10991 editor.context_menu_next(&Default::default(), window, cx);
10992 editor
10993 .confirm_completion(&ConfirmCompletion::default(), window, cx)
10994 .unwrap()
10995 });
10996 cx.assert_editor_state(indoc! {"
10997 one.second_completionˇ
10998 two
10999 three
11000 "});
11001
11002 handle_resolve_completion_request(
11003 &mut cx,
11004 Some(vec![
11005 (
11006 //This overlaps with the primary completion edit which is
11007 //misbehavior from the LSP spec, test that we filter it out
11008 indoc! {"
11009 one.second_ˇcompletion
11010 two
11011 threeˇ
11012 "},
11013 "overlapping additional edit",
11014 ),
11015 (
11016 indoc! {"
11017 one.second_completion
11018 two
11019 threeˇ
11020 "},
11021 "\nadditional edit",
11022 ),
11023 ]),
11024 )
11025 .await;
11026 apply_additional_edits.await.unwrap();
11027 cx.assert_editor_state(indoc! {"
11028 one.second_completionˇ
11029 two
11030 three
11031 additional edit
11032 "});
11033
11034 cx.set_state(indoc! {"
11035 one.second_completion
11036 twoˇ
11037 threeˇ
11038 additional edit
11039 "});
11040 cx.simulate_keystroke(" ");
11041 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11042 cx.simulate_keystroke("s");
11043 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11044
11045 cx.assert_editor_state(indoc! {"
11046 one.second_completion
11047 two sˇ
11048 three sˇ
11049 additional edit
11050 "});
11051 handle_completion_request(
11052 &mut cx,
11053 indoc! {"
11054 one.second_completion
11055 two s
11056 three <s|>
11057 additional edit
11058 "},
11059 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
11060 counter.clone(),
11061 )
11062 .await;
11063 cx.condition(|editor, _| editor.context_menu_visible())
11064 .await;
11065 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
11066
11067 cx.simulate_keystroke("i");
11068
11069 handle_completion_request(
11070 &mut cx,
11071 indoc! {"
11072 one.second_completion
11073 two si
11074 three <si|>
11075 additional edit
11076 "},
11077 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
11078 counter.clone(),
11079 )
11080 .await;
11081 cx.condition(|editor, _| editor.context_menu_visible())
11082 .await;
11083 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
11084
11085 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11086 editor
11087 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11088 .unwrap()
11089 });
11090 cx.assert_editor_state(indoc! {"
11091 one.second_completion
11092 two sixth_completionˇ
11093 three sixth_completionˇ
11094 additional edit
11095 "});
11096
11097 apply_additional_edits.await.unwrap();
11098
11099 update_test_language_settings(&mut cx, |settings| {
11100 settings.defaults.show_completions_on_input = Some(false);
11101 });
11102 cx.set_state("editorˇ");
11103 cx.simulate_keystroke(".");
11104 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11105 cx.simulate_keystrokes("c l o");
11106 cx.assert_editor_state("editor.cloˇ");
11107 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11108 cx.update_editor(|editor, window, cx| {
11109 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11110 });
11111 handle_completion_request(
11112 &mut cx,
11113 "editor.<clo|>",
11114 vec!["close", "clobber"],
11115 counter.clone(),
11116 )
11117 .await;
11118 cx.condition(|editor, _| editor.context_menu_visible())
11119 .await;
11120 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
11121
11122 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11123 editor
11124 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11125 .unwrap()
11126 });
11127 cx.assert_editor_state("editor.closeˇ");
11128 handle_resolve_completion_request(&mut cx, None).await;
11129 apply_additional_edits.await.unwrap();
11130}
11131
11132#[gpui::test]
11133async fn test_word_completion(cx: &mut TestAppContext) {
11134 let lsp_fetch_timeout_ms = 10;
11135 init_test(cx, |language_settings| {
11136 language_settings.defaults.completions = Some(CompletionSettings {
11137 words: WordsCompletionMode::Fallback,
11138 lsp: true,
11139 lsp_fetch_timeout_ms: 10,
11140 lsp_insert_mode: LspInsertMode::Insert,
11141 });
11142 });
11143
11144 let mut cx = EditorLspTestContext::new_rust(
11145 lsp::ServerCapabilities {
11146 completion_provider: Some(lsp::CompletionOptions {
11147 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11148 ..lsp::CompletionOptions::default()
11149 }),
11150 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11151 ..lsp::ServerCapabilities::default()
11152 },
11153 cx,
11154 )
11155 .await;
11156
11157 let throttle_completions = Arc::new(AtomicBool::new(false));
11158
11159 let lsp_throttle_completions = throttle_completions.clone();
11160 let _completion_requests_handler =
11161 cx.lsp
11162 .server
11163 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
11164 let lsp_throttle_completions = lsp_throttle_completions.clone();
11165 let cx = cx.clone();
11166 async move {
11167 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
11168 cx.background_executor()
11169 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
11170 .await;
11171 }
11172 Ok(Some(lsp::CompletionResponse::Array(vec![
11173 lsp::CompletionItem {
11174 label: "first".into(),
11175 ..lsp::CompletionItem::default()
11176 },
11177 lsp::CompletionItem {
11178 label: "last".into(),
11179 ..lsp::CompletionItem::default()
11180 },
11181 ])))
11182 }
11183 });
11184
11185 cx.set_state(indoc! {"
11186 oneˇ
11187 two
11188 three
11189 "});
11190 cx.simulate_keystroke(".");
11191 cx.executor().run_until_parked();
11192 cx.condition(|editor, _| editor.context_menu_visible())
11193 .await;
11194 cx.update_editor(|editor, window, cx| {
11195 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11196 {
11197 assert_eq!(
11198 completion_menu_entries(&menu),
11199 &["first", "last"],
11200 "When LSP server is fast to reply, no fallback word completions are used"
11201 );
11202 } else {
11203 panic!("expected completion menu to be open");
11204 }
11205 editor.cancel(&Cancel, window, cx);
11206 });
11207 cx.executor().run_until_parked();
11208 cx.condition(|editor, _| !editor.context_menu_visible())
11209 .await;
11210
11211 throttle_completions.store(true, atomic::Ordering::Release);
11212 cx.simulate_keystroke(".");
11213 cx.executor()
11214 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
11215 cx.executor().run_until_parked();
11216 cx.condition(|editor, _| editor.context_menu_visible())
11217 .await;
11218 cx.update_editor(|editor, _, _| {
11219 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11220 {
11221 assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
11222 "When LSP server is slow, document words can be shown instead, if configured accordingly");
11223 } else {
11224 panic!("expected completion menu to be open");
11225 }
11226 });
11227}
11228
11229#[gpui::test]
11230async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
11231 init_test(cx, |language_settings| {
11232 language_settings.defaults.completions = Some(CompletionSettings {
11233 words: WordsCompletionMode::Enabled,
11234 lsp: true,
11235 lsp_fetch_timeout_ms: 0,
11236 lsp_insert_mode: LspInsertMode::Insert,
11237 });
11238 });
11239
11240 let mut cx = EditorLspTestContext::new_rust(
11241 lsp::ServerCapabilities {
11242 completion_provider: Some(lsp::CompletionOptions {
11243 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11244 ..lsp::CompletionOptions::default()
11245 }),
11246 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11247 ..lsp::ServerCapabilities::default()
11248 },
11249 cx,
11250 )
11251 .await;
11252
11253 let _completion_requests_handler =
11254 cx.lsp
11255 .server
11256 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
11257 Ok(Some(lsp::CompletionResponse::Array(vec![
11258 lsp::CompletionItem {
11259 label: "first".into(),
11260 ..lsp::CompletionItem::default()
11261 },
11262 lsp::CompletionItem {
11263 label: "last".into(),
11264 ..lsp::CompletionItem::default()
11265 },
11266 ])))
11267 });
11268
11269 cx.set_state(indoc! {"ˇ
11270 first
11271 last
11272 second
11273 "});
11274 cx.simulate_keystroke(".");
11275 cx.executor().run_until_parked();
11276 cx.condition(|editor, _| editor.context_menu_visible())
11277 .await;
11278 cx.update_editor(|editor, _, _| {
11279 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11280 {
11281 assert_eq!(
11282 completion_menu_entries(&menu),
11283 &["first", "last", "second"],
11284 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
11285 );
11286 } else {
11287 panic!("expected completion menu to be open");
11288 }
11289 });
11290}
11291
11292#[gpui::test]
11293async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
11294 init_test(cx, |language_settings| {
11295 language_settings.defaults.completions = Some(CompletionSettings {
11296 words: WordsCompletionMode::Disabled,
11297 lsp: true,
11298 lsp_fetch_timeout_ms: 0,
11299 lsp_insert_mode: LspInsertMode::Insert,
11300 });
11301 });
11302
11303 let mut cx = EditorLspTestContext::new_rust(
11304 lsp::ServerCapabilities {
11305 completion_provider: Some(lsp::CompletionOptions {
11306 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11307 ..lsp::CompletionOptions::default()
11308 }),
11309 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11310 ..lsp::ServerCapabilities::default()
11311 },
11312 cx,
11313 )
11314 .await;
11315
11316 let _completion_requests_handler =
11317 cx.lsp
11318 .server
11319 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
11320 panic!("LSP completions should not be queried when dealing with word completions")
11321 });
11322
11323 cx.set_state(indoc! {"ˇ
11324 first
11325 last
11326 second
11327 "});
11328 cx.update_editor(|editor, window, cx| {
11329 editor.show_word_completions(&ShowWordCompletions, window, cx);
11330 });
11331 cx.executor().run_until_parked();
11332 cx.condition(|editor, _| editor.context_menu_visible())
11333 .await;
11334 cx.update_editor(|editor, _, _| {
11335 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11336 {
11337 assert_eq!(
11338 completion_menu_entries(&menu),
11339 &["first", "last", "second"],
11340 "`ShowWordCompletions` action should show word completions"
11341 );
11342 } else {
11343 panic!("expected completion menu to be open");
11344 }
11345 });
11346
11347 cx.simulate_keystroke("l");
11348 cx.executor().run_until_parked();
11349 cx.condition(|editor, _| editor.context_menu_visible())
11350 .await;
11351 cx.update_editor(|editor, _, _| {
11352 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11353 {
11354 assert_eq!(
11355 completion_menu_entries(&menu),
11356 &["last"],
11357 "After showing word completions, further editing should filter them and not query the LSP"
11358 );
11359 } else {
11360 panic!("expected completion menu to be open");
11361 }
11362 });
11363}
11364
11365#[gpui::test]
11366async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
11367 init_test(cx, |language_settings| {
11368 language_settings.defaults.completions = Some(CompletionSettings {
11369 words: WordsCompletionMode::Fallback,
11370 lsp: false,
11371 lsp_fetch_timeout_ms: 0,
11372 lsp_insert_mode: LspInsertMode::Insert,
11373 });
11374 });
11375
11376 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
11377
11378 cx.set_state(indoc! {"ˇ
11379 0_usize
11380 let
11381 33
11382 4.5f32
11383 "});
11384 cx.update_editor(|editor, window, cx| {
11385 editor.show_completions(&ShowCompletions::default(), window, cx);
11386 });
11387 cx.executor().run_until_parked();
11388 cx.condition(|editor, _| editor.context_menu_visible())
11389 .await;
11390 cx.update_editor(|editor, window, cx| {
11391 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11392 {
11393 assert_eq!(
11394 completion_menu_entries(&menu),
11395 &["let"],
11396 "With no digits in the completion query, no digits should be in the word completions"
11397 );
11398 } else {
11399 panic!("expected completion menu to be open");
11400 }
11401 editor.cancel(&Cancel, window, cx);
11402 });
11403
11404 cx.set_state(indoc! {"3ˇ
11405 0_usize
11406 let
11407 3
11408 33.35f32
11409 "});
11410 cx.update_editor(|editor, window, cx| {
11411 editor.show_completions(&ShowCompletions::default(), window, cx);
11412 });
11413 cx.executor().run_until_parked();
11414 cx.condition(|editor, _| editor.context_menu_visible())
11415 .await;
11416 cx.update_editor(|editor, _, _| {
11417 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11418 {
11419 assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
11420 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
11421 } else {
11422 panic!("expected completion menu to be open");
11423 }
11424 });
11425}
11426
11427fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
11428 let position = || lsp::Position {
11429 line: params.text_document_position.position.line,
11430 character: params.text_document_position.position.character,
11431 };
11432 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11433 range: lsp::Range {
11434 start: position(),
11435 end: position(),
11436 },
11437 new_text: text.to_string(),
11438 }))
11439}
11440
11441#[gpui::test]
11442async fn test_multiline_completion(cx: &mut TestAppContext) {
11443 init_test(cx, |_| {});
11444
11445 let fs = FakeFs::new(cx.executor());
11446 fs.insert_tree(
11447 path!("/a"),
11448 json!({
11449 "main.ts": "a",
11450 }),
11451 )
11452 .await;
11453
11454 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11455 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11456 let typescript_language = Arc::new(Language::new(
11457 LanguageConfig {
11458 name: "TypeScript".into(),
11459 matcher: LanguageMatcher {
11460 path_suffixes: vec!["ts".to_string()],
11461 ..LanguageMatcher::default()
11462 },
11463 line_comments: vec!["// ".into()],
11464 ..LanguageConfig::default()
11465 },
11466 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
11467 ));
11468 language_registry.add(typescript_language.clone());
11469 let mut fake_servers = language_registry.register_fake_lsp(
11470 "TypeScript",
11471 FakeLspAdapter {
11472 capabilities: lsp::ServerCapabilities {
11473 completion_provider: Some(lsp::CompletionOptions {
11474 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11475 ..lsp::CompletionOptions::default()
11476 }),
11477 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11478 ..lsp::ServerCapabilities::default()
11479 },
11480 // Emulate vtsls label generation
11481 label_for_completion: Some(Box::new(|item, _| {
11482 let text = if let Some(description) = item
11483 .label_details
11484 .as_ref()
11485 .and_then(|label_details| label_details.description.as_ref())
11486 {
11487 format!("{} {}", item.label, description)
11488 } else if let Some(detail) = &item.detail {
11489 format!("{} {}", item.label, detail)
11490 } else {
11491 item.label.clone()
11492 };
11493 let len = text.len();
11494 Some(language::CodeLabel {
11495 text,
11496 runs: Vec::new(),
11497 filter_range: 0..len,
11498 })
11499 })),
11500 ..FakeLspAdapter::default()
11501 },
11502 );
11503 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11504 let cx = &mut VisualTestContext::from_window(*workspace, cx);
11505 let worktree_id = workspace
11506 .update(cx, |workspace, _window, cx| {
11507 workspace.project().update(cx, |project, cx| {
11508 project.worktrees(cx).next().unwrap().read(cx).id()
11509 })
11510 })
11511 .unwrap();
11512 let _buffer = project
11513 .update(cx, |project, cx| {
11514 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
11515 })
11516 .await
11517 .unwrap();
11518 let editor = workspace
11519 .update(cx, |workspace, window, cx| {
11520 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
11521 })
11522 .unwrap()
11523 .await
11524 .unwrap()
11525 .downcast::<Editor>()
11526 .unwrap();
11527 let fake_server = fake_servers.next().await.unwrap();
11528
11529 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
11530 let multiline_label_2 = "a\nb\nc\n";
11531 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
11532 let multiline_description = "d\ne\nf\n";
11533 let multiline_detail_2 = "g\nh\ni\n";
11534
11535 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
11536 move |params, _| async move {
11537 Ok(Some(lsp::CompletionResponse::Array(vec![
11538 lsp::CompletionItem {
11539 label: multiline_label.to_string(),
11540 text_edit: gen_text_edit(¶ms, "new_text_1"),
11541 ..lsp::CompletionItem::default()
11542 },
11543 lsp::CompletionItem {
11544 label: "single line label 1".to_string(),
11545 detail: Some(multiline_detail.to_string()),
11546 text_edit: gen_text_edit(¶ms, "new_text_2"),
11547 ..lsp::CompletionItem::default()
11548 },
11549 lsp::CompletionItem {
11550 label: "single line label 2".to_string(),
11551 label_details: Some(lsp::CompletionItemLabelDetails {
11552 description: Some(multiline_description.to_string()),
11553 detail: None,
11554 }),
11555 text_edit: gen_text_edit(¶ms, "new_text_2"),
11556 ..lsp::CompletionItem::default()
11557 },
11558 lsp::CompletionItem {
11559 label: multiline_label_2.to_string(),
11560 detail: Some(multiline_detail_2.to_string()),
11561 text_edit: gen_text_edit(¶ms, "new_text_3"),
11562 ..lsp::CompletionItem::default()
11563 },
11564 lsp::CompletionItem {
11565 label: "Label with many spaces and \t but without newlines".to_string(),
11566 detail: Some(
11567 "Details with many spaces and \t but without newlines".to_string(),
11568 ),
11569 text_edit: gen_text_edit(¶ms, "new_text_4"),
11570 ..lsp::CompletionItem::default()
11571 },
11572 ])))
11573 },
11574 );
11575
11576 editor.update_in(cx, |editor, window, cx| {
11577 cx.focus_self(window);
11578 editor.move_to_end(&MoveToEnd, window, cx);
11579 editor.handle_input(".", window, cx);
11580 });
11581 cx.run_until_parked();
11582 completion_handle.next().await.unwrap();
11583
11584 editor.update(cx, |editor, _| {
11585 assert!(editor.context_menu_visible());
11586 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11587 {
11588 let completion_labels = menu
11589 .completions
11590 .borrow()
11591 .iter()
11592 .map(|c| c.label.text.clone())
11593 .collect::<Vec<_>>();
11594 assert_eq!(
11595 completion_labels,
11596 &[
11597 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
11598 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
11599 "single line label 2 d e f ",
11600 "a b c g h i ",
11601 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
11602 ],
11603 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
11604 );
11605
11606 for completion in menu
11607 .completions
11608 .borrow()
11609 .iter() {
11610 assert_eq!(
11611 completion.label.filter_range,
11612 0..completion.label.text.len(),
11613 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
11614 );
11615 }
11616 } else {
11617 panic!("expected completion menu to be open");
11618 }
11619 });
11620}
11621
11622#[gpui::test]
11623async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
11624 init_test(cx, |_| {});
11625 let mut cx = EditorLspTestContext::new_rust(
11626 lsp::ServerCapabilities {
11627 completion_provider: Some(lsp::CompletionOptions {
11628 trigger_characters: Some(vec![".".to_string()]),
11629 ..Default::default()
11630 }),
11631 ..Default::default()
11632 },
11633 cx,
11634 )
11635 .await;
11636 cx.lsp
11637 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
11638 Ok(Some(lsp::CompletionResponse::Array(vec![
11639 lsp::CompletionItem {
11640 label: "first".into(),
11641 ..Default::default()
11642 },
11643 lsp::CompletionItem {
11644 label: "last".into(),
11645 ..Default::default()
11646 },
11647 ])))
11648 });
11649 cx.set_state("variableˇ");
11650 cx.simulate_keystroke(".");
11651 cx.executor().run_until_parked();
11652
11653 cx.update_editor(|editor, _, _| {
11654 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11655 {
11656 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
11657 } else {
11658 panic!("expected completion menu to be open");
11659 }
11660 });
11661
11662 cx.update_editor(|editor, window, cx| {
11663 editor.move_page_down(&MovePageDown::default(), window, cx);
11664 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11665 {
11666 assert!(
11667 menu.selected_item == 1,
11668 "expected PageDown to select the last item from the context menu"
11669 );
11670 } else {
11671 panic!("expected completion menu to stay open after PageDown");
11672 }
11673 });
11674
11675 cx.update_editor(|editor, window, cx| {
11676 editor.move_page_up(&MovePageUp::default(), window, cx);
11677 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11678 {
11679 assert!(
11680 menu.selected_item == 0,
11681 "expected PageUp to select the first item from the context menu"
11682 );
11683 } else {
11684 panic!("expected completion menu to stay open after PageUp");
11685 }
11686 });
11687}
11688
11689#[gpui::test]
11690async fn test_as_is_completions(cx: &mut TestAppContext) {
11691 init_test(cx, |_| {});
11692 let mut cx = EditorLspTestContext::new_rust(
11693 lsp::ServerCapabilities {
11694 completion_provider: Some(lsp::CompletionOptions {
11695 ..Default::default()
11696 }),
11697 ..Default::default()
11698 },
11699 cx,
11700 )
11701 .await;
11702 cx.lsp
11703 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
11704 Ok(Some(lsp::CompletionResponse::Array(vec![
11705 lsp::CompletionItem {
11706 label: "unsafe".into(),
11707 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11708 range: lsp::Range {
11709 start: lsp::Position {
11710 line: 1,
11711 character: 2,
11712 },
11713 end: lsp::Position {
11714 line: 1,
11715 character: 3,
11716 },
11717 },
11718 new_text: "unsafe".to_string(),
11719 })),
11720 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
11721 ..Default::default()
11722 },
11723 ])))
11724 });
11725 cx.set_state("fn a() {}\n nˇ");
11726 cx.executor().run_until_parked();
11727 cx.update_editor(|editor, window, cx| {
11728 editor.show_completions(
11729 &ShowCompletions {
11730 trigger: Some("\n".into()),
11731 },
11732 window,
11733 cx,
11734 );
11735 });
11736 cx.executor().run_until_parked();
11737
11738 cx.update_editor(|editor, window, cx| {
11739 editor.confirm_completion(&Default::default(), window, cx)
11740 });
11741 cx.executor().run_until_parked();
11742 cx.assert_editor_state("fn a() {}\n unsafeˇ");
11743}
11744
11745#[gpui::test]
11746async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
11747 init_test(cx, |_| {});
11748
11749 let mut cx = EditorLspTestContext::new_rust(
11750 lsp::ServerCapabilities {
11751 completion_provider: Some(lsp::CompletionOptions {
11752 trigger_characters: Some(vec![".".to_string()]),
11753 resolve_provider: Some(true),
11754 ..Default::default()
11755 }),
11756 ..Default::default()
11757 },
11758 cx,
11759 )
11760 .await;
11761
11762 cx.set_state("fn main() { let a = 2ˇ; }");
11763 cx.simulate_keystroke(".");
11764 let completion_item = lsp::CompletionItem {
11765 label: "Some".into(),
11766 kind: Some(lsp::CompletionItemKind::SNIPPET),
11767 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
11768 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
11769 kind: lsp::MarkupKind::Markdown,
11770 value: "```rust\nSome(2)\n```".to_string(),
11771 })),
11772 deprecated: Some(false),
11773 sort_text: Some("Some".to_string()),
11774 filter_text: Some("Some".to_string()),
11775 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
11776 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11777 range: lsp::Range {
11778 start: lsp::Position {
11779 line: 0,
11780 character: 22,
11781 },
11782 end: lsp::Position {
11783 line: 0,
11784 character: 22,
11785 },
11786 },
11787 new_text: "Some(2)".to_string(),
11788 })),
11789 additional_text_edits: Some(vec![lsp::TextEdit {
11790 range: lsp::Range {
11791 start: lsp::Position {
11792 line: 0,
11793 character: 20,
11794 },
11795 end: lsp::Position {
11796 line: 0,
11797 character: 22,
11798 },
11799 },
11800 new_text: "".to_string(),
11801 }]),
11802 ..Default::default()
11803 };
11804
11805 let closure_completion_item = completion_item.clone();
11806 let counter = Arc::new(AtomicUsize::new(0));
11807 let counter_clone = counter.clone();
11808 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
11809 let task_completion_item = closure_completion_item.clone();
11810 counter_clone.fetch_add(1, atomic::Ordering::Release);
11811 async move {
11812 Ok(Some(lsp::CompletionResponse::Array(vec![
11813 task_completion_item,
11814 ])))
11815 }
11816 });
11817
11818 cx.condition(|editor, _| editor.context_menu_visible())
11819 .await;
11820 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
11821 assert!(request.next().await.is_some());
11822 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11823
11824 cx.simulate_keystrokes("S o m");
11825 cx.condition(|editor, _| editor.context_menu_visible())
11826 .await;
11827 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
11828 assert!(request.next().await.is_some());
11829 assert!(request.next().await.is_some());
11830 assert!(request.next().await.is_some());
11831 request.close();
11832 assert!(request.next().await.is_none());
11833 assert_eq!(
11834 counter.load(atomic::Ordering::Acquire),
11835 4,
11836 "With the completions menu open, only one LSP request should happen per input"
11837 );
11838}
11839
11840#[gpui::test]
11841async fn test_toggle_comment(cx: &mut TestAppContext) {
11842 init_test(cx, |_| {});
11843 let mut cx = EditorTestContext::new(cx).await;
11844 let language = Arc::new(Language::new(
11845 LanguageConfig {
11846 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
11847 ..Default::default()
11848 },
11849 Some(tree_sitter_rust::LANGUAGE.into()),
11850 ));
11851 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
11852
11853 // If multiple selections intersect a line, the line is only toggled once.
11854 cx.set_state(indoc! {"
11855 fn a() {
11856 «//b();
11857 ˇ»// «c();
11858 //ˇ» d();
11859 }
11860 "});
11861
11862 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11863
11864 cx.assert_editor_state(indoc! {"
11865 fn a() {
11866 «b();
11867 c();
11868 ˇ» d();
11869 }
11870 "});
11871
11872 // The comment prefix is inserted at the same column for every line in a
11873 // selection.
11874 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11875
11876 cx.assert_editor_state(indoc! {"
11877 fn a() {
11878 // «b();
11879 // c();
11880 ˇ»// d();
11881 }
11882 "});
11883
11884 // If a selection ends at the beginning of a line, that line is not toggled.
11885 cx.set_selections_state(indoc! {"
11886 fn a() {
11887 // b();
11888 «// c();
11889 ˇ» // d();
11890 }
11891 "});
11892
11893 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11894
11895 cx.assert_editor_state(indoc! {"
11896 fn a() {
11897 // b();
11898 «c();
11899 ˇ» // d();
11900 }
11901 "});
11902
11903 // If a selection span a single line and is empty, the line is toggled.
11904 cx.set_state(indoc! {"
11905 fn a() {
11906 a();
11907 b();
11908 ˇ
11909 }
11910 "});
11911
11912 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11913
11914 cx.assert_editor_state(indoc! {"
11915 fn a() {
11916 a();
11917 b();
11918 //•ˇ
11919 }
11920 "});
11921
11922 // If a selection span multiple lines, empty lines are not toggled.
11923 cx.set_state(indoc! {"
11924 fn a() {
11925 «a();
11926
11927 c();ˇ»
11928 }
11929 "});
11930
11931 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11932
11933 cx.assert_editor_state(indoc! {"
11934 fn a() {
11935 // «a();
11936
11937 // c();ˇ»
11938 }
11939 "});
11940
11941 // If a selection includes multiple comment prefixes, all lines are uncommented.
11942 cx.set_state(indoc! {"
11943 fn a() {
11944 «// a();
11945 /// b();
11946 //! c();ˇ»
11947 }
11948 "});
11949
11950 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11951
11952 cx.assert_editor_state(indoc! {"
11953 fn a() {
11954 «a();
11955 b();
11956 c();ˇ»
11957 }
11958 "});
11959}
11960
11961#[gpui::test]
11962async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
11963 init_test(cx, |_| {});
11964 let mut cx = EditorTestContext::new(cx).await;
11965 let language = Arc::new(Language::new(
11966 LanguageConfig {
11967 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
11968 ..Default::default()
11969 },
11970 Some(tree_sitter_rust::LANGUAGE.into()),
11971 ));
11972 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
11973
11974 let toggle_comments = &ToggleComments {
11975 advance_downwards: false,
11976 ignore_indent: true,
11977 };
11978
11979 // If multiple selections intersect a line, the line is only toggled once.
11980 cx.set_state(indoc! {"
11981 fn a() {
11982 // «b();
11983 // c();
11984 // ˇ» d();
11985 }
11986 "});
11987
11988 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11989
11990 cx.assert_editor_state(indoc! {"
11991 fn a() {
11992 «b();
11993 c();
11994 ˇ» d();
11995 }
11996 "});
11997
11998 // The comment prefix is inserted at the beginning of each line
11999 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12000
12001 cx.assert_editor_state(indoc! {"
12002 fn a() {
12003 // «b();
12004 // c();
12005 // ˇ» d();
12006 }
12007 "});
12008
12009 // If a selection ends at the beginning of a line, that line is not toggled.
12010 cx.set_selections_state(indoc! {"
12011 fn a() {
12012 // b();
12013 // «c();
12014 ˇ»// d();
12015 }
12016 "});
12017
12018 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12019
12020 cx.assert_editor_state(indoc! {"
12021 fn a() {
12022 // b();
12023 «c();
12024 ˇ»// d();
12025 }
12026 "});
12027
12028 // If a selection span a single line and is empty, the line is toggled.
12029 cx.set_state(indoc! {"
12030 fn a() {
12031 a();
12032 b();
12033 ˇ
12034 }
12035 "});
12036
12037 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12038
12039 cx.assert_editor_state(indoc! {"
12040 fn a() {
12041 a();
12042 b();
12043 //ˇ
12044 }
12045 "});
12046
12047 // If a selection span multiple lines, empty lines are not toggled.
12048 cx.set_state(indoc! {"
12049 fn a() {
12050 «a();
12051
12052 c();ˇ»
12053 }
12054 "});
12055
12056 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12057
12058 cx.assert_editor_state(indoc! {"
12059 fn a() {
12060 // «a();
12061
12062 // c();ˇ»
12063 }
12064 "});
12065
12066 // If a selection includes multiple comment prefixes, all lines are uncommented.
12067 cx.set_state(indoc! {"
12068 fn a() {
12069 // «a();
12070 /// b();
12071 //! c();ˇ»
12072 }
12073 "});
12074
12075 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12076
12077 cx.assert_editor_state(indoc! {"
12078 fn a() {
12079 «a();
12080 b();
12081 c();ˇ»
12082 }
12083 "});
12084}
12085
12086#[gpui::test]
12087async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
12088 init_test(cx, |_| {});
12089
12090 let language = Arc::new(Language::new(
12091 LanguageConfig {
12092 line_comments: vec!["// ".into()],
12093 ..Default::default()
12094 },
12095 Some(tree_sitter_rust::LANGUAGE.into()),
12096 ));
12097
12098 let mut cx = EditorTestContext::new(cx).await;
12099
12100 cx.language_registry().add(language.clone());
12101 cx.update_buffer(|buffer, cx| {
12102 buffer.set_language(Some(language), cx);
12103 });
12104
12105 let toggle_comments = &ToggleComments {
12106 advance_downwards: true,
12107 ignore_indent: false,
12108 };
12109
12110 // Single cursor on one line -> advance
12111 // Cursor moves horizontally 3 characters as well on non-blank line
12112 cx.set_state(indoc!(
12113 "fn a() {
12114 ˇdog();
12115 cat();
12116 }"
12117 ));
12118 cx.update_editor(|editor, window, cx| {
12119 editor.toggle_comments(toggle_comments, window, cx);
12120 });
12121 cx.assert_editor_state(indoc!(
12122 "fn a() {
12123 // dog();
12124 catˇ();
12125 }"
12126 ));
12127
12128 // Single selection on one line -> don't advance
12129 cx.set_state(indoc!(
12130 "fn a() {
12131 «dog()ˇ»;
12132 cat();
12133 }"
12134 ));
12135 cx.update_editor(|editor, window, cx| {
12136 editor.toggle_comments(toggle_comments, window, cx);
12137 });
12138 cx.assert_editor_state(indoc!(
12139 "fn a() {
12140 // «dog()ˇ»;
12141 cat();
12142 }"
12143 ));
12144
12145 // Multiple cursors on one line -> advance
12146 cx.set_state(indoc!(
12147 "fn a() {
12148 ˇdˇog();
12149 cat();
12150 }"
12151 ));
12152 cx.update_editor(|editor, window, cx| {
12153 editor.toggle_comments(toggle_comments, window, cx);
12154 });
12155 cx.assert_editor_state(indoc!(
12156 "fn a() {
12157 // dog();
12158 catˇ(ˇ);
12159 }"
12160 ));
12161
12162 // Multiple cursors on one line, with selection -> don't advance
12163 cx.set_state(indoc!(
12164 "fn a() {
12165 ˇdˇog«()ˇ»;
12166 cat();
12167 }"
12168 ));
12169 cx.update_editor(|editor, window, cx| {
12170 editor.toggle_comments(toggle_comments, window, cx);
12171 });
12172 cx.assert_editor_state(indoc!(
12173 "fn a() {
12174 // ˇdˇog«()ˇ»;
12175 cat();
12176 }"
12177 ));
12178
12179 // Single cursor on one line -> advance
12180 // Cursor moves to column 0 on blank line
12181 cx.set_state(indoc!(
12182 "fn a() {
12183 ˇdog();
12184
12185 cat();
12186 }"
12187 ));
12188 cx.update_editor(|editor, window, cx| {
12189 editor.toggle_comments(toggle_comments, window, cx);
12190 });
12191 cx.assert_editor_state(indoc!(
12192 "fn a() {
12193 // dog();
12194 ˇ
12195 cat();
12196 }"
12197 ));
12198
12199 // Single cursor on one line -> advance
12200 // Cursor starts and ends at column 0
12201 cx.set_state(indoc!(
12202 "fn a() {
12203 ˇ dog();
12204 cat();
12205 }"
12206 ));
12207 cx.update_editor(|editor, window, cx| {
12208 editor.toggle_comments(toggle_comments, window, cx);
12209 });
12210 cx.assert_editor_state(indoc!(
12211 "fn a() {
12212 // dog();
12213 ˇ cat();
12214 }"
12215 ));
12216}
12217
12218#[gpui::test]
12219async fn test_toggle_block_comment(cx: &mut TestAppContext) {
12220 init_test(cx, |_| {});
12221
12222 let mut cx = EditorTestContext::new(cx).await;
12223
12224 let html_language = Arc::new(
12225 Language::new(
12226 LanguageConfig {
12227 name: "HTML".into(),
12228 block_comment: Some(("<!-- ".into(), " -->".into())),
12229 ..Default::default()
12230 },
12231 Some(tree_sitter_html::LANGUAGE.into()),
12232 )
12233 .with_injection_query(
12234 r#"
12235 (script_element
12236 (raw_text) @injection.content
12237 (#set! injection.language "javascript"))
12238 "#,
12239 )
12240 .unwrap(),
12241 );
12242
12243 let javascript_language = Arc::new(Language::new(
12244 LanguageConfig {
12245 name: "JavaScript".into(),
12246 line_comments: vec!["// ".into()],
12247 ..Default::default()
12248 },
12249 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
12250 ));
12251
12252 cx.language_registry().add(html_language.clone());
12253 cx.language_registry().add(javascript_language.clone());
12254 cx.update_buffer(|buffer, cx| {
12255 buffer.set_language(Some(html_language), cx);
12256 });
12257
12258 // Toggle comments for empty selections
12259 cx.set_state(
12260 &r#"
12261 <p>A</p>ˇ
12262 <p>B</p>ˇ
12263 <p>C</p>ˇ
12264 "#
12265 .unindent(),
12266 );
12267 cx.update_editor(|editor, window, cx| {
12268 editor.toggle_comments(&ToggleComments::default(), window, cx)
12269 });
12270 cx.assert_editor_state(
12271 &r#"
12272 <!-- <p>A</p>ˇ -->
12273 <!-- <p>B</p>ˇ -->
12274 <!-- <p>C</p>ˇ -->
12275 "#
12276 .unindent(),
12277 );
12278 cx.update_editor(|editor, window, cx| {
12279 editor.toggle_comments(&ToggleComments::default(), window, cx)
12280 });
12281 cx.assert_editor_state(
12282 &r#"
12283 <p>A</p>ˇ
12284 <p>B</p>ˇ
12285 <p>C</p>ˇ
12286 "#
12287 .unindent(),
12288 );
12289
12290 // Toggle comments for mixture of empty and non-empty selections, where
12291 // multiple selections occupy a given line.
12292 cx.set_state(
12293 &r#"
12294 <p>A«</p>
12295 <p>ˇ»B</p>ˇ
12296 <p>C«</p>
12297 <p>ˇ»D</p>ˇ
12298 "#
12299 .unindent(),
12300 );
12301
12302 cx.update_editor(|editor, window, cx| {
12303 editor.toggle_comments(&ToggleComments::default(), window, cx)
12304 });
12305 cx.assert_editor_state(
12306 &r#"
12307 <!-- <p>A«</p>
12308 <p>ˇ»B</p>ˇ -->
12309 <!-- <p>C«</p>
12310 <p>ˇ»D</p>ˇ -->
12311 "#
12312 .unindent(),
12313 );
12314 cx.update_editor(|editor, window, cx| {
12315 editor.toggle_comments(&ToggleComments::default(), window, cx)
12316 });
12317 cx.assert_editor_state(
12318 &r#"
12319 <p>A«</p>
12320 <p>ˇ»B</p>ˇ
12321 <p>C«</p>
12322 <p>ˇ»D</p>ˇ
12323 "#
12324 .unindent(),
12325 );
12326
12327 // Toggle comments when different languages are active for different
12328 // selections.
12329 cx.set_state(
12330 &r#"
12331 ˇ<script>
12332 ˇvar x = new Y();
12333 ˇ</script>
12334 "#
12335 .unindent(),
12336 );
12337 cx.executor().run_until_parked();
12338 cx.update_editor(|editor, window, cx| {
12339 editor.toggle_comments(&ToggleComments::default(), window, cx)
12340 });
12341 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
12342 // Uncommenting and commenting from this position brings in even more wrong artifacts.
12343 cx.assert_editor_state(
12344 &r#"
12345 <!-- ˇ<script> -->
12346 // ˇvar x = new Y();
12347 <!-- ˇ</script> -->
12348 "#
12349 .unindent(),
12350 );
12351}
12352
12353#[gpui::test]
12354fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
12355 init_test(cx, |_| {});
12356
12357 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
12358 let multibuffer = cx.new(|cx| {
12359 let mut multibuffer = MultiBuffer::new(ReadWrite);
12360 multibuffer.push_excerpts(
12361 buffer.clone(),
12362 [
12363 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
12364 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
12365 ],
12366 cx,
12367 );
12368 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
12369 multibuffer
12370 });
12371
12372 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
12373 editor.update_in(cx, |editor, window, cx| {
12374 assert_eq!(editor.text(cx), "aaaa\nbbbb");
12375 editor.change_selections(None, window, cx, |s| {
12376 s.select_ranges([
12377 Point::new(0, 0)..Point::new(0, 0),
12378 Point::new(1, 0)..Point::new(1, 0),
12379 ])
12380 });
12381
12382 editor.handle_input("X", window, cx);
12383 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
12384 assert_eq!(
12385 editor.selections.ranges(cx),
12386 [
12387 Point::new(0, 1)..Point::new(0, 1),
12388 Point::new(1, 1)..Point::new(1, 1),
12389 ]
12390 );
12391
12392 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
12393 editor.change_selections(None, window, cx, |s| {
12394 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
12395 });
12396 editor.backspace(&Default::default(), window, cx);
12397 assert_eq!(editor.text(cx), "Xa\nbbb");
12398 assert_eq!(
12399 editor.selections.ranges(cx),
12400 [Point::new(1, 0)..Point::new(1, 0)]
12401 );
12402
12403 editor.change_selections(None, window, cx, |s| {
12404 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
12405 });
12406 editor.backspace(&Default::default(), window, cx);
12407 assert_eq!(editor.text(cx), "X\nbb");
12408 assert_eq!(
12409 editor.selections.ranges(cx),
12410 [Point::new(0, 1)..Point::new(0, 1)]
12411 );
12412 });
12413}
12414
12415#[gpui::test]
12416fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
12417 init_test(cx, |_| {});
12418
12419 let markers = vec![('[', ']').into(), ('(', ')').into()];
12420 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
12421 indoc! {"
12422 [aaaa
12423 (bbbb]
12424 cccc)",
12425 },
12426 markers.clone(),
12427 );
12428 let excerpt_ranges = markers.into_iter().map(|marker| {
12429 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
12430 ExcerptRange::new(context.clone())
12431 });
12432 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
12433 let multibuffer = cx.new(|cx| {
12434 let mut multibuffer = MultiBuffer::new(ReadWrite);
12435 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
12436 multibuffer
12437 });
12438
12439 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
12440 editor.update_in(cx, |editor, window, cx| {
12441 let (expected_text, selection_ranges) = marked_text_ranges(
12442 indoc! {"
12443 aaaa
12444 bˇbbb
12445 bˇbbˇb
12446 cccc"
12447 },
12448 true,
12449 );
12450 assert_eq!(editor.text(cx), expected_text);
12451 editor.change_selections(None, window, cx, |s| s.select_ranges(selection_ranges));
12452
12453 editor.handle_input("X", window, cx);
12454
12455 let (expected_text, expected_selections) = marked_text_ranges(
12456 indoc! {"
12457 aaaa
12458 bXˇbbXb
12459 bXˇbbXˇb
12460 cccc"
12461 },
12462 false,
12463 );
12464 assert_eq!(editor.text(cx), expected_text);
12465 assert_eq!(editor.selections.ranges(cx), expected_selections);
12466
12467 editor.newline(&Newline, window, cx);
12468 let (expected_text, expected_selections) = marked_text_ranges(
12469 indoc! {"
12470 aaaa
12471 bX
12472 ˇbbX
12473 b
12474 bX
12475 ˇbbX
12476 ˇb
12477 cccc"
12478 },
12479 false,
12480 );
12481 assert_eq!(editor.text(cx), expected_text);
12482 assert_eq!(editor.selections.ranges(cx), expected_selections);
12483 });
12484}
12485
12486#[gpui::test]
12487fn test_refresh_selections(cx: &mut TestAppContext) {
12488 init_test(cx, |_| {});
12489
12490 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
12491 let mut excerpt1_id = None;
12492 let multibuffer = cx.new(|cx| {
12493 let mut multibuffer = MultiBuffer::new(ReadWrite);
12494 excerpt1_id = multibuffer
12495 .push_excerpts(
12496 buffer.clone(),
12497 [
12498 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
12499 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
12500 ],
12501 cx,
12502 )
12503 .into_iter()
12504 .next();
12505 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
12506 multibuffer
12507 });
12508
12509 let editor = cx.add_window(|window, cx| {
12510 let mut editor = build_editor(multibuffer.clone(), window, cx);
12511 let snapshot = editor.snapshot(window, cx);
12512 editor.change_selections(None, window, cx, |s| {
12513 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
12514 });
12515 editor.begin_selection(
12516 Point::new(2, 1).to_display_point(&snapshot),
12517 true,
12518 1,
12519 window,
12520 cx,
12521 );
12522 assert_eq!(
12523 editor.selections.ranges(cx),
12524 [
12525 Point::new(1, 3)..Point::new(1, 3),
12526 Point::new(2, 1)..Point::new(2, 1),
12527 ]
12528 );
12529 editor
12530 });
12531
12532 // Refreshing selections is a no-op when excerpts haven't changed.
12533 _ = editor.update(cx, |editor, window, cx| {
12534 editor.change_selections(None, window, cx, |s| s.refresh());
12535 assert_eq!(
12536 editor.selections.ranges(cx),
12537 [
12538 Point::new(1, 3)..Point::new(1, 3),
12539 Point::new(2, 1)..Point::new(2, 1),
12540 ]
12541 );
12542 });
12543
12544 multibuffer.update(cx, |multibuffer, cx| {
12545 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
12546 });
12547 _ = editor.update(cx, |editor, window, cx| {
12548 // Removing an excerpt causes the first selection to become degenerate.
12549 assert_eq!(
12550 editor.selections.ranges(cx),
12551 [
12552 Point::new(0, 0)..Point::new(0, 0),
12553 Point::new(0, 1)..Point::new(0, 1)
12554 ]
12555 );
12556
12557 // Refreshing selections will relocate the first selection to the original buffer
12558 // location.
12559 editor.change_selections(None, window, cx, |s| s.refresh());
12560 assert_eq!(
12561 editor.selections.ranges(cx),
12562 [
12563 Point::new(0, 1)..Point::new(0, 1),
12564 Point::new(0, 3)..Point::new(0, 3)
12565 ]
12566 );
12567 assert!(editor.selections.pending_anchor().is_some());
12568 });
12569}
12570
12571#[gpui::test]
12572fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
12573 init_test(cx, |_| {});
12574
12575 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
12576 let mut excerpt1_id = None;
12577 let multibuffer = cx.new(|cx| {
12578 let mut multibuffer = MultiBuffer::new(ReadWrite);
12579 excerpt1_id = multibuffer
12580 .push_excerpts(
12581 buffer.clone(),
12582 [
12583 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
12584 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
12585 ],
12586 cx,
12587 )
12588 .into_iter()
12589 .next();
12590 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
12591 multibuffer
12592 });
12593
12594 let editor = cx.add_window(|window, cx| {
12595 let mut editor = build_editor(multibuffer.clone(), window, cx);
12596 let snapshot = editor.snapshot(window, cx);
12597 editor.begin_selection(
12598 Point::new(1, 3).to_display_point(&snapshot),
12599 false,
12600 1,
12601 window,
12602 cx,
12603 );
12604 assert_eq!(
12605 editor.selections.ranges(cx),
12606 [Point::new(1, 3)..Point::new(1, 3)]
12607 );
12608 editor
12609 });
12610
12611 multibuffer.update(cx, |multibuffer, cx| {
12612 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
12613 });
12614 _ = editor.update(cx, |editor, window, cx| {
12615 assert_eq!(
12616 editor.selections.ranges(cx),
12617 [Point::new(0, 0)..Point::new(0, 0)]
12618 );
12619
12620 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
12621 editor.change_selections(None, window, cx, |s| s.refresh());
12622 assert_eq!(
12623 editor.selections.ranges(cx),
12624 [Point::new(0, 3)..Point::new(0, 3)]
12625 );
12626 assert!(editor.selections.pending_anchor().is_some());
12627 });
12628}
12629
12630#[gpui::test]
12631async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
12632 init_test(cx, |_| {});
12633
12634 let language = Arc::new(
12635 Language::new(
12636 LanguageConfig {
12637 brackets: BracketPairConfig {
12638 pairs: vec![
12639 BracketPair {
12640 start: "{".to_string(),
12641 end: "}".to_string(),
12642 close: true,
12643 surround: true,
12644 newline: true,
12645 },
12646 BracketPair {
12647 start: "/* ".to_string(),
12648 end: " */".to_string(),
12649 close: true,
12650 surround: true,
12651 newline: true,
12652 },
12653 ],
12654 ..Default::default()
12655 },
12656 ..Default::default()
12657 },
12658 Some(tree_sitter_rust::LANGUAGE.into()),
12659 )
12660 .with_indents_query("")
12661 .unwrap(),
12662 );
12663
12664 let text = concat!(
12665 "{ }\n", //
12666 " x\n", //
12667 " /* */\n", //
12668 "x\n", //
12669 "{{} }\n", //
12670 );
12671
12672 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
12673 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12674 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
12675 editor
12676 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
12677 .await;
12678
12679 editor.update_in(cx, |editor, window, cx| {
12680 editor.change_selections(None, window, cx, |s| {
12681 s.select_display_ranges([
12682 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
12683 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
12684 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
12685 ])
12686 });
12687 editor.newline(&Newline, window, cx);
12688
12689 assert_eq!(
12690 editor.buffer().read(cx).read(cx).text(),
12691 concat!(
12692 "{ \n", // Suppress rustfmt
12693 "\n", //
12694 "}\n", //
12695 " x\n", //
12696 " /* \n", //
12697 " \n", //
12698 " */\n", //
12699 "x\n", //
12700 "{{} \n", //
12701 "}\n", //
12702 )
12703 );
12704 });
12705}
12706
12707#[gpui::test]
12708fn test_highlighted_ranges(cx: &mut TestAppContext) {
12709 init_test(cx, |_| {});
12710
12711 let editor = cx.add_window(|window, cx| {
12712 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
12713 build_editor(buffer.clone(), window, cx)
12714 });
12715
12716 _ = editor.update(cx, |editor, window, cx| {
12717 struct Type1;
12718 struct Type2;
12719
12720 let buffer = editor.buffer.read(cx).snapshot(cx);
12721
12722 let anchor_range =
12723 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
12724
12725 editor.highlight_background::<Type1>(
12726 &[
12727 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
12728 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
12729 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
12730 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
12731 ],
12732 |_| Hsla::red(),
12733 cx,
12734 );
12735 editor.highlight_background::<Type2>(
12736 &[
12737 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
12738 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
12739 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
12740 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
12741 ],
12742 |_| Hsla::green(),
12743 cx,
12744 );
12745
12746 let snapshot = editor.snapshot(window, cx);
12747 let mut highlighted_ranges = editor.background_highlights_in_range(
12748 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
12749 &snapshot,
12750 cx.theme().colors(),
12751 );
12752 // Enforce a consistent ordering based on color without relying on the ordering of the
12753 // highlight's `TypeId` which is non-executor.
12754 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
12755 assert_eq!(
12756 highlighted_ranges,
12757 &[
12758 (
12759 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
12760 Hsla::red(),
12761 ),
12762 (
12763 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
12764 Hsla::red(),
12765 ),
12766 (
12767 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
12768 Hsla::green(),
12769 ),
12770 (
12771 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
12772 Hsla::green(),
12773 ),
12774 ]
12775 );
12776 assert_eq!(
12777 editor.background_highlights_in_range(
12778 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
12779 &snapshot,
12780 cx.theme().colors(),
12781 ),
12782 &[(
12783 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
12784 Hsla::red(),
12785 )]
12786 );
12787 });
12788}
12789
12790#[gpui::test]
12791async fn test_following(cx: &mut TestAppContext) {
12792 init_test(cx, |_| {});
12793
12794 let fs = FakeFs::new(cx.executor());
12795 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
12796
12797 let buffer = project.update(cx, |project, cx| {
12798 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
12799 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
12800 });
12801 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
12802 let follower = cx.update(|cx| {
12803 cx.open_window(
12804 WindowOptions {
12805 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
12806 gpui::Point::new(px(0.), px(0.)),
12807 gpui::Point::new(px(10.), px(80.)),
12808 ))),
12809 ..Default::default()
12810 },
12811 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
12812 )
12813 .unwrap()
12814 });
12815
12816 let is_still_following = Rc::new(RefCell::new(true));
12817 let follower_edit_event_count = Rc::new(RefCell::new(0));
12818 let pending_update = Rc::new(RefCell::new(None));
12819 let leader_entity = leader.root(cx).unwrap();
12820 let follower_entity = follower.root(cx).unwrap();
12821 _ = follower.update(cx, {
12822 let update = pending_update.clone();
12823 let is_still_following = is_still_following.clone();
12824 let follower_edit_event_count = follower_edit_event_count.clone();
12825 |_, window, cx| {
12826 cx.subscribe_in(
12827 &leader_entity,
12828 window,
12829 move |_, leader, event, window, cx| {
12830 leader.read(cx).add_event_to_update_proto(
12831 event,
12832 &mut update.borrow_mut(),
12833 window,
12834 cx,
12835 );
12836 },
12837 )
12838 .detach();
12839
12840 cx.subscribe_in(
12841 &follower_entity,
12842 window,
12843 move |_, _, event: &EditorEvent, _window, _cx| {
12844 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
12845 *is_still_following.borrow_mut() = false;
12846 }
12847
12848 if let EditorEvent::BufferEdited = event {
12849 *follower_edit_event_count.borrow_mut() += 1;
12850 }
12851 },
12852 )
12853 .detach();
12854 }
12855 });
12856
12857 // Update the selections only
12858 _ = leader.update(cx, |leader, window, cx| {
12859 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
12860 });
12861 follower
12862 .update(cx, |follower, window, cx| {
12863 follower.apply_update_proto(
12864 &project,
12865 pending_update.borrow_mut().take().unwrap(),
12866 window,
12867 cx,
12868 )
12869 })
12870 .unwrap()
12871 .await
12872 .unwrap();
12873 _ = follower.update(cx, |follower, _, cx| {
12874 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
12875 });
12876 assert!(*is_still_following.borrow());
12877 assert_eq!(*follower_edit_event_count.borrow(), 0);
12878
12879 // Update the scroll position only
12880 _ = leader.update(cx, |leader, window, cx| {
12881 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
12882 });
12883 follower
12884 .update(cx, |follower, window, cx| {
12885 follower.apply_update_proto(
12886 &project,
12887 pending_update.borrow_mut().take().unwrap(),
12888 window,
12889 cx,
12890 )
12891 })
12892 .unwrap()
12893 .await
12894 .unwrap();
12895 assert_eq!(
12896 follower
12897 .update(cx, |follower, _, cx| follower.scroll_position(cx))
12898 .unwrap(),
12899 gpui::Point::new(1.5, 3.5)
12900 );
12901 assert!(*is_still_following.borrow());
12902 assert_eq!(*follower_edit_event_count.borrow(), 0);
12903
12904 // Update the selections and scroll position. The follower's scroll position is updated
12905 // via autoscroll, not via the leader's exact scroll position.
12906 _ = leader.update(cx, |leader, window, cx| {
12907 leader.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
12908 leader.request_autoscroll(Autoscroll::newest(), cx);
12909 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
12910 });
12911 follower
12912 .update(cx, |follower, window, cx| {
12913 follower.apply_update_proto(
12914 &project,
12915 pending_update.borrow_mut().take().unwrap(),
12916 window,
12917 cx,
12918 )
12919 })
12920 .unwrap()
12921 .await
12922 .unwrap();
12923 _ = follower.update(cx, |follower, _, cx| {
12924 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
12925 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
12926 });
12927 assert!(*is_still_following.borrow());
12928
12929 // Creating a pending selection that precedes another selection
12930 _ = leader.update(cx, |leader, window, cx| {
12931 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
12932 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
12933 });
12934 follower
12935 .update(cx, |follower, window, cx| {
12936 follower.apply_update_proto(
12937 &project,
12938 pending_update.borrow_mut().take().unwrap(),
12939 window,
12940 cx,
12941 )
12942 })
12943 .unwrap()
12944 .await
12945 .unwrap();
12946 _ = follower.update(cx, |follower, _, cx| {
12947 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
12948 });
12949 assert!(*is_still_following.borrow());
12950
12951 // Extend the pending selection so that it surrounds another selection
12952 _ = leader.update(cx, |leader, window, cx| {
12953 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
12954 });
12955 follower
12956 .update(cx, |follower, window, cx| {
12957 follower.apply_update_proto(
12958 &project,
12959 pending_update.borrow_mut().take().unwrap(),
12960 window,
12961 cx,
12962 )
12963 })
12964 .unwrap()
12965 .await
12966 .unwrap();
12967 _ = follower.update(cx, |follower, _, cx| {
12968 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
12969 });
12970
12971 // Scrolling locally breaks the follow
12972 _ = follower.update(cx, |follower, window, cx| {
12973 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
12974 follower.set_scroll_anchor(
12975 ScrollAnchor {
12976 anchor: top_anchor,
12977 offset: gpui::Point::new(0.0, 0.5),
12978 },
12979 window,
12980 cx,
12981 );
12982 });
12983 assert!(!(*is_still_following.borrow()));
12984}
12985
12986#[gpui::test]
12987async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
12988 init_test(cx, |_| {});
12989
12990 let fs = FakeFs::new(cx.executor());
12991 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
12992 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12993 let pane = workspace
12994 .update(cx, |workspace, _, _| workspace.active_pane().clone())
12995 .unwrap();
12996
12997 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
12998
12999 let leader = pane.update_in(cx, |_, window, cx| {
13000 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
13001 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
13002 });
13003
13004 // Start following the editor when it has no excerpts.
13005 let mut state_message =
13006 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
13007 let workspace_entity = workspace.root(cx).unwrap();
13008 let follower_1 = cx
13009 .update_window(*workspace.deref(), |_, window, cx| {
13010 Editor::from_state_proto(
13011 workspace_entity,
13012 ViewId {
13013 creator: CollaboratorId::PeerId(PeerId::default()),
13014 id: 0,
13015 },
13016 &mut state_message,
13017 window,
13018 cx,
13019 )
13020 })
13021 .unwrap()
13022 .unwrap()
13023 .await
13024 .unwrap();
13025
13026 let update_message = Rc::new(RefCell::new(None));
13027 follower_1.update_in(cx, {
13028 let update = update_message.clone();
13029 |_, window, cx| {
13030 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
13031 leader.read(cx).add_event_to_update_proto(
13032 event,
13033 &mut update.borrow_mut(),
13034 window,
13035 cx,
13036 );
13037 })
13038 .detach();
13039 }
13040 });
13041
13042 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
13043 (
13044 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
13045 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
13046 )
13047 });
13048
13049 // Insert some excerpts.
13050 leader.update(cx, |leader, cx| {
13051 leader.buffer.update(cx, |multibuffer, cx| {
13052 multibuffer.set_excerpts_for_path(
13053 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
13054 buffer_1.clone(),
13055 vec![
13056 Point::row_range(0..3),
13057 Point::row_range(1..6),
13058 Point::row_range(12..15),
13059 ],
13060 0,
13061 cx,
13062 );
13063 multibuffer.set_excerpts_for_path(
13064 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
13065 buffer_2.clone(),
13066 vec![Point::row_range(0..6), Point::row_range(8..12)],
13067 0,
13068 cx,
13069 );
13070 });
13071 });
13072
13073 // Apply the update of adding the excerpts.
13074 follower_1
13075 .update_in(cx, |follower, window, cx| {
13076 follower.apply_update_proto(
13077 &project,
13078 update_message.borrow().clone().unwrap(),
13079 window,
13080 cx,
13081 )
13082 })
13083 .await
13084 .unwrap();
13085 assert_eq!(
13086 follower_1.update(cx, |editor, cx| editor.text(cx)),
13087 leader.update(cx, |editor, cx| editor.text(cx))
13088 );
13089 update_message.borrow_mut().take();
13090
13091 // Start following separately after it already has excerpts.
13092 let mut state_message =
13093 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
13094 let workspace_entity = workspace.root(cx).unwrap();
13095 let follower_2 = cx
13096 .update_window(*workspace.deref(), |_, window, cx| {
13097 Editor::from_state_proto(
13098 workspace_entity,
13099 ViewId {
13100 creator: CollaboratorId::PeerId(PeerId::default()),
13101 id: 0,
13102 },
13103 &mut state_message,
13104 window,
13105 cx,
13106 )
13107 })
13108 .unwrap()
13109 .unwrap()
13110 .await
13111 .unwrap();
13112 assert_eq!(
13113 follower_2.update(cx, |editor, cx| editor.text(cx)),
13114 leader.update(cx, |editor, cx| editor.text(cx))
13115 );
13116
13117 // Remove some excerpts.
13118 leader.update(cx, |leader, cx| {
13119 leader.buffer.update(cx, |multibuffer, cx| {
13120 let excerpt_ids = multibuffer.excerpt_ids();
13121 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
13122 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
13123 });
13124 });
13125
13126 // Apply the update of removing the excerpts.
13127 follower_1
13128 .update_in(cx, |follower, window, cx| {
13129 follower.apply_update_proto(
13130 &project,
13131 update_message.borrow().clone().unwrap(),
13132 window,
13133 cx,
13134 )
13135 })
13136 .await
13137 .unwrap();
13138 follower_2
13139 .update_in(cx, |follower, window, cx| {
13140 follower.apply_update_proto(
13141 &project,
13142 update_message.borrow().clone().unwrap(),
13143 window,
13144 cx,
13145 )
13146 })
13147 .await
13148 .unwrap();
13149 update_message.borrow_mut().take();
13150 assert_eq!(
13151 follower_1.update(cx, |editor, cx| editor.text(cx)),
13152 leader.update(cx, |editor, cx| editor.text(cx))
13153 );
13154}
13155
13156#[gpui::test]
13157async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
13158 init_test(cx, |_| {});
13159
13160 let mut cx = EditorTestContext::new(cx).await;
13161 let lsp_store =
13162 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
13163
13164 cx.set_state(indoc! {"
13165 ˇfn func(abc def: i32) -> u32 {
13166 }
13167 "});
13168
13169 cx.update(|_, cx| {
13170 lsp_store.update(cx, |lsp_store, cx| {
13171 lsp_store
13172 .update_diagnostics(
13173 LanguageServerId(0),
13174 lsp::PublishDiagnosticsParams {
13175 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
13176 version: None,
13177 diagnostics: vec![
13178 lsp::Diagnostic {
13179 range: lsp::Range::new(
13180 lsp::Position::new(0, 11),
13181 lsp::Position::new(0, 12),
13182 ),
13183 severity: Some(lsp::DiagnosticSeverity::ERROR),
13184 ..Default::default()
13185 },
13186 lsp::Diagnostic {
13187 range: lsp::Range::new(
13188 lsp::Position::new(0, 12),
13189 lsp::Position::new(0, 15),
13190 ),
13191 severity: Some(lsp::DiagnosticSeverity::ERROR),
13192 ..Default::default()
13193 },
13194 lsp::Diagnostic {
13195 range: lsp::Range::new(
13196 lsp::Position::new(0, 25),
13197 lsp::Position::new(0, 28),
13198 ),
13199 severity: Some(lsp::DiagnosticSeverity::ERROR),
13200 ..Default::default()
13201 },
13202 ],
13203 },
13204 &[],
13205 cx,
13206 )
13207 .unwrap()
13208 });
13209 });
13210
13211 executor.run_until_parked();
13212
13213 cx.update_editor(|editor, window, cx| {
13214 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13215 });
13216
13217 cx.assert_editor_state(indoc! {"
13218 fn func(abc def: i32) -> ˇu32 {
13219 }
13220 "});
13221
13222 cx.update_editor(|editor, window, cx| {
13223 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13224 });
13225
13226 cx.assert_editor_state(indoc! {"
13227 fn func(abc ˇdef: i32) -> u32 {
13228 }
13229 "});
13230
13231 cx.update_editor(|editor, window, cx| {
13232 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13233 });
13234
13235 cx.assert_editor_state(indoc! {"
13236 fn func(abcˇ def: i32) -> u32 {
13237 }
13238 "});
13239
13240 cx.update_editor(|editor, window, cx| {
13241 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13242 });
13243
13244 cx.assert_editor_state(indoc! {"
13245 fn func(abc def: i32) -> ˇu32 {
13246 }
13247 "});
13248}
13249
13250#[gpui::test]
13251async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
13252 init_test(cx, |_| {});
13253
13254 let mut cx = EditorTestContext::new(cx).await;
13255
13256 let diff_base = r#"
13257 use some::mod;
13258
13259 const A: u32 = 42;
13260
13261 fn main() {
13262 println!("hello");
13263
13264 println!("world");
13265 }
13266 "#
13267 .unindent();
13268
13269 // Edits are modified, removed, modified, added
13270 cx.set_state(
13271 &r#"
13272 use some::modified;
13273
13274 ˇ
13275 fn main() {
13276 println!("hello there");
13277
13278 println!("around the");
13279 println!("world");
13280 }
13281 "#
13282 .unindent(),
13283 );
13284
13285 cx.set_head_text(&diff_base);
13286 executor.run_until_parked();
13287
13288 cx.update_editor(|editor, window, cx| {
13289 //Wrap around the bottom of the buffer
13290 for _ in 0..3 {
13291 editor.go_to_next_hunk(&GoToHunk, window, cx);
13292 }
13293 });
13294
13295 cx.assert_editor_state(
13296 &r#"
13297 ˇuse some::modified;
13298
13299
13300 fn main() {
13301 println!("hello there");
13302
13303 println!("around the");
13304 println!("world");
13305 }
13306 "#
13307 .unindent(),
13308 );
13309
13310 cx.update_editor(|editor, window, cx| {
13311 //Wrap around the top of the buffer
13312 for _ in 0..2 {
13313 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13314 }
13315 });
13316
13317 cx.assert_editor_state(
13318 &r#"
13319 use some::modified;
13320
13321
13322 fn main() {
13323 ˇ println!("hello there");
13324
13325 println!("around the");
13326 println!("world");
13327 }
13328 "#
13329 .unindent(),
13330 );
13331
13332 cx.update_editor(|editor, window, cx| {
13333 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13334 });
13335
13336 cx.assert_editor_state(
13337 &r#"
13338 use some::modified;
13339
13340 ˇ
13341 fn main() {
13342 println!("hello there");
13343
13344 println!("around the");
13345 println!("world");
13346 }
13347 "#
13348 .unindent(),
13349 );
13350
13351 cx.update_editor(|editor, window, cx| {
13352 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13353 });
13354
13355 cx.assert_editor_state(
13356 &r#"
13357 ˇuse some::modified;
13358
13359
13360 fn main() {
13361 println!("hello there");
13362
13363 println!("around the");
13364 println!("world");
13365 }
13366 "#
13367 .unindent(),
13368 );
13369
13370 cx.update_editor(|editor, window, cx| {
13371 for _ in 0..2 {
13372 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13373 }
13374 });
13375
13376 cx.assert_editor_state(
13377 &r#"
13378 use some::modified;
13379
13380
13381 fn main() {
13382 ˇ println!("hello there");
13383
13384 println!("around the");
13385 println!("world");
13386 }
13387 "#
13388 .unindent(),
13389 );
13390
13391 cx.update_editor(|editor, window, cx| {
13392 editor.fold(&Fold, window, cx);
13393 });
13394
13395 cx.update_editor(|editor, window, cx| {
13396 editor.go_to_next_hunk(&GoToHunk, window, cx);
13397 });
13398
13399 cx.assert_editor_state(
13400 &r#"
13401 ˇuse some::modified;
13402
13403
13404 fn main() {
13405 println!("hello there");
13406
13407 println!("around the");
13408 println!("world");
13409 }
13410 "#
13411 .unindent(),
13412 );
13413}
13414
13415#[test]
13416fn test_split_words() {
13417 fn split(text: &str) -> Vec<&str> {
13418 split_words(text).collect()
13419 }
13420
13421 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
13422 assert_eq!(split("hello_world"), &["hello_", "world"]);
13423 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
13424 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
13425 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
13426 assert_eq!(split("helloworld"), &["helloworld"]);
13427
13428 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
13429}
13430
13431#[gpui::test]
13432async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
13433 init_test(cx, |_| {});
13434
13435 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
13436 let mut assert = |before, after| {
13437 let _state_context = cx.set_state(before);
13438 cx.run_until_parked();
13439 cx.update_editor(|editor, window, cx| {
13440 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
13441 });
13442 cx.run_until_parked();
13443 cx.assert_editor_state(after);
13444 };
13445
13446 // Outside bracket jumps to outside of matching bracket
13447 assert("console.logˇ(var);", "console.log(var)ˇ;");
13448 assert("console.log(var)ˇ;", "console.logˇ(var);");
13449
13450 // Inside bracket jumps to inside of matching bracket
13451 assert("console.log(ˇvar);", "console.log(varˇ);");
13452 assert("console.log(varˇ);", "console.log(ˇvar);");
13453
13454 // When outside a bracket and inside, favor jumping to the inside bracket
13455 assert(
13456 "console.log('foo', [1, 2, 3]ˇ);",
13457 "console.log(ˇ'foo', [1, 2, 3]);",
13458 );
13459 assert(
13460 "console.log(ˇ'foo', [1, 2, 3]);",
13461 "console.log('foo', [1, 2, 3]ˇ);",
13462 );
13463
13464 // Bias forward if two options are equally likely
13465 assert(
13466 "let result = curried_fun()ˇ();",
13467 "let result = curried_fun()()ˇ;",
13468 );
13469
13470 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
13471 assert(
13472 indoc! {"
13473 function test() {
13474 console.log('test')ˇ
13475 }"},
13476 indoc! {"
13477 function test() {
13478 console.logˇ('test')
13479 }"},
13480 );
13481}
13482
13483#[gpui::test]
13484async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
13485 init_test(cx, |_| {});
13486
13487 let fs = FakeFs::new(cx.executor());
13488 fs.insert_tree(
13489 path!("/a"),
13490 json!({
13491 "main.rs": "fn main() { let a = 5; }",
13492 "other.rs": "// Test file",
13493 }),
13494 )
13495 .await;
13496 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13497
13498 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13499 language_registry.add(Arc::new(Language::new(
13500 LanguageConfig {
13501 name: "Rust".into(),
13502 matcher: LanguageMatcher {
13503 path_suffixes: vec!["rs".to_string()],
13504 ..Default::default()
13505 },
13506 brackets: BracketPairConfig {
13507 pairs: vec![BracketPair {
13508 start: "{".to_string(),
13509 end: "}".to_string(),
13510 close: true,
13511 surround: true,
13512 newline: true,
13513 }],
13514 disabled_scopes_by_bracket_ix: Vec::new(),
13515 },
13516 ..Default::default()
13517 },
13518 Some(tree_sitter_rust::LANGUAGE.into()),
13519 )));
13520 let mut fake_servers = language_registry.register_fake_lsp(
13521 "Rust",
13522 FakeLspAdapter {
13523 capabilities: lsp::ServerCapabilities {
13524 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
13525 first_trigger_character: "{".to_string(),
13526 more_trigger_character: None,
13527 }),
13528 ..Default::default()
13529 },
13530 ..Default::default()
13531 },
13532 );
13533
13534 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13535
13536 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13537
13538 let worktree_id = workspace
13539 .update(cx, |workspace, _, cx| {
13540 workspace.project().update(cx, |project, cx| {
13541 project.worktrees(cx).next().unwrap().read(cx).id()
13542 })
13543 })
13544 .unwrap();
13545
13546 let buffer = project
13547 .update(cx, |project, cx| {
13548 project.open_local_buffer(path!("/a/main.rs"), cx)
13549 })
13550 .await
13551 .unwrap();
13552 let editor_handle = workspace
13553 .update(cx, |workspace, window, cx| {
13554 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
13555 })
13556 .unwrap()
13557 .await
13558 .unwrap()
13559 .downcast::<Editor>()
13560 .unwrap();
13561
13562 cx.executor().start_waiting();
13563 let fake_server = fake_servers.next().await.unwrap();
13564
13565 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
13566 |params, _| async move {
13567 assert_eq!(
13568 params.text_document_position.text_document.uri,
13569 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
13570 );
13571 assert_eq!(
13572 params.text_document_position.position,
13573 lsp::Position::new(0, 21),
13574 );
13575
13576 Ok(Some(vec![lsp::TextEdit {
13577 new_text: "]".to_string(),
13578 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13579 }]))
13580 },
13581 );
13582
13583 editor_handle.update_in(cx, |editor, window, cx| {
13584 window.focus(&editor.focus_handle(cx));
13585 editor.change_selections(None, window, cx, |s| {
13586 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
13587 });
13588 editor.handle_input("{", window, cx);
13589 });
13590
13591 cx.executor().run_until_parked();
13592
13593 buffer.update(cx, |buffer, _| {
13594 assert_eq!(
13595 buffer.text(),
13596 "fn main() { let a = {5}; }",
13597 "No extra braces from on type formatting should appear in the buffer"
13598 )
13599 });
13600}
13601
13602#[gpui::test]
13603async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
13604 init_test(cx, |_| {});
13605
13606 let fs = FakeFs::new(cx.executor());
13607 fs.insert_tree(
13608 path!("/a"),
13609 json!({
13610 "main.rs": "fn main() { let a = 5; }",
13611 "other.rs": "// Test file",
13612 }),
13613 )
13614 .await;
13615
13616 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13617
13618 let server_restarts = Arc::new(AtomicUsize::new(0));
13619 let closure_restarts = Arc::clone(&server_restarts);
13620 let language_server_name = "test language server";
13621 let language_name: LanguageName = "Rust".into();
13622
13623 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13624 language_registry.add(Arc::new(Language::new(
13625 LanguageConfig {
13626 name: language_name.clone(),
13627 matcher: LanguageMatcher {
13628 path_suffixes: vec!["rs".to_string()],
13629 ..Default::default()
13630 },
13631 ..Default::default()
13632 },
13633 Some(tree_sitter_rust::LANGUAGE.into()),
13634 )));
13635 let mut fake_servers = language_registry.register_fake_lsp(
13636 "Rust",
13637 FakeLspAdapter {
13638 name: language_server_name,
13639 initialization_options: Some(json!({
13640 "testOptionValue": true
13641 })),
13642 initializer: Some(Box::new(move |fake_server| {
13643 let task_restarts = Arc::clone(&closure_restarts);
13644 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
13645 task_restarts.fetch_add(1, atomic::Ordering::Release);
13646 futures::future::ready(Ok(()))
13647 });
13648 })),
13649 ..Default::default()
13650 },
13651 );
13652
13653 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13654 let _buffer = project
13655 .update(cx, |project, cx| {
13656 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
13657 })
13658 .await
13659 .unwrap();
13660 let _fake_server = fake_servers.next().await.unwrap();
13661 update_test_language_settings(cx, |language_settings| {
13662 language_settings.languages.insert(
13663 language_name.clone(),
13664 LanguageSettingsContent {
13665 tab_size: NonZeroU32::new(8),
13666 ..Default::default()
13667 },
13668 );
13669 });
13670 cx.executor().run_until_parked();
13671 assert_eq!(
13672 server_restarts.load(atomic::Ordering::Acquire),
13673 0,
13674 "Should not restart LSP server on an unrelated change"
13675 );
13676
13677 update_test_project_settings(cx, |project_settings| {
13678 project_settings.lsp.insert(
13679 "Some other server name".into(),
13680 LspSettings {
13681 binary: None,
13682 settings: None,
13683 initialization_options: Some(json!({
13684 "some other init value": false
13685 })),
13686 enable_lsp_tasks: false,
13687 },
13688 );
13689 });
13690 cx.executor().run_until_parked();
13691 assert_eq!(
13692 server_restarts.load(atomic::Ordering::Acquire),
13693 0,
13694 "Should not restart LSP server on an unrelated LSP settings change"
13695 );
13696
13697 update_test_project_settings(cx, |project_settings| {
13698 project_settings.lsp.insert(
13699 language_server_name.into(),
13700 LspSettings {
13701 binary: None,
13702 settings: None,
13703 initialization_options: Some(json!({
13704 "anotherInitValue": false
13705 })),
13706 enable_lsp_tasks: false,
13707 },
13708 );
13709 });
13710 cx.executor().run_until_parked();
13711 assert_eq!(
13712 server_restarts.load(atomic::Ordering::Acquire),
13713 1,
13714 "Should restart LSP server on a related LSP settings change"
13715 );
13716
13717 update_test_project_settings(cx, |project_settings| {
13718 project_settings.lsp.insert(
13719 language_server_name.into(),
13720 LspSettings {
13721 binary: None,
13722 settings: None,
13723 initialization_options: Some(json!({
13724 "anotherInitValue": false
13725 })),
13726 enable_lsp_tasks: false,
13727 },
13728 );
13729 });
13730 cx.executor().run_until_parked();
13731 assert_eq!(
13732 server_restarts.load(atomic::Ordering::Acquire),
13733 1,
13734 "Should not restart LSP server on a related LSP settings change that is the same"
13735 );
13736
13737 update_test_project_settings(cx, |project_settings| {
13738 project_settings.lsp.insert(
13739 language_server_name.into(),
13740 LspSettings {
13741 binary: None,
13742 settings: None,
13743 initialization_options: None,
13744 enable_lsp_tasks: false,
13745 },
13746 );
13747 });
13748 cx.executor().run_until_parked();
13749 assert_eq!(
13750 server_restarts.load(atomic::Ordering::Acquire),
13751 2,
13752 "Should restart LSP server on another related LSP settings change"
13753 );
13754}
13755
13756#[gpui::test]
13757async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
13758 init_test(cx, |_| {});
13759
13760 let mut cx = EditorLspTestContext::new_rust(
13761 lsp::ServerCapabilities {
13762 completion_provider: Some(lsp::CompletionOptions {
13763 trigger_characters: Some(vec![".".to_string()]),
13764 resolve_provider: Some(true),
13765 ..Default::default()
13766 }),
13767 ..Default::default()
13768 },
13769 cx,
13770 )
13771 .await;
13772
13773 cx.set_state("fn main() { let a = 2ˇ; }");
13774 cx.simulate_keystroke(".");
13775 let completion_item = lsp::CompletionItem {
13776 label: "some".into(),
13777 kind: Some(lsp::CompletionItemKind::SNIPPET),
13778 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
13779 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
13780 kind: lsp::MarkupKind::Markdown,
13781 value: "```rust\nSome(2)\n```".to_string(),
13782 })),
13783 deprecated: Some(false),
13784 sort_text: Some("fffffff2".to_string()),
13785 filter_text: Some("some".to_string()),
13786 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
13787 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13788 range: lsp::Range {
13789 start: lsp::Position {
13790 line: 0,
13791 character: 22,
13792 },
13793 end: lsp::Position {
13794 line: 0,
13795 character: 22,
13796 },
13797 },
13798 new_text: "Some(2)".to_string(),
13799 })),
13800 additional_text_edits: Some(vec![lsp::TextEdit {
13801 range: lsp::Range {
13802 start: lsp::Position {
13803 line: 0,
13804 character: 20,
13805 },
13806 end: lsp::Position {
13807 line: 0,
13808 character: 22,
13809 },
13810 },
13811 new_text: "".to_string(),
13812 }]),
13813 ..Default::default()
13814 };
13815
13816 let closure_completion_item = completion_item.clone();
13817 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13818 let task_completion_item = closure_completion_item.clone();
13819 async move {
13820 Ok(Some(lsp::CompletionResponse::Array(vec![
13821 task_completion_item,
13822 ])))
13823 }
13824 });
13825
13826 request.next().await;
13827
13828 cx.condition(|editor, _| editor.context_menu_visible())
13829 .await;
13830 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13831 editor
13832 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13833 .unwrap()
13834 });
13835 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
13836
13837 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
13838 let task_completion_item = completion_item.clone();
13839 async move { Ok(task_completion_item) }
13840 })
13841 .next()
13842 .await
13843 .unwrap();
13844 apply_additional_edits.await.unwrap();
13845 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
13846}
13847
13848#[gpui::test]
13849async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
13850 init_test(cx, |_| {});
13851
13852 let mut cx = EditorLspTestContext::new_rust(
13853 lsp::ServerCapabilities {
13854 completion_provider: Some(lsp::CompletionOptions {
13855 trigger_characters: Some(vec![".".to_string()]),
13856 resolve_provider: Some(true),
13857 ..Default::default()
13858 }),
13859 ..Default::default()
13860 },
13861 cx,
13862 )
13863 .await;
13864
13865 cx.set_state("fn main() { let a = 2ˇ; }");
13866 cx.simulate_keystroke(".");
13867
13868 let item1 = lsp::CompletionItem {
13869 label: "method id()".to_string(),
13870 filter_text: Some("id".to_string()),
13871 detail: None,
13872 documentation: None,
13873 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13874 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13875 new_text: ".id".to_string(),
13876 })),
13877 ..lsp::CompletionItem::default()
13878 };
13879
13880 let item2 = lsp::CompletionItem {
13881 label: "other".to_string(),
13882 filter_text: Some("other".to_string()),
13883 detail: None,
13884 documentation: None,
13885 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13886 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13887 new_text: ".other".to_string(),
13888 })),
13889 ..lsp::CompletionItem::default()
13890 };
13891
13892 let item1 = item1.clone();
13893 cx.set_request_handler::<lsp::request::Completion, _, _>({
13894 let item1 = item1.clone();
13895 move |_, _, _| {
13896 let item1 = item1.clone();
13897 let item2 = item2.clone();
13898 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
13899 }
13900 })
13901 .next()
13902 .await;
13903
13904 cx.condition(|editor, _| editor.context_menu_visible())
13905 .await;
13906 cx.update_editor(|editor, _, _| {
13907 let context_menu = editor.context_menu.borrow_mut();
13908 let context_menu = context_menu
13909 .as_ref()
13910 .expect("Should have the context menu deployed");
13911 match context_menu {
13912 CodeContextMenu::Completions(completions_menu) => {
13913 let completions = completions_menu.completions.borrow_mut();
13914 assert_eq!(
13915 completions
13916 .iter()
13917 .map(|completion| &completion.label.text)
13918 .collect::<Vec<_>>(),
13919 vec!["method id()", "other"]
13920 )
13921 }
13922 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
13923 }
13924 });
13925
13926 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
13927 let item1 = item1.clone();
13928 move |_, item_to_resolve, _| {
13929 let item1 = item1.clone();
13930 async move {
13931 if item1 == item_to_resolve {
13932 Ok(lsp::CompletionItem {
13933 label: "method id()".to_string(),
13934 filter_text: Some("id".to_string()),
13935 detail: Some("Now resolved!".to_string()),
13936 documentation: Some(lsp::Documentation::String("Docs".to_string())),
13937 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13938 range: lsp::Range::new(
13939 lsp::Position::new(0, 22),
13940 lsp::Position::new(0, 22),
13941 ),
13942 new_text: ".id".to_string(),
13943 })),
13944 ..lsp::CompletionItem::default()
13945 })
13946 } else {
13947 Ok(item_to_resolve)
13948 }
13949 }
13950 }
13951 })
13952 .next()
13953 .await
13954 .unwrap();
13955 cx.run_until_parked();
13956
13957 cx.update_editor(|editor, window, cx| {
13958 editor.context_menu_next(&Default::default(), window, cx);
13959 });
13960
13961 cx.update_editor(|editor, _, _| {
13962 let context_menu = editor.context_menu.borrow_mut();
13963 let context_menu = context_menu
13964 .as_ref()
13965 .expect("Should have the context menu deployed");
13966 match context_menu {
13967 CodeContextMenu::Completions(completions_menu) => {
13968 let completions = completions_menu.completions.borrow_mut();
13969 assert_eq!(
13970 completions
13971 .iter()
13972 .map(|completion| &completion.label.text)
13973 .collect::<Vec<_>>(),
13974 vec!["method id() Now resolved!", "other"],
13975 "Should update first completion label, but not second as the filter text did not match."
13976 );
13977 }
13978 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
13979 }
13980 });
13981}
13982
13983#[gpui::test]
13984async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
13985 init_test(cx, |_| {});
13986
13987 let mut cx = EditorLspTestContext::new_rust(
13988 lsp::ServerCapabilities {
13989 completion_provider: Some(lsp::CompletionOptions {
13990 trigger_characters: Some(vec![".".to_string()]),
13991 resolve_provider: Some(true),
13992 ..Default::default()
13993 }),
13994 ..Default::default()
13995 },
13996 cx,
13997 )
13998 .await;
13999
14000 cx.set_state("fn main() { let a = 2ˇ; }");
14001 cx.simulate_keystroke(".");
14002
14003 let unresolved_item_1 = lsp::CompletionItem {
14004 label: "id".to_string(),
14005 filter_text: Some("id".to_string()),
14006 detail: None,
14007 documentation: None,
14008 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14009 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14010 new_text: ".id".to_string(),
14011 })),
14012 ..lsp::CompletionItem::default()
14013 };
14014 let resolved_item_1 = lsp::CompletionItem {
14015 additional_text_edits: Some(vec![lsp::TextEdit {
14016 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
14017 new_text: "!!".to_string(),
14018 }]),
14019 ..unresolved_item_1.clone()
14020 };
14021 let unresolved_item_2 = lsp::CompletionItem {
14022 label: "other".to_string(),
14023 filter_text: Some("other".to_string()),
14024 detail: None,
14025 documentation: None,
14026 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14027 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14028 new_text: ".other".to_string(),
14029 })),
14030 ..lsp::CompletionItem::default()
14031 };
14032 let resolved_item_2 = lsp::CompletionItem {
14033 additional_text_edits: Some(vec![lsp::TextEdit {
14034 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
14035 new_text: "??".to_string(),
14036 }]),
14037 ..unresolved_item_2.clone()
14038 };
14039
14040 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
14041 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
14042 cx.lsp
14043 .server
14044 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
14045 let unresolved_item_1 = unresolved_item_1.clone();
14046 let resolved_item_1 = resolved_item_1.clone();
14047 let unresolved_item_2 = unresolved_item_2.clone();
14048 let resolved_item_2 = resolved_item_2.clone();
14049 let resolve_requests_1 = resolve_requests_1.clone();
14050 let resolve_requests_2 = resolve_requests_2.clone();
14051 move |unresolved_request, _| {
14052 let unresolved_item_1 = unresolved_item_1.clone();
14053 let resolved_item_1 = resolved_item_1.clone();
14054 let unresolved_item_2 = unresolved_item_2.clone();
14055 let resolved_item_2 = resolved_item_2.clone();
14056 let resolve_requests_1 = resolve_requests_1.clone();
14057 let resolve_requests_2 = resolve_requests_2.clone();
14058 async move {
14059 if unresolved_request == unresolved_item_1 {
14060 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
14061 Ok(resolved_item_1.clone())
14062 } else if unresolved_request == unresolved_item_2 {
14063 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
14064 Ok(resolved_item_2.clone())
14065 } else {
14066 panic!("Unexpected completion item {unresolved_request:?}")
14067 }
14068 }
14069 }
14070 })
14071 .detach();
14072
14073 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14074 let unresolved_item_1 = unresolved_item_1.clone();
14075 let unresolved_item_2 = unresolved_item_2.clone();
14076 async move {
14077 Ok(Some(lsp::CompletionResponse::Array(vec![
14078 unresolved_item_1,
14079 unresolved_item_2,
14080 ])))
14081 }
14082 })
14083 .next()
14084 .await;
14085
14086 cx.condition(|editor, _| editor.context_menu_visible())
14087 .await;
14088 cx.update_editor(|editor, _, _| {
14089 let context_menu = editor.context_menu.borrow_mut();
14090 let context_menu = context_menu
14091 .as_ref()
14092 .expect("Should have the context menu deployed");
14093 match context_menu {
14094 CodeContextMenu::Completions(completions_menu) => {
14095 let completions = completions_menu.completions.borrow_mut();
14096 assert_eq!(
14097 completions
14098 .iter()
14099 .map(|completion| &completion.label.text)
14100 .collect::<Vec<_>>(),
14101 vec!["id", "other"]
14102 )
14103 }
14104 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
14105 }
14106 });
14107 cx.run_until_parked();
14108
14109 cx.update_editor(|editor, window, cx| {
14110 editor.context_menu_next(&ContextMenuNext, window, cx);
14111 });
14112 cx.run_until_parked();
14113 cx.update_editor(|editor, window, cx| {
14114 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
14115 });
14116 cx.run_until_parked();
14117 cx.update_editor(|editor, window, cx| {
14118 editor.context_menu_next(&ContextMenuNext, window, cx);
14119 });
14120 cx.run_until_parked();
14121 cx.update_editor(|editor, window, cx| {
14122 editor
14123 .compose_completion(&ComposeCompletion::default(), window, cx)
14124 .expect("No task returned")
14125 })
14126 .await
14127 .expect("Completion failed");
14128 cx.run_until_parked();
14129
14130 cx.update_editor(|editor, _, cx| {
14131 assert_eq!(
14132 resolve_requests_1.load(atomic::Ordering::Acquire),
14133 1,
14134 "Should always resolve once despite multiple selections"
14135 );
14136 assert_eq!(
14137 resolve_requests_2.load(atomic::Ordering::Acquire),
14138 1,
14139 "Should always resolve once after multiple selections and applying the completion"
14140 );
14141 assert_eq!(
14142 editor.text(cx),
14143 "fn main() { let a = ??.other; }",
14144 "Should use resolved data when applying the completion"
14145 );
14146 });
14147}
14148
14149#[gpui::test]
14150async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
14151 init_test(cx, |_| {});
14152
14153 let item_0 = lsp::CompletionItem {
14154 label: "abs".into(),
14155 insert_text: Some("abs".into()),
14156 data: Some(json!({ "very": "special"})),
14157 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
14158 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
14159 lsp::InsertReplaceEdit {
14160 new_text: "abs".to_string(),
14161 insert: lsp::Range::default(),
14162 replace: lsp::Range::default(),
14163 },
14164 )),
14165 ..lsp::CompletionItem::default()
14166 };
14167 let items = iter::once(item_0.clone())
14168 .chain((11..51).map(|i| lsp::CompletionItem {
14169 label: format!("item_{}", i),
14170 insert_text: Some(format!("item_{}", i)),
14171 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
14172 ..lsp::CompletionItem::default()
14173 }))
14174 .collect::<Vec<_>>();
14175
14176 let default_commit_characters = vec!["?".to_string()];
14177 let default_data = json!({ "default": "data"});
14178 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
14179 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
14180 let default_edit_range = lsp::Range {
14181 start: lsp::Position {
14182 line: 0,
14183 character: 5,
14184 },
14185 end: lsp::Position {
14186 line: 0,
14187 character: 5,
14188 },
14189 };
14190
14191 let mut cx = EditorLspTestContext::new_rust(
14192 lsp::ServerCapabilities {
14193 completion_provider: Some(lsp::CompletionOptions {
14194 trigger_characters: Some(vec![".".to_string()]),
14195 resolve_provider: Some(true),
14196 ..Default::default()
14197 }),
14198 ..Default::default()
14199 },
14200 cx,
14201 )
14202 .await;
14203
14204 cx.set_state("fn main() { let a = 2ˇ; }");
14205 cx.simulate_keystroke(".");
14206
14207 let completion_data = default_data.clone();
14208 let completion_characters = default_commit_characters.clone();
14209 let completion_items = items.clone();
14210 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14211 let default_data = completion_data.clone();
14212 let default_commit_characters = completion_characters.clone();
14213 let items = completion_items.clone();
14214 async move {
14215 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
14216 items,
14217 item_defaults: Some(lsp::CompletionListItemDefaults {
14218 data: Some(default_data.clone()),
14219 commit_characters: Some(default_commit_characters.clone()),
14220 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
14221 default_edit_range,
14222 )),
14223 insert_text_format: Some(default_insert_text_format),
14224 insert_text_mode: Some(default_insert_text_mode),
14225 }),
14226 ..lsp::CompletionList::default()
14227 })))
14228 }
14229 })
14230 .next()
14231 .await;
14232
14233 let resolved_items = Arc::new(Mutex::new(Vec::new()));
14234 cx.lsp
14235 .server
14236 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
14237 let closure_resolved_items = resolved_items.clone();
14238 move |item_to_resolve, _| {
14239 let closure_resolved_items = closure_resolved_items.clone();
14240 async move {
14241 closure_resolved_items.lock().push(item_to_resolve.clone());
14242 Ok(item_to_resolve)
14243 }
14244 }
14245 })
14246 .detach();
14247
14248 cx.condition(|editor, _| editor.context_menu_visible())
14249 .await;
14250 cx.run_until_parked();
14251 cx.update_editor(|editor, _, _| {
14252 let menu = editor.context_menu.borrow_mut();
14253 match menu.as_ref().expect("should have the completions menu") {
14254 CodeContextMenu::Completions(completions_menu) => {
14255 assert_eq!(
14256 completions_menu
14257 .entries
14258 .borrow()
14259 .iter()
14260 .map(|mat| mat.string.clone())
14261 .collect::<Vec<String>>(),
14262 items
14263 .iter()
14264 .map(|completion| completion.label.clone())
14265 .collect::<Vec<String>>()
14266 );
14267 }
14268 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
14269 }
14270 });
14271 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
14272 // with 4 from the end.
14273 assert_eq!(
14274 *resolved_items.lock(),
14275 [&items[0..16], &items[items.len() - 4..items.len()]]
14276 .concat()
14277 .iter()
14278 .cloned()
14279 .map(|mut item| {
14280 if item.data.is_none() {
14281 item.data = Some(default_data.clone());
14282 }
14283 item
14284 })
14285 .collect::<Vec<lsp::CompletionItem>>(),
14286 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
14287 );
14288 resolved_items.lock().clear();
14289
14290 cx.update_editor(|editor, window, cx| {
14291 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
14292 });
14293 cx.run_until_parked();
14294 // Completions that have already been resolved are skipped.
14295 assert_eq!(
14296 *resolved_items.lock(),
14297 items[items.len() - 16..items.len() - 4]
14298 .iter()
14299 .cloned()
14300 .map(|mut item| {
14301 if item.data.is_none() {
14302 item.data = Some(default_data.clone());
14303 }
14304 item
14305 })
14306 .collect::<Vec<lsp::CompletionItem>>()
14307 );
14308 resolved_items.lock().clear();
14309}
14310
14311#[gpui::test]
14312async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
14313 init_test(cx, |_| {});
14314
14315 let mut cx = EditorLspTestContext::new(
14316 Language::new(
14317 LanguageConfig {
14318 matcher: LanguageMatcher {
14319 path_suffixes: vec!["jsx".into()],
14320 ..Default::default()
14321 },
14322 overrides: [(
14323 "element".into(),
14324 LanguageConfigOverride {
14325 completion_query_characters: Override::Set(['-'].into_iter().collect()),
14326 ..Default::default()
14327 },
14328 )]
14329 .into_iter()
14330 .collect(),
14331 ..Default::default()
14332 },
14333 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
14334 )
14335 .with_override_query("(jsx_self_closing_element) @element")
14336 .unwrap(),
14337 lsp::ServerCapabilities {
14338 completion_provider: Some(lsp::CompletionOptions {
14339 trigger_characters: Some(vec![":".to_string()]),
14340 ..Default::default()
14341 }),
14342 ..Default::default()
14343 },
14344 cx,
14345 )
14346 .await;
14347
14348 cx.lsp
14349 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14350 Ok(Some(lsp::CompletionResponse::Array(vec![
14351 lsp::CompletionItem {
14352 label: "bg-blue".into(),
14353 ..Default::default()
14354 },
14355 lsp::CompletionItem {
14356 label: "bg-red".into(),
14357 ..Default::default()
14358 },
14359 lsp::CompletionItem {
14360 label: "bg-yellow".into(),
14361 ..Default::default()
14362 },
14363 ])))
14364 });
14365
14366 cx.set_state(r#"<p class="bgˇ" />"#);
14367
14368 // Trigger completion when typing a dash, because the dash is an extra
14369 // word character in the 'element' scope, which contains the cursor.
14370 cx.simulate_keystroke("-");
14371 cx.executor().run_until_parked();
14372 cx.update_editor(|editor, _, _| {
14373 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14374 {
14375 assert_eq!(
14376 completion_menu_entries(&menu),
14377 &["bg-red", "bg-blue", "bg-yellow"]
14378 );
14379 } else {
14380 panic!("expected completion menu to be open");
14381 }
14382 });
14383
14384 cx.simulate_keystroke("l");
14385 cx.executor().run_until_parked();
14386 cx.update_editor(|editor, _, _| {
14387 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14388 {
14389 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
14390 } else {
14391 panic!("expected completion menu to be open");
14392 }
14393 });
14394
14395 // When filtering completions, consider the character after the '-' to
14396 // be the start of a subword.
14397 cx.set_state(r#"<p class="yelˇ" />"#);
14398 cx.simulate_keystroke("l");
14399 cx.executor().run_until_parked();
14400 cx.update_editor(|editor, _, _| {
14401 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14402 {
14403 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
14404 } else {
14405 panic!("expected completion menu to be open");
14406 }
14407 });
14408}
14409
14410fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
14411 let entries = menu.entries.borrow();
14412 entries.iter().map(|mat| mat.string.clone()).collect()
14413}
14414
14415#[gpui::test]
14416async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
14417 init_test(cx, |settings| {
14418 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
14419 FormatterList(vec![Formatter::Prettier].into()),
14420 ))
14421 });
14422
14423 let fs = FakeFs::new(cx.executor());
14424 fs.insert_file(path!("/file.ts"), Default::default()).await;
14425
14426 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
14427 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14428
14429 language_registry.add(Arc::new(Language::new(
14430 LanguageConfig {
14431 name: "TypeScript".into(),
14432 matcher: LanguageMatcher {
14433 path_suffixes: vec!["ts".to_string()],
14434 ..Default::default()
14435 },
14436 ..Default::default()
14437 },
14438 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
14439 )));
14440 update_test_language_settings(cx, |settings| {
14441 settings.defaults.prettier = Some(PrettierSettings {
14442 allowed: true,
14443 ..PrettierSettings::default()
14444 });
14445 });
14446
14447 let test_plugin = "test_plugin";
14448 let _ = language_registry.register_fake_lsp(
14449 "TypeScript",
14450 FakeLspAdapter {
14451 prettier_plugins: vec![test_plugin],
14452 ..Default::default()
14453 },
14454 );
14455
14456 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
14457 let buffer = project
14458 .update(cx, |project, cx| {
14459 project.open_local_buffer(path!("/file.ts"), cx)
14460 })
14461 .await
14462 .unwrap();
14463
14464 let buffer_text = "one\ntwo\nthree\n";
14465 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
14466 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
14467 editor.update_in(cx, |editor, window, cx| {
14468 editor.set_text(buffer_text, window, cx)
14469 });
14470
14471 editor
14472 .update_in(cx, |editor, window, cx| {
14473 editor.perform_format(
14474 project.clone(),
14475 FormatTrigger::Manual,
14476 FormatTarget::Buffers,
14477 window,
14478 cx,
14479 )
14480 })
14481 .unwrap()
14482 .await;
14483 assert_eq!(
14484 editor.update(cx, |editor, cx| editor.text(cx)),
14485 buffer_text.to_string() + prettier_format_suffix,
14486 "Test prettier formatting was not applied to the original buffer text",
14487 );
14488
14489 update_test_language_settings(cx, |settings| {
14490 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
14491 });
14492 let format = editor.update_in(cx, |editor, window, cx| {
14493 editor.perform_format(
14494 project.clone(),
14495 FormatTrigger::Manual,
14496 FormatTarget::Buffers,
14497 window,
14498 cx,
14499 )
14500 });
14501 format.await.unwrap();
14502 assert_eq!(
14503 editor.update(cx, |editor, cx| editor.text(cx)),
14504 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
14505 "Autoformatting (via test prettier) was not applied to the original buffer text",
14506 );
14507}
14508
14509#[gpui::test]
14510async fn test_addition_reverts(cx: &mut TestAppContext) {
14511 init_test(cx, |_| {});
14512 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14513 let base_text = indoc! {r#"
14514 struct Row;
14515 struct Row1;
14516 struct Row2;
14517
14518 struct Row4;
14519 struct Row5;
14520 struct Row6;
14521
14522 struct Row8;
14523 struct Row9;
14524 struct Row10;"#};
14525
14526 // When addition hunks are not adjacent to carets, no hunk revert is performed
14527 assert_hunk_revert(
14528 indoc! {r#"struct Row;
14529 struct Row1;
14530 struct Row1.1;
14531 struct Row1.2;
14532 struct Row2;ˇ
14533
14534 struct Row4;
14535 struct Row5;
14536 struct Row6;
14537
14538 struct Row8;
14539 ˇstruct Row9;
14540 struct Row9.1;
14541 struct Row9.2;
14542 struct Row9.3;
14543 struct Row10;"#},
14544 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
14545 indoc! {r#"struct Row;
14546 struct Row1;
14547 struct Row1.1;
14548 struct Row1.2;
14549 struct Row2;ˇ
14550
14551 struct Row4;
14552 struct Row5;
14553 struct Row6;
14554
14555 struct Row8;
14556 ˇstruct Row9;
14557 struct Row9.1;
14558 struct Row9.2;
14559 struct Row9.3;
14560 struct Row10;"#},
14561 base_text,
14562 &mut cx,
14563 );
14564 // Same for selections
14565 assert_hunk_revert(
14566 indoc! {r#"struct Row;
14567 struct Row1;
14568 struct Row2;
14569 struct Row2.1;
14570 struct Row2.2;
14571 «ˇ
14572 struct Row4;
14573 struct» Row5;
14574 «struct Row6;
14575 ˇ»
14576 struct Row9.1;
14577 struct Row9.2;
14578 struct Row9.3;
14579 struct Row8;
14580 struct Row9;
14581 struct Row10;"#},
14582 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
14583 indoc! {r#"struct Row;
14584 struct Row1;
14585 struct Row2;
14586 struct Row2.1;
14587 struct Row2.2;
14588 «ˇ
14589 struct Row4;
14590 struct» Row5;
14591 «struct Row6;
14592 ˇ»
14593 struct Row9.1;
14594 struct Row9.2;
14595 struct Row9.3;
14596 struct Row8;
14597 struct Row9;
14598 struct Row10;"#},
14599 base_text,
14600 &mut cx,
14601 );
14602
14603 // When carets and selections intersect the addition hunks, those are reverted.
14604 // Adjacent carets got merged.
14605 assert_hunk_revert(
14606 indoc! {r#"struct Row;
14607 ˇ// something on the top
14608 struct Row1;
14609 struct Row2;
14610 struct Roˇw3.1;
14611 struct Row2.2;
14612 struct Row2.3;ˇ
14613
14614 struct Row4;
14615 struct ˇRow5.1;
14616 struct Row5.2;
14617 struct «Rowˇ»5.3;
14618 struct Row5;
14619 struct Row6;
14620 ˇ
14621 struct Row9.1;
14622 struct «Rowˇ»9.2;
14623 struct «ˇRow»9.3;
14624 struct Row8;
14625 struct Row9;
14626 «ˇ// something on bottom»
14627 struct Row10;"#},
14628 vec![
14629 DiffHunkStatusKind::Added,
14630 DiffHunkStatusKind::Added,
14631 DiffHunkStatusKind::Added,
14632 DiffHunkStatusKind::Added,
14633 DiffHunkStatusKind::Added,
14634 ],
14635 indoc! {r#"struct Row;
14636 ˇstruct Row1;
14637 struct Row2;
14638 ˇ
14639 struct Row4;
14640 ˇstruct Row5;
14641 struct Row6;
14642 ˇ
14643 ˇstruct Row8;
14644 struct Row9;
14645 ˇstruct Row10;"#},
14646 base_text,
14647 &mut cx,
14648 );
14649}
14650
14651#[gpui::test]
14652async fn test_modification_reverts(cx: &mut TestAppContext) {
14653 init_test(cx, |_| {});
14654 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14655 let base_text = indoc! {r#"
14656 struct Row;
14657 struct Row1;
14658 struct Row2;
14659
14660 struct Row4;
14661 struct Row5;
14662 struct Row6;
14663
14664 struct Row8;
14665 struct Row9;
14666 struct Row10;"#};
14667
14668 // Modification hunks behave the same as the addition ones.
14669 assert_hunk_revert(
14670 indoc! {r#"struct Row;
14671 struct Row1;
14672 struct Row33;
14673 ˇ
14674 struct Row4;
14675 struct Row5;
14676 struct Row6;
14677 ˇ
14678 struct Row99;
14679 struct Row9;
14680 struct Row10;"#},
14681 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
14682 indoc! {r#"struct Row;
14683 struct Row1;
14684 struct Row33;
14685 ˇ
14686 struct Row4;
14687 struct Row5;
14688 struct Row6;
14689 ˇ
14690 struct Row99;
14691 struct Row9;
14692 struct Row10;"#},
14693 base_text,
14694 &mut cx,
14695 );
14696 assert_hunk_revert(
14697 indoc! {r#"struct Row;
14698 struct Row1;
14699 struct Row33;
14700 «ˇ
14701 struct Row4;
14702 struct» Row5;
14703 «struct Row6;
14704 ˇ»
14705 struct Row99;
14706 struct Row9;
14707 struct Row10;"#},
14708 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
14709 indoc! {r#"struct Row;
14710 struct Row1;
14711 struct Row33;
14712 «ˇ
14713 struct Row4;
14714 struct» Row5;
14715 «struct Row6;
14716 ˇ»
14717 struct Row99;
14718 struct Row9;
14719 struct Row10;"#},
14720 base_text,
14721 &mut cx,
14722 );
14723
14724 assert_hunk_revert(
14725 indoc! {r#"ˇstruct Row1.1;
14726 struct Row1;
14727 «ˇstr»uct Row22;
14728
14729 struct ˇRow44;
14730 struct Row5;
14731 struct «Rˇ»ow66;ˇ
14732
14733 «struˇ»ct Row88;
14734 struct Row9;
14735 struct Row1011;ˇ"#},
14736 vec![
14737 DiffHunkStatusKind::Modified,
14738 DiffHunkStatusKind::Modified,
14739 DiffHunkStatusKind::Modified,
14740 DiffHunkStatusKind::Modified,
14741 DiffHunkStatusKind::Modified,
14742 DiffHunkStatusKind::Modified,
14743 ],
14744 indoc! {r#"struct Row;
14745 ˇstruct Row1;
14746 struct Row2;
14747 ˇ
14748 struct Row4;
14749 ˇstruct Row5;
14750 struct Row6;
14751 ˇ
14752 struct Row8;
14753 ˇstruct Row9;
14754 struct Row10;ˇ"#},
14755 base_text,
14756 &mut cx,
14757 );
14758}
14759
14760#[gpui::test]
14761async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
14762 init_test(cx, |_| {});
14763 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14764 let base_text = indoc! {r#"
14765 one
14766
14767 two
14768 three
14769 "#};
14770
14771 cx.set_head_text(base_text);
14772 cx.set_state("\nˇ\n");
14773 cx.executor().run_until_parked();
14774 cx.update_editor(|editor, _window, cx| {
14775 editor.expand_selected_diff_hunks(cx);
14776 });
14777 cx.executor().run_until_parked();
14778 cx.update_editor(|editor, window, cx| {
14779 editor.backspace(&Default::default(), window, cx);
14780 });
14781 cx.run_until_parked();
14782 cx.assert_state_with_diff(
14783 indoc! {r#"
14784
14785 - two
14786 - threeˇ
14787 +
14788 "#}
14789 .to_string(),
14790 );
14791}
14792
14793#[gpui::test]
14794async fn test_deletion_reverts(cx: &mut TestAppContext) {
14795 init_test(cx, |_| {});
14796 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14797 let base_text = indoc! {r#"struct Row;
14798struct Row1;
14799struct Row2;
14800
14801struct Row4;
14802struct Row5;
14803struct Row6;
14804
14805struct Row8;
14806struct Row9;
14807struct Row10;"#};
14808
14809 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
14810 assert_hunk_revert(
14811 indoc! {r#"struct Row;
14812 struct Row2;
14813
14814 ˇstruct Row4;
14815 struct Row5;
14816 struct Row6;
14817 ˇ
14818 struct Row8;
14819 struct Row10;"#},
14820 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
14821 indoc! {r#"struct Row;
14822 struct Row2;
14823
14824 ˇstruct Row4;
14825 struct Row5;
14826 struct Row6;
14827 ˇ
14828 struct Row8;
14829 struct Row10;"#},
14830 base_text,
14831 &mut cx,
14832 );
14833 assert_hunk_revert(
14834 indoc! {r#"struct Row;
14835 struct Row2;
14836
14837 «ˇstruct Row4;
14838 struct» Row5;
14839 «struct Row6;
14840 ˇ»
14841 struct Row8;
14842 struct Row10;"#},
14843 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
14844 indoc! {r#"struct Row;
14845 struct Row2;
14846
14847 «ˇstruct Row4;
14848 struct» Row5;
14849 «struct Row6;
14850 ˇ»
14851 struct Row8;
14852 struct Row10;"#},
14853 base_text,
14854 &mut cx,
14855 );
14856
14857 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
14858 assert_hunk_revert(
14859 indoc! {r#"struct Row;
14860 ˇstruct Row2;
14861
14862 struct Row4;
14863 struct Row5;
14864 struct Row6;
14865
14866 struct Row8;ˇ
14867 struct Row10;"#},
14868 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
14869 indoc! {r#"struct Row;
14870 struct Row1;
14871 ˇstruct Row2;
14872
14873 struct Row4;
14874 struct Row5;
14875 struct Row6;
14876
14877 struct Row8;ˇ
14878 struct Row9;
14879 struct Row10;"#},
14880 base_text,
14881 &mut cx,
14882 );
14883 assert_hunk_revert(
14884 indoc! {r#"struct Row;
14885 struct Row2«ˇ;
14886 struct Row4;
14887 struct» Row5;
14888 «struct Row6;
14889
14890 struct Row8;ˇ»
14891 struct Row10;"#},
14892 vec![
14893 DiffHunkStatusKind::Deleted,
14894 DiffHunkStatusKind::Deleted,
14895 DiffHunkStatusKind::Deleted,
14896 ],
14897 indoc! {r#"struct Row;
14898 struct Row1;
14899 struct Row2«ˇ;
14900
14901 struct Row4;
14902 struct» Row5;
14903 «struct Row6;
14904
14905 struct Row8;ˇ»
14906 struct Row9;
14907 struct Row10;"#},
14908 base_text,
14909 &mut cx,
14910 );
14911}
14912
14913#[gpui::test]
14914async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
14915 init_test(cx, |_| {});
14916
14917 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
14918 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
14919 let base_text_3 =
14920 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
14921
14922 let text_1 = edit_first_char_of_every_line(base_text_1);
14923 let text_2 = edit_first_char_of_every_line(base_text_2);
14924 let text_3 = edit_first_char_of_every_line(base_text_3);
14925
14926 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
14927 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
14928 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
14929
14930 let multibuffer = cx.new(|cx| {
14931 let mut multibuffer = MultiBuffer::new(ReadWrite);
14932 multibuffer.push_excerpts(
14933 buffer_1.clone(),
14934 [
14935 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14936 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14937 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14938 ],
14939 cx,
14940 );
14941 multibuffer.push_excerpts(
14942 buffer_2.clone(),
14943 [
14944 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14945 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14946 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14947 ],
14948 cx,
14949 );
14950 multibuffer.push_excerpts(
14951 buffer_3.clone(),
14952 [
14953 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14954 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14955 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14956 ],
14957 cx,
14958 );
14959 multibuffer
14960 });
14961
14962 let fs = FakeFs::new(cx.executor());
14963 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
14964 let (editor, cx) = cx
14965 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
14966 editor.update_in(cx, |editor, _window, cx| {
14967 for (buffer, diff_base) in [
14968 (buffer_1.clone(), base_text_1),
14969 (buffer_2.clone(), base_text_2),
14970 (buffer_3.clone(), base_text_3),
14971 ] {
14972 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
14973 editor
14974 .buffer
14975 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
14976 }
14977 });
14978 cx.executor().run_until_parked();
14979
14980 editor.update_in(cx, |editor, window, cx| {
14981 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}");
14982 editor.select_all(&SelectAll, window, cx);
14983 editor.git_restore(&Default::default(), window, cx);
14984 });
14985 cx.executor().run_until_parked();
14986
14987 // When all ranges are selected, all buffer hunks are reverted.
14988 editor.update(cx, |editor, cx| {
14989 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");
14990 });
14991 buffer_1.update(cx, |buffer, _| {
14992 assert_eq!(buffer.text(), base_text_1);
14993 });
14994 buffer_2.update(cx, |buffer, _| {
14995 assert_eq!(buffer.text(), base_text_2);
14996 });
14997 buffer_3.update(cx, |buffer, _| {
14998 assert_eq!(buffer.text(), base_text_3);
14999 });
15000
15001 editor.update_in(cx, |editor, window, cx| {
15002 editor.undo(&Default::default(), window, cx);
15003 });
15004
15005 editor.update_in(cx, |editor, window, cx| {
15006 editor.change_selections(None, window, cx, |s| {
15007 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
15008 });
15009 editor.git_restore(&Default::default(), window, cx);
15010 });
15011
15012 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
15013 // but not affect buffer_2 and its related excerpts.
15014 editor.update(cx, |editor, cx| {
15015 assert_eq!(
15016 editor.text(cx),
15017 "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}"
15018 );
15019 });
15020 buffer_1.update(cx, |buffer, _| {
15021 assert_eq!(buffer.text(), base_text_1);
15022 });
15023 buffer_2.update(cx, |buffer, _| {
15024 assert_eq!(
15025 buffer.text(),
15026 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
15027 );
15028 });
15029 buffer_3.update(cx, |buffer, _| {
15030 assert_eq!(
15031 buffer.text(),
15032 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
15033 );
15034 });
15035
15036 fn edit_first_char_of_every_line(text: &str) -> String {
15037 text.split('\n')
15038 .map(|line| format!("X{}", &line[1..]))
15039 .collect::<Vec<_>>()
15040 .join("\n")
15041 }
15042}
15043
15044#[gpui::test]
15045async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) {
15046 init_test(cx, |_| {});
15047
15048 let cols = 4;
15049 let rows = 10;
15050 let sample_text_1 = sample_text(rows, cols, 'a');
15051 assert_eq!(
15052 sample_text_1,
15053 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
15054 );
15055 let sample_text_2 = sample_text(rows, cols, 'l');
15056 assert_eq!(
15057 sample_text_2,
15058 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
15059 );
15060 let sample_text_3 = sample_text(rows, cols, 'v');
15061 assert_eq!(
15062 sample_text_3,
15063 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
15064 );
15065
15066 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
15067 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
15068 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
15069
15070 let multi_buffer = cx.new(|cx| {
15071 let mut multibuffer = MultiBuffer::new(ReadWrite);
15072 multibuffer.push_excerpts(
15073 buffer_1.clone(),
15074 [
15075 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15076 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15077 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15078 ],
15079 cx,
15080 );
15081 multibuffer.push_excerpts(
15082 buffer_2.clone(),
15083 [
15084 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15085 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15086 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15087 ],
15088 cx,
15089 );
15090 multibuffer.push_excerpts(
15091 buffer_3.clone(),
15092 [
15093 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15094 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15095 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15096 ],
15097 cx,
15098 );
15099 multibuffer
15100 });
15101
15102 let fs = FakeFs::new(cx.executor());
15103 fs.insert_tree(
15104 "/a",
15105 json!({
15106 "main.rs": sample_text_1,
15107 "other.rs": sample_text_2,
15108 "lib.rs": sample_text_3,
15109 }),
15110 )
15111 .await;
15112 let project = Project::test(fs, ["/a".as_ref()], cx).await;
15113 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15114 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
15115 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
15116 Editor::new(
15117 EditorMode::full(),
15118 multi_buffer,
15119 Some(project.clone()),
15120 window,
15121 cx,
15122 )
15123 });
15124 let multibuffer_item_id = workspace
15125 .update(cx, |workspace, window, cx| {
15126 assert!(
15127 workspace.active_item(cx).is_none(),
15128 "active item should be None before the first item is added"
15129 );
15130 workspace.add_item_to_active_pane(
15131 Box::new(multi_buffer_editor.clone()),
15132 None,
15133 true,
15134 window,
15135 cx,
15136 );
15137 let active_item = workspace
15138 .active_item(cx)
15139 .expect("should have an active item after adding the multi buffer");
15140 assert!(
15141 !active_item.is_singleton(cx),
15142 "A multi buffer was expected to active after adding"
15143 );
15144 active_item.item_id()
15145 })
15146 .unwrap();
15147 cx.executor().run_until_parked();
15148
15149 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15150 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
15151 s.select_ranges(Some(1..2))
15152 });
15153 editor.open_excerpts(&OpenExcerpts, window, cx);
15154 });
15155 cx.executor().run_until_parked();
15156 let first_item_id = workspace
15157 .update(cx, |workspace, window, cx| {
15158 let active_item = workspace
15159 .active_item(cx)
15160 .expect("should have an active item after navigating into the 1st buffer");
15161 let first_item_id = active_item.item_id();
15162 assert_ne!(
15163 first_item_id, multibuffer_item_id,
15164 "Should navigate into the 1st buffer and activate it"
15165 );
15166 assert!(
15167 active_item.is_singleton(cx),
15168 "New active item should be a singleton buffer"
15169 );
15170 assert_eq!(
15171 active_item
15172 .act_as::<Editor>(cx)
15173 .expect("should have navigated into an editor for the 1st buffer")
15174 .read(cx)
15175 .text(cx),
15176 sample_text_1
15177 );
15178
15179 workspace
15180 .go_back(workspace.active_pane().downgrade(), window, cx)
15181 .detach_and_log_err(cx);
15182
15183 first_item_id
15184 })
15185 .unwrap();
15186 cx.executor().run_until_parked();
15187 workspace
15188 .update(cx, |workspace, _, cx| {
15189 let active_item = workspace
15190 .active_item(cx)
15191 .expect("should have an active item after navigating back");
15192 assert_eq!(
15193 active_item.item_id(),
15194 multibuffer_item_id,
15195 "Should navigate back to the multi buffer"
15196 );
15197 assert!(!active_item.is_singleton(cx));
15198 })
15199 .unwrap();
15200
15201 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15202 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
15203 s.select_ranges(Some(39..40))
15204 });
15205 editor.open_excerpts(&OpenExcerpts, window, cx);
15206 });
15207 cx.executor().run_until_parked();
15208 let second_item_id = workspace
15209 .update(cx, |workspace, window, cx| {
15210 let active_item = workspace
15211 .active_item(cx)
15212 .expect("should have an active item after navigating into the 2nd buffer");
15213 let second_item_id = active_item.item_id();
15214 assert_ne!(
15215 second_item_id, multibuffer_item_id,
15216 "Should navigate away from the multibuffer"
15217 );
15218 assert_ne!(
15219 second_item_id, first_item_id,
15220 "Should navigate into the 2nd buffer and activate it"
15221 );
15222 assert!(
15223 active_item.is_singleton(cx),
15224 "New active item should be a singleton buffer"
15225 );
15226 assert_eq!(
15227 active_item
15228 .act_as::<Editor>(cx)
15229 .expect("should have navigated into an editor")
15230 .read(cx)
15231 .text(cx),
15232 sample_text_2
15233 );
15234
15235 workspace
15236 .go_back(workspace.active_pane().downgrade(), window, cx)
15237 .detach_and_log_err(cx);
15238
15239 second_item_id
15240 })
15241 .unwrap();
15242 cx.executor().run_until_parked();
15243 workspace
15244 .update(cx, |workspace, _, cx| {
15245 let active_item = workspace
15246 .active_item(cx)
15247 .expect("should have an active item after navigating back from the 2nd buffer");
15248 assert_eq!(
15249 active_item.item_id(),
15250 multibuffer_item_id,
15251 "Should navigate back from the 2nd buffer to the multi buffer"
15252 );
15253 assert!(!active_item.is_singleton(cx));
15254 })
15255 .unwrap();
15256
15257 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15258 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
15259 s.select_ranges(Some(70..70))
15260 });
15261 editor.open_excerpts(&OpenExcerpts, window, cx);
15262 });
15263 cx.executor().run_until_parked();
15264 workspace
15265 .update(cx, |workspace, window, cx| {
15266 let active_item = workspace
15267 .active_item(cx)
15268 .expect("should have an active item after navigating into the 3rd buffer");
15269 let third_item_id = active_item.item_id();
15270 assert_ne!(
15271 third_item_id, multibuffer_item_id,
15272 "Should navigate into the 3rd buffer and activate it"
15273 );
15274 assert_ne!(third_item_id, first_item_id);
15275 assert_ne!(third_item_id, second_item_id);
15276 assert!(
15277 active_item.is_singleton(cx),
15278 "New active item should be a singleton buffer"
15279 );
15280 assert_eq!(
15281 active_item
15282 .act_as::<Editor>(cx)
15283 .expect("should have navigated into an editor")
15284 .read(cx)
15285 .text(cx),
15286 sample_text_3
15287 );
15288
15289 workspace
15290 .go_back(workspace.active_pane().downgrade(), window, cx)
15291 .detach_and_log_err(cx);
15292 })
15293 .unwrap();
15294 cx.executor().run_until_parked();
15295 workspace
15296 .update(cx, |workspace, _, cx| {
15297 let active_item = workspace
15298 .active_item(cx)
15299 .expect("should have an active item after navigating back from the 3rd buffer");
15300 assert_eq!(
15301 active_item.item_id(),
15302 multibuffer_item_id,
15303 "Should navigate back from the 3rd buffer to the multi buffer"
15304 );
15305 assert!(!active_item.is_singleton(cx));
15306 })
15307 .unwrap();
15308}
15309
15310#[gpui::test]
15311async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
15312 init_test(cx, |_| {});
15313
15314 let mut cx = EditorTestContext::new(cx).await;
15315
15316 let diff_base = r#"
15317 use some::mod;
15318
15319 const A: u32 = 42;
15320
15321 fn main() {
15322 println!("hello");
15323
15324 println!("world");
15325 }
15326 "#
15327 .unindent();
15328
15329 cx.set_state(
15330 &r#"
15331 use some::modified;
15332
15333 ˇ
15334 fn main() {
15335 println!("hello there");
15336
15337 println!("around the");
15338 println!("world");
15339 }
15340 "#
15341 .unindent(),
15342 );
15343
15344 cx.set_head_text(&diff_base);
15345 executor.run_until_parked();
15346
15347 cx.update_editor(|editor, window, cx| {
15348 editor.go_to_next_hunk(&GoToHunk, window, cx);
15349 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15350 });
15351 executor.run_until_parked();
15352 cx.assert_state_with_diff(
15353 r#"
15354 use some::modified;
15355
15356
15357 fn main() {
15358 - println!("hello");
15359 + ˇ println!("hello there");
15360
15361 println!("around the");
15362 println!("world");
15363 }
15364 "#
15365 .unindent(),
15366 );
15367
15368 cx.update_editor(|editor, window, cx| {
15369 for _ in 0..2 {
15370 editor.go_to_next_hunk(&GoToHunk, window, cx);
15371 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15372 }
15373 });
15374 executor.run_until_parked();
15375 cx.assert_state_with_diff(
15376 r#"
15377 - use some::mod;
15378 + ˇuse some::modified;
15379
15380
15381 fn main() {
15382 - println!("hello");
15383 + println!("hello there");
15384
15385 + println!("around the");
15386 println!("world");
15387 }
15388 "#
15389 .unindent(),
15390 );
15391
15392 cx.update_editor(|editor, window, cx| {
15393 editor.go_to_next_hunk(&GoToHunk, window, cx);
15394 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15395 });
15396 executor.run_until_parked();
15397 cx.assert_state_with_diff(
15398 r#"
15399 - use some::mod;
15400 + use some::modified;
15401
15402 - const A: u32 = 42;
15403 ˇ
15404 fn main() {
15405 - println!("hello");
15406 + println!("hello there");
15407
15408 + println!("around the");
15409 println!("world");
15410 }
15411 "#
15412 .unindent(),
15413 );
15414
15415 cx.update_editor(|editor, window, cx| {
15416 editor.cancel(&Cancel, window, cx);
15417 });
15418
15419 cx.assert_state_with_diff(
15420 r#"
15421 use some::modified;
15422
15423 ˇ
15424 fn main() {
15425 println!("hello there");
15426
15427 println!("around the");
15428 println!("world");
15429 }
15430 "#
15431 .unindent(),
15432 );
15433}
15434
15435#[gpui::test]
15436async fn test_diff_base_change_with_expanded_diff_hunks(
15437 executor: BackgroundExecutor,
15438 cx: &mut TestAppContext,
15439) {
15440 init_test(cx, |_| {});
15441
15442 let mut cx = EditorTestContext::new(cx).await;
15443
15444 let diff_base = r#"
15445 use some::mod1;
15446 use some::mod2;
15447
15448 const A: u32 = 42;
15449 const B: u32 = 42;
15450 const C: u32 = 42;
15451
15452 fn main() {
15453 println!("hello");
15454
15455 println!("world");
15456 }
15457 "#
15458 .unindent();
15459
15460 cx.set_state(
15461 &r#"
15462 use some::mod2;
15463
15464 const A: u32 = 42;
15465 const C: u32 = 42;
15466
15467 fn main(ˇ) {
15468 //println!("hello");
15469
15470 println!("world");
15471 //
15472 //
15473 }
15474 "#
15475 .unindent(),
15476 );
15477
15478 cx.set_head_text(&diff_base);
15479 executor.run_until_parked();
15480
15481 cx.update_editor(|editor, window, cx| {
15482 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15483 });
15484 executor.run_until_parked();
15485 cx.assert_state_with_diff(
15486 r#"
15487 - use some::mod1;
15488 use some::mod2;
15489
15490 const A: u32 = 42;
15491 - const B: u32 = 42;
15492 const C: u32 = 42;
15493
15494 fn main(ˇ) {
15495 - println!("hello");
15496 + //println!("hello");
15497
15498 println!("world");
15499 + //
15500 + //
15501 }
15502 "#
15503 .unindent(),
15504 );
15505
15506 cx.set_head_text("new diff base!");
15507 executor.run_until_parked();
15508 cx.assert_state_with_diff(
15509 r#"
15510 - new diff base!
15511 + use some::mod2;
15512 +
15513 + const A: u32 = 42;
15514 + const C: u32 = 42;
15515 +
15516 + fn main(ˇ) {
15517 + //println!("hello");
15518 +
15519 + println!("world");
15520 + //
15521 + //
15522 + }
15523 "#
15524 .unindent(),
15525 );
15526}
15527
15528#[gpui::test]
15529async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
15530 init_test(cx, |_| {});
15531
15532 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
15533 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
15534 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
15535 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
15536 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
15537 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
15538
15539 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
15540 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
15541 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
15542
15543 let multi_buffer = cx.new(|cx| {
15544 let mut multibuffer = MultiBuffer::new(ReadWrite);
15545 multibuffer.push_excerpts(
15546 buffer_1.clone(),
15547 [
15548 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15549 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15550 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
15551 ],
15552 cx,
15553 );
15554 multibuffer.push_excerpts(
15555 buffer_2.clone(),
15556 [
15557 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15558 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15559 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
15560 ],
15561 cx,
15562 );
15563 multibuffer.push_excerpts(
15564 buffer_3.clone(),
15565 [
15566 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15567 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15568 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
15569 ],
15570 cx,
15571 );
15572 multibuffer
15573 });
15574
15575 let editor =
15576 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
15577 editor
15578 .update(cx, |editor, _window, cx| {
15579 for (buffer, diff_base) in [
15580 (buffer_1.clone(), file_1_old),
15581 (buffer_2.clone(), file_2_old),
15582 (buffer_3.clone(), file_3_old),
15583 ] {
15584 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
15585 editor
15586 .buffer
15587 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
15588 }
15589 })
15590 .unwrap();
15591
15592 let mut cx = EditorTestContext::for_editor(editor, cx).await;
15593 cx.run_until_parked();
15594
15595 cx.assert_editor_state(
15596 &"
15597 ˇaaa
15598 ccc
15599 ddd
15600
15601 ggg
15602 hhh
15603
15604
15605 lll
15606 mmm
15607 NNN
15608
15609 qqq
15610 rrr
15611
15612 uuu
15613 111
15614 222
15615 333
15616
15617 666
15618 777
15619
15620 000
15621 !!!"
15622 .unindent(),
15623 );
15624
15625 cx.update_editor(|editor, window, cx| {
15626 editor.select_all(&SelectAll, window, cx);
15627 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15628 });
15629 cx.executor().run_until_parked();
15630
15631 cx.assert_state_with_diff(
15632 "
15633 «aaa
15634 - bbb
15635 ccc
15636 ddd
15637
15638 ggg
15639 hhh
15640
15641
15642 lll
15643 mmm
15644 - nnn
15645 + NNN
15646
15647 qqq
15648 rrr
15649
15650 uuu
15651 111
15652 222
15653 333
15654
15655 + 666
15656 777
15657
15658 000
15659 !!!ˇ»"
15660 .unindent(),
15661 );
15662}
15663
15664#[gpui::test]
15665async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
15666 init_test(cx, |_| {});
15667
15668 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
15669 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
15670
15671 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
15672 let multi_buffer = cx.new(|cx| {
15673 let mut multibuffer = MultiBuffer::new(ReadWrite);
15674 multibuffer.push_excerpts(
15675 buffer.clone(),
15676 [
15677 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
15678 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
15679 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
15680 ],
15681 cx,
15682 );
15683 multibuffer
15684 });
15685
15686 let editor =
15687 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
15688 editor
15689 .update(cx, |editor, _window, cx| {
15690 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
15691 editor
15692 .buffer
15693 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
15694 })
15695 .unwrap();
15696
15697 let mut cx = EditorTestContext::for_editor(editor, cx).await;
15698 cx.run_until_parked();
15699
15700 cx.update_editor(|editor, window, cx| {
15701 editor.expand_all_diff_hunks(&Default::default(), window, cx)
15702 });
15703 cx.executor().run_until_parked();
15704
15705 // When the start of a hunk coincides with the start of its excerpt,
15706 // the hunk is expanded. When the start of a a hunk is earlier than
15707 // the start of its excerpt, the hunk is not expanded.
15708 cx.assert_state_with_diff(
15709 "
15710 ˇaaa
15711 - bbb
15712 + BBB
15713
15714 - ddd
15715 - eee
15716 + DDD
15717 + EEE
15718 fff
15719
15720 iii
15721 "
15722 .unindent(),
15723 );
15724}
15725
15726#[gpui::test]
15727async fn test_edits_around_expanded_insertion_hunks(
15728 executor: BackgroundExecutor,
15729 cx: &mut TestAppContext,
15730) {
15731 init_test(cx, |_| {});
15732
15733 let mut cx = EditorTestContext::new(cx).await;
15734
15735 let diff_base = r#"
15736 use some::mod1;
15737 use some::mod2;
15738
15739 const A: u32 = 42;
15740
15741 fn main() {
15742 println!("hello");
15743
15744 println!("world");
15745 }
15746 "#
15747 .unindent();
15748 executor.run_until_parked();
15749 cx.set_state(
15750 &r#"
15751 use some::mod1;
15752 use some::mod2;
15753
15754 const A: u32 = 42;
15755 const B: u32 = 42;
15756 const C: u32 = 42;
15757 ˇ
15758
15759 fn main() {
15760 println!("hello");
15761
15762 println!("world");
15763 }
15764 "#
15765 .unindent(),
15766 );
15767
15768 cx.set_head_text(&diff_base);
15769 executor.run_until_parked();
15770
15771 cx.update_editor(|editor, window, cx| {
15772 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15773 });
15774 executor.run_until_parked();
15775
15776 cx.assert_state_with_diff(
15777 r#"
15778 use some::mod1;
15779 use some::mod2;
15780
15781 const A: u32 = 42;
15782 + const B: u32 = 42;
15783 + const C: u32 = 42;
15784 + ˇ
15785
15786 fn main() {
15787 println!("hello");
15788
15789 println!("world");
15790 }
15791 "#
15792 .unindent(),
15793 );
15794
15795 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
15796 executor.run_until_parked();
15797
15798 cx.assert_state_with_diff(
15799 r#"
15800 use some::mod1;
15801 use some::mod2;
15802
15803 const A: u32 = 42;
15804 + const B: u32 = 42;
15805 + const C: u32 = 42;
15806 + const D: u32 = 42;
15807 + ˇ
15808
15809 fn main() {
15810 println!("hello");
15811
15812 println!("world");
15813 }
15814 "#
15815 .unindent(),
15816 );
15817
15818 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
15819 executor.run_until_parked();
15820
15821 cx.assert_state_with_diff(
15822 r#"
15823 use some::mod1;
15824 use some::mod2;
15825
15826 const A: u32 = 42;
15827 + const B: u32 = 42;
15828 + const C: u32 = 42;
15829 + const D: u32 = 42;
15830 + const E: u32 = 42;
15831 + ˇ
15832
15833 fn main() {
15834 println!("hello");
15835
15836 println!("world");
15837 }
15838 "#
15839 .unindent(),
15840 );
15841
15842 cx.update_editor(|editor, window, cx| {
15843 editor.delete_line(&DeleteLine, window, cx);
15844 });
15845 executor.run_until_parked();
15846
15847 cx.assert_state_with_diff(
15848 r#"
15849 use some::mod1;
15850 use some::mod2;
15851
15852 const A: u32 = 42;
15853 + const B: u32 = 42;
15854 + const C: u32 = 42;
15855 + const D: u32 = 42;
15856 + const E: u32 = 42;
15857 ˇ
15858 fn main() {
15859 println!("hello");
15860
15861 println!("world");
15862 }
15863 "#
15864 .unindent(),
15865 );
15866
15867 cx.update_editor(|editor, window, cx| {
15868 editor.move_up(&MoveUp, window, cx);
15869 editor.delete_line(&DeleteLine, window, cx);
15870 editor.move_up(&MoveUp, window, cx);
15871 editor.delete_line(&DeleteLine, window, cx);
15872 editor.move_up(&MoveUp, window, cx);
15873 editor.delete_line(&DeleteLine, window, cx);
15874 });
15875 executor.run_until_parked();
15876 cx.assert_state_with_diff(
15877 r#"
15878 use some::mod1;
15879 use some::mod2;
15880
15881 const A: u32 = 42;
15882 + const B: u32 = 42;
15883 ˇ
15884 fn main() {
15885 println!("hello");
15886
15887 println!("world");
15888 }
15889 "#
15890 .unindent(),
15891 );
15892
15893 cx.update_editor(|editor, window, cx| {
15894 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
15895 editor.delete_line(&DeleteLine, window, cx);
15896 });
15897 executor.run_until_parked();
15898 cx.assert_state_with_diff(
15899 r#"
15900 ˇ
15901 fn main() {
15902 println!("hello");
15903
15904 println!("world");
15905 }
15906 "#
15907 .unindent(),
15908 );
15909}
15910
15911#[gpui::test]
15912async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
15913 init_test(cx, |_| {});
15914
15915 let mut cx = EditorTestContext::new(cx).await;
15916 cx.set_head_text(indoc! { "
15917 one
15918 two
15919 three
15920 four
15921 five
15922 "
15923 });
15924 cx.set_state(indoc! { "
15925 one
15926 ˇthree
15927 five
15928 "});
15929 cx.run_until_parked();
15930 cx.update_editor(|editor, window, cx| {
15931 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
15932 });
15933 cx.assert_state_with_diff(
15934 indoc! { "
15935 one
15936 - two
15937 ˇthree
15938 - four
15939 five
15940 "}
15941 .to_string(),
15942 );
15943 cx.update_editor(|editor, window, cx| {
15944 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
15945 });
15946
15947 cx.assert_state_with_diff(
15948 indoc! { "
15949 one
15950 ˇthree
15951 five
15952 "}
15953 .to_string(),
15954 );
15955
15956 cx.set_state(indoc! { "
15957 one
15958 ˇTWO
15959 three
15960 four
15961 five
15962 "});
15963 cx.run_until_parked();
15964 cx.update_editor(|editor, window, cx| {
15965 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
15966 });
15967
15968 cx.assert_state_with_diff(
15969 indoc! { "
15970 one
15971 - two
15972 + ˇTWO
15973 three
15974 four
15975 five
15976 "}
15977 .to_string(),
15978 );
15979 cx.update_editor(|editor, window, cx| {
15980 editor.move_up(&Default::default(), window, cx);
15981 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
15982 });
15983 cx.assert_state_with_diff(
15984 indoc! { "
15985 one
15986 ˇTWO
15987 three
15988 four
15989 five
15990 "}
15991 .to_string(),
15992 );
15993}
15994
15995#[gpui::test]
15996async fn test_edits_around_expanded_deletion_hunks(
15997 executor: BackgroundExecutor,
15998 cx: &mut TestAppContext,
15999) {
16000 init_test(cx, |_| {});
16001
16002 let mut cx = EditorTestContext::new(cx).await;
16003
16004 let diff_base = r#"
16005 use some::mod1;
16006 use some::mod2;
16007
16008 const A: u32 = 42;
16009 const B: u32 = 42;
16010 const C: u32 = 42;
16011
16012
16013 fn main() {
16014 println!("hello");
16015
16016 println!("world");
16017 }
16018 "#
16019 .unindent();
16020 executor.run_until_parked();
16021 cx.set_state(
16022 &r#"
16023 use some::mod1;
16024 use some::mod2;
16025
16026 ˇconst B: u32 = 42;
16027 const C: u32 = 42;
16028
16029
16030 fn main() {
16031 println!("hello");
16032
16033 println!("world");
16034 }
16035 "#
16036 .unindent(),
16037 );
16038
16039 cx.set_head_text(&diff_base);
16040 executor.run_until_parked();
16041
16042 cx.update_editor(|editor, window, cx| {
16043 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16044 });
16045 executor.run_until_parked();
16046
16047 cx.assert_state_with_diff(
16048 r#"
16049 use some::mod1;
16050 use some::mod2;
16051
16052 - const A: u32 = 42;
16053 ˇconst B: u32 = 42;
16054 const C: u32 = 42;
16055
16056
16057 fn main() {
16058 println!("hello");
16059
16060 println!("world");
16061 }
16062 "#
16063 .unindent(),
16064 );
16065
16066 cx.update_editor(|editor, window, cx| {
16067 editor.delete_line(&DeleteLine, window, cx);
16068 });
16069 executor.run_until_parked();
16070 cx.assert_state_with_diff(
16071 r#"
16072 use some::mod1;
16073 use some::mod2;
16074
16075 - const A: u32 = 42;
16076 - const B: u32 = 42;
16077 ˇconst C: u32 = 42;
16078
16079
16080 fn main() {
16081 println!("hello");
16082
16083 println!("world");
16084 }
16085 "#
16086 .unindent(),
16087 );
16088
16089 cx.update_editor(|editor, window, cx| {
16090 editor.delete_line(&DeleteLine, window, cx);
16091 });
16092 executor.run_until_parked();
16093 cx.assert_state_with_diff(
16094 r#"
16095 use some::mod1;
16096 use some::mod2;
16097
16098 - const A: u32 = 42;
16099 - const B: u32 = 42;
16100 - const C: u32 = 42;
16101 ˇ
16102
16103 fn main() {
16104 println!("hello");
16105
16106 println!("world");
16107 }
16108 "#
16109 .unindent(),
16110 );
16111
16112 cx.update_editor(|editor, window, cx| {
16113 editor.handle_input("replacement", window, cx);
16114 });
16115 executor.run_until_parked();
16116 cx.assert_state_with_diff(
16117 r#"
16118 use some::mod1;
16119 use some::mod2;
16120
16121 - const A: u32 = 42;
16122 - const B: u32 = 42;
16123 - const C: u32 = 42;
16124 -
16125 + replacementˇ
16126
16127 fn main() {
16128 println!("hello");
16129
16130 println!("world");
16131 }
16132 "#
16133 .unindent(),
16134 );
16135}
16136
16137#[gpui::test]
16138async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16139 init_test(cx, |_| {});
16140
16141 let mut cx = EditorTestContext::new(cx).await;
16142
16143 let base_text = r#"
16144 one
16145 two
16146 three
16147 four
16148 five
16149 "#
16150 .unindent();
16151 executor.run_until_parked();
16152 cx.set_state(
16153 &r#"
16154 one
16155 two
16156 fˇour
16157 five
16158 "#
16159 .unindent(),
16160 );
16161
16162 cx.set_head_text(&base_text);
16163 executor.run_until_parked();
16164
16165 cx.update_editor(|editor, window, cx| {
16166 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16167 });
16168 executor.run_until_parked();
16169
16170 cx.assert_state_with_diff(
16171 r#"
16172 one
16173 two
16174 - three
16175 fˇour
16176 five
16177 "#
16178 .unindent(),
16179 );
16180
16181 cx.update_editor(|editor, window, cx| {
16182 editor.backspace(&Backspace, window, cx);
16183 editor.backspace(&Backspace, window, cx);
16184 });
16185 executor.run_until_parked();
16186 cx.assert_state_with_diff(
16187 r#"
16188 one
16189 two
16190 - threeˇ
16191 - four
16192 + our
16193 five
16194 "#
16195 .unindent(),
16196 );
16197}
16198
16199#[gpui::test]
16200async fn test_edit_after_expanded_modification_hunk(
16201 executor: BackgroundExecutor,
16202 cx: &mut TestAppContext,
16203) {
16204 init_test(cx, |_| {});
16205
16206 let mut cx = EditorTestContext::new(cx).await;
16207
16208 let diff_base = r#"
16209 use some::mod1;
16210 use some::mod2;
16211
16212 const A: u32 = 42;
16213 const B: u32 = 42;
16214 const C: u32 = 42;
16215 const D: u32 = 42;
16216
16217
16218 fn main() {
16219 println!("hello");
16220
16221 println!("world");
16222 }"#
16223 .unindent();
16224
16225 cx.set_state(
16226 &r#"
16227 use some::mod1;
16228 use some::mod2;
16229
16230 const A: u32 = 42;
16231 const B: u32 = 42;
16232 const C: u32 = 43ˇ
16233 const D: u32 = 42;
16234
16235
16236 fn main() {
16237 println!("hello");
16238
16239 println!("world");
16240 }"#
16241 .unindent(),
16242 );
16243
16244 cx.set_head_text(&diff_base);
16245 executor.run_until_parked();
16246 cx.update_editor(|editor, window, cx| {
16247 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16248 });
16249 executor.run_until_parked();
16250
16251 cx.assert_state_with_diff(
16252 r#"
16253 use some::mod1;
16254 use some::mod2;
16255
16256 const A: u32 = 42;
16257 const B: u32 = 42;
16258 - const C: u32 = 42;
16259 + const C: u32 = 43ˇ
16260 const D: u32 = 42;
16261
16262
16263 fn main() {
16264 println!("hello");
16265
16266 println!("world");
16267 }"#
16268 .unindent(),
16269 );
16270
16271 cx.update_editor(|editor, window, cx| {
16272 editor.handle_input("\nnew_line\n", window, cx);
16273 });
16274 executor.run_until_parked();
16275
16276 cx.assert_state_with_diff(
16277 r#"
16278 use some::mod1;
16279 use some::mod2;
16280
16281 const A: u32 = 42;
16282 const B: u32 = 42;
16283 - const C: u32 = 42;
16284 + const C: u32 = 43
16285 + new_line
16286 + ˇ
16287 const D: u32 = 42;
16288
16289
16290 fn main() {
16291 println!("hello");
16292
16293 println!("world");
16294 }"#
16295 .unindent(),
16296 );
16297}
16298
16299#[gpui::test]
16300async fn test_stage_and_unstage_added_file_hunk(
16301 executor: BackgroundExecutor,
16302 cx: &mut TestAppContext,
16303) {
16304 init_test(cx, |_| {});
16305
16306 let mut cx = EditorTestContext::new(cx).await;
16307 cx.update_editor(|editor, _, cx| {
16308 editor.set_expand_all_diff_hunks(cx);
16309 });
16310
16311 let working_copy = r#"
16312 ˇfn main() {
16313 println!("hello, world!");
16314 }
16315 "#
16316 .unindent();
16317
16318 cx.set_state(&working_copy);
16319 executor.run_until_parked();
16320
16321 cx.assert_state_with_diff(
16322 r#"
16323 + ˇfn main() {
16324 + println!("hello, world!");
16325 + }
16326 "#
16327 .unindent(),
16328 );
16329 cx.assert_index_text(None);
16330
16331 cx.update_editor(|editor, window, cx| {
16332 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16333 });
16334 executor.run_until_parked();
16335 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
16336 cx.assert_state_with_diff(
16337 r#"
16338 + ˇfn main() {
16339 + println!("hello, world!");
16340 + }
16341 "#
16342 .unindent(),
16343 );
16344
16345 cx.update_editor(|editor, window, cx| {
16346 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16347 });
16348 executor.run_until_parked();
16349 cx.assert_index_text(None);
16350}
16351
16352async fn setup_indent_guides_editor(
16353 text: &str,
16354 cx: &mut TestAppContext,
16355) -> (BufferId, EditorTestContext) {
16356 init_test(cx, |_| {});
16357
16358 let mut cx = EditorTestContext::new(cx).await;
16359
16360 let buffer_id = cx.update_editor(|editor, window, cx| {
16361 editor.set_text(text, window, cx);
16362 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
16363
16364 buffer_ids[0]
16365 });
16366
16367 (buffer_id, cx)
16368}
16369
16370fn assert_indent_guides(
16371 range: Range<u32>,
16372 expected: Vec<IndentGuide>,
16373 active_indices: Option<Vec<usize>>,
16374 cx: &mut EditorTestContext,
16375) {
16376 let indent_guides = cx.update_editor(|editor, window, cx| {
16377 let snapshot = editor.snapshot(window, cx).display_snapshot;
16378 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
16379 editor,
16380 MultiBufferRow(range.start)..MultiBufferRow(range.end),
16381 true,
16382 &snapshot,
16383 cx,
16384 );
16385
16386 indent_guides.sort_by(|a, b| {
16387 a.depth.cmp(&b.depth).then(
16388 a.start_row
16389 .cmp(&b.start_row)
16390 .then(a.end_row.cmp(&b.end_row)),
16391 )
16392 });
16393 indent_guides
16394 });
16395
16396 if let Some(expected) = active_indices {
16397 let active_indices = cx.update_editor(|editor, window, cx| {
16398 let snapshot = editor.snapshot(window, cx).display_snapshot;
16399 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
16400 });
16401
16402 assert_eq!(
16403 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
16404 expected,
16405 "Active indent guide indices do not match"
16406 );
16407 }
16408
16409 assert_eq!(indent_guides, expected, "Indent guides do not match");
16410}
16411
16412fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
16413 IndentGuide {
16414 buffer_id,
16415 start_row: MultiBufferRow(start_row),
16416 end_row: MultiBufferRow(end_row),
16417 depth,
16418 tab_size: 4,
16419 settings: IndentGuideSettings {
16420 enabled: true,
16421 line_width: 1,
16422 active_line_width: 1,
16423 ..Default::default()
16424 },
16425 }
16426}
16427
16428#[gpui::test]
16429async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
16430 let (buffer_id, mut cx) = setup_indent_guides_editor(
16431 &"
16432 fn main() {
16433 let a = 1;
16434 }"
16435 .unindent(),
16436 cx,
16437 )
16438 .await;
16439
16440 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
16441}
16442
16443#[gpui::test]
16444async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
16445 let (buffer_id, mut cx) = setup_indent_guides_editor(
16446 &"
16447 fn main() {
16448 let a = 1;
16449 let b = 2;
16450 }"
16451 .unindent(),
16452 cx,
16453 )
16454 .await;
16455
16456 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
16457}
16458
16459#[gpui::test]
16460async fn test_indent_guide_nested(cx: &mut TestAppContext) {
16461 let (buffer_id, mut cx) = setup_indent_guides_editor(
16462 &"
16463 fn main() {
16464 let a = 1;
16465 if a == 3 {
16466 let b = 2;
16467 } else {
16468 let c = 3;
16469 }
16470 }"
16471 .unindent(),
16472 cx,
16473 )
16474 .await;
16475
16476 assert_indent_guides(
16477 0..8,
16478 vec![
16479 indent_guide(buffer_id, 1, 6, 0),
16480 indent_guide(buffer_id, 3, 3, 1),
16481 indent_guide(buffer_id, 5, 5, 1),
16482 ],
16483 None,
16484 &mut cx,
16485 );
16486}
16487
16488#[gpui::test]
16489async fn test_indent_guide_tab(cx: &mut TestAppContext) {
16490 let (buffer_id, mut cx) = setup_indent_guides_editor(
16491 &"
16492 fn main() {
16493 let a = 1;
16494 let b = 2;
16495 let c = 3;
16496 }"
16497 .unindent(),
16498 cx,
16499 )
16500 .await;
16501
16502 assert_indent_guides(
16503 0..5,
16504 vec![
16505 indent_guide(buffer_id, 1, 3, 0),
16506 indent_guide(buffer_id, 2, 2, 1),
16507 ],
16508 None,
16509 &mut cx,
16510 );
16511}
16512
16513#[gpui::test]
16514async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
16515 let (buffer_id, mut cx) = setup_indent_guides_editor(
16516 &"
16517 fn main() {
16518 let a = 1;
16519
16520 let c = 3;
16521 }"
16522 .unindent(),
16523 cx,
16524 )
16525 .await;
16526
16527 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
16528}
16529
16530#[gpui::test]
16531async fn test_indent_guide_complex(cx: &mut TestAppContext) {
16532 let (buffer_id, mut cx) = setup_indent_guides_editor(
16533 &"
16534 fn main() {
16535 let a = 1;
16536
16537 let c = 3;
16538
16539 if a == 3 {
16540 let b = 2;
16541 } else {
16542 let c = 3;
16543 }
16544 }"
16545 .unindent(),
16546 cx,
16547 )
16548 .await;
16549
16550 assert_indent_guides(
16551 0..11,
16552 vec![
16553 indent_guide(buffer_id, 1, 9, 0),
16554 indent_guide(buffer_id, 6, 6, 1),
16555 indent_guide(buffer_id, 8, 8, 1),
16556 ],
16557 None,
16558 &mut cx,
16559 );
16560}
16561
16562#[gpui::test]
16563async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
16564 let (buffer_id, mut cx) = setup_indent_guides_editor(
16565 &"
16566 fn main() {
16567 let a = 1;
16568
16569 let c = 3;
16570
16571 if a == 3 {
16572 let b = 2;
16573 } else {
16574 let c = 3;
16575 }
16576 }"
16577 .unindent(),
16578 cx,
16579 )
16580 .await;
16581
16582 assert_indent_guides(
16583 1..11,
16584 vec![
16585 indent_guide(buffer_id, 1, 9, 0),
16586 indent_guide(buffer_id, 6, 6, 1),
16587 indent_guide(buffer_id, 8, 8, 1),
16588 ],
16589 None,
16590 &mut cx,
16591 );
16592}
16593
16594#[gpui::test]
16595async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
16596 let (buffer_id, mut cx) = setup_indent_guides_editor(
16597 &"
16598 fn main() {
16599 let a = 1;
16600
16601 let c = 3;
16602
16603 if a == 3 {
16604 let b = 2;
16605 } else {
16606 let c = 3;
16607 }
16608 }"
16609 .unindent(),
16610 cx,
16611 )
16612 .await;
16613
16614 assert_indent_guides(
16615 1..10,
16616 vec![
16617 indent_guide(buffer_id, 1, 9, 0),
16618 indent_guide(buffer_id, 6, 6, 1),
16619 indent_guide(buffer_id, 8, 8, 1),
16620 ],
16621 None,
16622 &mut cx,
16623 );
16624}
16625
16626#[gpui::test]
16627async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
16628 let (buffer_id, mut cx) = setup_indent_guides_editor(
16629 &"
16630 block1
16631 block2
16632 block3
16633 block4
16634 block2
16635 block1
16636 block1"
16637 .unindent(),
16638 cx,
16639 )
16640 .await;
16641
16642 assert_indent_guides(
16643 1..10,
16644 vec![
16645 indent_guide(buffer_id, 1, 4, 0),
16646 indent_guide(buffer_id, 2, 3, 1),
16647 indent_guide(buffer_id, 3, 3, 2),
16648 ],
16649 None,
16650 &mut cx,
16651 );
16652}
16653
16654#[gpui::test]
16655async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
16656 let (buffer_id, mut cx) = setup_indent_guides_editor(
16657 &"
16658 block1
16659 block2
16660 block3
16661
16662 block1
16663 block1"
16664 .unindent(),
16665 cx,
16666 )
16667 .await;
16668
16669 assert_indent_guides(
16670 0..6,
16671 vec![
16672 indent_guide(buffer_id, 1, 2, 0),
16673 indent_guide(buffer_id, 2, 2, 1),
16674 ],
16675 None,
16676 &mut cx,
16677 );
16678}
16679
16680#[gpui::test]
16681async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
16682 let (buffer_id, mut cx) = setup_indent_guides_editor(
16683 &"
16684 block1
16685
16686
16687
16688 block2
16689 "
16690 .unindent(),
16691 cx,
16692 )
16693 .await;
16694
16695 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
16696}
16697
16698#[gpui::test]
16699async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
16700 let (buffer_id, mut cx) = setup_indent_guides_editor(
16701 &"
16702 def a:
16703 \tb = 3
16704 \tif True:
16705 \t\tc = 4
16706 \t\td = 5
16707 \tprint(b)
16708 "
16709 .unindent(),
16710 cx,
16711 )
16712 .await;
16713
16714 assert_indent_guides(
16715 0..6,
16716 vec![
16717 indent_guide(buffer_id, 1, 5, 0),
16718 indent_guide(buffer_id, 3, 4, 1),
16719 ],
16720 None,
16721 &mut cx,
16722 );
16723}
16724
16725#[gpui::test]
16726async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
16727 let (buffer_id, mut cx) = setup_indent_guides_editor(
16728 &"
16729 fn main() {
16730 let a = 1;
16731 }"
16732 .unindent(),
16733 cx,
16734 )
16735 .await;
16736
16737 cx.update_editor(|editor, window, cx| {
16738 editor.change_selections(None, window, cx, |s| {
16739 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
16740 });
16741 });
16742
16743 assert_indent_guides(
16744 0..3,
16745 vec![indent_guide(buffer_id, 1, 1, 0)],
16746 Some(vec![0]),
16747 &mut cx,
16748 );
16749}
16750
16751#[gpui::test]
16752async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
16753 let (buffer_id, mut cx) = setup_indent_guides_editor(
16754 &"
16755 fn main() {
16756 if 1 == 2 {
16757 let a = 1;
16758 }
16759 }"
16760 .unindent(),
16761 cx,
16762 )
16763 .await;
16764
16765 cx.update_editor(|editor, window, cx| {
16766 editor.change_selections(None, window, cx, |s| {
16767 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
16768 });
16769 });
16770
16771 assert_indent_guides(
16772 0..4,
16773 vec![
16774 indent_guide(buffer_id, 1, 3, 0),
16775 indent_guide(buffer_id, 2, 2, 1),
16776 ],
16777 Some(vec![1]),
16778 &mut cx,
16779 );
16780
16781 cx.update_editor(|editor, window, cx| {
16782 editor.change_selections(None, window, cx, |s| {
16783 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
16784 });
16785 });
16786
16787 assert_indent_guides(
16788 0..4,
16789 vec![
16790 indent_guide(buffer_id, 1, 3, 0),
16791 indent_guide(buffer_id, 2, 2, 1),
16792 ],
16793 Some(vec![1]),
16794 &mut cx,
16795 );
16796
16797 cx.update_editor(|editor, window, cx| {
16798 editor.change_selections(None, window, cx, |s| {
16799 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
16800 });
16801 });
16802
16803 assert_indent_guides(
16804 0..4,
16805 vec![
16806 indent_guide(buffer_id, 1, 3, 0),
16807 indent_guide(buffer_id, 2, 2, 1),
16808 ],
16809 Some(vec![0]),
16810 &mut cx,
16811 );
16812}
16813
16814#[gpui::test]
16815async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
16816 let (buffer_id, mut cx) = setup_indent_guides_editor(
16817 &"
16818 fn main() {
16819 let a = 1;
16820
16821 let b = 2;
16822 }"
16823 .unindent(),
16824 cx,
16825 )
16826 .await;
16827
16828 cx.update_editor(|editor, window, cx| {
16829 editor.change_selections(None, window, cx, |s| {
16830 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
16831 });
16832 });
16833
16834 assert_indent_guides(
16835 0..5,
16836 vec![indent_guide(buffer_id, 1, 3, 0)],
16837 Some(vec![0]),
16838 &mut cx,
16839 );
16840}
16841
16842#[gpui::test]
16843async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
16844 let (buffer_id, mut cx) = setup_indent_guides_editor(
16845 &"
16846 def m:
16847 a = 1
16848 pass"
16849 .unindent(),
16850 cx,
16851 )
16852 .await;
16853
16854 cx.update_editor(|editor, window, cx| {
16855 editor.change_selections(None, window, cx, |s| {
16856 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
16857 });
16858 });
16859
16860 assert_indent_guides(
16861 0..3,
16862 vec![indent_guide(buffer_id, 1, 2, 0)],
16863 Some(vec![0]),
16864 &mut cx,
16865 );
16866}
16867
16868#[gpui::test]
16869async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
16870 init_test(cx, |_| {});
16871 let mut cx = EditorTestContext::new(cx).await;
16872 let text = indoc! {
16873 "
16874 impl A {
16875 fn b() {
16876 0;
16877 3;
16878 5;
16879 6;
16880 7;
16881 }
16882 }
16883 "
16884 };
16885 let base_text = indoc! {
16886 "
16887 impl A {
16888 fn b() {
16889 0;
16890 1;
16891 2;
16892 3;
16893 4;
16894 }
16895 fn c() {
16896 5;
16897 6;
16898 7;
16899 }
16900 }
16901 "
16902 };
16903
16904 cx.update_editor(|editor, window, cx| {
16905 editor.set_text(text, window, cx);
16906
16907 editor.buffer().update(cx, |multibuffer, cx| {
16908 let buffer = multibuffer.as_singleton().unwrap();
16909 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
16910
16911 multibuffer.set_all_diff_hunks_expanded(cx);
16912 multibuffer.add_diff(diff, cx);
16913
16914 buffer.read(cx).remote_id()
16915 })
16916 });
16917 cx.run_until_parked();
16918
16919 cx.assert_state_with_diff(
16920 indoc! { "
16921 impl A {
16922 fn b() {
16923 0;
16924 - 1;
16925 - 2;
16926 3;
16927 - 4;
16928 - }
16929 - fn c() {
16930 5;
16931 6;
16932 7;
16933 }
16934 }
16935 ˇ"
16936 }
16937 .to_string(),
16938 );
16939
16940 let mut actual_guides = cx.update_editor(|editor, window, cx| {
16941 editor
16942 .snapshot(window, cx)
16943 .buffer_snapshot
16944 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
16945 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
16946 .collect::<Vec<_>>()
16947 });
16948 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
16949 assert_eq!(
16950 actual_guides,
16951 vec![
16952 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
16953 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
16954 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
16955 ]
16956 );
16957}
16958
16959#[gpui::test]
16960async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16961 init_test(cx, |_| {});
16962 let mut cx = EditorTestContext::new(cx).await;
16963
16964 let diff_base = r#"
16965 a
16966 b
16967 c
16968 "#
16969 .unindent();
16970
16971 cx.set_state(
16972 &r#"
16973 ˇA
16974 b
16975 C
16976 "#
16977 .unindent(),
16978 );
16979 cx.set_head_text(&diff_base);
16980 cx.update_editor(|editor, window, cx| {
16981 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16982 });
16983 executor.run_until_parked();
16984
16985 let both_hunks_expanded = r#"
16986 - a
16987 + ˇA
16988 b
16989 - c
16990 + C
16991 "#
16992 .unindent();
16993
16994 cx.assert_state_with_diff(both_hunks_expanded.clone());
16995
16996 let hunk_ranges = cx.update_editor(|editor, window, cx| {
16997 let snapshot = editor.snapshot(window, cx);
16998 let hunks = editor
16999 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
17000 .collect::<Vec<_>>();
17001 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
17002 let buffer_id = hunks[0].buffer_id;
17003 hunks
17004 .into_iter()
17005 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
17006 .collect::<Vec<_>>()
17007 });
17008 assert_eq!(hunk_ranges.len(), 2);
17009
17010 cx.update_editor(|editor, _, cx| {
17011 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
17012 });
17013 executor.run_until_parked();
17014
17015 let second_hunk_expanded = r#"
17016 ˇA
17017 b
17018 - c
17019 + C
17020 "#
17021 .unindent();
17022
17023 cx.assert_state_with_diff(second_hunk_expanded);
17024
17025 cx.update_editor(|editor, _, cx| {
17026 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
17027 });
17028 executor.run_until_parked();
17029
17030 cx.assert_state_with_diff(both_hunks_expanded.clone());
17031
17032 cx.update_editor(|editor, _, cx| {
17033 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
17034 });
17035 executor.run_until_parked();
17036
17037 let first_hunk_expanded = r#"
17038 - a
17039 + ˇA
17040 b
17041 C
17042 "#
17043 .unindent();
17044
17045 cx.assert_state_with_diff(first_hunk_expanded);
17046
17047 cx.update_editor(|editor, _, cx| {
17048 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
17049 });
17050 executor.run_until_parked();
17051
17052 cx.assert_state_with_diff(both_hunks_expanded);
17053
17054 cx.set_state(
17055 &r#"
17056 ˇA
17057 b
17058 "#
17059 .unindent(),
17060 );
17061 cx.run_until_parked();
17062
17063 // TODO this cursor position seems bad
17064 cx.assert_state_with_diff(
17065 r#"
17066 - ˇa
17067 + A
17068 b
17069 "#
17070 .unindent(),
17071 );
17072
17073 cx.update_editor(|editor, window, cx| {
17074 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17075 });
17076
17077 cx.assert_state_with_diff(
17078 r#"
17079 - ˇa
17080 + A
17081 b
17082 - c
17083 "#
17084 .unindent(),
17085 );
17086
17087 let hunk_ranges = cx.update_editor(|editor, window, cx| {
17088 let snapshot = editor.snapshot(window, cx);
17089 let hunks = editor
17090 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
17091 .collect::<Vec<_>>();
17092 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
17093 let buffer_id = hunks[0].buffer_id;
17094 hunks
17095 .into_iter()
17096 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
17097 .collect::<Vec<_>>()
17098 });
17099 assert_eq!(hunk_ranges.len(), 2);
17100
17101 cx.update_editor(|editor, _, cx| {
17102 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
17103 });
17104 executor.run_until_parked();
17105
17106 cx.assert_state_with_diff(
17107 r#"
17108 - ˇa
17109 + A
17110 b
17111 "#
17112 .unindent(),
17113 );
17114}
17115
17116#[gpui::test]
17117async fn test_toggle_deletion_hunk_at_start_of_file(
17118 executor: BackgroundExecutor,
17119 cx: &mut TestAppContext,
17120) {
17121 init_test(cx, |_| {});
17122 let mut cx = EditorTestContext::new(cx).await;
17123
17124 let diff_base = r#"
17125 a
17126 b
17127 c
17128 "#
17129 .unindent();
17130
17131 cx.set_state(
17132 &r#"
17133 ˇb
17134 c
17135 "#
17136 .unindent(),
17137 );
17138 cx.set_head_text(&diff_base);
17139 cx.update_editor(|editor, window, cx| {
17140 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17141 });
17142 executor.run_until_parked();
17143
17144 let hunk_expanded = r#"
17145 - a
17146 ˇb
17147 c
17148 "#
17149 .unindent();
17150
17151 cx.assert_state_with_diff(hunk_expanded.clone());
17152
17153 let hunk_ranges = cx.update_editor(|editor, window, cx| {
17154 let snapshot = editor.snapshot(window, cx);
17155 let hunks = editor
17156 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
17157 .collect::<Vec<_>>();
17158 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
17159 let buffer_id = hunks[0].buffer_id;
17160 hunks
17161 .into_iter()
17162 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
17163 .collect::<Vec<_>>()
17164 });
17165 assert_eq!(hunk_ranges.len(), 1);
17166
17167 cx.update_editor(|editor, _, cx| {
17168 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
17169 });
17170 executor.run_until_parked();
17171
17172 let hunk_collapsed = r#"
17173 ˇb
17174 c
17175 "#
17176 .unindent();
17177
17178 cx.assert_state_with_diff(hunk_collapsed);
17179
17180 cx.update_editor(|editor, _, cx| {
17181 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
17182 });
17183 executor.run_until_parked();
17184
17185 cx.assert_state_with_diff(hunk_expanded.clone());
17186}
17187
17188#[gpui::test]
17189async fn test_display_diff_hunks(cx: &mut TestAppContext) {
17190 init_test(cx, |_| {});
17191
17192 let fs = FakeFs::new(cx.executor());
17193 fs.insert_tree(
17194 path!("/test"),
17195 json!({
17196 ".git": {},
17197 "file-1": "ONE\n",
17198 "file-2": "TWO\n",
17199 "file-3": "THREE\n",
17200 }),
17201 )
17202 .await;
17203
17204 fs.set_head_for_repo(
17205 path!("/test/.git").as_ref(),
17206 &[
17207 ("file-1".into(), "one\n".into()),
17208 ("file-2".into(), "two\n".into()),
17209 ("file-3".into(), "three\n".into()),
17210 ],
17211 );
17212
17213 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
17214 let mut buffers = vec![];
17215 for i in 1..=3 {
17216 let buffer = project
17217 .update(cx, |project, cx| {
17218 let path = format!(path!("/test/file-{}"), i);
17219 project.open_local_buffer(path, cx)
17220 })
17221 .await
17222 .unwrap();
17223 buffers.push(buffer);
17224 }
17225
17226 let multibuffer = cx.new(|cx| {
17227 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
17228 multibuffer.set_all_diff_hunks_expanded(cx);
17229 for buffer in &buffers {
17230 let snapshot = buffer.read(cx).snapshot();
17231 multibuffer.set_excerpts_for_path(
17232 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
17233 buffer.clone(),
17234 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
17235 DEFAULT_MULTIBUFFER_CONTEXT,
17236 cx,
17237 );
17238 }
17239 multibuffer
17240 });
17241
17242 let editor = cx.add_window(|window, cx| {
17243 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
17244 });
17245 cx.run_until_parked();
17246
17247 let snapshot = editor
17248 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
17249 .unwrap();
17250 let hunks = snapshot
17251 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
17252 .map(|hunk| match hunk {
17253 DisplayDiffHunk::Unfolded {
17254 display_row_range, ..
17255 } => display_row_range,
17256 DisplayDiffHunk::Folded { .. } => unreachable!(),
17257 })
17258 .collect::<Vec<_>>();
17259 assert_eq!(
17260 hunks,
17261 [
17262 DisplayRow(2)..DisplayRow(4),
17263 DisplayRow(7)..DisplayRow(9),
17264 DisplayRow(12)..DisplayRow(14),
17265 ]
17266 );
17267}
17268
17269#[gpui::test]
17270async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
17271 init_test(cx, |_| {});
17272
17273 let mut cx = EditorTestContext::new(cx).await;
17274 cx.set_head_text(indoc! { "
17275 one
17276 two
17277 three
17278 four
17279 five
17280 "
17281 });
17282 cx.set_index_text(indoc! { "
17283 one
17284 two
17285 three
17286 four
17287 five
17288 "
17289 });
17290 cx.set_state(indoc! {"
17291 one
17292 TWO
17293 ˇTHREE
17294 FOUR
17295 five
17296 "});
17297 cx.run_until_parked();
17298 cx.update_editor(|editor, window, cx| {
17299 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
17300 });
17301 cx.run_until_parked();
17302 cx.assert_index_text(Some(indoc! {"
17303 one
17304 TWO
17305 THREE
17306 FOUR
17307 five
17308 "}));
17309 cx.set_state(indoc! { "
17310 one
17311 TWO
17312 ˇTHREE-HUNDRED
17313 FOUR
17314 five
17315 "});
17316 cx.run_until_parked();
17317 cx.update_editor(|editor, window, cx| {
17318 let snapshot = editor.snapshot(window, cx);
17319 let hunks = editor
17320 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
17321 .collect::<Vec<_>>();
17322 assert_eq!(hunks.len(), 1);
17323 assert_eq!(
17324 hunks[0].status(),
17325 DiffHunkStatus {
17326 kind: DiffHunkStatusKind::Modified,
17327 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
17328 }
17329 );
17330
17331 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
17332 });
17333 cx.run_until_parked();
17334 cx.assert_index_text(Some(indoc! {"
17335 one
17336 TWO
17337 THREE-HUNDRED
17338 FOUR
17339 five
17340 "}));
17341}
17342
17343#[gpui::test]
17344fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
17345 init_test(cx, |_| {});
17346
17347 let editor = cx.add_window(|window, cx| {
17348 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
17349 build_editor(buffer, window, cx)
17350 });
17351
17352 let render_args = Arc::new(Mutex::new(None));
17353 let snapshot = editor
17354 .update(cx, |editor, window, cx| {
17355 let snapshot = editor.buffer().read(cx).snapshot(cx);
17356 let range =
17357 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
17358
17359 struct RenderArgs {
17360 row: MultiBufferRow,
17361 folded: bool,
17362 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
17363 }
17364
17365 let crease = Crease::inline(
17366 range,
17367 FoldPlaceholder::test(),
17368 {
17369 let toggle_callback = render_args.clone();
17370 move |row, folded, callback, _window, _cx| {
17371 *toggle_callback.lock() = Some(RenderArgs {
17372 row,
17373 folded,
17374 callback,
17375 });
17376 div()
17377 }
17378 },
17379 |_row, _folded, _window, _cx| div(),
17380 );
17381
17382 editor.insert_creases(Some(crease), cx);
17383 let snapshot = editor.snapshot(window, cx);
17384 let _div = snapshot.render_crease_toggle(
17385 MultiBufferRow(1),
17386 false,
17387 cx.entity().clone(),
17388 window,
17389 cx,
17390 );
17391 snapshot
17392 })
17393 .unwrap();
17394
17395 let render_args = render_args.lock().take().unwrap();
17396 assert_eq!(render_args.row, MultiBufferRow(1));
17397 assert!(!render_args.folded);
17398 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
17399
17400 cx.update_window(*editor, |_, window, cx| {
17401 (render_args.callback)(true, window, cx)
17402 })
17403 .unwrap();
17404 let snapshot = editor
17405 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
17406 .unwrap();
17407 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
17408
17409 cx.update_window(*editor, |_, window, cx| {
17410 (render_args.callback)(false, window, cx)
17411 })
17412 .unwrap();
17413 let snapshot = editor
17414 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
17415 .unwrap();
17416 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
17417}
17418
17419#[gpui::test]
17420async fn test_input_text(cx: &mut TestAppContext) {
17421 init_test(cx, |_| {});
17422 let mut cx = EditorTestContext::new(cx).await;
17423
17424 cx.set_state(
17425 &r#"ˇone
17426 two
17427
17428 three
17429 fourˇ
17430 five
17431
17432 siˇx"#
17433 .unindent(),
17434 );
17435
17436 cx.dispatch_action(HandleInput(String::new()));
17437 cx.assert_editor_state(
17438 &r#"ˇone
17439 two
17440
17441 three
17442 fourˇ
17443 five
17444
17445 siˇx"#
17446 .unindent(),
17447 );
17448
17449 cx.dispatch_action(HandleInput("AAAA".to_string()));
17450 cx.assert_editor_state(
17451 &r#"AAAAˇone
17452 two
17453
17454 three
17455 fourAAAAˇ
17456 five
17457
17458 siAAAAˇx"#
17459 .unindent(),
17460 );
17461}
17462
17463#[gpui::test]
17464async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
17465 init_test(cx, |_| {});
17466
17467 let mut cx = EditorTestContext::new(cx).await;
17468 cx.set_state(
17469 r#"let foo = 1;
17470let foo = 2;
17471let foo = 3;
17472let fooˇ = 4;
17473let foo = 5;
17474let foo = 6;
17475let foo = 7;
17476let foo = 8;
17477let foo = 9;
17478let foo = 10;
17479let foo = 11;
17480let foo = 12;
17481let foo = 13;
17482let foo = 14;
17483let foo = 15;"#,
17484 );
17485
17486 cx.update_editor(|e, window, cx| {
17487 assert_eq!(
17488 e.next_scroll_position,
17489 NextScrollCursorCenterTopBottom::Center,
17490 "Default next scroll direction is center",
17491 );
17492
17493 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17494 assert_eq!(
17495 e.next_scroll_position,
17496 NextScrollCursorCenterTopBottom::Top,
17497 "After center, next scroll direction should be top",
17498 );
17499
17500 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17501 assert_eq!(
17502 e.next_scroll_position,
17503 NextScrollCursorCenterTopBottom::Bottom,
17504 "After top, next scroll direction should be bottom",
17505 );
17506
17507 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17508 assert_eq!(
17509 e.next_scroll_position,
17510 NextScrollCursorCenterTopBottom::Center,
17511 "After bottom, scrolling should start over",
17512 );
17513
17514 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17515 assert_eq!(
17516 e.next_scroll_position,
17517 NextScrollCursorCenterTopBottom::Top,
17518 "Scrolling continues if retriggered fast enough"
17519 );
17520 });
17521
17522 cx.executor()
17523 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
17524 cx.executor().run_until_parked();
17525 cx.update_editor(|e, _, _| {
17526 assert_eq!(
17527 e.next_scroll_position,
17528 NextScrollCursorCenterTopBottom::Center,
17529 "If scrolling is not triggered fast enough, it should reset"
17530 );
17531 });
17532}
17533
17534#[gpui::test]
17535async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
17536 init_test(cx, |_| {});
17537 let mut cx = EditorLspTestContext::new_rust(
17538 lsp::ServerCapabilities {
17539 definition_provider: Some(lsp::OneOf::Left(true)),
17540 references_provider: Some(lsp::OneOf::Left(true)),
17541 ..lsp::ServerCapabilities::default()
17542 },
17543 cx,
17544 )
17545 .await;
17546
17547 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
17548 let go_to_definition = cx
17549 .lsp
17550 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
17551 move |params, _| async move {
17552 if empty_go_to_definition {
17553 Ok(None)
17554 } else {
17555 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
17556 uri: params.text_document_position_params.text_document.uri,
17557 range: lsp::Range::new(
17558 lsp::Position::new(4, 3),
17559 lsp::Position::new(4, 6),
17560 ),
17561 })))
17562 }
17563 },
17564 );
17565 let references = cx
17566 .lsp
17567 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
17568 Ok(Some(vec![lsp::Location {
17569 uri: params.text_document_position.text_document.uri,
17570 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
17571 }]))
17572 });
17573 (go_to_definition, references)
17574 };
17575
17576 cx.set_state(
17577 &r#"fn one() {
17578 let mut a = ˇtwo();
17579 }
17580
17581 fn two() {}"#
17582 .unindent(),
17583 );
17584 set_up_lsp_handlers(false, &mut cx);
17585 let navigated = cx
17586 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
17587 .await
17588 .expect("Failed to navigate to definition");
17589 assert_eq!(
17590 navigated,
17591 Navigated::Yes,
17592 "Should have navigated to definition from the GetDefinition response"
17593 );
17594 cx.assert_editor_state(
17595 &r#"fn one() {
17596 let mut a = two();
17597 }
17598
17599 fn «twoˇ»() {}"#
17600 .unindent(),
17601 );
17602
17603 let editors = cx.update_workspace(|workspace, _, cx| {
17604 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
17605 });
17606 cx.update_editor(|_, _, test_editor_cx| {
17607 assert_eq!(
17608 editors.len(),
17609 1,
17610 "Initially, only one, test, editor should be open in the workspace"
17611 );
17612 assert_eq!(
17613 test_editor_cx.entity(),
17614 editors.last().expect("Asserted len is 1").clone()
17615 );
17616 });
17617
17618 set_up_lsp_handlers(true, &mut cx);
17619 let navigated = cx
17620 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
17621 .await
17622 .expect("Failed to navigate to lookup references");
17623 assert_eq!(
17624 navigated,
17625 Navigated::Yes,
17626 "Should have navigated to references as a fallback after empty GoToDefinition response"
17627 );
17628 // We should not change the selections in the existing file,
17629 // if opening another milti buffer with the references
17630 cx.assert_editor_state(
17631 &r#"fn one() {
17632 let mut a = two();
17633 }
17634
17635 fn «twoˇ»() {}"#
17636 .unindent(),
17637 );
17638 let editors = cx.update_workspace(|workspace, _, cx| {
17639 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
17640 });
17641 cx.update_editor(|_, _, test_editor_cx| {
17642 assert_eq!(
17643 editors.len(),
17644 2,
17645 "After falling back to references search, we open a new editor with the results"
17646 );
17647 let references_fallback_text = editors
17648 .into_iter()
17649 .find(|new_editor| *new_editor != test_editor_cx.entity())
17650 .expect("Should have one non-test editor now")
17651 .read(test_editor_cx)
17652 .text(test_editor_cx);
17653 assert_eq!(
17654 references_fallback_text, "fn one() {\n let mut a = two();\n}",
17655 "Should use the range from the references response and not the GoToDefinition one"
17656 );
17657 });
17658}
17659
17660#[gpui::test]
17661async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
17662 init_test(cx, |_| {});
17663 cx.update(|cx| {
17664 let mut editor_settings = EditorSettings::get_global(cx).clone();
17665 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
17666 EditorSettings::override_global(editor_settings, cx);
17667 });
17668 let mut cx = EditorLspTestContext::new_rust(
17669 lsp::ServerCapabilities {
17670 definition_provider: Some(lsp::OneOf::Left(true)),
17671 references_provider: Some(lsp::OneOf::Left(true)),
17672 ..lsp::ServerCapabilities::default()
17673 },
17674 cx,
17675 )
17676 .await;
17677 let original_state = r#"fn one() {
17678 let mut a = ˇtwo();
17679 }
17680
17681 fn two() {}"#
17682 .unindent();
17683 cx.set_state(&original_state);
17684
17685 let mut go_to_definition = cx
17686 .lsp
17687 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
17688 move |_, _| async move { Ok(None) },
17689 );
17690 let _references = cx
17691 .lsp
17692 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
17693 panic!("Should not call for references with no go to definition fallback")
17694 });
17695
17696 let navigated = cx
17697 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
17698 .await
17699 .expect("Failed to navigate to lookup references");
17700 go_to_definition
17701 .next()
17702 .await
17703 .expect("Should have called the go_to_definition handler");
17704
17705 assert_eq!(
17706 navigated,
17707 Navigated::No,
17708 "Should have navigated to references as a fallback after empty GoToDefinition response"
17709 );
17710 cx.assert_editor_state(&original_state);
17711 let editors = cx.update_workspace(|workspace, _, cx| {
17712 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
17713 });
17714 cx.update_editor(|_, _, _| {
17715 assert_eq!(
17716 editors.len(),
17717 1,
17718 "After unsuccessful fallback, no other editor should have been opened"
17719 );
17720 });
17721}
17722
17723#[gpui::test]
17724async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
17725 init_test(cx, |_| {});
17726
17727 let language = Arc::new(Language::new(
17728 LanguageConfig::default(),
17729 Some(tree_sitter_rust::LANGUAGE.into()),
17730 ));
17731
17732 let text = r#"
17733 #[cfg(test)]
17734 mod tests() {
17735 #[test]
17736 fn runnable_1() {
17737 let a = 1;
17738 }
17739
17740 #[test]
17741 fn runnable_2() {
17742 let a = 1;
17743 let b = 2;
17744 }
17745 }
17746 "#
17747 .unindent();
17748
17749 let fs = FakeFs::new(cx.executor());
17750 fs.insert_file("/file.rs", Default::default()).await;
17751
17752 let project = Project::test(fs, ["/a".as_ref()], cx).await;
17753 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17754 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17755 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
17756 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
17757
17758 let editor = cx.new_window_entity(|window, cx| {
17759 Editor::new(
17760 EditorMode::full(),
17761 multi_buffer,
17762 Some(project.clone()),
17763 window,
17764 cx,
17765 )
17766 });
17767
17768 editor.update_in(cx, |editor, window, cx| {
17769 let snapshot = editor.buffer().read(cx).snapshot(cx);
17770 editor.tasks.insert(
17771 (buffer.read(cx).remote_id(), 3),
17772 RunnableTasks {
17773 templates: vec![],
17774 offset: snapshot.anchor_before(43),
17775 column: 0,
17776 extra_variables: HashMap::default(),
17777 context_range: BufferOffset(43)..BufferOffset(85),
17778 },
17779 );
17780 editor.tasks.insert(
17781 (buffer.read(cx).remote_id(), 8),
17782 RunnableTasks {
17783 templates: vec![],
17784 offset: snapshot.anchor_before(86),
17785 column: 0,
17786 extra_variables: HashMap::default(),
17787 context_range: BufferOffset(86)..BufferOffset(191),
17788 },
17789 );
17790
17791 // Test finding task when cursor is inside function body
17792 editor.change_selections(None, window, cx, |s| {
17793 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
17794 });
17795 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
17796 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
17797
17798 // Test finding task when cursor is on function name
17799 editor.change_selections(None, window, cx, |s| {
17800 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
17801 });
17802 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
17803 assert_eq!(row, 8, "Should find task when cursor is on function name");
17804 });
17805}
17806
17807#[gpui::test]
17808async fn test_folding_buffers(cx: &mut TestAppContext) {
17809 init_test(cx, |_| {});
17810
17811 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
17812 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
17813 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
17814
17815 let fs = FakeFs::new(cx.executor());
17816 fs.insert_tree(
17817 path!("/a"),
17818 json!({
17819 "first.rs": sample_text_1,
17820 "second.rs": sample_text_2,
17821 "third.rs": sample_text_3,
17822 }),
17823 )
17824 .await;
17825 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17826 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17827 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17828 let worktree = project.update(cx, |project, cx| {
17829 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
17830 assert_eq!(worktrees.len(), 1);
17831 worktrees.pop().unwrap()
17832 });
17833 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
17834
17835 let buffer_1 = project
17836 .update(cx, |project, cx| {
17837 project.open_buffer((worktree_id, "first.rs"), cx)
17838 })
17839 .await
17840 .unwrap();
17841 let buffer_2 = project
17842 .update(cx, |project, cx| {
17843 project.open_buffer((worktree_id, "second.rs"), cx)
17844 })
17845 .await
17846 .unwrap();
17847 let buffer_3 = project
17848 .update(cx, |project, cx| {
17849 project.open_buffer((worktree_id, "third.rs"), cx)
17850 })
17851 .await
17852 .unwrap();
17853
17854 let multi_buffer = cx.new(|cx| {
17855 let mut multi_buffer = MultiBuffer::new(ReadWrite);
17856 multi_buffer.push_excerpts(
17857 buffer_1.clone(),
17858 [
17859 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17860 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17861 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17862 ],
17863 cx,
17864 );
17865 multi_buffer.push_excerpts(
17866 buffer_2.clone(),
17867 [
17868 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17869 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17870 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17871 ],
17872 cx,
17873 );
17874 multi_buffer.push_excerpts(
17875 buffer_3.clone(),
17876 [
17877 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17878 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17879 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17880 ],
17881 cx,
17882 );
17883 multi_buffer
17884 });
17885 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
17886 Editor::new(
17887 EditorMode::full(),
17888 multi_buffer.clone(),
17889 Some(project.clone()),
17890 window,
17891 cx,
17892 )
17893 });
17894
17895 assert_eq!(
17896 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17897 "\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",
17898 );
17899
17900 multi_buffer_editor.update(cx, |editor, cx| {
17901 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
17902 });
17903 assert_eq!(
17904 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17905 "\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",
17906 "After folding the first buffer, its text should not be displayed"
17907 );
17908
17909 multi_buffer_editor.update(cx, |editor, cx| {
17910 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
17911 });
17912 assert_eq!(
17913 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17914 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
17915 "After folding the second buffer, its text should not be displayed"
17916 );
17917
17918 multi_buffer_editor.update(cx, |editor, cx| {
17919 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
17920 });
17921 assert_eq!(
17922 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17923 "\n\n\n\n\n",
17924 "After folding the third buffer, its text should not be displayed"
17925 );
17926
17927 // Emulate selection inside the fold logic, that should work
17928 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17929 editor
17930 .snapshot(window, cx)
17931 .next_line_boundary(Point::new(0, 4));
17932 });
17933
17934 multi_buffer_editor.update(cx, |editor, cx| {
17935 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
17936 });
17937 assert_eq!(
17938 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17939 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
17940 "After unfolding the second buffer, its text should be displayed"
17941 );
17942
17943 // Typing inside of buffer 1 causes that buffer to be unfolded.
17944 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17945 assert_eq!(
17946 multi_buffer
17947 .read(cx)
17948 .snapshot(cx)
17949 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
17950 .collect::<String>(),
17951 "bbbb"
17952 );
17953 editor.change_selections(None, window, cx, |selections| {
17954 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
17955 });
17956 editor.handle_input("B", window, cx);
17957 });
17958
17959 assert_eq!(
17960 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17961 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
17962 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
17963 );
17964
17965 multi_buffer_editor.update(cx, |editor, cx| {
17966 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
17967 });
17968 assert_eq!(
17969 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17970 "\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",
17971 "After unfolding the all buffers, all original text should be displayed"
17972 );
17973}
17974
17975#[gpui::test]
17976async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
17977 init_test(cx, |_| {});
17978
17979 let sample_text_1 = "1111\n2222\n3333".to_string();
17980 let sample_text_2 = "4444\n5555\n6666".to_string();
17981 let sample_text_3 = "7777\n8888\n9999".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 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
18027 cx,
18028 );
18029 multi_buffer.push_excerpts(
18030 buffer_2.clone(),
18031 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
18032 cx,
18033 );
18034 multi_buffer.push_excerpts(
18035 buffer_3.clone(),
18036 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
18037 cx,
18038 );
18039 multi_buffer
18040 });
18041
18042 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18043 Editor::new(
18044 EditorMode::full(),
18045 multi_buffer,
18046 Some(project.clone()),
18047 window,
18048 cx,
18049 )
18050 });
18051
18052 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
18053 assert_eq!(
18054 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18055 full_text,
18056 );
18057
18058 multi_buffer_editor.update(cx, |editor, cx| {
18059 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
18060 });
18061 assert_eq!(
18062 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18063 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
18064 "After folding the first buffer, its text should not be displayed"
18065 );
18066
18067 multi_buffer_editor.update(cx, |editor, cx| {
18068 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
18069 });
18070
18071 assert_eq!(
18072 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18073 "\n\n\n\n\n\n7777\n8888\n9999",
18074 "After folding the second buffer, its text should not be displayed"
18075 );
18076
18077 multi_buffer_editor.update(cx, |editor, cx| {
18078 editor.fold_buffer(buffer_3.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",
18083 "After folding the third buffer, its text should not be displayed"
18084 );
18085
18086 multi_buffer_editor.update(cx, |editor, cx| {
18087 editor.unfold_buffer(buffer_2.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\n4444\n5555\n6666\n\n",
18092 "After unfolding the second buffer, its text should be displayed"
18093 );
18094
18095 multi_buffer_editor.update(cx, |editor, cx| {
18096 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
18097 });
18098 assert_eq!(
18099 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18100 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
18101 "After unfolding the first buffer, its text should be displayed"
18102 );
18103
18104 multi_buffer_editor.update(cx, |editor, cx| {
18105 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
18106 });
18107 assert_eq!(
18108 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18109 full_text,
18110 "After unfolding all buffers, all original text should be displayed"
18111 );
18112}
18113
18114#[gpui::test]
18115async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
18116 init_test(cx, |_| {});
18117
18118 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
18119
18120 let fs = FakeFs::new(cx.executor());
18121 fs.insert_tree(
18122 path!("/a"),
18123 json!({
18124 "main.rs": sample_text,
18125 }),
18126 )
18127 .await;
18128 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18129 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18130 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18131 let worktree = project.update(cx, |project, cx| {
18132 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
18133 assert_eq!(worktrees.len(), 1);
18134 worktrees.pop().unwrap()
18135 });
18136 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
18137
18138 let buffer_1 = project
18139 .update(cx, |project, cx| {
18140 project.open_buffer((worktree_id, "main.rs"), cx)
18141 })
18142 .await
18143 .unwrap();
18144
18145 let multi_buffer = cx.new(|cx| {
18146 let mut multi_buffer = MultiBuffer::new(ReadWrite);
18147 multi_buffer.push_excerpts(
18148 buffer_1.clone(),
18149 [ExcerptRange::new(
18150 Point::new(0, 0)
18151 ..Point::new(
18152 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
18153 0,
18154 ),
18155 )],
18156 cx,
18157 );
18158 multi_buffer
18159 });
18160 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18161 Editor::new(
18162 EditorMode::full(),
18163 multi_buffer,
18164 Some(project.clone()),
18165 window,
18166 cx,
18167 )
18168 });
18169
18170 let selection_range = Point::new(1, 0)..Point::new(2, 0);
18171 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18172 enum TestHighlight {}
18173 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
18174 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
18175 editor.highlight_text::<TestHighlight>(
18176 vec![highlight_range.clone()],
18177 HighlightStyle::color(Hsla::green()),
18178 cx,
18179 );
18180 editor.change_selections(None, window, cx, |s| s.select_ranges(Some(highlight_range)));
18181 });
18182
18183 let full_text = format!("\n\n{sample_text}");
18184 assert_eq!(
18185 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18186 full_text,
18187 );
18188}
18189
18190#[gpui::test]
18191async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
18192 init_test(cx, |_| {});
18193 cx.update(|cx| {
18194 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
18195 "keymaps/default-linux.json",
18196 cx,
18197 )
18198 .unwrap();
18199 cx.bind_keys(default_key_bindings);
18200 });
18201
18202 let (editor, cx) = cx.add_window_view(|window, cx| {
18203 let multi_buffer = MultiBuffer::build_multi(
18204 [
18205 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
18206 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
18207 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
18208 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
18209 ],
18210 cx,
18211 );
18212 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
18213
18214 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
18215 // fold all but the second buffer, so that we test navigating between two
18216 // adjacent folded buffers, as well as folded buffers at the start and
18217 // end the multibuffer
18218 editor.fold_buffer(buffer_ids[0], cx);
18219 editor.fold_buffer(buffer_ids[2], cx);
18220 editor.fold_buffer(buffer_ids[3], cx);
18221
18222 editor
18223 });
18224 cx.simulate_resize(size(px(1000.), px(1000.)));
18225
18226 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
18227 cx.assert_excerpts_with_selections(indoc! {"
18228 [EXCERPT]
18229 ˇ[FOLDED]
18230 [EXCERPT]
18231 a1
18232 b1
18233 [EXCERPT]
18234 [FOLDED]
18235 [EXCERPT]
18236 [FOLDED]
18237 "
18238 });
18239 cx.simulate_keystroke("down");
18240 cx.assert_excerpts_with_selections(indoc! {"
18241 [EXCERPT]
18242 [FOLDED]
18243 [EXCERPT]
18244 ˇa1
18245 b1
18246 [EXCERPT]
18247 [FOLDED]
18248 [EXCERPT]
18249 [FOLDED]
18250 "
18251 });
18252 cx.simulate_keystroke("down");
18253 cx.assert_excerpts_with_selections(indoc! {"
18254 [EXCERPT]
18255 [FOLDED]
18256 [EXCERPT]
18257 a1
18258 ˇb1
18259 [EXCERPT]
18260 [FOLDED]
18261 [EXCERPT]
18262 [FOLDED]
18263 "
18264 });
18265 cx.simulate_keystroke("down");
18266 cx.assert_excerpts_with_selections(indoc! {"
18267 [EXCERPT]
18268 [FOLDED]
18269 [EXCERPT]
18270 a1
18271 b1
18272 ˇ[EXCERPT]
18273 [FOLDED]
18274 [EXCERPT]
18275 [FOLDED]
18276 "
18277 });
18278 cx.simulate_keystroke("down");
18279 cx.assert_excerpts_with_selections(indoc! {"
18280 [EXCERPT]
18281 [FOLDED]
18282 [EXCERPT]
18283 a1
18284 b1
18285 [EXCERPT]
18286 ˇ[FOLDED]
18287 [EXCERPT]
18288 [FOLDED]
18289 "
18290 });
18291 for _ in 0..5 {
18292 cx.simulate_keystroke("down");
18293 cx.assert_excerpts_with_selections(indoc! {"
18294 [EXCERPT]
18295 [FOLDED]
18296 [EXCERPT]
18297 a1
18298 b1
18299 [EXCERPT]
18300 [FOLDED]
18301 [EXCERPT]
18302 ˇ[FOLDED]
18303 "
18304 });
18305 }
18306
18307 cx.simulate_keystroke("up");
18308 cx.assert_excerpts_with_selections(indoc! {"
18309 [EXCERPT]
18310 [FOLDED]
18311 [EXCERPT]
18312 a1
18313 b1
18314 [EXCERPT]
18315 ˇ[FOLDED]
18316 [EXCERPT]
18317 [FOLDED]
18318 "
18319 });
18320 cx.simulate_keystroke("up");
18321 cx.assert_excerpts_with_selections(indoc! {"
18322 [EXCERPT]
18323 [FOLDED]
18324 [EXCERPT]
18325 a1
18326 b1
18327 ˇ[EXCERPT]
18328 [FOLDED]
18329 [EXCERPT]
18330 [FOLDED]
18331 "
18332 });
18333 cx.simulate_keystroke("up");
18334 cx.assert_excerpts_with_selections(indoc! {"
18335 [EXCERPT]
18336 [FOLDED]
18337 [EXCERPT]
18338 a1
18339 ˇb1
18340 [EXCERPT]
18341 [FOLDED]
18342 [EXCERPT]
18343 [FOLDED]
18344 "
18345 });
18346 cx.simulate_keystroke("up");
18347 cx.assert_excerpts_with_selections(indoc! {"
18348 [EXCERPT]
18349 [FOLDED]
18350 [EXCERPT]
18351 ˇa1
18352 b1
18353 [EXCERPT]
18354 [FOLDED]
18355 [EXCERPT]
18356 [FOLDED]
18357 "
18358 });
18359 for _ in 0..5 {
18360 cx.simulate_keystroke("up");
18361 cx.assert_excerpts_with_selections(indoc! {"
18362 [EXCERPT]
18363 ˇ[FOLDED]
18364 [EXCERPT]
18365 a1
18366 b1
18367 [EXCERPT]
18368 [FOLDED]
18369 [EXCERPT]
18370 [FOLDED]
18371 "
18372 });
18373 }
18374}
18375
18376#[gpui::test]
18377async fn test_inline_completion_text(cx: &mut TestAppContext) {
18378 init_test(cx, |_| {});
18379
18380 // Simple insertion
18381 assert_highlighted_edits(
18382 "Hello, world!",
18383 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
18384 true,
18385 cx,
18386 |highlighted_edits, cx| {
18387 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
18388 assert_eq!(highlighted_edits.highlights.len(), 1);
18389 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
18390 assert_eq!(
18391 highlighted_edits.highlights[0].1.background_color,
18392 Some(cx.theme().status().created_background)
18393 );
18394 },
18395 )
18396 .await;
18397
18398 // Replacement
18399 assert_highlighted_edits(
18400 "This is a test.",
18401 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
18402 false,
18403 cx,
18404 |highlighted_edits, cx| {
18405 assert_eq!(highlighted_edits.text, "That is a test.");
18406 assert_eq!(highlighted_edits.highlights.len(), 1);
18407 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
18408 assert_eq!(
18409 highlighted_edits.highlights[0].1.background_color,
18410 Some(cx.theme().status().created_background)
18411 );
18412 },
18413 )
18414 .await;
18415
18416 // Multiple edits
18417 assert_highlighted_edits(
18418 "Hello, world!",
18419 vec![
18420 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
18421 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
18422 ],
18423 false,
18424 cx,
18425 |highlighted_edits, cx| {
18426 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
18427 assert_eq!(highlighted_edits.highlights.len(), 2);
18428 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
18429 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
18430 assert_eq!(
18431 highlighted_edits.highlights[0].1.background_color,
18432 Some(cx.theme().status().created_background)
18433 );
18434 assert_eq!(
18435 highlighted_edits.highlights[1].1.background_color,
18436 Some(cx.theme().status().created_background)
18437 );
18438 },
18439 )
18440 .await;
18441
18442 // Multiple lines with edits
18443 assert_highlighted_edits(
18444 "First line\nSecond line\nThird line\nFourth line",
18445 vec![
18446 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
18447 (
18448 Point::new(2, 0)..Point::new(2, 10),
18449 "New third line".to_string(),
18450 ),
18451 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
18452 ],
18453 false,
18454 cx,
18455 |highlighted_edits, cx| {
18456 assert_eq!(
18457 highlighted_edits.text,
18458 "Second modified\nNew third line\nFourth updated line"
18459 );
18460 assert_eq!(highlighted_edits.highlights.len(), 3);
18461 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
18462 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
18463 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
18464 for highlight in &highlighted_edits.highlights {
18465 assert_eq!(
18466 highlight.1.background_color,
18467 Some(cx.theme().status().created_background)
18468 );
18469 }
18470 },
18471 )
18472 .await;
18473}
18474
18475#[gpui::test]
18476async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
18477 init_test(cx, |_| {});
18478
18479 // Deletion
18480 assert_highlighted_edits(
18481 "Hello, world!",
18482 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
18483 true,
18484 cx,
18485 |highlighted_edits, cx| {
18486 assert_eq!(highlighted_edits.text, "Hello, world!");
18487 assert_eq!(highlighted_edits.highlights.len(), 1);
18488 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
18489 assert_eq!(
18490 highlighted_edits.highlights[0].1.background_color,
18491 Some(cx.theme().status().deleted_background)
18492 );
18493 },
18494 )
18495 .await;
18496
18497 // Insertion
18498 assert_highlighted_edits(
18499 "Hello, world!",
18500 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
18501 true,
18502 cx,
18503 |highlighted_edits, cx| {
18504 assert_eq!(highlighted_edits.highlights.len(), 1);
18505 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
18506 assert_eq!(
18507 highlighted_edits.highlights[0].1.background_color,
18508 Some(cx.theme().status().created_background)
18509 );
18510 },
18511 )
18512 .await;
18513}
18514
18515async fn assert_highlighted_edits(
18516 text: &str,
18517 edits: Vec<(Range<Point>, String)>,
18518 include_deletions: bool,
18519 cx: &mut TestAppContext,
18520 assertion_fn: impl Fn(HighlightedText, &App),
18521) {
18522 let window = cx.add_window(|window, cx| {
18523 let buffer = MultiBuffer::build_simple(text, cx);
18524 Editor::new(EditorMode::full(), buffer, None, window, cx)
18525 });
18526 let cx = &mut VisualTestContext::from_window(*window, cx);
18527
18528 let (buffer, snapshot) = window
18529 .update(cx, |editor, _window, cx| {
18530 (
18531 editor.buffer().clone(),
18532 editor.buffer().read(cx).snapshot(cx),
18533 )
18534 })
18535 .unwrap();
18536
18537 let edits = edits
18538 .into_iter()
18539 .map(|(range, edit)| {
18540 (
18541 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
18542 edit,
18543 )
18544 })
18545 .collect::<Vec<_>>();
18546
18547 let text_anchor_edits = edits
18548 .clone()
18549 .into_iter()
18550 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
18551 .collect::<Vec<_>>();
18552
18553 let edit_preview = window
18554 .update(cx, |_, _window, cx| {
18555 buffer
18556 .read(cx)
18557 .as_singleton()
18558 .unwrap()
18559 .read(cx)
18560 .preview_edits(text_anchor_edits.into(), cx)
18561 })
18562 .unwrap()
18563 .await;
18564
18565 cx.update(|_window, cx| {
18566 let highlighted_edits = inline_completion_edit_text(
18567 &snapshot.as_singleton().unwrap().2,
18568 &edits,
18569 &edit_preview,
18570 include_deletions,
18571 cx,
18572 );
18573 assertion_fn(highlighted_edits, cx)
18574 });
18575}
18576
18577#[track_caller]
18578fn assert_breakpoint(
18579 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
18580 path: &Arc<Path>,
18581 expected: Vec<(u32, Breakpoint)>,
18582) {
18583 if expected.len() == 0usize {
18584 assert!(!breakpoints.contains_key(path), "{}", path.display());
18585 } else {
18586 let mut breakpoint = breakpoints
18587 .get(path)
18588 .unwrap()
18589 .into_iter()
18590 .map(|breakpoint| {
18591 (
18592 breakpoint.row,
18593 Breakpoint {
18594 message: breakpoint.message.clone(),
18595 state: breakpoint.state,
18596 condition: breakpoint.condition.clone(),
18597 hit_condition: breakpoint.hit_condition.clone(),
18598 },
18599 )
18600 })
18601 .collect::<Vec<_>>();
18602
18603 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
18604
18605 assert_eq!(expected, breakpoint);
18606 }
18607}
18608
18609fn add_log_breakpoint_at_cursor(
18610 editor: &mut Editor,
18611 log_message: &str,
18612 window: &mut Window,
18613 cx: &mut Context<Editor>,
18614) {
18615 let (anchor, bp) = editor
18616 .breakpoints_at_cursors(window, cx)
18617 .first()
18618 .and_then(|(anchor, bp)| {
18619 if let Some(bp) = bp {
18620 Some((*anchor, bp.clone()))
18621 } else {
18622 None
18623 }
18624 })
18625 .unwrap_or_else(|| {
18626 let cursor_position: Point = editor.selections.newest(cx).head();
18627
18628 let breakpoint_position = editor
18629 .snapshot(window, cx)
18630 .display_snapshot
18631 .buffer_snapshot
18632 .anchor_before(Point::new(cursor_position.row, 0));
18633
18634 (breakpoint_position, Breakpoint::new_log(&log_message))
18635 });
18636
18637 editor.edit_breakpoint_at_anchor(
18638 anchor,
18639 bp,
18640 BreakpointEditAction::EditLogMessage(log_message.into()),
18641 cx,
18642 );
18643}
18644
18645#[gpui::test]
18646async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
18647 init_test(cx, |_| {});
18648
18649 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
18650 let fs = FakeFs::new(cx.executor());
18651 fs.insert_tree(
18652 path!("/a"),
18653 json!({
18654 "main.rs": sample_text,
18655 }),
18656 )
18657 .await;
18658 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18659 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18660 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18661
18662 let fs = FakeFs::new(cx.executor());
18663 fs.insert_tree(
18664 path!("/a"),
18665 json!({
18666 "main.rs": sample_text,
18667 }),
18668 )
18669 .await;
18670 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18671 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18672 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18673 let worktree_id = workspace
18674 .update(cx, |workspace, _window, cx| {
18675 workspace.project().update(cx, |project, cx| {
18676 project.worktrees(cx).next().unwrap().read(cx).id()
18677 })
18678 })
18679 .unwrap();
18680
18681 let buffer = project
18682 .update(cx, |project, cx| {
18683 project.open_buffer((worktree_id, "main.rs"), cx)
18684 })
18685 .await
18686 .unwrap();
18687
18688 let (editor, cx) = cx.add_window_view(|window, cx| {
18689 Editor::new(
18690 EditorMode::full(),
18691 MultiBuffer::build_from_buffer(buffer, cx),
18692 Some(project.clone()),
18693 window,
18694 cx,
18695 )
18696 });
18697
18698 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
18699 let abs_path = project.read_with(cx, |project, cx| {
18700 project
18701 .absolute_path(&project_path, cx)
18702 .map(|path_buf| Arc::from(path_buf.to_owned()))
18703 .unwrap()
18704 });
18705
18706 // assert we can add breakpoint on the first line
18707 editor.update_in(cx, |editor, window, cx| {
18708 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18709 editor.move_to_end(&MoveToEnd, window, cx);
18710 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18711 });
18712
18713 let breakpoints = editor.update(cx, |editor, cx| {
18714 editor
18715 .breakpoint_store()
18716 .as_ref()
18717 .unwrap()
18718 .read(cx)
18719 .all_breakpoints(cx)
18720 .clone()
18721 });
18722
18723 assert_eq!(1, breakpoints.len());
18724 assert_breakpoint(
18725 &breakpoints,
18726 &abs_path,
18727 vec![
18728 (0, Breakpoint::new_standard()),
18729 (3, Breakpoint::new_standard()),
18730 ],
18731 );
18732
18733 editor.update_in(cx, |editor, window, cx| {
18734 editor.move_to_beginning(&MoveToBeginning, window, cx);
18735 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18736 });
18737
18738 let breakpoints = editor.update(cx, |editor, cx| {
18739 editor
18740 .breakpoint_store()
18741 .as_ref()
18742 .unwrap()
18743 .read(cx)
18744 .all_breakpoints(cx)
18745 .clone()
18746 });
18747
18748 assert_eq!(1, breakpoints.len());
18749 assert_breakpoint(
18750 &breakpoints,
18751 &abs_path,
18752 vec![(3, Breakpoint::new_standard())],
18753 );
18754
18755 editor.update_in(cx, |editor, window, cx| {
18756 editor.move_to_end(&MoveToEnd, window, cx);
18757 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18758 });
18759
18760 let breakpoints = editor.update(cx, |editor, cx| {
18761 editor
18762 .breakpoint_store()
18763 .as_ref()
18764 .unwrap()
18765 .read(cx)
18766 .all_breakpoints(cx)
18767 .clone()
18768 });
18769
18770 assert_eq!(0, breakpoints.len());
18771 assert_breakpoint(&breakpoints, &abs_path, vec![]);
18772}
18773
18774#[gpui::test]
18775async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
18776 init_test(cx, |_| {});
18777
18778 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
18779
18780 let fs = FakeFs::new(cx.executor());
18781 fs.insert_tree(
18782 path!("/a"),
18783 json!({
18784 "main.rs": sample_text,
18785 }),
18786 )
18787 .await;
18788 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18789 let (workspace, cx) =
18790 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
18791
18792 let worktree_id = workspace.update(cx, |workspace, cx| {
18793 workspace.project().update(cx, |project, cx| {
18794 project.worktrees(cx).next().unwrap().read(cx).id()
18795 })
18796 });
18797
18798 let buffer = project
18799 .update(cx, |project, cx| {
18800 project.open_buffer((worktree_id, "main.rs"), cx)
18801 })
18802 .await
18803 .unwrap();
18804
18805 let (editor, cx) = cx.add_window_view(|window, cx| {
18806 Editor::new(
18807 EditorMode::full(),
18808 MultiBuffer::build_from_buffer(buffer, cx),
18809 Some(project.clone()),
18810 window,
18811 cx,
18812 )
18813 });
18814
18815 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
18816 let abs_path = project.read_with(cx, |project, cx| {
18817 project
18818 .absolute_path(&project_path, cx)
18819 .map(|path_buf| Arc::from(path_buf.to_owned()))
18820 .unwrap()
18821 });
18822
18823 editor.update_in(cx, |editor, window, cx| {
18824 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
18825 });
18826
18827 let breakpoints = editor.update(cx, |editor, cx| {
18828 editor
18829 .breakpoint_store()
18830 .as_ref()
18831 .unwrap()
18832 .read(cx)
18833 .all_breakpoints(cx)
18834 .clone()
18835 });
18836
18837 assert_breakpoint(
18838 &breakpoints,
18839 &abs_path,
18840 vec![(0, Breakpoint::new_log("hello world"))],
18841 );
18842
18843 // Removing a log message from a log breakpoint should remove it
18844 editor.update_in(cx, |editor, window, cx| {
18845 add_log_breakpoint_at_cursor(editor, "", window, cx);
18846 });
18847
18848 let breakpoints = editor.update(cx, |editor, cx| {
18849 editor
18850 .breakpoint_store()
18851 .as_ref()
18852 .unwrap()
18853 .read(cx)
18854 .all_breakpoints(cx)
18855 .clone()
18856 });
18857
18858 assert_breakpoint(&breakpoints, &abs_path, vec![]);
18859
18860 editor.update_in(cx, |editor, window, cx| {
18861 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18862 editor.move_to_end(&MoveToEnd, window, cx);
18863 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18864 // Not adding a log message to a standard breakpoint shouldn't remove it
18865 add_log_breakpoint_at_cursor(editor, "", window, cx);
18866 });
18867
18868 let breakpoints = editor.update(cx, |editor, cx| {
18869 editor
18870 .breakpoint_store()
18871 .as_ref()
18872 .unwrap()
18873 .read(cx)
18874 .all_breakpoints(cx)
18875 .clone()
18876 });
18877
18878 assert_breakpoint(
18879 &breakpoints,
18880 &abs_path,
18881 vec![
18882 (0, Breakpoint::new_standard()),
18883 (3, Breakpoint::new_standard()),
18884 ],
18885 );
18886
18887 editor.update_in(cx, |editor, window, cx| {
18888 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
18889 });
18890
18891 let breakpoints = editor.update(cx, |editor, cx| {
18892 editor
18893 .breakpoint_store()
18894 .as_ref()
18895 .unwrap()
18896 .read(cx)
18897 .all_breakpoints(cx)
18898 .clone()
18899 });
18900
18901 assert_breakpoint(
18902 &breakpoints,
18903 &abs_path,
18904 vec![
18905 (0, Breakpoint::new_standard()),
18906 (3, Breakpoint::new_log("hello world")),
18907 ],
18908 );
18909
18910 editor.update_in(cx, |editor, window, cx| {
18911 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
18912 });
18913
18914 let breakpoints = editor.update(cx, |editor, cx| {
18915 editor
18916 .breakpoint_store()
18917 .as_ref()
18918 .unwrap()
18919 .read(cx)
18920 .all_breakpoints(cx)
18921 .clone()
18922 });
18923
18924 assert_breakpoint(
18925 &breakpoints,
18926 &abs_path,
18927 vec![
18928 (0, Breakpoint::new_standard()),
18929 (3, Breakpoint::new_log("hello Earth!!")),
18930 ],
18931 );
18932}
18933
18934/// This also tests that Editor::breakpoint_at_cursor_head is working properly
18935/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
18936/// or when breakpoints were placed out of order. This tests for a regression too
18937#[gpui::test]
18938async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
18939 init_test(cx, |_| {});
18940
18941 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
18942 let fs = FakeFs::new(cx.executor());
18943 fs.insert_tree(
18944 path!("/a"),
18945 json!({
18946 "main.rs": sample_text,
18947 }),
18948 )
18949 .await;
18950 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18951 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18952 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18953
18954 let fs = FakeFs::new(cx.executor());
18955 fs.insert_tree(
18956 path!("/a"),
18957 json!({
18958 "main.rs": sample_text,
18959 }),
18960 )
18961 .await;
18962 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18963 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18964 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18965 let worktree_id = workspace
18966 .update(cx, |workspace, _window, cx| {
18967 workspace.project().update(cx, |project, cx| {
18968 project.worktrees(cx).next().unwrap().read(cx).id()
18969 })
18970 })
18971 .unwrap();
18972
18973 let buffer = project
18974 .update(cx, |project, cx| {
18975 project.open_buffer((worktree_id, "main.rs"), cx)
18976 })
18977 .await
18978 .unwrap();
18979
18980 let (editor, cx) = cx.add_window_view(|window, cx| {
18981 Editor::new(
18982 EditorMode::full(),
18983 MultiBuffer::build_from_buffer(buffer, cx),
18984 Some(project.clone()),
18985 window,
18986 cx,
18987 )
18988 });
18989
18990 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
18991 let abs_path = project.read_with(cx, |project, cx| {
18992 project
18993 .absolute_path(&project_path, cx)
18994 .map(|path_buf| Arc::from(path_buf.to_owned()))
18995 .unwrap()
18996 });
18997
18998 // assert we can add breakpoint on the first line
18999 editor.update_in(cx, |editor, window, cx| {
19000 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19001 editor.move_to_end(&MoveToEnd, window, cx);
19002 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19003 editor.move_up(&MoveUp, window, cx);
19004 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19005 });
19006
19007 let breakpoints = editor.update(cx, |editor, cx| {
19008 editor
19009 .breakpoint_store()
19010 .as_ref()
19011 .unwrap()
19012 .read(cx)
19013 .all_breakpoints(cx)
19014 .clone()
19015 });
19016
19017 assert_eq!(1, breakpoints.len());
19018 assert_breakpoint(
19019 &breakpoints,
19020 &abs_path,
19021 vec![
19022 (0, Breakpoint::new_standard()),
19023 (2, Breakpoint::new_standard()),
19024 (3, Breakpoint::new_standard()),
19025 ],
19026 );
19027
19028 editor.update_in(cx, |editor, window, cx| {
19029 editor.move_to_beginning(&MoveToBeginning, window, cx);
19030 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
19031 editor.move_to_end(&MoveToEnd, window, cx);
19032 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
19033 // Disabling a breakpoint that doesn't exist should do nothing
19034 editor.move_up(&MoveUp, window, cx);
19035 editor.move_up(&MoveUp, window, cx);
19036 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
19037 });
19038
19039 let breakpoints = editor.update(cx, |editor, cx| {
19040 editor
19041 .breakpoint_store()
19042 .as_ref()
19043 .unwrap()
19044 .read(cx)
19045 .all_breakpoints(cx)
19046 .clone()
19047 });
19048
19049 let disable_breakpoint = {
19050 let mut bp = Breakpoint::new_standard();
19051 bp.state = BreakpointState::Disabled;
19052 bp
19053 };
19054
19055 assert_eq!(1, breakpoints.len());
19056 assert_breakpoint(
19057 &breakpoints,
19058 &abs_path,
19059 vec![
19060 (0, disable_breakpoint.clone()),
19061 (2, Breakpoint::new_standard()),
19062 (3, disable_breakpoint.clone()),
19063 ],
19064 );
19065
19066 editor.update_in(cx, |editor, window, cx| {
19067 editor.move_to_beginning(&MoveToBeginning, window, cx);
19068 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
19069 editor.move_to_end(&MoveToEnd, window, cx);
19070 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
19071 editor.move_up(&MoveUp, window, cx);
19072 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
19073 });
19074
19075 let breakpoints = editor.update(cx, |editor, cx| {
19076 editor
19077 .breakpoint_store()
19078 .as_ref()
19079 .unwrap()
19080 .read(cx)
19081 .all_breakpoints(cx)
19082 .clone()
19083 });
19084
19085 assert_eq!(1, breakpoints.len());
19086 assert_breakpoint(
19087 &breakpoints,
19088 &abs_path,
19089 vec![
19090 (0, Breakpoint::new_standard()),
19091 (2, disable_breakpoint),
19092 (3, Breakpoint::new_standard()),
19093 ],
19094 );
19095}
19096
19097#[gpui::test]
19098async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
19099 init_test(cx, |_| {});
19100 let capabilities = lsp::ServerCapabilities {
19101 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
19102 prepare_provider: Some(true),
19103 work_done_progress_options: Default::default(),
19104 })),
19105 ..Default::default()
19106 };
19107 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
19108
19109 cx.set_state(indoc! {"
19110 struct Fˇoo {}
19111 "});
19112
19113 cx.update_editor(|editor, _, cx| {
19114 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
19115 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
19116 editor.highlight_background::<DocumentHighlightRead>(
19117 &[highlight_range],
19118 |c| c.editor_document_highlight_read_background,
19119 cx,
19120 );
19121 });
19122
19123 let mut prepare_rename_handler = cx
19124 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
19125 move |_, _, _| async move {
19126 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
19127 start: lsp::Position {
19128 line: 0,
19129 character: 7,
19130 },
19131 end: lsp::Position {
19132 line: 0,
19133 character: 10,
19134 },
19135 })))
19136 },
19137 );
19138 let prepare_rename_task = cx
19139 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
19140 .expect("Prepare rename was not started");
19141 prepare_rename_handler.next().await.unwrap();
19142 prepare_rename_task.await.expect("Prepare rename failed");
19143
19144 let mut rename_handler =
19145 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
19146 let edit = lsp::TextEdit {
19147 range: lsp::Range {
19148 start: lsp::Position {
19149 line: 0,
19150 character: 7,
19151 },
19152 end: lsp::Position {
19153 line: 0,
19154 character: 10,
19155 },
19156 },
19157 new_text: "FooRenamed".to_string(),
19158 };
19159 Ok(Some(lsp::WorkspaceEdit::new(
19160 // Specify the same edit twice
19161 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
19162 )))
19163 });
19164 let rename_task = cx
19165 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
19166 .expect("Confirm rename was not started");
19167 rename_handler.next().await.unwrap();
19168 rename_task.await.expect("Confirm rename failed");
19169 cx.run_until_parked();
19170
19171 // Despite two edits, only one is actually applied as those are identical
19172 cx.assert_editor_state(indoc! {"
19173 struct FooRenamedˇ {}
19174 "});
19175}
19176
19177#[gpui::test]
19178async fn test_rename_without_prepare(cx: &mut TestAppContext) {
19179 init_test(cx, |_| {});
19180 // These capabilities indicate that the server does not support prepare rename.
19181 let capabilities = lsp::ServerCapabilities {
19182 rename_provider: Some(lsp::OneOf::Left(true)),
19183 ..Default::default()
19184 };
19185 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
19186
19187 cx.set_state(indoc! {"
19188 struct Fˇoo {}
19189 "});
19190
19191 cx.update_editor(|editor, _window, cx| {
19192 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
19193 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
19194 editor.highlight_background::<DocumentHighlightRead>(
19195 &[highlight_range],
19196 |c| c.editor_document_highlight_read_background,
19197 cx,
19198 );
19199 });
19200
19201 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
19202 .expect("Prepare rename was not started")
19203 .await
19204 .expect("Prepare rename failed");
19205
19206 let mut rename_handler =
19207 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
19208 let edit = lsp::TextEdit {
19209 range: lsp::Range {
19210 start: lsp::Position {
19211 line: 0,
19212 character: 7,
19213 },
19214 end: lsp::Position {
19215 line: 0,
19216 character: 10,
19217 },
19218 },
19219 new_text: "FooRenamed".to_string(),
19220 };
19221 Ok(Some(lsp::WorkspaceEdit::new(
19222 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
19223 )))
19224 });
19225 let rename_task = cx
19226 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
19227 .expect("Confirm rename was not started");
19228 rename_handler.next().await.unwrap();
19229 rename_task.await.expect("Confirm rename failed");
19230 cx.run_until_parked();
19231
19232 // Correct range is renamed, as `surrounding_word` is used to find it.
19233 cx.assert_editor_state(indoc! {"
19234 struct FooRenamedˇ {}
19235 "});
19236}
19237
19238#[gpui::test]
19239async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
19240 init_test(cx, |_| {});
19241 let mut cx = EditorTestContext::new(cx).await;
19242
19243 let language = Arc::new(
19244 Language::new(
19245 LanguageConfig::default(),
19246 Some(tree_sitter_html::LANGUAGE.into()),
19247 )
19248 .with_brackets_query(
19249 r#"
19250 ("<" @open "/>" @close)
19251 ("</" @open ">" @close)
19252 ("<" @open ">" @close)
19253 ("\"" @open "\"" @close)
19254 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
19255 "#,
19256 )
19257 .unwrap(),
19258 );
19259 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
19260
19261 cx.set_state(indoc! {"
19262 <span>ˇ</span>
19263 "});
19264 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
19265 cx.assert_editor_state(indoc! {"
19266 <span>
19267 ˇ
19268 </span>
19269 "});
19270
19271 cx.set_state(indoc! {"
19272 <span><span></span>ˇ</span>
19273 "});
19274 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
19275 cx.assert_editor_state(indoc! {"
19276 <span><span></span>
19277 ˇ</span>
19278 "});
19279
19280 cx.set_state(indoc! {"
19281 <span>ˇ
19282 </span>
19283 "});
19284 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
19285 cx.assert_editor_state(indoc! {"
19286 <span>
19287 ˇ
19288 </span>
19289 "});
19290}
19291
19292#[gpui::test(iterations = 10)]
19293async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
19294 init_test(cx, |_| {});
19295
19296 let fs = FakeFs::new(cx.executor());
19297 fs.insert_tree(
19298 path!("/dir"),
19299 json!({
19300 "a.ts": "a",
19301 }),
19302 )
19303 .await;
19304
19305 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
19306 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19307 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19308
19309 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
19310 language_registry.add(Arc::new(Language::new(
19311 LanguageConfig {
19312 name: "TypeScript".into(),
19313 matcher: LanguageMatcher {
19314 path_suffixes: vec!["ts".to_string()],
19315 ..Default::default()
19316 },
19317 ..Default::default()
19318 },
19319 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
19320 )));
19321 let mut fake_language_servers = language_registry.register_fake_lsp(
19322 "TypeScript",
19323 FakeLspAdapter {
19324 capabilities: lsp::ServerCapabilities {
19325 code_lens_provider: Some(lsp::CodeLensOptions {
19326 resolve_provider: Some(true),
19327 }),
19328 execute_command_provider: Some(lsp::ExecuteCommandOptions {
19329 commands: vec!["_the/command".to_string()],
19330 ..lsp::ExecuteCommandOptions::default()
19331 }),
19332 ..lsp::ServerCapabilities::default()
19333 },
19334 ..FakeLspAdapter::default()
19335 },
19336 );
19337
19338 let (buffer, _handle) = project
19339 .update(cx, |p, cx| {
19340 p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
19341 })
19342 .await
19343 .unwrap();
19344 cx.executor().run_until_parked();
19345
19346 let fake_server = fake_language_servers.next().await.unwrap();
19347
19348 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
19349 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
19350 drop(buffer_snapshot);
19351 let actions = cx
19352 .update_window(*workspace, |_, window, cx| {
19353 project.code_actions(&buffer, anchor..anchor, window, cx)
19354 })
19355 .unwrap();
19356
19357 fake_server
19358 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
19359 Ok(Some(vec![
19360 lsp::CodeLens {
19361 range: lsp::Range::default(),
19362 command: Some(lsp::Command {
19363 title: "Code lens command".to_owned(),
19364 command: "_the/command".to_owned(),
19365 arguments: None,
19366 }),
19367 data: None,
19368 },
19369 lsp::CodeLens {
19370 range: lsp::Range::default(),
19371 command: Some(lsp::Command {
19372 title: "Command not in capabilities".to_owned(),
19373 command: "not in capabilities".to_owned(),
19374 arguments: None,
19375 }),
19376 data: None,
19377 },
19378 lsp::CodeLens {
19379 range: lsp::Range {
19380 start: lsp::Position {
19381 line: 1,
19382 character: 1,
19383 },
19384 end: lsp::Position {
19385 line: 1,
19386 character: 1,
19387 },
19388 },
19389 command: Some(lsp::Command {
19390 title: "Command not in range".to_owned(),
19391 command: "_the/command".to_owned(),
19392 arguments: None,
19393 }),
19394 data: None,
19395 },
19396 ]))
19397 })
19398 .next()
19399 .await;
19400
19401 let actions = actions.await.unwrap();
19402 assert_eq!(
19403 actions.len(),
19404 1,
19405 "Should have only one valid action for the 0..0 range"
19406 );
19407 let action = actions[0].clone();
19408 let apply = project.update(cx, |project, cx| {
19409 project.apply_code_action(buffer.clone(), action, true, cx)
19410 });
19411
19412 // Resolving the code action does not populate its edits. In absence of
19413 // edits, we must execute the given command.
19414 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
19415 |mut lens, _| async move {
19416 let lens_command = lens.command.as_mut().expect("should have a command");
19417 assert_eq!(lens_command.title, "Code lens command");
19418 lens_command.arguments = Some(vec![json!("the-argument")]);
19419 Ok(lens)
19420 },
19421 );
19422
19423 // While executing the command, the language server sends the editor
19424 // a `workspaceEdit` request.
19425 fake_server
19426 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
19427 let fake = fake_server.clone();
19428 move |params, _| {
19429 assert_eq!(params.command, "_the/command");
19430 let fake = fake.clone();
19431 async move {
19432 fake.server
19433 .request::<lsp::request::ApplyWorkspaceEdit>(
19434 lsp::ApplyWorkspaceEditParams {
19435 label: None,
19436 edit: lsp::WorkspaceEdit {
19437 changes: Some(
19438 [(
19439 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
19440 vec![lsp::TextEdit {
19441 range: lsp::Range::new(
19442 lsp::Position::new(0, 0),
19443 lsp::Position::new(0, 0),
19444 ),
19445 new_text: "X".into(),
19446 }],
19447 )]
19448 .into_iter()
19449 .collect(),
19450 ),
19451 ..Default::default()
19452 },
19453 },
19454 )
19455 .await
19456 .into_response()
19457 .unwrap();
19458 Ok(Some(json!(null)))
19459 }
19460 }
19461 })
19462 .next()
19463 .await;
19464
19465 // Applying the code lens command returns a project transaction containing the edits
19466 // sent by the language server in its `workspaceEdit` request.
19467 let transaction = apply.await.unwrap();
19468 assert!(transaction.0.contains_key(&buffer));
19469 buffer.update(cx, |buffer, cx| {
19470 assert_eq!(buffer.text(), "Xa");
19471 buffer.undo(cx);
19472 assert_eq!(buffer.text(), "a");
19473 });
19474}
19475
19476#[gpui::test]
19477async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
19478 init_test(cx, |_| {});
19479
19480 let fs = FakeFs::new(cx.executor());
19481 let main_text = r#"fn main() {
19482println!("1");
19483println!("2");
19484println!("3");
19485println!("4");
19486println!("5");
19487}"#;
19488 let lib_text = "mod foo {}";
19489 fs.insert_tree(
19490 path!("/a"),
19491 json!({
19492 "lib.rs": lib_text,
19493 "main.rs": main_text,
19494 }),
19495 )
19496 .await;
19497
19498 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19499 let (workspace, cx) =
19500 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
19501 let worktree_id = workspace.update(cx, |workspace, cx| {
19502 workspace.project().update(cx, |project, cx| {
19503 project.worktrees(cx).next().unwrap().read(cx).id()
19504 })
19505 });
19506
19507 let expected_ranges = vec![
19508 Point::new(0, 0)..Point::new(0, 0),
19509 Point::new(1, 0)..Point::new(1, 1),
19510 Point::new(2, 0)..Point::new(2, 2),
19511 Point::new(3, 0)..Point::new(3, 3),
19512 ];
19513
19514 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
19515 let editor_1 = workspace
19516 .update_in(cx, |workspace, window, cx| {
19517 workspace.open_path(
19518 (worktree_id, "main.rs"),
19519 Some(pane_1.downgrade()),
19520 true,
19521 window,
19522 cx,
19523 )
19524 })
19525 .unwrap()
19526 .await
19527 .downcast::<Editor>()
19528 .unwrap();
19529 pane_1.update(cx, |pane, cx| {
19530 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19531 open_editor.update(cx, |editor, cx| {
19532 assert_eq!(
19533 editor.display_text(cx),
19534 main_text,
19535 "Original main.rs text on initial open",
19536 );
19537 assert_eq!(
19538 editor
19539 .selections
19540 .all::<Point>(cx)
19541 .into_iter()
19542 .map(|s| s.range())
19543 .collect::<Vec<_>>(),
19544 vec![Point::zero()..Point::zero()],
19545 "Default selections on initial open",
19546 );
19547 })
19548 });
19549 editor_1.update_in(cx, |editor, window, cx| {
19550 editor.change_selections(None, window, cx, |s| {
19551 s.select_ranges(expected_ranges.clone());
19552 });
19553 });
19554
19555 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
19556 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
19557 });
19558 let editor_2 = workspace
19559 .update_in(cx, |workspace, window, cx| {
19560 workspace.open_path(
19561 (worktree_id, "main.rs"),
19562 Some(pane_2.downgrade()),
19563 true,
19564 window,
19565 cx,
19566 )
19567 })
19568 .unwrap()
19569 .await
19570 .downcast::<Editor>()
19571 .unwrap();
19572 pane_2.update(cx, |pane, cx| {
19573 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19574 open_editor.update(cx, |editor, cx| {
19575 assert_eq!(
19576 editor.display_text(cx),
19577 main_text,
19578 "Original main.rs text on initial open in another panel",
19579 );
19580 assert_eq!(
19581 editor
19582 .selections
19583 .all::<Point>(cx)
19584 .into_iter()
19585 .map(|s| s.range())
19586 .collect::<Vec<_>>(),
19587 vec![Point::zero()..Point::zero()],
19588 "Default selections on initial open in another panel",
19589 );
19590 })
19591 });
19592
19593 editor_2.update_in(cx, |editor, window, cx| {
19594 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
19595 });
19596
19597 let _other_editor_1 = workspace
19598 .update_in(cx, |workspace, window, cx| {
19599 workspace.open_path(
19600 (worktree_id, "lib.rs"),
19601 Some(pane_1.downgrade()),
19602 true,
19603 window,
19604 cx,
19605 )
19606 })
19607 .unwrap()
19608 .await
19609 .downcast::<Editor>()
19610 .unwrap();
19611 pane_1
19612 .update_in(cx, |pane, window, cx| {
19613 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
19614 .unwrap()
19615 })
19616 .await
19617 .unwrap();
19618 drop(editor_1);
19619 pane_1.update(cx, |pane, cx| {
19620 pane.active_item()
19621 .unwrap()
19622 .downcast::<Editor>()
19623 .unwrap()
19624 .update(cx, |editor, cx| {
19625 assert_eq!(
19626 editor.display_text(cx),
19627 lib_text,
19628 "Other file should be open and active",
19629 );
19630 });
19631 assert_eq!(pane.items().count(), 1, "No other editors should be open");
19632 });
19633
19634 let _other_editor_2 = workspace
19635 .update_in(cx, |workspace, window, cx| {
19636 workspace.open_path(
19637 (worktree_id, "lib.rs"),
19638 Some(pane_2.downgrade()),
19639 true,
19640 window,
19641 cx,
19642 )
19643 })
19644 .unwrap()
19645 .await
19646 .downcast::<Editor>()
19647 .unwrap();
19648 pane_2
19649 .update_in(cx, |pane, window, cx| {
19650 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
19651 .unwrap()
19652 })
19653 .await
19654 .unwrap();
19655 drop(editor_2);
19656 pane_2.update(cx, |pane, cx| {
19657 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19658 open_editor.update(cx, |editor, cx| {
19659 assert_eq!(
19660 editor.display_text(cx),
19661 lib_text,
19662 "Other file should be open and active in another panel too",
19663 );
19664 });
19665 assert_eq!(
19666 pane.items().count(),
19667 1,
19668 "No other editors should be open in another pane",
19669 );
19670 });
19671
19672 let _editor_1_reopened = workspace
19673 .update_in(cx, |workspace, window, cx| {
19674 workspace.open_path(
19675 (worktree_id, "main.rs"),
19676 Some(pane_1.downgrade()),
19677 true,
19678 window,
19679 cx,
19680 )
19681 })
19682 .unwrap()
19683 .await
19684 .downcast::<Editor>()
19685 .unwrap();
19686 let _editor_2_reopened = workspace
19687 .update_in(cx, |workspace, window, cx| {
19688 workspace.open_path(
19689 (worktree_id, "main.rs"),
19690 Some(pane_2.downgrade()),
19691 true,
19692 window,
19693 cx,
19694 )
19695 })
19696 .unwrap()
19697 .await
19698 .downcast::<Editor>()
19699 .unwrap();
19700 pane_1.update(cx, |pane, cx| {
19701 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19702 open_editor.update(cx, |editor, cx| {
19703 assert_eq!(
19704 editor.display_text(cx),
19705 main_text,
19706 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
19707 );
19708 assert_eq!(
19709 editor
19710 .selections
19711 .all::<Point>(cx)
19712 .into_iter()
19713 .map(|s| s.range())
19714 .collect::<Vec<_>>(),
19715 expected_ranges,
19716 "Previous editor in the 1st panel had selections and should get them restored on reopen",
19717 );
19718 })
19719 });
19720 pane_2.update(cx, |pane, cx| {
19721 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19722 open_editor.update(cx, |editor, cx| {
19723 assert_eq!(
19724 editor.display_text(cx),
19725 r#"fn main() {
19726⋯rintln!("1");
19727⋯intln!("2");
19728⋯ntln!("3");
19729println!("4");
19730println!("5");
19731}"#,
19732 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
19733 );
19734 assert_eq!(
19735 editor
19736 .selections
19737 .all::<Point>(cx)
19738 .into_iter()
19739 .map(|s| s.range())
19740 .collect::<Vec<_>>(),
19741 vec![Point::zero()..Point::zero()],
19742 "Previous editor in the 2nd pane had no selections changed hence should restore none",
19743 );
19744 })
19745 });
19746}
19747
19748#[gpui::test]
19749async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
19750 init_test(cx, |_| {});
19751
19752 let fs = FakeFs::new(cx.executor());
19753 let main_text = r#"fn main() {
19754println!("1");
19755println!("2");
19756println!("3");
19757println!("4");
19758println!("5");
19759}"#;
19760 let lib_text = "mod foo {}";
19761 fs.insert_tree(
19762 path!("/a"),
19763 json!({
19764 "lib.rs": lib_text,
19765 "main.rs": main_text,
19766 }),
19767 )
19768 .await;
19769
19770 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19771 let (workspace, cx) =
19772 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
19773 let worktree_id = workspace.update(cx, |workspace, cx| {
19774 workspace.project().update(cx, |project, cx| {
19775 project.worktrees(cx).next().unwrap().read(cx).id()
19776 })
19777 });
19778
19779 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
19780 let editor = workspace
19781 .update_in(cx, |workspace, window, cx| {
19782 workspace.open_path(
19783 (worktree_id, "main.rs"),
19784 Some(pane.downgrade()),
19785 true,
19786 window,
19787 cx,
19788 )
19789 })
19790 .unwrap()
19791 .await
19792 .downcast::<Editor>()
19793 .unwrap();
19794 pane.update(cx, |pane, cx| {
19795 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19796 open_editor.update(cx, |editor, cx| {
19797 assert_eq!(
19798 editor.display_text(cx),
19799 main_text,
19800 "Original main.rs text on initial open",
19801 );
19802 })
19803 });
19804 editor.update_in(cx, |editor, window, cx| {
19805 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
19806 });
19807
19808 cx.update_global(|store: &mut SettingsStore, cx| {
19809 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
19810 s.restore_on_file_reopen = Some(false);
19811 });
19812 });
19813 editor.update_in(cx, |editor, window, cx| {
19814 editor.fold_ranges(
19815 vec![
19816 Point::new(1, 0)..Point::new(1, 1),
19817 Point::new(2, 0)..Point::new(2, 2),
19818 Point::new(3, 0)..Point::new(3, 3),
19819 ],
19820 false,
19821 window,
19822 cx,
19823 );
19824 });
19825 pane.update_in(cx, |pane, window, cx| {
19826 pane.close_all_items(&CloseAllItems::default(), window, cx)
19827 .unwrap()
19828 })
19829 .await
19830 .unwrap();
19831 pane.update(cx, |pane, _| {
19832 assert!(pane.active_item().is_none());
19833 });
19834 cx.update_global(|store: &mut SettingsStore, cx| {
19835 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
19836 s.restore_on_file_reopen = Some(true);
19837 });
19838 });
19839
19840 let _editor_reopened = workspace
19841 .update_in(cx, |workspace, window, cx| {
19842 workspace.open_path(
19843 (worktree_id, "main.rs"),
19844 Some(pane.downgrade()),
19845 true,
19846 window,
19847 cx,
19848 )
19849 })
19850 .unwrap()
19851 .await
19852 .downcast::<Editor>()
19853 .unwrap();
19854 pane.update(cx, |pane, cx| {
19855 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19856 open_editor.update(cx, |editor, cx| {
19857 assert_eq!(
19858 editor.display_text(cx),
19859 main_text,
19860 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
19861 );
19862 })
19863 });
19864}
19865
19866#[gpui::test]
19867async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
19868 struct EmptyModalView {
19869 focus_handle: gpui::FocusHandle,
19870 }
19871 impl EventEmitter<DismissEvent> for EmptyModalView {}
19872 impl Render for EmptyModalView {
19873 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
19874 div()
19875 }
19876 }
19877 impl Focusable for EmptyModalView {
19878 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
19879 self.focus_handle.clone()
19880 }
19881 }
19882 impl workspace::ModalView for EmptyModalView {}
19883 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
19884 EmptyModalView {
19885 focus_handle: cx.focus_handle(),
19886 }
19887 }
19888
19889 init_test(cx, |_| {});
19890
19891 let fs = FakeFs::new(cx.executor());
19892 let project = Project::test(fs, [], cx).await;
19893 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19894 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
19895 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19896 let editor = cx.new_window_entity(|window, cx| {
19897 Editor::new(
19898 EditorMode::full(),
19899 buffer,
19900 Some(project.clone()),
19901 window,
19902 cx,
19903 )
19904 });
19905 workspace
19906 .update(cx, |workspace, window, cx| {
19907 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
19908 })
19909 .unwrap();
19910 editor.update_in(cx, |editor, window, cx| {
19911 editor.open_context_menu(&OpenContextMenu, window, cx);
19912 assert!(editor.mouse_context_menu.is_some());
19913 });
19914 workspace
19915 .update(cx, |workspace, window, cx| {
19916 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
19917 })
19918 .unwrap();
19919 cx.read(|cx| {
19920 assert!(editor.read(cx).mouse_context_menu.is_none());
19921 });
19922}
19923
19924#[gpui::test]
19925async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
19926 init_test(cx, |_| {});
19927
19928 let fs = FakeFs::new(cx.executor());
19929 fs.insert_file(path!("/file.html"), Default::default())
19930 .await;
19931
19932 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
19933
19934 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
19935 let html_language = Arc::new(Language::new(
19936 LanguageConfig {
19937 name: "HTML".into(),
19938 matcher: LanguageMatcher {
19939 path_suffixes: vec!["html".to_string()],
19940 ..LanguageMatcher::default()
19941 },
19942 brackets: BracketPairConfig {
19943 pairs: vec![BracketPair {
19944 start: "<".into(),
19945 end: ">".into(),
19946 close: true,
19947 ..Default::default()
19948 }],
19949 ..Default::default()
19950 },
19951 ..Default::default()
19952 },
19953 Some(tree_sitter_html::LANGUAGE.into()),
19954 ));
19955 language_registry.add(html_language);
19956 let mut fake_servers = language_registry.register_fake_lsp(
19957 "HTML",
19958 FakeLspAdapter {
19959 capabilities: lsp::ServerCapabilities {
19960 completion_provider: Some(lsp::CompletionOptions {
19961 resolve_provider: Some(true),
19962 ..Default::default()
19963 }),
19964 ..Default::default()
19965 },
19966 ..Default::default()
19967 },
19968 );
19969
19970 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19971 let cx = &mut VisualTestContext::from_window(*workspace, cx);
19972
19973 let worktree_id = workspace
19974 .update(cx, |workspace, _window, cx| {
19975 workspace.project().update(cx, |project, cx| {
19976 project.worktrees(cx).next().unwrap().read(cx).id()
19977 })
19978 })
19979 .unwrap();
19980 project
19981 .update(cx, |project, cx| {
19982 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
19983 })
19984 .await
19985 .unwrap();
19986 let editor = workspace
19987 .update(cx, |workspace, window, cx| {
19988 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
19989 })
19990 .unwrap()
19991 .await
19992 .unwrap()
19993 .downcast::<Editor>()
19994 .unwrap();
19995
19996 let fake_server = fake_servers.next().await.unwrap();
19997 editor.update_in(cx, |editor, window, cx| {
19998 editor.set_text("<ad></ad>", window, cx);
19999 editor.change_selections(None, window, cx, |selections| {
20000 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
20001 });
20002 let Some((buffer, _)) = editor
20003 .buffer
20004 .read(cx)
20005 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
20006 else {
20007 panic!("Failed to get buffer for selection position");
20008 };
20009 let buffer = buffer.read(cx);
20010 let buffer_id = buffer.remote_id();
20011 let opening_range =
20012 buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3));
20013 let closing_range =
20014 buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
20015 let mut linked_ranges = HashMap::default();
20016 linked_ranges.insert(
20017 buffer_id,
20018 vec![(opening_range.clone(), vec![closing_range.clone()])],
20019 );
20020 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
20021 });
20022 let mut completion_handle =
20023 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
20024 Ok(Some(lsp::CompletionResponse::Array(vec![
20025 lsp::CompletionItem {
20026 label: "head".to_string(),
20027 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
20028 lsp::InsertReplaceEdit {
20029 new_text: "head".to_string(),
20030 insert: lsp::Range::new(
20031 lsp::Position::new(0, 1),
20032 lsp::Position::new(0, 3),
20033 ),
20034 replace: lsp::Range::new(
20035 lsp::Position::new(0, 1),
20036 lsp::Position::new(0, 3),
20037 ),
20038 },
20039 )),
20040 ..Default::default()
20041 },
20042 ])))
20043 });
20044 editor.update_in(cx, |editor, window, cx| {
20045 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
20046 });
20047 cx.run_until_parked();
20048 completion_handle.next().await.unwrap();
20049 editor.update(cx, |editor, _| {
20050 assert!(
20051 editor.context_menu_visible(),
20052 "Completion menu should be visible"
20053 );
20054 });
20055 editor.update_in(cx, |editor, window, cx| {
20056 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
20057 });
20058 cx.executor().run_until_parked();
20059 editor.update(cx, |editor, cx| {
20060 assert_eq!(editor.text(cx), "<head></head>");
20061 });
20062}
20063
20064#[gpui::test]
20065async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
20066 init_test(cx, |_| {});
20067
20068 let fs = FakeFs::new(cx.executor());
20069 fs.insert_tree(
20070 path!("/root"),
20071 json!({
20072 "a": {
20073 "main.rs": "fn main() {}",
20074 },
20075 "foo": {
20076 "bar": {
20077 "external_file.rs": "pub mod external {}",
20078 }
20079 }
20080 }),
20081 )
20082 .await;
20083
20084 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
20085 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
20086 language_registry.add(rust_lang());
20087 let _fake_servers = language_registry.register_fake_lsp(
20088 "Rust",
20089 FakeLspAdapter {
20090 ..FakeLspAdapter::default()
20091 },
20092 );
20093 let (workspace, cx) =
20094 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20095 let worktree_id = workspace.update(cx, |workspace, cx| {
20096 workspace.project().update(cx, |project, cx| {
20097 project.worktrees(cx).next().unwrap().read(cx).id()
20098 })
20099 });
20100
20101 let assert_language_servers_count =
20102 |expected: usize, context: &str, cx: &mut VisualTestContext| {
20103 project.update(cx, |project, cx| {
20104 let current = project
20105 .lsp_store()
20106 .read(cx)
20107 .as_local()
20108 .unwrap()
20109 .language_servers
20110 .len();
20111 assert_eq!(expected, current, "{context}");
20112 });
20113 };
20114
20115 assert_language_servers_count(
20116 0,
20117 "No servers should be running before any file is open",
20118 cx,
20119 );
20120 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
20121 let main_editor = workspace
20122 .update_in(cx, |workspace, window, cx| {
20123 workspace.open_path(
20124 (worktree_id, "main.rs"),
20125 Some(pane.downgrade()),
20126 true,
20127 window,
20128 cx,
20129 )
20130 })
20131 .unwrap()
20132 .await
20133 .downcast::<Editor>()
20134 .unwrap();
20135 pane.update(cx, |pane, cx| {
20136 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20137 open_editor.update(cx, |editor, cx| {
20138 assert_eq!(
20139 editor.display_text(cx),
20140 "fn main() {}",
20141 "Original main.rs text on initial open",
20142 );
20143 });
20144 assert_eq!(open_editor, main_editor);
20145 });
20146 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
20147
20148 let external_editor = workspace
20149 .update_in(cx, |workspace, window, cx| {
20150 workspace.open_abs_path(
20151 PathBuf::from("/root/foo/bar/external_file.rs"),
20152 OpenOptions::default(),
20153 window,
20154 cx,
20155 )
20156 })
20157 .await
20158 .expect("opening external file")
20159 .downcast::<Editor>()
20160 .expect("downcasted external file's open element to editor");
20161 pane.update(cx, |pane, cx| {
20162 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20163 open_editor.update(cx, |editor, cx| {
20164 assert_eq!(
20165 editor.display_text(cx),
20166 "pub mod external {}",
20167 "External file is open now",
20168 );
20169 });
20170 assert_eq!(open_editor, external_editor);
20171 });
20172 assert_language_servers_count(
20173 1,
20174 "Second, external, *.rs file should join the existing server",
20175 cx,
20176 );
20177
20178 pane.update_in(cx, |pane, window, cx| {
20179 pane.close_active_item(&CloseActiveItem::default(), window, cx)
20180 })
20181 .unwrap()
20182 .await
20183 .unwrap();
20184 pane.update_in(cx, |pane, window, cx| {
20185 pane.navigate_backward(window, cx);
20186 });
20187 cx.run_until_parked();
20188 pane.update(cx, |pane, cx| {
20189 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20190 open_editor.update(cx, |editor, cx| {
20191 assert_eq!(
20192 editor.display_text(cx),
20193 "pub mod external {}",
20194 "External file is open now",
20195 );
20196 });
20197 });
20198 assert_language_servers_count(
20199 1,
20200 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
20201 cx,
20202 );
20203
20204 cx.update(|_, cx| {
20205 workspace::reload(&workspace::Reload::default(), cx);
20206 });
20207 assert_language_servers_count(
20208 1,
20209 "After reloading the worktree with local and external files opened, only one project should be started",
20210 cx,
20211 );
20212}
20213
20214#[gpui::test]
20215async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
20216 init_test(cx, |_| {});
20217
20218 let mut cx = EditorTestContext::new(cx).await;
20219 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
20220 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
20221
20222 // test cursor move to start of each line on tab
20223 // for `if`, `elif`, `else`, `while`, `with` and `for`
20224 cx.set_state(indoc! {"
20225 def main():
20226 ˇ for item in items:
20227 ˇ while item.active:
20228 ˇ if item.value > 10:
20229 ˇ continue
20230 ˇ elif item.value < 0:
20231 ˇ break
20232 ˇ else:
20233 ˇ with item.context() as ctx:
20234 ˇ yield count
20235 ˇ else:
20236 ˇ log('while else')
20237 ˇ else:
20238 ˇ log('for else')
20239 "});
20240 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
20241 cx.assert_editor_state(indoc! {"
20242 def main():
20243 ˇfor item in items:
20244 ˇwhile item.active:
20245 ˇif item.value > 10:
20246 ˇcontinue
20247 ˇelif item.value < 0:
20248 ˇbreak
20249 ˇelse:
20250 ˇwith item.context() as ctx:
20251 ˇyield count
20252 ˇelse:
20253 ˇlog('while else')
20254 ˇelse:
20255 ˇlog('for else')
20256 "});
20257 // test relative indent is preserved when tab
20258 // for `if`, `elif`, `else`, `while`, `with` and `for`
20259 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
20260 cx.assert_editor_state(indoc! {"
20261 def main():
20262 ˇfor item in items:
20263 ˇwhile item.active:
20264 ˇif item.value > 10:
20265 ˇcontinue
20266 ˇelif item.value < 0:
20267 ˇbreak
20268 ˇelse:
20269 ˇwith item.context() as ctx:
20270 ˇyield count
20271 ˇelse:
20272 ˇlog('while else')
20273 ˇelse:
20274 ˇlog('for else')
20275 "});
20276
20277 // test cursor move to start of each line on tab
20278 // for `try`, `except`, `else`, `finally`, `match` and `def`
20279 cx.set_state(indoc! {"
20280 def main():
20281 ˇ try:
20282 ˇ fetch()
20283 ˇ except ValueError:
20284 ˇ handle_error()
20285 ˇ else:
20286 ˇ match value:
20287 ˇ case _:
20288 ˇ finally:
20289 ˇ def status():
20290 ˇ return 0
20291 "});
20292 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
20293 cx.assert_editor_state(indoc! {"
20294 def main():
20295 ˇtry:
20296 ˇfetch()
20297 ˇexcept ValueError:
20298 ˇhandle_error()
20299 ˇelse:
20300 ˇmatch value:
20301 ˇcase _:
20302 ˇfinally:
20303 ˇdef status():
20304 ˇreturn 0
20305 "});
20306 // test relative indent is preserved when tab
20307 // for `try`, `except`, `else`, `finally`, `match` and `def`
20308 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
20309 cx.assert_editor_state(indoc! {"
20310 def main():
20311 ˇtry:
20312 ˇfetch()
20313 ˇexcept ValueError:
20314 ˇhandle_error()
20315 ˇelse:
20316 ˇmatch value:
20317 ˇcase _:
20318 ˇfinally:
20319 ˇdef status():
20320 ˇreturn 0
20321 "});
20322}
20323
20324#[gpui::test]
20325async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
20326 init_test(cx, |_| {});
20327
20328 let mut cx = EditorTestContext::new(cx).await;
20329 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
20330 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
20331
20332 // test `else` auto outdents when typed inside `if` block
20333 cx.set_state(indoc! {"
20334 def main():
20335 if i == 2:
20336 return
20337 ˇ
20338 "});
20339 cx.update_editor(|editor, window, cx| {
20340 editor.handle_input("else:", window, cx);
20341 });
20342 cx.assert_editor_state(indoc! {"
20343 def main():
20344 if i == 2:
20345 return
20346 else:ˇ
20347 "});
20348
20349 // test `except` auto outdents when typed inside `try` block
20350 cx.set_state(indoc! {"
20351 def main():
20352 try:
20353 i = 2
20354 ˇ
20355 "});
20356 cx.update_editor(|editor, window, cx| {
20357 editor.handle_input("except:", window, cx);
20358 });
20359 cx.assert_editor_state(indoc! {"
20360 def main():
20361 try:
20362 i = 2
20363 except:ˇ
20364 "});
20365
20366 // test `else` auto outdents when typed inside `except` block
20367 cx.set_state(indoc! {"
20368 def main():
20369 try:
20370 i = 2
20371 except:
20372 j = 2
20373 ˇ
20374 "});
20375 cx.update_editor(|editor, window, cx| {
20376 editor.handle_input("else:", window, cx);
20377 });
20378 cx.assert_editor_state(indoc! {"
20379 def main():
20380 try:
20381 i = 2
20382 except:
20383 j = 2
20384 else:ˇ
20385 "});
20386
20387 // test `finally` auto outdents when typed inside `else` block
20388 cx.set_state(indoc! {"
20389 def main():
20390 try:
20391 i = 2
20392 except:
20393 j = 2
20394 else:
20395 k = 2
20396 ˇ
20397 "});
20398 cx.update_editor(|editor, window, cx| {
20399 editor.handle_input("finally:", window, cx);
20400 });
20401 cx.assert_editor_state(indoc! {"
20402 def main():
20403 try:
20404 i = 2
20405 except:
20406 j = 2
20407 else:
20408 k = 2
20409 finally:ˇ
20410 "});
20411
20412 // TODO: test `except` auto outdents when typed inside `try` block right after for block
20413 // cx.set_state(indoc! {"
20414 // def main():
20415 // try:
20416 // for i in range(n):
20417 // pass
20418 // ˇ
20419 // "});
20420 // cx.update_editor(|editor, window, cx| {
20421 // editor.handle_input("except:", window, cx);
20422 // });
20423 // cx.assert_editor_state(indoc! {"
20424 // def main():
20425 // try:
20426 // for i in range(n):
20427 // pass
20428 // except:ˇ
20429 // "});
20430
20431 // TODO: test `else` auto outdents when typed inside `except` block right after for block
20432 // cx.set_state(indoc! {"
20433 // def main():
20434 // try:
20435 // i = 2
20436 // except:
20437 // for i in range(n):
20438 // pass
20439 // ˇ
20440 // "});
20441 // cx.update_editor(|editor, window, cx| {
20442 // editor.handle_input("else:", window, cx);
20443 // });
20444 // cx.assert_editor_state(indoc! {"
20445 // def main():
20446 // try:
20447 // i = 2
20448 // except:
20449 // for i in range(n):
20450 // pass
20451 // else:ˇ
20452 // "});
20453
20454 // TODO: test `finally` auto outdents when typed inside `else` block right after for block
20455 // cx.set_state(indoc! {"
20456 // def main():
20457 // try:
20458 // i = 2
20459 // except:
20460 // j = 2
20461 // else:
20462 // for i in range(n):
20463 // pass
20464 // ˇ
20465 // "});
20466 // cx.update_editor(|editor, window, cx| {
20467 // editor.handle_input("finally:", window, cx);
20468 // });
20469 // cx.assert_editor_state(indoc! {"
20470 // def main():
20471 // try:
20472 // i = 2
20473 // except:
20474 // j = 2
20475 // else:
20476 // for i in range(n):
20477 // pass
20478 // finally:ˇ
20479 // "});
20480
20481 // test `else` stays at correct indent when typed after `for` block
20482 cx.set_state(indoc! {"
20483 def main():
20484 for i in range(10):
20485 if i == 3:
20486 break
20487 ˇ
20488 "});
20489 cx.update_editor(|editor, window, cx| {
20490 editor.handle_input("else:", window, cx);
20491 });
20492 cx.assert_editor_state(indoc! {"
20493 def main():
20494 for i in range(10):
20495 if i == 3:
20496 break
20497 else:ˇ
20498 "});
20499}
20500
20501#[gpui::test]
20502async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
20503 init_test(cx, |_| {});
20504 update_test_language_settings(cx, |settings| {
20505 settings.defaults.extend_comment_on_newline = Some(false);
20506 });
20507 let mut cx = EditorTestContext::new(cx).await;
20508 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
20509 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
20510
20511 // test correct indent after newline on comment
20512 cx.set_state(indoc! {"
20513 # COMMENT:ˇ
20514 "});
20515 cx.update_editor(|editor, window, cx| {
20516 editor.newline(&Newline, window, cx);
20517 });
20518 cx.assert_editor_state(indoc! {"
20519 # COMMENT:
20520 ˇ
20521 "});
20522
20523 // test correct indent after newline in curly brackets
20524 cx.set_state(indoc! {"
20525 {ˇ}
20526 "});
20527 cx.update_editor(|editor, window, cx| {
20528 editor.newline(&Newline, window, cx);
20529 });
20530 cx.run_until_parked();
20531 cx.assert_editor_state(indoc! {"
20532 {
20533 ˇ
20534 }
20535 "});
20536}
20537
20538fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
20539 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
20540 point..point
20541}
20542
20543fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
20544 let (text, ranges) = marked_text_ranges(marked_text, true);
20545 assert_eq!(editor.text(cx), text);
20546 assert_eq!(
20547 editor.selections.ranges(cx),
20548 ranges,
20549 "Assert selections are {}",
20550 marked_text
20551 );
20552}
20553
20554pub fn handle_signature_help_request(
20555 cx: &mut EditorLspTestContext,
20556 mocked_response: lsp::SignatureHelp,
20557) -> impl Future<Output = ()> + use<> {
20558 let mut request =
20559 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
20560 let mocked_response = mocked_response.clone();
20561 async move { Ok(Some(mocked_response)) }
20562 });
20563
20564 async move {
20565 request.next().await;
20566 }
20567}
20568
20569/// Handle completion request passing a marked string specifying where the completion
20570/// should be triggered from using '|' character, what range should be replaced, and what completions
20571/// should be returned using '<' and '>' to delimit the range.
20572///
20573/// Also see `handle_completion_request_with_insert_and_replace`.
20574#[track_caller]
20575pub fn handle_completion_request(
20576 cx: &mut EditorLspTestContext,
20577 marked_string: &str,
20578 completions: Vec<&'static str>,
20579 counter: Arc<AtomicUsize>,
20580) -> impl Future<Output = ()> {
20581 let complete_from_marker: TextRangeMarker = '|'.into();
20582 let replace_range_marker: TextRangeMarker = ('<', '>').into();
20583 let (_, mut marked_ranges) = marked_text_ranges_by(
20584 marked_string,
20585 vec![complete_from_marker.clone(), replace_range_marker.clone()],
20586 );
20587
20588 let complete_from_position =
20589 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
20590 let replace_range =
20591 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
20592
20593 let mut request =
20594 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
20595 let completions = completions.clone();
20596 counter.fetch_add(1, atomic::Ordering::Release);
20597 async move {
20598 assert_eq!(params.text_document_position.text_document.uri, url.clone());
20599 assert_eq!(
20600 params.text_document_position.position,
20601 complete_from_position
20602 );
20603 Ok(Some(lsp::CompletionResponse::Array(
20604 completions
20605 .iter()
20606 .map(|completion_text| lsp::CompletionItem {
20607 label: completion_text.to_string(),
20608 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
20609 range: replace_range,
20610 new_text: completion_text.to_string(),
20611 })),
20612 ..Default::default()
20613 })
20614 .collect(),
20615 )))
20616 }
20617 });
20618
20619 async move {
20620 request.next().await;
20621 }
20622}
20623
20624/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
20625/// given instead, which also contains an `insert` range.
20626///
20627/// This function uses the cursor position to mimic what Rust-Analyzer provides as the `insert` range,
20628/// that is, `replace_range.start..cursor_pos`.
20629pub fn handle_completion_request_with_insert_and_replace(
20630 cx: &mut EditorLspTestContext,
20631 marked_string: &str,
20632 completions: Vec<&'static str>,
20633 counter: Arc<AtomicUsize>,
20634) -> impl Future<Output = ()> {
20635 let complete_from_marker: TextRangeMarker = '|'.into();
20636 let replace_range_marker: TextRangeMarker = ('<', '>').into();
20637 let (_, mut marked_ranges) = marked_text_ranges_by(
20638 marked_string,
20639 vec![complete_from_marker.clone(), replace_range_marker.clone()],
20640 );
20641
20642 let complete_from_position =
20643 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
20644 let replace_range =
20645 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
20646
20647 let mut request =
20648 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
20649 let completions = completions.clone();
20650 counter.fetch_add(1, atomic::Ordering::Release);
20651 async move {
20652 assert_eq!(params.text_document_position.text_document.uri, url.clone());
20653 assert_eq!(
20654 params.text_document_position.position, complete_from_position,
20655 "marker `|` position doesn't match",
20656 );
20657 Ok(Some(lsp::CompletionResponse::Array(
20658 completions
20659 .iter()
20660 .map(|completion_text| lsp::CompletionItem {
20661 label: completion_text.to_string(),
20662 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
20663 lsp::InsertReplaceEdit {
20664 insert: lsp::Range {
20665 start: replace_range.start,
20666 end: complete_from_position,
20667 },
20668 replace: replace_range,
20669 new_text: completion_text.to_string(),
20670 },
20671 )),
20672 ..Default::default()
20673 })
20674 .collect(),
20675 )))
20676 }
20677 });
20678
20679 async move {
20680 request.next().await;
20681 }
20682}
20683
20684fn handle_resolve_completion_request(
20685 cx: &mut EditorLspTestContext,
20686 edits: Option<Vec<(&'static str, &'static str)>>,
20687) -> impl Future<Output = ()> {
20688 let edits = edits.map(|edits| {
20689 edits
20690 .iter()
20691 .map(|(marked_string, new_text)| {
20692 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
20693 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
20694 lsp::TextEdit::new(replace_range, new_text.to_string())
20695 })
20696 .collect::<Vec<_>>()
20697 });
20698
20699 let mut request =
20700 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
20701 let edits = edits.clone();
20702 async move {
20703 Ok(lsp::CompletionItem {
20704 additional_text_edits: edits,
20705 ..Default::default()
20706 })
20707 }
20708 });
20709
20710 async move {
20711 request.next().await;
20712 }
20713}
20714
20715pub(crate) fn update_test_language_settings(
20716 cx: &mut TestAppContext,
20717 f: impl Fn(&mut AllLanguageSettingsContent),
20718) {
20719 cx.update(|cx| {
20720 SettingsStore::update_global(cx, |store, cx| {
20721 store.update_user_settings::<AllLanguageSettings>(cx, f);
20722 });
20723 });
20724}
20725
20726pub(crate) fn update_test_project_settings(
20727 cx: &mut TestAppContext,
20728 f: impl Fn(&mut ProjectSettings),
20729) {
20730 cx.update(|cx| {
20731 SettingsStore::update_global(cx, |store, cx| {
20732 store.update_user_settings::<ProjectSettings>(cx, f);
20733 });
20734 });
20735}
20736
20737pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
20738 cx.update(|cx| {
20739 assets::Assets.load_test_fonts(cx);
20740 let store = SettingsStore::test(cx);
20741 cx.set_global(store);
20742 theme::init(theme::LoadThemes::JustBase, cx);
20743 release_channel::init(SemanticVersion::default(), cx);
20744 client::init_settings(cx);
20745 language::init(cx);
20746 Project::init_settings(cx);
20747 workspace::init_settings(cx);
20748 crate::init(cx);
20749 });
20750
20751 update_test_language_settings(cx, f);
20752}
20753
20754#[track_caller]
20755fn assert_hunk_revert(
20756 not_reverted_text_with_selections: &str,
20757 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
20758 expected_reverted_text_with_selections: &str,
20759 base_text: &str,
20760 cx: &mut EditorLspTestContext,
20761) {
20762 cx.set_state(not_reverted_text_with_selections);
20763 cx.set_head_text(base_text);
20764 cx.executor().run_until_parked();
20765
20766 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
20767 let snapshot = editor.snapshot(window, cx);
20768 let reverted_hunk_statuses = snapshot
20769 .buffer_snapshot
20770 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
20771 .map(|hunk| hunk.status().kind)
20772 .collect::<Vec<_>>();
20773
20774 editor.git_restore(&Default::default(), window, cx);
20775 reverted_hunk_statuses
20776 });
20777 cx.executor().run_until_parked();
20778 cx.assert_editor_state(expected_reverted_text_with_selections);
20779 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
20780}