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};
30use language_settings::{Formatter, FormatterList, IndentGuideSettings};
31use lsp::CompletionParams;
32use multi_buffer::{IndentGuide, PathKey};
33use parking_lot::Mutex;
34use pretty_assertions::{assert_eq, assert_ne};
35use project::{
36 FakeFs,
37 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
38 project_settings::{LspSettings, ProjectSettings},
39};
40use serde_json::{self, json};
41use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
42use std::{
43 iter,
44 sync::atomic::{self, AtomicUsize},
45};
46use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
47use text::ToPoint as _;
48use unindent::Unindent;
49use util::{
50 assert_set_eq, path,
51 test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
52 uri,
53};
54use workspace::{
55 CloseActiveItem, CloseAllItems, CloseInactiveItems, NavigationEntry, OpenOptions, ViewId,
56 item::{FollowEvent, FollowableItem, Item, ItemHandle},
57};
58
59#[gpui::test]
60fn test_edit_events(cx: &mut TestAppContext) {
61 init_test(cx, |_| {});
62
63 let buffer = cx.new(|cx| {
64 let mut buffer = language::Buffer::local("123456", cx);
65 buffer.set_group_interval(Duration::from_secs(1));
66 buffer
67 });
68
69 let events = Rc::new(RefCell::new(Vec::new()));
70 let editor1 = cx.add_window({
71 let events = events.clone();
72 |window, cx| {
73 let entity = cx.entity().clone();
74 cx.subscribe_in(
75 &entity,
76 window,
77 move |_, _, event: &EditorEvent, _, _| match event {
78 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
79 EditorEvent::BufferEdited => {
80 events.borrow_mut().push(("editor1", "buffer edited"))
81 }
82 _ => {}
83 },
84 )
85 .detach();
86 Editor::for_buffer(buffer.clone(), None, window, cx)
87 }
88 });
89
90 let editor2 = cx.add_window({
91 let events = events.clone();
92 |window, cx| {
93 cx.subscribe_in(
94 &cx.entity().clone(),
95 window,
96 move |_, _, event: &EditorEvent, _, _| match event {
97 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
98 EditorEvent::BufferEdited => {
99 events.borrow_mut().push(("editor2", "buffer edited"))
100 }
101 _ => {}
102 },
103 )
104 .detach();
105 Editor::for_buffer(buffer.clone(), None, window, cx)
106 }
107 });
108
109 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
110
111 // Mutating editor 1 will emit an `Edited` event only for that editor.
112 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
113 assert_eq!(
114 mem::take(&mut *events.borrow_mut()),
115 [
116 ("editor1", "edited"),
117 ("editor1", "buffer edited"),
118 ("editor2", "buffer edited"),
119 ]
120 );
121
122 // Mutating editor 2 will emit an `Edited` event only for that editor.
123 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
124 assert_eq!(
125 mem::take(&mut *events.borrow_mut()),
126 [
127 ("editor2", "edited"),
128 ("editor1", "buffer edited"),
129 ("editor2", "buffer edited"),
130 ]
131 );
132
133 // Undoing on editor 1 will emit an `Edited` event only for that editor.
134 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
135 assert_eq!(
136 mem::take(&mut *events.borrow_mut()),
137 [
138 ("editor1", "edited"),
139 ("editor1", "buffer edited"),
140 ("editor2", "buffer edited"),
141 ]
142 );
143
144 // Redoing on editor 1 will emit an `Edited` event only for that editor.
145 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
146 assert_eq!(
147 mem::take(&mut *events.borrow_mut()),
148 [
149 ("editor1", "edited"),
150 ("editor1", "buffer edited"),
151 ("editor2", "buffer edited"),
152 ]
153 );
154
155 // Undoing on editor 2 will emit an `Edited` event only for that editor.
156 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
157 assert_eq!(
158 mem::take(&mut *events.borrow_mut()),
159 [
160 ("editor2", "edited"),
161 ("editor1", "buffer edited"),
162 ("editor2", "buffer edited"),
163 ]
164 );
165
166 // Redoing on editor 2 will emit an `Edited` event only for that editor.
167 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
168 assert_eq!(
169 mem::take(&mut *events.borrow_mut()),
170 [
171 ("editor2", "edited"),
172 ("editor1", "buffer edited"),
173 ("editor2", "buffer edited"),
174 ]
175 );
176
177 // No event is emitted when the mutation is a no-op.
178 _ = editor2.update(cx, |editor, window, cx| {
179 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
180
181 editor.backspace(&Backspace, window, cx);
182 });
183 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
184}
185
186#[gpui::test]
187fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
188 init_test(cx, |_| {});
189
190 let mut now = Instant::now();
191 let group_interval = Duration::from_millis(1);
192 let buffer = cx.new(|cx| {
193 let mut buf = language::Buffer::local("123456", cx);
194 buf.set_group_interval(group_interval);
195 buf
196 });
197 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
198 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
199
200 _ = editor.update(cx, |editor, window, cx| {
201 editor.start_transaction_at(now, window, cx);
202 editor.change_selections(None, window, cx, |s| s.select_ranges([2..4]));
203
204 editor.insert("cd", window, cx);
205 editor.end_transaction_at(now, cx);
206 assert_eq!(editor.text(cx), "12cd56");
207 assert_eq!(editor.selections.ranges(cx), vec![4..4]);
208
209 editor.start_transaction_at(now, window, cx);
210 editor.change_selections(None, window, cx, |s| s.select_ranges([4..5]));
211 editor.insert("e", window, cx);
212 editor.end_transaction_at(now, cx);
213 assert_eq!(editor.text(cx), "12cde6");
214 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
215
216 now += group_interval + Duration::from_millis(1);
217 editor.change_selections(None, window, cx, |s| s.select_ranges([2..2]));
218
219 // Simulate an edit in another editor
220 buffer.update(cx, |buffer, cx| {
221 buffer.start_transaction_at(now, cx);
222 buffer.edit([(0..1, "a")], None, cx);
223 buffer.edit([(1..1, "b")], None, cx);
224 buffer.end_transaction_at(now, cx);
225 });
226
227 assert_eq!(editor.text(cx), "ab2cde6");
228 assert_eq!(editor.selections.ranges(cx), vec![3..3]);
229
230 // Last transaction happened past the group interval in a different editor.
231 // Undo it individually and don't restore selections.
232 editor.undo(&Undo, window, cx);
233 assert_eq!(editor.text(cx), "12cde6");
234 assert_eq!(editor.selections.ranges(cx), vec![2..2]);
235
236 // First two transactions happened within the group interval in this editor.
237 // Undo them together and restore selections.
238 editor.undo(&Undo, window, cx);
239 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
240 assert_eq!(editor.text(cx), "123456");
241 assert_eq!(editor.selections.ranges(cx), vec![0..0]);
242
243 // Redo the first two transactions together.
244 editor.redo(&Redo, window, cx);
245 assert_eq!(editor.text(cx), "12cde6");
246 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
247
248 // Redo the last transaction on its own.
249 editor.redo(&Redo, window, cx);
250 assert_eq!(editor.text(cx), "ab2cde6");
251 assert_eq!(editor.selections.ranges(cx), vec![6..6]);
252
253 // Test empty transactions.
254 editor.start_transaction_at(now, window, cx);
255 editor.end_transaction_at(now, cx);
256 editor.undo(&Undo, window, cx);
257 assert_eq!(editor.text(cx), "12cde6");
258 });
259}
260
261#[gpui::test]
262fn test_ime_composition(cx: &mut TestAppContext) {
263 init_test(cx, |_| {});
264
265 let buffer = cx.new(|cx| {
266 let mut buffer = language::Buffer::local("abcde", cx);
267 // Ensure automatic grouping doesn't occur.
268 buffer.set_group_interval(Duration::ZERO);
269 buffer
270 });
271
272 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
273 cx.add_window(|window, cx| {
274 let mut editor = build_editor(buffer.clone(), window, cx);
275
276 // Start a new IME composition.
277 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
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 assert_eq!(editor.text(cx), "äbcde");
281 assert_eq!(
282 editor.marked_text_ranges(cx),
283 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
284 );
285
286 // Finalize IME composition.
287 editor.replace_text_in_range(None, "ā", window, cx);
288 assert_eq!(editor.text(cx), "ābcde");
289 assert_eq!(editor.marked_text_ranges(cx), None);
290
291 // IME composition edits are grouped and are undone/redone at once.
292 editor.undo(&Default::default(), window, cx);
293 assert_eq!(editor.text(cx), "abcde");
294 assert_eq!(editor.marked_text_ranges(cx), None);
295 editor.redo(&Default::default(), window, cx);
296 assert_eq!(editor.text(cx), "ābcde");
297 assert_eq!(editor.marked_text_ranges(cx), None);
298
299 // Start a new IME composition.
300 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
301 assert_eq!(
302 editor.marked_text_ranges(cx),
303 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
304 );
305
306 // Undoing during an IME composition cancels it.
307 editor.undo(&Default::default(), window, cx);
308 assert_eq!(editor.text(cx), "ābcde");
309 assert_eq!(editor.marked_text_ranges(cx), None);
310
311 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
312 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
313 assert_eq!(editor.text(cx), "ābcdè");
314 assert_eq!(
315 editor.marked_text_ranges(cx),
316 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
317 );
318
319 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
320 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
321 assert_eq!(editor.text(cx), "ābcdę");
322 assert_eq!(editor.marked_text_ranges(cx), None);
323
324 // Start a new IME composition with multiple cursors.
325 editor.change_selections(None, window, cx, |s| {
326 s.select_ranges([
327 OffsetUtf16(1)..OffsetUtf16(1),
328 OffsetUtf16(3)..OffsetUtf16(3),
329 OffsetUtf16(5)..OffsetUtf16(5),
330 ])
331 });
332 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
333 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
334 assert_eq!(
335 editor.marked_text_ranges(cx),
336 Some(vec![
337 OffsetUtf16(0)..OffsetUtf16(3),
338 OffsetUtf16(4)..OffsetUtf16(7),
339 OffsetUtf16(8)..OffsetUtf16(11)
340 ])
341 );
342
343 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
344 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
345 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
346 assert_eq!(
347 editor.marked_text_ranges(cx),
348 Some(vec![
349 OffsetUtf16(1)..OffsetUtf16(2),
350 OffsetUtf16(5)..OffsetUtf16(6),
351 OffsetUtf16(9)..OffsetUtf16(10)
352 ])
353 );
354
355 // Finalize IME composition with multiple cursors.
356 editor.replace_text_in_range(Some(9..10), "2", window, cx);
357 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
358 assert_eq!(editor.marked_text_ranges(cx), None);
359
360 editor
361 });
362}
363
364#[gpui::test]
365fn test_selection_with_mouse(cx: &mut TestAppContext) {
366 init_test(cx, |_| {});
367
368 let editor = cx.add_window(|window, cx| {
369 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
370 build_editor(buffer, window, cx)
371 });
372
373 _ = editor.update(cx, |editor, window, cx| {
374 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
375 });
376 assert_eq!(
377 editor
378 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
379 .unwrap(),
380 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
381 );
382
383 _ = editor.update(cx, |editor, window, cx| {
384 editor.update_selection(
385 DisplayPoint::new(DisplayRow(3), 3),
386 0,
387 gpui::Point::<f32>::default(),
388 window,
389 cx,
390 );
391 });
392
393 assert_eq!(
394 editor
395 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
396 .unwrap(),
397 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
398 );
399
400 _ = editor.update(cx, |editor, window, cx| {
401 editor.update_selection(
402 DisplayPoint::new(DisplayRow(1), 1),
403 0,
404 gpui::Point::<f32>::default(),
405 window,
406 cx,
407 );
408 });
409
410 assert_eq!(
411 editor
412 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
413 .unwrap(),
414 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
415 );
416
417 _ = editor.update(cx, |editor, window, cx| {
418 editor.end_selection(window, cx);
419 editor.update_selection(
420 DisplayPoint::new(DisplayRow(3), 3),
421 0,
422 gpui::Point::<f32>::default(),
423 window,
424 cx,
425 );
426 });
427
428 assert_eq!(
429 editor
430 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
431 .unwrap(),
432 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
433 );
434
435 _ = editor.update(cx, |editor, window, cx| {
436 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
437 editor.update_selection(
438 DisplayPoint::new(DisplayRow(0), 0),
439 0,
440 gpui::Point::<f32>::default(),
441 window,
442 cx,
443 );
444 });
445
446 assert_eq!(
447 editor
448 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
449 .unwrap(),
450 [
451 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
452 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
453 ]
454 );
455
456 _ = editor.update(cx, |editor, window, cx| {
457 editor.end_selection(window, cx);
458 });
459
460 assert_eq!(
461 editor
462 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
463 .unwrap(),
464 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
465 );
466}
467
468#[gpui::test]
469fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
470 init_test(cx, |_| {});
471
472 let editor = cx.add_window(|window, cx| {
473 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
474 build_editor(buffer, window, cx)
475 });
476
477 _ = editor.update(cx, |editor, window, cx| {
478 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
479 });
480
481 _ = editor.update(cx, |editor, window, cx| {
482 editor.end_selection(window, cx);
483 });
484
485 _ = editor.update(cx, |editor, window, cx| {
486 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
487 });
488
489 _ = editor.update(cx, |editor, window, cx| {
490 editor.end_selection(window, cx);
491 });
492
493 assert_eq!(
494 editor
495 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
496 .unwrap(),
497 [
498 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
499 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
500 ]
501 );
502
503 _ = editor.update(cx, |editor, window, cx| {
504 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
505 });
506
507 _ = editor.update(cx, |editor, window, cx| {
508 editor.end_selection(window, cx);
509 });
510
511 assert_eq!(
512 editor
513 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
514 .unwrap(),
515 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
516 );
517}
518
519#[gpui::test]
520fn test_canceling_pending_selection(cx: &mut TestAppContext) {
521 init_test(cx, |_| {});
522
523 let editor = cx.add_window(|window, cx| {
524 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
525 build_editor(buffer, window, cx)
526 });
527
528 _ = editor.update(cx, |editor, window, cx| {
529 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
530 assert_eq!(
531 editor.selections.display_ranges(cx),
532 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
533 );
534 });
535
536 _ = editor.update(cx, |editor, window, cx| {
537 editor.update_selection(
538 DisplayPoint::new(DisplayRow(3), 3),
539 0,
540 gpui::Point::<f32>::default(),
541 window,
542 cx,
543 );
544 assert_eq!(
545 editor.selections.display_ranges(cx),
546 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
547 );
548 });
549
550 _ = editor.update(cx, |editor, window, cx| {
551 editor.cancel(&Cancel, window, cx);
552 editor.update_selection(
553 DisplayPoint::new(DisplayRow(1), 1),
554 0,
555 gpui::Point::<f32>::default(),
556 window,
557 cx,
558 );
559 assert_eq!(
560 editor.selections.display_ranges(cx),
561 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
562 );
563 });
564}
565
566#[gpui::test]
567fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
568 init_test(cx, |_| {});
569
570 let editor = cx.add_window(|window, cx| {
571 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
572 build_editor(buffer, window, cx)
573 });
574
575 _ = editor.update(cx, |editor, window, cx| {
576 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
577 assert_eq!(
578 editor.selections.display_ranges(cx),
579 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
580 );
581
582 editor.move_down(&Default::default(), window, cx);
583 assert_eq!(
584 editor.selections.display_ranges(cx),
585 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
586 );
587
588 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
589 assert_eq!(
590 editor.selections.display_ranges(cx),
591 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
592 );
593
594 editor.move_up(&Default::default(), window, cx);
595 assert_eq!(
596 editor.selections.display_ranges(cx),
597 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
598 );
599 });
600}
601
602#[gpui::test]
603fn test_clone(cx: &mut TestAppContext) {
604 init_test(cx, |_| {});
605
606 let (text, selection_ranges) = marked_text_ranges(
607 indoc! {"
608 one
609 two
610 threeˇ
611 four
612 fiveˇ
613 "},
614 true,
615 );
616
617 let editor = cx.add_window(|window, cx| {
618 let buffer = MultiBuffer::build_simple(&text, cx);
619 build_editor(buffer, window, cx)
620 });
621
622 _ = editor.update(cx, |editor, window, cx| {
623 editor.change_selections(None, window, cx, |s| {
624 s.select_ranges(selection_ranges.clone())
625 });
626 editor.fold_creases(
627 vec![
628 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
629 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
630 ],
631 true,
632 window,
633 cx,
634 );
635 });
636
637 let cloned_editor = editor
638 .update(cx, |editor, _, cx| {
639 cx.open_window(Default::default(), |window, cx| {
640 cx.new(|cx| editor.clone(window, cx))
641 })
642 })
643 .unwrap()
644 .unwrap();
645
646 let snapshot = editor
647 .update(cx, |e, window, cx| e.snapshot(window, cx))
648 .unwrap();
649 let cloned_snapshot = cloned_editor
650 .update(cx, |e, window, cx| e.snapshot(window, cx))
651 .unwrap();
652
653 assert_eq!(
654 cloned_editor
655 .update(cx, |e, _, cx| e.display_text(cx))
656 .unwrap(),
657 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
658 );
659 assert_eq!(
660 cloned_snapshot
661 .folds_in_range(0..text.len())
662 .collect::<Vec<_>>(),
663 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
664 );
665 assert_set_eq!(
666 cloned_editor
667 .update(cx, |editor, _, cx| editor.selections.ranges::<Point>(cx))
668 .unwrap(),
669 editor
670 .update(cx, |editor, _, cx| editor.selections.ranges(cx))
671 .unwrap()
672 );
673 assert_set_eq!(
674 cloned_editor
675 .update(cx, |e, _window, cx| e.selections.display_ranges(cx))
676 .unwrap(),
677 editor
678 .update(cx, |e, _, cx| e.selections.display_ranges(cx))
679 .unwrap()
680 );
681}
682
683#[gpui::test]
684async fn test_navigation_history(cx: &mut TestAppContext) {
685 init_test(cx, |_| {});
686
687 use workspace::item::Item;
688
689 let fs = FakeFs::new(cx.executor());
690 let project = Project::test(fs, [], cx).await;
691 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
692 let pane = workspace
693 .update(cx, |workspace, _, _| workspace.active_pane().clone())
694 .unwrap();
695
696 _ = workspace.update(cx, |_v, window, cx| {
697 cx.new(|cx| {
698 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
699 let mut editor = build_editor(buffer.clone(), window, cx);
700 let handle = cx.entity();
701 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
702
703 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
704 editor.nav_history.as_mut().unwrap().pop_backward(cx)
705 }
706
707 // Move the cursor a small distance.
708 // Nothing is added to the navigation history.
709 editor.change_selections(None, window, cx, |s| {
710 s.select_display_ranges([
711 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
712 ])
713 });
714 editor.change_selections(None, window, cx, |s| {
715 s.select_display_ranges([
716 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
717 ])
718 });
719 assert!(pop_history(&mut editor, cx).is_none());
720
721 // Move the cursor a large distance.
722 // The history can jump back to the previous position.
723 editor.change_selections(None, window, cx, |s| {
724 s.select_display_ranges([
725 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
726 ])
727 });
728 let nav_entry = pop_history(&mut editor, cx).unwrap();
729 editor.navigate(nav_entry.data.unwrap(), window, cx);
730 assert_eq!(nav_entry.item.id(), cx.entity_id());
731 assert_eq!(
732 editor.selections.display_ranges(cx),
733 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
734 );
735 assert!(pop_history(&mut editor, cx).is_none());
736
737 // Move the cursor a small distance via the mouse.
738 // Nothing is added to the navigation history.
739 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
740 editor.end_selection(window, cx);
741 assert_eq!(
742 editor.selections.display_ranges(cx),
743 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
744 );
745 assert!(pop_history(&mut editor, cx).is_none());
746
747 // Move the cursor a large distance via the mouse.
748 // The history can jump back to the previous position.
749 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
750 editor.end_selection(window, cx);
751 assert_eq!(
752 editor.selections.display_ranges(cx),
753 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
754 );
755 let nav_entry = pop_history(&mut editor, cx).unwrap();
756 editor.navigate(nav_entry.data.unwrap(), window, cx);
757 assert_eq!(nav_entry.item.id(), cx.entity_id());
758 assert_eq!(
759 editor.selections.display_ranges(cx),
760 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
761 );
762 assert!(pop_history(&mut editor, cx).is_none());
763
764 // Set scroll position to check later
765 editor.set_scroll_position(gpui::Point::<f32>::new(5.5, 5.5), window, cx);
766 let original_scroll_position = editor.scroll_manager.anchor();
767
768 // Jump to the end of the document and adjust scroll
769 editor.move_to_end(&MoveToEnd, window, cx);
770 editor.set_scroll_position(gpui::Point::<f32>::new(-2.5, -0.5), window, cx);
771 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
772
773 let nav_entry = pop_history(&mut editor, cx).unwrap();
774 editor.navigate(nav_entry.data.unwrap(), window, cx);
775 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
776
777 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
778 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
779 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
780 let invalid_point = Point::new(9999, 0);
781 editor.navigate(
782 Box::new(NavigationData {
783 cursor_anchor: invalid_anchor,
784 cursor_position: invalid_point,
785 scroll_anchor: ScrollAnchor {
786 anchor: invalid_anchor,
787 offset: Default::default(),
788 },
789 scroll_top_row: invalid_point.row,
790 }),
791 window,
792 cx,
793 );
794 assert_eq!(
795 editor.selections.display_ranges(cx),
796 &[editor.max_point(cx)..editor.max_point(cx)]
797 );
798 assert_eq!(
799 editor.scroll_position(cx),
800 gpui::Point::new(0., editor.max_point(cx).row().as_f32())
801 );
802
803 editor
804 })
805 });
806}
807
808#[gpui::test]
809fn test_cancel(cx: &mut TestAppContext) {
810 init_test(cx, |_| {});
811
812 let editor = cx.add_window(|window, cx| {
813 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
814 build_editor(buffer, window, cx)
815 });
816
817 _ = editor.update(cx, |editor, window, cx| {
818 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
819 editor.update_selection(
820 DisplayPoint::new(DisplayRow(1), 1),
821 0,
822 gpui::Point::<f32>::default(),
823 window,
824 cx,
825 );
826 editor.end_selection(window, cx);
827
828 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
829 editor.update_selection(
830 DisplayPoint::new(DisplayRow(0), 3),
831 0,
832 gpui::Point::<f32>::default(),
833 window,
834 cx,
835 );
836 editor.end_selection(window, cx);
837 assert_eq!(
838 editor.selections.display_ranges(cx),
839 [
840 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
841 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
842 ]
843 );
844 });
845
846 _ = editor.update(cx, |editor, window, cx| {
847 editor.cancel(&Cancel, window, cx);
848 assert_eq!(
849 editor.selections.display_ranges(cx),
850 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
851 );
852 });
853
854 _ = editor.update(cx, |editor, window, cx| {
855 editor.cancel(&Cancel, window, cx);
856 assert_eq!(
857 editor.selections.display_ranges(cx),
858 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
859 );
860 });
861}
862
863#[gpui::test]
864fn test_fold_action(cx: &mut TestAppContext) {
865 init_test(cx, |_| {});
866
867 let editor = cx.add_window(|window, cx| {
868 let buffer = MultiBuffer::build_simple(
869 &"
870 impl Foo {
871 // Hello!
872
873 fn a() {
874 1
875 }
876
877 fn b() {
878 2
879 }
880
881 fn c() {
882 3
883 }
884 }
885 "
886 .unindent(),
887 cx,
888 );
889 build_editor(buffer.clone(), window, cx)
890 });
891
892 _ = editor.update(cx, |editor, window, cx| {
893 editor.change_selections(None, window, cx, |s| {
894 s.select_display_ranges([
895 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
896 ]);
897 });
898 editor.fold(&Fold, window, cx);
899 assert_eq!(
900 editor.display_text(cx),
901 "
902 impl Foo {
903 // Hello!
904
905 fn a() {
906 1
907 }
908
909 fn b() {⋯
910 }
911
912 fn c() {⋯
913 }
914 }
915 "
916 .unindent(),
917 );
918
919 editor.fold(&Fold, window, cx);
920 assert_eq!(
921 editor.display_text(cx),
922 "
923 impl Foo {⋯
924 }
925 "
926 .unindent(),
927 );
928
929 editor.unfold_lines(&UnfoldLines, window, cx);
930 assert_eq!(
931 editor.display_text(cx),
932 "
933 impl Foo {
934 // Hello!
935
936 fn a() {
937 1
938 }
939
940 fn b() {⋯
941 }
942
943 fn c() {⋯
944 }
945 }
946 "
947 .unindent(),
948 );
949
950 editor.unfold_lines(&UnfoldLines, window, cx);
951 assert_eq!(
952 editor.display_text(cx),
953 editor.buffer.read(cx).read(cx).text()
954 );
955 });
956}
957
958#[gpui::test]
959fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
960 init_test(cx, |_| {});
961
962 let editor = cx.add_window(|window, cx| {
963 let buffer = MultiBuffer::build_simple(
964 &"
965 class Foo:
966 # Hello!
967
968 def a():
969 print(1)
970
971 def b():
972 print(2)
973
974 def c():
975 print(3)
976 "
977 .unindent(),
978 cx,
979 );
980 build_editor(buffer.clone(), window, cx)
981 });
982
983 _ = editor.update(cx, |editor, window, cx| {
984 editor.change_selections(None, window, cx, |s| {
985 s.select_display_ranges([
986 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
987 ]);
988 });
989 editor.fold(&Fold, window, cx);
990 assert_eq!(
991 editor.display_text(cx),
992 "
993 class Foo:
994 # Hello!
995
996 def a():
997 print(1)
998
999 def b():⋯
1000
1001 def c():⋯
1002 "
1003 .unindent(),
1004 );
1005
1006 editor.fold(&Fold, window, cx);
1007 assert_eq!(
1008 editor.display_text(cx),
1009 "
1010 class Foo:⋯
1011 "
1012 .unindent(),
1013 );
1014
1015 editor.unfold_lines(&UnfoldLines, window, cx);
1016 assert_eq!(
1017 editor.display_text(cx),
1018 "
1019 class Foo:
1020 # Hello!
1021
1022 def a():
1023 print(1)
1024
1025 def b():⋯
1026
1027 def c():⋯
1028 "
1029 .unindent(),
1030 );
1031
1032 editor.unfold_lines(&UnfoldLines, window, cx);
1033 assert_eq!(
1034 editor.display_text(cx),
1035 editor.buffer.read(cx).read(cx).text()
1036 );
1037 });
1038}
1039
1040#[gpui::test]
1041fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1042 init_test(cx, |_| {});
1043
1044 let editor = cx.add_window(|window, cx| {
1045 let buffer = MultiBuffer::build_simple(
1046 &"
1047 class Foo:
1048 # Hello!
1049
1050 def a():
1051 print(1)
1052
1053 def b():
1054 print(2)
1055
1056
1057 def c():
1058 print(3)
1059
1060
1061 "
1062 .unindent(),
1063 cx,
1064 );
1065 build_editor(buffer.clone(), window, cx)
1066 });
1067
1068 _ = editor.update(cx, |editor, window, cx| {
1069 editor.change_selections(None, window, cx, |s| {
1070 s.select_display_ranges([
1071 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1072 ]);
1073 });
1074 editor.fold(&Fold, window, cx);
1075 assert_eq!(
1076 editor.display_text(cx),
1077 "
1078 class Foo:
1079 # Hello!
1080
1081 def a():
1082 print(1)
1083
1084 def b():⋯
1085
1086
1087 def c():⋯
1088
1089
1090 "
1091 .unindent(),
1092 );
1093
1094 editor.fold(&Fold, window, cx);
1095 assert_eq!(
1096 editor.display_text(cx),
1097 "
1098 class Foo:⋯
1099
1100
1101 "
1102 .unindent(),
1103 );
1104
1105 editor.unfold_lines(&UnfoldLines, window, cx);
1106 assert_eq!(
1107 editor.display_text(cx),
1108 "
1109 class Foo:
1110 # Hello!
1111
1112 def a():
1113 print(1)
1114
1115 def b():⋯
1116
1117
1118 def c():⋯
1119
1120
1121 "
1122 .unindent(),
1123 );
1124
1125 editor.unfold_lines(&UnfoldLines, window, cx);
1126 assert_eq!(
1127 editor.display_text(cx),
1128 editor.buffer.read(cx).read(cx).text()
1129 );
1130 });
1131}
1132
1133#[gpui::test]
1134fn test_fold_at_level(cx: &mut TestAppContext) {
1135 init_test(cx, |_| {});
1136
1137 let editor = cx.add_window(|window, cx| {
1138 let buffer = MultiBuffer::build_simple(
1139 &"
1140 class Foo:
1141 # Hello!
1142
1143 def a():
1144 print(1)
1145
1146 def b():
1147 print(2)
1148
1149
1150 class Bar:
1151 # World!
1152
1153 def a():
1154 print(1)
1155
1156 def b():
1157 print(2)
1158
1159
1160 "
1161 .unindent(),
1162 cx,
1163 );
1164 build_editor(buffer.clone(), window, cx)
1165 });
1166
1167 _ = editor.update(cx, |editor, window, cx| {
1168 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1169 assert_eq!(
1170 editor.display_text(cx),
1171 "
1172 class Foo:
1173 # Hello!
1174
1175 def a():⋯
1176
1177 def b():⋯
1178
1179
1180 class Bar:
1181 # World!
1182
1183 def a():⋯
1184
1185 def b():⋯
1186
1187
1188 "
1189 .unindent(),
1190 );
1191
1192 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1193 assert_eq!(
1194 editor.display_text(cx),
1195 "
1196 class Foo:⋯
1197
1198
1199 class Bar:⋯
1200
1201
1202 "
1203 .unindent(),
1204 );
1205
1206 editor.unfold_all(&UnfoldAll, window, cx);
1207 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1208 assert_eq!(
1209 editor.display_text(cx),
1210 "
1211 class Foo:
1212 # Hello!
1213
1214 def a():
1215 print(1)
1216
1217 def b():
1218 print(2)
1219
1220
1221 class Bar:
1222 # World!
1223
1224 def a():
1225 print(1)
1226
1227 def b():
1228 print(2)
1229
1230
1231 "
1232 .unindent(),
1233 );
1234
1235 assert_eq!(
1236 editor.display_text(cx),
1237 editor.buffer.read(cx).read(cx).text()
1238 );
1239 });
1240}
1241
1242#[gpui::test]
1243fn test_move_cursor(cx: &mut TestAppContext) {
1244 init_test(cx, |_| {});
1245
1246 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1247 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1248
1249 buffer.update(cx, |buffer, cx| {
1250 buffer.edit(
1251 vec![
1252 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1253 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1254 ],
1255 None,
1256 cx,
1257 );
1258 });
1259 _ = editor.update(cx, |editor, window, cx| {
1260 assert_eq!(
1261 editor.selections.display_ranges(cx),
1262 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1263 );
1264
1265 editor.move_down(&MoveDown, window, cx);
1266 assert_eq!(
1267 editor.selections.display_ranges(cx),
1268 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1269 );
1270
1271 editor.move_right(&MoveRight, window, cx);
1272 assert_eq!(
1273 editor.selections.display_ranges(cx),
1274 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1275 );
1276
1277 editor.move_left(&MoveLeft, window, cx);
1278 assert_eq!(
1279 editor.selections.display_ranges(cx),
1280 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1281 );
1282
1283 editor.move_up(&MoveUp, window, cx);
1284 assert_eq!(
1285 editor.selections.display_ranges(cx),
1286 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1287 );
1288
1289 editor.move_to_end(&MoveToEnd, window, cx);
1290 assert_eq!(
1291 editor.selections.display_ranges(cx),
1292 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1293 );
1294
1295 editor.move_to_beginning(&MoveToBeginning, window, cx);
1296 assert_eq!(
1297 editor.selections.display_ranges(cx),
1298 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1299 );
1300
1301 editor.change_selections(None, window, cx, |s| {
1302 s.select_display_ranges([
1303 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1304 ]);
1305 });
1306 editor.select_to_beginning(&SelectToBeginning, window, cx);
1307 assert_eq!(
1308 editor.selections.display_ranges(cx),
1309 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1310 );
1311
1312 editor.select_to_end(&SelectToEnd, window, cx);
1313 assert_eq!(
1314 editor.selections.display_ranges(cx),
1315 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1316 );
1317 });
1318}
1319
1320#[gpui::test]
1321fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1322 init_test(cx, |_| {});
1323
1324 let editor = cx.add_window(|window, cx| {
1325 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1326 build_editor(buffer.clone(), window, cx)
1327 });
1328
1329 assert_eq!('🟥'.len_utf8(), 4);
1330 assert_eq!('α'.len_utf8(), 2);
1331
1332 _ = editor.update(cx, |editor, window, cx| {
1333 editor.fold_creases(
1334 vec![
1335 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1336 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1337 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1338 ],
1339 true,
1340 window,
1341 cx,
1342 );
1343 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1344
1345 editor.move_right(&MoveRight, window, cx);
1346 assert_eq!(
1347 editor.selections.display_ranges(cx),
1348 &[empty_range(0, "🟥".len())]
1349 );
1350 editor.move_right(&MoveRight, window, cx);
1351 assert_eq!(
1352 editor.selections.display_ranges(cx),
1353 &[empty_range(0, "🟥🟧".len())]
1354 );
1355 editor.move_right(&MoveRight, window, cx);
1356 assert_eq!(
1357 editor.selections.display_ranges(cx),
1358 &[empty_range(0, "🟥🟧⋯".len())]
1359 );
1360
1361 editor.move_down(&MoveDown, window, cx);
1362 assert_eq!(
1363 editor.selections.display_ranges(cx),
1364 &[empty_range(1, "ab⋯e".len())]
1365 );
1366 editor.move_left(&MoveLeft, window, cx);
1367 assert_eq!(
1368 editor.selections.display_ranges(cx),
1369 &[empty_range(1, "ab⋯".len())]
1370 );
1371 editor.move_left(&MoveLeft, window, cx);
1372 assert_eq!(
1373 editor.selections.display_ranges(cx),
1374 &[empty_range(1, "ab".len())]
1375 );
1376 editor.move_left(&MoveLeft, window, cx);
1377 assert_eq!(
1378 editor.selections.display_ranges(cx),
1379 &[empty_range(1, "a".len())]
1380 );
1381
1382 editor.move_down(&MoveDown, window, cx);
1383 assert_eq!(
1384 editor.selections.display_ranges(cx),
1385 &[empty_range(2, "α".len())]
1386 );
1387 editor.move_right(&MoveRight, window, cx);
1388 assert_eq!(
1389 editor.selections.display_ranges(cx),
1390 &[empty_range(2, "αβ".len())]
1391 );
1392 editor.move_right(&MoveRight, window, cx);
1393 assert_eq!(
1394 editor.selections.display_ranges(cx),
1395 &[empty_range(2, "αβ⋯".len())]
1396 );
1397 editor.move_right(&MoveRight, window, cx);
1398 assert_eq!(
1399 editor.selections.display_ranges(cx),
1400 &[empty_range(2, "αβ⋯ε".len())]
1401 );
1402
1403 editor.move_up(&MoveUp, window, cx);
1404 assert_eq!(
1405 editor.selections.display_ranges(cx),
1406 &[empty_range(1, "ab⋯e".len())]
1407 );
1408 editor.move_down(&MoveDown, window, cx);
1409 assert_eq!(
1410 editor.selections.display_ranges(cx),
1411 &[empty_range(2, "αβ⋯ε".len())]
1412 );
1413 editor.move_up(&MoveUp, window, cx);
1414 assert_eq!(
1415 editor.selections.display_ranges(cx),
1416 &[empty_range(1, "ab⋯e".len())]
1417 );
1418
1419 editor.move_up(&MoveUp, window, cx);
1420 assert_eq!(
1421 editor.selections.display_ranges(cx),
1422 &[empty_range(0, "🟥🟧".len())]
1423 );
1424 editor.move_left(&MoveLeft, window, cx);
1425 assert_eq!(
1426 editor.selections.display_ranges(cx),
1427 &[empty_range(0, "🟥".len())]
1428 );
1429 editor.move_left(&MoveLeft, window, cx);
1430 assert_eq!(
1431 editor.selections.display_ranges(cx),
1432 &[empty_range(0, "".len())]
1433 );
1434 });
1435}
1436
1437#[gpui::test]
1438fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1439 init_test(cx, |_| {});
1440
1441 let editor = cx.add_window(|window, cx| {
1442 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1443 build_editor(buffer.clone(), window, cx)
1444 });
1445 _ = editor.update(cx, |editor, window, cx| {
1446 editor.change_selections(None, window, cx, |s| {
1447 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1448 });
1449
1450 // moving above start of document should move selection to start of document,
1451 // but the next move down should still be at the original goal_x
1452 editor.move_up(&MoveUp, window, cx);
1453 assert_eq!(
1454 editor.selections.display_ranges(cx),
1455 &[empty_range(0, "".len())]
1456 );
1457
1458 editor.move_down(&MoveDown, window, cx);
1459 assert_eq!(
1460 editor.selections.display_ranges(cx),
1461 &[empty_range(1, "abcd".len())]
1462 );
1463
1464 editor.move_down(&MoveDown, window, cx);
1465 assert_eq!(
1466 editor.selections.display_ranges(cx),
1467 &[empty_range(2, "αβγ".len())]
1468 );
1469
1470 editor.move_down(&MoveDown, window, cx);
1471 assert_eq!(
1472 editor.selections.display_ranges(cx),
1473 &[empty_range(3, "abcd".len())]
1474 );
1475
1476 editor.move_down(&MoveDown, window, cx);
1477 assert_eq!(
1478 editor.selections.display_ranges(cx),
1479 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1480 );
1481
1482 // moving past end of document should not change goal_x
1483 editor.move_down(&MoveDown, window, cx);
1484 assert_eq!(
1485 editor.selections.display_ranges(cx),
1486 &[empty_range(5, "".len())]
1487 );
1488
1489 editor.move_down(&MoveDown, window, cx);
1490 assert_eq!(
1491 editor.selections.display_ranges(cx),
1492 &[empty_range(5, "".len())]
1493 );
1494
1495 editor.move_up(&MoveUp, window, cx);
1496 assert_eq!(
1497 editor.selections.display_ranges(cx),
1498 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1499 );
1500
1501 editor.move_up(&MoveUp, window, cx);
1502 assert_eq!(
1503 editor.selections.display_ranges(cx),
1504 &[empty_range(3, "abcd".len())]
1505 );
1506
1507 editor.move_up(&MoveUp, window, cx);
1508 assert_eq!(
1509 editor.selections.display_ranges(cx),
1510 &[empty_range(2, "αβγ".len())]
1511 );
1512 });
1513}
1514
1515#[gpui::test]
1516fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1517 init_test(cx, |_| {});
1518 let move_to_beg = MoveToBeginningOfLine {
1519 stop_at_soft_wraps: true,
1520 stop_at_indent: true,
1521 };
1522
1523 let delete_to_beg = DeleteToBeginningOfLine {
1524 stop_at_indent: false,
1525 };
1526
1527 let move_to_end = MoveToEndOfLine {
1528 stop_at_soft_wraps: true,
1529 };
1530
1531 let editor = cx.add_window(|window, cx| {
1532 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1533 build_editor(buffer, window, cx)
1534 });
1535 _ = editor.update(cx, |editor, window, cx| {
1536 editor.change_selections(None, window, cx, |s| {
1537 s.select_display_ranges([
1538 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1539 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1540 ]);
1541 });
1542 });
1543
1544 _ = editor.update(cx, |editor, window, cx| {
1545 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1546 assert_eq!(
1547 editor.selections.display_ranges(cx),
1548 &[
1549 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1550 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1551 ]
1552 );
1553 });
1554
1555 _ = editor.update(cx, |editor, window, cx| {
1556 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1557 assert_eq!(
1558 editor.selections.display_ranges(cx),
1559 &[
1560 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1561 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1562 ]
1563 );
1564 });
1565
1566 _ = editor.update(cx, |editor, window, cx| {
1567 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1568 assert_eq!(
1569 editor.selections.display_ranges(cx),
1570 &[
1571 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1572 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1573 ]
1574 );
1575 });
1576
1577 _ = editor.update(cx, |editor, window, cx| {
1578 editor.move_to_end_of_line(&move_to_end, window, cx);
1579 assert_eq!(
1580 editor.selections.display_ranges(cx),
1581 &[
1582 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1583 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1584 ]
1585 );
1586 });
1587
1588 // Moving to the end of line again is a no-op.
1589 _ = editor.update(cx, |editor, window, cx| {
1590 editor.move_to_end_of_line(&move_to_end, window, cx);
1591 assert_eq!(
1592 editor.selections.display_ranges(cx),
1593 &[
1594 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1595 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1596 ]
1597 );
1598 });
1599
1600 _ = editor.update(cx, |editor, window, cx| {
1601 editor.move_left(&MoveLeft, window, cx);
1602 editor.select_to_beginning_of_line(
1603 &SelectToBeginningOfLine {
1604 stop_at_soft_wraps: true,
1605 stop_at_indent: true,
1606 },
1607 window,
1608 cx,
1609 );
1610 assert_eq!(
1611 editor.selections.display_ranges(cx),
1612 &[
1613 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1614 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1615 ]
1616 );
1617 });
1618
1619 _ = editor.update(cx, |editor, window, cx| {
1620 editor.select_to_beginning_of_line(
1621 &SelectToBeginningOfLine {
1622 stop_at_soft_wraps: true,
1623 stop_at_indent: true,
1624 },
1625 window,
1626 cx,
1627 );
1628 assert_eq!(
1629 editor.selections.display_ranges(cx),
1630 &[
1631 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1632 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1633 ]
1634 );
1635 });
1636
1637 _ = editor.update(cx, |editor, window, cx| {
1638 editor.select_to_beginning_of_line(
1639 &SelectToBeginningOfLine {
1640 stop_at_soft_wraps: true,
1641 stop_at_indent: true,
1642 },
1643 window,
1644 cx,
1645 );
1646 assert_eq!(
1647 editor.selections.display_ranges(cx),
1648 &[
1649 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1650 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1651 ]
1652 );
1653 });
1654
1655 _ = editor.update(cx, |editor, window, cx| {
1656 editor.select_to_end_of_line(
1657 &SelectToEndOfLine {
1658 stop_at_soft_wraps: true,
1659 },
1660 window,
1661 cx,
1662 );
1663 assert_eq!(
1664 editor.selections.display_ranges(cx),
1665 &[
1666 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1667 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1668 ]
1669 );
1670 });
1671
1672 _ = editor.update(cx, |editor, window, cx| {
1673 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1674 assert_eq!(editor.display_text(cx), "ab\n de");
1675 assert_eq!(
1676 editor.selections.display_ranges(cx),
1677 &[
1678 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1679 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1680 ]
1681 );
1682 });
1683
1684 _ = editor.update(cx, |editor, window, cx| {
1685 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1686 assert_eq!(editor.display_text(cx), "\n");
1687 assert_eq!(
1688 editor.selections.display_ranges(cx),
1689 &[
1690 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1691 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1692 ]
1693 );
1694 });
1695}
1696
1697#[gpui::test]
1698fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1699 init_test(cx, |_| {});
1700 let move_to_beg = MoveToBeginningOfLine {
1701 stop_at_soft_wraps: false,
1702 stop_at_indent: false,
1703 };
1704
1705 let move_to_end = MoveToEndOfLine {
1706 stop_at_soft_wraps: false,
1707 };
1708
1709 let editor = cx.add_window(|window, cx| {
1710 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1711 build_editor(buffer, window, cx)
1712 });
1713
1714 _ = editor.update(cx, |editor, window, cx| {
1715 editor.set_wrap_width(Some(140.0.into()), cx);
1716
1717 // We expect the following lines after wrapping
1718 // ```
1719 // thequickbrownfox
1720 // jumpedoverthelazydo
1721 // gs
1722 // ```
1723 // The final `gs` was soft-wrapped onto a new line.
1724 assert_eq!(
1725 "thequickbrownfox\njumpedoverthelaz\nydogs",
1726 editor.display_text(cx),
1727 );
1728
1729 // First, let's assert behavior on the first line, that was not soft-wrapped.
1730 // Start the cursor at the `k` on the first line
1731 editor.change_selections(None, window, cx, |s| {
1732 s.select_display_ranges([
1733 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1734 ]);
1735 });
1736
1737 // Moving to the beginning of the line should put us at the beginning of the line.
1738 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1739 assert_eq!(
1740 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1741 editor.selections.display_ranges(cx)
1742 );
1743
1744 // Moving to the end of the line should put us at the end of the line.
1745 editor.move_to_end_of_line(&move_to_end, window, cx);
1746 assert_eq!(
1747 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1748 editor.selections.display_ranges(cx)
1749 );
1750
1751 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1752 // Start the cursor at the last line (`y` that was wrapped to a new line)
1753 editor.change_selections(None, window, cx, |s| {
1754 s.select_display_ranges([
1755 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1756 ]);
1757 });
1758
1759 // Moving to the beginning of the line should put us at the start of the second line of
1760 // display text, i.e., the `j`.
1761 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1762 assert_eq!(
1763 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1764 editor.selections.display_ranges(cx)
1765 );
1766
1767 // Moving to the beginning of the line again should be a no-op.
1768 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1769 assert_eq!(
1770 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1771 editor.selections.display_ranges(cx)
1772 );
1773
1774 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1775 // next display line.
1776 editor.move_to_end_of_line(&move_to_end, window, cx);
1777 assert_eq!(
1778 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1779 editor.selections.display_ranges(cx)
1780 );
1781
1782 // Moving to the end of the line again should be a no-op.
1783 editor.move_to_end_of_line(&move_to_end, window, cx);
1784 assert_eq!(
1785 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1786 editor.selections.display_ranges(cx)
1787 );
1788 });
1789}
1790
1791#[gpui::test]
1792fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1793 init_test(cx, |_| {});
1794
1795 let move_to_beg = MoveToBeginningOfLine {
1796 stop_at_soft_wraps: true,
1797 stop_at_indent: true,
1798 };
1799
1800 let select_to_beg = SelectToBeginningOfLine {
1801 stop_at_soft_wraps: true,
1802 stop_at_indent: true,
1803 };
1804
1805 let delete_to_beg = DeleteToBeginningOfLine {
1806 stop_at_indent: true,
1807 };
1808
1809 let move_to_end = MoveToEndOfLine {
1810 stop_at_soft_wraps: false,
1811 };
1812
1813 let editor = cx.add_window(|window, cx| {
1814 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1815 build_editor(buffer, window, cx)
1816 });
1817
1818 _ = editor.update(cx, |editor, window, cx| {
1819 editor.change_selections(None, window, cx, |s| {
1820 s.select_display_ranges([
1821 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1822 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1823 ]);
1824 });
1825
1826 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1827 // and the second cursor at the first non-whitespace character in the line.
1828 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1829 assert_eq!(
1830 editor.selections.display_ranges(cx),
1831 &[
1832 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1833 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1834 ]
1835 );
1836
1837 // Moving to the beginning of the line again should be a no-op for the first cursor,
1838 // and should move the second cursor to the beginning of the line.
1839 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1840 assert_eq!(
1841 editor.selections.display_ranges(cx),
1842 &[
1843 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1844 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1845 ]
1846 );
1847
1848 // Moving to the beginning of the line again should still be a no-op for the first cursor,
1849 // and should move the second cursor back to the first non-whitespace character in the line.
1850 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1851 assert_eq!(
1852 editor.selections.display_ranges(cx),
1853 &[
1854 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1855 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1856 ]
1857 );
1858
1859 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
1860 // and to the first non-whitespace character in the line for the second cursor.
1861 editor.move_to_end_of_line(&move_to_end, window, cx);
1862 editor.move_left(&MoveLeft, window, cx);
1863 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1864 assert_eq!(
1865 editor.selections.display_ranges(cx),
1866 &[
1867 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1868 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1869 ]
1870 );
1871
1872 // Selecting to the beginning of the line again should be a no-op for the first cursor,
1873 // and should select to the beginning of the line for the second cursor.
1874 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1875 assert_eq!(
1876 editor.selections.display_ranges(cx),
1877 &[
1878 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1879 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1880 ]
1881 );
1882
1883 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
1884 // and should delete to the first non-whitespace character in the line for the second cursor.
1885 editor.move_to_end_of_line(&move_to_end, window, cx);
1886 editor.move_left(&MoveLeft, window, cx);
1887 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1888 assert_eq!(editor.text(cx), "c\n f");
1889 });
1890}
1891
1892#[gpui::test]
1893fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
1894 init_test(cx, |_| {});
1895
1896 let editor = cx.add_window(|window, cx| {
1897 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
1898 build_editor(buffer, window, cx)
1899 });
1900 _ = editor.update(cx, |editor, window, cx| {
1901 editor.change_selections(None, window, cx, |s| {
1902 s.select_display_ranges([
1903 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
1904 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
1905 ])
1906 });
1907
1908 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1909 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1910
1911 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1912 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\n ˇ{baz.qux()}", editor, cx);
1913
1914 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1915 assert_selection_ranges("use ˇstd::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
1916
1917 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1918 assert_selection_ranges("ˇuse std::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1919
1920 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1921 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1922
1923 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1924 assert_selection_ranges("useˇ std::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
1925
1926 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1927 assert_selection_ranges("use stdˇ::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1928
1929 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1930 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1931
1932 editor.move_right(&MoveRight, window, cx);
1933 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1934 assert_selection_ranges(
1935 "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
1936 editor,
1937 cx,
1938 );
1939
1940 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1941 assert_selection_ranges(
1942 "use std«ˇ::s»tr::{foo, bar}\n\n «ˇ{b»az.qux()}",
1943 editor,
1944 cx,
1945 );
1946
1947 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
1948 assert_selection_ranges(
1949 "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
1950 editor,
1951 cx,
1952 );
1953 });
1954}
1955
1956#[gpui::test]
1957fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
1958 init_test(cx, |_| {});
1959
1960 let editor = cx.add_window(|window, cx| {
1961 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
1962 build_editor(buffer, window, cx)
1963 });
1964
1965 _ = editor.update(cx, |editor, window, cx| {
1966 editor.set_wrap_width(Some(140.0.into()), cx);
1967 assert_eq!(
1968 editor.display_text(cx),
1969 "use one::{\n two::three::\n four::five\n};"
1970 );
1971
1972 editor.change_selections(None, window, cx, |s| {
1973 s.select_display_ranges([
1974 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
1975 ]);
1976 });
1977
1978 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1979 assert_eq!(
1980 editor.selections.display_ranges(cx),
1981 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
1982 );
1983
1984 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1985 assert_eq!(
1986 editor.selections.display_ranges(cx),
1987 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
1988 );
1989
1990 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1991 assert_eq!(
1992 editor.selections.display_ranges(cx),
1993 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
1994 );
1995
1996 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1997 assert_eq!(
1998 editor.selections.display_ranges(cx),
1999 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2000 );
2001
2002 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2003 assert_eq!(
2004 editor.selections.display_ranges(cx),
2005 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2006 );
2007
2008 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2009 assert_eq!(
2010 editor.selections.display_ranges(cx),
2011 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2012 );
2013 });
2014}
2015
2016#[gpui::test]
2017async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2018 init_test(cx, |_| {});
2019 let mut cx = EditorTestContext::new(cx).await;
2020
2021 let line_height = cx.editor(|editor, window, _| {
2022 editor
2023 .style()
2024 .unwrap()
2025 .text
2026 .line_height_in_pixels(window.rem_size())
2027 });
2028 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2029
2030 cx.set_state(
2031 &r#"ˇone
2032 two
2033
2034 three
2035 fourˇ
2036 five
2037
2038 six"#
2039 .unindent(),
2040 );
2041
2042 cx.update_editor(|editor, window, cx| {
2043 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2044 });
2045 cx.assert_editor_state(
2046 &r#"one
2047 two
2048 ˇ
2049 three
2050 four
2051 five
2052 ˇ
2053 six"#
2054 .unindent(),
2055 );
2056
2057 cx.update_editor(|editor, window, cx| {
2058 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2059 });
2060 cx.assert_editor_state(
2061 &r#"one
2062 two
2063
2064 three
2065 four
2066 five
2067 ˇ
2068 sixˇ"#
2069 .unindent(),
2070 );
2071
2072 cx.update_editor(|editor, window, cx| {
2073 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2074 });
2075 cx.assert_editor_state(
2076 &r#"one
2077 two
2078
2079 three
2080 four
2081 five
2082
2083 sixˇ"#
2084 .unindent(),
2085 );
2086
2087 cx.update_editor(|editor, window, cx| {
2088 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2089 });
2090 cx.assert_editor_state(
2091 &r#"one
2092 two
2093
2094 three
2095 four
2096 five
2097 ˇ
2098 six"#
2099 .unindent(),
2100 );
2101
2102 cx.update_editor(|editor, window, cx| {
2103 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2104 });
2105 cx.assert_editor_state(
2106 &r#"one
2107 two
2108 ˇ
2109 three
2110 four
2111 five
2112
2113 six"#
2114 .unindent(),
2115 );
2116
2117 cx.update_editor(|editor, window, cx| {
2118 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2119 });
2120 cx.assert_editor_state(
2121 &r#"ˇone
2122 two
2123
2124 three
2125 four
2126 five
2127
2128 six"#
2129 .unindent(),
2130 );
2131}
2132
2133#[gpui::test]
2134async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2135 init_test(cx, |_| {});
2136 let mut cx = EditorTestContext::new(cx).await;
2137 let line_height = cx.editor(|editor, window, _| {
2138 editor
2139 .style()
2140 .unwrap()
2141 .text
2142 .line_height_in_pixels(window.rem_size())
2143 });
2144 let window = cx.window;
2145 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2146
2147 cx.set_state(
2148 r#"ˇone
2149 two
2150 three
2151 four
2152 five
2153 six
2154 seven
2155 eight
2156 nine
2157 ten
2158 "#,
2159 );
2160
2161 cx.update_editor(|editor, window, cx| {
2162 assert_eq!(
2163 editor.snapshot(window, cx).scroll_position(),
2164 gpui::Point::new(0., 0.)
2165 );
2166 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2167 assert_eq!(
2168 editor.snapshot(window, cx).scroll_position(),
2169 gpui::Point::new(0., 3.)
2170 );
2171 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2172 assert_eq!(
2173 editor.snapshot(window, cx).scroll_position(),
2174 gpui::Point::new(0., 6.)
2175 );
2176 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2177 assert_eq!(
2178 editor.snapshot(window, cx).scroll_position(),
2179 gpui::Point::new(0., 3.)
2180 );
2181
2182 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2183 assert_eq!(
2184 editor.snapshot(window, cx).scroll_position(),
2185 gpui::Point::new(0., 1.)
2186 );
2187 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2188 assert_eq!(
2189 editor.snapshot(window, cx).scroll_position(),
2190 gpui::Point::new(0., 3.)
2191 );
2192 });
2193}
2194
2195#[gpui::test]
2196async fn test_autoscroll(cx: &mut TestAppContext) {
2197 init_test(cx, |_| {});
2198 let mut cx = EditorTestContext::new(cx).await;
2199
2200 let line_height = cx.update_editor(|editor, window, cx| {
2201 editor.set_vertical_scroll_margin(2, cx);
2202 editor
2203 .style()
2204 .unwrap()
2205 .text
2206 .line_height_in_pixels(window.rem_size())
2207 });
2208 let window = cx.window;
2209 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2210
2211 cx.set_state(
2212 r#"ˇone
2213 two
2214 three
2215 four
2216 five
2217 six
2218 seven
2219 eight
2220 nine
2221 ten
2222 "#,
2223 );
2224 cx.update_editor(|editor, window, cx| {
2225 assert_eq!(
2226 editor.snapshot(window, cx).scroll_position(),
2227 gpui::Point::new(0., 0.0)
2228 );
2229 });
2230
2231 // Add a cursor below the visible area. Since both cursors cannot fit
2232 // on screen, the editor autoscrolls to reveal the newest cursor, and
2233 // allows the vertical scroll margin below that cursor.
2234 cx.update_editor(|editor, window, cx| {
2235 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2236 selections.select_ranges([
2237 Point::new(0, 0)..Point::new(0, 0),
2238 Point::new(6, 0)..Point::new(6, 0),
2239 ]);
2240 })
2241 });
2242 cx.update_editor(|editor, window, cx| {
2243 assert_eq!(
2244 editor.snapshot(window, cx).scroll_position(),
2245 gpui::Point::new(0., 3.0)
2246 );
2247 });
2248
2249 // Move down. The editor cursor scrolls down to track the newest cursor.
2250 cx.update_editor(|editor, window, cx| {
2251 editor.move_down(&Default::default(), window, cx);
2252 });
2253 cx.update_editor(|editor, window, cx| {
2254 assert_eq!(
2255 editor.snapshot(window, cx).scroll_position(),
2256 gpui::Point::new(0., 4.0)
2257 );
2258 });
2259
2260 // Add a cursor above the visible area. Since both cursors fit on screen,
2261 // the editor scrolls to show both.
2262 cx.update_editor(|editor, window, cx| {
2263 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2264 selections.select_ranges([
2265 Point::new(1, 0)..Point::new(1, 0),
2266 Point::new(6, 0)..Point::new(6, 0),
2267 ]);
2268 })
2269 });
2270 cx.update_editor(|editor, window, cx| {
2271 assert_eq!(
2272 editor.snapshot(window, cx).scroll_position(),
2273 gpui::Point::new(0., 1.0)
2274 );
2275 });
2276}
2277
2278#[gpui::test]
2279async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2280 init_test(cx, |_| {});
2281 let mut cx = EditorTestContext::new(cx).await;
2282
2283 let line_height = cx.editor(|editor, window, _cx| {
2284 editor
2285 .style()
2286 .unwrap()
2287 .text
2288 .line_height_in_pixels(window.rem_size())
2289 });
2290 let window = cx.window;
2291 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2292 cx.set_state(
2293 &r#"
2294 ˇone
2295 two
2296 threeˇ
2297 four
2298 five
2299 six
2300 seven
2301 eight
2302 nine
2303 ten
2304 "#
2305 .unindent(),
2306 );
2307
2308 cx.update_editor(|editor, window, cx| {
2309 editor.move_page_down(&MovePageDown::default(), window, cx)
2310 });
2311 cx.assert_editor_state(
2312 &r#"
2313 one
2314 two
2315 three
2316 ˇfour
2317 five
2318 sixˇ
2319 seven
2320 eight
2321 nine
2322 ten
2323 "#
2324 .unindent(),
2325 );
2326
2327 cx.update_editor(|editor, window, cx| {
2328 editor.move_page_down(&MovePageDown::default(), window, cx)
2329 });
2330 cx.assert_editor_state(
2331 &r#"
2332 one
2333 two
2334 three
2335 four
2336 five
2337 six
2338 ˇseven
2339 eight
2340 nineˇ
2341 ten
2342 "#
2343 .unindent(),
2344 );
2345
2346 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2347 cx.assert_editor_state(
2348 &r#"
2349 one
2350 two
2351 three
2352 ˇfour
2353 five
2354 sixˇ
2355 seven
2356 eight
2357 nine
2358 ten
2359 "#
2360 .unindent(),
2361 );
2362
2363 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2364 cx.assert_editor_state(
2365 &r#"
2366 ˇone
2367 two
2368 threeˇ
2369 four
2370 five
2371 six
2372 seven
2373 eight
2374 nine
2375 ten
2376 "#
2377 .unindent(),
2378 );
2379
2380 // Test select collapsing
2381 cx.update_editor(|editor, window, cx| {
2382 editor.move_page_down(&MovePageDown::default(), window, cx);
2383 editor.move_page_down(&MovePageDown::default(), window, cx);
2384 editor.move_page_down(&MovePageDown::default(), window, cx);
2385 });
2386 cx.assert_editor_state(
2387 &r#"
2388 one
2389 two
2390 three
2391 four
2392 five
2393 six
2394 seven
2395 eight
2396 nine
2397 ˇten
2398 ˇ"#
2399 .unindent(),
2400 );
2401}
2402
2403#[gpui::test]
2404async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2405 init_test(cx, |_| {});
2406 let mut cx = EditorTestContext::new(cx).await;
2407 cx.set_state("one «two threeˇ» four");
2408 cx.update_editor(|editor, window, cx| {
2409 editor.delete_to_beginning_of_line(
2410 &DeleteToBeginningOfLine {
2411 stop_at_indent: false,
2412 },
2413 window,
2414 cx,
2415 );
2416 assert_eq!(editor.text(cx), " four");
2417 });
2418}
2419
2420#[gpui::test]
2421fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2422 init_test(cx, |_| {});
2423
2424 let editor = cx.add_window(|window, cx| {
2425 let buffer = MultiBuffer::build_simple("one two three four", cx);
2426 build_editor(buffer.clone(), window, cx)
2427 });
2428
2429 _ = editor.update(cx, |editor, window, cx| {
2430 editor.change_selections(None, window, cx, |s| {
2431 s.select_display_ranges([
2432 // an empty selection - the preceding word fragment is deleted
2433 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2434 // characters selected - they are deleted
2435 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 12),
2436 ])
2437 });
2438 editor.delete_to_previous_word_start(
2439 &DeleteToPreviousWordStart {
2440 ignore_newlines: false,
2441 },
2442 window,
2443 cx,
2444 );
2445 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e two te four");
2446 });
2447
2448 _ = editor.update(cx, |editor, window, cx| {
2449 editor.change_selections(None, window, cx, |s| {
2450 s.select_display_ranges([
2451 // an empty selection - the following word fragment is deleted
2452 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
2453 // characters selected - they are deleted
2454 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 10),
2455 ])
2456 });
2457 editor.delete_to_next_word_end(
2458 &DeleteToNextWordEnd {
2459 ignore_newlines: false,
2460 },
2461 window,
2462 cx,
2463 );
2464 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e t te our");
2465 });
2466}
2467
2468#[gpui::test]
2469fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2470 init_test(cx, |_| {});
2471
2472 let editor = cx.add_window(|window, cx| {
2473 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2474 build_editor(buffer.clone(), window, cx)
2475 });
2476 let del_to_prev_word_start = DeleteToPreviousWordStart {
2477 ignore_newlines: false,
2478 };
2479 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2480 ignore_newlines: true,
2481 };
2482
2483 _ = editor.update(cx, |editor, window, cx| {
2484 editor.change_selections(None, window, cx, |s| {
2485 s.select_display_ranges([
2486 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2487 ])
2488 });
2489 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2490 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
2491 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2492 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
2493 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2494 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
2495 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2496 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
2497 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2498 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
2499 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2500 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2501 });
2502}
2503
2504#[gpui::test]
2505fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
2506 init_test(cx, |_| {});
2507
2508 let editor = cx.add_window(|window, cx| {
2509 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
2510 build_editor(buffer.clone(), window, cx)
2511 });
2512 let del_to_next_word_end = DeleteToNextWordEnd {
2513 ignore_newlines: false,
2514 };
2515 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
2516 ignore_newlines: true,
2517 };
2518
2519 _ = editor.update(cx, |editor, window, cx| {
2520 editor.change_selections(None, window, cx, |s| {
2521 s.select_display_ranges([
2522 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2523 ])
2524 });
2525 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2526 assert_eq!(
2527 editor.buffer.read(cx).read(cx).text(),
2528 "one\n two\nthree\n four"
2529 );
2530 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2531 assert_eq!(
2532 editor.buffer.read(cx).read(cx).text(),
2533 "\n two\nthree\n four"
2534 );
2535 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2536 assert_eq!(
2537 editor.buffer.read(cx).read(cx).text(),
2538 "two\nthree\n four"
2539 );
2540 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2541 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
2542 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2543 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
2544 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2545 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2546 });
2547}
2548
2549#[gpui::test]
2550fn test_newline(cx: &mut TestAppContext) {
2551 init_test(cx, |_| {});
2552
2553 let editor = cx.add_window(|window, cx| {
2554 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
2555 build_editor(buffer.clone(), window, cx)
2556 });
2557
2558 _ = editor.update(cx, |editor, window, cx| {
2559 editor.change_selections(None, window, cx, |s| {
2560 s.select_display_ranges([
2561 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2562 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2563 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
2564 ])
2565 });
2566
2567 editor.newline(&Newline, window, cx);
2568 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
2569 });
2570}
2571
2572#[gpui::test]
2573fn test_newline_with_old_selections(cx: &mut TestAppContext) {
2574 init_test(cx, |_| {});
2575
2576 let editor = cx.add_window(|window, cx| {
2577 let buffer = MultiBuffer::build_simple(
2578 "
2579 a
2580 b(
2581 X
2582 )
2583 c(
2584 X
2585 )
2586 "
2587 .unindent()
2588 .as_str(),
2589 cx,
2590 );
2591 let mut editor = build_editor(buffer.clone(), window, cx);
2592 editor.change_selections(None, window, cx, |s| {
2593 s.select_ranges([
2594 Point::new(2, 4)..Point::new(2, 5),
2595 Point::new(5, 4)..Point::new(5, 5),
2596 ])
2597 });
2598 editor
2599 });
2600
2601 _ = editor.update(cx, |editor, window, cx| {
2602 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2603 editor.buffer.update(cx, |buffer, cx| {
2604 buffer.edit(
2605 [
2606 (Point::new(1, 2)..Point::new(3, 0), ""),
2607 (Point::new(4, 2)..Point::new(6, 0), ""),
2608 ],
2609 None,
2610 cx,
2611 );
2612 assert_eq!(
2613 buffer.read(cx).text(),
2614 "
2615 a
2616 b()
2617 c()
2618 "
2619 .unindent()
2620 );
2621 });
2622 assert_eq!(
2623 editor.selections.ranges(cx),
2624 &[
2625 Point::new(1, 2)..Point::new(1, 2),
2626 Point::new(2, 2)..Point::new(2, 2),
2627 ],
2628 );
2629
2630 editor.newline(&Newline, window, cx);
2631 assert_eq!(
2632 editor.text(cx),
2633 "
2634 a
2635 b(
2636 )
2637 c(
2638 )
2639 "
2640 .unindent()
2641 );
2642
2643 // The selections are moved after the inserted newlines
2644 assert_eq!(
2645 editor.selections.ranges(cx),
2646 &[
2647 Point::new(2, 0)..Point::new(2, 0),
2648 Point::new(4, 0)..Point::new(4, 0),
2649 ],
2650 );
2651 });
2652}
2653
2654#[gpui::test]
2655async fn test_newline_above(cx: &mut TestAppContext) {
2656 init_test(cx, |settings| {
2657 settings.defaults.tab_size = NonZeroU32::new(4)
2658 });
2659
2660 let language = Arc::new(
2661 Language::new(
2662 LanguageConfig::default(),
2663 Some(tree_sitter_rust::LANGUAGE.into()),
2664 )
2665 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2666 .unwrap(),
2667 );
2668
2669 let mut cx = EditorTestContext::new(cx).await;
2670 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2671 cx.set_state(indoc! {"
2672 const a: ˇA = (
2673 (ˇ
2674 «const_functionˇ»(ˇ),
2675 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2676 )ˇ
2677 ˇ);ˇ
2678 "});
2679
2680 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
2681 cx.assert_editor_state(indoc! {"
2682 ˇ
2683 const a: A = (
2684 ˇ
2685 (
2686 ˇ
2687 ˇ
2688 const_function(),
2689 ˇ
2690 ˇ
2691 ˇ
2692 ˇ
2693 something_else,
2694 ˇ
2695 )
2696 ˇ
2697 ˇ
2698 );
2699 "});
2700}
2701
2702#[gpui::test]
2703async fn test_newline_below(cx: &mut TestAppContext) {
2704 init_test(cx, |settings| {
2705 settings.defaults.tab_size = NonZeroU32::new(4)
2706 });
2707
2708 let language = Arc::new(
2709 Language::new(
2710 LanguageConfig::default(),
2711 Some(tree_sitter_rust::LANGUAGE.into()),
2712 )
2713 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2714 .unwrap(),
2715 );
2716
2717 let mut cx = EditorTestContext::new(cx).await;
2718 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2719 cx.set_state(indoc! {"
2720 const a: ˇA = (
2721 (ˇ
2722 «const_functionˇ»(ˇ),
2723 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2724 )ˇ
2725 ˇ);ˇ
2726 "});
2727
2728 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
2729 cx.assert_editor_state(indoc! {"
2730 const a: A = (
2731 ˇ
2732 (
2733 ˇ
2734 const_function(),
2735 ˇ
2736 ˇ
2737 something_else,
2738 ˇ
2739 ˇ
2740 ˇ
2741 ˇ
2742 )
2743 ˇ
2744 );
2745 ˇ
2746 ˇ
2747 "});
2748}
2749
2750#[gpui::test]
2751async fn test_newline_comments(cx: &mut TestAppContext) {
2752 init_test(cx, |settings| {
2753 settings.defaults.tab_size = NonZeroU32::new(4)
2754 });
2755
2756 let language = Arc::new(Language::new(
2757 LanguageConfig {
2758 line_comments: vec!["// ".into()],
2759 ..LanguageConfig::default()
2760 },
2761 None,
2762 ));
2763 {
2764 let mut cx = EditorTestContext::new(cx).await;
2765 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2766 cx.set_state(indoc! {"
2767 // Fooˇ
2768 "});
2769
2770 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2771 cx.assert_editor_state(indoc! {"
2772 // Foo
2773 // ˇ
2774 "});
2775 // Ensure that we add comment prefix when existing line contains space
2776 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2777 cx.assert_editor_state(
2778 indoc! {"
2779 // Foo
2780 //s
2781 // ˇ
2782 "}
2783 .replace("s", " ") // s is used as space placeholder to prevent format on save
2784 .as_str(),
2785 );
2786 // Ensure that we add comment prefix when existing line does not contain space
2787 cx.set_state(indoc! {"
2788 // Foo
2789 //ˇ
2790 "});
2791 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2792 cx.assert_editor_state(indoc! {"
2793 // Foo
2794 //
2795 // ˇ
2796 "});
2797 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
2798 cx.set_state(indoc! {"
2799 ˇ// Foo
2800 "});
2801 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2802 cx.assert_editor_state(indoc! {"
2803
2804 ˇ// Foo
2805 "});
2806 }
2807 // Ensure that comment continuations can be disabled.
2808 update_test_language_settings(cx, |settings| {
2809 settings.defaults.extend_comment_on_newline = Some(false);
2810 });
2811 let mut cx = EditorTestContext::new(cx).await;
2812 cx.set_state(indoc! {"
2813 // Fooˇ
2814 "});
2815 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2816 cx.assert_editor_state(indoc! {"
2817 // Foo
2818 ˇ
2819 "});
2820}
2821
2822#[gpui::test]
2823async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
2824 init_test(cx, |settings| {
2825 settings.defaults.tab_size = NonZeroU32::new(4)
2826 });
2827
2828 let language = Arc::new(Language::new(
2829 LanguageConfig {
2830 documentation: Some(language::DocumentationConfig {
2831 start: "/**".into(),
2832 end: "*/".into(),
2833 prefix: "* ".into(),
2834 tab_size: NonZeroU32::new(1).unwrap(),
2835 }),
2836 ..LanguageConfig::default()
2837 },
2838 None,
2839 ));
2840 {
2841 let mut cx = EditorTestContext::new(cx).await;
2842 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2843 cx.set_state(indoc! {"
2844 /**ˇ
2845 "});
2846
2847 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2848 cx.assert_editor_state(indoc! {"
2849 /**
2850 * ˇ
2851 "});
2852 // Ensure that if cursor is before the comment start,
2853 // we do not actually insert a comment prefix.
2854 cx.set_state(indoc! {"
2855 ˇ/**
2856 "});
2857 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2858 cx.assert_editor_state(indoc! {"
2859
2860 ˇ/**
2861 "});
2862 // Ensure that if cursor is between it doesn't add comment prefix.
2863 cx.set_state(indoc! {"
2864 /*ˇ*
2865 "});
2866 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2867 cx.assert_editor_state(indoc! {"
2868 /*
2869 ˇ*
2870 "});
2871 // Ensure that if suffix exists on same line after cursor it adds new line.
2872 cx.set_state(indoc! {"
2873 /**ˇ*/
2874 "});
2875 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2876 cx.assert_editor_state(indoc! {"
2877 /**
2878 * ˇ
2879 */
2880 "});
2881 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2882 cx.set_state(indoc! {"
2883 /**ˇ */
2884 "});
2885 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2886 cx.assert_editor_state(indoc! {"
2887 /**
2888 * ˇ
2889 */
2890 "});
2891 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2892 cx.set_state(indoc! {"
2893 /** ˇ*/
2894 "});
2895 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2896 cx.assert_editor_state(
2897 indoc! {"
2898 /**s
2899 * ˇ
2900 */
2901 "}
2902 .replace("s", " ") // s is used as space placeholder to prevent format on save
2903 .as_str(),
2904 );
2905 // Ensure that delimiter space is preserved when newline on already
2906 // spaced delimiter.
2907 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2908 cx.assert_editor_state(
2909 indoc! {"
2910 /**s
2911 *s
2912 * ˇ
2913 */
2914 "}
2915 .replace("s", " ") // s is used as space placeholder to prevent format on save
2916 .as_str(),
2917 );
2918 // Ensure that delimiter space is preserved when space is not
2919 // on existing delimiter.
2920 cx.set_state(indoc! {"
2921 /**
2922 *ˇ
2923 */
2924 "});
2925 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2926 cx.assert_editor_state(indoc! {"
2927 /**
2928 *
2929 * ˇ
2930 */
2931 "});
2932 // Ensure that if suffix exists on same line after cursor it
2933 // doesn't add extra new line if prefix is not on same line.
2934 cx.set_state(indoc! {"
2935 /**
2936 ˇ*/
2937 "});
2938 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2939 cx.assert_editor_state(indoc! {"
2940 /**
2941
2942 ˇ*/
2943 "});
2944 // Ensure that it detects suffix after existing prefix.
2945 cx.set_state(indoc! {"
2946 /**ˇ/
2947 "});
2948 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2949 cx.assert_editor_state(indoc! {"
2950 /**
2951 ˇ/
2952 "});
2953 // Ensure that if suffix exists on same line before
2954 // cursor it does not add comment prefix.
2955 cx.set_state(indoc! {"
2956 /** */ˇ
2957 "});
2958 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2959 cx.assert_editor_state(indoc! {"
2960 /** */
2961 ˇ
2962 "});
2963 // Ensure that if suffix exists on same line before
2964 // cursor it does not add comment prefix.
2965 cx.set_state(indoc! {"
2966 /**
2967 *
2968 */ˇ
2969 "});
2970 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2971 cx.assert_editor_state(indoc! {"
2972 /**
2973 *
2974 */
2975 ˇ
2976 "});
2977 }
2978 // Ensure that comment continuations can be disabled.
2979 update_test_language_settings(cx, |settings| {
2980 settings.defaults.extend_comment_on_newline = Some(false);
2981 });
2982 let mut cx = EditorTestContext::new(cx).await;
2983 cx.set_state(indoc! {"
2984 /**ˇ
2985 "});
2986 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2987 cx.assert_editor_state(indoc! {"
2988 /**
2989 ˇ
2990 "});
2991}
2992
2993#[gpui::test]
2994fn test_insert_with_old_selections(cx: &mut TestAppContext) {
2995 init_test(cx, |_| {});
2996
2997 let editor = cx.add_window(|window, cx| {
2998 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
2999 let mut editor = build_editor(buffer.clone(), window, cx);
3000 editor.change_selections(None, window, cx, |s| {
3001 s.select_ranges([3..4, 11..12, 19..20])
3002 });
3003 editor
3004 });
3005
3006 _ = editor.update(cx, |editor, window, cx| {
3007 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3008 editor.buffer.update(cx, |buffer, cx| {
3009 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
3010 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3011 });
3012 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
3013
3014 editor.insert("Z", window, cx);
3015 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3016
3017 // The selections are moved after the inserted characters
3018 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
3019 });
3020}
3021
3022#[gpui::test]
3023async fn test_tab(cx: &mut TestAppContext) {
3024 init_test(cx, |settings| {
3025 settings.defaults.tab_size = NonZeroU32::new(3)
3026 });
3027
3028 let mut cx = EditorTestContext::new(cx).await;
3029 cx.set_state(indoc! {"
3030 ˇabˇc
3031 ˇ🏀ˇ🏀ˇefg
3032 dˇ
3033 "});
3034 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3035 cx.assert_editor_state(indoc! {"
3036 ˇab ˇc
3037 ˇ🏀 ˇ🏀 ˇefg
3038 d ˇ
3039 "});
3040
3041 cx.set_state(indoc! {"
3042 a
3043 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3044 "});
3045 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3046 cx.assert_editor_state(indoc! {"
3047 a
3048 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3049 "});
3050}
3051
3052#[gpui::test]
3053async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3054 init_test(cx, |_| {});
3055
3056 let mut cx = EditorTestContext::new(cx).await;
3057 let language = Arc::new(
3058 Language::new(
3059 LanguageConfig::default(),
3060 Some(tree_sitter_rust::LANGUAGE.into()),
3061 )
3062 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3063 .unwrap(),
3064 );
3065 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3066
3067 // test when all cursors are not at suggested indent
3068 // then simply move to their suggested indent location
3069 cx.set_state(indoc! {"
3070 const a: B = (
3071 c(
3072 ˇ
3073 ˇ )
3074 );
3075 "});
3076 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3077 cx.assert_editor_state(indoc! {"
3078 const a: B = (
3079 c(
3080 ˇ
3081 ˇ)
3082 );
3083 "});
3084
3085 // test cursor already at suggested indent not moving when
3086 // other cursors are yet to reach their suggested indents
3087 cx.set_state(indoc! {"
3088 ˇ
3089 const a: B = (
3090 c(
3091 d(
3092 ˇ
3093 )
3094 ˇ
3095 ˇ )
3096 );
3097 "});
3098 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3099 cx.assert_editor_state(indoc! {"
3100 ˇ
3101 const a: B = (
3102 c(
3103 d(
3104 ˇ
3105 )
3106 ˇ
3107 ˇ)
3108 );
3109 "});
3110 // test when all cursors are at suggested indent then tab is inserted
3111 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3112 cx.assert_editor_state(indoc! {"
3113 ˇ
3114 const a: B = (
3115 c(
3116 d(
3117 ˇ
3118 )
3119 ˇ
3120 ˇ)
3121 );
3122 "});
3123
3124 // test when current indent is less than suggested indent,
3125 // we adjust line to match suggested indent and move cursor to it
3126 //
3127 // when no other cursor is at word boundary, all of them should move
3128 cx.set_state(indoc! {"
3129 const a: B = (
3130 c(
3131 d(
3132 ˇ
3133 ˇ )
3134 ˇ )
3135 );
3136 "});
3137 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3138 cx.assert_editor_state(indoc! {"
3139 const a: B = (
3140 c(
3141 d(
3142 ˇ
3143 ˇ)
3144 ˇ)
3145 );
3146 "});
3147
3148 // test when current indent is less than suggested indent,
3149 // we adjust line to match suggested indent and move cursor to it
3150 //
3151 // when some other cursor is at word boundary, it should not move
3152 cx.set_state(indoc! {"
3153 const a: B = (
3154 c(
3155 d(
3156 ˇ
3157 ˇ )
3158 ˇ)
3159 );
3160 "});
3161 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3162 cx.assert_editor_state(indoc! {"
3163 const a: B = (
3164 c(
3165 d(
3166 ˇ
3167 ˇ)
3168 ˇ)
3169 );
3170 "});
3171
3172 // test when current indent is more than suggested indent,
3173 // we just move cursor to current indent instead of suggested indent
3174 //
3175 // when no other cursor is at word boundary, all of them should move
3176 cx.set_state(indoc! {"
3177 const a: B = (
3178 c(
3179 d(
3180 ˇ
3181 ˇ )
3182 ˇ )
3183 );
3184 "});
3185 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3186 cx.assert_editor_state(indoc! {"
3187 const a: B = (
3188 c(
3189 d(
3190 ˇ
3191 ˇ)
3192 ˇ)
3193 );
3194 "});
3195 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3196 cx.assert_editor_state(indoc! {"
3197 const a: B = (
3198 c(
3199 d(
3200 ˇ
3201 ˇ)
3202 ˇ)
3203 );
3204 "});
3205
3206 // test when current indent is more than suggested indent,
3207 // we just move cursor to current indent instead of suggested indent
3208 //
3209 // when some other cursor is at word boundary, it doesn't move
3210 cx.set_state(indoc! {"
3211 const a: B = (
3212 c(
3213 d(
3214 ˇ
3215 ˇ )
3216 ˇ)
3217 );
3218 "});
3219 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3220 cx.assert_editor_state(indoc! {"
3221 const a: B = (
3222 c(
3223 d(
3224 ˇ
3225 ˇ)
3226 ˇ)
3227 );
3228 "});
3229
3230 // handle auto-indent when there are multiple cursors on the same line
3231 cx.set_state(indoc! {"
3232 const a: B = (
3233 c(
3234 ˇ ˇ
3235 ˇ )
3236 );
3237 "});
3238 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3239 cx.assert_editor_state(indoc! {"
3240 const a: B = (
3241 c(
3242 ˇ
3243 ˇ)
3244 );
3245 "});
3246}
3247
3248#[gpui::test]
3249async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3250 init_test(cx, |settings| {
3251 settings.defaults.tab_size = NonZeroU32::new(3)
3252 });
3253
3254 let mut cx = EditorTestContext::new(cx).await;
3255 cx.set_state(indoc! {"
3256 ˇ
3257 \t ˇ
3258 \t ˇ
3259 \t ˇ
3260 \t \t\t \t \t\t \t\t \t \t ˇ
3261 "});
3262
3263 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3264 cx.assert_editor_state(indoc! {"
3265 ˇ
3266 \t ˇ
3267 \t ˇ
3268 \t ˇ
3269 \t \t\t \t \t\t \t\t \t \t ˇ
3270 "});
3271}
3272
3273#[gpui::test]
3274async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3275 init_test(cx, |settings| {
3276 settings.defaults.tab_size = NonZeroU32::new(4)
3277 });
3278
3279 let language = Arc::new(
3280 Language::new(
3281 LanguageConfig::default(),
3282 Some(tree_sitter_rust::LANGUAGE.into()),
3283 )
3284 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3285 .unwrap(),
3286 );
3287
3288 let mut cx = EditorTestContext::new(cx).await;
3289 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3290 cx.set_state(indoc! {"
3291 fn a() {
3292 if b {
3293 \t ˇc
3294 }
3295 }
3296 "});
3297
3298 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3299 cx.assert_editor_state(indoc! {"
3300 fn a() {
3301 if b {
3302 ˇc
3303 }
3304 }
3305 "});
3306}
3307
3308#[gpui::test]
3309async fn test_indent_outdent(cx: &mut TestAppContext) {
3310 init_test(cx, |settings| {
3311 settings.defaults.tab_size = NonZeroU32::new(4);
3312 });
3313
3314 let mut cx = EditorTestContext::new(cx).await;
3315
3316 cx.set_state(indoc! {"
3317 «oneˇ» «twoˇ»
3318 three
3319 four
3320 "});
3321 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3322 cx.assert_editor_state(indoc! {"
3323 «oneˇ» «twoˇ»
3324 three
3325 four
3326 "});
3327
3328 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3329 cx.assert_editor_state(indoc! {"
3330 «oneˇ» «twoˇ»
3331 three
3332 four
3333 "});
3334
3335 // select across line ending
3336 cx.set_state(indoc! {"
3337 one two
3338 t«hree
3339 ˇ» four
3340 "});
3341 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3342 cx.assert_editor_state(indoc! {"
3343 one two
3344 t«hree
3345 ˇ» four
3346 "});
3347
3348 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3349 cx.assert_editor_state(indoc! {"
3350 one two
3351 t«hree
3352 ˇ» four
3353 "});
3354
3355 // Ensure that indenting/outdenting works when the cursor is at column 0.
3356 cx.set_state(indoc! {"
3357 one two
3358 ˇthree
3359 four
3360 "});
3361 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3362 cx.assert_editor_state(indoc! {"
3363 one two
3364 ˇthree
3365 four
3366 "});
3367
3368 cx.set_state(indoc! {"
3369 one two
3370 ˇ three
3371 four
3372 "});
3373 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3374 cx.assert_editor_state(indoc! {"
3375 one two
3376 ˇthree
3377 four
3378 "});
3379}
3380
3381#[gpui::test]
3382async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
3383 init_test(cx, |settings| {
3384 settings.defaults.hard_tabs = Some(true);
3385 });
3386
3387 let mut cx = EditorTestContext::new(cx).await;
3388
3389 // select two ranges on one line
3390 cx.set_state(indoc! {"
3391 «oneˇ» «twoˇ»
3392 three
3393 four
3394 "});
3395 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3396 cx.assert_editor_state(indoc! {"
3397 \t«oneˇ» «twoˇ»
3398 three
3399 four
3400 "});
3401 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3402 cx.assert_editor_state(indoc! {"
3403 \t\t«oneˇ» «twoˇ»
3404 three
3405 four
3406 "});
3407 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3408 cx.assert_editor_state(indoc! {"
3409 \t«oneˇ» «twoˇ»
3410 three
3411 four
3412 "});
3413 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3414 cx.assert_editor_state(indoc! {"
3415 «oneˇ» «twoˇ»
3416 three
3417 four
3418 "});
3419
3420 // select across a line ending
3421 cx.set_state(indoc! {"
3422 one two
3423 t«hree
3424 ˇ»four
3425 "});
3426 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3427 cx.assert_editor_state(indoc! {"
3428 one two
3429 \tt«hree
3430 ˇ»four
3431 "});
3432 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3433 cx.assert_editor_state(indoc! {"
3434 one two
3435 \t\tt«hree
3436 ˇ»four
3437 "});
3438 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3439 cx.assert_editor_state(indoc! {"
3440 one two
3441 \tt«hree
3442 ˇ»four
3443 "});
3444 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3445 cx.assert_editor_state(indoc! {"
3446 one two
3447 t«hree
3448 ˇ»four
3449 "});
3450
3451 // Ensure that indenting/outdenting works when the cursor is at column 0.
3452 cx.set_state(indoc! {"
3453 one two
3454 ˇthree
3455 four
3456 "});
3457 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3458 cx.assert_editor_state(indoc! {"
3459 one two
3460 ˇthree
3461 four
3462 "});
3463 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3464 cx.assert_editor_state(indoc! {"
3465 one two
3466 \tˇthree
3467 four
3468 "});
3469 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3470 cx.assert_editor_state(indoc! {"
3471 one two
3472 ˇthree
3473 four
3474 "});
3475}
3476
3477#[gpui::test]
3478fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
3479 init_test(cx, |settings| {
3480 settings.languages.extend([
3481 (
3482 "TOML".into(),
3483 LanguageSettingsContent {
3484 tab_size: NonZeroU32::new(2),
3485 ..Default::default()
3486 },
3487 ),
3488 (
3489 "Rust".into(),
3490 LanguageSettingsContent {
3491 tab_size: NonZeroU32::new(4),
3492 ..Default::default()
3493 },
3494 ),
3495 ]);
3496 });
3497
3498 let toml_language = Arc::new(Language::new(
3499 LanguageConfig {
3500 name: "TOML".into(),
3501 ..Default::default()
3502 },
3503 None,
3504 ));
3505 let rust_language = Arc::new(Language::new(
3506 LanguageConfig {
3507 name: "Rust".into(),
3508 ..Default::default()
3509 },
3510 None,
3511 ));
3512
3513 let toml_buffer =
3514 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
3515 let rust_buffer =
3516 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
3517 let multibuffer = cx.new(|cx| {
3518 let mut multibuffer = MultiBuffer::new(ReadWrite);
3519 multibuffer.push_excerpts(
3520 toml_buffer.clone(),
3521 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
3522 cx,
3523 );
3524 multibuffer.push_excerpts(
3525 rust_buffer.clone(),
3526 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
3527 cx,
3528 );
3529 multibuffer
3530 });
3531
3532 cx.add_window(|window, cx| {
3533 let mut editor = build_editor(multibuffer, window, cx);
3534
3535 assert_eq!(
3536 editor.text(cx),
3537 indoc! {"
3538 a = 1
3539 b = 2
3540
3541 const c: usize = 3;
3542 "}
3543 );
3544
3545 select_ranges(
3546 &mut editor,
3547 indoc! {"
3548 «aˇ» = 1
3549 b = 2
3550
3551 «const c:ˇ» usize = 3;
3552 "},
3553 window,
3554 cx,
3555 );
3556
3557 editor.tab(&Tab, window, cx);
3558 assert_text_with_selections(
3559 &mut editor,
3560 indoc! {"
3561 «aˇ» = 1
3562 b = 2
3563
3564 «const c:ˇ» usize = 3;
3565 "},
3566 cx,
3567 );
3568 editor.backtab(&Backtab, window, cx);
3569 assert_text_with_selections(
3570 &mut editor,
3571 indoc! {"
3572 «aˇ» = 1
3573 b = 2
3574
3575 «const c:ˇ» usize = 3;
3576 "},
3577 cx,
3578 );
3579
3580 editor
3581 });
3582}
3583
3584#[gpui::test]
3585async fn test_backspace(cx: &mut TestAppContext) {
3586 init_test(cx, |_| {});
3587
3588 let mut cx = EditorTestContext::new(cx).await;
3589
3590 // Basic backspace
3591 cx.set_state(indoc! {"
3592 onˇe two three
3593 fou«rˇ» five six
3594 seven «ˇeight nine
3595 »ten
3596 "});
3597 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3598 cx.assert_editor_state(indoc! {"
3599 oˇe two three
3600 fouˇ five six
3601 seven ˇten
3602 "});
3603
3604 // Test backspace inside and around indents
3605 cx.set_state(indoc! {"
3606 zero
3607 ˇone
3608 ˇtwo
3609 ˇ ˇ ˇ three
3610 ˇ ˇ four
3611 "});
3612 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3613 cx.assert_editor_state(indoc! {"
3614 zero
3615 ˇone
3616 ˇtwo
3617 ˇ threeˇ four
3618 "});
3619}
3620
3621#[gpui::test]
3622async fn test_delete(cx: &mut TestAppContext) {
3623 init_test(cx, |_| {});
3624
3625 let mut cx = EditorTestContext::new(cx).await;
3626 cx.set_state(indoc! {"
3627 onˇe two three
3628 fou«rˇ» five six
3629 seven «ˇeight nine
3630 »ten
3631 "});
3632 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
3633 cx.assert_editor_state(indoc! {"
3634 onˇ two three
3635 fouˇ five six
3636 seven ˇten
3637 "});
3638}
3639
3640#[gpui::test]
3641fn test_delete_line(cx: &mut TestAppContext) {
3642 init_test(cx, |_| {});
3643
3644 let editor = cx.add_window(|window, cx| {
3645 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3646 build_editor(buffer, window, cx)
3647 });
3648 _ = editor.update(cx, |editor, window, cx| {
3649 editor.change_selections(None, window, cx, |s| {
3650 s.select_display_ranges([
3651 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
3652 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3653 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3654 ])
3655 });
3656 editor.delete_line(&DeleteLine, window, cx);
3657 assert_eq!(editor.display_text(cx), "ghi");
3658 assert_eq!(
3659 editor.selections.display_ranges(cx),
3660 vec![
3661 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
3662 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
3663 ]
3664 );
3665 });
3666
3667 let editor = cx.add_window(|window, cx| {
3668 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3669 build_editor(buffer, window, cx)
3670 });
3671 _ = editor.update(cx, |editor, window, cx| {
3672 editor.change_selections(None, window, cx, |s| {
3673 s.select_display_ranges([
3674 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
3675 ])
3676 });
3677 editor.delete_line(&DeleteLine, window, cx);
3678 assert_eq!(editor.display_text(cx), "ghi\n");
3679 assert_eq!(
3680 editor.selections.display_ranges(cx),
3681 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
3682 );
3683 });
3684}
3685
3686#[gpui::test]
3687fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
3688 init_test(cx, |_| {});
3689
3690 cx.add_window(|window, cx| {
3691 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3692 let mut editor = build_editor(buffer.clone(), window, cx);
3693 let buffer = buffer.read(cx).as_singleton().unwrap();
3694
3695 assert_eq!(
3696 editor.selections.ranges::<Point>(cx),
3697 &[Point::new(0, 0)..Point::new(0, 0)]
3698 );
3699
3700 // When on single line, replace newline at end by space
3701 editor.join_lines(&JoinLines, window, cx);
3702 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3703 assert_eq!(
3704 editor.selections.ranges::<Point>(cx),
3705 &[Point::new(0, 3)..Point::new(0, 3)]
3706 );
3707
3708 // When multiple lines are selected, remove newlines that are spanned by the selection
3709 editor.change_selections(None, window, cx, |s| {
3710 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
3711 });
3712 editor.join_lines(&JoinLines, window, cx);
3713 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
3714 assert_eq!(
3715 editor.selections.ranges::<Point>(cx),
3716 &[Point::new(0, 11)..Point::new(0, 11)]
3717 );
3718
3719 // Undo should be transactional
3720 editor.undo(&Undo, window, cx);
3721 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3722 assert_eq!(
3723 editor.selections.ranges::<Point>(cx),
3724 &[Point::new(0, 5)..Point::new(2, 2)]
3725 );
3726
3727 // When joining an empty line don't insert a space
3728 editor.change_selections(None, window, cx, |s| {
3729 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
3730 });
3731 editor.join_lines(&JoinLines, window, cx);
3732 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
3733 assert_eq!(
3734 editor.selections.ranges::<Point>(cx),
3735 [Point::new(2, 3)..Point::new(2, 3)]
3736 );
3737
3738 // We can remove trailing newlines
3739 editor.join_lines(&JoinLines, window, cx);
3740 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3741 assert_eq!(
3742 editor.selections.ranges::<Point>(cx),
3743 [Point::new(2, 3)..Point::new(2, 3)]
3744 );
3745
3746 // We don't blow up on the last line
3747 editor.join_lines(&JoinLines, window, cx);
3748 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3749 assert_eq!(
3750 editor.selections.ranges::<Point>(cx),
3751 [Point::new(2, 3)..Point::new(2, 3)]
3752 );
3753
3754 // reset to test indentation
3755 editor.buffer.update(cx, |buffer, cx| {
3756 buffer.edit(
3757 [
3758 (Point::new(1, 0)..Point::new(1, 2), " "),
3759 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
3760 ],
3761 None,
3762 cx,
3763 )
3764 });
3765
3766 // We remove any leading spaces
3767 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
3768 editor.change_selections(None, window, cx, |s| {
3769 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
3770 });
3771 editor.join_lines(&JoinLines, window, cx);
3772 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
3773
3774 // We don't insert a space for a line containing only spaces
3775 editor.join_lines(&JoinLines, window, cx);
3776 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
3777
3778 // We ignore any leading tabs
3779 editor.join_lines(&JoinLines, window, cx);
3780 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
3781
3782 editor
3783 });
3784}
3785
3786#[gpui::test]
3787fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
3788 init_test(cx, |_| {});
3789
3790 cx.add_window(|window, cx| {
3791 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3792 let mut editor = build_editor(buffer.clone(), window, cx);
3793 let buffer = buffer.read(cx).as_singleton().unwrap();
3794
3795 editor.change_selections(None, window, cx, |s| {
3796 s.select_ranges([
3797 Point::new(0, 2)..Point::new(1, 1),
3798 Point::new(1, 2)..Point::new(1, 2),
3799 Point::new(3, 1)..Point::new(3, 2),
3800 ])
3801 });
3802
3803 editor.join_lines(&JoinLines, window, cx);
3804 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
3805
3806 assert_eq!(
3807 editor.selections.ranges::<Point>(cx),
3808 [
3809 Point::new(0, 7)..Point::new(0, 7),
3810 Point::new(1, 3)..Point::new(1, 3)
3811 ]
3812 );
3813 editor
3814 });
3815}
3816
3817#[gpui::test]
3818async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
3819 init_test(cx, |_| {});
3820
3821 let mut cx = EditorTestContext::new(cx).await;
3822
3823 let diff_base = r#"
3824 Line 0
3825 Line 1
3826 Line 2
3827 Line 3
3828 "#
3829 .unindent();
3830
3831 cx.set_state(
3832 &r#"
3833 ˇLine 0
3834 Line 1
3835 Line 2
3836 Line 3
3837 "#
3838 .unindent(),
3839 );
3840
3841 cx.set_head_text(&diff_base);
3842 executor.run_until_parked();
3843
3844 // Join lines
3845 cx.update_editor(|editor, window, cx| {
3846 editor.join_lines(&JoinLines, window, cx);
3847 });
3848 executor.run_until_parked();
3849
3850 cx.assert_editor_state(
3851 &r#"
3852 Line 0ˇ Line 1
3853 Line 2
3854 Line 3
3855 "#
3856 .unindent(),
3857 );
3858 // Join again
3859 cx.update_editor(|editor, window, cx| {
3860 editor.join_lines(&JoinLines, window, cx);
3861 });
3862 executor.run_until_parked();
3863
3864 cx.assert_editor_state(
3865 &r#"
3866 Line 0 Line 1ˇ Line 2
3867 Line 3
3868 "#
3869 .unindent(),
3870 );
3871}
3872
3873#[gpui::test]
3874async fn test_custom_newlines_cause_no_false_positive_diffs(
3875 executor: BackgroundExecutor,
3876 cx: &mut TestAppContext,
3877) {
3878 init_test(cx, |_| {});
3879 let mut cx = EditorTestContext::new(cx).await;
3880 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
3881 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
3882 executor.run_until_parked();
3883
3884 cx.update_editor(|editor, window, cx| {
3885 let snapshot = editor.snapshot(window, cx);
3886 assert_eq!(
3887 snapshot
3888 .buffer_snapshot
3889 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
3890 .collect::<Vec<_>>(),
3891 Vec::new(),
3892 "Should not have any diffs for files with custom newlines"
3893 );
3894 });
3895}
3896
3897#[gpui::test]
3898async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) {
3899 init_test(cx, |_| {});
3900
3901 let mut cx = EditorTestContext::new(cx).await;
3902
3903 // Test sort_lines_case_insensitive()
3904 cx.set_state(indoc! {"
3905 «z
3906 y
3907 x
3908 Z
3909 Y
3910 Xˇ»
3911 "});
3912 cx.update_editor(|e, window, cx| {
3913 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
3914 });
3915 cx.assert_editor_state(indoc! {"
3916 «x
3917 X
3918 y
3919 Y
3920 z
3921 Zˇ»
3922 "});
3923
3924 // Test reverse_lines()
3925 cx.set_state(indoc! {"
3926 «5
3927 4
3928 3
3929 2
3930 1ˇ»
3931 "});
3932 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
3933 cx.assert_editor_state(indoc! {"
3934 «1
3935 2
3936 3
3937 4
3938 5ˇ»
3939 "});
3940
3941 // Skip testing shuffle_line()
3942
3943 // From here on out, test more complex cases of manipulate_lines() with a single driver method: sort_lines_case_sensitive()
3944 // Since all methods calling manipulate_lines() are doing the exact same general thing (reordering lines)
3945
3946 // Don't manipulate when cursor is on single line, but expand the selection
3947 cx.set_state(indoc! {"
3948 ddˇdd
3949 ccc
3950 bb
3951 a
3952 "});
3953 cx.update_editor(|e, window, cx| {
3954 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3955 });
3956 cx.assert_editor_state(indoc! {"
3957 «ddddˇ»
3958 ccc
3959 bb
3960 a
3961 "});
3962
3963 // Basic manipulate case
3964 // Start selection moves to column 0
3965 // End of selection shrinks to fit shorter line
3966 cx.set_state(indoc! {"
3967 dd«d
3968 ccc
3969 bb
3970 aaaaaˇ»
3971 "});
3972 cx.update_editor(|e, window, cx| {
3973 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3974 });
3975 cx.assert_editor_state(indoc! {"
3976 «aaaaa
3977 bb
3978 ccc
3979 dddˇ»
3980 "});
3981
3982 // Manipulate case with newlines
3983 cx.set_state(indoc! {"
3984 dd«d
3985 ccc
3986
3987 bb
3988 aaaaa
3989
3990 ˇ»
3991 "});
3992 cx.update_editor(|e, window, cx| {
3993 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3994 });
3995 cx.assert_editor_state(indoc! {"
3996 «
3997
3998 aaaaa
3999 bb
4000 ccc
4001 dddˇ»
4002
4003 "});
4004
4005 // Adding new line
4006 cx.set_state(indoc! {"
4007 aa«a
4008 bbˇ»b
4009 "});
4010 cx.update_editor(|e, window, cx| {
4011 e.manipulate_lines(window, cx, |lines| lines.push("added_line"))
4012 });
4013 cx.assert_editor_state(indoc! {"
4014 «aaa
4015 bbb
4016 added_lineˇ»
4017 "});
4018
4019 // Removing line
4020 cx.set_state(indoc! {"
4021 aa«a
4022 bbbˇ»
4023 "});
4024 cx.update_editor(|e, window, cx| {
4025 e.manipulate_lines(window, cx, |lines| {
4026 lines.pop();
4027 })
4028 });
4029 cx.assert_editor_state(indoc! {"
4030 «aaaˇ»
4031 "});
4032
4033 // Removing all lines
4034 cx.set_state(indoc! {"
4035 aa«a
4036 bbbˇ»
4037 "});
4038 cx.update_editor(|e, window, cx| {
4039 e.manipulate_lines(window, cx, |lines| {
4040 lines.drain(..);
4041 })
4042 });
4043 cx.assert_editor_state(indoc! {"
4044 ˇ
4045 "});
4046}
4047
4048#[gpui::test]
4049async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4050 init_test(cx, |_| {});
4051
4052 let mut cx = EditorTestContext::new(cx).await;
4053
4054 // Consider continuous selection as single selection
4055 cx.set_state(indoc! {"
4056 Aaa«aa
4057 cˇ»c«c
4058 bb
4059 aaaˇ»aa
4060 "});
4061 cx.update_editor(|e, window, cx| {
4062 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4063 });
4064 cx.assert_editor_state(indoc! {"
4065 «Aaaaa
4066 ccc
4067 bb
4068 aaaaaˇ»
4069 "});
4070
4071 cx.set_state(indoc! {"
4072 Aaa«aa
4073 cˇ»c«c
4074 bb
4075 aaaˇ»aa
4076 "});
4077 cx.update_editor(|e, window, cx| {
4078 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4079 });
4080 cx.assert_editor_state(indoc! {"
4081 «Aaaaa
4082 ccc
4083 bbˇ»
4084 "});
4085
4086 // Consider non continuous selection as distinct dedup operations
4087 cx.set_state(indoc! {"
4088 «aaaaa
4089 bb
4090 aaaaa
4091 aaaaaˇ»
4092
4093 aaa«aaˇ»
4094 "});
4095 cx.update_editor(|e, window, cx| {
4096 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4097 });
4098 cx.assert_editor_state(indoc! {"
4099 «aaaaa
4100 bbˇ»
4101
4102 «aaaaaˇ»
4103 "});
4104}
4105
4106#[gpui::test]
4107async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4108 init_test(cx, |_| {});
4109
4110 let mut cx = EditorTestContext::new(cx).await;
4111
4112 cx.set_state(indoc! {"
4113 «Aaa
4114 aAa
4115 Aaaˇ»
4116 "});
4117 cx.update_editor(|e, window, cx| {
4118 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4119 });
4120 cx.assert_editor_state(indoc! {"
4121 «Aaa
4122 aAaˇ»
4123 "});
4124
4125 cx.set_state(indoc! {"
4126 «Aaa
4127 aAa
4128 aaAˇ»
4129 "});
4130 cx.update_editor(|e, window, cx| {
4131 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4132 });
4133 cx.assert_editor_state(indoc! {"
4134 «Aaaˇ»
4135 "});
4136}
4137
4138#[gpui::test]
4139async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) {
4140 init_test(cx, |_| {});
4141
4142 let mut cx = EditorTestContext::new(cx).await;
4143
4144 // Manipulate with multiple selections on a single line
4145 cx.set_state(indoc! {"
4146 dd«dd
4147 cˇ»c«c
4148 bb
4149 aaaˇ»aa
4150 "});
4151 cx.update_editor(|e, window, cx| {
4152 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4153 });
4154 cx.assert_editor_state(indoc! {"
4155 «aaaaa
4156 bb
4157 ccc
4158 ddddˇ»
4159 "});
4160
4161 // Manipulate with multiple disjoin selections
4162 cx.set_state(indoc! {"
4163 5«
4164 4
4165 3
4166 2
4167 1ˇ»
4168
4169 dd«dd
4170 ccc
4171 bb
4172 aaaˇ»aa
4173 "});
4174 cx.update_editor(|e, window, cx| {
4175 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4176 });
4177 cx.assert_editor_state(indoc! {"
4178 «1
4179 2
4180 3
4181 4
4182 5ˇ»
4183
4184 «aaaaa
4185 bb
4186 ccc
4187 ddddˇ»
4188 "});
4189
4190 // Adding lines on each selection
4191 cx.set_state(indoc! {"
4192 2«
4193 1ˇ»
4194
4195 bb«bb
4196 aaaˇ»aa
4197 "});
4198 cx.update_editor(|e, window, cx| {
4199 e.manipulate_lines(window, cx, |lines| lines.push("added line"))
4200 });
4201 cx.assert_editor_state(indoc! {"
4202 «2
4203 1
4204 added lineˇ»
4205
4206 «bbbb
4207 aaaaa
4208 added lineˇ»
4209 "});
4210
4211 // Removing lines on each selection
4212 cx.set_state(indoc! {"
4213 2«
4214 1ˇ»
4215
4216 bb«bb
4217 aaaˇ»aa
4218 "});
4219 cx.update_editor(|e, window, cx| {
4220 e.manipulate_lines(window, cx, |lines| {
4221 lines.pop();
4222 })
4223 });
4224 cx.assert_editor_state(indoc! {"
4225 «2ˇ»
4226
4227 «bbbbˇ»
4228 "});
4229}
4230
4231#[gpui::test]
4232async fn test_toggle_case(cx: &mut TestAppContext) {
4233 init_test(cx, |_| {});
4234
4235 let mut cx = EditorTestContext::new(cx).await;
4236
4237 // If all lower case -> upper case
4238 cx.set_state(indoc! {"
4239 «hello worldˇ»
4240 "});
4241 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4242 cx.assert_editor_state(indoc! {"
4243 «HELLO WORLDˇ»
4244 "});
4245
4246 // If all upper case -> lower case
4247 cx.set_state(indoc! {"
4248 «HELLO WORLDˇ»
4249 "});
4250 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4251 cx.assert_editor_state(indoc! {"
4252 «hello worldˇ»
4253 "});
4254
4255 // If any upper case characters are identified -> lower case
4256 // This matches JetBrains IDEs
4257 cx.set_state(indoc! {"
4258 «hEllo worldˇ»
4259 "});
4260 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4261 cx.assert_editor_state(indoc! {"
4262 «hello worldˇ»
4263 "});
4264}
4265
4266#[gpui::test]
4267async fn test_manipulate_text(cx: &mut TestAppContext) {
4268 init_test(cx, |_| {});
4269
4270 let mut cx = EditorTestContext::new(cx).await;
4271
4272 // Test convert_to_upper_case()
4273 cx.set_state(indoc! {"
4274 «hello worldˇ»
4275 "});
4276 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4277 cx.assert_editor_state(indoc! {"
4278 «HELLO WORLDˇ»
4279 "});
4280
4281 // Test convert_to_lower_case()
4282 cx.set_state(indoc! {"
4283 «HELLO WORLDˇ»
4284 "});
4285 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
4286 cx.assert_editor_state(indoc! {"
4287 «hello worldˇ»
4288 "});
4289
4290 // Test multiple line, single selection case
4291 cx.set_state(indoc! {"
4292 «The quick brown
4293 fox jumps over
4294 the lazy dogˇ»
4295 "});
4296 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
4297 cx.assert_editor_state(indoc! {"
4298 «The Quick Brown
4299 Fox Jumps Over
4300 The Lazy Dogˇ»
4301 "});
4302
4303 // Test multiple line, single selection case
4304 cx.set_state(indoc! {"
4305 «The quick brown
4306 fox jumps over
4307 the lazy dogˇ»
4308 "});
4309 cx.update_editor(|e, window, cx| {
4310 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
4311 });
4312 cx.assert_editor_state(indoc! {"
4313 «TheQuickBrown
4314 FoxJumpsOver
4315 TheLazyDogˇ»
4316 "});
4317
4318 // From here on out, test more complex cases of manipulate_text()
4319
4320 // Test no selection case - should affect words cursors are in
4321 // Cursor at beginning, middle, and end of word
4322 cx.set_state(indoc! {"
4323 ˇhello big beauˇtiful worldˇ
4324 "});
4325 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4326 cx.assert_editor_state(indoc! {"
4327 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
4328 "});
4329
4330 // Test multiple selections on a single line and across multiple lines
4331 cx.set_state(indoc! {"
4332 «Theˇ» quick «brown
4333 foxˇ» jumps «overˇ»
4334 the «lazyˇ» dog
4335 "});
4336 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4337 cx.assert_editor_state(indoc! {"
4338 «THEˇ» quick «BROWN
4339 FOXˇ» jumps «OVERˇ»
4340 the «LAZYˇ» dog
4341 "});
4342
4343 // Test case where text length grows
4344 cx.set_state(indoc! {"
4345 «tschüߡ»
4346 "});
4347 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4348 cx.assert_editor_state(indoc! {"
4349 «TSCHÜSSˇ»
4350 "});
4351
4352 // Test to make sure we don't crash when text shrinks
4353 cx.set_state(indoc! {"
4354 aaa_bbbˇ
4355 "});
4356 cx.update_editor(|e, window, cx| {
4357 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4358 });
4359 cx.assert_editor_state(indoc! {"
4360 «aaaBbbˇ»
4361 "});
4362
4363 // Test to make sure we all aware of the fact that each word can grow and shrink
4364 // Final selections should be aware of this fact
4365 cx.set_state(indoc! {"
4366 aaa_bˇbb bbˇb_ccc ˇccc_ddd
4367 "});
4368 cx.update_editor(|e, window, cx| {
4369 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4370 });
4371 cx.assert_editor_state(indoc! {"
4372 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
4373 "});
4374
4375 cx.set_state(indoc! {"
4376 «hElLo, WoRld!ˇ»
4377 "});
4378 cx.update_editor(|e, window, cx| {
4379 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
4380 });
4381 cx.assert_editor_state(indoc! {"
4382 «HeLlO, wOrLD!ˇ»
4383 "});
4384}
4385
4386#[gpui::test]
4387fn test_duplicate_line(cx: &mut TestAppContext) {
4388 init_test(cx, |_| {});
4389
4390 let editor = cx.add_window(|window, cx| {
4391 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4392 build_editor(buffer, window, cx)
4393 });
4394 _ = editor.update(cx, |editor, window, cx| {
4395 editor.change_selections(None, window, cx, |s| {
4396 s.select_display_ranges([
4397 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4398 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4399 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4400 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4401 ])
4402 });
4403 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4404 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4405 assert_eq!(
4406 editor.selections.display_ranges(cx),
4407 vec![
4408 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4409 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
4410 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4411 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4412 ]
4413 );
4414 });
4415
4416 let editor = cx.add_window(|window, cx| {
4417 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4418 build_editor(buffer, window, cx)
4419 });
4420 _ = editor.update(cx, |editor, window, cx| {
4421 editor.change_selections(None, window, cx, |s| {
4422 s.select_display_ranges([
4423 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4424 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4425 ])
4426 });
4427 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4428 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4429 assert_eq!(
4430 editor.selections.display_ranges(cx),
4431 vec![
4432 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
4433 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
4434 ]
4435 );
4436 });
4437
4438 // With `move_upwards` the selections stay in place, except for
4439 // the lines inserted above them
4440 let editor = cx.add_window(|window, cx| {
4441 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4442 build_editor(buffer, window, cx)
4443 });
4444 _ = editor.update(cx, |editor, window, cx| {
4445 editor.change_selections(None, window, cx, |s| {
4446 s.select_display_ranges([
4447 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4448 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4449 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4450 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4451 ])
4452 });
4453 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4454 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4455 assert_eq!(
4456 editor.selections.display_ranges(cx),
4457 vec![
4458 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4459 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4460 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
4461 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4462 ]
4463 );
4464 });
4465
4466 let editor = cx.add_window(|window, cx| {
4467 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4468 build_editor(buffer, window, cx)
4469 });
4470 _ = editor.update(cx, |editor, window, cx| {
4471 editor.change_selections(None, window, cx, |s| {
4472 s.select_display_ranges([
4473 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4474 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4475 ])
4476 });
4477 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4478 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4479 assert_eq!(
4480 editor.selections.display_ranges(cx),
4481 vec![
4482 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4483 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4484 ]
4485 );
4486 });
4487
4488 let editor = cx.add_window(|window, cx| {
4489 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4490 build_editor(buffer, window, cx)
4491 });
4492 _ = editor.update(cx, |editor, window, cx| {
4493 editor.change_selections(None, window, cx, |s| {
4494 s.select_display_ranges([
4495 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4496 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4497 ])
4498 });
4499 editor.duplicate_selection(&DuplicateSelection, window, cx);
4500 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
4501 assert_eq!(
4502 editor.selections.display_ranges(cx),
4503 vec![
4504 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4505 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
4506 ]
4507 );
4508 });
4509}
4510
4511#[gpui::test]
4512fn test_move_line_up_down(cx: &mut TestAppContext) {
4513 init_test(cx, |_| {});
4514
4515 let editor = cx.add_window(|window, cx| {
4516 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4517 build_editor(buffer, window, cx)
4518 });
4519 _ = editor.update(cx, |editor, window, cx| {
4520 editor.fold_creases(
4521 vec![
4522 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
4523 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
4524 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
4525 ],
4526 true,
4527 window,
4528 cx,
4529 );
4530 editor.change_selections(None, window, cx, |s| {
4531 s.select_display_ranges([
4532 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4533 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4534 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4535 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
4536 ])
4537 });
4538 assert_eq!(
4539 editor.display_text(cx),
4540 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
4541 );
4542
4543 editor.move_line_up(&MoveLineUp, window, cx);
4544 assert_eq!(
4545 editor.display_text(cx),
4546 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
4547 );
4548 assert_eq!(
4549 editor.selections.display_ranges(cx),
4550 vec![
4551 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4552 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4553 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4554 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4555 ]
4556 );
4557 });
4558
4559 _ = editor.update(cx, |editor, window, cx| {
4560 editor.move_line_down(&MoveLineDown, window, cx);
4561 assert_eq!(
4562 editor.display_text(cx),
4563 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
4564 );
4565 assert_eq!(
4566 editor.selections.display_ranges(cx),
4567 vec![
4568 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4569 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4570 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4571 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4572 ]
4573 );
4574 });
4575
4576 _ = editor.update(cx, |editor, window, cx| {
4577 editor.move_line_down(&MoveLineDown, window, cx);
4578 assert_eq!(
4579 editor.display_text(cx),
4580 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
4581 );
4582 assert_eq!(
4583 editor.selections.display_ranges(cx),
4584 vec![
4585 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4586 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4587 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4588 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4589 ]
4590 );
4591 });
4592
4593 _ = editor.update(cx, |editor, window, cx| {
4594 editor.move_line_up(&MoveLineUp, window, cx);
4595 assert_eq!(
4596 editor.display_text(cx),
4597 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
4598 );
4599 assert_eq!(
4600 editor.selections.display_ranges(cx),
4601 vec![
4602 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4603 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4604 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4605 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4606 ]
4607 );
4608 });
4609}
4610
4611#[gpui::test]
4612fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
4613 init_test(cx, |_| {});
4614
4615 let editor = cx.add_window(|window, cx| {
4616 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4617 build_editor(buffer, window, cx)
4618 });
4619 _ = editor.update(cx, |editor, window, cx| {
4620 let snapshot = editor.buffer.read(cx).snapshot(cx);
4621 editor.insert_blocks(
4622 [BlockProperties {
4623 style: BlockStyle::Fixed,
4624 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
4625 height: Some(1),
4626 render: Arc::new(|_| div().into_any()),
4627 priority: 0,
4628 render_in_minimap: true,
4629 }],
4630 Some(Autoscroll::fit()),
4631 cx,
4632 );
4633 editor.change_selections(None, window, cx, |s| {
4634 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
4635 });
4636 editor.move_line_down(&MoveLineDown, window, cx);
4637 });
4638}
4639
4640#[gpui::test]
4641async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
4642 init_test(cx, |_| {});
4643
4644 let mut cx = EditorTestContext::new(cx).await;
4645 cx.set_state(
4646 &"
4647 ˇzero
4648 one
4649 two
4650 three
4651 four
4652 five
4653 "
4654 .unindent(),
4655 );
4656
4657 // Create a four-line block that replaces three lines of text.
4658 cx.update_editor(|editor, window, cx| {
4659 let snapshot = editor.snapshot(window, cx);
4660 let snapshot = &snapshot.buffer_snapshot;
4661 let placement = BlockPlacement::Replace(
4662 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
4663 );
4664 editor.insert_blocks(
4665 [BlockProperties {
4666 placement,
4667 height: Some(4),
4668 style: BlockStyle::Sticky,
4669 render: Arc::new(|_| gpui::div().into_any_element()),
4670 priority: 0,
4671 render_in_minimap: true,
4672 }],
4673 None,
4674 cx,
4675 );
4676 });
4677
4678 // Move down so that the cursor touches the block.
4679 cx.update_editor(|editor, window, cx| {
4680 editor.move_down(&Default::default(), window, cx);
4681 });
4682 cx.assert_editor_state(
4683 &"
4684 zero
4685 «one
4686 two
4687 threeˇ»
4688 four
4689 five
4690 "
4691 .unindent(),
4692 );
4693
4694 // Move down past the block.
4695 cx.update_editor(|editor, window, cx| {
4696 editor.move_down(&Default::default(), window, cx);
4697 });
4698 cx.assert_editor_state(
4699 &"
4700 zero
4701 one
4702 two
4703 three
4704 ˇfour
4705 five
4706 "
4707 .unindent(),
4708 );
4709}
4710
4711#[gpui::test]
4712fn test_transpose(cx: &mut TestAppContext) {
4713 init_test(cx, |_| {});
4714
4715 _ = cx.add_window(|window, cx| {
4716 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
4717 editor.set_style(EditorStyle::default(), window, cx);
4718 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
4719 editor.transpose(&Default::default(), window, cx);
4720 assert_eq!(editor.text(cx), "bac");
4721 assert_eq!(editor.selections.ranges(cx), [2..2]);
4722
4723 editor.transpose(&Default::default(), window, cx);
4724 assert_eq!(editor.text(cx), "bca");
4725 assert_eq!(editor.selections.ranges(cx), [3..3]);
4726
4727 editor.transpose(&Default::default(), window, cx);
4728 assert_eq!(editor.text(cx), "bac");
4729 assert_eq!(editor.selections.ranges(cx), [3..3]);
4730
4731 editor
4732 });
4733
4734 _ = cx.add_window(|window, cx| {
4735 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4736 editor.set_style(EditorStyle::default(), window, cx);
4737 editor.change_selections(None, window, cx, |s| s.select_ranges([3..3]));
4738 editor.transpose(&Default::default(), window, cx);
4739 assert_eq!(editor.text(cx), "acb\nde");
4740 assert_eq!(editor.selections.ranges(cx), [3..3]);
4741
4742 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4743 editor.transpose(&Default::default(), window, cx);
4744 assert_eq!(editor.text(cx), "acbd\ne");
4745 assert_eq!(editor.selections.ranges(cx), [5..5]);
4746
4747 editor.transpose(&Default::default(), window, cx);
4748 assert_eq!(editor.text(cx), "acbde\n");
4749 assert_eq!(editor.selections.ranges(cx), [6..6]);
4750
4751 editor.transpose(&Default::default(), window, cx);
4752 assert_eq!(editor.text(cx), "acbd\ne");
4753 assert_eq!(editor.selections.ranges(cx), [6..6]);
4754
4755 editor
4756 });
4757
4758 _ = cx.add_window(|window, cx| {
4759 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4760 editor.set_style(EditorStyle::default(), window, cx);
4761 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1, 2..2, 4..4]));
4762 editor.transpose(&Default::default(), window, cx);
4763 assert_eq!(editor.text(cx), "bacd\ne");
4764 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
4765
4766 editor.transpose(&Default::default(), window, cx);
4767 assert_eq!(editor.text(cx), "bcade\n");
4768 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
4769
4770 editor.transpose(&Default::default(), window, cx);
4771 assert_eq!(editor.text(cx), "bcda\ne");
4772 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4773
4774 editor.transpose(&Default::default(), window, cx);
4775 assert_eq!(editor.text(cx), "bcade\n");
4776 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4777
4778 editor.transpose(&Default::default(), window, cx);
4779 assert_eq!(editor.text(cx), "bcaed\n");
4780 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
4781
4782 editor
4783 });
4784
4785 _ = cx.add_window(|window, cx| {
4786 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
4787 editor.set_style(EditorStyle::default(), window, cx);
4788 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4789 editor.transpose(&Default::default(), window, cx);
4790 assert_eq!(editor.text(cx), "🏀🍐✋");
4791 assert_eq!(editor.selections.ranges(cx), [8..8]);
4792
4793 editor.transpose(&Default::default(), window, cx);
4794 assert_eq!(editor.text(cx), "🏀✋🍐");
4795 assert_eq!(editor.selections.ranges(cx), [11..11]);
4796
4797 editor.transpose(&Default::default(), window, cx);
4798 assert_eq!(editor.text(cx), "🏀🍐✋");
4799 assert_eq!(editor.selections.ranges(cx), [11..11]);
4800
4801 editor
4802 });
4803}
4804
4805#[gpui::test]
4806async fn test_rewrap(cx: &mut TestAppContext) {
4807 init_test(cx, |settings| {
4808 settings.languages.extend([
4809 (
4810 "Markdown".into(),
4811 LanguageSettingsContent {
4812 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4813 ..Default::default()
4814 },
4815 ),
4816 (
4817 "Plain Text".into(),
4818 LanguageSettingsContent {
4819 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4820 ..Default::default()
4821 },
4822 ),
4823 ])
4824 });
4825
4826 let mut cx = EditorTestContext::new(cx).await;
4827
4828 let language_with_c_comments = Arc::new(Language::new(
4829 LanguageConfig {
4830 line_comments: vec!["// ".into()],
4831 ..LanguageConfig::default()
4832 },
4833 None,
4834 ));
4835 let language_with_pound_comments = Arc::new(Language::new(
4836 LanguageConfig {
4837 line_comments: vec!["# ".into()],
4838 ..LanguageConfig::default()
4839 },
4840 None,
4841 ));
4842 let markdown_language = Arc::new(Language::new(
4843 LanguageConfig {
4844 name: "Markdown".into(),
4845 ..LanguageConfig::default()
4846 },
4847 None,
4848 ));
4849 let language_with_doc_comments = Arc::new(Language::new(
4850 LanguageConfig {
4851 line_comments: vec!["// ".into(), "/// ".into()],
4852 ..LanguageConfig::default()
4853 },
4854 Some(tree_sitter_rust::LANGUAGE.into()),
4855 ));
4856
4857 let plaintext_language = Arc::new(Language::new(
4858 LanguageConfig {
4859 name: "Plain Text".into(),
4860 ..LanguageConfig::default()
4861 },
4862 None,
4863 ));
4864
4865 assert_rewrap(
4866 indoc! {"
4867 // ˇ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.
4868 "},
4869 indoc! {"
4870 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4871 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4872 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4873 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4874 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4875 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4876 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4877 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4878 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4879 // porttitor id. Aliquam id accumsan eros.
4880 "},
4881 language_with_c_comments.clone(),
4882 &mut cx,
4883 );
4884
4885 // Test that rewrapping works inside of a selection
4886 assert_rewrap(
4887 indoc! {"
4888 «// 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.ˇ»
4889 "},
4890 indoc! {"
4891 «// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4892 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4893 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4894 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4895 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4896 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4897 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4898 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4899 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4900 // porttitor id. Aliquam id accumsan eros.ˇ»
4901 "},
4902 language_with_c_comments.clone(),
4903 &mut cx,
4904 );
4905
4906 // Test that cursors that expand to the same region are collapsed.
4907 assert_rewrap(
4908 indoc! {"
4909 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4910 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4911 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4912 // ˇ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.
4913 "},
4914 indoc! {"
4915 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4916 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4917 // auctor, eu lacinia sapien scelerisque. ˇVivamus sit amet neque et quam
4918 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4919 // Pellentesque odio lectus, iaculis ac volutpat et, ˇblandit quis urna. Sed
4920 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4921 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4922 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4923 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4924 // porttitor id. Aliquam id accumsan eros.
4925 "},
4926 language_with_c_comments.clone(),
4927 &mut cx,
4928 );
4929
4930 // Test that non-contiguous selections are treated separately.
4931 assert_rewrap(
4932 indoc! {"
4933 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4934 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4935 //
4936 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4937 // ˇ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.
4938 "},
4939 indoc! {"
4940 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4941 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4942 // auctor, eu lacinia sapien scelerisque.
4943 //
4944 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas
4945 // tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4946 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec
4947 // molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque
4948 // nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas
4949 // porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id
4950 // vulputate turpis porttitor id. Aliquam id accumsan eros.
4951 "},
4952 language_with_c_comments.clone(),
4953 &mut cx,
4954 );
4955
4956 // Test that different comment prefixes are supported.
4957 assert_rewrap(
4958 indoc! {"
4959 # ˇ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.
4960 "},
4961 indoc! {"
4962 # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4963 # purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4964 # eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4965 # hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4966 # lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit
4967 # amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet
4968 # in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur
4969 # adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis.
4970 # Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id
4971 # accumsan eros.
4972 "},
4973 language_with_pound_comments.clone(),
4974 &mut cx,
4975 );
4976
4977 // Test that rewrapping is ignored outside of comments in most languages.
4978 assert_rewrap(
4979 indoc! {"
4980 /// Adds two numbers.
4981 /// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
4982 fn add(a: u32, b: u32) -> u32 {
4983 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ˇ
4984 }
4985 "},
4986 indoc! {"
4987 /// Adds two numbers. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
4988 /// Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
4989 fn add(a: u32, b: u32) -> u32 {
4990 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ˇ
4991 }
4992 "},
4993 language_with_doc_comments.clone(),
4994 &mut cx,
4995 );
4996
4997 // Test that rewrapping works in Markdown and Plain Text languages.
4998 assert_rewrap(
4999 indoc! {"
5000 # Hello
5001
5002 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.
5003 "},
5004 indoc! {"
5005 # Hello
5006
5007 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
5008 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
5009 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
5010 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
5011 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
5012 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
5013 Integer sit amet scelerisque nisi.
5014 "},
5015 markdown_language,
5016 &mut cx,
5017 );
5018
5019 assert_rewrap(
5020 indoc! {"
5021 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.
5022 "},
5023 indoc! {"
5024 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
5025 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
5026 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
5027 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
5028 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
5029 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
5030 Integer sit amet scelerisque nisi.
5031 "},
5032 plaintext_language,
5033 &mut cx,
5034 );
5035
5036 // Test rewrapping unaligned comments in a selection.
5037 assert_rewrap(
5038 indoc! {"
5039 fn foo() {
5040 if true {
5041 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
5042 // Praesent semper egestas tellus id dignissim.ˇ»
5043 do_something();
5044 } else {
5045 //
5046 }
5047 }
5048 "},
5049 indoc! {"
5050 fn foo() {
5051 if true {
5052 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
5053 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
5054 // egestas tellus id dignissim.ˇ»
5055 do_something();
5056 } else {
5057 //
5058 }
5059 }
5060 "},
5061 language_with_doc_comments.clone(),
5062 &mut cx,
5063 );
5064
5065 assert_rewrap(
5066 indoc! {"
5067 fn foo() {
5068 if true {
5069 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
5070 // Praesent semper egestas tellus id dignissim.»
5071 do_something();
5072 } else {
5073 //
5074 }
5075
5076 }
5077 "},
5078 indoc! {"
5079 fn foo() {
5080 if true {
5081 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
5082 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
5083 // egestas tellus id dignissim.»
5084 do_something();
5085 } else {
5086 //
5087 }
5088
5089 }
5090 "},
5091 language_with_doc_comments.clone(),
5092 &mut cx,
5093 );
5094
5095 #[track_caller]
5096 fn assert_rewrap(
5097 unwrapped_text: &str,
5098 wrapped_text: &str,
5099 language: Arc<Language>,
5100 cx: &mut EditorTestContext,
5101 ) {
5102 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
5103 cx.set_state(unwrapped_text);
5104 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
5105 cx.assert_editor_state(wrapped_text);
5106 }
5107}
5108
5109#[gpui::test]
5110async fn test_hard_wrap(cx: &mut TestAppContext) {
5111 init_test(cx, |_| {});
5112 let mut cx = EditorTestContext::new(cx).await;
5113
5114 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
5115 cx.update_editor(|editor, _, cx| {
5116 editor.set_hard_wrap(Some(14), cx);
5117 });
5118
5119 cx.set_state(indoc!(
5120 "
5121 one two three ˇ
5122 "
5123 ));
5124 cx.simulate_input("four");
5125 cx.run_until_parked();
5126
5127 cx.assert_editor_state(indoc!(
5128 "
5129 one two three
5130 fourˇ
5131 "
5132 ));
5133
5134 cx.update_editor(|editor, window, cx| {
5135 editor.newline(&Default::default(), window, cx);
5136 });
5137 cx.run_until_parked();
5138 cx.assert_editor_state(indoc!(
5139 "
5140 one two three
5141 four
5142 ˇ
5143 "
5144 ));
5145
5146 cx.simulate_input("five");
5147 cx.run_until_parked();
5148 cx.assert_editor_state(indoc!(
5149 "
5150 one two three
5151 four
5152 fiveˇ
5153 "
5154 ));
5155
5156 cx.update_editor(|editor, window, cx| {
5157 editor.newline(&Default::default(), window, cx);
5158 });
5159 cx.run_until_parked();
5160 cx.simulate_input("# ");
5161 cx.run_until_parked();
5162 cx.assert_editor_state(indoc!(
5163 "
5164 one two three
5165 four
5166 five
5167 # ˇ
5168 "
5169 ));
5170
5171 cx.update_editor(|editor, window, cx| {
5172 editor.newline(&Default::default(), window, cx);
5173 });
5174 cx.run_until_parked();
5175 cx.assert_editor_state(indoc!(
5176 "
5177 one two three
5178 four
5179 five
5180 #\x20
5181 #ˇ
5182 "
5183 ));
5184
5185 cx.simulate_input(" 6");
5186 cx.run_until_parked();
5187 cx.assert_editor_state(indoc!(
5188 "
5189 one two three
5190 four
5191 five
5192 #
5193 # 6ˇ
5194 "
5195 ));
5196}
5197
5198#[gpui::test]
5199async fn test_clipboard(cx: &mut TestAppContext) {
5200 init_test(cx, |_| {});
5201
5202 let mut cx = EditorTestContext::new(cx).await;
5203
5204 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
5205 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5206 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
5207
5208 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
5209 cx.set_state("two ˇfour ˇsix ˇ");
5210 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5211 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
5212
5213 // Paste again but with only two cursors. Since the number of cursors doesn't
5214 // match the number of slices in the clipboard, the entire clipboard text
5215 // is pasted at each cursor.
5216 cx.set_state("ˇtwo one✅ four three six five ˇ");
5217 cx.update_editor(|e, window, cx| {
5218 e.handle_input("( ", window, cx);
5219 e.paste(&Paste, window, cx);
5220 e.handle_input(") ", window, cx);
5221 });
5222 cx.assert_editor_state(
5223 &([
5224 "( one✅ ",
5225 "three ",
5226 "five ) ˇtwo one✅ four three six five ( one✅ ",
5227 "three ",
5228 "five ) ˇ",
5229 ]
5230 .join("\n")),
5231 );
5232
5233 // Cut with three selections, one of which is full-line.
5234 cx.set_state(indoc! {"
5235 1«2ˇ»3
5236 4ˇ567
5237 «8ˇ»9"});
5238 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5239 cx.assert_editor_state(indoc! {"
5240 1ˇ3
5241 ˇ9"});
5242
5243 // Paste with three selections, noticing how the copied selection that was full-line
5244 // gets inserted before the second cursor.
5245 cx.set_state(indoc! {"
5246 1ˇ3
5247 9ˇ
5248 «oˇ»ne"});
5249 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5250 cx.assert_editor_state(indoc! {"
5251 12ˇ3
5252 4567
5253 9ˇ
5254 8ˇne"});
5255
5256 // Copy with a single cursor only, which writes the whole line into the clipboard.
5257 cx.set_state(indoc! {"
5258 The quick brown
5259 fox juˇmps over
5260 the lazy dog"});
5261 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5262 assert_eq!(
5263 cx.read_from_clipboard()
5264 .and_then(|item| item.text().as_deref().map(str::to_string)),
5265 Some("fox jumps over\n".to_string())
5266 );
5267
5268 // Paste with three selections, noticing how the copied full-line selection is inserted
5269 // before the empty selections but replaces the selection that is non-empty.
5270 cx.set_state(indoc! {"
5271 Tˇhe quick brown
5272 «foˇ»x jumps over
5273 tˇhe lazy dog"});
5274 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5275 cx.assert_editor_state(indoc! {"
5276 fox jumps over
5277 Tˇhe quick brown
5278 fox jumps over
5279 ˇx jumps over
5280 fox jumps over
5281 tˇhe lazy dog"});
5282}
5283
5284#[gpui::test]
5285async fn test_copy_trim(cx: &mut TestAppContext) {
5286 init_test(cx, |_| {});
5287
5288 let mut cx = EditorTestContext::new(cx).await;
5289 cx.set_state(
5290 r#" «for selection in selections.iter() {
5291 let mut start = selection.start;
5292 let mut end = selection.end;
5293 let is_entire_line = selection.is_empty();
5294 if is_entire_line {
5295 start = Point::new(start.row, 0);ˇ»
5296 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5297 }
5298 "#,
5299 );
5300 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5301 assert_eq!(
5302 cx.read_from_clipboard()
5303 .and_then(|item| item.text().as_deref().map(str::to_string)),
5304 Some(
5305 "for selection in selections.iter() {
5306 let mut start = selection.start;
5307 let mut end = selection.end;
5308 let is_entire_line = selection.is_empty();
5309 if is_entire_line {
5310 start = Point::new(start.row, 0);"
5311 .to_string()
5312 ),
5313 "Regular copying preserves all indentation selected",
5314 );
5315 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5316 assert_eq!(
5317 cx.read_from_clipboard()
5318 .and_then(|item| item.text().as_deref().map(str::to_string)),
5319 Some(
5320 "for selection in selections.iter() {
5321let mut start = selection.start;
5322let mut end = selection.end;
5323let is_entire_line = selection.is_empty();
5324if is_entire_line {
5325 start = Point::new(start.row, 0);"
5326 .to_string()
5327 ),
5328 "Copying with stripping should strip all leading whitespaces"
5329 );
5330
5331 cx.set_state(
5332 r#" « for selection in selections.iter() {
5333 let mut start = selection.start;
5334 let mut end = selection.end;
5335 let is_entire_line = selection.is_empty();
5336 if is_entire_line {
5337 start = Point::new(start.row, 0);ˇ»
5338 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5339 }
5340 "#,
5341 );
5342 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5343 assert_eq!(
5344 cx.read_from_clipboard()
5345 .and_then(|item| item.text().as_deref().map(str::to_string)),
5346 Some(
5347 " for selection in selections.iter() {
5348 let mut start = selection.start;
5349 let mut end = selection.end;
5350 let is_entire_line = selection.is_empty();
5351 if is_entire_line {
5352 start = Point::new(start.row, 0);"
5353 .to_string()
5354 ),
5355 "Regular copying preserves all indentation selected",
5356 );
5357 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5358 assert_eq!(
5359 cx.read_from_clipboard()
5360 .and_then(|item| item.text().as_deref().map(str::to_string)),
5361 Some(
5362 "for selection in selections.iter() {
5363let mut start = selection.start;
5364let mut end = selection.end;
5365let is_entire_line = selection.is_empty();
5366if is_entire_line {
5367 start = Point::new(start.row, 0);"
5368 .to_string()
5369 ),
5370 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
5371 );
5372
5373 cx.set_state(
5374 r#" «ˇ for selection in selections.iter() {
5375 let mut start = selection.start;
5376 let mut end = selection.end;
5377 let is_entire_line = selection.is_empty();
5378 if is_entire_line {
5379 start = Point::new(start.row, 0);»
5380 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5381 }
5382 "#,
5383 );
5384 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5385 assert_eq!(
5386 cx.read_from_clipboard()
5387 .and_then(|item| item.text().as_deref().map(str::to_string)),
5388 Some(
5389 " for selection in selections.iter() {
5390 let mut start = selection.start;
5391 let mut end = selection.end;
5392 let is_entire_line = selection.is_empty();
5393 if is_entire_line {
5394 start = Point::new(start.row, 0);"
5395 .to_string()
5396 ),
5397 "Regular copying for reverse selection works the same",
5398 );
5399 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5400 assert_eq!(
5401 cx.read_from_clipboard()
5402 .and_then(|item| item.text().as_deref().map(str::to_string)),
5403 Some(
5404 "for selection in selections.iter() {
5405let mut start = selection.start;
5406let mut end = selection.end;
5407let is_entire_line = selection.is_empty();
5408if is_entire_line {
5409 start = Point::new(start.row, 0);"
5410 .to_string()
5411 ),
5412 "Copying with stripping for reverse selection works the same"
5413 );
5414
5415 cx.set_state(
5416 r#" for selection «in selections.iter() {
5417 let mut start = selection.start;
5418 let mut end = selection.end;
5419 let is_entire_line = selection.is_empty();
5420 if is_entire_line {
5421 start = Point::new(start.row, 0);ˇ»
5422 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5423 }
5424 "#,
5425 );
5426 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5427 assert_eq!(
5428 cx.read_from_clipboard()
5429 .and_then(|item| item.text().as_deref().map(str::to_string)),
5430 Some(
5431 "in selections.iter() {
5432 let mut start = selection.start;
5433 let mut end = selection.end;
5434 let is_entire_line = selection.is_empty();
5435 if is_entire_line {
5436 start = Point::new(start.row, 0);"
5437 .to_string()
5438 ),
5439 "When selecting past the indent, the copying works as usual",
5440 );
5441 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5442 assert_eq!(
5443 cx.read_from_clipboard()
5444 .and_then(|item| item.text().as_deref().map(str::to_string)),
5445 Some(
5446 "in selections.iter() {
5447 let mut start = selection.start;
5448 let mut end = selection.end;
5449 let is_entire_line = selection.is_empty();
5450 if is_entire_line {
5451 start = Point::new(start.row, 0);"
5452 .to_string()
5453 ),
5454 "When selecting past the indent, nothing is trimmed"
5455 );
5456
5457 cx.set_state(
5458 r#" «for selection in selections.iter() {
5459 let mut start = selection.start;
5460
5461 let mut end = selection.end;
5462 let is_entire_line = selection.is_empty();
5463 if is_entire_line {
5464 start = Point::new(start.row, 0);
5465ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
5466 }
5467 "#,
5468 );
5469 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5470 assert_eq!(
5471 cx.read_from_clipboard()
5472 .and_then(|item| item.text().as_deref().map(str::to_string)),
5473 Some(
5474 "for selection in selections.iter() {
5475let mut start = selection.start;
5476
5477let mut end = selection.end;
5478let is_entire_line = selection.is_empty();
5479if is_entire_line {
5480 start = Point::new(start.row, 0);
5481"
5482 .to_string()
5483 ),
5484 "Copying with stripping should ignore empty lines"
5485 );
5486}
5487
5488#[gpui::test]
5489async fn test_paste_multiline(cx: &mut TestAppContext) {
5490 init_test(cx, |_| {});
5491
5492 let mut cx = EditorTestContext::new(cx).await;
5493 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5494
5495 // Cut an indented block, without the leading whitespace.
5496 cx.set_state(indoc! {"
5497 const a: B = (
5498 c(),
5499 «d(
5500 e,
5501 f
5502 )ˇ»
5503 );
5504 "});
5505 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5506 cx.assert_editor_state(indoc! {"
5507 const a: B = (
5508 c(),
5509 ˇ
5510 );
5511 "});
5512
5513 // Paste it at the same position.
5514 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5515 cx.assert_editor_state(indoc! {"
5516 const a: B = (
5517 c(),
5518 d(
5519 e,
5520 f
5521 )ˇ
5522 );
5523 "});
5524
5525 // Paste it at a line with a lower indent level.
5526 cx.set_state(indoc! {"
5527 ˇ
5528 const a: B = (
5529 c(),
5530 );
5531 "});
5532 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5533 cx.assert_editor_state(indoc! {"
5534 d(
5535 e,
5536 f
5537 )ˇ
5538 const a: B = (
5539 c(),
5540 );
5541 "});
5542
5543 // Cut an indented block, with the leading whitespace.
5544 cx.set_state(indoc! {"
5545 const a: B = (
5546 c(),
5547 « d(
5548 e,
5549 f
5550 )
5551 ˇ»);
5552 "});
5553 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5554 cx.assert_editor_state(indoc! {"
5555 const a: B = (
5556 c(),
5557 ˇ);
5558 "});
5559
5560 // Paste it at the same position.
5561 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5562 cx.assert_editor_state(indoc! {"
5563 const a: B = (
5564 c(),
5565 d(
5566 e,
5567 f
5568 )
5569 ˇ);
5570 "});
5571
5572 // Paste it at a line with a higher indent level.
5573 cx.set_state(indoc! {"
5574 const a: B = (
5575 c(),
5576 d(
5577 e,
5578 fˇ
5579 )
5580 );
5581 "});
5582 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5583 cx.assert_editor_state(indoc! {"
5584 const a: B = (
5585 c(),
5586 d(
5587 e,
5588 f d(
5589 e,
5590 f
5591 )
5592 ˇ
5593 )
5594 );
5595 "});
5596
5597 // Copy an indented block, starting mid-line
5598 cx.set_state(indoc! {"
5599 const a: B = (
5600 c(),
5601 somethin«g(
5602 e,
5603 f
5604 )ˇ»
5605 );
5606 "});
5607 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5608
5609 // Paste it on a line with a lower indent level
5610 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
5611 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5612 cx.assert_editor_state(indoc! {"
5613 const a: B = (
5614 c(),
5615 something(
5616 e,
5617 f
5618 )
5619 );
5620 g(
5621 e,
5622 f
5623 )ˇ"});
5624}
5625
5626#[gpui::test]
5627async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
5628 init_test(cx, |_| {});
5629
5630 cx.write_to_clipboard(ClipboardItem::new_string(
5631 " d(\n e\n );\n".into(),
5632 ));
5633
5634 let mut cx = EditorTestContext::new(cx).await;
5635 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5636
5637 cx.set_state(indoc! {"
5638 fn a() {
5639 b();
5640 if c() {
5641 ˇ
5642 }
5643 }
5644 "});
5645
5646 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5647 cx.assert_editor_state(indoc! {"
5648 fn a() {
5649 b();
5650 if c() {
5651 d(
5652 e
5653 );
5654 ˇ
5655 }
5656 }
5657 "});
5658
5659 cx.set_state(indoc! {"
5660 fn a() {
5661 b();
5662 ˇ
5663 }
5664 "});
5665
5666 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5667 cx.assert_editor_state(indoc! {"
5668 fn a() {
5669 b();
5670 d(
5671 e
5672 );
5673 ˇ
5674 }
5675 "});
5676}
5677
5678#[gpui::test]
5679fn test_select_all(cx: &mut TestAppContext) {
5680 init_test(cx, |_| {});
5681
5682 let editor = cx.add_window(|window, cx| {
5683 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
5684 build_editor(buffer, window, cx)
5685 });
5686 _ = editor.update(cx, |editor, window, cx| {
5687 editor.select_all(&SelectAll, window, cx);
5688 assert_eq!(
5689 editor.selections.display_ranges(cx),
5690 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
5691 );
5692 });
5693}
5694
5695#[gpui::test]
5696fn test_select_line(cx: &mut TestAppContext) {
5697 init_test(cx, |_| {});
5698
5699 let editor = cx.add_window(|window, cx| {
5700 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
5701 build_editor(buffer, window, cx)
5702 });
5703 _ = editor.update(cx, |editor, window, cx| {
5704 editor.change_selections(None, window, cx, |s| {
5705 s.select_display_ranges([
5706 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5707 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5708 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5709 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
5710 ])
5711 });
5712 editor.select_line(&SelectLine, window, cx);
5713 assert_eq!(
5714 editor.selections.display_ranges(cx),
5715 vec![
5716 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
5717 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
5718 ]
5719 );
5720 });
5721
5722 _ = editor.update(cx, |editor, window, cx| {
5723 editor.select_line(&SelectLine, window, cx);
5724 assert_eq!(
5725 editor.selections.display_ranges(cx),
5726 vec![
5727 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
5728 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
5729 ]
5730 );
5731 });
5732
5733 _ = editor.update(cx, |editor, window, cx| {
5734 editor.select_line(&SelectLine, window, cx);
5735 assert_eq!(
5736 editor.selections.display_ranges(cx),
5737 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
5738 );
5739 });
5740}
5741
5742#[gpui::test]
5743async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
5744 init_test(cx, |_| {});
5745 let mut cx = EditorTestContext::new(cx).await;
5746
5747 #[track_caller]
5748 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
5749 cx.set_state(initial_state);
5750 cx.update_editor(|e, window, cx| {
5751 e.split_selection_into_lines(&SplitSelectionIntoLines, window, cx)
5752 });
5753 cx.assert_editor_state(expected_state);
5754 }
5755
5756 // Selection starts and ends at the middle of lines, left-to-right
5757 test(
5758 &mut cx,
5759 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
5760 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5761 );
5762 // Same thing, right-to-left
5763 test(
5764 &mut cx,
5765 "aa\nb«b\ncc\ndd\neˇ»e\nff",
5766 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5767 );
5768
5769 // Whole buffer, left-to-right, last line *doesn't* end with newline
5770 test(
5771 &mut cx,
5772 "«ˇaa\nbb\ncc\ndd\nee\nff»",
5773 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5774 );
5775 // Same thing, right-to-left
5776 test(
5777 &mut cx,
5778 "«aa\nbb\ncc\ndd\nee\nffˇ»",
5779 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5780 );
5781
5782 // Whole buffer, left-to-right, last line ends with newline
5783 test(
5784 &mut cx,
5785 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
5786 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5787 );
5788 // Same thing, right-to-left
5789 test(
5790 &mut cx,
5791 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
5792 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5793 );
5794
5795 // Starts at the end of a line, ends at the start of another
5796 test(
5797 &mut cx,
5798 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
5799 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
5800 );
5801}
5802
5803#[gpui::test]
5804async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
5805 init_test(cx, |_| {});
5806
5807 let editor = cx.add_window(|window, cx| {
5808 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
5809 build_editor(buffer, window, cx)
5810 });
5811
5812 // setup
5813 _ = editor.update(cx, |editor, window, cx| {
5814 editor.fold_creases(
5815 vec![
5816 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5817 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5818 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5819 ],
5820 true,
5821 window,
5822 cx,
5823 );
5824 assert_eq!(
5825 editor.display_text(cx),
5826 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5827 );
5828 });
5829
5830 _ = editor.update(cx, |editor, window, cx| {
5831 editor.change_selections(None, window, cx, |s| {
5832 s.select_display_ranges([
5833 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5834 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5835 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5836 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
5837 ])
5838 });
5839 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5840 assert_eq!(
5841 editor.display_text(cx),
5842 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5843 );
5844 });
5845 EditorTestContext::for_editor(editor, cx)
5846 .await
5847 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
5848
5849 _ = editor.update(cx, |editor, window, cx| {
5850 editor.change_selections(None, window, cx, |s| {
5851 s.select_display_ranges([
5852 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
5853 ])
5854 });
5855 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5856 assert_eq!(
5857 editor.display_text(cx),
5858 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
5859 );
5860 assert_eq!(
5861 editor.selections.display_ranges(cx),
5862 [
5863 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
5864 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
5865 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
5866 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
5867 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
5868 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
5869 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
5870 ]
5871 );
5872 });
5873 EditorTestContext::for_editor(editor, cx)
5874 .await
5875 .assert_editor_state(
5876 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
5877 );
5878}
5879
5880#[gpui::test]
5881async fn test_add_selection_above_below(cx: &mut TestAppContext) {
5882 init_test(cx, |_| {});
5883
5884 let mut cx = EditorTestContext::new(cx).await;
5885
5886 cx.set_state(indoc!(
5887 r#"abc
5888 defˇghi
5889
5890 jk
5891 nlmo
5892 "#
5893 ));
5894
5895 cx.update_editor(|editor, window, cx| {
5896 editor.add_selection_above(&Default::default(), window, cx);
5897 });
5898
5899 cx.assert_editor_state(indoc!(
5900 r#"abcˇ
5901 defˇghi
5902
5903 jk
5904 nlmo
5905 "#
5906 ));
5907
5908 cx.update_editor(|editor, window, cx| {
5909 editor.add_selection_above(&Default::default(), window, cx);
5910 });
5911
5912 cx.assert_editor_state(indoc!(
5913 r#"abcˇ
5914 defˇghi
5915
5916 jk
5917 nlmo
5918 "#
5919 ));
5920
5921 cx.update_editor(|editor, window, cx| {
5922 editor.add_selection_below(&Default::default(), window, cx);
5923 });
5924
5925 cx.assert_editor_state(indoc!(
5926 r#"abc
5927 defˇghi
5928
5929 jk
5930 nlmo
5931 "#
5932 ));
5933
5934 cx.update_editor(|editor, window, cx| {
5935 editor.undo_selection(&Default::default(), window, cx);
5936 });
5937
5938 cx.assert_editor_state(indoc!(
5939 r#"abcˇ
5940 defˇghi
5941
5942 jk
5943 nlmo
5944 "#
5945 ));
5946
5947 cx.update_editor(|editor, window, cx| {
5948 editor.redo_selection(&Default::default(), window, cx);
5949 });
5950
5951 cx.assert_editor_state(indoc!(
5952 r#"abc
5953 defˇghi
5954
5955 jk
5956 nlmo
5957 "#
5958 ));
5959
5960 cx.update_editor(|editor, window, cx| {
5961 editor.add_selection_below(&Default::default(), window, cx);
5962 });
5963
5964 cx.assert_editor_state(indoc!(
5965 r#"abc
5966 defˇghi
5967
5968 jk
5969 nlmˇo
5970 "#
5971 ));
5972
5973 cx.update_editor(|editor, window, cx| {
5974 editor.add_selection_below(&Default::default(), window, cx);
5975 });
5976
5977 cx.assert_editor_state(indoc!(
5978 r#"abc
5979 defˇghi
5980
5981 jk
5982 nlmˇo
5983 "#
5984 ));
5985
5986 // change selections
5987 cx.set_state(indoc!(
5988 r#"abc
5989 def«ˇg»hi
5990
5991 jk
5992 nlmo
5993 "#
5994 ));
5995
5996 cx.update_editor(|editor, window, cx| {
5997 editor.add_selection_below(&Default::default(), window, cx);
5998 });
5999
6000 cx.assert_editor_state(indoc!(
6001 r#"abc
6002 def«ˇg»hi
6003
6004 jk
6005 nlm«ˇo»
6006 "#
6007 ));
6008
6009 cx.update_editor(|editor, window, cx| {
6010 editor.add_selection_below(&Default::default(), window, cx);
6011 });
6012
6013 cx.assert_editor_state(indoc!(
6014 r#"abc
6015 def«ˇg»hi
6016
6017 jk
6018 nlm«ˇo»
6019 "#
6020 ));
6021
6022 cx.update_editor(|editor, window, cx| {
6023 editor.add_selection_above(&Default::default(), window, cx);
6024 });
6025
6026 cx.assert_editor_state(indoc!(
6027 r#"abc
6028 def«ˇg»hi
6029
6030 jk
6031 nlmo
6032 "#
6033 ));
6034
6035 cx.update_editor(|editor, window, cx| {
6036 editor.add_selection_above(&Default::default(), window, cx);
6037 });
6038
6039 cx.assert_editor_state(indoc!(
6040 r#"abc
6041 def«ˇg»hi
6042
6043 jk
6044 nlmo
6045 "#
6046 ));
6047
6048 // Change selections again
6049 cx.set_state(indoc!(
6050 r#"a«bc
6051 defgˇ»hi
6052
6053 jk
6054 nlmo
6055 "#
6056 ));
6057
6058 cx.update_editor(|editor, window, cx| {
6059 editor.add_selection_below(&Default::default(), window, cx);
6060 });
6061
6062 cx.assert_editor_state(indoc!(
6063 r#"a«bcˇ»
6064 d«efgˇ»hi
6065
6066 j«kˇ»
6067 nlmo
6068 "#
6069 ));
6070
6071 cx.update_editor(|editor, window, cx| {
6072 editor.add_selection_below(&Default::default(), window, cx);
6073 });
6074 cx.assert_editor_state(indoc!(
6075 r#"a«bcˇ»
6076 d«efgˇ»hi
6077
6078 j«kˇ»
6079 n«lmoˇ»
6080 "#
6081 ));
6082 cx.update_editor(|editor, window, cx| {
6083 editor.add_selection_above(&Default::default(), window, cx);
6084 });
6085
6086 cx.assert_editor_state(indoc!(
6087 r#"a«bcˇ»
6088 d«efgˇ»hi
6089
6090 j«kˇ»
6091 nlmo
6092 "#
6093 ));
6094
6095 // Change selections again
6096 cx.set_state(indoc!(
6097 r#"abc
6098 d«ˇefghi
6099
6100 jk
6101 nlm»o
6102 "#
6103 ));
6104
6105 cx.update_editor(|editor, window, cx| {
6106 editor.add_selection_above(&Default::default(), window, cx);
6107 });
6108
6109 cx.assert_editor_state(indoc!(
6110 r#"a«ˇbc»
6111 d«ˇef»ghi
6112
6113 j«ˇk»
6114 n«ˇlm»o
6115 "#
6116 ));
6117
6118 cx.update_editor(|editor, window, cx| {
6119 editor.add_selection_below(&Default::default(), window, cx);
6120 });
6121
6122 cx.assert_editor_state(indoc!(
6123 r#"abc
6124 d«ˇef»ghi
6125
6126 j«ˇk»
6127 n«ˇlm»o
6128 "#
6129 ));
6130}
6131
6132#[gpui::test]
6133async fn test_select_next(cx: &mut TestAppContext) {
6134 init_test(cx, |_| {});
6135
6136 let mut cx = EditorTestContext::new(cx).await;
6137 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6138
6139 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6140 .unwrap();
6141 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6142
6143 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6144 .unwrap();
6145 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
6146
6147 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6148 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6149
6150 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6151 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
6152
6153 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6154 .unwrap();
6155 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6156
6157 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6158 .unwrap();
6159 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6160
6161 // Test selection direction should be preserved
6162 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
6163
6164 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6165 .unwrap();
6166 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
6167}
6168
6169#[gpui::test]
6170async fn test_select_all_matches(cx: &mut TestAppContext) {
6171 init_test(cx, |_| {});
6172
6173 let mut cx = EditorTestContext::new(cx).await;
6174
6175 // Test caret-only selections
6176 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6177 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6178 .unwrap();
6179 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6180
6181 // Test left-to-right selections
6182 cx.set_state("abc\n«abcˇ»\nabc");
6183 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6184 .unwrap();
6185 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
6186
6187 // Test right-to-left selections
6188 cx.set_state("abc\n«ˇabc»\nabc");
6189 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6190 .unwrap();
6191 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
6192
6193 // Test selecting whitespace with caret selection
6194 cx.set_state("abc\nˇ abc\nabc");
6195 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6196 .unwrap();
6197 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
6198
6199 // Test selecting whitespace with left-to-right selection
6200 cx.set_state("abc\n«ˇ »abc\nabc");
6201 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6202 .unwrap();
6203 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
6204
6205 // Test no matches with right-to-left selection
6206 cx.set_state("abc\n« ˇ»abc\nabc");
6207 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6208 .unwrap();
6209 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
6210}
6211
6212#[gpui::test]
6213async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
6214 init_test(cx, |_| {});
6215
6216 let mut cx = EditorTestContext::new(cx).await;
6217
6218 let large_body_1 = "\nd".repeat(200);
6219 let large_body_2 = "\ne".repeat(200);
6220
6221 cx.set_state(&format!(
6222 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
6223 ));
6224 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
6225 let scroll_position = editor.scroll_position(cx);
6226 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
6227 scroll_position
6228 });
6229
6230 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6231 .unwrap();
6232 cx.assert_editor_state(&format!(
6233 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
6234 ));
6235 let scroll_position_after_selection =
6236 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
6237 assert_eq!(
6238 initial_scroll_position, scroll_position_after_selection,
6239 "Scroll position should not change after selecting all matches"
6240 );
6241}
6242
6243#[gpui::test]
6244async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
6245 init_test(cx, |_| {});
6246
6247 let mut cx = EditorLspTestContext::new_rust(
6248 lsp::ServerCapabilities {
6249 document_formatting_provider: Some(lsp::OneOf::Left(true)),
6250 ..Default::default()
6251 },
6252 cx,
6253 )
6254 .await;
6255
6256 cx.set_state(indoc! {"
6257 line 1
6258 line 2
6259 linˇe 3
6260 line 4
6261 line 5
6262 "});
6263
6264 // Make an edit
6265 cx.update_editor(|editor, window, cx| {
6266 editor.handle_input("X", window, cx);
6267 });
6268
6269 // Move cursor to a different position
6270 cx.update_editor(|editor, window, cx| {
6271 editor.change_selections(None, window, cx, |s| {
6272 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
6273 });
6274 });
6275
6276 cx.assert_editor_state(indoc! {"
6277 line 1
6278 line 2
6279 linXe 3
6280 line 4
6281 liˇne 5
6282 "});
6283
6284 cx.lsp
6285 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
6286 Ok(Some(vec![lsp::TextEdit::new(
6287 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
6288 "PREFIX ".to_string(),
6289 )]))
6290 });
6291
6292 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
6293 .unwrap()
6294 .await
6295 .unwrap();
6296
6297 cx.assert_editor_state(indoc! {"
6298 PREFIX line 1
6299 line 2
6300 linXe 3
6301 line 4
6302 liˇne 5
6303 "});
6304
6305 // Undo formatting
6306 cx.update_editor(|editor, window, cx| {
6307 editor.undo(&Default::default(), window, cx);
6308 });
6309
6310 // Verify cursor moved back to position after edit
6311 cx.assert_editor_state(indoc! {"
6312 line 1
6313 line 2
6314 linXˇe 3
6315 line 4
6316 line 5
6317 "});
6318}
6319
6320#[gpui::test]
6321async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
6322 init_test(cx, |_| {});
6323
6324 let mut cx = EditorTestContext::new(cx).await;
6325 cx.set_state(
6326 r#"let foo = 2;
6327lˇet foo = 2;
6328let fooˇ = 2;
6329let foo = 2;
6330let foo = ˇ2;"#,
6331 );
6332
6333 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6334 .unwrap();
6335 cx.assert_editor_state(
6336 r#"let foo = 2;
6337«letˇ» foo = 2;
6338let «fooˇ» = 2;
6339let foo = 2;
6340let foo = «2ˇ»;"#,
6341 );
6342
6343 // noop for multiple selections with different contents
6344 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6345 .unwrap();
6346 cx.assert_editor_state(
6347 r#"let foo = 2;
6348«letˇ» foo = 2;
6349let «fooˇ» = 2;
6350let foo = 2;
6351let foo = «2ˇ»;"#,
6352 );
6353
6354 // Test last selection direction should be preserved
6355 cx.set_state(
6356 r#"let foo = 2;
6357let foo = 2;
6358let «fooˇ» = 2;
6359let «ˇfoo» = 2;
6360let foo = 2;"#,
6361 );
6362
6363 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6364 .unwrap();
6365 cx.assert_editor_state(
6366 r#"let foo = 2;
6367let foo = 2;
6368let «fooˇ» = 2;
6369let «ˇfoo» = 2;
6370let «ˇfoo» = 2;"#,
6371 );
6372}
6373
6374#[gpui::test]
6375async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
6376 init_test(cx, |_| {});
6377
6378 let mut cx =
6379 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
6380
6381 cx.assert_editor_state(indoc! {"
6382 ˇbbb
6383 ccc
6384
6385 bbb
6386 ccc
6387 "});
6388 cx.dispatch_action(SelectPrevious::default());
6389 cx.assert_editor_state(indoc! {"
6390 «bbbˇ»
6391 ccc
6392
6393 bbb
6394 ccc
6395 "});
6396 cx.dispatch_action(SelectPrevious::default());
6397 cx.assert_editor_state(indoc! {"
6398 «bbbˇ»
6399 ccc
6400
6401 «bbbˇ»
6402 ccc
6403 "});
6404}
6405
6406#[gpui::test]
6407async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
6408 init_test(cx, |_| {});
6409
6410 let mut cx = EditorTestContext::new(cx).await;
6411 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6412
6413 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6414 .unwrap();
6415 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6416
6417 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6418 .unwrap();
6419 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
6420
6421 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6422 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6423
6424 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6425 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
6426
6427 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6428 .unwrap();
6429 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
6430
6431 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6432 .unwrap();
6433 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6434}
6435
6436#[gpui::test]
6437async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
6438 init_test(cx, |_| {});
6439
6440 let mut cx = EditorTestContext::new(cx).await;
6441 cx.set_state("aˇ");
6442
6443 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6444 .unwrap();
6445 cx.assert_editor_state("«aˇ»");
6446 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6447 .unwrap();
6448 cx.assert_editor_state("«aˇ»");
6449}
6450
6451#[gpui::test]
6452async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
6453 init_test(cx, |_| {});
6454
6455 let mut cx = EditorTestContext::new(cx).await;
6456 cx.set_state(
6457 r#"let foo = 2;
6458lˇet foo = 2;
6459let fooˇ = 2;
6460let foo = 2;
6461let foo = ˇ2;"#,
6462 );
6463
6464 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6465 .unwrap();
6466 cx.assert_editor_state(
6467 r#"let foo = 2;
6468«letˇ» foo = 2;
6469let «fooˇ» = 2;
6470let foo = 2;
6471let foo = «2ˇ»;"#,
6472 );
6473
6474 // noop for multiple selections with different contents
6475 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6476 .unwrap();
6477 cx.assert_editor_state(
6478 r#"let foo = 2;
6479«letˇ» foo = 2;
6480let «fooˇ» = 2;
6481let foo = 2;
6482let foo = «2ˇ»;"#,
6483 );
6484}
6485
6486#[gpui::test]
6487async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
6488 init_test(cx, |_| {});
6489
6490 let mut cx = EditorTestContext::new(cx).await;
6491 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
6492
6493 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6494 .unwrap();
6495 // selection direction is preserved
6496 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
6497
6498 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6499 .unwrap();
6500 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
6501
6502 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6503 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
6504
6505 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6506 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
6507
6508 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6509 .unwrap();
6510 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
6511
6512 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6513 .unwrap();
6514 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
6515}
6516
6517#[gpui::test]
6518async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
6519 init_test(cx, |_| {});
6520
6521 let language = Arc::new(Language::new(
6522 LanguageConfig::default(),
6523 Some(tree_sitter_rust::LANGUAGE.into()),
6524 ));
6525
6526 let text = r#"
6527 use mod1::mod2::{mod3, mod4};
6528
6529 fn fn_1(param1: bool, param2: &str) {
6530 let var1 = "text";
6531 }
6532 "#
6533 .unindent();
6534
6535 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6536 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6537 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6538
6539 editor
6540 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6541 .await;
6542
6543 editor.update_in(cx, |editor, window, cx| {
6544 editor.change_selections(None, window, cx, |s| {
6545 s.select_display_ranges([
6546 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
6547 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
6548 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
6549 ]);
6550 });
6551 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6552 });
6553 editor.update(cx, |editor, cx| {
6554 assert_text_with_selections(
6555 editor,
6556 indoc! {r#"
6557 use mod1::mod2::{mod3, «mod4ˇ»};
6558
6559 fn fn_1«ˇ(param1: bool, param2: &str)» {
6560 let var1 = "«ˇtext»";
6561 }
6562 "#},
6563 cx,
6564 );
6565 });
6566
6567 editor.update_in(cx, |editor, window, cx| {
6568 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6569 });
6570 editor.update(cx, |editor, cx| {
6571 assert_text_with_selections(
6572 editor,
6573 indoc! {r#"
6574 use mod1::mod2::«{mod3, mod4}ˇ»;
6575
6576 «ˇfn fn_1(param1: bool, param2: &str) {
6577 let var1 = "text";
6578 }»
6579 "#},
6580 cx,
6581 );
6582 });
6583
6584 editor.update_in(cx, |editor, window, cx| {
6585 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6586 });
6587 assert_eq!(
6588 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
6589 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
6590 );
6591
6592 // Trying to expand the selected syntax node one more time has no effect.
6593 editor.update_in(cx, |editor, window, cx| {
6594 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6595 });
6596 assert_eq!(
6597 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
6598 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
6599 );
6600
6601 editor.update_in(cx, |editor, window, cx| {
6602 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6603 });
6604 editor.update(cx, |editor, cx| {
6605 assert_text_with_selections(
6606 editor,
6607 indoc! {r#"
6608 use mod1::mod2::«{mod3, mod4}ˇ»;
6609
6610 «ˇfn fn_1(param1: bool, param2: &str) {
6611 let var1 = "text";
6612 }»
6613 "#},
6614 cx,
6615 );
6616 });
6617
6618 editor.update_in(cx, |editor, window, cx| {
6619 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6620 });
6621 editor.update(cx, |editor, cx| {
6622 assert_text_with_selections(
6623 editor,
6624 indoc! {r#"
6625 use mod1::mod2::{mod3, «mod4ˇ»};
6626
6627 fn fn_1«ˇ(param1: bool, param2: &str)» {
6628 let var1 = "«ˇtext»";
6629 }
6630 "#},
6631 cx,
6632 );
6633 });
6634
6635 editor.update_in(cx, |editor, window, cx| {
6636 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6637 });
6638 editor.update(cx, |editor, cx| {
6639 assert_text_with_selections(
6640 editor,
6641 indoc! {r#"
6642 use mod1::mod2::{mod3, mo«ˇ»d4};
6643
6644 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
6645 let var1 = "te«ˇ»xt";
6646 }
6647 "#},
6648 cx,
6649 );
6650 });
6651
6652 // Trying to shrink the selected syntax node one more time has no effect.
6653 editor.update_in(cx, |editor, window, cx| {
6654 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6655 });
6656 editor.update_in(cx, |editor, _, cx| {
6657 assert_text_with_selections(
6658 editor,
6659 indoc! {r#"
6660 use mod1::mod2::{mod3, mo«ˇ»d4};
6661
6662 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
6663 let var1 = "te«ˇ»xt";
6664 }
6665 "#},
6666 cx,
6667 );
6668 });
6669
6670 // Ensure that we keep expanding the selection if the larger selection starts or ends within
6671 // a fold.
6672 editor.update_in(cx, |editor, window, cx| {
6673 editor.fold_creases(
6674 vec![
6675 Crease::simple(
6676 Point::new(0, 21)..Point::new(0, 24),
6677 FoldPlaceholder::test(),
6678 ),
6679 Crease::simple(
6680 Point::new(3, 20)..Point::new(3, 22),
6681 FoldPlaceholder::test(),
6682 ),
6683 ],
6684 true,
6685 window,
6686 cx,
6687 );
6688 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6689 });
6690 editor.update(cx, |editor, cx| {
6691 assert_text_with_selections(
6692 editor,
6693 indoc! {r#"
6694 use mod1::mod2::«{mod3, mod4}ˇ»;
6695
6696 fn fn_1«ˇ(param1: bool, param2: &str)» {
6697 let var1 = "«ˇtext»";
6698 }
6699 "#},
6700 cx,
6701 );
6702 });
6703}
6704
6705#[gpui::test]
6706async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
6707 init_test(cx, |_| {});
6708
6709 let language = Arc::new(Language::new(
6710 LanguageConfig::default(),
6711 Some(tree_sitter_rust::LANGUAGE.into()),
6712 ));
6713
6714 let text = "let a = 2;";
6715
6716 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6717 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6718 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6719
6720 editor
6721 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6722 .await;
6723
6724 // Test case 1: Cursor at end of word
6725 editor.update_in(cx, |editor, window, cx| {
6726 editor.change_selections(None, window, cx, |s| {
6727 s.select_display_ranges([
6728 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
6729 ]);
6730 });
6731 });
6732 editor.update(cx, |editor, cx| {
6733 assert_text_with_selections(editor, "let aˇ = 2;", cx);
6734 });
6735 editor.update_in(cx, |editor, window, cx| {
6736 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6737 });
6738 editor.update(cx, |editor, cx| {
6739 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
6740 });
6741 editor.update_in(cx, |editor, window, cx| {
6742 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6743 });
6744 editor.update(cx, |editor, cx| {
6745 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
6746 });
6747
6748 // Test case 2: Cursor at end of statement
6749 editor.update_in(cx, |editor, window, cx| {
6750 editor.change_selections(None, window, cx, |s| {
6751 s.select_display_ranges([
6752 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
6753 ]);
6754 });
6755 });
6756 editor.update(cx, |editor, cx| {
6757 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
6758 });
6759 editor.update_in(cx, |editor, window, cx| {
6760 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6761 });
6762 editor.update(cx, |editor, cx| {
6763 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
6764 });
6765}
6766
6767#[gpui::test]
6768async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
6769 init_test(cx, |_| {});
6770
6771 let language = Arc::new(Language::new(
6772 LanguageConfig::default(),
6773 Some(tree_sitter_rust::LANGUAGE.into()),
6774 ));
6775
6776 let text = r#"
6777 use mod1::mod2::{mod3, mod4};
6778
6779 fn fn_1(param1: bool, param2: &str) {
6780 let var1 = "hello world";
6781 }
6782 "#
6783 .unindent();
6784
6785 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6786 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6787 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6788
6789 editor
6790 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6791 .await;
6792
6793 // Test 1: Cursor on a letter of a string word
6794 editor.update_in(cx, |editor, window, cx| {
6795 editor.change_selections(None, window, cx, |s| {
6796 s.select_display_ranges([
6797 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
6798 ]);
6799 });
6800 });
6801 editor.update_in(cx, |editor, window, cx| {
6802 assert_text_with_selections(
6803 editor,
6804 indoc! {r#"
6805 use mod1::mod2::{mod3, mod4};
6806
6807 fn fn_1(param1: bool, param2: &str) {
6808 let var1 = "hˇello world";
6809 }
6810 "#},
6811 cx,
6812 );
6813 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6814 assert_text_with_selections(
6815 editor,
6816 indoc! {r#"
6817 use mod1::mod2::{mod3, mod4};
6818
6819 fn fn_1(param1: bool, param2: &str) {
6820 let var1 = "«ˇhello» world";
6821 }
6822 "#},
6823 cx,
6824 );
6825 });
6826
6827 // Test 2: Partial selection within a word
6828 editor.update_in(cx, |editor, window, cx| {
6829 editor.change_selections(None, window, cx, |s| {
6830 s.select_display_ranges([
6831 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
6832 ]);
6833 });
6834 });
6835 editor.update_in(cx, |editor, window, cx| {
6836 assert_text_with_selections(
6837 editor,
6838 indoc! {r#"
6839 use mod1::mod2::{mod3, mod4};
6840
6841 fn fn_1(param1: bool, param2: &str) {
6842 let var1 = "h«elˇ»lo world";
6843 }
6844 "#},
6845 cx,
6846 );
6847 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6848 assert_text_with_selections(
6849 editor,
6850 indoc! {r#"
6851 use mod1::mod2::{mod3, mod4};
6852
6853 fn fn_1(param1: bool, param2: &str) {
6854 let var1 = "«ˇhello» world";
6855 }
6856 "#},
6857 cx,
6858 );
6859 });
6860
6861 // Test 3: Complete word already selected
6862 editor.update_in(cx, |editor, window, cx| {
6863 editor.change_selections(None, window, cx, |s| {
6864 s.select_display_ranges([
6865 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
6866 ]);
6867 });
6868 });
6869 editor.update_in(cx, |editor, window, cx| {
6870 assert_text_with_selections(
6871 editor,
6872 indoc! {r#"
6873 use mod1::mod2::{mod3, mod4};
6874
6875 fn fn_1(param1: bool, param2: &str) {
6876 let var1 = "«helloˇ» world";
6877 }
6878 "#},
6879 cx,
6880 );
6881 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6882 assert_text_with_selections(
6883 editor,
6884 indoc! {r#"
6885 use mod1::mod2::{mod3, mod4};
6886
6887 fn fn_1(param1: bool, param2: &str) {
6888 let var1 = "«hello worldˇ»";
6889 }
6890 "#},
6891 cx,
6892 );
6893 });
6894
6895 // Test 4: Selection spanning across words
6896 editor.update_in(cx, |editor, window, cx| {
6897 editor.change_selections(None, window, cx, |s| {
6898 s.select_display_ranges([
6899 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
6900 ]);
6901 });
6902 });
6903 editor.update_in(cx, |editor, window, cx| {
6904 assert_text_with_selections(
6905 editor,
6906 indoc! {r#"
6907 use mod1::mod2::{mod3, mod4};
6908
6909 fn fn_1(param1: bool, param2: &str) {
6910 let var1 = "hel«lo woˇ»rld";
6911 }
6912 "#},
6913 cx,
6914 );
6915 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6916 assert_text_with_selections(
6917 editor,
6918 indoc! {r#"
6919 use mod1::mod2::{mod3, mod4};
6920
6921 fn fn_1(param1: bool, param2: &str) {
6922 let var1 = "«ˇhello world»";
6923 }
6924 "#},
6925 cx,
6926 );
6927 });
6928
6929 // Test 5: Expansion beyond string
6930 editor.update_in(cx, |editor, window, cx| {
6931 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6932 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6933 assert_text_with_selections(
6934 editor,
6935 indoc! {r#"
6936 use mod1::mod2::{mod3, mod4};
6937
6938 fn fn_1(param1: bool, param2: &str) {
6939 «ˇlet var1 = "hello world";»
6940 }
6941 "#},
6942 cx,
6943 );
6944 });
6945}
6946
6947#[gpui::test]
6948async fn test_fold_function_bodies(cx: &mut TestAppContext) {
6949 init_test(cx, |_| {});
6950
6951 let base_text = r#"
6952 impl A {
6953 // this is an uncommitted comment
6954
6955 fn b() {
6956 c();
6957 }
6958
6959 // this is another uncommitted comment
6960
6961 fn d() {
6962 // e
6963 // f
6964 }
6965 }
6966
6967 fn g() {
6968 // h
6969 }
6970 "#
6971 .unindent();
6972
6973 let text = r#"
6974 ˇimpl A {
6975
6976 fn b() {
6977 c();
6978 }
6979
6980 fn d() {
6981 // e
6982 // f
6983 }
6984 }
6985
6986 fn g() {
6987 // h
6988 }
6989 "#
6990 .unindent();
6991
6992 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
6993 cx.set_state(&text);
6994 cx.set_head_text(&base_text);
6995 cx.update_editor(|editor, window, cx| {
6996 editor.expand_all_diff_hunks(&Default::default(), window, cx);
6997 });
6998
6999 cx.assert_state_with_diff(
7000 "
7001 ˇimpl A {
7002 - // this is an uncommitted comment
7003
7004 fn b() {
7005 c();
7006 }
7007
7008 - // this is another uncommitted comment
7009 -
7010 fn d() {
7011 // e
7012 // f
7013 }
7014 }
7015
7016 fn g() {
7017 // h
7018 }
7019 "
7020 .unindent(),
7021 );
7022
7023 let expected_display_text = "
7024 impl A {
7025 // this is an uncommitted comment
7026
7027 fn b() {
7028 ⋯
7029 }
7030
7031 // this is another uncommitted comment
7032
7033 fn d() {
7034 ⋯
7035 }
7036 }
7037
7038 fn g() {
7039 ⋯
7040 }
7041 "
7042 .unindent();
7043
7044 cx.update_editor(|editor, window, cx| {
7045 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
7046 assert_eq!(editor.display_text(cx), expected_display_text);
7047 });
7048}
7049
7050#[gpui::test]
7051async fn test_autoindent(cx: &mut TestAppContext) {
7052 init_test(cx, |_| {});
7053
7054 let language = Arc::new(
7055 Language::new(
7056 LanguageConfig {
7057 brackets: BracketPairConfig {
7058 pairs: vec![
7059 BracketPair {
7060 start: "{".to_string(),
7061 end: "}".to_string(),
7062 close: false,
7063 surround: false,
7064 newline: true,
7065 },
7066 BracketPair {
7067 start: "(".to_string(),
7068 end: ")".to_string(),
7069 close: false,
7070 surround: false,
7071 newline: true,
7072 },
7073 ],
7074 ..Default::default()
7075 },
7076 ..Default::default()
7077 },
7078 Some(tree_sitter_rust::LANGUAGE.into()),
7079 )
7080 .with_indents_query(
7081 r#"
7082 (_ "(" ")" @end) @indent
7083 (_ "{" "}" @end) @indent
7084 "#,
7085 )
7086 .unwrap(),
7087 );
7088
7089 let text = "fn a() {}";
7090
7091 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7092 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7093 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7094 editor
7095 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7096 .await;
7097
7098 editor.update_in(cx, |editor, window, cx| {
7099 editor.change_selections(None, window, cx, |s| s.select_ranges([5..5, 8..8, 9..9]));
7100 editor.newline(&Newline, window, cx);
7101 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
7102 assert_eq!(
7103 editor.selections.ranges(cx),
7104 &[
7105 Point::new(1, 4)..Point::new(1, 4),
7106 Point::new(3, 4)..Point::new(3, 4),
7107 Point::new(5, 0)..Point::new(5, 0)
7108 ]
7109 );
7110 });
7111}
7112
7113#[gpui::test]
7114async fn test_autoindent_selections(cx: &mut TestAppContext) {
7115 init_test(cx, |_| {});
7116
7117 {
7118 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
7119 cx.set_state(indoc! {"
7120 impl A {
7121
7122 fn b() {}
7123
7124 «fn c() {
7125
7126 }ˇ»
7127 }
7128 "});
7129
7130 cx.update_editor(|editor, window, cx| {
7131 editor.autoindent(&Default::default(), window, cx);
7132 });
7133
7134 cx.assert_editor_state(indoc! {"
7135 impl A {
7136
7137 fn b() {}
7138
7139 «fn c() {
7140
7141 }ˇ»
7142 }
7143 "});
7144 }
7145
7146 {
7147 let mut cx = EditorTestContext::new_multibuffer(
7148 cx,
7149 [indoc! { "
7150 impl A {
7151 «
7152 // a
7153 fn b(){}
7154 »
7155 «
7156 }
7157 fn c(){}
7158 »
7159 "}],
7160 );
7161
7162 let buffer = cx.update_editor(|editor, _, cx| {
7163 let buffer = editor.buffer().update(cx, |buffer, _| {
7164 buffer.all_buffers().iter().next().unwrap().clone()
7165 });
7166 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7167 buffer
7168 });
7169
7170 cx.run_until_parked();
7171 cx.update_editor(|editor, window, cx| {
7172 editor.select_all(&Default::default(), window, cx);
7173 editor.autoindent(&Default::default(), window, cx)
7174 });
7175 cx.run_until_parked();
7176
7177 cx.update(|_, cx| {
7178 assert_eq!(
7179 buffer.read(cx).text(),
7180 indoc! { "
7181 impl A {
7182
7183 // a
7184 fn b(){}
7185
7186
7187 }
7188 fn c(){}
7189
7190 " }
7191 )
7192 });
7193 }
7194}
7195
7196#[gpui::test]
7197async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
7198 init_test(cx, |_| {});
7199
7200 let mut cx = EditorTestContext::new(cx).await;
7201
7202 let language = Arc::new(Language::new(
7203 LanguageConfig {
7204 brackets: BracketPairConfig {
7205 pairs: vec![
7206 BracketPair {
7207 start: "{".to_string(),
7208 end: "}".to_string(),
7209 close: true,
7210 surround: true,
7211 newline: true,
7212 },
7213 BracketPair {
7214 start: "(".to_string(),
7215 end: ")".to_string(),
7216 close: true,
7217 surround: true,
7218 newline: true,
7219 },
7220 BracketPair {
7221 start: "/*".to_string(),
7222 end: " */".to_string(),
7223 close: true,
7224 surround: true,
7225 newline: true,
7226 },
7227 BracketPair {
7228 start: "[".to_string(),
7229 end: "]".to_string(),
7230 close: false,
7231 surround: false,
7232 newline: true,
7233 },
7234 BracketPair {
7235 start: "\"".to_string(),
7236 end: "\"".to_string(),
7237 close: true,
7238 surround: true,
7239 newline: false,
7240 },
7241 BracketPair {
7242 start: "<".to_string(),
7243 end: ">".to_string(),
7244 close: false,
7245 surround: true,
7246 newline: true,
7247 },
7248 ],
7249 ..Default::default()
7250 },
7251 autoclose_before: "})]".to_string(),
7252 ..Default::default()
7253 },
7254 Some(tree_sitter_rust::LANGUAGE.into()),
7255 ));
7256
7257 cx.language_registry().add(language.clone());
7258 cx.update_buffer(|buffer, cx| {
7259 buffer.set_language(Some(language), cx);
7260 });
7261
7262 cx.set_state(
7263 &r#"
7264 🏀ˇ
7265 εˇ
7266 ❤️ˇ
7267 "#
7268 .unindent(),
7269 );
7270
7271 // autoclose multiple nested brackets at multiple cursors
7272 cx.update_editor(|editor, window, cx| {
7273 editor.handle_input("{", window, cx);
7274 editor.handle_input("{", window, cx);
7275 editor.handle_input("{", window, cx);
7276 });
7277 cx.assert_editor_state(
7278 &"
7279 🏀{{{ˇ}}}
7280 ε{{{ˇ}}}
7281 ❤️{{{ˇ}}}
7282 "
7283 .unindent(),
7284 );
7285
7286 // insert a different closing bracket
7287 cx.update_editor(|editor, window, cx| {
7288 editor.handle_input(")", window, cx);
7289 });
7290 cx.assert_editor_state(
7291 &"
7292 🏀{{{)ˇ}}}
7293 ε{{{)ˇ}}}
7294 ❤️{{{)ˇ}}}
7295 "
7296 .unindent(),
7297 );
7298
7299 // skip over the auto-closed brackets when typing a closing bracket
7300 cx.update_editor(|editor, window, cx| {
7301 editor.move_right(&MoveRight, window, cx);
7302 editor.handle_input("}", window, cx);
7303 editor.handle_input("}", window, cx);
7304 editor.handle_input("}", window, cx);
7305 });
7306 cx.assert_editor_state(
7307 &"
7308 🏀{{{)}}}}ˇ
7309 ε{{{)}}}}ˇ
7310 ❤️{{{)}}}}ˇ
7311 "
7312 .unindent(),
7313 );
7314
7315 // autoclose multi-character pairs
7316 cx.set_state(
7317 &"
7318 ˇ
7319 ˇ
7320 "
7321 .unindent(),
7322 );
7323 cx.update_editor(|editor, window, cx| {
7324 editor.handle_input("/", window, cx);
7325 editor.handle_input("*", window, cx);
7326 });
7327 cx.assert_editor_state(
7328 &"
7329 /*ˇ */
7330 /*ˇ */
7331 "
7332 .unindent(),
7333 );
7334
7335 // one cursor autocloses a multi-character pair, one cursor
7336 // does not autoclose.
7337 cx.set_state(
7338 &"
7339 /ˇ
7340 ˇ
7341 "
7342 .unindent(),
7343 );
7344 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
7345 cx.assert_editor_state(
7346 &"
7347 /*ˇ */
7348 *ˇ
7349 "
7350 .unindent(),
7351 );
7352
7353 // Don't autoclose if the next character isn't whitespace and isn't
7354 // listed in the language's "autoclose_before" section.
7355 cx.set_state("ˇa b");
7356 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7357 cx.assert_editor_state("{ˇa b");
7358
7359 // Don't autoclose if `close` is false for the bracket pair
7360 cx.set_state("ˇ");
7361 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
7362 cx.assert_editor_state("[ˇ");
7363
7364 // Surround with brackets if text is selected
7365 cx.set_state("«aˇ» b");
7366 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7367 cx.assert_editor_state("{«aˇ»} b");
7368
7369 // Autoclose when not immediately after a word character
7370 cx.set_state("a ˇ");
7371 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7372 cx.assert_editor_state("a \"ˇ\"");
7373
7374 // Autoclose pair where the start and end characters are the same
7375 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7376 cx.assert_editor_state("a \"\"ˇ");
7377
7378 // Don't autoclose when immediately after a word character
7379 cx.set_state("aˇ");
7380 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7381 cx.assert_editor_state("a\"ˇ");
7382
7383 // Do autoclose when after a non-word character
7384 cx.set_state("{ˇ");
7385 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7386 cx.assert_editor_state("{\"ˇ\"");
7387
7388 // Non identical pairs autoclose regardless of preceding character
7389 cx.set_state("aˇ");
7390 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7391 cx.assert_editor_state("a{ˇ}");
7392
7393 // Don't autoclose pair if autoclose is disabled
7394 cx.set_state("ˇ");
7395 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
7396 cx.assert_editor_state("<ˇ");
7397
7398 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
7399 cx.set_state("«aˇ» b");
7400 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
7401 cx.assert_editor_state("<«aˇ»> b");
7402}
7403
7404#[gpui::test]
7405async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
7406 init_test(cx, |settings| {
7407 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
7408 });
7409
7410 let mut cx = EditorTestContext::new(cx).await;
7411
7412 let language = Arc::new(Language::new(
7413 LanguageConfig {
7414 brackets: BracketPairConfig {
7415 pairs: vec![
7416 BracketPair {
7417 start: "{".to_string(),
7418 end: "}".to_string(),
7419 close: true,
7420 surround: true,
7421 newline: true,
7422 },
7423 BracketPair {
7424 start: "(".to_string(),
7425 end: ")".to_string(),
7426 close: true,
7427 surround: true,
7428 newline: true,
7429 },
7430 BracketPair {
7431 start: "[".to_string(),
7432 end: "]".to_string(),
7433 close: false,
7434 surround: false,
7435 newline: true,
7436 },
7437 ],
7438 ..Default::default()
7439 },
7440 autoclose_before: "})]".to_string(),
7441 ..Default::default()
7442 },
7443 Some(tree_sitter_rust::LANGUAGE.into()),
7444 ));
7445
7446 cx.language_registry().add(language.clone());
7447 cx.update_buffer(|buffer, cx| {
7448 buffer.set_language(Some(language), cx);
7449 });
7450
7451 cx.set_state(
7452 &"
7453 ˇ
7454 ˇ
7455 ˇ
7456 "
7457 .unindent(),
7458 );
7459
7460 // ensure only matching closing brackets are skipped over
7461 cx.update_editor(|editor, window, cx| {
7462 editor.handle_input("}", window, cx);
7463 editor.move_left(&MoveLeft, window, cx);
7464 editor.handle_input(")", window, cx);
7465 editor.move_left(&MoveLeft, window, cx);
7466 });
7467 cx.assert_editor_state(
7468 &"
7469 ˇ)}
7470 ˇ)}
7471 ˇ)}
7472 "
7473 .unindent(),
7474 );
7475
7476 // skip-over closing brackets at multiple cursors
7477 cx.update_editor(|editor, window, cx| {
7478 editor.handle_input(")", window, cx);
7479 editor.handle_input("}", window, cx);
7480 });
7481 cx.assert_editor_state(
7482 &"
7483 )}ˇ
7484 )}ˇ
7485 )}ˇ
7486 "
7487 .unindent(),
7488 );
7489
7490 // ignore non-close brackets
7491 cx.update_editor(|editor, window, cx| {
7492 editor.handle_input("]", window, cx);
7493 editor.move_left(&MoveLeft, window, cx);
7494 editor.handle_input("]", window, cx);
7495 });
7496 cx.assert_editor_state(
7497 &"
7498 )}]ˇ]
7499 )}]ˇ]
7500 )}]ˇ]
7501 "
7502 .unindent(),
7503 );
7504}
7505
7506#[gpui::test]
7507async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
7508 init_test(cx, |_| {});
7509
7510 let mut cx = EditorTestContext::new(cx).await;
7511
7512 let html_language = Arc::new(
7513 Language::new(
7514 LanguageConfig {
7515 name: "HTML".into(),
7516 brackets: BracketPairConfig {
7517 pairs: vec![
7518 BracketPair {
7519 start: "<".into(),
7520 end: ">".into(),
7521 close: true,
7522 ..Default::default()
7523 },
7524 BracketPair {
7525 start: "{".into(),
7526 end: "}".into(),
7527 close: true,
7528 ..Default::default()
7529 },
7530 BracketPair {
7531 start: "(".into(),
7532 end: ")".into(),
7533 close: true,
7534 ..Default::default()
7535 },
7536 ],
7537 ..Default::default()
7538 },
7539 autoclose_before: "})]>".into(),
7540 ..Default::default()
7541 },
7542 Some(tree_sitter_html::LANGUAGE.into()),
7543 )
7544 .with_injection_query(
7545 r#"
7546 (script_element
7547 (raw_text) @injection.content
7548 (#set! injection.language "javascript"))
7549 "#,
7550 )
7551 .unwrap(),
7552 );
7553
7554 let javascript_language = Arc::new(Language::new(
7555 LanguageConfig {
7556 name: "JavaScript".into(),
7557 brackets: BracketPairConfig {
7558 pairs: vec![
7559 BracketPair {
7560 start: "/*".into(),
7561 end: " */".into(),
7562 close: true,
7563 ..Default::default()
7564 },
7565 BracketPair {
7566 start: "{".into(),
7567 end: "}".into(),
7568 close: true,
7569 ..Default::default()
7570 },
7571 BracketPair {
7572 start: "(".into(),
7573 end: ")".into(),
7574 close: true,
7575 ..Default::default()
7576 },
7577 ],
7578 ..Default::default()
7579 },
7580 autoclose_before: "})]>".into(),
7581 ..Default::default()
7582 },
7583 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
7584 ));
7585
7586 cx.language_registry().add(html_language.clone());
7587 cx.language_registry().add(javascript_language.clone());
7588
7589 cx.update_buffer(|buffer, cx| {
7590 buffer.set_language(Some(html_language), cx);
7591 });
7592
7593 cx.set_state(
7594 &r#"
7595 <body>ˇ
7596 <script>
7597 var x = 1;ˇ
7598 </script>
7599 </body>ˇ
7600 "#
7601 .unindent(),
7602 );
7603
7604 // Precondition: different languages are active at different locations.
7605 cx.update_editor(|editor, window, cx| {
7606 let snapshot = editor.snapshot(window, cx);
7607 let cursors = editor.selections.ranges::<usize>(cx);
7608 let languages = cursors
7609 .iter()
7610 .map(|c| snapshot.language_at(c.start).unwrap().name())
7611 .collect::<Vec<_>>();
7612 assert_eq!(
7613 languages,
7614 &["HTML".into(), "JavaScript".into(), "HTML".into()]
7615 );
7616 });
7617
7618 // Angle brackets autoclose in HTML, but not JavaScript.
7619 cx.update_editor(|editor, window, cx| {
7620 editor.handle_input("<", window, cx);
7621 editor.handle_input("a", window, cx);
7622 });
7623 cx.assert_editor_state(
7624 &r#"
7625 <body><aˇ>
7626 <script>
7627 var x = 1;<aˇ
7628 </script>
7629 </body><aˇ>
7630 "#
7631 .unindent(),
7632 );
7633
7634 // Curly braces and parens autoclose in both HTML and JavaScript.
7635 cx.update_editor(|editor, window, cx| {
7636 editor.handle_input(" b=", window, cx);
7637 editor.handle_input("{", window, cx);
7638 editor.handle_input("c", window, cx);
7639 editor.handle_input("(", window, cx);
7640 });
7641 cx.assert_editor_state(
7642 &r#"
7643 <body><a b={c(ˇ)}>
7644 <script>
7645 var x = 1;<a b={c(ˇ)}
7646 </script>
7647 </body><a b={c(ˇ)}>
7648 "#
7649 .unindent(),
7650 );
7651
7652 // Brackets that were already autoclosed are skipped.
7653 cx.update_editor(|editor, window, cx| {
7654 editor.handle_input(")", window, cx);
7655 editor.handle_input("d", window, cx);
7656 editor.handle_input("}", window, cx);
7657 });
7658 cx.assert_editor_state(
7659 &r#"
7660 <body><a b={c()d}ˇ>
7661 <script>
7662 var x = 1;<a b={c()d}ˇ
7663 </script>
7664 </body><a b={c()d}ˇ>
7665 "#
7666 .unindent(),
7667 );
7668 cx.update_editor(|editor, window, cx| {
7669 editor.handle_input(">", window, cx);
7670 });
7671 cx.assert_editor_state(
7672 &r#"
7673 <body><a b={c()d}>ˇ
7674 <script>
7675 var x = 1;<a b={c()d}>ˇ
7676 </script>
7677 </body><a b={c()d}>ˇ
7678 "#
7679 .unindent(),
7680 );
7681
7682 // Reset
7683 cx.set_state(
7684 &r#"
7685 <body>ˇ
7686 <script>
7687 var x = 1;ˇ
7688 </script>
7689 </body>ˇ
7690 "#
7691 .unindent(),
7692 );
7693
7694 cx.update_editor(|editor, window, cx| {
7695 editor.handle_input("<", window, cx);
7696 });
7697 cx.assert_editor_state(
7698 &r#"
7699 <body><ˇ>
7700 <script>
7701 var x = 1;<ˇ
7702 </script>
7703 </body><ˇ>
7704 "#
7705 .unindent(),
7706 );
7707
7708 // When backspacing, the closing angle brackets are removed.
7709 cx.update_editor(|editor, window, cx| {
7710 editor.backspace(&Backspace, window, cx);
7711 });
7712 cx.assert_editor_state(
7713 &r#"
7714 <body>ˇ
7715 <script>
7716 var x = 1;ˇ
7717 </script>
7718 </body>ˇ
7719 "#
7720 .unindent(),
7721 );
7722
7723 // Block comments autoclose in JavaScript, but not HTML.
7724 cx.update_editor(|editor, window, cx| {
7725 editor.handle_input("/", window, cx);
7726 editor.handle_input("*", window, cx);
7727 });
7728 cx.assert_editor_state(
7729 &r#"
7730 <body>/*ˇ
7731 <script>
7732 var x = 1;/*ˇ */
7733 </script>
7734 </body>/*ˇ
7735 "#
7736 .unindent(),
7737 );
7738}
7739
7740#[gpui::test]
7741async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
7742 init_test(cx, |_| {});
7743
7744 let mut cx = EditorTestContext::new(cx).await;
7745
7746 let rust_language = Arc::new(
7747 Language::new(
7748 LanguageConfig {
7749 name: "Rust".into(),
7750 brackets: serde_json::from_value(json!([
7751 { "start": "{", "end": "}", "close": true, "newline": true },
7752 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
7753 ]))
7754 .unwrap(),
7755 autoclose_before: "})]>".into(),
7756 ..Default::default()
7757 },
7758 Some(tree_sitter_rust::LANGUAGE.into()),
7759 )
7760 .with_override_query("(string_literal) @string")
7761 .unwrap(),
7762 );
7763
7764 cx.language_registry().add(rust_language.clone());
7765 cx.update_buffer(|buffer, cx| {
7766 buffer.set_language(Some(rust_language), cx);
7767 });
7768
7769 cx.set_state(
7770 &r#"
7771 let x = ˇ
7772 "#
7773 .unindent(),
7774 );
7775
7776 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
7777 cx.update_editor(|editor, window, cx| {
7778 editor.handle_input("\"", window, cx);
7779 });
7780 cx.assert_editor_state(
7781 &r#"
7782 let x = "ˇ"
7783 "#
7784 .unindent(),
7785 );
7786
7787 // Inserting another quotation mark. The cursor moves across the existing
7788 // automatically-inserted quotation mark.
7789 cx.update_editor(|editor, window, cx| {
7790 editor.handle_input("\"", window, cx);
7791 });
7792 cx.assert_editor_state(
7793 &r#"
7794 let x = ""ˇ
7795 "#
7796 .unindent(),
7797 );
7798
7799 // Reset
7800 cx.set_state(
7801 &r#"
7802 let x = ˇ
7803 "#
7804 .unindent(),
7805 );
7806
7807 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
7808 cx.update_editor(|editor, window, cx| {
7809 editor.handle_input("\"", window, cx);
7810 editor.handle_input(" ", window, cx);
7811 editor.move_left(&Default::default(), window, cx);
7812 editor.handle_input("\\", window, cx);
7813 editor.handle_input("\"", window, cx);
7814 });
7815 cx.assert_editor_state(
7816 &r#"
7817 let x = "\"ˇ "
7818 "#
7819 .unindent(),
7820 );
7821
7822 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
7823 // mark. Nothing is inserted.
7824 cx.update_editor(|editor, window, cx| {
7825 editor.move_right(&Default::default(), window, cx);
7826 editor.handle_input("\"", window, cx);
7827 });
7828 cx.assert_editor_state(
7829 &r#"
7830 let x = "\" "ˇ
7831 "#
7832 .unindent(),
7833 );
7834}
7835
7836#[gpui::test]
7837async fn test_surround_with_pair(cx: &mut TestAppContext) {
7838 init_test(cx, |_| {});
7839
7840 let language = Arc::new(Language::new(
7841 LanguageConfig {
7842 brackets: BracketPairConfig {
7843 pairs: vec![
7844 BracketPair {
7845 start: "{".to_string(),
7846 end: "}".to_string(),
7847 close: true,
7848 surround: true,
7849 newline: true,
7850 },
7851 BracketPair {
7852 start: "/* ".to_string(),
7853 end: "*/".to_string(),
7854 close: true,
7855 surround: true,
7856 ..Default::default()
7857 },
7858 ],
7859 ..Default::default()
7860 },
7861 ..Default::default()
7862 },
7863 Some(tree_sitter_rust::LANGUAGE.into()),
7864 ));
7865
7866 let text = r#"
7867 a
7868 b
7869 c
7870 "#
7871 .unindent();
7872
7873 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7874 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7875 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7876 editor
7877 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7878 .await;
7879
7880 editor.update_in(cx, |editor, window, cx| {
7881 editor.change_selections(None, window, cx, |s| {
7882 s.select_display_ranges([
7883 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7884 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7885 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
7886 ])
7887 });
7888
7889 editor.handle_input("{", window, cx);
7890 editor.handle_input("{", window, cx);
7891 editor.handle_input("{", window, cx);
7892 assert_eq!(
7893 editor.text(cx),
7894 "
7895 {{{a}}}
7896 {{{b}}}
7897 {{{c}}}
7898 "
7899 .unindent()
7900 );
7901 assert_eq!(
7902 editor.selections.display_ranges(cx),
7903 [
7904 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
7905 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
7906 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
7907 ]
7908 );
7909
7910 editor.undo(&Undo, window, cx);
7911 editor.undo(&Undo, window, cx);
7912 editor.undo(&Undo, window, cx);
7913 assert_eq!(
7914 editor.text(cx),
7915 "
7916 a
7917 b
7918 c
7919 "
7920 .unindent()
7921 );
7922 assert_eq!(
7923 editor.selections.display_ranges(cx),
7924 [
7925 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7926 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7927 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
7928 ]
7929 );
7930
7931 // Ensure inserting the first character of a multi-byte bracket pair
7932 // doesn't surround the selections with the bracket.
7933 editor.handle_input("/", window, cx);
7934 assert_eq!(
7935 editor.text(cx),
7936 "
7937 /
7938 /
7939 /
7940 "
7941 .unindent()
7942 );
7943 assert_eq!(
7944 editor.selections.display_ranges(cx),
7945 [
7946 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
7947 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
7948 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
7949 ]
7950 );
7951
7952 editor.undo(&Undo, window, cx);
7953 assert_eq!(
7954 editor.text(cx),
7955 "
7956 a
7957 b
7958 c
7959 "
7960 .unindent()
7961 );
7962 assert_eq!(
7963 editor.selections.display_ranges(cx),
7964 [
7965 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7966 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7967 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
7968 ]
7969 );
7970
7971 // Ensure inserting the last character of a multi-byte bracket pair
7972 // doesn't surround the selections with the bracket.
7973 editor.handle_input("*", window, cx);
7974 assert_eq!(
7975 editor.text(cx),
7976 "
7977 *
7978 *
7979 *
7980 "
7981 .unindent()
7982 );
7983 assert_eq!(
7984 editor.selections.display_ranges(cx),
7985 [
7986 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
7987 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
7988 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
7989 ]
7990 );
7991 });
7992}
7993
7994#[gpui::test]
7995async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
7996 init_test(cx, |_| {});
7997
7998 let language = Arc::new(Language::new(
7999 LanguageConfig {
8000 brackets: BracketPairConfig {
8001 pairs: vec![BracketPair {
8002 start: "{".to_string(),
8003 end: "}".to_string(),
8004 close: true,
8005 surround: true,
8006 newline: true,
8007 }],
8008 ..Default::default()
8009 },
8010 autoclose_before: "}".to_string(),
8011 ..Default::default()
8012 },
8013 Some(tree_sitter_rust::LANGUAGE.into()),
8014 ));
8015
8016 let text = r#"
8017 a
8018 b
8019 c
8020 "#
8021 .unindent();
8022
8023 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8024 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8025 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8026 editor
8027 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8028 .await;
8029
8030 editor.update_in(cx, |editor, window, cx| {
8031 editor.change_selections(None, window, cx, |s| {
8032 s.select_ranges([
8033 Point::new(0, 1)..Point::new(0, 1),
8034 Point::new(1, 1)..Point::new(1, 1),
8035 Point::new(2, 1)..Point::new(2, 1),
8036 ])
8037 });
8038
8039 editor.handle_input("{", window, cx);
8040 editor.handle_input("{", window, cx);
8041 editor.handle_input("_", window, cx);
8042 assert_eq!(
8043 editor.text(cx),
8044 "
8045 a{{_}}
8046 b{{_}}
8047 c{{_}}
8048 "
8049 .unindent()
8050 );
8051 assert_eq!(
8052 editor.selections.ranges::<Point>(cx),
8053 [
8054 Point::new(0, 4)..Point::new(0, 4),
8055 Point::new(1, 4)..Point::new(1, 4),
8056 Point::new(2, 4)..Point::new(2, 4)
8057 ]
8058 );
8059
8060 editor.backspace(&Default::default(), window, cx);
8061 editor.backspace(&Default::default(), window, cx);
8062 assert_eq!(
8063 editor.text(cx),
8064 "
8065 a{}
8066 b{}
8067 c{}
8068 "
8069 .unindent()
8070 );
8071 assert_eq!(
8072 editor.selections.ranges::<Point>(cx),
8073 [
8074 Point::new(0, 2)..Point::new(0, 2),
8075 Point::new(1, 2)..Point::new(1, 2),
8076 Point::new(2, 2)..Point::new(2, 2)
8077 ]
8078 );
8079
8080 editor.delete_to_previous_word_start(&Default::default(), window, cx);
8081 assert_eq!(
8082 editor.text(cx),
8083 "
8084 a
8085 b
8086 c
8087 "
8088 .unindent()
8089 );
8090 assert_eq!(
8091 editor.selections.ranges::<Point>(cx),
8092 [
8093 Point::new(0, 1)..Point::new(0, 1),
8094 Point::new(1, 1)..Point::new(1, 1),
8095 Point::new(2, 1)..Point::new(2, 1)
8096 ]
8097 );
8098 });
8099}
8100
8101#[gpui::test]
8102async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
8103 init_test(cx, |settings| {
8104 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
8105 });
8106
8107 let mut cx = EditorTestContext::new(cx).await;
8108
8109 let language = Arc::new(Language::new(
8110 LanguageConfig {
8111 brackets: BracketPairConfig {
8112 pairs: vec![
8113 BracketPair {
8114 start: "{".to_string(),
8115 end: "}".to_string(),
8116 close: true,
8117 surround: true,
8118 newline: true,
8119 },
8120 BracketPair {
8121 start: "(".to_string(),
8122 end: ")".to_string(),
8123 close: true,
8124 surround: true,
8125 newline: true,
8126 },
8127 BracketPair {
8128 start: "[".to_string(),
8129 end: "]".to_string(),
8130 close: false,
8131 surround: true,
8132 newline: true,
8133 },
8134 ],
8135 ..Default::default()
8136 },
8137 autoclose_before: "})]".to_string(),
8138 ..Default::default()
8139 },
8140 Some(tree_sitter_rust::LANGUAGE.into()),
8141 ));
8142
8143 cx.language_registry().add(language.clone());
8144 cx.update_buffer(|buffer, cx| {
8145 buffer.set_language(Some(language), cx);
8146 });
8147
8148 cx.set_state(
8149 &"
8150 {(ˇ)}
8151 [[ˇ]]
8152 {(ˇ)}
8153 "
8154 .unindent(),
8155 );
8156
8157 cx.update_editor(|editor, window, cx| {
8158 editor.backspace(&Default::default(), window, cx);
8159 editor.backspace(&Default::default(), window, cx);
8160 });
8161
8162 cx.assert_editor_state(
8163 &"
8164 ˇ
8165 ˇ]]
8166 ˇ
8167 "
8168 .unindent(),
8169 );
8170
8171 cx.update_editor(|editor, window, cx| {
8172 editor.handle_input("{", window, cx);
8173 editor.handle_input("{", window, cx);
8174 editor.move_right(&MoveRight, window, cx);
8175 editor.move_right(&MoveRight, window, cx);
8176 editor.move_left(&MoveLeft, window, cx);
8177 editor.move_left(&MoveLeft, window, cx);
8178 editor.backspace(&Default::default(), window, cx);
8179 });
8180
8181 cx.assert_editor_state(
8182 &"
8183 {ˇ}
8184 {ˇ}]]
8185 {ˇ}
8186 "
8187 .unindent(),
8188 );
8189
8190 cx.update_editor(|editor, window, cx| {
8191 editor.backspace(&Default::default(), window, cx);
8192 });
8193
8194 cx.assert_editor_state(
8195 &"
8196 ˇ
8197 ˇ]]
8198 ˇ
8199 "
8200 .unindent(),
8201 );
8202}
8203
8204#[gpui::test]
8205async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
8206 init_test(cx, |_| {});
8207
8208 let language = Arc::new(Language::new(
8209 LanguageConfig::default(),
8210 Some(tree_sitter_rust::LANGUAGE.into()),
8211 ));
8212
8213 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
8214 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8215 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8216 editor
8217 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8218 .await;
8219
8220 editor.update_in(cx, |editor, window, cx| {
8221 editor.set_auto_replace_emoji_shortcode(true);
8222
8223 editor.handle_input("Hello ", window, cx);
8224 editor.handle_input(":wave", window, cx);
8225 assert_eq!(editor.text(cx), "Hello :wave".unindent());
8226
8227 editor.handle_input(":", window, cx);
8228 assert_eq!(editor.text(cx), "Hello 👋".unindent());
8229
8230 editor.handle_input(" :smile", window, cx);
8231 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
8232
8233 editor.handle_input(":", window, cx);
8234 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
8235
8236 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
8237 editor.handle_input(":wave", window, cx);
8238 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
8239
8240 editor.handle_input(":", window, cx);
8241 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
8242
8243 editor.handle_input(":1", window, cx);
8244 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
8245
8246 editor.handle_input(":", window, cx);
8247 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
8248
8249 // Ensure shortcode does not get replaced when it is part of a word
8250 editor.handle_input(" Test:wave", window, cx);
8251 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
8252
8253 editor.handle_input(":", window, cx);
8254 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
8255
8256 editor.set_auto_replace_emoji_shortcode(false);
8257
8258 // Ensure shortcode does not get replaced when auto replace is off
8259 editor.handle_input(" :wave", window, cx);
8260 assert_eq!(
8261 editor.text(cx),
8262 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
8263 );
8264
8265 editor.handle_input(":", window, cx);
8266 assert_eq!(
8267 editor.text(cx),
8268 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
8269 );
8270 });
8271}
8272
8273#[gpui::test]
8274async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
8275 init_test(cx, |_| {});
8276
8277 let (text, insertion_ranges) = marked_text_ranges(
8278 indoc! {"
8279 ˇ
8280 "},
8281 false,
8282 );
8283
8284 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
8285 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8286
8287 _ = editor.update_in(cx, |editor, window, cx| {
8288 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
8289
8290 editor
8291 .insert_snippet(&insertion_ranges, snippet, window, cx)
8292 .unwrap();
8293
8294 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
8295 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
8296 assert_eq!(editor.text(cx), expected_text);
8297 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
8298 }
8299
8300 assert(
8301 editor,
8302 cx,
8303 indoc! {"
8304 type «» =•
8305 "},
8306 );
8307
8308 assert!(editor.context_menu_visible(), "There should be a matches");
8309 });
8310}
8311
8312#[gpui::test]
8313async fn test_snippets(cx: &mut TestAppContext) {
8314 init_test(cx, |_| {});
8315
8316 let (text, insertion_ranges) = marked_text_ranges(
8317 indoc! {"
8318 a.ˇ b
8319 a.ˇ b
8320 a.ˇ b
8321 "},
8322 false,
8323 );
8324
8325 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
8326 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8327
8328 editor.update_in(cx, |editor, window, cx| {
8329 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
8330
8331 editor
8332 .insert_snippet(&insertion_ranges, snippet, window, cx)
8333 .unwrap();
8334
8335 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
8336 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
8337 assert_eq!(editor.text(cx), expected_text);
8338 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
8339 }
8340
8341 assert(
8342 editor,
8343 cx,
8344 indoc! {"
8345 a.f(«one», two, «three») b
8346 a.f(«one», two, «three») b
8347 a.f(«one», two, «three») b
8348 "},
8349 );
8350
8351 // Can't move earlier than the first tab stop
8352 assert!(!editor.move_to_prev_snippet_tabstop(window, cx));
8353 assert(
8354 editor,
8355 cx,
8356 indoc! {"
8357 a.f(«one», two, «three») b
8358 a.f(«one», two, «three») b
8359 a.f(«one», two, «three») b
8360 "},
8361 );
8362
8363 assert!(editor.move_to_next_snippet_tabstop(window, cx));
8364 assert(
8365 editor,
8366 cx,
8367 indoc! {"
8368 a.f(one, «two», three) b
8369 a.f(one, «two», three) b
8370 a.f(one, «two», three) b
8371 "},
8372 );
8373
8374 editor.move_to_prev_snippet_tabstop(window, cx);
8375 assert(
8376 editor,
8377 cx,
8378 indoc! {"
8379 a.f(«one», two, «three») b
8380 a.f(«one», two, «three») b
8381 a.f(«one», two, «three») b
8382 "},
8383 );
8384
8385 assert!(editor.move_to_next_snippet_tabstop(window, cx));
8386 assert(
8387 editor,
8388 cx,
8389 indoc! {"
8390 a.f(one, «two», three) b
8391 a.f(one, «two», three) b
8392 a.f(one, «two», three) b
8393 "},
8394 );
8395 assert!(editor.move_to_next_snippet_tabstop(window, cx));
8396 assert(
8397 editor,
8398 cx,
8399 indoc! {"
8400 a.f(one, two, three)ˇ b
8401 a.f(one, two, three)ˇ b
8402 a.f(one, two, three)ˇ b
8403 "},
8404 );
8405
8406 // As soon as the last tab stop is reached, snippet state is gone
8407 editor.move_to_prev_snippet_tabstop(window, cx);
8408 assert(
8409 editor,
8410 cx,
8411 indoc! {"
8412 a.f(one, two, three)ˇ b
8413 a.f(one, two, three)ˇ b
8414 a.f(one, two, three)ˇ b
8415 "},
8416 );
8417 });
8418}
8419
8420#[gpui::test]
8421async fn test_document_format_during_save(cx: &mut TestAppContext) {
8422 init_test(cx, |_| {});
8423
8424 let fs = FakeFs::new(cx.executor());
8425 fs.insert_file(path!("/file.rs"), Default::default()).await;
8426
8427 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
8428
8429 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8430 language_registry.add(rust_lang());
8431 let mut fake_servers = language_registry.register_fake_lsp(
8432 "Rust",
8433 FakeLspAdapter {
8434 capabilities: lsp::ServerCapabilities {
8435 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8436 ..Default::default()
8437 },
8438 ..Default::default()
8439 },
8440 );
8441
8442 let buffer = project
8443 .update(cx, |project, cx| {
8444 project.open_local_buffer(path!("/file.rs"), cx)
8445 })
8446 .await
8447 .unwrap();
8448
8449 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8450 let (editor, cx) = cx.add_window_view(|window, cx| {
8451 build_editor_with_project(project.clone(), buffer, window, cx)
8452 });
8453 editor.update_in(cx, |editor, window, cx| {
8454 editor.set_text("one\ntwo\nthree\n", window, cx)
8455 });
8456 assert!(cx.read(|cx| editor.is_dirty(cx)));
8457
8458 cx.executor().start_waiting();
8459 let fake_server = fake_servers.next().await.unwrap();
8460
8461 {
8462 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8463 move |params, _| async move {
8464 assert_eq!(
8465 params.text_document.uri,
8466 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8467 );
8468 assert_eq!(params.options.tab_size, 4);
8469 Ok(Some(vec![lsp::TextEdit::new(
8470 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8471 ", ".to_string(),
8472 )]))
8473 },
8474 );
8475 let save = editor
8476 .update_in(cx, |editor, window, cx| {
8477 editor.save(true, project.clone(), window, cx)
8478 })
8479 .unwrap();
8480 cx.executor().start_waiting();
8481 save.await;
8482
8483 assert_eq!(
8484 editor.update(cx, |editor, cx| editor.text(cx)),
8485 "one, two\nthree\n"
8486 );
8487 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8488 }
8489
8490 {
8491 editor.update_in(cx, |editor, window, cx| {
8492 editor.set_text("one\ntwo\nthree\n", window, cx)
8493 });
8494 assert!(cx.read(|cx| editor.is_dirty(cx)));
8495
8496 // Ensure we can still save even if formatting hangs.
8497 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8498 move |params, _| async move {
8499 assert_eq!(
8500 params.text_document.uri,
8501 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8502 );
8503 futures::future::pending::<()>().await;
8504 unreachable!()
8505 },
8506 );
8507 let save = editor
8508 .update_in(cx, |editor, window, cx| {
8509 editor.save(true, project.clone(), window, cx)
8510 })
8511 .unwrap();
8512 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8513 cx.executor().start_waiting();
8514 save.await;
8515 assert_eq!(
8516 editor.update(cx, |editor, cx| editor.text(cx)),
8517 "one\ntwo\nthree\n"
8518 );
8519 }
8520
8521 // For non-dirty buffer, no formatting request should be sent
8522 {
8523 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8524
8525 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
8526 panic!("Should not be invoked on non-dirty buffer");
8527 });
8528 let save = editor
8529 .update_in(cx, |editor, window, cx| {
8530 editor.save(true, project.clone(), window, cx)
8531 })
8532 .unwrap();
8533 cx.executor().start_waiting();
8534 save.await;
8535 }
8536
8537 // Set rust language override and assert overridden tabsize is sent to language server
8538 update_test_language_settings(cx, |settings| {
8539 settings.languages.insert(
8540 "Rust".into(),
8541 LanguageSettingsContent {
8542 tab_size: NonZeroU32::new(8),
8543 ..Default::default()
8544 },
8545 );
8546 });
8547
8548 {
8549 editor.update_in(cx, |editor, window, cx| {
8550 editor.set_text("somehting_new\n", window, cx)
8551 });
8552 assert!(cx.read(|cx| editor.is_dirty(cx)));
8553 let _formatting_request_signal = fake_server
8554 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
8555 assert_eq!(
8556 params.text_document.uri,
8557 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8558 );
8559 assert_eq!(params.options.tab_size, 8);
8560 Ok(Some(vec![]))
8561 });
8562 let save = editor
8563 .update_in(cx, |editor, window, cx| {
8564 editor.save(true, project.clone(), window, cx)
8565 })
8566 .unwrap();
8567 cx.executor().start_waiting();
8568 save.await;
8569 }
8570}
8571
8572#[gpui::test]
8573async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
8574 init_test(cx, |_| {});
8575
8576 let cols = 4;
8577 let rows = 10;
8578 let sample_text_1 = sample_text(rows, cols, 'a');
8579 assert_eq!(
8580 sample_text_1,
8581 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
8582 );
8583 let sample_text_2 = sample_text(rows, cols, 'l');
8584 assert_eq!(
8585 sample_text_2,
8586 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
8587 );
8588 let sample_text_3 = sample_text(rows, cols, 'v');
8589 assert_eq!(
8590 sample_text_3,
8591 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
8592 );
8593
8594 let fs = FakeFs::new(cx.executor());
8595 fs.insert_tree(
8596 path!("/a"),
8597 json!({
8598 "main.rs": sample_text_1,
8599 "other.rs": sample_text_2,
8600 "lib.rs": sample_text_3,
8601 }),
8602 )
8603 .await;
8604
8605 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
8606 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
8607 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
8608
8609 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8610 language_registry.add(rust_lang());
8611 let mut fake_servers = language_registry.register_fake_lsp(
8612 "Rust",
8613 FakeLspAdapter {
8614 capabilities: lsp::ServerCapabilities {
8615 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8616 ..Default::default()
8617 },
8618 ..Default::default()
8619 },
8620 );
8621
8622 let worktree = project.update(cx, |project, cx| {
8623 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
8624 assert_eq!(worktrees.len(), 1);
8625 worktrees.pop().unwrap()
8626 });
8627 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
8628
8629 let buffer_1 = project
8630 .update(cx, |project, cx| {
8631 project.open_buffer((worktree_id, "main.rs"), cx)
8632 })
8633 .await
8634 .unwrap();
8635 let buffer_2 = project
8636 .update(cx, |project, cx| {
8637 project.open_buffer((worktree_id, "other.rs"), cx)
8638 })
8639 .await
8640 .unwrap();
8641 let buffer_3 = project
8642 .update(cx, |project, cx| {
8643 project.open_buffer((worktree_id, "lib.rs"), cx)
8644 })
8645 .await
8646 .unwrap();
8647
8648 let multi_buffer = cx.new(|cx| {
8649 let mut multi_buffer = MultiBuffer::new(ReadWrite);
8650 multi_buffer.push_excerpts(
8651 buffer_1.clone(),
8652 [
8653 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8654 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8655 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8656 ],
8657 cx,
8658 );
8659 multi_buffer.push_excerpts(
8660 buffer_2.clone(),
8661 [
8662 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8663 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8664 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8665 ],
8666 cx,
8667 );
8668 multi_buffer.push_excerpts(
8669 buffer_3.clone(),
8670 [
8671 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8672 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8673 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8674 ],
8675 cx,
8676 );
8677 multi_buffer
8678 });
8679 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
8680 Editor::new(
8681 EditorMode::full(),
8682 multi_buffer,
8683 Some(project.clone()),
8684 window,
8685 cx,
8686 )
8687 });
8688
8689 multi_buffer_editor.update_in(cx, |editor, window, cx| {
8690 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
8691 s.select_ranges(Some(1..2))
8692 });
8693 editor.insert("|one|two|three|", window, cx);
8694 });
8695 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
8696 multi_buffer_editor.update_in(cx, |editor, window, cx| {
8697 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
8698 s.select_ranges(Some(60..70))
8699 });
8700 editor.insert("|four|five|six|", window, cx);
8701 });
8702 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
8703
8704 // First two buffers should be edited, but not the third one.
8705 assert_eq!(
8706 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
8707 "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}",
8708 );
8709 buffer_1.update(cx, |buffer, _| {
8710 assert!(buffer.is_dirty());
8711 assert_eq!(
8712 buffer.text(),
8713 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
8714 )
8715 });
8716 buffer_2.update(cx, |buffer, _| {
8717 assert!(buffer.is_dirty());
8718 assert_eq!(
8719 buffer.text(),
8720 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
8721 )
8722 });
8723 buffer_3.update(cx, |buffer, _| {
8724 assert!(!buffer.is_dirty());
8725 assert_eq!(buffer.text(), sample_text_3,)
8726 });
8727 cx.executor().run_until_parked();
8728
8729 cx.executor().start_waiting();
8730 let save = multi_buffer_editor
8731 .update_in(cx, |editor, window, cx| {
8732 editor.save(true, project.clone(), window, cx)
8733 })
8734 .unwrap();
8735
8736 let fake_server = fake_servers.next().await.unwrap();
8737 fake_server
8738 .server
8739 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
8740 Ok(Some(vec![lsp::TextEdit::new(
8741 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8742 format!("[{} formatted]", params.text_document.uri),
8743 )]))
8744 })
8745 .detach();
8746 save.await;
8747
8748 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
8749 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
8750 assert_eq!(
8751 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
8752 uri!(
8753 "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}"
8754 ),
8755 );
8756 buffer_1.update(cx, |buffer, _| {
8757 assert!(!buffer.is_dirty());
8758 assert_eq!(
8759 buffer.text(),
8760 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
8761 )
8762 });
8763 buffer_2.update(cx, |buffer, _| {
8764 assert!(!buffer.is_dirty());
8765 assert_eq!(
8766 buffer.text(),
8767 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
8768 )
8769 });
8770 buffer_3.update(cx, |buffer, _| {
8771 assert!(!buffer.is_dirty());
8772 assert_eq!(buffer.text(), sample_text_3,)
8773 });
8774}
8775
8776#[gpui::test]
8777async fn test_range_format_during_save(cx: &mut TestAppContext) {
8778 init_test(cx, |_| {});
8779
8780 let fs = FakeFs::new(cx.executor());
8781 fs.insert_file(path!("/file.rs"), Default::default()).await;
8782
8783 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8784
8785 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8786 language_registry.add(rust_lang());
8787 let mut fake_servers = language_registry.register_fake_lsp(
8788 "Rust",
8789 FakeLspAdapter {
8790 capabilities: lsp::ServerCapabilities {
8791 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
8792 ..Default::default()
8793 },
8794 ..Default::default()
8795 },
8796 );
8797
8798 let buffer = project
8799 .update(cx, |project, cx| {
8800 project.open_local_buffer(path!("/file.rs"), cx)
8801 })
8802 .await
8803 .unwrap();
8804
8805 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8806 let (editor, cx) = cx.add_window_view(|window, cx| {
8807 build_editor_with_project(project.clone(), buffer, window, cx)
8808 });
8809 editor.update_in(cx, |editor, window, cx| {
8810 editor.set_text("one\ntwo\nthree\n", window, cx)
8811 });
8812 assert!(cx.read(|cx| editor.is_dirty(cx)));
8813
8814 cx.executor().start_waiting();
8815 let fake_server = fake_servers.next().await.unwrap();
8816
8817 let save = editor
8818 .update_in(cx, |editor, window, cx| {
8819 editor.save(true, project.clone(), window, cx)
8820 })
8821 .unwrap();
8822 fake_server
8823 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
8824 assert_eq!(
8825 params.text_document.uri,
8826 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8827 );
8828 assert_eq!(params.options.tab_size, 4);
8829 Ok(Some(vec![lsp::TextEdit::new(
8830 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8831 ", ".to_string(),
8832 )]))
8833 })
8834 .next()
8835 .await;
8836 cx.executor().start_waiting();
8837 save.await;
8838 assert_eq!(
8839 editor.update(cx, |editor, cx| editor.text(cx)),
8840 "one, two\nthree\n"
8841 );
8842 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8843
8844 editor.update_in(cx, |editor, window, cx| {
8845 editor.set_text("one\ntwo\nthree\n", window, cx)
8846 });
8847 assert!(cx.read(|cx| editor.is_dirty(cx)));
8848
8849 // Ensure we can still save even if formatting hangs.
8850 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
8851 move |params, _| async move {
8852 assert_eq!(
8853 params.text_document.uri,
8854 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8855 );
8856 futures::future::pending::<()>().await;
8857 unreachable!()
8858 },
8859 );
8860 let save = editor
8861 .update_in(cx, |editor, window, cx| {
8862 editor.save(true, project.clone(), window, cx)
8863 })
8864 .unwrap();
8865 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8866 cx.executor().start_waiting();
8867 save.await;
8868 assert_eq!(
8869 editor.update(cx, |editor, cx| editor.text(cx)),
8870 "one\ntwo\nthree\n"
8871 );
8872 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8873
8874 // For non-dirty buffer, no formatting request should be sent
8875 let save = editor
8876 .update_in(cx, |editor, window, cx| {
8877 editor.save(true, project.clone(), window, cx)
8878 })
8879 .unwrap();
8880 let _pending_format_request = fake_server
8881 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
8882 panic!("Should not be invoked on non-dirty buffer");
8883 })
8884 .next();
8885 cx.executor().start_waiting();
8886 save.await;
8887
8888 // Set Rust language override and assert overridden tabsize is sent to language server
8889 update_test_language_settings(cx, |settings| {
8890 settings.languages.insert(
8891 "Rust".into(),
8892 LanguageSettingsContent {
8893 tab_size: NonZeroU32::new(8),
8894 ..Default::default()
8895 },
8896 );
8897 });
8898
8899 editor.update_in(cx, |editor, window, cx| {
8900 editor.set_text("somehting_new\n", window, cx)
8901 });
8902 assert!(cx.read(|cx| editor.is_dirty(cx)));
8903 let save = editor
8904 .update_in(cx, |editor, window, cx| {
8905 editor.save(true, project.clone(), window, cx)
8906 })
8907 .unwrap();
8908 fake_server
8909 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
8910 assert_eq!(
8911 params.text_document.uri,
8912 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8913 );
8914 assert_eq!(params.options.tab_size, 8);
8915 Ok(Some(vec![]))
8916 })
8917 .next()
8918 .await;
8919 cx.executor().start_waiting();
8920 save.await;
8921}
8922
8923#[gpui::test]
8924async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
8925 init_test(cx, |settings| {
8926 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
8927 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
8928 ))
8929 });
8930
8931 let fs = FakeFs::new(cx.executor());
8932 fs.insert_file(path!("/file.rs"), Default::default()).await;
8933
8934 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8935
8936 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8937 language_registry.add(Arc::new(Language::new(
8938 LanguageConfig {
8939 name: "Rust".into(),
8940 matcher: LanguageMatcher {
8941 path_suffixes: vec!["rs".to_string()],
8942 ..Default::default()
8943 },
8944 ..LanguageConfig::default()
8945 },
8946 Some(tree_sitter_rust::LANGUAGE.into()),
8947 )));
8948 update_test_language_settings(cx, |settings| {
8949 // Enable Prettier formatting for the same buffer, and ensure
8950 // LSP is called instead of Prettier.
8951 settings.defaults.prettier = Some(PrettierSettings {
8952 allowed: true,
8953 ..PrettierSettings::default()
8954 });
8955 });
8956 let mut fake_servers = language_registry.register_fake_lsp(
8957 "Rust",
8958 FakeLspAdapter {
8959 capabilities: lsp::ServerCapabilities {
8960 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8961 ..Default::default()
8962 },
8963 ..Default::default()
8964 },
8965 );
8966
8967 let buffer = project
8968 .update(cx, |project, cx| {
8969 project.open_local_buffer(path!("/file.rs"), cx)
8970 })
8971 .await
8972 .unwrap();
8973
8974 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8975 let (editor, cx) = cx.add_window_view(|window, cx| {
8976 build_editor_with_project(project.clone(), buffer, window, cx)
8977 });
8978 editor.update_in(cx, |editor, window, cx| {
8979 editor.set_text("one\ntwo\nthree\n", window, cx)
8980 });
8981
8982 cx.executor().start_waiting();
8983 let fake_server = fake_servers.next().await.unwrap();
8984
8985 let format = editor
8986 .update_in(cx, |editor, window, cx| {
8987 editor.perform_format(
8988 project.clone(),
8989 FormatTrigger::Manual,
8990 FormatTarget::Buffers,
8991 window,
8992 cx,
8993 )
8994 })
8995 .unwrap();
8996 fake_server
8997 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
8998 assert_eq!(
8999 params.text_document.uri,
9000 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9001 );
9002 assert_eq!(params.options.tab_size, 4);
9003 Ok(Some(vec![lsp::TextEdit::new(
9004 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9005 ", ".to_string(),
9006 )]))
9007 })
9008 .next()
9009 .await;
9010 cx.executor().start_waiting();
9011 format.await;
9012 assert_eq!(
9013 editor.update(cx, |editor, cx| editor.text(cx)),
9014 "one, two\nthree\n"
9015 );
9016
9017 editor.update_in(cx, |editor, window, cx| {
9018 editor.set_text("one\ntwo\nthree\n", window, cx)
9019 });
9020 // Ensure we don't lock if formatting hangs.
9021 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9022 move |params, _| async move {
9023 assert_eq!(
9024 params.text_document.uri,
9025 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9026 );
9027 futures::future::pending::<()>().await;
9028 unreachable!()
9029 },
9030 );
9031 let format = editor
9032 .update_in(cx, |editor, window, cx| {
9033 editor.perform_format(
9034 project,
9035 FormatTrigger::Manual,
9036 FormatTarget::Buffers,
9037 window,
9038 cx,
9039 )
9040 })
9041 .unwrap();
9042 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9043 cx.executor().start_waiting();
9044 format.await;
9045 assert_eq!(
9046 editor.update(cx, |editor, cx| editor.text(cx)),
9047 "one\ntwo\nthree\n"
9048 );
9049}
9050
9051#[gpui::test]
9052async fn test_multiple_formatters(cx: &mut TestAppContext) {
9053 init_test(cx, |settings| {
9054 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
9055 settings.defaults.formatter =
9056 Some(language_settings::SelectedFormatter::List(FormatterList(
9057 vec![
9058 Formatter::LanguageServer { name: None },
9059 Formatter::CodeActions(
9060 [
9061 ("code-action-1".into(), true),
9062 ("code-action-2".into(), true),
9063 ]
9064 .into_iter()
9065 .collect(),
9066 ),
9067 ]
9068 .into(),
9069 )))
9070 });
9071
9072 let fs = FakeFs::new(cx.executor());
9073 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
9074 .await;
9075
9076 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9077 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9078 language_registry.add(rust_lang());
9079
9080 let mut fake_servers = language_registry.register_fake_lsp(
9081 "Rust",
9082 FakeLspAdapter {
9083 capabilities: lsp::ServerCapabilities {
9084 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9085 execute_command_provider: Some(lsp::ExecuteCommandOptions {
9086 commands: vec!["the-command-for-code-action-1".into()],
9087 ..Default::default()
9088 }),
9089 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
9090 ..Default::default()
9091 },
9092 ..Default::default()
9093 },
9094 );
9095
9096 let buffer = project
9097 .update(cx, |project, cx| {
9098 project.open_local_buffer(path!("/file.rs"), cx)
9099 })
9100 .await
9101 .unwrap();
9102
9103 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9104 let (editor, cx) = cx.add_window_view(|window, cx| {
9105 build_editor_with_project(project.clone(), buffer, window, cx)
9106 });
9107
9108 cx.executor().start_waiting();
9109
9110 let fake_server = fake_servers.next().await.unwrap();
9111 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9112 move |_params, _| async move {
9113 Ok(Some(vec![lsp::TextEdit::new(
9114 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9115 "applied-formatting\n".to_string(),
9116 )]))
9117 },
9118 );
9119 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
9120 move |params, _| async move {
9121 assert_eq!(
9122 params.context.only,
9123 Some(vec!["code-action-1".into(), "code-action-2".into()])
9124 );
9125 let uri = lsp::Url::from_file_path(path!("/file.rs")).unwrap();
9126 Ok(Some(vec![
9127 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
9128 kind: Some("code-action-1".into()),
9129 edit: Some(lsp::WorkspaceEdit::new(
9130 [(
9131 uri.clone(),
9132 vec![lsp::TextEdit::new(
9133 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9134 "applied-code-action-1-edit\n".to_string(),
9135 )],
9136 )]
9137 .into_iter()
9138 .collect(),
9139 )),
9140 command: Some(lsp::Command {
9141 command: "the-command-for-code-action-1".into(),
9142 ..Default::default()
9143 }),
9144 ..Default::default()
9145 }),
9146 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
9147 kind: Some("code-action-2".into()),
9148 edit: Some(lsp::WorkspaceEdit::new(
9149 [(
9150 uri.clone(),
9151 vec![lsp::TextEdit::new(
9152 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9153 "applied-code-action-2-edit\n".to_string(),
9154 )],
9155 )]
9156 .into_iter()
9157 .collect(),
9158 )),
9159 ..Default::default()
9160 }),
9161 ]))
9162 },
9163 );
9164
9165 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
9166 move |params, _| async move { Ok(params) }
9167 });
9168
9169 let command_lock = Arc::new(futures::lock::Mutex::new(()));
9170 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
9171 let fake = fake_server.clone();
9172 let lock = command_lock.clone();
9173 move |params, _| {
9174 assert_eq!(params.command, "the-command-for-code-action-1");
9175 let fake = fake.clone();
9176 let lock = lock.clone();
9177 async move {
9178 lock.lock().await;
9179 fake.server
9180 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
9181 label: None,
9182 edit: lsp::WorkspaceEdit {
9183 changes: Some(
9184 [(
9185 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
9186 vec![lsp::TextEdit {
9187 range: lsp::Range::new(
9188 lsp::Position::new(0, 0),
9189 lsp::Position::new(0, 0),
9190 ),
9191 new_text: "applied-code-action-1-command\n".into(),
9192 }],
9193 )]
9194 .into_iter()
9195 .collect(),
9196 ),
9197 ..Default::default()
9198 },
9199 })
9200 .await
9201 .into_response()
9202 .unwrap();
9203 Ok(Some(json!(null)))
9204 }
9205 }
9206 });
9207
9208 cx.executor().start_waiting();
9209 editor
9210 .update_in(cx, |editor, window, cx| {
9211 editor.perform_format(
9212 project.clone(),
9213 FormatTrigger::Manual,
9214 FormatTarget::Buffers,
9215 window,
9216 cx,
9217 )
9218 })
9219 .unwrap()
9220 .await;
9221 editor.update(cx, |editor, cx| {
9222 assert_eq!(
9223 editor.text(cx),
9224 r#"
9225 applied-code-action-2-edit
9226 applied-code-action-1-command
9227 applied-code-action-1-edit
9228 applied-formatting
9229 one
9230 two
9231 three
9232 "#
9233 .unindent()
9234 );
9235 });
9236
9237 editor.update_in(cx, |editor, window, cx| {
9238 editor.undo(&Default::default(), window, cx);
9239 assert_eq!(editor.text(cx), "one \ntwo \nthree");
9240 });
9241
9242 // Perform a manual edit while waiting for an LSP command
9243 // that's being run as part of a formatting code action.
9244 let lock_guard = command_lock.lock().await;
9245 let format = editor
9246 .update_in(cx, |editor, window, cx| {
9247 editor.perform_format(
9248 project.clone(),
9249 FormatTrigger::Manual,
9250 FormatTarget::Buffers,
9251 window,
9252 cx,
9253 )
9254 })
9255 .unwrap();
9256 cx.run_until_parked();
9257 editor.update(cx, |editor, cx| {
9258 assert_eq!(
9259 editor.text(cx),
9260 r#"
9261 applied-code-action-1-edit
9262 applied-formatting
9263 one
9264 two
9265 three
9266 "#
9267 .unindent()
9268 );
9269
9270 editor.buffer.update(cx, |buffer, cx| {
9271 let ix = buffer.len(cx);
9272 buffer.edit([(ix..ix, "edited\n")], None, cx);
9273 });
9274 });
9275
9276 // Allow the LSP command to proceed. Because the buffer was edited,
9277 // the second code action will not be run.
9278 drop(lock_guard);
9279 format.await;
9280 editor.update_in(cx, |editor, window, cx| {
9281 assert_eq!(
9282 editor.text(cx),
9283 r#"
9284 applied-code-action-1-command
9285 applied-code-action-1-edit
9286 applied-formatting
9287 one
9288 two
9289 three
9290 edited
9291 "#
9292 .unindent()
9293 );
9294
9295 // The manual edit is undone first, because it is the last thing the user did
9296 // (even though the command completed afterwards).
9297 editor.undo(&Default::default(), window, cx);
9298 assert_eq!(
9299 editor.text(cx),
9300 r#"
9301 applied-code-action-1-command
9302 applied-code-action-1-edit
9303 applied-formatting
9304 one
9305 two
9306 three
9307 "#
9308 .unindent()
9309 );
9310
9311 // All the formatting (including the command, which completed after the manual edit)
9312 // is undone together.
9313 editor.undo(&Default::default(), window, cx);
9314 assert_eq!(editor.text(cx), "one \ntwo \nthree");
9315 });
9316}
9317
9318#[gpui::test]
9319async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
9320 init_test(cx, |settings| {
9321 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
9322 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
9323 ))
9324 });
9325
9326 let fs = FakeFs::new(cx.executor());
9327 fs.insert_file(path!("/file.ts"), Default::default()).await;
9328
9329 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9330
9331 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9332 language_registry.add(Arc::new(Language::new(
9333 LanguageConfig {
9334 name: "TypeScript".into(),
9335 matcher: LanguageMatcher {
9336 path_suffixes: vec!["ts".to_string()],
9337 ..Default::default()
9338 },
9339 ..LanguageConfig::default()
9340 },
9341 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
9342 )));
9343 update_test_language_settings(cx, |settings| {
9344 settings.defaults.prettier = Some(PrettierSettings {
9345 allowed: true,
9346 ..PrettierSettings::default()
9347 });
9348 });
9349 let mut fake_servers = language_registry.register_fake_lsp(
9350 "TypeScript",
9351 FakeLspAdapter {
9352 capabilities: lsp::ServerCapabilities {
9353 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
9354 ..Default::default()
9355 },
9356 ..Default::default()
9357 },
9358 );
9359
9360 let buffer = project
9361 .update(cx, |project, cx| {
9362 project.open_local_buffer(path!("/file.ts"), cx)
9363 })
9364 .await
9365 .unwrap();
9366
9367 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9368 let (editor, cx) = cx.add_window_view(|window, cx| {
9369 build_editor_with_project(project.clone(), buffer, window, cx)
9370 });
9371 editor.update_in(cx, |editor, window, cx| {
9372 editor.set_text(
9373 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
9374 window,
9375 cx,
9376 )
9377 });
9378
9379 cx.executor().start_waiting();
9380 let fake_server = fake_servers.next().await.unwrap();
9381
9382 let format = editor
9383 .update_in(cx, |editor, window, cx| {
9384 editor.perform_code_action_kind(
9385 project.clone(),
9386 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
9387 window,
9388 cx,
9389 )
9390 })
9391 .unwrap();
9392 fake_server
9393 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
9394 assert_eq!(
9395 params.text_document.uri,
9396 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
9397 );
9398 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
9399 lsp::CodeAction {
9400 title: "Organize Imports".to_string(),
9401 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
9402 edit: Some(lsp::WorkspaceEdit {
9403 changes: Some(
9404 [(
9405 params.text_document.uri.clone(),
9406 vec![lsp::TextEdit::new(
9407 lsp::Range::new(
9408 lsp::Position::new(1, 0),
9409 lsp::Position::new(2, 0),
9410 ),
9411 "".to_string(),
9412 )],
9413 )]
9414 .into_iter()
9415 .collect(),
9416 ),
9417 ..Default::default()
9418 }),
9419 ..Default::default()
9420 },
9421 )]))
9422 })
9423 .next()
9424 .await;
9425 cx.executor().start_waiting();
9426 format.await;
9427 assert_eq!(
9428 editor.update(cx, |editor, cx| editor.text(cx)),
9429 "import { a } from 'module';\n\nconst x = a;\n"
9430 );
9431
9432 editor.update_in(cx, |editor, window, cx| {
9433 editor.set_text(
9434 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
9435 window,
9436 cx,
9437 )
9438 });
9439 // Ensure we don't lock if code action hangs.
9440 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
9441 move |params, _| async move {
9442 assert_eq!(
9443 params.text_document.uri,
9444 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
9445 );
9446 futures::future::pending::<()>().await;
9447 unreachable!()
9448 },
9449 );
9450 let format = editor
9451 .update_in(cx, |editor, window, cx| {
9452 editor.perform_code_action_kind(
9453 project,
9454 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
9455 window,
9456 cx,
9457 )
9458 })
9459 .unwrap();
9460 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
9461 cx.executor().start_waiting();
9462 format.await;
9463 assert_eq!(
9464 editor.update(cx, |editor, cx| editor.text(cx)),
9465 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
9466 );
9467}
9468
9469#[gpui::test]
9470async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
9471 init_test(cx, |_| {});
9472
9473 let mut cx = EditorLspTestContext::new_rust(
9474 lsp::ServerCapabilities {
9475 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9476 ..Default::default()
9477 },
9478 cx,
9479 )
9480 .await;
9481
9482 cx.set_state(indoc! {"
9483 one.twoˇ
9484 "});
9485
9486 // The format request takes a long time. When it completes, it inserts
9487 // a newline and an indent before the `.`
9488 cx.lsp
9489 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
9490 let executor = cx.background_executor().clone();
9491 async move {
9492 executor.timer(Duration::from_millis(100)).await;
9493 Ok(Some(vec![lsp::TextEdit {
9494 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
9495 new_text: "\n ".into(),
9496 }]))
9497 }
9498 });
9499
9500 // Submit a format request.
9501 let format_1 = cx
9502 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
9503 .unwrap();
9504 cx.executor().run_until_parked();
9505
9506 // Submit a second format request.
9507 let format_2 = cx
9508 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
9509 .unwrap();
9510 cx.executor().run_until_parked();
9511
9512 // Wait for both format requests to complete
9513 cx.executor().advance_clock(Duration::from_millis(200));
9514 cx.executor().start_waiting();
9515 format_1.await.unwrap();
9516 cx.executor().start_waiting();
9517 format_2.await.unwrap();
9518
9519 // The formatting edits only happens once.
9520 cx.assert_editor_state(indoc! {"
9521 one
9522 .twoˇ
9523 "});
9524}
9525
9526#[gpui::test]
9527async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
9528 init_test(cx, |settings| {
9529 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
9530 });
9531
9532 let mut cx = EditorLspTestContext::new_rust(
9533 lsp::ServerCapabilities {
9534 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9535 ..Default::default()
9536 },
9537 cx,
9538 )
9539 .await;
9540
9541 // Set up a buffer white some trailing whitespace and no trailing newline.
9542 cx.set_state(
9543 &[
9544 "one ", //
9545 "twoˇ", //
9546 "three ", //
9547 "four", //
9548 ]
9549 .join("\n"),
9550 );
9551
9552 // Submit a format request.
9553 let format = cx
9554 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
9555 .unwrap();
9556
9557 // Record which buffer changes have been sent to the language server
9558 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
9559 cx.lsp
9560 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
9561 let buffer_changes = buffer_changes.clone();
9562 move |params, _| {
9563 buffer_changes.lock().extend(
9564 params
9565 .content_changes
9566 .into_iter()
9567 .map(|e| (e.range.unwrap(), e.text)),
9568 );
9569 }
9570 });
9571
9572 // Handle formatting requests to the language server.
9573 cx.lsp
9574 .set_request_handler::<lsp::request::Formatting, _, _>({
9575 let buffer_changes = buffer_changes.clone();
9576 move |_, _| {
9577 // When formatting is requested, trailing whitespace has already been stripped,
9578 // and the trailing newline has already been added.
9579 assert_eq!(
9580 &buffer_changes.lock()[1..],
9581 &[
9582 (
9583 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
9584 "".into()
9585 ),
9586 (
9587 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
9588 "".into()
9589 ),
9590 (
9591 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
9592 "\n".into()
9593 ),
9594 ]
9595 );
9596
9597 // Insert blank lines between each line of the buffer.
9598 async move {
9599 Ok(Some(vec![
9600 lsp::TextEdit {
9601 range: lsp::Range::new(
9602 lsp::Position::new(1, 0),
9603 lsp::Position::new(1, 0),
9604 ),
9605 new_text: "\n".into(),
9606 },
9607 lsp::TextEdit {
9608 range: lsp::Range::new(
9609 lsp::Position::new(2, 0),
9610 lsp::Position::new(2, 0),
9611 ),
9612 new_text: "\n".into(),
9613 },
9614 ]))
9615 }
9616 }
9617 });
9618
9619 // After formatting the buffer, the trailing whitespace is stripped,
9620 // a newline is appended, and the edits provided by the language server
9621 // have been applied.
9622 format.await.unwrap();
9623 cx.assert_editor_state(
9624 &[
9625 "one", //
9626 "", //
9627 "twoˇ", //
9628 "", //
9629 "three", //
9630 "four", //
9631 "", //
9632 ]
9633 .join("\n"),
9634 );
9635
9636 // Undoing the formatting undoes the trailing whitespace removal, the
9637 // trailing newline, and the LSP edits.
9638 cx.update_buffer(|buffer, cx| buffer.undo(cx));
9639 cx.assert_editor_state(
9640 &[
9641 "one ", //
9642 "twoˇ", //
9643 "three ", //
9644 "four", //
9645 ]
9646 .join("\n"),
9647 );
9648}
9649
9650#[gpui::test]
9651async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
9652 cx: &mut TestAppContext,
9653) {
9654 init_test(cx, |_| {});
9655
9656 cx.update(|cx| {
9657 cx.update_global::<SettingsStore, _>(|settings, cx| {
9658 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9659 settings.auto_signature_help = Some(true);
9660 });
9661 });
9662 });
9663
9664 let mut cx = EditorLspTestContext::new_rust(
9665 lsp::ServerCapabilities {
9666 signature_help_provider: Some(lsp::SignatureHelpOptions {
9667 ..Default::default()
9668 }),
9669 ..Default::default()
9670 },
9671 cx,
9672 )
9673 .await;
9674
9675 let language = Language::new(
9676 LanguageConfig {
9677 name: "Rust".into(),
9678 brackets: BracketPairConfig {
9679 pairs: vec![
9680 BracketPair {
9681 start: "{".to_string(),
9682 end: "}".to_string(),
9683 close: true,
9684 surround: true,
9685 newline: true,
9686 },
9687 BracketPair {
9688 start: "(".to_string(),
9689 end: ")".to_string(),
9690 close: true,
9691 surround: true,
9692 newline: true,
9693 },
9694 BracketPair {
9695 start: "/*".to_string(),
9696 end: " */".to_string(),
9697 close: true,
9698 surround: true,
9699 newline: true,
9700 },
9701 BracketPair {
9702 start: "[".to_string(),
9703 end: "]".to_string(),
9704 close: false,
9705 surround: false,
9706 newline: true,
9707 },
9708 BracketPair {
9709 start: "\"".to_string(),
9710 end: "\"".to_string(),
9711 close: true,
9712 surround: true,
9713 newline: false,
9714 },
9715 BracketPair {
9716 start: "<".to_string(),
9717 end: ">".to_string(),
9718 close: false,
9719 surround: true,
9720 newline: true,
9721 },
9722 ],
9723 ..Default::default()
9724 },
9725 autoclose_before: "})]".to_string(),
9726 ..Default::default()
9727 },
9728 Some(tree_sitter_rust::LANGUAGE.into()),
9729 );
9730 let language = Arc::new(language);
9731
9732 cx.language_registry().add(language.clone());
9733 cx.update_buffer(|buffer, cx| {
9734 buffer.set_language(Some(language), cx);
9735 });
9736
9737 cx.set_state(
9738 &r#"
9739 fn main() {
9740 sampleˇ
9741 }
9742 "#
9743 .unindent(),
9744 );
9745
9746 cx.update_editor(|editor, window, cx| {
9747 editor.handle_input("(", window, cx);
9748 });
9749 cx.assert_editor_state(
9750 &"
9751 fn main() {
9752 sample(ˇ)
9753 }
9754 "
9755 .unindent(),
9756 );
9757
9758 let mocked_response = lsp::SignatureHelp {
9759 signatures: vec![lsp::SignatureInformation {
9760 label: "fn sample(param1: u8, param2: u8)".to_string(),
9761 documentation: None,
9762 parameters: Some(vec![
9763 lsp::ParameterInformation {
9764 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9765 documentation: None,
9766 },
9767 lsp::ParameterInformation {
9768 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9769 documentation: None,
9770 },
9771 ]),
9772 active_parameter: None,
9773 }],
9774 active_signature: Some(0),
9775 active_parameter: Some(0),
9776 };
9777 handle_signature_help_request(&mut cx, mocked_response).await;
9778
9779 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9780 .await;
9781
9782 cx.editor(|editor, _, _| {
9783 let signature_help_state = editor.signature_help_state.popover().cloned();
9784 assert_eq!(
9785 signature_help_state.unwrap().label,
9786 "param1: u8, param2: u8"
9787 );
9788 });
9789}
9790
9791#[gpui::test]
9792async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
9793 init_test(cx, |_| {});
9794
9795 cx.update(|cx| {
9796 cx.update_global::<SettingsStore, _>(|settings, cx| {
9797 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9798 settings.auto_signature_help = Some(false);
9799 settings.show_signature_help_after_edits = Some(false);
9800 });
9801 });
9802 });
9803
9804 let mut cx = EditorLspTestContext::new_rust(
9805 lsp::ServerCapabilities {
9806 signature_help_provider: Some(lsp::SignatureHelpOptions {
9807 ..Default::default()
9808 }),
9809 ..Default::default()
9810 },
9811 cx,
9812 )
9813 .await;
9814
9815 let language = Language::new(
9816 LanguageConfig {
9817 name: "Rust".into(),
9818 brackets: BracketPairConfig {
9819 pairs: vec![
9820 BracketPair {
9821 start: "{".to_string(),
9822 end: "}".to_string(),
9823 close: true,
9824 surround: true,
9825 newline: true,
9826 },
9827 BracketPair {
9828 start: "(".to_string(),
9829 end: ")".to_string(),
9830 close: true,
9831 surround: true,
9832 newline: true,
9833 },
9834 BracketPair {
9835 start: "/*".to_string(),
9836 end: " */".to_string(),
9837 close: true,
9838 surround: true,
9839 newline: true,
9840 },
9841 BracketPair {
9842 start: "[".to_string(),
9843 end: "]".to_string(),
9844 close: false,
9845 surround: false,
9846 newline: true,
9847 },
9848 BracketPair {
9849 start: "\"".to_string(),
9850 end: "\"".to_string(),
9851 close: true,
9852 surround: true,
9853 newline: false,
9854 },
9855 BracketPair {
9856 start: "<".to_string(),
9857 end: ">".to_string(),
9858 close: false,
9859 surround: true,
9860 newline: true,
9861 },
9862 ],
9863 ..Default::default()
9864 },
9865 autoclose_before: "})]".to_string(),
9866 ..Default::default()
9867 },
9868 Some(tree_sitter_rust::LANGUAGE.into()),
9869 );
9870 let language = Arc::new(language);
9871
9872 cx.language_registry().add(language.clone());
9873 cx.update_buffer(|buffer, cx| {
9874 buffer.set_language(Some(language), cx);
9875 });
9876
9877 // Ensure that signature_help is not called when no signature help is enabled.
9878 cx.set_state(
9879 &r#"
9880 fn main() {
9881 sampleˇ
9882 }
9883 "#
9884 .unindent(),
9885 );
9886 cx.update_editor(|editor, window, cx| {
9887 editor.handle_input("(", window, cx);
9888 });
9889 cx.assert_editor_state(
9890 &"
9891 fn main() {
9892 sample(ˇ)
9893 }
9894 "
9895 .unindent(),
9896 );
9897 cx.editor(|editor, _, _| {
9898 assert!(editor.signature_help_state.task().is_none());
9899 });
9900
9901 let mocked_response = lsp::SignatureHelp {
9902 signatures: vec![lsp::SignatureInformation {
9903 label: "fn sample(param1: u8, param2: u8)".to_string(),
9904 documentation: None,
9905 parameters: Some(vec![
9906 lsp::ParameterInformation {
9907 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9908 documentation: None,
9909 },
9910 lsp::ParameterInformation {
9911 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9912 documentation: None,
9913 },
9914 ]),
9915 active_parameter: None,
9916 }],
9917 active_signature: Some(0),
9918 active_parameter: Some(0),
9919 };
9920
9921 // Ensure that signature_help is called when enabled afte edits
9922 cx.update(|_, cx| {
9923 cx.update_global::<SettingsStore, _>(|settings, cx| {
9924 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9925 settings.auto_signature_help = Some(false);
9926 settings.show_signature_help_after_edits = Some(true);
9927 });
9928 });
9929 });
9930 cx.set_state(
9931 &r#"
9932 fn main() {
9933 sampleˇ
9934 }
9935 "#
9936 .unindent(),
9937 );
9938 cx.update_editor(|editor, window, cx| {
9939 editor.handle_input("(", window, cx);
9940 });
9941 cx.assert_editor_state(
9942 &"
9943 fn main() {
9944 sample(ˇ)
9945 }
9946 "
9947 .unindent(),
9948 );
9949 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9950 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9951 .await;
9952 cx.update_editor(|editor, _, _| {
9953 let signature_help_state = editor.signature_help_state.popover().cloned();
9954 assert!(signature_help_state.is_some());
9955 assert_eq!(
9956 signature_help_state.unwrap().label,
9957 "param1: u8, param2: u8"
9958 );
9959 editor.signature_help_state = SignatureHelpState::default();
9960 });
9961
9962 // Ensure that signature_help is called when auto signature help override is enabled
9963 cx.update(|_, cx| {
9964 cx.update_global::<SettingsStore, _>(|settings, cx| {
9965 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9966 settings.auto_signature_help = Some(true);
9967 settings.show_signature_help_after_edits = Some(false);
9968 });
9969 });
9970 });
9971 cx.set_state(
9972 &r#"
9973 fn main() {
9974 sampleˇ
9975 }
9976 "#
9977 .unindent(),
9978 );
9979 cx.update_editor(|editor, window, cx| {
9980 editor.handle_input("(", window, cx);
9981 });
9982 cx.assert_editor_state(
9983 &"
9984 fn main() {
9985 sample(ˇ)
9986 }
9987 "
9988 .unindent(),
9989 );
9990 handle_signature_help_request(&mut cx, mocked_response).await;
9991 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9992 .await;
9993 cx.editor(|editor, _, _| {
9994 let signature_help_state = editor.signature_help_state.popover().cloned();
9995 assert!(signature_help_state.is_some());
9996 assert_eq!(
9997 signature_help_state.unwrap().label,
9998 "param1: u8, param2: u8"
9999 );
10000 });
10001}
10002
10003#[gpui::test]
10004async fn test_signature_help(cx: &mut TestAppContext) {
10005 init_test(cx, |_| {});
10006 cx.update(|cx| {
10007 cx.update_global::<SettingsStore, _>(|settings, cx| {
10008 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10009 settings.auto_signature_help = Some(true);
10010 });
10011 });
10012 });
10013
10014 let mut cx = EditorLspTestContext::new_rust(
10015 lsp::ServerCapabilities {
10016 signature_help_provider: Some(lsp::SignatureHelpOptions {
10017 ..Default::default()
10018 }),
10019 ..Default::default()
10020 },
10021 cx,
10022 )
10023 .await;
10024
10025 // A test that directly calls `show_signature_help`
10026 cx.update_editor(|editor, window, cx| {
10027 editor.show_signature_help(&ShowSignatureHelp, window, cx);
10028 });
10029
10030 let mocked_response = lsp::SignatureHelp {
10031 signatures: vec![lsp::SignatureInformation {
10032 label: "fn sample(param1: u8, param2: u8)".to_string(),
10033 documentation: None,
10034 parameters: Some(vec![
10035 lsp::ParameterInformation {
10036 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10037 documentation: None,
10038 },
10039 lsp::ParameterInformation {
10040 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10041 documentation: None,
10042 },
10043 ]),
10044 active_parameter: None,
10045 }],
10046 active_signature: Some(0),
10047 active_parameter: Some(0),
10048 };
10049 handle_signature_help_request(&mut cx, mocked_response).await;
10050
10051 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10052 .await;
10053
10054 cx.editor(|editor, _, _| {
10055 let signature_help_state = editor.signature_help_state.popover().cloned();
10056 assert!(signature_help_state.is_some());
10057 assert_eq!(
10058 signature_help_state.unwrap().label,
10059 "param1: u8, param2: u8"
10060 );
10061 });
10062
10063 // When exiting outside from inside the brackets, `signature_help` is closed.
10064 cx.set_state(indoc! {"
10065 fn main() {
10066 sample(ˇ);
10067 }
10068
10069 fn sample(param1: u8, param2: u8) {}
10070 "});
10071
10072 cx.update_editor(|editor, window, cx| {
10073 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
10074 });
10075
10076 let mocked_response = lsp::SignatureHelp {
10077 signatures: Vec::new(),
10078 active_signature: None,
10079 active_parameter: None,
10080 };
10081 handle_signature_help_request(&mut cx, mocked_response).await;
10082
10083 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
10084 .await;
10085
10086 cx.editor(|editor, _, _| {
10087 assert!(!editor.signature_help_state.is_shown());
10088 });
10089
10090 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
10091 cx.set_state(indoc! {"
10092 fn main() {
10093 sample(ˇ);
10094 }
10095
10096 fn sample(param1: u8, param2: u8) {}
10097 "});
10098
10099 let mocked_response = lsp::SignatureHelp {
10100 signatures: vec![lsp::SignatureInformation {
10101 label: "fn sample(param1: u8, param2: u8)".to_string(),
10102 documentation: None,
10103 parameters: Some(vec![
10104 lsp::ParameterInformation {
10105 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10106 documentation: None,
10107 },
10108 lsp::ParameterInformation {
10109 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10110 documentation: None,
10111 },
10112 ]),
10113 active_parameter: None,
10114 }],
10115 active_signature: Some(0),
10116 active_parameter: Some(0),
10117 };
10118 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10119 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10120 .await;
10121 cx.editor(|editor, _, _| {
10122 assert!(editor.signature_help_state.is_shown());
10123 });
10124
10125 // Restore the popover with more parameter input
10126 cx.set_state(indoc! {"
10127 fn main() {
10128 sample(param1, param2ˇ);
10129 }
10130
10131 fn sample(param1: u8, param2: u8) {}
10132 "});
10133
10134 let mocked_response = lsp::SignatureHelp {
10135 signatures: vec![lsp::SignatureInformation {
10136 label: "fn sample(param1: u8, param2: u8)".to_string(),
10137 documentation: None,
10138 parameters: Some(vec![
10139 lsp::ParameterInformation {
10140 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10141 documentation: None,
10142 },
10143 lsp::ParameterInformation {
10144 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10145 documentation: None,
10146 },
10147 ]),
10148 active_parameter: None,
10149 }],
10150 active_signature: Some(0),
10151 active_parameter: Some(1),
10152 };
10153 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10154 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10155 .await;
10156
10157 // When selecting a range, the popover is gone.
10158 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
10159 cx.update_editor(|editor, window, cx| {
10160 editor.change_selections(None, window, cx, |s| {
10161 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
10162 })
10163 });
10164 cx.assert_editor_state(indoc! {"
10165 fn main() {
10166 sample(param1, «ˇparam2»);
10167 }
10168
10169 fn sample(param1: u8, param2: u8) {}
10170 "});
10171 cx.editor(|editor, _, _| {
10172 assert!(!editor.signature_help_state.is_shown());
10173 });
10174
10175 // When unselecting again, the popover is back if within the brackets.
10176 cx.update_editor(|editor, window, cx| {
10177 editor.change_selections(None, window, cx, |s| {
10178 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
10179 })
10180 });
10181 cx.assert_editor_state(indoc! {"
10182 fn main() {
10183 sample(param1, ˇparam2);
10184 }
10185
10186 fn sample(param1: u8, param2: u8) {}
10187 "});
10188 handle_signature_help_request(&mut cx, mocked_response).await;
10189 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10190 .await;
10191 cx.editor(|editor, _, _| {
10192 assert!(editor.signature_help_state.is_shown());
10193 });
10194
10195 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
10196 cx.update_editor(|editor, window, cx| {
10197 editor.change_selections(None, window, cx, |s| {
10198 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
10199 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
10200 })
10201 });
10202 cx.assert_editor_state(indoc! {"
10203 fn main() {
10204 sample(param1, ˇparam2);
10205 }
10206
10207 fn sample(param1: u8, param2: u8) {}
10208 "});
10209
10210 let mocked_response = lsp::SignatureHelp {
10211 signatures: vec![lsp::SignatureInformation {
10212 label: "fn sample(param1: u8, param2: u8)".to_string(),
10213 documentation: None,
10214 parameters: Some(vec![
10215 lsp::ParameterInformation {
10216 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10217 documentation: None,
10218 },
10219 lsp::ParameterInformation {
10220 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10221 documentation: None,
10222 },
10223 ]),
10224 active_parameter: None,
10225 }],
10226 active_signature: Some(0),
10227 active_parameter: Some(1),
10228 };
10229 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10230 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10231 .await;
10232 cx.update_editor(|editor, _, cx| {
10233 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
10234 });
10235 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
10236 .await;
10237 cx.update_editor(|editor, window, cx| {
10238 editor.change_selections(None, window, cx, |s| {
10239 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
10240 })
10241 });
10242 cx.assert_editor_state(indoc! {"
10243 fn main() {
10244 sample(param1, «ˇparam2»);
10245 }
10246
10247 fn sample(param1: u8, param2: u8) {}
10248 "});
10249 cx.update_editor(|editor, window, cx| {
10250 editor.change_selections(None, window, cx, |s| {
10251 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
10252 })
10253 });
10254 cx.assert_editor_state(indoc! {"
10255 fn main() {
10256 sample(param1, ˇparam2);
10257 }
10258
10259 fn sample(param1: u8, param2: u8) {}
10260 "});
10261 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
10262 .await;
10263}
10264
10265#[gpui::test]
10266async fn test_completion_mode(cx: &mut TestAppContext) {
10267 init_test(cx, |_| {});
10268 let mut cx = EditorLspTestContext::new_rust(
10269 lsp::ServerCapabilities {
10270 completion_provider: Some(lsp::CompletionOptions {
10271 resolve_provider: Some(true),
10272 ..Default::default()
10273 }),
10274 ..Default::default()
10275 },
10276 cx,
10277 )
10278 .await;
10279
10280 struct Run {
10281 run_description: &'static str,
10282 initial_state: String,
10283 buffer_marked_text: String,
10284 completion_text: &'static str,
10285 expected_with_insert_mode: String,
10286 expected_with_replace_mode: String,
10287 expected_with_replace_subsequence_mode: String,
10288 expected_with_replace_suffix_mode: String,
10289 }
10290
10291 let runs = [
10292 Run {
10293 run_description: "Start of word matches completion text",
10294 initial_state: "before ediˇ after".into(),
10295 buffer_marked_text: "before <edi|> after".into(),
10296 completion_text: "editor",
10297 expected_with_insert_mode: "before editorˇ after".into(),
10298 expected_with_replace_mode: "before editorˇ after".into(),
10299 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10300 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10301 },
10302 Run {
10303 run_description: "Accept same text at the middle of the word",
10304 initial_state: "before ediˇtor after".into(),
10305 buffer_marked_text: "before <edi|tor> after".into(),
10306 completion_text: "editor",
10307 expected_with_insert_mode: "before editorˇtor after".into(),
10308 expected_with_replace_mode: "before editorˇ after".into(),
10309 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10310 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10311 },
10312 Run {
10313 run_description: "End of word matches completion text -- cursor at end",
10314 initial_state: "before torˇ after".into(),
10315 buffer_marked_text: "before <tor|> after".into(),
10316 completion_text: "editor",
10317 expected_with_insert_mode: "before editorˇ after".into(),
10318 expected_with_replace_mode: "before editorˇ after".into(),
10319 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10320 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10321 },
10322 Run {
10323 run_description: "End of word matches completion text -- cursor at start",
10324 initial_state: "before ˇtor after".into(),
10325 buffer_marked_text: "before <|tor> after".into(),
10326 completion_text: "editor",
10327 expected_with_insert_mode: "before editorˇtor after".into(),
10328 expected_with_replace_mode: "before editorˇ after".into(),
10329 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10330 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10331 },
10332 Run {
10333 run_description: "Prepend text containing whitespace",
10334 initial_state: "pˇfield: bool".into(),
10335 buffer_marked_text: "<p|field>: bool".into(),
10336 completion_text: "pub ",
10337 expected_with_insert_mode: "pub ˇfield: bool".into(),
10338 expected_with_replace_mode: "pub ˇ: bool".into(),
10339 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
10340 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
10341 },
10342 Run {
10343 run_description: "Add element to start of list",
10344 initial_state: "[element_ˇelement_2]".into(),
10345 buffer_marked_text: "[<element_|element_2>]".into(),
10346 completion_text: "element_1",
10347 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
10348 expected_with_replace_mode: "[element_1ˇ]".into(),
10349 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
10350 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
10351 },
10352 Run {
10353 run_description: "Add element to start of list -- first and second elements are equal",
10354 initial_state: "[elˇelement]".into(),
10355 buffer_marked_text: "[<el|element>]".into(),
10356 completion_text: "element",
10357 expected_with_insert_mode: "[elementˇelement]".into(),
10358 expected_with_replace_mode: "[elementˇ]".into(),
10359 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
10360 expected_with_replace_suffix_mode: "[elementˇ]".into(),
10361 },
10362 Run {
10363 run_description: "Ends with matching suffix",
10364 initial_state: "SubˇError".into(),
10365 buffer_marked_text: "<Sub|Error>".into(),
10366 completion_text: "SubscriptionError",
10367 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
10368 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10369 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10370 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
10371 },
10372 Run {
10373 run_description: "Suffix is a subsequence -- contiguous",
10374 initial_state: "SubˇErr".into(),
10375 buffer_marked_text: "<Sub|Err>".into(),
10376 completion_text: "SubscriptionError",
10377 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
10378 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10379 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10380 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
10381 },
10382 Run {
10383 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
10384 initial_state: "Suˇscrirr".into(),
10385 buffer_marked_text: "<Su|scrirr>".into(),
10386 completion_text: "SubscriptionError",
10387 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
10388 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10389 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10390 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
10391 },
10392 Run {
10393 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
10394 initial_state: "foo(indˇix)".into(),
10395 buffer_marked_text: "foo(<ind|ix>)".into(),
10396 completion_text: "node_index",
10397 expected_with_insert_mode: "foo(node_indexˇix)".into(),
10398 expected_with_replace_mode: "foo(node_indexˇ)".into(),
10399 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
10400 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
10401 },
10402 ];
10403
10404 for run in runs {
10405 let run_variations = [
10406 (LspInsertMode::Insert, run.expected_with_insert_mode),
10407 (LspInsertMode::Replace, run.expected_with_replace_mode),
10408 (
10409 LspInsertMode::ReplaceSubsequence,
10410 run.expected_with_replace_subsequence_mode,
10411 ),
10412 (
10413 LspInsertMode::ReplaceSuffix,
10414 run.expected_with_replace_suffix_mode,
10415 ),
10416 ];
10417
10418 for (lsp_insert_mode, expected_text) in run_variations {
10419 eprintln!(
10420 "run = {:?}, mode = {lsp_insert_mode:.?}",
10421 run.run_description,
10422 );
10423
10424 update_test_language_settings(&mut cx, |settings| {
10425 settings.defaults.completions = Some(CompletionSettings {
10426 lsp_insert_mode,
10427 words: WordsCompletionMode::Disabled,
10428 lsp: true,
10429 lsp_fetch_timeout_ms: 0,
10430 });
10431 });
10432
10433 cx.set_state(&run.initial_state);
10434 cx.update_editor(|editor, window, cx| {
10435 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10436 });
10437
10438 let counter = Arc::new(AtomicUsize::new(0));
10439 handle_completion_request_with_insert_and_replace(
10440 &mut cx,
10441 &run.buffer_marked_text,
10442 vec![run.completion_text],
10443 counter.clone(),
10444 )
10445 .await;
10446 cx.condition(|editor, _| editor.context_menu_visible())
10447 .await;
10448 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
10449
10450 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10451 editor
10452 .confirm_completion(&ConfirmCompletion::default(), window, cx)
10453 .unwrap()
10454 });
10455 cx.assert_editor_state(&expected_text);
10456 handle_resolve_completion_request(&mut cx, None).await;
10457 apply_additional_edits.await.unwrap();
10458 }
10459 }
10460}
10461
10462#[gpui::test]
10463async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
10464 init_test(cx, |_| {});
10465 let mut cx = EditorLspTestContext::new_rust(
10466 lsp::ServerCapabilities {
10467 completion_provider: Some(lsp::CompletionOptions {
10468 resolve_provider: Some(true),
10469 ..Default::default()
10470 }),
10471 ..Default::default()
10472 },
10473 cx,
10474 )
10475 .await;
10476
10477 let initial_state = "SubˇError";
10478 let buffer_marked_text = "<Sub|Error>";
10479 let completion_text = "SubscriptionError";
10480 let expected_with_insert_mode = "SubscriptionErrorˇError";
10481 let expected_with_replace_mode = "SubscriptionErrorˇ";
10482
10483 update_test_language_settings(&mut cx, |settings| {
10484 settings.defaults.completions = Some(CompletionSettings {
10485 words: WordsCompletionMode::Disabled,
10486 // set the opposite here to ensure that the action is overriding the default behavior
10487 lsp_insert_mode: LspInsertMode::Insert,
10488 lsp: true,
10489 lsp_fetch_timeout_ms: 0,
10490 });
10491 });
10492
10493 cx.set_state(initial_state);
10494 cx.update_editor(|editor, window, cx| {
10495 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10496 });
10497
10498 let counter = Arc::new(AtomicUsize::new(0));
10499 handle_completion_request_with_insert_and_replace(
10500 &mut cx,
10501 &buffer_marked_text,
10502 vec![completion_text],
10503 counter.clone(),
10504 )
10505 .await;
10506 cx.condition(|editor, _| editor.context_menu_visible())
10507 .await;
10508 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
10509
10510 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10511 editor
10512 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10513 .unwrap()
10514 });
10515 cx.assert_editor_state(&expected_with_replace_mode);
10516 handle_resolve_completion_request(&mut cx, None).await;
10517 apply_additional_edits.await.unwrap();
10518
10519 update_test_language_settings(&mut cx, |settings| {
10520 settings.defaults.completions = Some(CompletionSettings {
10521 words: WordsCompletionMode::Disabled,
10522 // set the opposite here to ensure that the action is overriding the default behavior
10523 lsp_insert_mode: LspInsertMode::Replace,
10524 lsp: true,
10525 lsp_fetch_timeout_ms: 0,
10526 });
10527 });
10528
10529 cx.set_state(initial_state);
10530 cx.update_editor(|editor, window, cx| {
10531 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10532 });
10533 handle_completion_request_with_insert_and_replace(
10534 &mut cx,
10535 &buffer_marked_text,
10536 vec![completion_text],
10537 counter.clone(),
10538 )
10539 .await;
10540 cx.condition(|editor, _| editor.context_menu_visible())
10541 .await;
10542 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
10543
10544 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10545 editor
10546 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
10547 .unwrap()
10548 });
10549 cx.assert_editor_state(&expected_with_insert_mode);
10550 handle_resolve_completion_request(&mut cx, None).await;
10551 apply_additional_edits.await.unwrap();
10552}
10553
10554#[gpui::test]
10555async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
10556 init_test(cx, |_| {});
10557 let mut cx = EditorLspTestContext::new_rust(
10558 lsp::ServerCapabilities {
10559 completion_provider: Some(lsp::CompletionOptions {
10560 resolve_provider: Some(true),
10561 ..Default::default()
10562 }),
10563 ..Default::default()
10564 },
10565 cx,
10566 )
10567 .await;
10568
10569 // scenario: surrounding text matches completion text
10570 let completion_text = "to_offset";
10571 let initial_state = indoc! {"
10572 1. buf.to_offˇsuffix
10573 2. buf.to_offˇsuf
10574 3. buf.to_offˇfix
10575 4. buf.to_offˇ
10576 5. into_offˇensive
10577 6. ˇsuffix
10578 7. let ˇ //
10579 8. aaˇzz
10580 9. buf.to_off«zzzzzˇ»suffix
10581 10. buf.«ˇzzzzz»suffix
10582 11. to_off«ˇzzzzz»
10583
10584 buf.to_offˇsuffix // newest cursor
10585 "};
10586 let completion_marked_buffer = indoc! {"
10587 1. buf.to_offsuffix
10588 2. buf.to_offsuf
10589 3. buf.to_offfix
10590 4. buf.to_off
10591 5. into_offensive
10592 6. suffix
10593 7. let //
10594 8. aazz
10595 9. buf.to_offzzzzzsuffix
10596 10. buf.zzzzzsuffix
10597 11. to_offzzzzz
10598
10599 buf.<to_off|suffix> // newest cursor
10600 "};
10601 let expected = indoc! {"
10602 1. buf.to_offsetˇ
10603 2. buf.to_offsetˇsuf
10604 3. buf.to_offsetˇfix
10605 4. buf.to_offsetˇ
10606 5. into_offsetˇensive
10607 6. to_offsetˇsuffix
10608 7. let to_offsetˇ //
10609 8. aato_offsetˇzz
10610 9. buf.to_offsetˇ
10611 10. buf.to_offsetˇsuffix
10612 11. to_offsetˇ
10613
10614 buf.to_offsetˇ // newest cursor
10615 "};
10616 cx.set_state(initial_state);
10617 cx.update_editor(|editor, window, cx| {
10618 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10619 });
10620 handle_completion_request_with_insert_and_replace(
10621 &mut cx,
10622 completion_marked_buffer,
10623 vec![completion_text],
10624 Arc::new(AtomicUsize::new(0)),
10625 )
10626 .await;
10627 cx.condition(|editor, _| editor.context_menu_visible())
10628 .await;
10629 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10630 editor
10631 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10632 .unwrap()
10633 });
10634 cx.assert_editor_state(expected);
10635 handle_resolve_completion_request(&mut cx, None).await;
10636 apply_additional_edits.await.unwrap();
10637
10638 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
10639 let completion_text = "foo_and_bar";
10640 let initial_state = indoc! {"
10641 1. ooanbˇ
10642 2. zooanbˇ
10643 3. ooanbˇz
10644 4. zooanbˇz
10645 5. ooanˇ
10646 6. oanbˇ
10647
10648 ooanbˇ
10649 "};
10650 let completion_marked_buffer = indoc! {"
10651 1. ooanb
10652 2. zooanb
10653 3. ooanbz
10654 4. zooanbz
10655 5. ooan
10656 6. oanb
10657
10658 <ooanb|>
10659 "};
10660 let expected = indoc! {"
10661 1. foo_and_barˇ
10662 2. zfoo_and_barˇ
10663 3. foo_and_barˇz
10664 4. zfoo_and_barˇz
10665 5. ooanfoo_and_barˇ
10666 6. oanbfoo_and_barˇ
10667
10668 foo_and_barˇ
10669 "};
10670 cx.set_state(initial_state);
10671 cx.update_editor(|editor, window, cx| {
10672 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10673 });
10674 handle_completion_request_with_insert_and_replace(
10675 &mut cx,
10676 completion_marked_buffer,
10677 vec![completion_text],
10678 Arc::new(AtomicUsize::new(0)),
10679 )
10680 .await;
10681 cx.condition(|editor, _| editor.context_menu_visible())
10682 .await;
10683 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10684 editor
10685 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10686 .unwrap()
10687 });
10688 cx.assert_editor_state(expected);
10689 handle_resolve_completion_request(&mut cx, None).await;
10690 apply_additional_edits.await.unwrap();
10691
10692 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
10693 // (expects the same as if it was inserted at the end)
10694 let completion_text = "foo_and_bar";
10695 let initial_state = indoc! {"
10696 1. ooˇanb
10697 2. zooˇanb
10698 3. ooˇanbz
10699 4. zooˇanbz
10700
10701 ooˇanb
10702 "};
10703 let completion_marked_buffer = indoc! {"
10704 1. ooanb
10705 2. zooanb
10706 3. ooanbz
10707 4. zooanbz
10708
10709 <oo|anb>
10710 "};
10711 let expected = indoc! {"
10712 1. foo_and_barˇ
10713 2. zfoo_and_barˇ
10714 3. foo_and_barˇz
10715 4. zfoo_and_barˇz
10716
10717 foo_and_barˇ
10718 "};
10719 cx.set_state(initial_state);
10720 cx.update_editor(|editor, window, cx| {
10721 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10722 });
10723 handle_completion_request_with_insert_and_replace(
10724 &mut cx,
10725 completion_marked_buffer,
10726 vec![completion_text],
10727 Arc::new(AtomicUsize::new(0)),
10728 )
10729 .await;
10730 cx.condition(|editor, _| editor.context_menu_visible())
10731 .await;
10732 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10733 editor
10734 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10735 .unwrap()
10736 });
10737 cx.assert_editor_state(expected);
10738 handle_resolve_completion_request(&mut cx, None).await;
10739 apply_additional_edits.await.unwrap();
10740}
10741
10742// This used to crash
10743#[gpui::test]
10744async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
10745 init_test(cx, |_| {});
10746
10747 let buffer_text = indoc! {"
10748 fn main() {
10749 10.satu;
10750
10751 //
10752 // separate cursors so they open in different excerpts (manually reproducible)
10753 //
10754
10755 10.satu20;
10756 }
10757 "};
10758 let multibuffer_text_with_selections = indoc! {"
10759 fn main() {
10760 10.satuˇ;
10761
10762 //
10763
10764 //
10765
10766 10.satuˇ20;
10767 }
10768 "};
10769 let expected_multibuffer = indoc! {"
10770 fn main() {
10771 10.saturating_sub()ˇ;
10772
10773 //
10774
10775 //
10776
10777 10.saturating_sub()ˇ;
10778 }
10779 "};
10780
10781 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
10782 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
10783
10784 let fs = FakeFs::new(cx.executor());
10785 fs.insert_tree(
10786 path!("/a"),
10787 json!({
10788 "main.rs": buffer_text,
10789 }),
10790 )
10791 .await;
10792
10793 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
10794 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10795 language_registry.add(rust_lang());
10796 let mut fake_servers = language_registry.register_fake_lsp(
10797 "Rust",
10798 FakeLspAdapter {
10799 capabilities: lsp::ServerCapabilities {
10800 completion_provider: Some(lsp::CompletionOptions {
10801 resolve_provider: None,
10802 ..lsp::CompletionOptions::default()
10803 }),
10804 ..lsp::ServerCapabilities::default()
10805 },
10806 ..FakeLspAdapter::default()
10807 },
10808 );
10809 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
10810 let cx = &mut VisualTestContext::from_window(*workspace, cx);
10811 let buffer = project
10812 .update(cx, |project, cx| {
10813 project.open_local_buffer(path!("/a/main.rs"), cx)
10814 })
10815 .await
10816 .unwrap();
10817
10818 let multi_buffer = cx.new(|cx| {
10819 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
10820 multi_buffer.push_excerpts(
10821 buffer.clone(),
10822 [ExcerptRange::new(0..first_excerpt_end)],
10823 cx,
10824 );
10825 multi_buffer.push_excerpts(
10826 buffer.clone(),
10827 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
10828 cx,
10829 );
10830 multi_buffer
10831 });
10832
10833 let editor = workspace
10834 .update(cx, |_, window, cx| {
10835 cx.new(|cx| {
10836 Editor::new(
10837 EditorMode::Full {
10838 scale_ui_elements_with_buffer_font_size: false,
10839 show_active_line_background: false,
10840 sized_by_content: false,
10841 },
10842 multi_buffer.clone(),
10843 Some(project.clone()),
10844 window,
10845 cx,
10846 )
10847 })
10848 })
10849 .unwrap();
10850
10851 let pane = workspace
10852 .update(cx, |workspace, _, _| workspace.active_pane().clone())
10853 .unwrap();
10854 pane.update_in(cx, |pane, window, cx| {
10855 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
10856 });
10857
10858 let fake_server = fake_servers.next().await.unwrap();
10859
10860 editor.update_in(cx, |editor, window, cx| {
10861 editor.change_selections(None, window, cx, |s| {
10862 s.select_ranges([
10863 Point::new(1, 11)..Point::new(1, 11),
10864 Point::new(7, 11)..Point::new(7, 11),
10865 ])
10866 });
10867
10868 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
10869 });
10870
10871 editor.update_in(cx, |editor, window, cx| {
10872 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10873 });
10874
10875 fake_server
10876 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
10877 let completion_item = lsp::CompletionItem {
10878 label: "saturating_sub()".into(),
10879 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
10880 lsp::InsertReplaceEdit {
10881 new_text: "saturating_sub()".to_owned(),
10882 insert: lsp::Range::new(
10883 lsp::Position::new(7, 7),
10884 lsp::Position::new(7, 11),
10885 ),
10886 replace: lsp::Range::new(
10887 lsp::Position::new(7, 7),
10888 lsp::Position::new(7, 13),
10889 ),
10890 },
10891 )),
10892 ..lsp::CompletionItem::default()
10893 };
10894
10895 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
10896 })
10897 .next()
10898 .await
10899 .unwrap();
10900
10901 cx.condition(&editor, |editor, _| editor.context_menu_visible())
10902 .await;
10903
10904 editor
10905 .update_in(cx, |editor, window, cx| {
10906 editor
10907 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10908 .unwrap()
10909 })
10910 .await
10911 .unwrap();
10912
10913 editor.update(cx, |editor, cx| {
10914 assert_text_with_selections(editor, expected_multibuffer, cx);
10915 })
10916}
10917
10918#[gpui::test]
10919async fn test_completion(cx: &mut TestAppContext) {
10920 init_test(cx, |_| {});
10921
10922 let mut cx = EditorLspTestContext::new_rust(
10923 lsp::ServerCapabilities {
10924 completion_provider: Some(lsp::CompletionOptions {
10925 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
10926 resolve_provider: Some(true),
10927 ..Default::default()
10928 }),
10929 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
10930 ..Default::default()
10931 },
10932 cx,
10933 )
10934 .await;
10935 let counter = Arc::new(AtomicUsize::new(0));
10936
10937 cx.set_state(indoc! {"
10938 oneˇ
10939 two
10940 three
10941 "});
10942 cx.simulate_keystroke(".");
10943 handle_completion_request(
10944 &mut cx,
10945 indoc! {"
10946 one.|<>
10947 two
10948 three
10949 "},
10950 vec!["first_completion", "second_completion"],
10951 counter.clone(),
10952 )
10953 .await;
10954 cx.condition(|editor, _| editor.context_menu_visible())
10955 .await;
10956 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
10957
10958 let _handler = handle_signature_help_request(
10959 &mut cx,
10960 lsp::SignatureHelp {
10961 signatures: vec![lsp::SignatureInformation {
10962 label: "test signature".to_string(),
10963 documentation: None,
10964 parameters: Some(vec![lsp::ParameterInformation {
10965 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
10966 documentation: None,
10967 }]),
10968 active_parameter: None,
10969 }],
10970 active_signature: None,
10971 active_parameter: None,
10972 },
10973 );
10974 cx.update_editor(|editor, window, cx| {
10975 assert!(
10976 !editor.signature_help_state.is_shown(),
10977 "No signature help was called for"
10978 );
10979 editor.show_signature_help(&ShowSignatureHelp, window, cx);
10980 });
10981 cx.run_until_parked();
10982 cx.update_editor(|editor, _, _| {
10983 assert!(
10984 !editor.signature_help_state.is_shown(),
10985 "No signature help should be shown when completions menu is open"
10986 );
10987 });
10988
10989 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10990 editor.context_menu_next(&Default::default(), window, cx);
10991 editor
10992 .confirm_completion(&ConfirmCompletion::default(), window, cx)
10993 .unwrap()
10994 });
10995 cx.assert_editor_state(indoc! {"
10996 one.second_completionˇ
10997 two
10998 three
10999 "});
11000
11001 handle_resolve_completion_request(
11002 &mut cx,
11003 Some(vec![
11004 (
11005 //This overlaps with the primary completion edit which is
11006 //misbehavior from the LSP spec, test that we filter it out
11007 indoc! {"
11008 one.second_ˇcompletion
11009 two
11010 threeˇ
11011 "},
11012 "overlapping additional edit",
11013 ),
11014 (
11015 indoc! {"
11016 one.second_completion
11017 two
11018 threeˇ
11019 "},
11020 "\nadditional edit",
11021 ),
11022 ]),
11023 )
11024 .await;
11025 apply_additional_edits.await.unwrap();
11026 cx.assert_editor_state(indoc! {"
11027 one.second_completionˇ
11028 two
11029 three
11030 additional edit
11031 "});
11032
11033 cx.set_state(indoc! {"
11034 one.second_completion
11035 twoˇ
11036 threeˇ
11037 additional edit
11038 "});
11039 cx.simulate_keystroke(" ");
11040 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11041 cx.simulate_keystroke("s");
11042 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11043
11044 cx.assert_editor_state(indoc! {"
11045 one.second_completion
11046 two sˇ
11047 three sˇ
11048 additional edit
11049 "});
11050 handle_completion_request(
11051 &mut cx,
11052 indoc! {"
11053 one.second_completion
11054 two s
11055 three <s|>
11056 additional edit
11057 "},
11058 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
11059 counter.clone(),
11060 )
11061 .await;
11062 cx.condition(|editor, _| editor.context_menu_visible())
11063 .await;
11064 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
11065
11066 cx.simulate_keystroke("i");
11067
11068 handle_completion_request(
11069 &mut cx,
11070 indoc! {"
11071 one.second_completion
11072 two si
11073 three <si|>
11074 additional edit
11075 "},
11076 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
11077 counter.clone(),
11078 )
11079 .await;
11080 cx.condition(|editor, _| editor.context_menu_visible())
11081 .await;
11082 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
11083
11084 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11085 editor
11086 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11087 .unwrap()
11088 });
11089 cx.assert_editor_state(indoc! {"
11090 one.second_completion
11091 two sixth_completionˇ
11092 three sixth_completionˇ
11093 additional edit
11094 "});
11095
11096 apply_additional_edits.await.unwrap();
11097
11098 update_test_language_settings(&mut cx, |settings| {
11099 settings.defaults.show_completions_on_input = Some(false);
11100 });
11101 cx.set_state("editorˇ");
11102 cx.simulate_keystroke(".");
11103 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11104 cx.simulate_keystrokes("c l o");
11105 cx.assert_editor_state("editor.cloˇ");
11106 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11107 cx.update_editor(|editor, window, cx| {
11108 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11109 });
11110 handle_completion_request(
11111 &mut cx,
11112 "editor.<clo|>",
11113 vec!["close", "clobber"],
11114 counter.clone(),
11115 )
11116 .await;
11117 cx.condition(|editor, _| editor.context_menu_visible())
11118 .await;
11119 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
11120
11121 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11122 editor
11123 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11124 .unwrap()
11125 });
11126 cx.assert_editor_state("editor.closeˇ");
11127 handle_resolve_completion_request(&mut cx, None).await;
11128 apply_additional_edits.await.unwrap();
11129}
11130
11131#[gpui::test]
11132async fn test_word_completion(cx: &mut TestAppContext) {
11133 let lsp_fetch_timeout_ms = 10;
11134 init_test(cx, |language_settings| {
11135 language_settings.defaults.completions = Some(CompletionSettings {
11136 words: WordsCompletionMode::Fallback,
11137 lsp: true,
11138 lsp_fetch_timeout_ms: 10,
11139 lsp_insert_mode: LspInsertMode::Insert,
11140 });
11141 });
11142
11143 let mut cx = EditorLspTestContext::new_rust(
11144 lsp::ServerCapabilities {
11145 completion_provider: Some(lsp::CompletionOptions {
11146 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11147 ..lsp::CompletionOptions::default()
11148 }),
11149 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11150 ..lsp::ServerCapabilities::default()
11151 },
11152 cx,
11153 )
11154 .await;
11155
11156 let throttle_completions = Arc::new(AtomicBool::new(false));
11157
11158 let lsp_throttle_completions = throttle_completions.clone();
11159 let _completion_requests_handler =
11160 cx.lsp
11161 .server
11162 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
11163 let lsp_throttle_completions = lsp_throttle_completions.clone();
11164 let cx = cx.clone();
11165 async move {
11166 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
11167 cx.background_executor()
11168 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
11169 .await;
11170 }
11171 Ok(Some(lsp::CompletionResponse::Array(vec![
11172 lsp::CompletionItem {
11173 label: "first".into(),
11174 ..lsp::CompletionItem::default()
11175 },
11176 lsp::CompletionItem {
11177 label: "last".into(),
11178 ..lsp::CompletionItem::default()
11179 },
11180 ])))
11181 }
11182 });
11183
11184 cx.set_state(indoc! {"
11185 oneˇ
11186 two
11187 three
11188 "});
11189 cx.simulate_keystroke(".");
11190 cx.executor().run_until_parked();
11191 cx.condition(|editor, _| editor.context_menu_visible())
11192 .await;
11193 cx.update_editor(|editor, window, cx| {
11194 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11195 {
11196 assert_eq!(
11197 completion_menu_entries(&menu),
11198 &["first", "last"],
11199 "When LSP server is fast to reply, no fallback word completions are used"
11200 );
11201 } else {
11202 panic!("expected completion menu to be open");
11203 }
11204 editor.cancel(&Cancel, window, cx);
11205 });
11206 cx.executor().run_until_parked();
11207 cx.condition(|editor, _| !editor.context_menu_visible())
11208 .await;
11209
11210 throttle_completions.store(true, atomic::Ordering::Release);
11211 cx.simulate_keystroke(".");
11212 cx.executor()
11213 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
11214 cx.executor().run_until_parked();
11215 cx.condition(|editor, _| editor.context_menu_visible())
11216 .await;
11217 cx.update_editor(|editor, _, _| {
11218 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11219 {
11220 assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
11221 "When LSP server is slow, document words can be shown instead, if configured accordingly");
11222 } else {
11223 panic!("expected completion menu to be open");
11224 }
11225 });
11226}
11227
11228#[gpui::test]
11229async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
11230 init_test(cx, |language_settings| {
11231 language_settings.defaults.completions = Some(CompletionSettings {
11232 words: WordsCompletionMode::Enabled,
11233 lsp: true,
11234 lsp_fetch_timeout_ms: 0,
11235 lsp_insert_mode: LspInsertMode::Insert,
11236 });
11237 });
11238
11239 let mut cx = EditorLspTestContext::new_rust(
11240 lsp::ServerCapabilities {
11241 completion_provider: Some(lsp::CompletionOptions {
11242 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11243 ..lsp::CompletionOptions::default()
11244 }),
11245 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11246 ..lsp::ServerCapabilities::default()
11247 },
11248 cx,
11249 )
11250 .await;
11251
11252 let _completion_requests_handler =
11253 cx.lsp
11254 .server
11255 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
11256 Ok(Some(lsp::CompletionResponse::Array(vec![
11257 lsp::CompletionItem {
11258 label: "first".into(),
11259 ..lsp::CompletionItem::default()
11260 },
11261 lsp::CompletionItem {
11262 label: "last".into(),
11263 ..lsp::CompletionItem::default()
11264 },
11265 ])))
11266 });
11267
11268 cx.set_state(indoc! {"ˇ
11269 first
11270 last
11271 second
11272 "});
11273 cx.simulate_keystroke(".");
11274 cx.executor().run_until_parked();
11275 cx.condition(|editor, _| editor.context_menu_visible())
11276 .await;
11277 cx.update_editor(|editor, _, _| {
11278 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11279 {
11280 assert_eq!(
11281 completion_menu_entries(&menu),
11282 &["first", "last", "second"],
11283 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
11284 );
11285 } else {
11286 panic!("expected completion menu to be open");
11287 }
11288 });
11289}
11290
11291#[gpui::test]
11292async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
11293 init_test(cx, |language_settings| {
11294 language_settings.defaults.completions = Some(CompletionSettings {
11295 words: WordsCompletionMode::Disabled,
11296 lsp: true,
11297 lsp_fetch_timeout_ms: 0,
11298 lsp_insert_mode: LspInsertMode::Insert,
11299 });
11300 });
11301
11302 let mut cx = EditorLspTestContext::new_rust(
11303 lsp::ServerCapabilities {
11304 completion_provider: Some(lsp::CompletionOptions {
11305 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11306 ..lsp::CompletionOptions::default()
11307 }),
11308 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11309 ..lsp::ServerCapabilities::default()
11310 },
11311 cx,
11312 )
11313 .await;
11314
11315 let _completion_requests_handler =
11316 cx.lsp
11317 .server
11318 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
11319 panic!("LSP completions should not be queried when dealing with word completions")
11320 });
11321
11322 cx.set_state(indoc! {"ˇ
11323 first
11324 last
11325 second
11326 "});
11327 cx.update_editor(|editor, window, cx| {
11328 editor.show_word_completions(&ShowWordCompletions, window, cx);
11329 });
11330 cx.executor().run_until_parked();
11331 cx.condition(|editor, _| editor.context_menu_visible())
11332 .await;
11333 cx.update_editor(|editor, _, _| {
11334 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11335 {
11336 assert_eq!(
11337 completion_menu_entries(&menu),
11338 &["first", "last", "second"],
11339 "`ShowWordCompletions` action should show word completions"
11340 );
11341 } else {
11342 panic!("expected completion menu to be open");
11343 }
11344 });
11345
11346 cx.simulate_keystroke("l");
11347 cx.executor().run_until_parked();
11348 cx.condition(|editor, _| editor.context_menu_visible())
11349 .await;
11350 cx.update_editor(|editor, _, _| {
11351 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11352 {
11353 assert_eq!(
11354 completion_menu_entries(&menu),
11355 &["last"],
11356 "After showing word completions, further editing should filter them and not query the LSP"
11357 );
11358 } else {
11359 panic!("expected completion menu to be open");
11360 }
11361 });
11362}
11363
11364#[gpui::test]
11365async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
11366 init_test(cx, |language_settings| {
11367 language_settings.defaults.completions = Some(CompletionSettings {
11368 words: WordsCompletionMode::Fallback,
11369 lsp: false,
11370 lsp_fetch_timeout_ms: 0,
11371 lsp_insert_mode: LspInsertMode::Insert,
11372 });
11373 });
11374
11375 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
11376
11377 cx.set_state(indoc! {"ˇ
11378 0_usize
11379 let
11380 33
11381 4.5f32
11382 "});
11383 cx.update_editor(|editor, window, cx| {
11384 editor.show_completions(&ShowCompletions::default(), window, cx);
11385 });
11386 cx.executor().run_until_parked();
11387 cx.condition(|editor, _| editor.context_menu_visible())
11388 .await;
11389 cx.update_editor(|editor, window, cx| {
11390 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11391 {
11392 assert_eq!(
11393 completion_menu_entries(&menu),
11394 &["let"],
11395 "With no digits in the completion query, no digits should be in the word completions"
11396 );
11397 } else {
11398 panic!("expected completion menu to be open");
11399 }
11400 editor.cancel(&Cancel, window, cx);
11401 });
11402
11403 cx.set_state(indoc! {"3ˇ
11404 0_usize
11405 let
11406 3
11407 33.35f32
11408 "});
11409 cx.update_editor(|editor, window, cx| {
11410 editor.show_completions(&ShowCompletions::default(), window, cx);
11411 });
11412 cx.executor().run_until_parked();
11413 cx.condition(|editor, _| editor.context_menu_visible())
11414 .await;
11415 cx.update_editor(|editor, _, _| {
11416 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11417 {
11418 assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
11419 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
11420 } else {
11421 panic!("expected completion menu to be open");
11422 }
11423 });
11424}
11425
11426fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
11427 let position = || lsp::Position {
11428 line: params.text_document_position.position.line,
11429 character: params.text_document_position.position.character,
11430 };
11431 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11432 range: lsp::Range {
11433 start: position(),
11434 end: position(),
11435 },
11436 new_text: text.to_string(),
11437 }))
11438}
11439
11440#[gpui::test]
11441async fn test_multiline_completion(cx: &mut TestAppContext) {
11442 init_test(cx, |_| {});
11443
11444 let fs = FakeFs::new(cx.executor());
11445 fs.insert_tree(
11446 path!("/a"),
11447 json!({
11448 "main.ts": "a",
11449 }),
11450 )
11451 .await;
11452
11453 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11454 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11455 let typescript_language = Arc::new(Language::new(
11456 LanguageConfig {
11457 name: "TypeScript".into(),
11458 matcher: LanguageMatcher {
11459 path_suffixes: vec!["ts".to_string()],
11460 ..LanguageMatcher::default()
11461 },
11462 line_comments: vec!["// ".into()],
11463 ..LanguageConfig::default()
11464 },
11465 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
11466 ));
11467 language_registry.add(typescript_language.clone());
11468 let mut fake_servers = language_registry.register_fake_lsp(
11469 "TypeScript",
11470 FakeLspAdapter {
11471 capabilities: lsp::ServerCapabilities {
11472 completion_provider: Some(lsp::CompletionOptions {
11473 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11474 ..lsp::CompletionOptions::default()
11475 }),
11476 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11477 ..lsp::ServerCapabilities::default()
11478 },
11479 // Emulate vtsls label generation
11480 label_for_completion: Some(Box::new(|item, _| {
11481 let text = if let Some(description) = item
11482 .label_details
11483 .as_ref()
11484 .and_then(|label_details| label_details.description.as_ref())
11485 {
11486 format!("{} {}", item.label, description)
11487 } else if let Some(detail) = &item.detail {
11488 format!("{} {}", item.label, detail)
11489 } else {
11490 item.label.clone()
11491 };
11492 let len = text.len();
11493 Some(language::CodeLabel {
11494 text,
11495 runs: Vec::new(),
11496 filter_range: 0..len,
11497 })
11498 })),
11499 ..FakeLspAdapter::default()
11500 },
11501 );
11502 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11503 let cx = &mut VisualTestContext::from_window(*workspace, cx);
11504 let worktree_id = workspace
11505 .update(cx, |workspace, _window, cx| {
11506 workspace.project().update(cx, |project, cx| {
11507 project.worktrees(cx).next().unwrap().read(cx).id()
11508 })
11509 })
11510 .unwrap();
11511 let _buffer = project
11512 .update(cx, |project, cx| {
11513 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
11514 })
11515 .await
11516 .unwrap();
11517 let editor = workspace
11518 .update(cx, |workspace, window, cx| {
11519 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
11520 })
11521 .unwrap()
11522 .await
11523 .unwrap()
11524 .downcast::<Editor>()
11525 .unwrap();
11526 let fake_server = fake_servers.next().await.unwrap();
11527
11528 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
11529 let multiline_label_2 = "a\nb\nc\n";
11530 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
11531 let multiline_description = "d\ne\nf\n";
11532 let multiline_detail_2 = "g\nh\ni\n";
11533
11534 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
11535 move |params, _| async move {
11536 Ok(Some(lsp::CompletionResponse::Array(vec![
11537 lsp::CompletionItem {
11538 label: multiline_label.to_string(),
11539 text_edit: gen_text_edit(¶ms, "new_text_1"),
11540 ..lsp::CompletionItem::default()
11541 },
11542 lsp::CompletionItem {
11543 label: "single line label 1".to_string(),
11544 detail: Some(multiline_detail.to_string()),
11545 text_edit: gen_text_edit(¶ms, "new_text_2"),
11546 ..lsp::CompletionItem::default()
11547 },
11548 lsp::CompletionItem {
11549 label: "single line label 2".to_string(),
11550 label_details: Some(lsp::CompletionItemLabelDetails {
11551 description: Some(multiline_description.to_string()),
11552 detail: None,
11553 }),
11554 text_edit: gen_text_edit(¶ms, "new_text_2"),
11555 ..lsp::CompletionItem::default()
11556 },
11557 lsp::CompletionItem {
11558 label: multiline_label_2.to_string(),
11559 detail: Some(multiline_detail_2.to_string()),
11560 text_edit: gen_text_edit(¶ms, "new_text_3"),
11561 ..lsp::CompletionItem::default()
11562 },
11563 lsp::CompletionItem {
11564 label: "Label with many spaces and \t but without newlines".to_string(),
11565 detail: Some(
11566 "Details with many spaces and \t but without newlines".to_string(),
11567 ),
11568 text_edit: gen_text_edit(¶ms, "new_text_4"),
11569 ..lsp::CompletionItem::default()
11570 },
11571 ])))
11572 },
11573 );
11574
11575 editor.update_in(cx, |editor, window, cx| {
11576 cx.focus_self(window);
11577 editor.move_to_end(&MoveToEnd, window, cx);
11578 editor.handle_input(".", window, cx);
11579 });
11580 cx.run_until_parked();
11581 completion_handle.next().await.unwrap();
11582
11583 editor.update(cx, |editor, _| {
11584 assert!(editor.context_menu_visible());
11585 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11586 {
11587 let completion_labels = menu
11588 .completions
11589 .borrow()
11590 .iter()
11591 .map(|c| c.label.text.clone())
11592 .collect::<Vec<_>>();
11593 assert_eq!(
11594 completion_labels,
11595 &[
11596 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
11597 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
11598 "single line label 2 d e f ",
11599 "a b c g h i ",
11600 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
11601 ],
11602 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
11603 );
11604
11605 for completion in menu
11606 .completions
11607 .borrow()
11608 .iter() {
11609 assert_eq!(
11610 completion.label.filter_range,
11611 0..completion.label.text.len(),
11612 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
11613 );
11614 }
11615 } else {
11616 panic!("expected completion menu to be open");
11617 }
11618 });
11619}
11620
11621#[gpui::test]
11622async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
11623 init_test(cx, |_| {});
11624 let mut cx = EditorLspTestContext::new_rust(
11625 lsp::ServerCapabilities {
11626 completion_provider: Some(lsp::CompletionOptions {
11627 trigger_characters: Some(vec![".".to_string()]),
11628 ..Default::default()
11629 }),
11630 ..Default::default()
11631 },
11632 cx,
11633 )
11634 .await;
11635 cx.lsp
11636 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
11637 Ok(Some(lsp::CompletionResponse::Array(vec![
11638 lsp::CompletionItem {
11639 label: "first".into(),
11640 ..Default::default()
11641 },
11642 lsp::CompletionItem {
11643 label: "last".into(),
11644 ..Default::default()
11645 },
11646 ])))
11647 });
11648 cx.set_state("variableˇ");
11649 cx.simulate_keystroke(".");
11650 cx.executor().run_until_parked();
11651
11652 cx.update_editor(|editor, _, _| {
11653 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11654 {
11655 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
11656 } else {
11657 panic!("expected completion menu to be open");
11658 }
11659 });
11660
11661 cx.update_editor(|editor, window, cx| {
11662 editor.move_page_down(&MovePageDown::default(), window, cx);
11663 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11664 {
11665 assert!(
11666 menu.selected_item == 1,
11667 "expected PageDown to select the last item from the context menu"
11668 );
11669 } else {
11670 panic!("expected completion menu to stay open after PageDown");
11671 }
11672 });
11673
11674 cx.update_editor(|editor, window, cx| {
11675 editor.move_page_up(&MovePageUp::default(), window, cx);
11676 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11677 {
11678 assert!(
11679 menu.selected_item == 0,
11680 "expected PageUp to select the first item from the context menu"
11681 );
11682 } else {
11683 panic!("expected completion menu to stay open after PageUp");
11684 }
11685 });
11686}
11687
11688#[gpui::test]
11689async fn test_as_is_completions(cx: &mut TestAppContext) {
11690 init_test(cx, |_| {});
11691 let mut cx = EditorLspTestContext::new_rust(
11692 lsp::ServerCapabilities {
11693 completion_provider: Some(lsp::CompletionOptions {
11694 ..Default::default()
11695 }),
11696 ..Default::default()
11697 },
11698 cx,
11699 )
11700 .await;
11701 cx.lsp
11702 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
11703 Ok(Some(lsp::CompletionResponse::Array(vec![
11704 lsp::CompletionItem {
11705 label: "unsafe".into(),
11706 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11707 range: lsp::Range {
11708 start: lsp::Position {
11709 line: 1,
11710 character: 2,
11711 },
11712 end: lsp::Position {
11713 line: 1,
11714 character: 3,
11715 },
11716 },
11717 new_text: "unsafe".to_string(),
11718 })),
11719 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
11720 ..Default::default()
11721 },
11722 ])))
11723 });
11724 cx.set_state("fn a() {}\n nˇ");
11725 cx.executor().run_until_parked();
11726 cx.update_editor(|editor, window, cx| {
11727 editor.show_completions(
11728 &ShowCompletions {
11729 trigger: Some("\n".into()),
11730 },
11731 window,
11732 cx,
11733 );
11734 });
11735 cx.executor().run_until_parked();
11736
11737 cx.update_editor(|editor, window, cx| {
11738 editor.confirm_completion(&Default::default(), window, cx)
11739 });
11740 cx.executor().run_until_parked();
11741 cx.assert_editor_state("fn a() {}\n unsafeˇ");
11742}
11743
11744#[gpui::test]
11745async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
11746 init_test(cx, |_| {});
11747
11748 let mut cx = EditorLspTestContext::new_rust(
11749 lsp::ServerCapabilities {
11750 completion_provider: Some(lsp::CompletionOptions {
11751 trigger_characters: Some(vec![".".to_string()]),
11752 resolve_provider: Some(true),
11753 ..Default::default()
11754 }),
11755 ..Default::default()
11756 },
11757 cx,
11758 )
11759 .await;
11760
11761 cx.set_state("fn main() { let a = 2ˇ; }");
11762 cx.simulate_keystroke(".");
11763 let completion_item = lsp::CompletionItem {
11764 label: "Some".into(),
11765 kind: Some(lsp::CompletionItemKind::SNIPPET),
11766 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
11767 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
11768 kind: lsp::MarkupKind::Markdown,
11769 value: "```rust\nSome(2)\n```".to_string(),
11770 })),
11771 deprecated: Some(false),
11772 sort_text: Some("Some".to_string()),
11773 filter_text: Some("Some".to_string()),
11774 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
11775 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11776 range: lsp::Range {
11777 start: lsp::Position {
11778 line: 0,
11779 character: 22,
11780 },
11781 end: lsp::Position {
11782 line: 0,
11783 character: 22,
11784 },
11785 },
11786 new_text: "Some(2)".to_string(),
11787 })),
11788 additional_text_edits: Some(vec![lsp::TextEdit {
11789 range: lsp::Range {
11790 start: lsp::Position {
11791 line: 0,
11792 character: 20,
11793 },
11794 end: lsp::Position {
11795 line: 0,
11796 character: 22,
11797 },
11798 },
11799 new_text: "".to_string(),
11800 }]),
11801 ..Default::default()
11802 };
11803
11804 let closure_completion_item = completion_item.clone();
11805 let counter = Arc::new(AtomicUsize::new(0));
11806 let counter_clone = counter.clone();
11807 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
11808 let task_completion_item = closure_completion_item.clone();
11809 counter_clone.fetch_add(1, atomic::Ordering::Release);
11810 async move {
11811 Ok(Some(lsp::CompletionResponse::Array(vec![
11812 task_completion_item,
11813 ])))
11814 }
11815 });
11816
11817 cx.condition(|editor, _| editor.context_menu_visible())
11818 .await;
11819 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
11820 assert!(request.next().await.is_some());
11821 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11822
11823 cx.simulate_keystrokes("S o m");
11824 cx.condition(|editor, _| editor.context_menu_visible())
11825 .await;
11826 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
11827 assert!(request.next().await.is_some());
11828 assert!(request.next().await.is_some());
11829 assert!(request.next().await.is_some());
11830 request.close();
11831 assert!(request.next().await.is_none());
11832 assert_eq!(
11833 counter.load(atomic::Ordering::Acquire),
11834 4,
11835 "With the completions menu open, only one LSP request should happen per input"
11836 );
11837}
11838
11839#[gpui::test]
11840async fn test_toggle_comment(cx: &mut TestAppContext) {
11841 init_test(cx, |_| {});
11842 let mut cx = EditorTestContext::new(cx).await;
11843 let language = Arc::new(Language::new(
11844 LanguageConfig {
11845 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
11846 ..Default::default()
11847 },
11848 Some(tree_sitter_rust::LANGUAGE.into()),
11849 ));
11850 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
11851
11852 // If multiple selections intersect a line, the line is only toggled once.
11853 cx.set_state(indoc! {"
11854 fn a() {
11855 «//b();
11856 ˇ»// «c();
11857 //ˇ» d();
11858 }
11859 "});
11860
11861 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11862
11863 cx.assert_editor_state(indoc! {"
11864 fn a() {
11865 «b();
11866 c();
11867 ˇ» d();
11868 }
11869 "});
11870
11871 // The comment prefix is inserted at the same column for every line in a
11872 // selection.
11873 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11874
11875 cx.assert_editor_state(indoc! {"
11876 fn a() {
11877 // «b();
11878 // c();
11879 ˇ»// d();
11880 }
11881 "});
11882
11883 // If a selection ends at the beginning of a line, that line is not toggled.
11884 cx.set_selections_state(indoc! {"
11885 fn a() {
11886 // b();
11887 «// c();
11888 ˇ» // d();
11889 }
11890 "});
11891
11892 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11893
11894 cx.assert_editor_state(indoc! {"
11895 fn a() {
11896 // b();
11897 «c();
11898 ˇ» // d();
11899 }
11900 "});
11901
11902 // If a selection span a single line and is empty, the line is toggled.
11903 cx.set_state(indoc! {"
11904 fn a() {
11905 a();
11906 b();
11907 ˇ
11908 }
11909 "});
11910
11911 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11912
11913 cx.assert_editor_state(indoc! {"
11914 fn a() {
11915 a();
11916 b();
11917 //•ˇ
11918 }
11919 "});
11920
11921 // If a selection span multiple lines, empty lines are not toggled.
11922 cx.set_state(indoc! {"
11923 fn a() {
11924 «a();
11925
11926 c();ˇ»
11927 }
11928 "});
11929
11930 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11931
11932 cx.assert_editor_state(indoc! {"
11933 fn a() {
11934 // «a();
11935
11936 // c();ˇ»
11937 }
11938 "});
11939
11940 // If a selection includes multiple comment prefixes, all lines are uncommented.
11941 cx.set_state(indoc! {"
11942 fn a() {
11943 «// a();
11944 /// b();
11945 //! c();ˇ»
11946 }
11947 "});
11948
11949 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11950
11951 cx.assert_editor_state(indoc! {"
11952 fn a() {
11953 «a();
11954 b();
11955 c();ˇ»
11956 }
11957 "});
11958}
11959
11960#[gpui::test]
11961async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
11962 init_test(cx, |_| {});
11963 let mut cx = EditorTestContext::new(cx).await;
11964 let language = Arc::new(Language::new(
11965 LanguageConfig {
11966 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
11967 ..Default::default()
11968 },
11969 Some(tree_sitter_rust::LANGUAGE.into()),
11970 ));
11971 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
11972
11973 let toggle_comments = &ToggleComments {
11974 advance_downwards: false,
11975 ignore_indent: true,
11976 };
11977
11978 // If multiple selections intersect a line, the line is only toggled once.
11979 cx.set_state(indoc! {"
11980 fn a() {
11981 // «b();
11982 // c();
11983 // ˇ» d();
11984 }
11985 "});
11986
11987 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11988
11989 cx.assert_editor_state(indoc! {"
11990 fn a() {
11991 «b();
11992 c();
11993 ˇ» d();
11994 }
11995 "});
11996
11997 // The comment prefix is inserted at the beginning of each line
11998 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11999
12000 cx.assert_editor_state(indoc! {"
12001 fn a() {
12002 // «b();
12003 // c();
12004 // ˇ» d();
12005 }
12006 "});
12007
12008 // If a selection ends at the beginning of a line, that line is not toggled.
12009 cx.set_selections_state(indoc! {"
12010 fn a() {
12011 // b();
12012 // «c();
12013 ˇ»// d();
12014 }
12015 "});
12016
12017 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12018
12019 cx.assert_editor_state(indoc! {"
12020 fn a() {
12021 // b();
12022 «c();
12023 ˇ»// d();
12024 }
12025 "});
12026
12027 // If a selection span a single line and is empty, the line is toggled.
12028 cx.set_state(indoc! {"
12029 fn a() {
12030 a();
12031 b();
12032 ˇ
12033 }
12034 "});
12035
12036 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12037
12038 cx.assert_editor_state(indoc! {"
12039 fn a() {
12040 a();
12041 b();
12042 //ˇ
12043 }
12044 "});
12045
12046 // If a selection span multiple lines, empty lines are not toggled.
12047 cx.set_state(indoc! {"
12048 fn a() {
12049 «a();
12050
12051 c();ˇ»
12052 }
12053 "});
12054
12055 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12056
12057 cx.assert_editor_state(indoc! {"
12058 fn a() {
12059 // «a();
12060
12061 // c();ˇ»
12062 }
12063 "});
12064
12065 // If a selection includes multiple comment prefixes, all lines are uncommented.
12066 cx.set_state(indoc! {"
12067 fn a() {
12068 // «a();
12069 /// b();
12070 //! c();ˇ»
12071 }
12072 "});
12073
12074 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12075
12076 cx.assert_editor_state(indoc! {"
12077 fn a() {
12078 «a();
12079 b();
12080 c();ˇ»
12081 }
12082 "});
12083}
12084
12085#[gpui::test]
12086async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
12087 init_test(cx, |_| {});
12088
12089 let language = Arc::new(Language::new(
12090 LanguageConfig {
12091 line_comments: vec!["// ".into()],
12092 ..Default::default()
12093 },
12094 Some(tree_sitter_rust::LANGUAGE.into()),
12095 ));
12096
12097 let mut cx = EditorTestContext::new(cx).await;
12098
12099 cx.language_registry().add(language.clone());
12100 cx.update_buffer(|buffer, cx| {
12101 buffer.set_language(Some(language), cx);
12102 });
12103
12104 let toggle_comments = &ToggleComments {
12105 advance_downwards: true,
12106 ignore_indent: false,
12107 };
12108
12109 // Single cursor on one line -> advance
12110 // Cursor moves horizontally 3 characters as well on non-blank line
12111 cx.set_state(indoc!(
12112 "fn a() {
12113 ˇdog();
12114 cat();
12115 }"
12116 ));
12117 cx.update_editor(|editor, window, cx| {
12118 editor.toggle_comments(toggle_comments, window, cx);
12119 });
12120 cx.assert_editor_state(indoc!(
12121 "fn a() {
12122 // dog();
12123 catˇ();
12124 }"
12125 ));
12126
12127 // Single selection on one line -> don't advance
12128 cx.set_state(indoc!(
12129 "fn a() {
12130 «dog()ˇ»;
12131 cat();
12132 }"
12133 ));
12134 cx.update_editor(|editor, window, cx| {
12135 editor.toggle_comments(toggle_comments, window, cx);
12136 });
12137 cx.assert_editor_state(indoc!(
12138 "fn a() {
12139 // «dog()ˇ»;
12140 cat();
12141 }"
12142 ));
12143
12144 // Multiple cursors on one line -> advance
12145 cx.set_state(indoc!(
12146 "fn a() {
12147 ˇdˇog();
12148 cat();
12149 }"
12150 ));
12151 cx.update_editor(|editor, window, cx| {
12152 editor.toggle_comments(toggle_comments, window, cx);
12153 });
12154 cx.assert_editor_state(indoc!(
12155 "fn a() {
12156 // dog();
12157 catˇ(ˇ);
12158 }"
12159 ));
12160
12161 // Multiple cursors on one line, with selection -> don't advance
12162 cx.set_state(indoc!(
12163 "fn a() {
12164 ˇdˇog«()ˇ»;
12165 cat();
12166 }"
12167 ));
12168 cx.update_editor(|editor, window, cx| {
12169 editor.toggle_comments(toggle_comments, window, cx);
12170 });
12171 cx.assert_editor_state(indoc!(
12172 "fn a() {
12173 // ˇdˇog«()ˇ»;
12174 cat();
12175 }"
12176 ));
12177
12178 // Single cursor on one line -> advance
12179 // Cursor moves to column 0 on blank line
12180 cx.set_state(indoc!(
12181 "fn a() {
12182 ˇdog();
12183
12184 cat();
12185 }"
12186 ));
12187 cx.update_editor(|editor, window, cx| {
12188 editor.toggle_comments(toggle_comments, window, cx);
12189 });
12190 cx.assert_editor_state(indoc!(
12191 "fn a() {
12192 // dog();
12193 ˇ
12194 cat();
12195 }"
12196 ));
12197
12198 // Single cursor on one line -> advance
12199 // Cursor starts and ends at column 0
12200 cx.set_state(indoc!(
12201 "fn a() {
12202 ˇ dog();
12203 cat();
12204 }"
12205 ));
12206 cx.update_editor(|editor, window, cx| {
12207 editor.toggle_comments(toggle_comments, window, cx);
12208 });
12209 cx.assert_editor_state(indoc!(
12210 "fn a() {
12211 // dog();
12212 ˇ cat();
12213 }"
12214 ));
12215}
12216
12217#[gpui::test]
12218async fn test_toggle_block_comment(cx: &mut TestAppContext) {
12219 init_test(cx, |_| {});
12220
12221 let mut cx = EditorTestContext::new(cx).await;
12222
12223 let html_language = Arc::new(
12224 Language::new(
12225 LanguageConfig {
12226 name: "HTML".into(),
12227 block_comment: Some(("<!-- ".into(), " -->".into())),
12228 ..Default::default()
12229 },
12230 Some(tree_sitter_html::LANGUAGE.into()),
12231 )
12232 .with_injection_query(
12233 r#"
12234 (script_element
12235 (raw_text) @injection.content
12236 (#set! injection.language "javascript"))
12237 "#,
12238 )
12239 .unwrap(),
12240 );
12241
12242 let javascript_language = Arc::new(Language::new(
12243 LanguageConfig {
12244 name: "JavaScript".into(),
12245 line_comments: vec!["// ".into()],
12246 ..Default::default()
12247 },
12248 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
12249 ));
12250
12251 cx.language_registry().add(html_language.clone());
12252 cx.language_registry().add(javascript_language.clone());
12253 cx.update_buffer(|buffer, cx| {
12254 buffer.set_language(Some(html_language), cx);
12255 });
12256
12257 // Toggle comments for empty selections
12258 cx.set_state(
12259 &r#"
12260 <p>A</p>ˇ
12261 <p>B</p>ˇ
12262 <p>C</p>ˇ
12263 "#
12264 .unindent(),
12265 );
12266 cx.update_editor(|editor, window, cx| {
12267 editor.toggle_comments(&ToggleComments::default(), window, cx)
12268 });
12269 cx.assert_editor_state(
12270 &r#"
12271 <!-- <p>A</p>ˇ -->
12272 <!-- <p>B</p>ˇ -->
12273 <!-- <p>C</p>ˇ -->
12274 "#
12275 .unindent(),
12276 );
12277 cx.update_editor(|editor, window, cx| {
12278 editor.toggle_comments(&ToggleComments::default(), window, cx)
12279 });
12280 cx.assert_editor_state(
12281 &r#"
12282 <p>A</p>ˇ
12283 <p>B</p>ˇ
12284 <p>C</p>ˇ
12285 "#
12286 .unindent(),
12287 );
12288
12289 // Toggle comments for mixture of empty and non-empty selections, where
12290 // multiple selections occupy a given line.
12291 cx.set_state(
12292 &r#"
12293 <p>A«</p>
12294 <p>ˇ»B</p>ˇ
12295 <p>C«</p>
12296 <p>ˇ»D</p>ˇ
12297 "#
12298 .unindent(),
12299 );
12300
12301 cx.update_editor(|editor, window, cx| {
12302 editor.toggle_comments(&ToggleComments::default(), window, cx)
12303 });
12304 cx.assert_editor_state(
12305 &r#"
12306 <!-- <p>A«</p>
12307 <p>ˇ»B</p>ˇ -->
12308 <!-- <p>C«</p>
12309 <p>ˇ»D</p>ˇ -->
12310 "#
12311 .unindent(),
12312 );
12313 cx.update_editor(|editor, window, cx| {
12314 editor.toggle_comments(&ToggleComments::default(), window, cx)
12315 });
12316 cx.assert_editor_state(
12317 &r#"
12318 <p>A«</p>
12319 <p>ˇ»B</p>ˇ
12320 <p>C«</p>
12321 <p>ˇ»D</p>ˇ
12322 "#
12323 .unindent(),
12324 );
12325
12326 // Toggle comments when different languages are active for different
12327 // selections.
12328 cx.set_state(
12329 &r#"
12330 ˇ<script>
12331 ˇvar x = new Y();
12332 ˇ</script>
12333 "#
12334 .unindent(),
12335 );
12336 cx.executor().run_until_parked();
12337 cx.update_editor(|editor, window, cx| {
12338 editor.toggle_comments(&ToggleComments::default(), window, cx)
12339 });
12340 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
12341 // Uncommenting and commenting from this position brings in even more wrong artifacts.
12342 cx.assert_editor_state(
12343 &r#"
12344 <!-- ˇ<script> -->
12345 // ˇvar x = new Y();
12346 <!-- ˇ</script> -->
12347 "#
12348 .unindent(),
12349 );
12350}
12351
12352#[gpui::test]
12353fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
12354 init_test(cx, |_| {});
12355
12356 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
12357 let multibuffer = cx.new(|cx| {
12358 let mut multibuffer = MultiBuffer::new(ReadWrite);
12359 multibuffer.push_excerpts(
12360 buffer.clone(),
12361 [
12362 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
12363 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
12364 ],
12365 cx,
12366 );
12367 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
12368 multibuffer
12369 });
12370
12371 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
12372 editor.update_in(cx, |editor, window, cx| {
12373 assert_eq!(editor.text(cx), "aaaa\nbbbb");
12374 editor.change_selections(None, window, cx, |s| {
12375 s.select_ranges([
12376 Point::new(0, 0)..Point::new(0, 0),
12377 Point::new(1, 0)..Point::new(1, 0),
12378 ])
12379 });
12380
12381 editor.handle_input("X", window, cx);
12382 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
12383 assert_eq!(
12384 editor.selections.ranges(cx),
12385 [
12386 Point::new(0, 1)..Point::new(0, 1),
12387 Point::new(1, 1)..Point::new(1, 1),
12388 ]
12389 );
12390
12391 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
12392 editor.change_selections(None, window, cx, |s| {
12393 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
12394 });
12395 editor.backspace(&Default::default(), window, cx);
12396 assert_eq!(editor.text(cx), "Xa\nbbb");
12397 assert_eq!(
12398 editor.selections.ranges(cx),
12399 [Point::new(1, 0)..Point::new(1, 0)]
12400 );
12401
12402 editor.change_selections(None, window, cx, |s| {
12403 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
12404 });
12405 editor.backspace(&Default::default(), window, cx);
12406 assert_eq!(editor.text(cx), "X\nbb");
12407 assert_eq!(
12408 editor.selections.ranges(cx),
12409 [Point::new(0, 1)..Point::new(0, 1)]
12410 );
12411 });
12412}
12413
12414#[gpui::test]
12415fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
12416 init_test(cx, |_| {});
12417
12418 let markers = vec![('[', ']').into(), ('(', ')').into()];
12419 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
12420 indoc! {"
12421 [aaaa
12422 (bbbb]
12423 cccc)",
12424 },
12425 markers.clone(),
12426 );
12427 let excerpt_ranges = markers.into_iter().map(|marker| {
12428 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
12429 ExcerptRange::new(context.clone())
12430 });
12431 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
12432 let multibuffer = cx.new(|cx| {
12433 let mut multibuffer = MultiBuffer::new(ReadWrite);
12434 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
12435 multibuffer
12436 });
12437
12438 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
12439 editor.update_in(cx, |editor, window, cx| {
12440 let (expected_text, selection_ranges) = marked_text_ranges(
12441 indoc! {"
12442 aaaa
12443 bˇbbb
12444 bˇbbˇb
12445 cccc"
12446 },
12447 true,
12448 );
12449 assert_eq!(editor.text(cx), expected_text);
12450 editor.change_selections(None, window, cx, |s| s.select_ranges(selection_ranges));
12451
12452 editor.handle_input("X", window, cx);
12453
12454 let (expected_text, expected_selections) = marked_text_ranges(
12455 indoc! {"
12456 aaaa
12457 bXˇbbXb
12458 bXˇbbXˇb
12459 cccc"
12460 },
12461 false,
12462 );
12463 assert_eq!(editor.text(cx), expected_text);
12464 assert_eq!(editor.selections.ranges(cx), expected_selections);
12465
12466 editor.newline(&Newline, window, cx);
12467 let (expected_text, expected_selections) = marked_text_ranges(
12468 indoc! {"
12469 aaaa
12470 bX
12471 ˇbbX
12472 b
12473 bX
12474 ˇbbX
12475 ˇb
12476 cccc"
12477 },
12478 false,
12479 );
12480 assert_eq!(editor.text(cx), expected_text);
12481 assert_eq!(editor.selections.ranges(cx), expected_selections);
12482 });
12483}
12484
12485#[gpui::test]
12486fn test_refresh_selections(cx: &mut TestAppContext) {
12487 init_test(cx, |_| {});
12488
12489 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
12490 let mut excerpt1_id = None;
12491 let multibuffer = cx.new(|cx| {
12492 let mut multibuffer = MultiBuffer::new(ReadWrite);
12493 excerpt1_id = multibuffer
12494 .push_excerpts(
12495 buffer.clone(),
12496 [
12497 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
12498 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
12499 ],
12500 cx,
12501 )
12502 .into_iter()
12503 .next();
12504 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
12505 multibuffer
12506 });
12507
12508 let editor = cx.add_window(|window, cx| {
12509 let mut editor = build_editor(multibuffer.clone(), window, cx);
12510 let snapshot = editor.snapshot(window, cx);
12511 editor.change_selections(None, window, cx, |s| {
12512 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
12513 });
12514 editor.begin_selection(
12515 Point::new(2, 1).to_display_point(&snapshot),
12516 true,
12517 1,
12518 window,
12519 cx,
12520 );
12521 assert_eq!(
12522 editor.selections.ranges(cx),
12523 [
12524 Point::new(1, 3)..Point::new(1, 3),
12525 Point::new(2, 1)..Point::new(2, 1),
12526 ]
12527 );
12528 editor
12529 });
12530
12531 // Refreshing selections is a no-op when excerpts haven't changed.
12532 _ = editor.update(cx, |editor, window, cx| {
12533 editor.change_selections(None, window, cx, |s| s.refresh());
12534 assert_eq!(
12535 editor.selections.ranges(cx),
12536 [
12537 Point::new(1, 3)..Point::new(1, 3),
12538 Point::new(2, 1)..Point::new(2, 1),
12539 ]
12540 );
12541 });
12542
12543 multibuffer.update(cx, |multibuffer, cx| {
12544 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
12545 });
12546 _ = editor.update(cx, |editor, window, cx| {
12547 // Removing an excerpt causes the first selection to become degenerate.
12548 assert_eq!(
12549 editor.selections.ranges(cx),
12550 [
12551 Point::new(0, 0)..Point::new(0, 0),
12552 Point::new(0, 1)..Point::new(0, 1)
12553 ]
12554 );
12555
12556 // Refreshing selections will relocate the first selection to the original buffer
12557 // location.
12558 editor.change_selections(None, window, cx, |s| s.refresh());
12559 assert_eq!(
12560 editor.selections.ranges(cx),
12561 [
12562 Point::new(0, 1)..Point::new(0, 1),
12563 Point::new(0, 3)..Point::new(0, 3)
12564 ]
12565 );
12566 assert!(editor.selections.pending_anchor().is_some());
12567 });
12568}
12569
12570#[gpui::test]
12571fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
12572 init_test(cx, |_| {});
12573
12574 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
12575 let mut excerpt1_id = None;
12576 let multibuffer = cx.new(|cx| {
12577 let mut multibuffer = MultiBuffer::new(ReadWrite);
12578 excerpt1_id = multibuffer
12579 .push_excerpts(
12580 buffer.clone(),
12581 [
12582 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
12583 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
12584 ],
12585 cx,
12586 )
12587 .into_iter()
12588 .next();
12589 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
12590 multibuffer
12591 });
12592
12593 let editor = cx.add_window(|window, cx| {
12594 let mut editor = build_editor(multibuffer.clone(), window, cx);
12595 let snapshot = editor.snapshot(window, cx);
12596 editor.begin_selection(
12597 Point::new(1, 3).to_display_point(&snapshot),
12598 false,
12599 1,
12600 window,
12601 cx,
12602 );
12603 assert_eq!(
12604 editor.selections.ranges(cx),
12605 [Point::new(1, 3)..Point::new(1, 3)]
12606 );
12607 editor
12608 });
12609
12610 multibuffer.update(cx, |multibuffer, cx| {
12611 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
12612 });
12613 _ = editor.update(cx, |editor, window, cx| {
12614 assert_eq!(
12615 editor.selections.ranges(cx),
12616 [Point::new(0, 0)..Point::new(0, 0)]
12617 );
12618
12619 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
12620 editor.change_selections(None, window, cx, |s| s.refresh());
12621 assert_eq!(
12622 editor.selections.ranges(cx),
12623 [Point::new(0, 3)..Point::new(0, 3)]
12624 );
12625 assert!(editor.selections.pending_anchor().is_some());
12626 });
12627}
12628
12629#[gpui::test]
12630async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
12631 init_test(cx, |_| {});
12632
12633 let language = Arc::new(
12634 Language::new(
12635 LanguageConfig {
12636 brackets: BracketPairConfig {
12637 pairs: vec![
12638 BracketPair {
12639 start: "{".to_string(),
12640 end: "}".to_string(),
12641 close: true,
12642 surround: true,
12643 newline: true,
12644 },
12645 BracketPair {
12646 start: "/* ".to_string(),
12647 end: " */".to_string(),
12648 close: true,
12649 surround: true,
12650 newline: true,
12651 },
12652 ],
12653 ..Default::default()
12654 },
12655 ..Default::default()
12656 },
12657 Some(tree_sitter_rust::LANGUAGE.into()),
12658 )
12659 .with_indents_query("")
12660 .unwrap(),
12661 );
12662
12663 let text = concat!(
12664 "{ }\n", //
12665 " x\n", //
12666 " /* */\n", //
12667 "x\n", //
12668 "{{} }\n", //
12669 );
12670
12671 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
12672 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12673 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
12674 editor
12675 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
12676 .await;
12677
12678 editor.update_in(cx, |editor, window, cx| {
12679 editor.change_selections(None, window, cx, |s| {
12680 s.select_display_ranges([
12681 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
12682 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
12683 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
12684 ])
12685 });
12686 editor.newline(&Newline, window, cx);
12687
12688 assert_eq!(
12689 editor.buffer().read(cx).read(cx).text(),
12690 concat!(
12691 "{ \n", // Suppress rustfmt
12692 "\n", //
12693 "}\n", //
12694 " x\n", //
12695 " /* \n", //
12696 " \n", //
12697 " */\n", //
12698 "x\n", //
12699 "{{} \n", //
12700 "}\n", //
12701 )
12702 );
12703 });
12704}
12705
12706#[gpui::test]
12707fn test_highlighted_ranges(cx: &mut TestAppContext) {
12708 init_test(cx, |_| {});
12709
12710 let editor = cx.add_window(|window, cx| {
12711 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
12712 build_editor(buffer.clone(), window, cx)
12713 });
12714
12715 _ = editor.update(cx, |editor, window, cx| {
12716 struct Type1;
12717 struct Type2;
12718
12719 let buffer = editor.buffer.read(cx).snapshot(cx);
12720
12721 let anchor_range =
12722 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
12723
12724 editor.highlight_background::<Type1>(
12725 &[
12726 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
12727 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
12728 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
12729 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
12730 ],
12731 |_| Hsla::red(),
12732 cx,
12733 );
12734 editor.highlight_background::<Type2>(
12735 &[
12736 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
12737 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
12738 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
12739 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
12740 ],
12741 |_| Hsla::green(),
12742 cx,
12743 );
12744
12745 let snapshot = editor.snapshot(window, cx);
12746 let mut highlighted_ranges = editor.background_highlights_in_range(
12747 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
12748 &snapshot,
12749 cx.theme().colors(),
12750 );
12751 // Enforce a consistent ordering based on color without relying on the ordering of the
12752 // highlight's `TypeId` which is non-executor.
12753 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
12754 assert_eq!(
12755 highlighted_ranges,
12756 &[
12757 (
12758 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
12759 Hsla::red(),
12760 ),
12761 (
12762 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
12763 Hsla::red(),
12764 ),
12765 (
12766 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
12767 Hsla::green(),
12768 ),
12769 (
12770 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
12771 Hsla::green(),
12772 ),
12773 ]
12774 );
12775 assert_eq!(
12776 editor.background_highlights_in_range(
12777 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
12778 &snapshot,
12779 cx.theme().colors(),
12780 ),
12781 &[(
12782 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
12783 Hsla::red(),
12784 )]
12785 );
12786 });
12787}
12788
12789#[gpui::test]
12790async fn test_following(cx: &mut TestAppContext) {
12791 init_test(cx, |_| {});
12792
12793 let fs = FakeFs::new(cx.executor());
12794 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
12795
12796 let buffer = project.update(cx, |project, cx| {
12797 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
12798 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
12799 });
12800 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
12801 let follower = cx.update(|cx| {
12802 cx.open_window(
12803 WindowOptions {
12804 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
12805 gpui::Point::new(px(0.), px(0.)),
12806 gpui::Point::new(px(10.), px(80.)),
12807 ))),
12808 ..Default::default()
12809 },
12810 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
12811 )
12812 .unwrap()
12813 });
12814
12815 let is_still_following = Rc::new(RefCell::new(true));
12816 let follower_edit_event_count = Rc::new(RefCell::new(0));
12817 let pending_update = Rc::new(RefCell::new(None));
12818 let leader_entity = leader.root(cx).unwrap();
12819 let follower_entity = follower.root(cx).unwrap();
12820 _ = follower.update(cx, {
12821 let update = pending_update.clone();
12822 let is_still_following = is_still_following.clone();
12823 let follower_edit_event_count = follower_edit_event_count.clone();
12824 |_, window, cx| {
12825 cx.subscribe_in(
12826 &leader_entity,
12827 window,
12828 move |_, leader, event, window, cx| {
12829 leader.read(cx).add_event_to_update_proto(
12830 event,
12831 &mut update.borrow_mut(),
12832 window,
12833 cx,
12834 );
12835 },
12836 )
12837 .detach();
12838
12839 cx.subscribe_in(
12840 &follower_entity,
12841 window,
12842 move |_, _, event: &EditorEvent, _window, _cx| {
12843 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
12844 *is_still_following.borrow_mut() = false;
12845 }
12846
12847 if let EditorEvent::BufferEdited = event {
12848 *follower_edit_event_count.borrow_mut() += 1;
12849 }
12850 },
12851 )
12852 .detach();
12853 }
12854 });
12855
12856 // Update the selections only
12857 _ = leader.update(cx, |leader, window, cx| {
12858 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
12859 });
12860 follower
12861 .update(cx, |follower, window, cx| {
12862 follower.apply_update_proto(
12863 &project,
12864 pending_update.borrow_mut().take().unwrap(),
12865 window,
12866 cx,
12867 )
12868 })
12869 .unwrap()
12870 .await
12871 .unwrap();
12872 _ = follower.update(cx, |follower, _, cx| {
12873 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
12874 });
12875 assert!(*is_still_following.borrow());
12876 assert_eq!(*follower_edit_event_count.borrow(), 0);
12877
12878 // Update the scroll position only
12879 _ = leader.update(cx, |leader, window, cx| {
12880 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
12881 });
12882 follower
12883 .update(cx, |follower, window, cx| {
12884 follower.apply_update_proto(
12885 &project,
12886 pending_update.borrow_mut().take().unwrap(),
12887 window,
12888 cx,
12889 )
12890 })
12891 .unwrap()
12892 .await
12893 .unwrap();
12894 assert_eq!(
12895 follower
12896 .update(cx, |follower, _, cx| follower.scroll_position(cx))
12897 .unwrap(),
12898 gpui::Point::new(1.5, 3.5)
12899 );
12900 assert!(*is_still_following.borrow());
12901 assert_eq!(*follower_edit_event_count.borrow(), 0);
12902
12903 // Update the selections and scroll position. The follower's scroll position is updated
12904 // via autoscroll, not via the leader's exact scroll position.
12905 _ = leader.update(cx, |leader, window, cx| {
12906 leader.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
12907 leader.request_autoscroll(Autoscroll::newest(), cx);
12908 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
12909 });
12910 follower
12911 .update(cx, |follower, window, cx| {
12912 follower.apply_update_proto(
12913 &project,
12914 pending_update.borrow_mut().take().unwrap(),
12915 window,
12916 cx,
12917 )
12918 })
12919 .unwrap()
12920 .await
12921 .unwrap();
12922 _ = follower.update(cx, |follower, _, cx| {
12923 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
12924 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
12925 });
12926 assert!(*is_still_following.borrow());
12927
12928 // Creating a pending selection that precedes another selection
12929 _ = leader.update(cx, |leader, window, cx| {
12930 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
12931 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
12932 });
12933 follower
12934 .update(cx, |follower, window, cx| {
12935 follower.apply_update_proto(
12936 &project,
12937 pending_update.borrow_mut().take().unwrap(),
12938 window,
12939 cx,
12940 )
12941 })
12942 .unwrap()
12943 .await
12944 .unwrap();
12945 _ = follower.update(cx, |follower, _, cx| {
12946 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
12947 });
12948 assert!(*is_still_following.borrow());
12949
12950 // Extend the pending selection so that it surrounds another selection
12951 _ = leader.update(cx, |leader, window, cx| {
12952 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
12953 });
12954 follower
12955 .update(cx, |follower, window, cx| {
12956 follower.apply_update_proto(
12957 &project,
12958 pending_update.borrow_mut().take().unwrap(),
12959 window,
12960 cx,
12961 )
12962 })
12963 .unwrap()
12964 .await
12965 .unwrap();
12966 _ = follower.update(cx, |follower, _, cx| {
12967 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
12968 });
12969
12970 // Scrolling locally breaks the follow
12971 _ = follower.update(cx, |follower, window, cx| {
12972 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
12973 follower.set_scroll_anchor(
12974 ScrollAnchor {
12975 anchor: top_anchor,
12976 offset: gpui::Point::new(0.0, 0.5),
12977 },
12978 window,
12979 cx,
12980 );
12981 });
12982 assert!(!(*is_still_following.borrow()));
12983}
12984
12985#[gpui::test]
12986async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
12987 init_test(cx, |_| {});
12988
12989 let fs = FakeFs::new(cx.executor());
12990 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
12991 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12992 let pane = workspace
12993 .update(cx, |workspace, _, _| workspace.active_pane().clone())
12994 .unwrap();
12995
12996 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
12997
12998 let leader = pane.update_in(cx, |_, window, cx| {
12999 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
13000 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
13001 });
13002
13003 // Start following the editor when it has no excerpts.
13004 let mut state_message =
13005 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
13006 let workspace_entity = workspace.root(cx).unwrap();
13007 let follower_1 = cx
13008 .update_window(*workspace.deref(), |_, window, cx| {
13009 Editor::from_state_proto(
13010 workspace_entity,
13011 ViewId {
13012 creator: CollaboratorId::PeerId(PeerId::default()),
13013 id: 0,
13014 },
13015 &mut state_message,
13016 window,
13017 cx,
13018 )
13019 })
13020 .unwrap()
13021 .unwrap()
13022 .await
13023 .unwrap();
13024
13025 let update_message = Rc::new(RefCell::new(None));
13026 follower_1.update_in(cx, {
13027 let update = update_message.clone();
13028 |_, window, cx| {
13029 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
13030 leader.read(cx).add_event_to_update_proto(
13031 event,
13032 &mut update.borrow_mut(),
13033 window,
13034 cx,
13035 );
13036 })
13037 .detach();
13038 }
13039 });
13040
13041 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
13042 (
13043 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
13044 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
13045 )
13046 });
13047
13048 // Insert some excerpts.
13049 leader.update(cx, |leader, cx| {
13050 leader.buffer.update(cx, |multibuffer, cx| {
13051 multibuffer.set_excerpts_for_path(
13052 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
13053 buffer_1.clone(),
13054 vec![
13055 Point::row_range(0..3),
13056 Point::row_range(1..6),
13057 Point::row_range(12..15),
13058 ],
13059 0,
13060 cx,
13061 );
13062 multibuffer.set_excerpts_for_path(
13063 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
13064 buffer_2.clone(),
13065 vec![Point::row_range(0..6), Point::row_range(8..12)],
13066 0,
13067 cx,
13068 );
13069 });
13070 });
13071
13072 // Apply the update of adding the excerpts.
13073 follower_1
13074 .update_in(cx, |follower, window, cx| {
13075 follower.apply_update_proto(
13076 &project,
13077 update_message.borrow().clone().unwrap(),
13078 window,
13079 cx,
13080 )
13081 })
13082 .await
13083 .unwrap();
13084 assert_eq!(
13085 follower_1.update(cx, |editor, cx| editor.text(cx)),
13086 leader.update(cx, |editor, cx| editor.text(cx))
13087 );
13088 update_message.borrow_mut().take();
13089
13090 // Start following separately after it already has excerpts.
13091 let mut state_message =
13092 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
13093 let workspace_entity = workspace.root(cx).unwrap();
13094 let follower_2 = cx
13095 .update_window(*workspace.deref(), |_, window, cx| {
13096 Editor::from_state_proto(
13097 workspace_entity,
13098 ViewId {
13099 creator: CollaboratorId::PeerId(PeerId::default()),
13100 id: 0,
13101 },
13102 &mut state_message,
13103 window,
13104 cx,
13105 )
13106 })
13107 .unwrap()
13108 .unwrap()
13109 .await
13110 .unwrap();
13111 assert_eq!(
13112 follower_2.update(cx, |editor, cx| editor.text(cx)),
13113 leader.update(cx, |editor, cx| editor.text(cx))
13114 );
13115
13116 // Remove some excerpts.
13117 leader.update(cx, |leader, cx| {
13118 leader.buffer.update(cx, |multibuffer, cx| {
13119 let excerpt_ids = multibuffer.excerpt_ids();
13120 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
13121 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
13122 });
13123 });
13124
13125 // Apply the update of removing the excerpts.
13126 follower_1
13127 .update_in(cx, |follower, window, cx| {
13128 follower.apply_update_proto(
13129 &project,
13130 update_message.borrow().clone().unwrap(),
13131 window,
13132 cx,
13133 )
13134 })
13135 .await
13136 .unwrap();
13137 follower_2
13138 .update_in(cx, |follower, window, cx| {
13139 follower.apply_update_proto(
13140 &project,
13141 update_message.borrow().clone().unwrap(),
13142 window,
13143 cx,
13144 )
13145 })
13146 .await
13147 .unwrap();
13148 update_message.borrow_mut().take();
13149 assert_eq!(
13150 follower_1.update(cx, |editor, cx| editor.text(cx)),
13151 leader.update(cx, |editor, cx| editor.text(cx))
13152 );
13153}
13154
13155#[gpui::test]
13156async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
13157 init_test(cx, |_| {});
13158
13159 let mut cx = EditorTestContext::new(cx).await;
13160 let lsp_store =
13161 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
13162
13163 cx.set_state(indoc! {"
13164 ˇfn func(abc def: i32) -> u32 {
13165 }
13166 "});
13167
13168 cx.update(|_, cx| {
13169 lsp_store.update(cx, |lsp_store, cx| {
13170 lsp_store
13171 .update_diagnostics(
13172 LanguageServerId(0),
13173 lsp::PublishDiagnosticsParams {
13174 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
13175 version: None,
13176 diagnostics: vec![
13177 lsp::Diagnostic {
13178 range: lsp::Range::new(
13179 lsp::Position::new(0, 11),
13180 lsp::Position::new(0, 12),
13181 ),
13182 severity: Some(lsp::DiagnosticSeverity::ERROR),
13183 ..Default::default()
13184 },
13185 lsp::Diagnostic {
13186 range: lsp::Range::new(
13187 lsp::Position::new(0, 12),
13188 lsp::Position::new(0, 15),
13189 ),
13190 severity: Some(lsp::DiagnosticSeverity::ERROR),
13191 ..Default::default()
13192 },
13193 lsp::Diagnostic {
13194 range: lsp::Range::new(
13195 lsp::Position::new(0, 25),
13196 lsp::Position::new(0, 28),
13197 ),
13198 severity: Some(lsp::DiagnosticSeverity::ERROR),
13199 ..Default::default()
13200 },
13201 ],
13202 },
13203 &[],
13204 cx,
13205 )
13206 .unwrap()
13207 });
13208 });
13209
13210 executor.run_until_parked();
13211
13212 cx.update_editor(|editor, window, cx| {
13213 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13214 });
13215
13216 cx.assert_editor_state(indoc! {"
13217 fn func(abc def: i32) -> ˇu32 {
13218 }
13219 "});
13220
13221 cx.update_editor(|editor, window, cx| {
13222 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13223 });
13224
13225 cx.assert_editor_state(indoc! {"
13226 fn func(abc ˇdef: i32) -> u32 {
13227 }
13228 "});
13229
13230 cx.update_editor(|editor, window, cx| {
13231 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13232 });
13233
13234 cx.assert_editor_state(indoc! {"
13235 fn func(abcˇ def: i32) -> u32 {
13236 }
13237 "});
13238
13239 cx.update_editor(|editor, window, cx| {
13240 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13241 });
13242
13243 cx.assert_editor_state(indoc! {"
13244 fn func(abc def: i32) -> ˇu32 {
13245 }
13246 "});
13247}
13248
13249#[gpui::test]
13250async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
13251 init_test(cx, |_| {});
13252
13253 let mut cx = EditorTestContext::new(cx).await;
13254
13255 let diff_base = r#"
13256 use some::mod;
13257
13258 const A: u32 = 42;
13259
13260 fn main() {
13261 println!("hello");
13262
13263 println!("world");
13264 }
13265 "#
13266 .unindent();
13267
13268 // Edits are modified, removed, modified, added
13269 cx.set_state(
13270 &r#"
13271 use some::modified;
13272
13273 ˇ
13274 fn main() {
13275 println!("hello there");
13276
13277 println!("around the");
13278 println!("world");
13279 }
13280 "#
13281 .unindent(),
13282 );
13283
13284 cx.set_head_text(&diff_base);
13285 executor.run_until_parked();
13286
13287 cx.update_editor(|editor, window, cx| {
13288 //Wrap around the bottom of the buffer
13289 for _ in 0..3 {
13290 editor.go_to_next_hunk(&GoToHunk, window, cx);
13291 }
13292 });
13293
13294 cx.assert_editor_state(
13295 &r#"
13296 ˇuse some::modified;
13297
13298
13299 fn main() {
13300 println!("hello there");
13301
13302 println!("around the");
13303 println!("world");
13304 }
13305 "#
13306 .unindent(),
13307 );
13308
13309 cx.update_editor(|editor, window, cx| {
13310 //Wrap around the top of the buffer
13311 for _ in 0..2 {
13312 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13313 }
13314 });
13315
13316 cx.assert_editor_state(
13317 &r#"
13318 use some::modified;
13319
13320
13321 fn main() {
13322 ˇ println!("hello there");
13323
13324 println!("around the");
13325 println!("world");
13326 }
13327 "#
13328 .unindent(),
13329 );
13330
13331 cx.update_editor(|editor, window, cx| {
13332 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13333 });
13334
13335 cx.assert_editor_state(
13336 &r#"
13337 use some::modified;
13338
13339 ˇ
13340 fn main() {
13341 println!("hello there");
13342
13343 println!("around the");
13344 println!("world");
13345 }
13346 "#
13347 .unindent(),
13348 );
13349
13350 cx.update_editor(|editor, window, cx| {
13351 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13352 });
13353
13354 cx.assert_editor_state(
13355 &r#"
13356 ˇuse some::modified;
13357
13358
13359 fn main() {
13360 println!("hello there");
13361
13362 println!("around the");
13363 println!("world");
13364 }
13365 "#
13366 .unindent(),
13367 );
13368
13369 cx.update_editor(|editor, window, cx| {
13370 for _ in 0..2 {
13371 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13372 }
13373 });
13374
13375 cx.assert_editor_state(
13376 &r#"
13377 use some::modified;
13378
13379
13380 fn main() {
13381 ˇ println!("hello there");
13382
13383 println!("around the");
13384 println!("world");
13385 }
13386 "#
13387 .unindent(),
13388 );
13389
13390 cx.update_editor(|editor, window, cx| {
13391 editor.fold(&Fold, window, cx);
13392 });
13393
13394 cx.update_editor(|editor, window, cx| {
13395 editor.go_to_next_hunk(&GoToHunk, window, cx);
13396 });
13397
13398 cx.assert_editor_state(
13399 &r#"
13400 ˇuse some::modified;
13401
13402
13403 fn main() {
13404 println!("hello there");
13405
13406 println!("around the");
13407 println!("world");
13408 }
13409 "#
13410 .unindent(),
13411 );
13412}
13413
13414#[test]
13415fn test_split_words() {
13416 fn split(text: &str) -> Vec<&str> {
13417 split_words(text).collect()
13418 }
13419
13420 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
13421 assert_eq!(split("hello_world"), &["hello_", "world"]);
13422 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
13423 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
13424 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
13425 assert_eq!(split("helloworld"), &["helloworld"]);
13426
13427 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
13428}
13429
13430#[gpui::test]
13431async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
13432 init_test(cx, |_| {});
13433
13434 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
13435 let mut assert = |before, after| {
13436 let _state_context = cx.set_state(before);
13437 cx.run_until_parked();
13438 cx.update_editor(|editor, window, cx| {
13439 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
13440 });
13441 cx.run_until_parked();
13442 cx.assert_editor_state(after);
13443 };
13444
13445 // Outside bracket jumps to outside of matching bracket
13446 assert("console.logˇ(var);", "console.log(var)ˇ;");
13447 assert("console.log(var)ˇ;", "console.logˇ(var);");
13448
13449 // Inside bracket jumps to inside of matching bracket
13450 assert("console.log(ˇvar);", "console.log(varˇ);");
13451 assert("console.log(varˇ);", "console.log(ˇvar);");
13452
13453 // When outside a bracket and inside, favor jumping to the inside bracket
13454 assert(
13455 "console.log('foo', [1, 2, 3]ˇ);",
13456 "console.log(ˇ'foo', [1, 2, 3]);",
13457 );
13458 assert(
13459 "console.log(ˇ'foo', [1, 2, 3]);",
13460 "console.log('foo', [1, 2, 3]ˇ);",
13461 );
13462
13463 // Bias forward if two options are equally likely
13464 assert(
13465 "let result = curried_fun()ˇ();",
13466 "let result = curried_fun()()ˇ;",
13467 );
13468
13469 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
13470 assert(
13471 indoc! {"
13472 function test() {
13473 console.log('test')ˇ
13474 }"},
13475 indoc! {"
13476 function test() {
13477 console.logˇ('test')
13478 }"},
13479 );
13480}
13481
13482#[gpui::test]
13483async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
13484 init_test(cx, |_| {});
13485
13486 let fs = FakeFs::new(cx.executor());
13487 fs.insert_tree(
13488 path!("/a"),
13489 json!({
13490 "main.rs": "fn main() { let a = 5; }",
13491 "other.rs": "// Test file",
13492 }),
13493 )
13494 .await;
13495 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13496
13497 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13498 language_registry.add(Arc::new(Language::new(
13499 LanguageConfig {
13500 name: "Rust".into(),
13501 matcher: LanguageMatcher {
13502 path_suffixes: vec!["rs".to_string()],
13503 ..Default::default()
13504 },
13505 brackets: BracketPairConfig {
13506 pairs: vec![BracketPair {
13507 start: "{".to_string(),
13508 end: "}".to_string(),
13509 close: true,
13510 surround: true,
13511 newline: true,
13512 }],
13513 disabled_scopes_by_bracket_ix: Vec::new(),
13514 },
13515 ..Default::default()
13516 },
13517 Some(tree_sitter_rust::LANGUAGE.into()),
13518 )));
13519 let mut fake_servers = language_registry.register_fake_lsp(
13520 "Rust",
13521 FakeLspAdapter {
13522 capabilities: lsp::ServerCapabilities {
13523 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
13524 first_trigger_character: "{".to_string(),
13525 more_trigger_character: None,
13526 }),
13527 ..Default::default()
13528 },
13529 ..Default::default()
13530 },
13531 );
13532
13533 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13534
13535 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13536
13537 let worktree_id = workspace
13538 .update(cx, |workspace, _, cx| {
13539 workspace.project().update(cx, |project, cx| {
13540 project.worktrees(cx).next().unwrap().read(cx).id()
13541 })
13542 })
13543 .unwrap();
13544
13545 let buffer = project
13546 .update(cx, |project, cx| {
13547 project.open_local_buffer(path!("/a/main.rs"), cx)
13548 })
13549 .await
13550 .unwrap();
13551 let editor_handle = workspace
13552 .update(cx, |workspace, window, cx| {
13553 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
13554 })
13555 .unwrap()
13556 .await
13557 .unwrap()
13558 .downcast::<Editor>()
13559 .unwrap();
13560
13561 cx.executor().start_waiting();
13562 let fake_server = fake_servers.next().await.unwrap();
13563
13564 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
13565 |params, _| async move {
13566 assert_eq!(
13567 params.text_document_position.text_document.uri,
13568 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
13569 );
13570 assert_eq!(
13571 params.text_document_position.position,
13572 lsp::Position::new(0, 21),
13573 );
13574
13575 Ok(Some(vec![lsp::TextEdit {
13576 new_text: "]".to_string(),
13577 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13578 }]))
13579 },
13580 );
13581
13582 editor_handle.update_in(cx, |editor, window, cx| {
13583 window.focus(&editor.focus_handle(cx));
13584 editor.change_selections(None, window, cx, |s| {
13585 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
13586 });
13587 editor.handle_input("{", window, cx);
13588 });
13589
13590 cx.executor().run_until_parked();
13591
13592 buffer.update(cx, |buffer, _| {
13593 assert_eq!(
13594 buffer.text(),
13595 "fn main() { let a = {5}; }",
13596 "No extra braces from on type formatting should appear in the buffer"
13597 )
13598 });
13599}
13600
13601#[gpui::test]
13602async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
13603 init_test(cx, |_| {});
13604
13605 let fs = FakeFs::new(cx.executor());
13606 fs.insert_tree(
13607 path!("/a"),
13608 json!({
13609 "main.rs": "fn main() { let a = 5; }",
13610 "other.rs": "// Test file",
13611 }),
13612 )
13613 .await;
13614
13615 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13616
13617 let server_restarts = Arc::new(AtomicUsize::new(0));
13618 let closure_restarts = Arc::clone(&server_restarts);
13619 let language_server_name = "test language server";
13620 let language_name: LanguageName = "Rust".into();
13621
13622 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13623 language_registry.add(Arc::new(Language::new(
13624 LanguageConfig {
13625 name: language_name.clone(),
13626 matcher: LanguageMatcher {
13627 path_suffixes: vec!["rs".to_string()],
13628 ..Default::default()
13629 },
13630 ..Default::default()
13631 },
13632 Some(tree_sitter_rust::LANGUAGE.into()),
13633 )));
13634 let mut fake_servers = language_registry.register_fake_lsp(
13635 "Rust",
13636 FakeLspAdapter {
13637 name: language_server_name,
13638 initialization_options: Some(json!({
13639 "testOptionValue": true
13640 })),
13641 initializer: Some(Box::new(move |fake_server| {
13642 let task_restarts = Arc::clone(&closure_restarts);
13643 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
13644 task_restarts.fetch_add(1, atomic::Ordering::Release);
13645 futures::future::ready(Ok(()))
13646 });
13647 })),
13648 ..Default::default()
13649 },
13650 );
13651
13652 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13653 let _buffer = project
13654 .update(cx, |project, cx| {
13655 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
13656 })
13657 .await
13658 .unwrap();
13659 let _fake_server = fake_servers.next().await.unwrap();
13660 update_test_language_settings(cx, |language_settings| {
13661 language_settings.languages.insert(
13662 language_name.clone(),
13663 LanguageSettingsContent {
13664 tab_size: NonZeroU32::new(8),
13665 ..Default::default()
13666 },
13667 );
13668 });
13669 cx.executor().run_until_parked();
13670 assert_eq!(
13671 server_restarts.load(atomic::Ordering::Acquire),
13672 0,
13673 "Should not restart LSP server on an unrelated change"
13674 );
13675
13676 update_test_project_settings(cx, |project_settings| {
13677 project_settings.lsp.insert(
13678 "Some other server name".into(),
13679 LspSettings {
13680 binary: None,
13681 settings: None,
13682 initialization_options: Some(json!({
13683 "some other init value": false
13684 })),
13685 enable_lsp_tasks: false,
13686 },
13687 );
13688 });
13689 cx.executor().run_until_parked();
13690 assert_eq!(
13691 server_restarts.load(atomic::Ordering::Acquire),
13692 0,
13693 "Should not restart LSP server on an unrelated LSP settings change"
13694 );
13695
13696 update_test_project_settings(cx, |project_settings| {
13697 project_settings.lsp.insert(
13698 language_server_name.into(),
13699 LspSettings {
13700 binary: None,
13701 settings: None,
13702 initialization_options: Some(json!({
13703 "anotherInitValue": false
13704 })),
13705 enable_lsp_tasks: false,
13706 },
13707 );
13708 });
13709 cx.executor().run_until_parked();
13710 assert_eq!(
13711 server_restarts.load(atomic::Ordering::Acquire),
13712 1,
13713 "Should restart LSP server on a related LSP settings change"
13714 );
13715
13716 update_test_project_settings(cx, |project_settings| {
13717 project_settings.lsp.insert(
13718 language_server_name.into(),
13719 LspSettings {
13720 binary: None,
13721 settings: None,
13722 initialization_options: Some(json!({
13723 "anotherInitValue": false
13724 })),
13725 enable_lsp_tasks: false,
13726 },
13727 );
13728 });
13729 cx.executor().run_until_parked();
13730 assert_eq!(
13731 server_restarts.load(atomic::Ordering::Acquire),
13732 1,
13733 "Should not restart LSP server on a related LSP settings change that is the same"
13734 );
13735
13736 update_test_project_settings(cx, |project_settings| {
13737 project_settings.lsp.insert(
13738 language_server_name.into(),
13739 LspSettings {
13740 binary: None,
13741 settings: None,
13742 initialization_options: None,
13743 enable_lsp_tasks: false,
13744 },
13745 );
13746 });
13747 cx.executor().run_until_parked();
13748 assert_eq!(
13749 server_restarts.load(atomic::Ordering::Acquire),
13750 2,
13751 "Should restart LSP server on another related LSP settings change"
13752 );
13753}
13754
13755#[gpui::test]
13756async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
13757 init_test(cx, |_| {});
13758
13759 let mut cx = EditorLspTestContext::new_rust(
13760 lsp::ServerCapabilities {
13761 completion_provider: Some(lsp::CompletionOptions {
13762 trigger_characters: Some(vec![".".to_string()]),
13763 resolve_provider: Some(true),
13764 ..Default::default()
13765 }),
13766 ..Default::default()
13767 },
13768 cx,
13769 )
13770 .await;
13771
13772 cx.set_state("fn main() { let a = 2ˇ; }");
13773 cx.simulate_keystroke(".");
13774 let completion_item = lsp::CompletionItem {
13775 label: "some".into(),
13776 kind: Some(lsp::CompletionItemKind::SNIPPET),
13777 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
13778 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
13779 kind: lsp::MarkupKind::Markdown,
13780 value: "```rust\nSome(2)\n```".to_string(),
13781 })),
13782 deprecated: Some(false),
13783 sort_text: Some("fffffff2".to_string()),
13784 filter_text: Some("some".to_string()),
13785 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
13786 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13787 range: lsp::Range {
13788 start: lsp::Position {
13789 line: 0,
13790 character: 22,
13791 },
13792 end: lsp::Position {
13793 line: 0,
13794 character: 22,
13795 },
13796 },
13797 new_text: "Some(2)".to_string(),
13798 })),
13799 additional_text_edits: Some(vec![lsp::TextEdit {
13800 range: lsp::Range {
13801 start: lsp::Position {
13802 line: 0,
13803 character: 20,
13804 },
13805 end: lsp::Position {
13806 line: 0,
13807 character: 22,
13808 },
13809 },
13810 new_text: "".to_string(),
13811 }]),
13812 ..Default::default()
13813 };
13814
13815 let closure_completion_item = completion_item.clone();
13816 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13817 let task_completion_item = closure_completion_item.clone();
13818 async move {
13819 Ok(Some(lsp::CompletionResponse::Array(vec![
13820 task_completion_item,
13821 ])))
13822 }
13823 });
13824
13825 request.next().await;
13826
13827 cx.condition(|editor, _| editor.context_menu_visible())
13828 .await;
13829 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13830 editor
13831 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13832 .unwrap()
13833 });
13834 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
13835
13836 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
13837 let task_completion_item = completion_item.clone();
13838 async move { Ok(task_completion_item) }
13839 })
13840 .next()
13841 .await
13842 .unwrap();
13843 apply_additional_edits.await.unwrap();
13844 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
13845}
13846
13847#[gpui::test]
13848async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
13849 init_test(cx, |_| {});
13850
13851 let mut cx = EditorLspTestContext::new_rust(
13852 lsp::ServerCapabilities {
13853 completion_provider: Some(lsp::CompletionOptions {
13854 trigger_characters: Some(vec![".".to_string()]),
13855 resolve_provider: Some(true),
13856 ..Default::default()
13857 }),
13858 ..Default::default()
13859 },
13860 cx,
13861 )
13862 .await;
13863
13864 cx.set_state("fn main() { let a = 2ˇ; }");
13865 cx.simulate_keystroke(".");
13866
13867 let item1 = lsp::CompletionItem {
13868 label: "method id()".to_string(),
13869 filter_text: Some("id".to_string()),
13870 detail: None,
13871 documentation: None,
13872 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13873 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13874 new_text: ".id".to_string(),
13875 })),
13876 ..lsp::CompletionItem::default()
13877 };
13878
13879 let item2 = lsp::CompletionItem {
13880 label: "other".to_string(),
13881 filter_text: Some("other".to_string()),
13882 detail: None,
13883 documentation: None,
13884 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13885 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13886 new_text: ".other".to_string(),
13887 })),
13888 ..lsp::CompletionItem::default()
13889 };
13890
13891 let item1 = item1.clone();
13892 cx.set_request_handler::<lsp::request::Completion, _, _>({
13893 let item1 = item1.clone();
13894 move |_, _, _| {
13895 let item1 = item1.clone();
13896 let item2 = item2.clone();
13897 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
13898 }
13899 })
13900 .next()
13901 .await;
13902
13903 cx.condition(|editor, _| editor.context_menu_visible())
13904 .await;
13905 cx.update_editor(|editor, _, _| {
13906 let context_menu = editor.context_menu.borrow_mut();
13907 let context_menu = context_menu
13908 .as_ref()
13909 .expect("Should have the context menu deployed");
13910 match context_menu {
13911 CodeContextMenu::Completions(completions_menu) => {
13912 let completions = completions_menu.completions.borrow_mut();
13913 assert_eq!(
13914 completions
13915 .iter()
13916 .map(|completion| &completion.label.text)
13917 .collect::<Vec<_>>(),
13918 vec!["method id()", "other"]
13919 )
13920 }
13921 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
13922 }
13923 });
13924
13925 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
13926 let item1 = item1.clone();
13927 move |_, item_to_resolve, _| {
13928 let item1 = item1.clone();
13929 async move {
13930 if item1 == item_to_resolve {
13931 Ok(lsp::CompletionItem {
13932 label: "method id()".to_string(),
13933 filter_text: Some("id".to_string()),
13934 detail: Some("Now resolved!".to_string()),
13935 documentation: Some(lsp::Documentation::String("Docs".to_string())),
13936 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13937 range: lsp::Range::new(
13938 lsp::Position::new(0, 22),
13939 lsp::Position::new(0, 22),
13940 ),
13941 new_text: ".id".to_string(),
13942 })),
13943 ..lsp::CompletionItem::default()
13944 })
13945 } else {
13946 Ok(item_to_resolve)
13947 }
13948 }
13949 }
13950 })
13951 .next()
13952 .await
13953 .unwrap();
13954 cx.run_until_parked();
13955
13956 cx.update_editor(|editor, window, cx| {
13957 editor.context_menu_next(&Default::default(), window, cx);
13958 });
13959
13960 cx.update_editor(|editor, _, _| {
13961 let context_menu = editor.context_menu.borrow_mut();
13962 let context_menu = context_menu
13963 .as_ref()
13964 .expect("Should have the context menu deployed");
13965 match context_menu {
13966 CodeContextMenu::Completions(completions_menu) => {
13967 let completions = completions_menu.completions.borrow_mut();
13968 assert_eq!(
13969 completions
13970 .iter()
13971 .map(|completion| &completion.label.text)
13972 .collect::<Vec<_>>(),
13973 vec!["method id() Now resolved!", "other"],
13974 "Should update first completion label, but not second as the filter text did not match."
13975 );
13976 }
13977 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
13978 }
13979 });
13980}
13981
13982#[gpui::test]
13983async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
13984 init_test(cx, |_| {});
13985
13986 let mut cx = EditorLspTestContext::new_rust(
13987 lsp::ServerCapabilities {
13988 completion_provider: Some(lsp::CompletionOptions {
13989 trigger_characters: Some(vec![".".to_string()]),
13990 resolve_provider: Some(true),
13991 ..Default::default()
13992 }),
13993 ..Default::default()
13994 },
13995 cx,
13996 )
13997 .await;
13998
13999 cx.set_state("fn main() { let a = 2ˇ; }");
14000 cx.simulate_keystroke(".");
14001
14002 let unresolved_item_1 = lsp::CompletionItem {
14003 label: "id".to_string(),
14004 filter_text: Some("id".to_string()),
14005 detail: None,
14006 documentation: None,
14007 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14008 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14009 new_text: ".id".to_string(),
14010 })),
14011 ..lsp::CompletionItem::default()
14012 };
14013 let resolved_item_1 = lsp::CompletionItem {
14014 additional_text_edits: Some(vec![lsp::TextEdit {
14015 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
14016 new_text: "!!".to_string(),
14017 }]),
14018 ..unresolved_item_1.clone()
14019 };
14020 let unresolved_item_2 = lsp::CompletionItem {
14021 label: "other".to_string(),
14022 filter_text: Some("other".to_string()),
14023 detail: None,
14024 documentation: None,
14025 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14026 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14027 new_text: ".other".to_string(),
14028 })),
14029 ..lsp::CompletionItem::default()
14030 };
14031 let resolved_item_2 = lsp::CompletionItem {
14032 additional_text_edits: Some(vec![lsp::TextEdit {
14033 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
14034 new_text: "??".to_string(),
14035 }]),
14036 ..unresolved_item_2.clone()
14037 };
14038
14039 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
14040 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
14041 cx.lsp
14042 .server
14043 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
14044 let unresolved_item_1 = unresolved_item_1.clone();
14045 let resolved_item_1 = resolved_item_1.clone();
14046 let unresolved_item_2 = unresolved_item_2.clone();
14047 let resolved_item_2 = resolved_item_2.clone();
14048 let resolve_requests_1 = resolve_requests_1.clone();
14049 let resolve_requests_2 = resolve_requests_2.clone();
14050 move |unresolved_request, _| {
14051 let unresolved_item_1 = unresolved_item_1.clone();
14052 let resolved_item_1 = resolved_item_1.clone();
14053 let unresolved_item_2 = unresolved_item_2.clone();
14054 let resolved_item_2 = resolved_item_2.clone();
14055 let resolve_requests_1 = resolve_requests_1.clone();
14056 let resolve_requests_2 = resolve_requests_2.clone();
14057 async move {
14058 if unresolved_request == unresolved_item_1 {
14059 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
14060 Ok(resolved_item_1.clone())
14061 } else if unresolved_request == unresolved_item_2 {
14062 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
14063 Ok(resolved_item_2.clone())
14064 } else {
14065 panic!("Unexpected completion item {unresolved_request:?}")
14066 }
14067 }
14068 }
14069 })
14070 .detach();
14071
14072 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14073 let unresolved_item_1 = unresolved_item_1.clone();
14074 let unresolved_item_2 = unresolved_item_2.clone();
14075 async move {
14076 Ok(Some(lsp::CompletionResponse::Array(vec![
14077 unresolved_item_1,
14078 unresolved_item_2,
14079 ])))
14080 }
14081 })
14082 .next()
14083 .await;
14084
14085 cx.condition(|editor, _| editor.context_menu_visible())
14086 .await;
14087 cx.update_editor(|editor, _, _| {
14088 let context_menu = editor.context_menu.borrow_mut();
14089 let context_menu = context_menu
14090 .as_ref()
14091 .expect("Should have the context menu deployed");
14092 match context_menu {
14093 CodeContextMenu::Completions(completions_menu) => {
14094 let completions = completions_menu.completions.borrow_mut();
14095 assert_eq!(
14096 completions
14097 .iter()
14098 .map(|completion| &completion.label.text)
14099 .collect::<Vec<_>>(),
14100 vec!["id", "other"]
14101 )
14102 }
14103 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
14104 }
14105 });
14106 cx.run_until_parked();
14107
14108 cx.update_editor(|editor, window, cx| {
14109 editor.context_menu_next(&ContextMenuNext, window, cx);
14110 });
14111 cx.run_until_parked();
14112 cx.update_editor(|editor, window, cx| {
14113 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
14114 });
14115 cx.run_until_parked();
14116 cx.update_editor(|editor, window, cx| {
14117 editor.context_menu_next(&ContextMenuNext, window, cx);
14118 });
14119 cx.run_until_parked();
14120 cx.update_editor(|editor, window, cx| {
14121 editor
14122 .compose_completion(&ComposeCompletion::default(), window, cx)
14123 .expect("No task returned")
14124 })
14125 .await
14126 .expect("Completion failed");
14127 cx.run_until_parked();
14128
14129 cx.update_editor(|editor, _, cx| {
14130 assert_eq!(
14131 resolve_requests_1.load(atomic::Ordering::Acquire),
14132 1,
14133 "Should always resolve once despite multiple selections"
14134 );
14135 assert_eq!(
14136 resolve_requests_2.load(atomic::Ordering::Acquire),
14137 1,
14138 "Should always resolve once after multiple selections and applying the completion"
14139 );
14140 assert_eq!(
14141 editor.text(cx),
14142 "fn main() { let a = ??.other; }",
14143 "Should use resolved data when applying the completion"
14144 );
14145 });
14146}
14147
14148#[gpui::test]
14149async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
14150 init_test(cx, |_| {});
14151
14152 let item_0 = lsp::CompletionItem {
14153 label: "abs".into(),
14154 insert_text: Some("abs".into()),
14155 data: Some(json!({ "very": "special"})),
14156 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
14157 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
14158 lsp::InsertReplaceEdit {
14159 new_text: "abs".to_string(),
14160 insert: lsp::Range::default(),
14161 replace: lsp::Range::default(),
14162 },
14163 )),
14164 ..lsp::CompletionItem::default()
14165 };
14166 let items = iter::once(item_0.clone())
14167 .chain((11..51).map(|i| lsp::CompletionItem {
14168 label: format!("item_{}", i),
14169 insert_text: Some(format!("item_{}", i)),
14170 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
14171 ..lsp::CompletionItem::default()
14172 }))
14173 .collect::<Vec<_>>();
14174
14175 let default_commit_characters = vec!["?".to_string()];
14176 let default_data = json!({ "default": "data"});
14177 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
14178 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
14179 let default_edit_range = lsp::Range {
14180 start: lsp::Position {
14181 line: 0,
14182 character: 5,
14183 },
14184 end: lsp::Position {
14185 line: 0,
14186 character: 5,
14187 },
14188 };
14189
14190 let mut cx = EditorLspTestContext::new_rust(
14191 lsp::ServerCapabilities {
14192 completion_provider: Some(lsp::CompletionOptions {
14193 trigger_characters: Some(vec![".".to_string()]),
14194 resolve_provider: Some(true),
14195 ..Default::default()
14196 }),
14197 ..Default::default()
14198 },
14199 cx,
14200 )
14201 .await;
14202
14203 cx.set_state("fn main() { let a = 2ˇ; }");
14204 cx.simulate_keystroke(".");
14205
14206 let completion_data = default_data.clone();
14207 let completion_characters = default_commit_characters.clone();
14208 let completion_items = items.clone();
14209 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14210 let default_data = completion_data.clone();
14211 let default_commit_characters = completion_characters.clone();
14212 let items = completion_items.clone();
14213 async move {
14214 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
14215 items,
14216 item_defaults: Some(lsp::CompletionListItemDefaults {
14217 data: Some(default_data.clone()),
14218 commit_characters: Some(default_commit_characters.clone()),
14219 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
14220 default_edit_range,
14221 )),
14222 insert_text_format: Some(default_insert_text_format),
14223 insert_text_mode: Some(default_insert_text_mode),
14224 }),
14225 ..lsp::CompletionList::default()
14226 })))
14227 }
14228 })
14229 .next()
14230 .await;
14231
14232 let resolved_items = Arc::new(Mutex::new(Vec::new()));
14233 cx.lsp
14234 .server
14235 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
14236 let closure_resolved_items = resolved_items.clone();
14237 move |item_to_resolve, _| {
14238 let closure_resolved_items = closure_resolved_items.clone();
14239 async move {
14240 closure_resolved_items.lock().push(item_to_resolve.clone());
14241 Ok(item_to_resolve)
14242 }
14243 }
14244 })
14245 .detach();
14246
14247 cx.condition(|editor, _| editor.context_menu_visible())
14248 .await;
14249 cx.run_until_parked();
14250 cx.update_editor(|editor, _, _| {
14251 let menu = editor.context_menu.borrow_mut();
14252 match menu.as_ref().expect("should have the completions menu") {
14253 CodeContextMenu::Completions(completions_menu) => {
14254 assert_eq!(
14255 completions_menu
14256 .entries
14257 .borrow()
14258 .iter()
14259 .map(|mat| mat.string.clone())
14260 .collect::<Vec<String>>(),
14261 items
14262 .iter()
14263 .map(|completion| completion.label.clone())
14264 .collect::<Vec<String>>()
14265 );
14266 }
14267 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
14268 }
14269 });
14270 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
14271 // with 4 from the end.
14272 assert_eq!(
14273 *resolved_items.lock(),
14274 [&items[0..16], &items[items.len() - 4..items.len()]]
14275 .concat()
14276 .iter()
14277 .cloned()
14278 .map(|mut item| {
14279 if item.data.is_none() {
14280 item.data = Some(default_data.clone());
14281 }
14282 item
14283 })
14284 .collect::<Vec<lsp::CompletionItem>>(),
14285 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
14286 );
14287 resolved_items.lock().clear();
14288
14289 cx.update_editor(|editor, window, cx| {
14290 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
14291 });
14292 cx.run_until_parked();
14293 // Completions that have already been resolved are skipped.
14294 assert_eq!(
14295 *resolved_items.lock(),
14296 items[items.len() - 16..items.len() - 4]
14297 .iter()
14298 .cloned()
14299 .map(|mut item| {
14300 if item.data.is_none() {
14301 item.data = Some(default_data.clone());
14302 }
14303 item
14304 })
14305 .collect::<Vec<lsp::CompletionItem>>()
14306 );
14307 resolved_items.lock().clear();
14308}
14309
14310#[gpui::test]
14311async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
14312 init_test(cx, |_| {});
14313
14314 let mut cx = EditorLspTestContext::new(
14315 Language::new(
14316 LanguageConfig {
14317 matcher: LanguageMatcher {
14318 path_suffixes: vec!["jsx".into()],
14319 ..Default::default()
14320 },
14321 overrides: [(
14322 "element".into(),
14323 LanguageConfigOverride {
14324 completion_query_characters: Override::Set(['-'].into_iter().collect()),
14325 ..Default::default()
14326 },
14327 )]
14328 .into_iter()
14329 .collect(),
14330 ..Default::default()
14331 },
14332 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
14333 )
14334 .with_override_query("(jsx_self_closing_element) @element")
14335 .unwrap(),
14336 lsp::ServerCapabilities {
14337 completion_provider: Some(lsp::CompletionOptions {
14338 trigger_characters: Some(vec![":".to_string()]),
14339 ..Default::default()
14340 }),
14341 ..Default::default()
14342 },
14343 cx,
14344 )
14345 .await;
14346
14347 cx.lsp
14348 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14349 Ok(Some(lsp::CompletionResponse::Array(vec![
14350 lsp::CompletionItem {
14351 label: "bg-blue".into(),
14352 ..Default::default()
14353 },
14354 lsp::CompletionItem {
14355 label: "bg-red".into(),
14356 ..Default::default()
14357 },
14358 lsp::CompletionItem {
14359 label: "bg-yellow".into(),
14360 ..Default::default()
14361 },
14362 ])))
14363 });
14364
14365 cx.set_state(r#"<p class="bgˇ" />"#);
14366
14367 // Trigger completion when typing a dash, because the dash is an extra
14368 // word character in the 'element' scope, which contains the cursor.
14369 cx.simulate_keystroke("-");
14370 cx.executor().run_until_parked();
14371 cx.update_editor(|editor, _, _| {
14372 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14373 {
14374 assert_eq!(
14375 completion_menu_entries(&menu),
14376 &["bg-red", "bg-blue", "bg-yellow"]
14377 );
14378 } else {
14379 panic!("expected completion menu to be open");
14380 }
14381 });
14382
14383 cx.simulate_keystroke("l");
14384 cx.executor().run_until_parked();
14385 cx.update_editor(|editor, _, _| {
14386 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14387 {
14388 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
14389 } else {
14390 panic!("expected completion menu to be open");
14391 }
14392 });
14393
14394 // When filtering completions, consider the character after the '-' to
14395 // be the start of a subword.
14396 cx.set_state(r#"<p class="yelˇ" />"#);
14397 cx.simulate_keystroke("l");
14398 cx.executor().run_until_parked();
14399 cx.update_editor(|editor, _, _| {
14400 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14401 {
14402 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
14403 } else {
14404 panic!("expected completion menu to be open");
14405 }
14406 });
14407}
14408
14409fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
14410 let entries = menu.entries.borrow();
14411 entries.iter().map(|mat| mat.string.clone()).collect()
14412}
14413
14414#[gpui::test]
14415async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
14416 init_test(cx, |settings| {
14417 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
14418 FormatterList(vec![Formatter::Prettier].into()),
14419 ))
14420 });
14421
14422 let fs = FakeFs::new(cx.executor());
14423 fs.insert_file(path!("/file.ts"), Default::default()).await;
14424
14425 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
14426 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14427
14428 language_registry.add(Arc::new(Language::new(
14429 LanguageConfig {
14430 name: "TypeScript".into(),
14431 matcher: LanguageMatcher {
14432 path_suffixes: vec!["ts".to_string()],
14433 ..Default::default()
14434 },
14435 ..Default::default()
14436 },
14437 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
14438 )));
14439 update_test_language_settings(cx, |settings| {
14440 settings.defaults.prettier = Some(PrettierSettings {
14441 allowed: true,
14442 ..PrettierSettings::default()
14443 });
14444 });
14445
14446 let test_plugin = "test_plugin";
14447 let _ = language_registry.register_fake_lsp(
14448 "TypeScript",
14449 FakeLspAdapter {
14450 prettier_plugins: vec![test_plugin],
14451 ..Default::default()
14452 },
14453 );
14454
14455 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
14456 let buffer = project
14457 .update(cx, |project, cx| {
14458 project.open_local_buffer(path!("/file.ts"), cx)
14459 })
14460 .await
14461 .unwrap();
14462
14463 let buffer_text = "one\ntwo\nthree\n";
14464 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
14465 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
14466 editor.update_in(cx, |editor, window, cx| {
14467 editor.set_text(buffer_text, window, cx)
14468 });
14469
14470 editor
14471 .update_in(cx, |editor, window, cx| {
14472 editor.perform_format(
14473 project.clone(),
14474 FormatTrigger::Manual,
14475 FormatTarget::Buffers,
14476 window,
14477 cx,
14478 )
14479 })
14480 .unwrap()
14481 .await;
14482 assert_eq!(
14483 editor.update(cx, |editor, cx| editor.text(cx)),
14484 buffer_text.to_string() + prettier_format_suffix,
14485 "Test prettier formatting was not applied to the original buffer text",
14486 );
14487
14488 update_test_language_settings(cx, |settings| {
14489 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
14490 });
14491 let format = editor.update_in(cx, |editor, window, cx| {
14492 editor.perform_format(
14493 project.clone(),
14494 FormatTrigger::Manual,
14495 FormatTarget::Buffers,
14496 window,
14497 cx,
14498 )
14499 });
14500 format.await.unwrap();
14501 assert_eq!(
14502 editor.update(cx, |editor, cx| editor.text(cx)),
14503 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
14504 "Autoformatting (via test prettier) was not applied to the original buffer text",
14505 );
14506}
14507
14508#[gpui::test]
14509async fn test_addition_reverts(cx: &mut TestAppContext) {
14510 init_test(cx, |_| {});
14511 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14512 let base_text = indoc! {r#"
14513 struct Row;
14514 struct Row1;
14515 struct Row2;
14516
14517 struct Row4;
14518 struct Row5;
14519 struct Row6;
14520
14521 struct Row8;
14522 struct Row9;
14523 struct Row10;"#};
14524
14525 // When addition hunks are not adjacent to carets, no hunk revert is performed
14526 assert_hunk_revert(
14527 indoc! {r#"struct Row;
14528 struct Row1;
14529 struct Row1.1;
14530 struct Row1.2;
14531 struct Row2;ˇ
14532
14533 struct Row4;
14534 struct Row5;
14535 struct Row6;
14536
14537 struct Row8;
14538 ˇstruct Row9;
14539 struct Row9.1;
14540 struct Row9.2;
14541 struct Row9.3;
14542 struct Row10;"#},
14543 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
14544 indoc! {r#"struct Row;
14545 struct Row1;
14546 struct Row1.1;
14547 struct Row1.2;
14548 struct Row2;ˇ
14549
14550 struct Row4;
14551 struct Row5;
14552 struct Row6;
14553
14554 struct Row8;
14555 ˇstruct Row9;
14556 struct Row9.1;
14557 struct Row9.2;
14558 struct Row9.3;
14559 struct Row10;"#},
14560 base_text,
14561 &mut cx,
14562 );
14563 // Same for selections
14564 assert_hunk_revert(
14565 indoc! {r#"struct Row;
14566 struct Row1;
14567 struct Row2;
14568 struct Row2.1;
14569 struct Row2.2;
14570 «ˇ
14571 struct Row4;
14572 struct» Row5;
14573 «struct Row6;
14574 ˇ»
14575 struct Row9.1;
14576 struct Row9.2;
14577 struct Row9.3;
14578 struct Row8;
14579 struct Row9;
14580 struct Row10;"#},
14581 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
14582 indoc! {r#"struct Row;
14583 struct Row1;
14584 struct Row2;
14585 struct Row2.1;
14586 struct Row2.2;
14587 «ˇ
14588 struct Row4;
14589 struct» Row5;
14590 «struct Row6;
14591 ˇ»
14592 struct Row9.1;
14593 struct Row9.2;
14594 struct Row9.3;
14595 struct Row8;
14596 struct Row9;
14597 struct Row10;"#},
14598 base_text,
14599 &mut cx,
14600 );
14601
14602 // When carets and selections intersect the addition hunks, those are reverted.
14603 // Adjacent carets got merged.
14604 assert_hunk_revert(
14605 indoc! {r#"struct Row;
14606 ˇ// something on the top
14607 struct Row1;
14608 struct Row2;
14609 struct Roˇw3.1;
14610 struct Row2.2;
14611 struct Row2.3;ˇ
14612
14613 struct Row4;
14614 struct ˇRow5.1;
14615 struct Row5.2;
14616 struct «Rowˇ»5.3;
14617 struct Row5;
14618 struct Row6;
14619 ˇ
14620 struct Row9.1;
14621 struct «Rowˇ»9.2;
14622 struct «ˇRow»9.3;
14623 struct Row8;
14624 struct Row9;
14625 «ˇ// something on bottom»
14626 struct Row10;"#},
14627 vec![
14628 DiffHunkStatusKind::Added,
14629 DiffHunkStatusKind::Added,
14630 DiffHunkStatusKind::Added,
14631 DiffHunkStatusKind::Added,
14632 DiffHunkStatusKind::Added,
14633 ],
14634 indoc! {r#"struct Row;
14635 ˇstruct Row1;
14636 struct Row2;
14637 ˇ
14638 struct Row4;
14639 ˇstruct Row5;
14640 struct Row6;
14641 ˇ
14642 ˇstruct Row8;
14643 struct Row9;
14644 ˇstruct Row10;"#},
14645 base_text,
14646 &mut cx,
14647 );
14648}
14649
14650#[gpui::test]
14651async fn test_modification_reverts(cx: &mut TestAppContext) {
14652 init_test(cx, |_| {});
14653 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14654 let base_text = indoc! {r#"
14655 struct Row;
14656 struct Row1;
14657 struct Row2;
14658
14659 struct Row4;
14660 struct Row5;
14661 struct Row6;
14662
14663 struct Row8;
14664 struct Row9;
14665 struct Row10;"#};
14666
14667 // Modification hunks behave the same as the addition ones.
14668 assert_hunk_revert(
14669 indoc! {r#"struct Row;
14670 struct Row1;
14671 struct Row33;
14672 ˇ
14673 struct Row4;
14674 struct Row5;
14675 struct Row6;
14676 ˇ
14677 struct Row99;
14678 struct Row9;
14679 struct Row10;"#},
14680 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
14681 indoc! {r#"struct Row;
14682 struct Row1;
14683 struct Row33;
14684 ˇ
14685 struct Row4;
14686 struct Row5;
14687 struct Row6;
14688 ˇ
14689 struct Row99;
14690 struct Row9;
14691 struct Row10;"#},
14692 base_text,
14693 &mut cx,
14694 );
14695 assert_hunk_revert(
14696 indoc! {r#"struct Row;
14697 struct Row1;
14698 struct Row33;
14699 «ˇ
14700 struct Row4;
14701 struct» Row5;
14702 «struct Row6;
14703 ˇ»
14704 struct Row99;
14705 struct Row9;
14706 struct Row10;"#},
14707 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
14708 indoc! {r#"struct Row;
14709 struct Row1;
14710 struct Row33;
14711 «ˇ
14712 struct Row4;
14713 struct» Row5;
14714 «struct Row6;
14715 ˇ»
14716 struct Row99;
14717 struct Row9;
14718 struct Row10;"#},
14719 base_text,
14720 &mut cx,
14721 );
14722
14723 assert_hunk_revert(
14724 indoc! {r#"ˇstruct Row1.1;
14725 struct Row1;
14726 «ˇstr»uct Row22;
14727
14728 struct ˇRow44;
14729 struct Row5;
14730 struct «Rˇ»ow66;ˇ
14731
14732 «struˇ»ct Row88;
14733 struct Row9;
14734 struct Row1011;ˇ"#},
14735 vec![
14736 DiffHunkStatusKind::Modified,
14737 DiffHunkStatusKind::Modified,
14738 DiffHunkStatusKind::Modified,
14739 DiffHunkStatusKind::Modified,
14740 DiffHunkStatusKind::Modified,
14741 DiffHunkStatusKind::Modified,
14742 ],
14743 indoc! {r#"struct Row;
14744 ˇstruct Row1;
14745 struct Row2;
14746 ˇ
14747 struct Row4;
14748 ˇstruct Row5;
14749 struct Row6;
14750 ˇ
14751 struct Row8;
14752 ˇstruct Row9;
14753 struct Row10;ˇ"#},
14754 base_text,
14755 &mut cx,
14756 );
14757}
14758
14759#[gpui::test]
14760async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
14761 init_test(cx, |_| {});
14762 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14763 let base_text = indoc! {r#"
14764 one
14765
14766 two
14767 three
14768 "#};
14769
14770 cx.set_head_text(base_text);
14771 cx.set_state("\nˇ\n");
14772 cx.executor().run_until_parked();
14773 cx.update_editor(|editor, _window, cx| {
14774 editor.expand_selected_diff_hunks(cx);
14775 });
14776 cx.executor().run_until_parked();
14777 cx.update_editor(|editor, window, cx| {
14778 editor.backspace(&Default::default(), window, cx);
14779 });
14780 cx.run_until_parked();
14781 cx.assert_state_with_diff(
14782 indoc! {r#"
14783
14784 - two
14785 - threeˇ
14786 +
14787 "#}
14788 .to_string(),
14789 );
14790}
14791
14792#[gpui::test]
14793async fn test_deletion_reverts(cx: &mut TestAppContext) {
14794 init_test(cx, |_| {});
14795 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14796 let base_text = indoc! {r#"struct Row;
14797struct Row1;
14798struct Row2;
14799
14800struct Row4;
14801struct Row5;
14802struct Row6;
14803
14804struct Row8;
14805struct Row9;
14806struct Row10;"#};
14807
14808 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
14809 assert_hunk_revert(
14810 indoc! {r#"struct Row;
14811 struct Row2;
14812
14813 ˇstruct Row4;
14814 struct Row5;
14815 struct Row6;
14816 ˇ
14817 struct Row8;
14818 struct Row10;"#},
14819 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
14820 indoc! {r#"struct Row;
14821 struct Row2;
14822
14823 ˇstruct Row4;
14824 struct Row5;
14825 struct Row6;
14826 ˇ
14827 struct Row8;
14828 struct Row10;"#},
14829 base_text,
14830 &mut cx,
14831 );
14832 assert_hunk_revert(
14833 indoc! {r#"struct Row;
14834 struct Row2;
14835
14836 «ˇstruct Row4;
14837 struct» Row5;
14838 «struct Row6;
14839 ˇ»
14840 struct Row8;
14841 struct Row10;"#},
14842 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
14843 indoc! {r#"struct Row;
14844 struct Row2;
14845
14846 «ˇstruct Row4;
14847 struct» Row5;
14848 «struct Row6;
14849 ˇ»
14850 struct Row8;
14851 struct Row10;"#},
14852 base_text,
14853 &mut cx,
14854 );
14855
14856 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
14857 assert_hunk_revert(
14858 indoc! {r#"struct Row;
14859 ˇstruct Row2;
14860
14861 struct Row4;
14862 struct Row5;
14863 struct Row6;
14864
14865 struct Row8;ˇ
14866 struct Row10;"#},
14867 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
14868 indoc! {r#"struct Row;
14869 struct Row1;
14870 ˇstruct Row2;
14871
14872 struct Row4;
14873 struct Row5;
14874 struct Row6;
14875
14876 struct Row8;ˇ
14877 struct Row9;
14878 struct Row10;"#},
14879 base_text,
14880 &mut cx,
14881 );
14882 assert_hunk_revert(
14883 indoc! {r#"struct Row;
14884 struct Row2«ˇ;
14885 struct Row4;
14886 struct» Row5;
14887 «struct Row6;
14888
14889 struct Row8;ˇ»
14890 struct Row10;"#},
14891 vec![
14892 DiffHunkStatusKind::Deleted,
14893 DiffHunkStatusKind::Deleted,
14894 DiffHunkStatusKind::Deleted,
14895 ],
14896 indoc! {r#"struct Row;
14897 struct Row1;
14898 struct Row2«ˇ;
14899
14900 struct Row4;
14901 struct» Row5;
14902 «struct Row6;
14903
14904 struct Row8;ˇ»
14905 struct Row9;
14906 struct Row10;"#},
14907 base_text,
14908 &mut cx,
14909 );
14910}
14911
14912#[gpui::test]
14913async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
14914 init_test(cx, |_| {});
14915
14916 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
14917 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
14918 let base_text_3 =
14919 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
14920
14921 let text_1 = edit_first_char_of_every_line(base_text_1);
14922 let text_2 = edit_first_char_of_every_line(base_text_2);
14923 let text_3 = edit_first_char_of_every_line(base_text_3);
14924
14925 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
14926 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
14927 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
14928
14929 let multibuffer = cx.new(|cx| {
14930 let mut multibuffer = MultiBuffer::new(ReadWrite);
14931 multibuffer.push_excerpts(
14932 buffer_1.clone(),
14933 [
14934 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14935 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14936 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14937 ],
14938 cx,
14939 );
14940 multibuffer.push_excerpts(
14941 buffer_2.clone(),
14942 [
14943 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14944 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14945 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14946 ],
14947 cx,
14948 );
14949 multibuffer.push_excerpts(
14950 buffer_3.clone(),
14951 [
14952 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14953 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14954 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14955 ],
14956 cx,
14957 );
14958 multibuffer
14959 });
14960
14961 let fs = FakeFs::new(cx.executor());
14962 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
14963 let (editor, cx) = cx
14964 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
14965 editor.update_in(cx, |editor, _window, cx| {
14966 for (buffer, diff_base) in [
14967 (buffer_1.clone(), base_text_1),
14968 (buffer_2.clone(), base_text_2),
14969 (buffer_3.clone(), base_text_3),
14970 ] {
14971 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
14972 editor
14973 .buffer
14974 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
14975 }
14976 });
14977 cx.executor().run_until_parked();
14978
14979 editor.update_in(cx, |editor, window, cx| {
14980 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}");
14981 editor.select_all(&SelectAll, window, cx);
14982 editor.git_restore(&Default::default(), window, cx);
14983 });
14984 cx.executor().run_until_parked();
14985
14986 // When all ranges are selected, all buffer hunks are reverted.
14987 editor.update(cx, |editor, cx| {
14988 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");
14989 });
14990 buffer_1.update(cx, |buffer, _| {
14991 assert_eq!(buffer.text(), base_text_1);
14992 });
14993 buffer_2.update(cx, |buffer, _| {
14994 assert_eq!(buffer.text(), base_text_2);
14995 });
14996 buffer_3.update(cx, |buffer, _| {
14997 assert_eq!(buffer.text(), base_text_3);
14998 });
14999
15000 editor.update_in(cx, |editor, window, cx| {
15001 editor.undo(&Default::default(), window, cx);
15002 });
15003
15004 editor.update_in(cx, |editor, window, cx| {
15005 editor.change_selections(None, window, cx, |s| {
15006 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
15007 });
15008 editor.git_restore(&Default::default(), window, cx);
15009 });
15010
15011 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
15012 // but not affect buffer_2 and its related excerpts.
15013 editor.update(cx, |editor, cx| {
15014 assert_eq!(
15015 editor.text(cx),
15016 "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}"
15017 );
15018 });
15019 buffer_1.update(cx, |buffer, _| {
15020 assert_eq!(buffer.text(), base_text_1);
15021 });
15022 buffer_2.update(cx, |buffer, _| {
15023 assert_eq!(
15024 buffer.text(),
15025 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
15026 );
15027 });
15028 buffer_3.update(cx, |buffer, _| {
15029 assert_eq!(
15030 buffer.text(),
15031 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
15032 );
15033 });
15034
15035 fn edit_first_char_of_every_line(text: &str) -> String {
15036 text.split('\n')
15037 .map(|line| format!("X{}", &line[1..]))
15038 .collect::<Vec<_>>()
15039 .join("\n")
15040 }
15041}
15042
15043#[gpui::test]
15044async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) {
15045 init_test(cx, |_| {});
15046
15047 let cols = 4;
15048 let rows = 10;
15049 let sample_text_1 = sample_text(rows, cols, 'a');
15050 assert_eq!(
15051 sample_text_1,
15052 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
15053 );
15054 let sample_text_2 = sample_text(rows, cols, 'l');
15055 assert_eq!(
15056 sample_text_2,
15057 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
15058 );
15059 let sample_text_3 = sample_text(rows, cols, 'v');
15060 assert_eq!(
15061 sample_text_3,
15062 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
15063 );
15064
15065 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
15066 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
15067 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
15068
15069 let multi_buffer = cx.new(|cx| {
15070 let mut multibuffer = MultiBuffer::new(ReadWrite);
15071 multibuffer.push_excerpts(
15072 buffer_1.clone(),
15073 [
15074 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15075 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15076 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15077 ],
15078 cx,
15079 );
15080 multibuffer.push_excerpts(
15081 buffer_2.clone(),
15082 [
15083 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15084 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15085 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15086 ],
15087 cx,
15088 );
15089 multibuffer.push_excerpts(
15090 buffer_3.clone(),
15091 [
15092 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15093 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15094 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15095 ],
15096 cx,
15097 );
15098 multibuffer
15099 });
15100
15101 let fs = FakeFs::new(cx.executor());
15102 fs.insert_tree(
15103 "/a",
15104 json!({
15105 "main.rs": sample_text_1,
15106 "other.rs": sample_text_2,
15107 "lib.rs": sample_text_3,
15108 }),
15109 )
15110 .await;
15111 let project = Project::test(fs, ["/a".as_ref()], cx).await;
15112 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15113 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
15114 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
15115 Editor::new(
15116 EditorMode::full(),
15117 multi_buffer,
15118 Some(project.clone()),
15119 window,
15120 cx,
15121 )
15122 });
15123 let multibuffer_item_id = workspace
15124 .update(cx, |workspace, window, cx| {
15125 assert!(
15126 workspace.active_item(cx).is_none(),
15127 "active item should be None before the first item is added"
15128 );
15129 workspace.add_item_to_active_pane(
15130 Box::new(multi_buffer_editor.clone()),
15131 None,
15132 true,
15133 window,
15134 cx,
15135 );
15136 let active_item = workspace
15137 .active_item(cx)
15138 .expect("should have an active item after adding the multi buffer");
15139 assert!(
15140 !active_item.is_singleton(cx),
15141 "A multi buffer was expected to active after adding"
15142 );
15143 active_item.item_id()
15144 })
15145 .unwrap();
15146 cx.executor().run_until_parked();
15147
15148 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15149 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
15150 s.select_ranges(Some(1..2))
15151 });
15152 editor.open_excerpts(&OpenExcerpts, window, cx);
15153 });
15154 cx.executor().run_until_parked();
15155 let first_item_id = workspace
15156 .update(cx, |workspace, window, cx| {
15157 let active_item = workspace
15158 .active_item(cx)
15159 .expect("should have an active item after navigating into the 1st buffer");
15160 let first_item_id = active_item.item_id();
15161 assert_ne!(
15162 first_item_id, multibuffer_item_id,
15163 "Should navigate into the 1st buffer and activate it"
15164 );
15165 assert!(
15166 active_item.is_singleton(cx),
15167 "New active item should be a singleton buffer"
15168 );
15169 assert_eq!(
15170 active_item
15171 .act_as::<Editor>(cx)
15172 .expect("should have navigated into an editor for the 1st buffer")
15173 .read(cx)
15174 .text(cx),
15175 sample_text_1
15176 );
15177
15178 workspace
15179 .go_back(workspace.active_pane().downgrade(), window, cx)
15180 .detach_and_log_err(cx);
15181
15182 first_item_id
15183 })
15184 .unwrap();
15185 cx.executor().run_until_parked();
15186 workspace
15187 .update(cx, |workspace, _, cx| {
15188 let active_item = workspace
15189 .active_item(cx)
15190 .expect("should have an active item after navigating back");
15191 assert_eq!(
15192 active_item.item_id(),
15193 multibuffer_item_id,
15194 "Should navigate back to the multi buffer"
15195 );
15196 assert!(!active_item.is_singleton(cx));
15197 })
15198 .unwrap();
15199
15200 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15201 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
15202 s.select_ranges(Some(39..40))
15203 });
15204 editor.open_excerpts(&OpenExcerpts, window, cx);
15205 });
15206 cx.executor().run_until_parked();
15207 let second_item_id = workspace
15208 .update(cx, |workspace, window, cx| {
15209 let active_item = workspace
15210 .active_item(cx)
15211 .expect("should have an active item after navigating into the 2nd buffer");
15212 let second_item_id = active_item.item_id();
15213 assert_ne!(
15214 second_item_id, multibuffer_item_id,
15215 "Should navigate away from the multibuffer"
15216 );
15217 assert_ne!(
15218 second_item_id, first_item_id,
15219 "Should navigate into the 2nd buffer and activate it"
15220 );
15221 assert!(
15222 active_item.is_singleton(cx),
15223 "New active item should be a singleton buffer"
15224 );
15225 assert_eq!(
15226 active_item
15227 .act_as::<Editor>(cx)
15228 .expect("should have navigated into an editor")
15229 .read(cx)
15230 .text(cx),
15231 sample_text_2
15232 );
15233
15234 workspace
15235 .go_back(workspace.active_pane().downgrade(), window, cx)
15236 .detach_and_log_err(cx);
15237
15238 second_item_id
15239 })
15240 .unwrap();
15241 cx.executor().run_until_parked();
15242 workspace
15243 .update(cx, |workspace, _, cx| {
15244 let active_item = workspace
15245 .active_item(cx)
15246 .expect("should have an active item after navigating back from the 2nd buffer");
15247 assert_eq!(
15248 active_item.item_id(),
15249 multibuffer_item_id,
15250 "Should navigate back from the 2nd buffer to the multi buffer"
15251 );
15252 assert!(!active_item.is_singleton(cx));
15253 })
15254 .unwrap();
15255
15256 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15257 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
15258 s.select_ranges(Some(70..70))
15259 });
15260 editor.open_excerpts(&OpenExcerpts, window, cx);
15261 });
15262 cx.executor().run_until_parked();
15263 workspace
15264 .update(cx, |workspace, window, cx| {
15265 let active_item = workspace
15266 .active_item(cx)
15267 .expect("should have an active item after navigating into the 3rd buffer");
15268 let third_item_id = active_item.item_id();
15269 assert_ne!(
15270 third_item_id, multibuffer_item_id,
15271 "Should navigate into the 3rd buffer and activate it"
15272 );
15273 assert_ne!(third_item_id, first_item_id);
15274 assert_ne!(third_item_id, second_item_id);
15275 assert!(
15276 active_item.is_singleton(cx),
15277 "New active item should be a singleton buffer"
15278 );
15279 assert_eq!(
15280 active_item
15281 .act_as::<Editor>(cx)
15282 .expect("should have navigated into an editor")
15283 .read(cx)
15284 .text(cx),
15285 sample_text_3
15286 );
15287
15288 workspace
15289 .go_back(workspace.active_pane().downgrade(), window, cx)
15290 .detach_and_log_err(cx);
15291 })
15292 .unwrap();
15293 cx.executor().run_until_parked();
15294 workspace
15295 .update(cx, |workspace, _, cx| {
15296 let active_item = workspace
15297 .active_item(cx)
15298 .expect("should have an active item after navigating back from the 3rd buffer");
15299 assert_eq!(
15300 active_item.item_id(),
15301 multibuffer_item_id,
15302 "Should navigate back from the 3rd buffer to the multi buffer"
15303 );
15304 assert!(!active_item.is_singleton(cx));
15305 })
15306 .unwrap();
15307}
15308
15309#[gpui::test]
15310async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
15311 init_test(cx, |_| {});
15312
15313 let mut cx = EditorTestContext::new(cx).await;
15314
15315 let diff_base = r#"
15316 use some::mod;
15317
15318 const A: u32 = 42;
15319
15320 fn main() {
15321 println!("hello");
15322
15323 println!("world");
15324 }
15325 "#
15326 .unindent();
15327
15328 cx.set_state(
15329 &r#"
15330 use some::modified;
15331
15332 ˇ
15333 fn main() {
15334 println!("hello there");
15335
15336 println!("around the");
15337 println!("world");
15338 }
15339 "#
15340 .unindent(),
15341 );
15342
15343 cx.set_head_text(&diff_base);
15344 executor.run_until_parked();
15345
15346 cx.update_editor(|editor, window, cx| {
15347 editor.go_to_next_hunk(&GoToHunk, window, cx);
15348 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15349 });
15350 executor.run_until_parked();
15351 cx.assert_state_with_diff(
15352 r#"
15353 use some::modified;
15354
15355
15356 fn main() {
15357 - println!("hello");
15358 + ˇ println!("hello there");
15359
15360 println!("around the");
15361 println!("world");
15362 }
15363 "#
15364 .unindent(),
15365 );
15366
15367 cx.update_editor(|editor, window, cx| {
15368 for _ in 0..2 {
15369 editor.go_to_next_hunk(&GoToHunk, window, cx);
15370 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15371 }
15372 });
15373 executor.run_until_parked();
15374 cx.assert_state_with_diff(
15375 r#"
15376 - use some::mod;
15377 + ˇuse some::modified;
15378
15379
15380 fn main() {
15381 - println!("hello");
15382 + println!("hello there");
15383
15384 + println!("around the");
15385 println!("world");
15386 }
15387 "#
15388 .unindent(),
15389 );
15390
15391 cx.update_editor(|editor, window, cx| {
15392 editor.go_to_next_hunk(&GoToHunk, window, cx);
15393 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15394 });
15395 executor.run_until_parked();
15396 cx.assert_state_with_diff(
15397 r#"
15398 - use some::mod;
15399 + use some::modified;
15400
15401 - const A: u32 = 42;
15402 ˇ
15403 fn main() {
15404 - println!("hello");
15405 + println!("hello there");
15406
15407 + println!("around the");
15408 println!("world");
15409 }
15410 "#
15411 .unindent(),
15412 );
15413
15414 cx.update_editor(|editor, window, cx| {
15415 editor.cancel(&Cancel, window, cx);
15416 });
15417
15418 cx.assert_state_with_diff(
15419 r#"
15420 use some::modified;
15421
15422 ˇ
15423 fn main() {
15424 println!("hello there");
15425
15426 println!("around the");
15427 println!("world");
15428 }
15429 "#
15430 .unindent(),
15431 );
15432}
15433
15434#[gpui::test]
15435async fn test_diff_base_change_with_expanded_diff_hunks(
15436 executor: BackgroundExecutor,
15437 cx: &mut TestAppContext,
15438) {
15439 init_test(cx, |_| {});
15440
15441 let mut cx = EditorTestContext::new(cx).await;
15442
15443 let diff_base = r#"
15444 use some::mod1;
15445 use some::mod2;
15446
15447 const A: u32 = 42;
15448 const B: u32 = 42;
15449 const C: u32 = 42;
15450
15451 fn main() {
15452 println!("hello");
15453
15454 println!("world");
15455 }
15456 "#
15457 .unindent();
15458
15459 cx.set_state(
15460 &r#"
15461 use some::mod2;
15462
15463 const A: u32 = 42;
15464 const C: u32 = 42;
15465
15466 fn main(ˇ) {
15467 //println!("hello");
15468
15469 println!("world");
15470 //
15471 //
15472 }
15473 "#
15474 .unindent(),
15475 );
15476
15477 cx.set_head_text(&diff_base);
15478 executor.run_until_parked();
15479
15480 cx.update_editor(|editor, window, cx| {
15481 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15482 });
15483 executor.run_until_parked();
15484 cx.assert_state_with_diff(
15485 r#"
15486 - use some::mod1;
15487 use some::mod2;
15488
15489 const A: u32 = 42;
15490 - const B: u32 = 42;
15491 const C: u32 = 42;
15492
15493 fn main(ˇ) {
15494 - println!("hello");
15495 + //println!("hello");
15496
15497 println!("world");
15498 + //
15499 + //
15500 }
15501 "#
15502 .unindent(),
15503 );
15504
15505 cx.set_head_text("new diff base!");
15506 executor.run_until_parked();
15507 cx.assert_state_with_diff(
15508 r#"
15509 - new diff base!
15510 + use some::mod2;
15511 +
15512 + const A: u32 = 42;
15513 + const C: u32 = 42;
15514 +
15515 + fn main(ˇ) {
15516 + //println!("hello");
15517 +
15518 + println!("world");
15519 + //
15520 + //
15521 + }
15522 "#
15523 .unindent(),
15524 );
15525}
15526
15527#[gpui::test]
15528async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
15529 init_test(cx, |_| {});
15530
15531 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
15532 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
15533 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
15534 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
15535 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
15536 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
15537
15538 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
15539 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
15540 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
15541
15542 let multi_buffer = cx.new(|cx| {
15543 let mut multibuffer = MultiBuffer::new(ReadWrite);
15544 multibuffer.push_excerpts(
15545 buffer_1.clone(),
15546 [
15547 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15548 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15549 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
15550 ],
15551 cx,
15552 );
15553 multibuffer.push_excerpts(
15554 buffer_2.clone(),
15555 [
15556 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15557 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15558 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
15559 ],
15560 cx,
15561 );
15562 multibuffer.push_excerpts(
15563 buffer_3.clone(),
15564 [
15565 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15566 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15567 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
15568 ],
15569 cx,
15570 );
15571 multibuffer
15572 });
15573
15574 let editor =
15575 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
15576 editor
15577 .update(cx, |editor, _window, cx| {
15578 for (buffer, diff_base) in [
15579 (buffer_1.clone(), file_1_old),
15580 (buffer_2.clone(), file_2_old),
15581 (buffer_3.clone(), file_3_old),
15582 ] {
15583 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
15584 editor
15585 .buffer
15586 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
15587 }
15588 })
15589 .unwrap();
15590
15591 let mut cx = EditorTestContext::for_editor(editor, cx).await;
15592 cx.run_until_parked();
15593
15594 cx.assert_editor_state(
15595 &"
15596 ˇaaa
15597 ccc
15598 ddd
15599
15600 ggg
15601 hhh
15602
15603
15604 lll
15605 mmm
15606 NNN
15607
15608 qqq
15609 rrr
15610
15611 uuu
15612 111
15613 222
15614 333
15615
15616 666
15617 777
15618
15619 000
15620 !!!"
15621 .unindent(),
15622 );
15623
15624 cx.update_editor(|editor, window, cx| {
15625 editor.select_all(&SelectAll, window, cx);
15626 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15627 });
15628 cx.executor().run_until_parked();
15629
15630 cx.assert_state_with_diff(
15631 "
15632 «aaa
15633 - bbb
15634 ccc
15635 ddd
15636
15637 ggg
15638 hhh
15639
15640
15641 lll
15642 mmm
15643 - nnn
15644 + NNN
15645
15646 qqq
15647 rrr
15648
15649 uuu
15650 111
15651 222
15652 333
15653
15654 + 666
15655 777
15656
15657 000
15658 !!!ˇ»"
15659 .unindent(),
15660 );
15661}
15662
15663#[gpui::test]
15664async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
15665 init_test(cx, |_| {});
15666
15667 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
15668 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
15669
15670 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
15671 let multi_buffer = cx.new(|cx| {
15672 let mut multibuffer = MultiBuffer::new(ReadWrite);
15673 multibuffer.push_excerpts(
15674 buffer.clone(),
15675 [
15676 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
15677 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
15678 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
15679 ],
15680 cx,
15681 );
15682 multibuffer
15683 });
15684
15685 let editor =
15686 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
15687 editor
15688 .update(cx, |editor, _window, cx| {
15689 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
15690 editor
15691 .buffer
15692 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
15693 })
15694 .unwrap();
15695
15696 let mut cx = EditorTestContext::for_editor(editor, cx).await;
15697 cx.run_until_parked();
15698
15699 cx.update_editor(|editor, window, cx| {
15700 editor.expand_all_diff_hunks(&Default::default(), window, cx)
15701 });
15702 cx.executor().run_until_parked();
15703
15704 // When the start of a hunk coincides with the start of its excerpt,
15705 // the hunk is expanded. When the start of a a hunk is earlier than
15706 // the start of its excerpt, the hunk is not expanded.
15707 cx.assert_state_with_diff(
15708 "
15709 ˇaaa
15710 - bbb
15711 + BBB
15712
15713 - ddd
15714 - eee
15715 + DDD
15716 + EEE
15717 fff
15718
15719 iii
15720 "
15721 .unindent(),
15722 );
15723}
15724
15725#[gpui::test]
15726async fn test_edits_around_expanded_insertion_hunks(
15727 executor: BackgroundExecutor,
15728 cx: &mut TestAppContext,
15729) {
15730 init_test(cx, |_| {});
15731
15732 let mut cx = EditorTestContext::new(cx).await;
15733
15734 let diff_base = r#"
15735 use some::mod1;
15736 use some::mod2;
15737
15738 const A: u32 = 42;
15739
15740 fn main() {
15741 println!("hello");
15742
15743 println!("world");
15744 }
15745 "#
15746 .unindent();
15747 executor.run_until_parked();
15748 cx.set_state(
15749 &r#"
15750 use some::mod1;
15751 use some::mod2;
15752
15753 const A: u32 = 42;
15754 const B: u32 = 42;
15755 const C: u32 = 42;
15756 ˇ
15757
15758 fn main() {
15759 println!("hello");
15760
15761 println!("world");
15762 }
15763 "#
15764 .unindent(),
15765 );
15766
15767 cx.set_head_text(&diff_base);
15768 executor.run_until_parked();
15769
15770 cx.update_editor(|editor, window, cx| {
15771 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15772 });
15773 executor.run_until_parked();
15774
15775 cx.assert_state_with_diff(
15776 r#"
15777 use some::mod1;
15778 use some::mod2;
15779
15780 const A: u32 = 42;
15781 + const B: u32 = 42;
15782 + const C: u32 = 42;
15783 + ˇ
15784
15785 fn main() {
15786 println!("hello");
15787
15788 println!("world");
15789 }
15790 "#
15791 .unindent(),
15792 );
15793
15794 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
15795 executor.run_until_parked();
15796
15797 cx.assert_state_with_diff(
15798 r#"
15799 use some::mod1;
15800 use some::mod2;
15801
15802 const A: u32 = 42;
15803 + const B: u32 = 42;
15804 + const C: u32 = 42;
15805 + const D: u32 = 42;
15806 + ˇ
15807
15808 fn main() {
15809 println!("hello");
15810
15811 println!("world");
15812 }
15813 "#
15814 .unindent(),
15815 );
15816
15817 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
15818 executor.run_until_parked();
15819
15820 cx.assert_state_with_diff(
15821 r#"
15822 use some::mod1;
15823 use some::mod2;
15824
15825 const A: u32 = 42;
15826 + const B: u32 = 42;
15827 + const C: u32 = 42;
15828 + const D: u32 = 42;
15829 + const E: u32 = 42;
15830 + ˇ
15831
15832 fn main() {
15833 println!("hello");
15834
15835 println!("world");
15836 }
15837 "#
15838 .unindent(),
15839 );
15840
15841 cx.update_editor(|editor, window, cx| {
15842 editor.delete_line(&DeleteLine, window, cx);
15843 });
15844 executor.run_until_parked();
15845
15846 cx.assert_state_with_diff(
15847 r#"
15848 use some::mod1;
15849 use some::mod2;
15850
15851 const A: u32 = 42;
15852 + const B: u32 = 42;
15853 + const C: u32 = 42;
15854 + const D: u32 = 42;
15855 + const E: u32 = 42;
15856 ˇ
15857 fn main() {
15858 println!("hello");
15859
15860 println!("world");
15861 }
15862 "#
15863 .unindent(),
15864 );
15865
15866 cx.update_editor(|editor, window, cx| {
15867 editor.move_up(&MoveUp, window, cx);
15868 editor.delete_line(&DeleteLine, window, cx);
15869 editor.move_up(&MoveUp, window, cx);
15870 editor.delete_line(&DeleteLine, window, cx);
15871 editor.move_up(&MoveUp, window, cx);
15872 editor.delete_line(&DeleteLine, window, cx);
15873 });
15874 executor.run_until_parked();
15875 cx.assert_state_with_diff(
15876 r#"
15877 use some::mod1;
15878 use some::mod2;
15879
15880 const A: u32 = 42;
15881 + const B: u32 = 42;
15882 ˇ
15883 fn main() {
15884 println!("hello");
15885
15886 println!("world");
15887 }
15888 "#
15889 .unindent(),
15890 );
15891
15892 cx.update_editor(|editor, window, cx| {
15893 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
15894 editor.delete_line(&DeleteLine, window, cx);
15895 });
15896 executor.run_until_parked();
15897 cx.assert_state_with_diff(
15898 r#"
15899 ˇ
15900 fn main() {
15901 println!("hello");
15902
15903 println!("world");
15904 }
15905 "#
15906 .unindent(),
15907 );
15908}
15909
15910#[gpui::test]
15911async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
15912 init_test(cx, |_| {});
15913
15914 let mut cx = EditorTestContext::new(cx).await;
15915 cx.set_head_text(indoc! { "
15916 one
15917 two
15918 three
15919 four
15920 five
15921 "
15922 });
15923 cx.set_state(indoc! { "
15924 one
15925 ˇthree
15926 five
15927 "});
15928 cx.run_until_parked();
15929 cx.update_editor(|editor, window, cx| {
15930 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
15931 });
15932 cx.assert_state_with_diff(
15933 indoc! { "
15934 one
15935 - two
15936 ˇthree
15937 - four
15938 five
15939 "}
15940 .to_string(),
15941 );
15942 cx.update_editor(|editor, window, cx| {
15943 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
15944 });
15945
15946 cx.assert_state_with_diff(
15947 indoc! { "
15948 one
15949 ˇthree
15950 five
15951 "}
15952 .to_string(),
15953 );
15954
15955 cx.set_state(indoc! { "
15956 one
15957 ˇTWO
15958 three
15959 four
15960 five
15961 "});
15962 cx.run_until_parked();
15963 cx.update_editor(|editor, window, cx| {
15964 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
15965 });
15966
15967 cx.assert_state_with_diff(
15968 indoc! { "
15969 one
15970 - two
15971 + ˇTWO
15972 three
15973 four
15974 five
15975 "}
15976 .to_string(),
15977 );
15978 cx.update_editor(|editor, window, cx| {
15979 editor.move_up(&Default::default(), window, cx);
15980 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
15981 });
15982 cx.assert_state_with_diff(
15983 indoc! { "
15984 one
15985 ˇTWO
15986 three
15987 four
15988 five
15989 "}
15990 .to_string(),
15991 );
15992}
15993
15994#[gpui::test]
15995async fn test_edits_around_expanded_deletion_hunks(
15996 executor: BackgroundExecutor,
15997 cx: &mut TestAppContext,
15998) {
15999 init_test(cx, |_| {});
16000
16001 let mut cx = EditorTestContext::new(cx).await;
16002
16003 let diff_base = r#"
16004 use some::mod1;
16005 use some::mod2;
16006
16007 const A: u32 = 42;
16008 const B: u32 = 42;
16009 const C: u32 = 42;
16010
16011
16012 fn main() {
16013 println!("hello");
16014
16015 println!("world");
16016 }
16017 "#
16018 .unindent();
16019 executor.run_until_parked();
16020 cx.set_state(
16021 &r#"
16022 use some::mod1;
16023 use some::mod2;
16024
16025 ˇconst B: u32 = 42;
16026 const C: u32 = 42;
16027
16028
16029 fn main() {
16030 println!("hello");
16031
16032 println!("world");
16033 }
16034 "#
16035 .unindent(),
16036 );
16037
16038 cx.set_head_text(&diff_base);
16039 executor.run_until_parked();
16040
16041 cx.update_editor(|editor, window, cx| {
16042 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16043 });
16044 executor.run_until_parked();
16045
16046 cx.assert_state_with_diff(
16047 r#"
16048 use some::mod1;
16049 use some::mod2;
16050
16051 - const A: u32 = 42;
16052 ˇconst B: u32 = 42;
16053 const C: u32 = 42;
16054
16055
16056 fn main() {
16057 println!("hello");
16058
16059 println!("world");
16060 }
16061 "#
16062 .unindent(),
16063 );
16064
16065 cx.update_editor(|editor, window, cx| {
16066 editor.delete_line(&DeleteLine, window, cx);
16067 });
16068 executor.run_until_parked();
16069 cx.assert_state_with_diff(
16070 r#"
16071 use some::mod1;
16072 use some::mod2;
16073
16074 - const A: u32 = 42;
16075 - const B: u32 = 42;
16076 ˇconst C: u32 = 42;
16077
16078
16079 fn main() {
16080 println!("hello");
16081
16082 println!("world");
16083 }
16084 "#
16085 .unindent(),
16086 );
16087
16088 cx.update_editor(|editor, window, cx| {
16089 editor.delete_line(&DeleteLine, window, cx);
16090 });
16091 executor.run_until_parked();
16092 cx.assert_state_with_diff(
16093 r#"
16094 use some::mod1;
16095 use some::mod2;
16096
16097 - const A: u32 = 42;
16098 - const B: u32 = 42;
16099 - const C: u32 = 42;
16100 ˇ
16101
16102 fn main() {
16103 println!("hello");
16104
16105 println!("world");
16106 }
16107 "#
16108 .unindent(),
16109 );
16110
16111 cx.update_editor(|editor, window, cx| {
16112 editor.handle_input("replacement", window, cx);
16113 });
16114 executor.run_until_parked();
16115 cx.assert_state_with_diff(
16116 r#"
16117 use some::mod1;
16118 use some::mod2;
16119
16120 - const A: u32 = 42;
16121 - const B: u32 = 42;
16122 - const C: u32 = 42;
16123 -
16124 + replacementˇ
16125
16126 fn main() {
16127 println!("hello");
16128
16129 println!("world");
16130 }
16131 "#
16132 .unindent(),
16133 );
16134}
16135
16136#[gpui::test]
16137async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16138 init_test(cx, |_| {});
16139
16140 let mut cx = EditorTestContext::new(cx).await;
16141
16142 let base_text = r#"
16143 one
16144 two
16145 three
16146 four
16147 five
16148 "#
16149 .unindent();
16150 executor.run_until_parked();
16151 cx.set_state(
16152 &r#"
16153 one
16154 two
16155 fˇour
16156 five
16157 "#
16158 .unindent(),
16159 );
16160
16161 cx.set_head_text(&base_text);
16162 executor.run_until_parked();
16163
16164 cx.update_editor(|editor, window, cx| {
16165 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16166 });
16167 executor.run_until_parked();
16168
16169 cx.assert_state_with_diff(
16170 r#"
16171 one
16172 two
16173 - three
16174 fˇour
16175 five
16176 "#
16177 .unindent(),
16178 );
16179
16180 cx.update_editor(|editor, window, cx| {
16181 editor.backspace(&Backspace, window, cx);
16182 editor.backspace(&Backspace, window, cx);
16183 });
16184 executor.run_until_parked();
16185 cx.assert_state_with_diff(
16186 r#"
16187 one
16188 two
16189 - threeˇ
16190 - four
16191 + our
16192 five
16193 "#
16194 .unindent(),
16195 );
16196}
16197
16198#[gpui::test]
16199async fn test_edit_after_expanded_modification_hunk(
16200 executor: BackgroundExecutor,
16201 cx: &mut TestAppContext,
16202) {
16203 init_test(cx, |_| {});
16204
16205 let mut cx = EditorTestContext::new(cx).await;
16206
16207 let diff_base = r#"
16208 use some::mod1;
16209 use some::mod2;
16210
16211 const A: u32 = 42;
16212 const B: u32 = 42;
16213 const C: u32 = 42;
16214 const D: u32 = 42;
16215
16216
16217 fn main() {
16218 println!("hello");
16219
16220 println!("world");
16221 }"#
16222 .unindent();
16223
16224 cx.set_state(
16225 &r#"
16226 use some::mod1;
16227 use some::mod2;
16228
16229 const A: u32 = 42;
16230 const B: u32 = 42;
16231 const C: u32 = 43ˇ
16232 const D: u32 = 42;
16233
16234
16235 fn main() {
16236 println!("hello");
16237
16238 println!("world");
16239 }"#
16240 .unindent(),
16241 );
16242
16243 cx.set_head_text(&diff_base);
16244 executor.run_until_parked();
16245 cx.update_editor(|editor, window, cx| {
16246 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16247 });
16248 executor.run_until_parked();
16249
16250 cx.assert_state_with_diff(
16251 r#"
16252 use some::mod1;
16253 use some::mod2;
16254
16255 const A: u32 = 42;
16256 const B: u32 = 42;
16257 - const C: u32 = 42;
16258 + const C: u32 = 43ˇ
16259 const D: u32 = 42;
16260
16261
16262 fn main() {
16263 println!("hello");
16264
16265 println!("world");
16266 }"#
16267 .unindent(),
16268 );
16269
16270 cx.update_editor(|editor, window, cx| {
16271 editor.handle_input("\nnew_line\n", window, cx);
16272 });
16273 executor.run_until_parked();
16274
16275 cx.assert_state_with_diff(
16276 r#"
16277 use some::mod1;
16278 use some::mod2;
16279
16280 const A: u32 = 42;
16281 const B: u32 = 42;
16282 - const C: u32 = 42;
16283 + const C: u32 = 43
16284 + new_line
16285 + ˇ
16286 const D: u32 = 42;
16287
16288
16289 fn main() {
16290 println!("hello");
16291
16292 println!("world");
16293 }"#
16294 .unindent(),
16295 );
16296}
16297
16298#[gpui::test]
16299async fn test_stage_and_unstage_added_file_hunk(
16300 executor: BackgroundExecutor,
16301 cx: &mut TestAppContext,
16302) {
16303 init_test(cx, |_| {});
16304
16305 let mut cx = EditorTestContext::new(cx).await;
16306 cx.update_editor(|editor, _, cx| {
16307 editor.set_expand_all_diff_hunks(cx);
16308 });
16309
16310 let working_copy = r#"
16311 ˇfn main() {
16312 println!("hello, world!");
16313 }
16314 "#
16315 .unindent();
16316
16317 cx.set_state(&working_copy);
16318 executor.run_until_parked();
16319
16320 cx.assert_state_with_diff(
16321 r#"
16322 + ˇfn main() {
16323 + println!("hello, world!");
16324 + }
16325 "#
16326 .unindent(),
16327 );
16328 cx.assert_index_text(None);
16329
16330 cx.update_editor(|editor, window, cx| {
16331 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16332 });
16333 executor.run_until_parked();
16334 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
16335 cx.assert_state_with_diff(
16336 r#"
16337 + ˇfn main() {
16338 + println!("hello, world!");
16339 + }
16340 "#
16341 .unindent(),
16342 );
16343
16344 cx.update_editor(|editor, window, cx| {
16345 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16346 });
16347 executor.run_until_parked();
16348 cx.assert_index_text(None);
16349}
16350
16351async fn setup_indent_guides_editor(
16352 text: &str,
16353 cx: &mut TestAppContext,
16354) -> (BufferId, EditorTestContext) {
16355 init_test(cx, |_| {});
16356
16357 let mut cx = EditorTestContext::new(cx).await;
16358
16359 let buffer_id = cx.update_editor(|editor, window, cx| {
16360 editor.set_text(text, window, cx);
16361 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
16362
16363 buffer_ids[0]
16364 });
16365
16366 (buffer_id, cx)
16367}
16368
16369fn assert_indent_guides(
16370 range: Range<u32>,
16371 expected: Vec<IndentGuide>,
16372 active_indices: Option<Vec<usize>>,
16373 cx: &mut EditorTestContext,
16374) {
16375 let indent_guides = cx.update_editor(|editor, window, cx| {
16376 let snapshot = editor.snapshot(window, cx).display_snapshot;
16377 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
16378 editor,
16379 MultiBufferRow(range.start)..MultiBufferRow(range.end),
16380 true,
16381 &snapshot,
16382 cx,
16383 );
16384
16385 indent_guides.sort_by(|a, b| {
16386 a.depth.cmp(&b.depth).then(
16387 a.start_row
16388 .cmp(&b.start_row)
16389 .then(a.end_row.cmp(&b.end_row)),
16390 )
16391 });
16392 indent_guides
16393 });
16394
16395 if let Some(expected) = active_indices {
16396 let active_indices = cx.update_editor(|editor, window, cx| {
16397 let snapshot = editor.snapshot(window, cx).display_snapshot;
16398 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
16399 });
16400
16401 assert_eq!(
16402 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
16403 expected,
16404 "Active indent guide indices do not match"
16405 );
16406 }
16407
16408 assert_eq!(indent_guides, expected, "Indent guides do not match");
16409}
16410
16411fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
16412 IndentGuide {
16413 buffer_id,
16414 start_row: MultiBufferRow(start_row),
16415 end_row: MultiBufferRow(end_row),
16416 depth,
16417 tab_size: 4,
16418 settings: IndentGuideSettings {
16419 enabled: true,
16420 line_width: 1,
16421 active_line_width: 1,
16422 ..Default::default()
16423 },
16424 }
16425}
16426
16427#[gpui::test]
16428async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
16429 let (buffer_id, mut cx) = setup_indent_guides_editor(
16430 &"
16431 fn main() {
16432 let a = 1;
16433 }"
16434 .unindent(),
16435 cx,
16436 )
16437 .await;
16438
16439 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
16440}
16441
16442#[gpui::test]
16443async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
16444 let (buffer_id, mut cx) = setup_indent_guides_editor(
16445 &"
16446 fn main() {
16447 let a = 1;
16448 let b = 2;
16449 }"
16450 .unindent(),
16451 cx,
16452 )
16453 .await;
16454
16455 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
16456}
16457
16458#[gpui::test]
16459async fn test_indent_guide_nested(cx: &mut TestAppContext) {
16460 let (buffer_id, mut cx) = setup_indent_guides_editor(
16461 &"
16462 fn main() {
16463 let a = 1;
16464 if a == 3 {
16465 let b = 2;
16466 } else {
16467 let c = 3;
16468 }
16469 }"
16470 .unindent(),
16471 cx,
16472 )
16473 .await;
16474
16475 assert_indent_guides(
16476 0..8,
16477 vec![
16478 indent_guide(buffer_id, 1, 6, 0),
16479 indent_guide(buffer_id, 3, 3, 1),
16480 indent_guide(buffer_id, 5, 5, 1),
16481 ],
16482 None,
16483 &mut cx,
16484 );
16485}
16486
16487#[gpui::test]
16488async fn test_indent_guide_tab(cx: &mut TestAppContext) {
16489 let (buffer_id, mut cx) = setup_indent_guides_editor(
16490 &"
16491 fn main() {
16492 let a = 1;
16493 let b = 2;
16494 let c = 3;
16495 }"
16496 .unindent(),
16497 cx,
16498 )
16499 .await;
16500
16501 assert_indent_guides(
16502 0..5,
16503 vec![
16504 indent_guide(buffer_id, 1, 3, 0),
16505 indent_guide(buffer_id, 2, 2, 1),
16506 ],
16507 None,
16508 &mut cx,
16509 );
16510}
16511
16512#[gpui::test]
16513async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
16514 let (buffer_id, mut cx) = setup_indent_guides_editor(
16515 &"
16516 fn main() {
16517 let a = 1;
16518
16519 let c = 3;
16520 }"
16521 .unindent(),
16522 cx,
16523 )
16524 .await;
16525
16526 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
16527}
16528
16529#[gpui::test]
16530async fn test_indent_guide_complex(cx: &mut TestAppContext) {
16531 let (buffer_id, mut cx) = setup_indent_guides_editor(
16532 &"
16533 fn main() {
16534 let a = 1;
16535
16536 let c = 3;
16537
16538 if a == 3 {
16539 let b = 2;
16540 } else {
16541 let c = 3;
16542 }
16543 }"
16544 .unindent(),
16545 cx,
16546 )
16547 .await;
16548
16549 assert_indent_guides(
16550 0..11,
16551 vec![
16552 indent_guide(buffer_id, 1, 9, 0),
16553 indent_guide(buffer_id, 6, 6, 1),
16554 indent_guide(buffer_id, 8, 8, 1),
16555 ],
16556 None,
16557 &mut cx,
16558 );
16559}
16560
16561#[gpui::test]
16562async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
16563 let (buffer_id, mut cx) = setup_indent_guides_editor(
16564 &"
16565 fn main() {
16566 let a = 1;
16567
16568 let c = 3;
16569
16570 if a == 3 {
16571 let b = 2;
16572 } else {
16573 let c = 3;
16574 }
16575 }"
16576 .unindent(),
16577 cx,
16578 )
16579 .await;
16580
16581 assert_indent_guides(
16582 1..11,
16583 vec![
16584 indent_guide(buffer_id, 1, 9, 0),
16585 indent_guide(buffer_id, 6, 6, 1),
16586 indent_guide(buffer_id, 8, 8, 1),
16587 ],
16588 None,
16589 &mut cx,
16590 );
16591}
16592
16593#[gpui::test]
16594async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
16595 let (buffer_id, mut cx) = setup_indent_guides_editor(
16596 &"
16597 fn main() {
16598 let a = 1;
16599
16600 let c = 3;
16601
16602 if a == 3 {
16603 let b = 2;
16604 } else {
16605 let c = 3;
16606 }
16607 }"
16608 .unindent(),
16609 cx,
16610 )
16611 .await;
16612
16613 assert_indent_guides(
16614 1..10,
16615 vec![
16616 indent_guide(buffer_id, 1, 9, 0),
16617 indent_guide(buffer_id, 6, 6, 1),
16618 indent_guide(buffer_id, 8, 8, 1),
16619 ],
16620 None,
16621 &mut cx,
16622 );
16623}
16624
16625#[gpui::test]
16626async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
16627 let (buffer_id, mut cx) = setup_indent_guides_editor(
16628 &"
16629 block1
16630 block2
16631 block3
16632 block4
16633 block2
16634 block1
16635 block1"
16636 .unindent(),
16637 cx,
16638 )
16639 .await;
16640
16641 assert_indent_guides(
16642 1..10,
16643 vec![
16644 indent_guide(buffer_id, 1, 4, 0),
16645 indent_guide(buffer_id, 2, 3, 1),
16646 indent_guide(buffer_id, 3, 3, 2),
16647 ],
16648 None,
16649 &mut cx,
16650 );
16651}
16652
16653#[gpui::test]
16654async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
16655 let (buffer_id, mut cx) = setup_indent_guides_editor(
16656 &"
16657 block1
16658 block2
16659 block3
16660
16661 block1
16662 block1"
16663 .unindent(),
16664 cx,
16665 )
16666 .await;
16667
16668 assert_indent_guides(
16669 0..6,
16670 vec![
16671 indent_guide(buffer_id, 1, 2, 0),
16672 indent_guide(buffer_id, 2, 2, 1),
16673 ],
16674 None,
16675 &mut cx,
16676 );
16677}
16678
16679#[gpui::test]
16680async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
16681 let (buffer_id, mut cx) = setup_indent_guides_editor(
16682 &"
16683 block1
16684
16685
16686
16687 block2
16688 "
16689 .unindent(),
16690 cx,
16691 )
16692 .await;
16693
16694 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
16695}
16696
16697#[gpui::test]
16698async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
16699 let (buffer_id, mut cx) = setup_indent_guides_editor(
16700 &"
16701 def a:
16702 \tb = 3
16703 \tif True:
16704 \t\tc = 4
16705 \t\td = 5
16706 \tprint(b)
16707 "
16708 .unindent(),
16709 cx,
16710 )
16711 .await;
16712
16713 assert_indent_guides(
16714 0..6,
16715 vec![
16716 indent_guide(buffer_id, 1, 5, 0),
16717 indent_guide(buffer_id, 3, 4, 1),
16718 ],
16719 None,
16720 &mut cx,
16721 );
16722}
16723
16724#[gpui::test]
16725async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
16726 let (buffer_id, mut cx) = setup_indent_guides_editor(
16727 &"
16728 fn main() {
16729 let a = 1;
16730 }"
16731 .unindent(),
16732 cx,
16733 )
16734 .await;
16735
16736 cx.update_editor(|editor, window, cx| {
16737 editor.change_selections(None, window, cx, |s| {
16738 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
16739 });
16740 });
16741
16742 assert_indent_guides(
16743 0..3,
16744 vec![indent_guide(buffer_id, 1, 1, 0)],
16745 Some(vec![0]),
16746 &mut cx,
16747 );
16748}
16749
16750#[gpui::test]
16751async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
16752 let (buffer_id, mut cx) = setup_indent_guides_editor(
16753 &"
16754 fn main() {
16755 if 1 == 2 {
16756 let a = 1;
16757 }
16758 }"
16759 .unindent(),
16760 cx,
16761 )
16762 .await;
16763
16764 cx.update_editor(|editor, window, cx| {
16765 editor.change_selections(None, window, cx, |s| {
16766 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
16767 });
16768 });
16769
16770 assert_indent_guides(
16771 0..4,
16772 vec![
16773 indent_guide(buffer_id, 1, 3, 0),
16774 indent_guide(buffer_id, 2, 2, 1),
16775 ],
16776 Some(vec![1]),
16777 &mut cx,
16778 );
16779
16780 cx.update_editor(|editor, window, cx| {
16781 editor.change_selections(None, window, cx, |s| {
16782 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
16783 });
16784 });
16785
16786 assert_indent_guides(
16787 0..4,
16788 vec![
16789 indent_guide(buffer_id, 1, 3, 0),
16790 indent_guide(buffer_id, 2, 2, 1),
16791 ],
16792 Some(vec![1]),
16793 &mut cx,
16794 );
16795
16796 cx.update_editor(|editor, window, cx| {
16797 editor.change_selections(None, window, cx, |s| {
16798 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
16799 });
16800 });
16801
16802 assert_indent_guides(
16803 0..4,
16804 vec![
16805 indent_guide(buffer_id, 1, 3, 0),
16806 indent_guide(buffer_id, 2, 2, 1),
16807 ],
16808 Some(vec![0]),
16809 &mut cx,
16810 );
16811}
16812
16813#[gpui::test]
16814async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
16815 let (buffer_id, mut cx) = setup_indent_guides_editor(
16816 &"
16817 fn main() {
16818 let a = 1;
16819
16820 let b = 2;
16821 }"
16822 .unindent(),
16823 cx,
16824 )
16825 .await;
16826
16827 cx.update_editor(|editor, window, cx| {
16828 editor.change_selections(None, window, cx, |s| {
16829 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
16830 });
16831 });
16832
16833 assert_indent_guides(
16834 0..5,
16835 vec![indent_guide(buffer_id, 1, 3, 0)],
16836 Some(vec![0]),
16837 &mut cx,
16838 );
16839}
16840
16841#[gpui::test]
16842async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
16843 let (buffer_id, mut cx) = setup_indent_guides_editor(
16844 &"
16845 def m:
16846 a = 1
16847 pass"
16848 .unindent(),
16849 cx,
16850 )
16851 .await;
16852
16853 cx.update_editor(|editor, window, cx| {
16854 editor.change_selections(None, window, cx, |s| {
16855 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
16856 });
16857 });
16858
16859 assert_indent_guides(
16860 0..3,
16861 vec![indent_guide(buffer_id, 1, 2, 0)],
16862 Some(vec![0]),
16863 &mut cx,
16864 );
16865}
16866
16867#[gpui::test]
16868async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
16869 init_test(cx, |_| {});
16870 let mut cx = EditorTestContext::new(cx).await;
16871 let text = indoc! {
16872 "
16873 impl A {
16874 fn b() {
16875 0;
16876 3;
16877 5;
16878 6;
16879 7;
16880 }
16881 }
16882 "
16883 };
16884 let base_text = indoc! {
16885 "
16886 impl A {
16887 fn b() {
16888 0;
16889 1;
16890 2;
16891 3;
16892 4;
16893 }
16894 fn c() {
16895 5;
16896 6;
16897 7;
16898 }
16899 }
16900 "
16901 };
16902
16903 cx.update_editor(|editor, window, cx| {
16904 editor.set_text(text, window, cx);
16905
16906 editor.buffer().update(cx, |multibuffer, cx| {
16907 let buffer = multibuffer.as_singleton().unwrap();
16908 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
16909
16910 multibuffer.set_all_diff_hunks_expanded(cx);
16911 multibuffer.add_diff(diff, cx);
16912
16913 buffer.read(cx).remote_id()
16914 })
16915 });
16916 cx.run_until_parked();
16917
16918 cx.assert_state_with_diff(
16919 indoc! { "
16920 impl A {
16921 fn b() {
16922 0;
16923 - 1;
16924 - 2;
16925 3;
16926 - 4;
16927 - }
16928 - fn c() {
16929 5;
16930 6;
16931 7;
16932 }
16933 }
16934 ˇ"
16935 }
16936 .to_string(),
16937 );
16938
16939 let mut actual_guides = cx.update_editor(|editor, window, cx| {
16940 editor
16941 .snapshot(window, cx)
16942 .buffer_snapshot
16943 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
16944 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
16945 .collect::<Vec<_>>()
16946 });
16947 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
16948 assert_eq!(
16949 actual_guides,
16950 vec![
16951 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
16952 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
16953 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
16954 ]
16955 );
16956}
16957
16958#[gpui::test]
16959async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16960 init_test(cx, |_| {});
16961 let mut cx = EditorTestContext::new(cx).await;
16962
16963 let diff_base = r#"
16964 a
16965 b
16966 c
16967 "#
16968 .unindent();
16969
16970 cx.set_state(
16971 &r#"
16972 ˇA
16973 b
16974 C
16975 "#
16976 .unindent(),
16977 );
16978 cx.set_head_text(&diff_base);
16979 cx.update_editor(|editor, window, cx| {
16980 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16981 });
16982 executor.run_until_parked();
16983
16984 let both_hunks_expanded = r#"
16985 - a
16986 + ˇA
16987 b
16988 - c
16989 + C
16990 "#
16991 .unindent();
16992
16993 cx.assert_state_with_diff(both_hunks_expanded.clone());
16994
16995 let hunk_ranges = cx.update_editor(|editor, window, cx| {
16996 let snapshot = editor.snapshot(window, cx);
16997 let hunks = editor
16998 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
16999 .collect::<Vec<_>>();
17000 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
17001 let buffer_id = hunks[0].buffer_id;
17002 hunks
17003 .into_iter()
17004 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
17005 .collect::<Vec<_>>()
17006 });
17007 assert_eq!(hunk_ranges.len(), 2);
17008
17009 cx.update_editor(|editor, _, cx| {
17010 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
17011 });
17012 executor.run_until_parked();
17013
17014 let second_hunk_expanded = r#"
17015 ˇA
17016 b
17017 - c
17018 + C
17019 "#
17020 .unindent();
17021
17022 cx.assert_state_with_diff(second_hunk_expanded);
17023
17024 cx.update_editor(|editor, _, cx| {
17025 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
17026 });
17027 executor.run_until_parked();
17028
17029 cx.assert_state_with_diff(both_hunks_expanded.clone());
17030
17031 cx.update_editor(|editor, _, cx| {
17032 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
17033 });
17034 executor.run_until_parked();
17035
17036 let first_hunk_expanded = r#"
17037 - a
17038 + ˇA
17039 b
17040 C
17041 "#
17042 .unindent();
17043
17044 cx.assert_state_with_diff(first_hunk_expanded);
17045
17046 cx.update_editor(|editor, _, cx| {
17047 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
17048 });
17049 executor.run_until_parked();
17050
17051 cx.assert_state_with_diff(both_hunks_expanded);
17052
17053 cx.set_state(
17054 &r#"
17055 ˇA
17056 b
17057 "#
17058 .unindent(),
17059 );
17060 cx.run_until_parked();
17061
17062 // TODO this cursor position seems bad
17063 cx.assert_state_with_diff(
17064 r#"
17065 - ˇa
17066 + A
17067 b
17068 "#
17069 .unindent(),
17070 );
17071
17072 cx.update_editor(|editor, window, cx| {
17073 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17074 });
17075
17076 cx.assert_state_with_diff(
17077 r#"
17078 - ˇa
17079 + A
17080 b
17081 - c
17082 "#
17083 .unindent(),
17084 );
17085
17086 let hunk_ranges = cx.update_editor(|editor, window, cx| {
17087 let snapshot = editor.snapshot(window, cx);
17088 let hunks = editor
17089 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
17090 .collect::<Vec<_>>();
17091 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
17092 let buffer_id = hunks[0].buffer_id;
17093 hunks
17094 .into_iter()
17095 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
17096 .collect::<Vec<_>>()
17097 });
17098 assert_eq!(hunk_ranges.len(), 2);
17099
17100 cx.update_editor(|editor, _, cx| {
17101 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
17102 });
17103 executor.run_until_parked();
17104
17105 cx.assert_state_with_diff(
17106 r#"
17107 - ˇa
17108 + A
17109 b
17110 "#
17111 .unindent(),
17112 );
17113}
17114
17115#[gpui::test]
17116async fn test_toggle_deletion_hunk_at_start_of_file(
17117 executor: BackgroundExecutor,
17118 cx: &mut TestAppContext,
17119) {
17120 init_test(cx, |_| {});
17121 let mut cx = EditorTestContext::new(cx).await;
17122
17123 let diff_base = r#"
17124 a
17125 b
17126 c
17127 "#
17128 .unindent();
17129
17130 cx.set_state(
17131 &r#"
17132 ˇb
17133 c
17134 "#
17135 .unindent(),
17136 );
17137 cx.set_head_text(&diff_base);
17138 cx.update_editor(|editor, window, cx| {
17139 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17140 });
17141 executor.run_until_parked();
17142
17143 let hunk_expanded = r#"
17144 - a
17145 ˇb
17146 c
17147 "#
17148 .unindent();
17149
17150 cx.assert_state_with_diff(hunk_expanded.clone());
17151
17152 let hunk_ranges = cx.update_editor(|editor, window, cx| {
17153 let snapshot = editor.snapshot(window, cx);
17154 let hunks = editor
17155 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
17156 .collect::<Vec<_>>();
17157 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
17158 let buffer_id = hunks[0].buffer_id;
17159 hunks
17160 .into_iter()
17161 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
17162 .collect::<Vec<_>>()
17163 });
17164 assert_eq!(hunk_ranges.len(), 1);
17165
17166 cx.update_editor(|editor, _, cx| {
17167 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
17168 });
17169 executor.run_until_parked();
17170
17171 let hunk_collapsed = r#"
17172 ˇb
17173 c
17174 "#
17175 .unindent();
17176
17177 cx.assert_state_with_diff(hunk_collapsed);
17178
17179 cx.update_editor(|editor, _, cx| {
17180 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
17181 });
17182 executor.run_until_parked();
17183
17184 cx.assert_state_with_diff(hunk_expanded.clone());
17185}
17186
17187#[gpui::test]
17188async fn test_display_diff_hunks(cx: &mut TestAppContext) {
17189 init_test(cx, |_| {});
17190
17191 let fs = FakeFs::new(cx.executor());
17192 fs.insert_tree(
17193 path!("/test"),
17194 json!({
17195 ".git": {},
17196 "file-1": "ONE\n",
17197 "file-2": "TWO\n",
17198 "file-3": "THREE\n",
17199 }),
17200 )
17201 .await;
17202
17203 fs.set_head_for_repo(
17204 path!("/test/.git").as_ref(),
17205 &[
17206 ("file-1".into(), "one\n".into()),
17207 ("file-2".into(), "two\n".into()),
17208 ("file-3".into(), "three\n".into()),
17209 ],
17210 );
17211
17212 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
17213 let mut buffers = vec![];
17214 for i in 1..=3 {
17215 let buffer = project
17216 .update(cx, |project, cx| {
17217 let path = format!(path!("/test/file-{}"), i);
17218 project.open_local_buffer(path, cx)
17219 })
17220 .await
17221 .unwrap();
17222 buffers.push(buffer);
17223 }
17224
17225 let multibuffer = cx.new(|cx| {
17226 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
17227 multibuffer.set_all_diff_hunks_expanded(cx);
17228 for buffer in &buffers {
17229 let snapshot = buffer.read(cx).snapshot();
17230 multibuffer.set_excerpts_for_path(
17231 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
17232 buffer.clone(),
17233 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
17234 DEFAULT_MULTIBUFFER_CONTEXT,
17235 cx,
17236 );
17237 }
17238 multibuffer
17239 });
17240
17241 let editor = cx.add_window(|window, cx| {
17242 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
17243 });
17244 cx.run_until_parked();
17245
17246 let snapshot = editor
17247 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
17248 .unwrap();
17249 let hunks = snapshot
17250 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
17251 .map(|hunk| match hunk {
17252 DisplayDiffHunk::Unfolded {
17253 display_row_range, ..
17254 } => display_row_range,
17255 DisplayDiffHunk::Folded { .. } => unreachable!(),
17256 })
17257 .collect::<Vec<_>>();
17258 assert_eq!(
17259 hunks,
17260 [
17261 DisplayRow(2)..DisplayRow(4),
17262 DisplayRow(7)..DisplayRow(9),
17263 DisplayRow(12)..DisplayRow(14),
17264 ]
17265 );
17266}
17267
17268#[gpui::test]
17269async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
17270 init_test(cx, |_| {});
17271
17272 let mut cx = EditorTestContext::new(cx).await;
17273 cx.set_head_text(indoc! { "
17274 one
17275 two
17276 three
17277 four
17278 five
17279 "
17280 });
17281 cx.set_index_text(indoc! { "
17282 one
17283 two
17284 three
17285 four
17286 five
17287 "
17288 });
17289 cx.set_state(indoc! {"
17290 one
17291 TWO
17292 ˇTHREE
17293 FOUR
17294 five
17295 "});
17296 cx.run_until_parked();
17297 cx.update_editor(|editor, window, cx| {
17298 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
17299 });
17300 cx.run_until_parked();
17301 cx.assert_index_text(Some(indoc! {"
17302 one
17303 TWO
17304 THREE
17305 FOUR
17306 five
17307 "}));
17308 cx.set_state(indoc! { "
17309 one
17310 TWO
17311 ˇTHREE-HUNDRED
17312 FOUR
17313 five
17314 "});
17315 cx.run_until_parked();
17316 cx.update_editor(|editor, window, cx| {
17317 let snapshot = editor.snapshot(window, cx);
17318 let hunks = editor
17319 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
17320 .collect::<Vec<_>>();
17321 assert_eq!(hunks.len(), 1);
17322 assert_eq!(
17323 hunks[0].status(),
17324 DiffHunkStatus {
17325 kind: DiffHunkStatusKind::Modified,
17326 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
17327 }
17328 );
17329
17330 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
17331 });
17332 cx.run_until_parked();
17333 cx.assert_index_text(Some(indoc! {"
17334 one
17335 TWO
17336 THREE-HUNDRED
17337 FOUR
17338 five
17339 "}));
17340}
17341
17342#[gpui::test]
17343fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
17344 init_test(cx, |_| {});
17345
17346 let editor = cx.add_window(|window, cx| {
17347 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
17348 build_editor(buffer, window, cx)
17349 });
17350
17351 let render_args = Arc::new(Mutex::new(None));
17352 let snapshot = editor
17353 .update(cx, |editor, window, cx| {
17354 let snapshot = editor.buffer().read(cx).snapshot(cx);
17355 let range =
17356 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
17357
17358 struct RenderArgs {
17359 row: MultiBufferRow,
17360 folded: bool,
17361 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
17362 }
17363
17364 let crease = Crease::inline(
17365 range,
17366 FoldPlaceholder::test(),
17367 {
17368 let toggle_callback = render_args.clone();
17369 move |row, folded, callback, _window, _cx| {
17370 *toggle_callback.lock() = Some(RenderArgs {
17371 row,
17372 folded,
17373 callback,
17374 });
17375 div()
17376 }
17377 },
17378 |_row, _folded, _window, _cx| div(),
17379 );
17380
17381 editor.insert_creases(Some(crease), cx);
17382 let snapshot = editor.snapshot(window, cx);
17383 let _div = snapshot.render_crease_toggle(
17384 MultiBufferRow(1),
17385 false,
17386 cx.entity().clone(),
17387 window,
17388 cx,
17389 );
17390 snapshot
17391 })
17392 .unwrap();
17393
17394 let render_args = render_args.lock().take().unwrap();
17395 assert_eq!(render_args.row, MultiBufferRow(1));
17396 assert!(!render_args.folded);
17397 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
17398
17399 cx.update_window(*editor, |_, window, cx| {
17400 (render_args.callback)(true, window, cx)
17401 })
17402 .unwrap();
17403 let snapshot = editor
17404 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
17405 .unwrap();
17406 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
17407
17408 cx.update_window(*editor, |_, window, cx| {
17409 (render_args.callback)(false, window, cx)
17410 })
17411 .unwrap();
17412 let snapshot = editor
17413 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
17414 .unwrap();
17415 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
17416}
17417
17418#[gpui::test]
17419async fn test_input_text(cx: &mut TestAppContext) {
17420 init_test(cx, |_| {});
17421 let mut cx = EditorTestContext::new(cx).await;
17422
17423 cx.set_state(
17424 &r#"ˇone
17425 two
17426
17427 three
17428 fourˇ
17429 five
17430
17431 siˇx"#
17432 .unindent(),
17433 );
17434
17435 cx.dispatch_action(HandleInput(String::new()));
17436 cx.assert_editor_state(
17437 &r#"ˇone
17438 two
17439
17440 three
17441 fourˇ
17442 five
17443
17444 siˇx"#
17445 .unindent(),
17446 );
17447
17448 cx.dispatch_action(HandleInput("AAAA".to_string()));
17449 cx.assert_editor_state(
17450 &r#"AAAAˇone
17451 two
17452
17453 three
17454 fourAAAAˇ
17455 five
17456
17457 siAAAAˇx"#
17458 .unindent(),
17459 );
17460}
17461
17462#[gpui::test]
17463async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
17464 init_test(cx, |_| {});
17465
17466 let mut cx = EditorTestContext::new(cx).await;
17467 cx.set_state(
17468 r#"let foo = 1;
17469let foo = 2;
17470let foo = 3;
17471let fooˇ = 4;
17472let foo = 5;
17473let foo = 6;
17474let foo = 7;
17475let foo = 8;
17476let foo = 9;
17477let foo = 10;
17478let foo = 11;
17479let foo = 12;
17480let foo = 13;
17481let foo = 14;
17482let foo = 15;"#,
17483 );
17484
17485 cx.update_editor(|e, window, cx| {
17486 assert_eq!(
17487 e.next_scroll_position,
17488 NextScrollCursorCenterTopBottom::Center,
17489 "Default next scroll direction is center",
17490 );
17491
17492 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17493 assert_eq!(
17494 e.next_scroll_position,
17495 NextScrollCursorCenterTopBottom::Top,
17496 "After center, next scroll direction should be top",
17497 );
17498
17499 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17500 assert_eq!(
17501 e.next_scroll_position,
17502 NextScrollCursorCenterTopBottom::Bottom,
17503 "After top, next scroll direction should be bottom",
17504 );
17505
17506 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17507 assert_eq!(
17508 e.next_scroll_position,
17509 NextScrollCursorCenterTopBottom::Center,
17510 "After bottom, scrolling should start over",
17511 );
17512
17513 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17514 assert_eq!(
17515 e.next_scroll_position,
17516 NextScrollCursorCenterTopBottom::Top,
17517 "Scrolling continues if retriggered fast enough"
17518 );
17519 });
17520
17521 cx.executor()
17522 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
17523 cx.executor().run_until_parked();
17524 cx.update_editor(|e, _, _| {
17525 assert_eq!(
17526 e.next_scroll_position,
17527 NextScrollCursorCenterTopBottom::Center,
17528 "If scrolling is not triggered fast enough, it should reset"
17529 );
17530 });
17531}
17532
17533#[gpui::test]
17534async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
17535 init_test(cx, |_| {});
17536 let mut cx = EditorLspTestContext::new_rust(
17537 lsp::ServerCapabilities {
17538 definition_provider: Some(lsp::OneOf::Left(true)),
17539 references_provider: Some(lsp::OneOf::Left(true)),
17540 ..lsp::ServerCapabilities::default()
17541 },
17542 cx,
17543 )
17544 .await;
17545
17546 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
17547 let go_to_definition = cx
17548 .lsp
17549 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
17550 move |params, _| async move {
17551 if empty_go_to_definition {
17552 Ok(None)
17553 } else {
17554 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
17555 uri: params.text_document_position_params.text_document.uri,
17556 range: lsp::Range::new(
17557 lsp::Position::new(4, 3),
17558 lsp::Position::new(4, 6),
17559 ),
17560 })))
17561 }
17562 },
17563 );
17564 let references = cx
17565 .lsp
17566 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
17567 Ok(Some(vec![lsp::Location {
17568 uri: params.text_document_position.text_document.uri,
17569 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
17570 }]))
17571 });
17572 (go_to_definition, references)
17573 };
17574
17575 cx.set_state(
17576 &r#"fn one() {
17577 let mut a = ˇtwo();
17578 }
17579
17580 fn two() {}"#
17581 .unindent(),
17582 );
17583 set_up_lsp_handlers(false, &mut cx);
17584 let navigated = cx
17585 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
17586 .await
17587 .expect("Failed to navigate to definition");
17588 assert_eq!(
17589 navigated,
17590 Navigated::Yes,
17591 "Should have navigated to definition from the GetDefinition response"
17592 );
17593 cx.assert_editor_state(
17594 &r#"fn one() {
17595 let mut a = two();
17596 }
17597
17598 fn «twoˇ»() {}"#
17599 .unindent(),
17600 );
17601
17602 let editors = cx.update_workspace(|workspace, _, cx| {
17603 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
17604 });
17605 cx.update_editor(|_, _, test_editor_cx| {
17606 assert_eq!(
17607 editors.len(),
17608 1,
17609 "Initially, only one, test, editor should be open in the workspace"
17610 );
17611 assert_eq!(
17612 test_editor_cx.entity(),
17613 editors.last().expect("Asserted len is 1").clone()
17614 );
17615 });
17616
17617 set_up_lsp_handlers(true, &mut cx);
17618 let navigated = cx
17619 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
17620 .await
17621 .expect("Failed to navigate to lookup references");
17622 assert_eq!(
17623 navigated,
17624 Navigated::Yes,
17625 "Should have navigated to references as a fallback after empty GoToDefinition response"
17626 );
17627 // We should not change the selections in the existing file,
17628 // if opening another milti buffer with the references
17629 cx.assert_editor_state(
17630 &r#"fn one() {
17631 let mut a = two();
17632 }
17633
17634 fn «twoˇ»() {}"#
17635 .unindent(),
17636 );
17637 let editors = cx.update_workspace(|workspace, _, cx| {
17638 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
17639 });
17640 cx.update_editor(|_, _, test_editor_cx| {
17641 assert_eq!(
17642 editors.len(),
17643 2,
17644 "After falling back to references search, we open a new editor with the results"
17645 );
17646 let references_fallback_text = editors
17647 .into_iter()
17648 .find(|new_editor| *new_editor != test_editor_cx.entity())
17649 .expect("Should have one non-test editor now")
17650 .read(test_editor_cx)
17651 .text(test_editor_cx);
17652 assert_eq!(
17653 references_fallback_text, "fn one() {\n let mut a = two();\n}",
17654 "Should use the range from the references response and not the GoToDefinition one"
17655 );
17656 });
17657}
17658
17659#[gpui::test]
17660async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
17661 init_test(cx, |_| {});
17662 cx.update(|cx| {
17663 let mut editor_settings = EditorSettings::get_global(cx).clone();
17664 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
17665 EditorSettings::override_global(editor_settings, cx);
17666 });
17667 let mut cx = EditorLspTestContext::new_rust(
17668 lsp::ServerCapabilities {
17669 definition_provider: Some(lsp::OneOf::Left(true)),
17670 references_provider: Some(lsp::OneOf::Left(true)),
17671 ..lsp::ServerCapabilities::default()
17672 },
17673 cx,
17674 )
17675 .await;
17676 let original_state = r#"fn one() {
17677 let mut a = ˇtwo();
17678 }
17679
17680 fn two() {}"#
17681 .unindent();
17682 cx.set_state(&original_state);
17683
17684 let mut go_to_definition = cx
17685 .lsp
17686 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
17687 move |_, _| async move { Ok(None) },
17688 );
17689 let _references = cx
17690 .lsp
17691 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
17692 panic!("Should not call for references with no go to definition fallback")
17693 });
17694
17695 let navigated = cx
17696 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
17697 .await
17698 .expect("Failed to navigate to lookup references");
17699 go_to_definition
17700 .next()
17701 .await
17702 .expect("Should have called the go_to_definition handler");
17703
17704 assert_eq!(
17705 navigated,
17706 Navigated::No,
17707 "Should have navigated to references as a fallback after empty GoToDefinition response"
17708 );
17709 cx.assert_editor_state(&original_state);
17710 let editors = cx.update_workspace(|workspace, _, cx| {
17711 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
17712 });
17713 cx.update_editor(|_, _, _| {
17714 assert_eq!(
17715 editors.len(),
17716 1,
17717 "After unsuccessful fallback, no other editor should have been opened"
17718 );
17719 });
17720}
17721
17722#[gpui::test]
17723async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
17724 init_test(cx, |_| {});
17725
17726 let language = Arc::new(Language::new(
17727 LanguageConfig::default(),
17728 Some(tree_sitter_rust::LANGUAGE.into()),
17729 ));
17730
17731 let text = r#"
17732 #[cfg(test)]
17733 mod tests() {
17734 #[test]
17735 fn runnable_1() {
17736 let a = 1;
17737 }
17738
17739 #[test]
17740 fn runnable_2() {
17741 let a = 1;
17742 let b = 2;
17743 }
17744 }
17745 "#
17746 .unindent();
17747
17748 let fs = FakeFs::new(cx.executor());
17749 fs.insert_file("/file.rs", Default::default()).await;
17750
17751 let project = Project::test(fs, ["/a".as_ref()], cx).await;
17752 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17753 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17754 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
17755 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
17756
17757 let editor = cx.new_window_entity(|window, cx| {
17758 Editor::new(
17759 EditorMode::full(),
17760 multi_buffer,
17761 Some(project.clone()),
17762 window,
17763 cx,
17764 )
17765 });
17766
17767 editor.update_in(cx, |editor, window, cx| {
17768 let snapshot = editor.buffer().read(cx).snapshot(cx);
17769 editor.tasks.insert(
17770 (buffer.read(cx).remote_id(), 3),
17771 RunnableTasks {
17772 templates: vec![],
17773 offset: snapshot.anchor_before(43),
17774 column: 0,
17775 extra_variables: HashMap::default(),
17776 context_range: BufferOffset(43)..BufferOffset(85),
17777 },
17778 );
17779 editor.tasks.insert(
17780 (buffer.read(cx).remote_id(), 8),
17781 RunnableTasks {
17782 templates: vec![],
17783 offset: snapshot.anchor_before(86),
17784 column: 0,
17785 extra_variables: HashMap::default(),
17786 context_range: BufferOffset(86)..BufferOffset(191),
17787 },
17788 );
17789
17790 // Test finding task when cursor is inside function body
17791 editor.change_selections(None, window, cx, |s| {
17792 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
17793 });
17794 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
17795 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
17796
17797 // Test finding task when cursor is on function name
17798 editor.change_selections(None, window, cx, |s| {
17799 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
17800 });
17801 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
17802 assert_eq!(row, 8, "Should find task when cursor is on function name");
17803 });
17804}
17805
17806#[gpui::test]
17807async fn test_folding_buffers(cx: &mut TestAppContext) {
17808 init_test(cx, |_| {});
17809
17810 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
17811 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
17812 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
17813
17814 let fs = FakeFs::new(cx.executor());
17815 fs.insert_tree(
17816 path!("/a"),
17817 json!({
17818 "first.rs": sample_text_1,
17819 "second.rs": sample_text_2,
17820 "third.rs": sample_text_3,
17821 }),
17822 )
17823 .await;
17824 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17825 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17826 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17827 let worktree = project.update(cx, |project, cx| {
17828 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
17829 assert_eq!(worktrees.len(), 1);
17830 worktrees.pop().unwrap()
17831 });
17832 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
17833
17834 let buffer_1 = project
17835 .update(cx, |project, cx| {
17836 project.open_buffer((worktree_id, "first.rs"), cx)
17837 })
17838 .await
17839 .unwrap();
17840 let buffer_2 = project
17841 .update(cx, |project, cx| {
17842 project.open_buffer((worktree_id, "second.rs"), cx)
17843 })
17844 .await
17845 .unwrap();
17846 let buffer_3 = project
17847 .update(cx, |project, cx| {
17848 project.open_buffer((worktree_id, "third.rs"), cx)
17849 })
17850 .await
17851 .unwrap();
17852
17853 let multi_buffer = cx.new(|cx| {
17854 let mut multi_buffer = MultiBuffer::new(ReadWrite);
17855 multi_buffer.push_excerpts(
17856 buffer_1.clone(),
17857 [
17858 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17859 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17860 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17861 ],
17862 cx,
17863 );
17864 multi_buffer.push_excerpts(
17865 buffer_2.clone(),
17866 [
17867 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17868 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17869 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17870 ],
17871 cx,
17872 );
17873 multi_buffer.push_excerpts(
17874 buffer_3.clone(),
17875 [
17876 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17877 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17878 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17879 ],
17880 cx,
17881 );
17882 multi_buffer
17883 });
17884 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
17885 Editor::new(
17886 EditorMode::full(),
17887 multi_buffer.clone(),
17888 Some(project.clone()),
17889 window,
17890 cx,
17891 )
17892 });
17893
17894 assert_eq!(
17895 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17896 "\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",
17897 );
17898
17899 multi_buffer_editor.update(cx, |editor, cx| {
17900 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
17901 });
17902 assert_eq!(
17903 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17904 "\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",
17905 "After folding the first buffer, its text should not be displayed"
17906 );
17907
17908 multi_buffer_editor.update(cx, |editor, cx| {
17909 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
17910 });
17911 assert_eq!(
17912 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17913 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
17914 "After folding the second buffer, its text should not be displayed"
17915 );
17916
17917 multi_buffer_editor.update(cx, |editor, cx| {
17918 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
17919 });
17920 assert_eq!(
17921 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17922 "\n\n\n\n\n",
17923 "After folding the third buffer, its text should not be displayed"
17924 );
17925
17926 // Emulate selection inside the fold logic, that should work
17927 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17928 editor
17929 .snapshot(window, cx)
17930 .next_line_boundary(Point::new(0, 4));
17931 });
17932
17933 multi_buffer_editor.update(cx, |editor, cx| {
17934 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
17935 });
17936 assert_eq!(
17937 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17938 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
17939 "After unfolding the second buffer, its text should be displayed"
17940 );
17941
17942 // Typing inside of buffer 1 causes that buffer to be unfolded.
17943 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17944 assert_eq!(
17945 multi_buffer
17946 .read(cx)
17947 .snapshot(cx)
17948 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
17949 .collect::<String>(),
17950 "bbbb"
17951 );
17952 editor.change_selections(None, window, cx, |selections| {
17953 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
17954 });
17955 editor.handle_input("B", window, cx);
17956 });
17957
17958 assert_eq!(
17959 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17960 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
17961 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
17962 );
17963
17964 multi_buffer_editor.update(cx, |editor, cx| {
17965 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
17966 });
17967 assert_eq!(
17968 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17969 "\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",
17970 "After unfolding the all buffers, all original text should be displayed"
17971 );
17972}
17973
17974#[gpui::test]
17975async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
17976 init_test(cx, |_| {});
17977
17978 let sample_text_1 = "1111\n2222\n3333".to_string();
17979 let sample_text_2 = "4444\n5555\n6666".to_string();
17980 let sample_text_3 = "7777\n8888\n9999".to_string();
17981
17982 let fs = FakeFs::new(cx.executor());
17983 fs.insert_tree(
17984 path!("/a"),
17985 json!({
17986 "first.rs": sample_text_1,
17987 "second.rs": sample_text_2,
17988 "third.rs": sample_text_3,
17989 }),
17990 )
17991 .await;
17992 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17993 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17994 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17995 let worktree = project.update(cx, |project, cx| {
17996 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
17997 assert_eq!(worktrees.len(), 1);
17998 worktrees.pop().unwrap()
17999 });
18000 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
18001
18002 let buffer_1 = project
18003 .update(cx, |project, cx| {
18004 project.open_buffer((worktree_id, "first.rs"), cx)
18005 })
18006 .await
18007 .unwrap();
18008 let buffer_2 = project
18009 .update(cx, |project, cx| {
18010 project.open_buffer((worktree_id, "second.rs"), cx)
18011 })
18012 .await
18013 .unwrap();
18014 let buffer_3 = project
18015 .update(cx, |project, cx| {
18016 project.open_buffer((worktree_id, "third.rs"), cx)
18017 })
18018 .await
18019 .unwrap();
18020
18021 let multi_buffer = cx.new(|cx| {
18022 let mut multi_buffer = MultiBuffer::new(ReadWrite);
18023 multi_buffer.push_excerpts(
18024 buffer_1.clone(),
18025 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
18026 cx,
18027 );
18028 multi_buffer.push_excerpts(
18029 buffer_2.clone(),
18030 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
18031 cx,
18032 );
18033 multi_buffer.push_excerpts(
18034 buffer_3.clone(),
18035 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
18036 cx,
18037 );
18038 multi_buffer
18039 });
18040
18041 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18042 Editor::new(
18043 EditorMode::full(),
18044 multi_buffer,
18045 Some(project.clone()),
18046 window,
18047 cx,
18048 )
18049 });
18050
18051 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
18052 assert_eq!(
18053 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18054 full_text,
18055 );
18056
18057 multi_buffer_editor.update(cx, |editor, cx| {
18058 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
18059 });
18060 assert_eq!(
18061 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18062 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
18063 "After folding the first buffer, its text should not be displayed"
18064 );
18065
18066 multi_buffer_editor.update(cx, |editor, cx| {
18067 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
18068 });
18069
18070 assert_eq!(
18071 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18072 "\n\n\n\n\n\n7777\n8888\n9999",
18073 "After folding the second buffer, its text should not be displayed"
18074 );
18075
18076 multi_buffer_editor.update(cx, |editor, cx| {
18077 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
18078 });
18079 assert_eq!(
18080 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18081 "\n\n\n\n\n",
18082 "After folding the third buffer, its text should not be displayed"
18083 );
18084
18085 multi_buffer_editor.update(cx, |editor, cx| {
18086 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
18087 });
18088 assert_eq!(
18089 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18090 "\n\n\n\n4444\n5555\n6666\n\n",
18091 "After unfolding the second buffer, its text should be displayed"
18092 );
18093
18094 multi_buffer_editor.update(cx, |editor, cx| {
18095 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
18096 });
18097 assert_eq!(
18098 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18099 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
18100 "After unfolding the first buffer, its text should be displayed"
18101 );
18102
18103 multi_buffer_editor.update(cx, |editor, cx| {
18104 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
18105 });
18106 assert_eq!(
18107 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18108 full_text,
18109 "After unfolding all buffers, all original text should be displayed"
18110 );
18111}
18112
18113#[gpui::test]
18114async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
18115 init_test(cx, |_| {});
18116
18117 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
18118
18119 let fs = FakeFs::new(cx.executor());
18120 fs.insert_tree(
18121 path!("/a"),
18122 json!({
18123 "main.rs": sample_text,
18124 }),
18125 )
18126 .await;
18127 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18128 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18129 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18130 let worktree = project.update(cx, |project, cx| {
18131 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
18132 assert_eq!(worktrees.len(), 1);
18133 worktrees.pop().unwrap()
18134 });
18135 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
18136
18137 let buffer_1 = project
18138 .update(cx, |project, cx| {
18139 project.open_buffer((worktree_id, "main.rs"), cx)
18140 })
18141 .await
18142 .unwrap();
18143
18144 let multi_buffer = cx.new(|cx| {
18145 let mut multi_buffer = MultiBuffer::new(ReadWrite);
18146 multi_buffer.push_excerpts(
18147 buffer_1.clone(),
18148 [ExcerptRange::new(
18149 Point::new(0, 0)
18150 ..Point::new(
18151 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
18152 0,
18153 ),
18154 )],
18155 cx,
18156 );
18157 multi_buffer
18158 });
18159 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18160 Editor::new(
18161 EditorMode::full(),
18162 multi_buffer,
18163 Some(project.clone()),
18164 window,
18165 cx,
18166 )
18167 });
18168
18169 let selection_range = Point::new(1, 0)..Point::new(2, 0);
18170 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18171 enum TestHighlight {}
18172 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
18173 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
18174 editor.highlight_text::<TestHighlight>(
18175 vec![highlight_range.clone()],
18176 HighlightStyle::color(Hsla::green()),
18177 cx,
18178 );
18179 editor.change_selections(None, window, cx, |s| s.select_ranges(Some(highlight_range)));
18180 });
18181
18182 let full_text = format!("\n\n{sample_text}");
18183 assert_eq!(
18184 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18185 full_text,
18186 );
18187}
18188
18189#[gpui::test]
18190async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
18191 init_test(cx, |_| {});
18192 cx.update(|cx| {
18193 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
18194 "keymaps/default-linux.json",
18195 cx,
18196 )
18197 .unwrap();
18198 cx.bind_keys(default_key_bindings);
18199 });
18200
18201 let (editor, cx) = cx.add_window_view(|window, cx| {
18202 let multi_buffer = MultiBuffer::build_multi(
18203 [
18204 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
18205 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
18206 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
18207 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
18208 ],
18209 cx,
18210 );
18211 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
18212
18213 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
18214 // fold all but the second buffer, so that we test navigating between two
18215 // adjacent folded buffers, as well as folded buffers at the start and
18216 // end the multibuffer
18217 editor.fold_buffer(buffer_ids[0], cx);
18218 editor.fold_buffer(buffer_ids[2], cx);
18219 editor.fold_buffer(buffer_ids[3], cx);
18220
18221 editor
18222 });
18223 cx.simulate_resize(size(px(1000.), px(1000.)));
18224
18225 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
18226 cx.assert_excerpts_with_selections(indoc! {"
18227 [EXCERPT]
18228 ˇ[FOLDED]
18229 [EXCERPT]
18230 a1
18231 b1
18232 [EXCERPT]
18233 [FOLDED]
18234 [EXCERPT]
18235 [FOLDED]
18236 "
18237 });
18238 cx.simulate_keystroke("down");
18239 cx.assert_excerpts_with_selections(indoc! {"
18240 [EXCERPT]
18241 [FOLDED]
18242 [EXCERPT]
18243 ˇa1
18244 b1
18245 [EXCERPT]
18246 [FOLDED]
18247 [EXCERPT]
18248 [FOLDED]
18249 "
18250 });
18251 cx.simulate_keystroke("down");
18252 cx.assert_excerpts_with_selections(indoc! {"
18253 [EXCERPT]
18254 [FOLDED]
18255 [EXCERPT]
18256 a1
18257 ˇb1
18258 [EXCERPT]
18259 [FOLDED]
18260 [EXCERPT]
18261 [FOLDED]
18262 "
18263 });
18264 cx.simulate_keystroke("down");
18265 cx.assert_excerpts_with_selections(indoc! {"
18266 [EXCERPT]
18267 [FOLDED]
18268 [EXCERPT]
18269 a1
18270 b1
18271 ˇ[EXCERPT]
18272 [FOLDED]
18273 [EXCERPT]
18274 [FOLDED]
18275 "
18276 });
18277 cx.simulate_keystroke("down");
18278 cx.assert_excerpts_with_selections(indoc! {"
18279 [EXCERPT]
18280 [FOLDED]
18281 [EXCERPT]
18282 a1
18283 b1
18284 [EXCERPT]
18285 ˇ[FOLDED]
18286 [EXCERPT]
18287 [FOLDED]
18288 "
18289 });
18290 for _ in 0..5 {
18291 cx.simulate_keystroke("down");
18292 cx.assert_excerpts_with_selections(indoc! {"
18293 [EXCERPT]
18294 [FOLDED]
18295 [EXCERPT]
18296 a1
18297 b1
18298 [EXCERPT]
18299 [FOLDED]
18300 [EXCERPT]
18301 ˇ[FOLDED]
18302 "
18303 });
18304 }
18305
18306 cx.simulate_keystroke("up");
18307 cx.assert_excerpts_with_selections(indoc! {"
18308 [EXCERPT]
18309 [FOLDED]
18310 [EXCERPT]
18311 a1
18312 b1
18313 [EXCERPT]
18314 ˇ[FOLDED]
18315 [EXCERPT]
18316 [FOLDED]
18317 "
18318 });
18319 cx.simulate_keystroke("up");
18320 cx.assert_excerpts_with_selections(indoc! {"
18321 [EXCERPT]
18322 [FOLDED]
18323 [EXCERPT]
18324 a1
18325 b1
18326 ˇ[EXCERPT]
18327 [FOLDED]
18328 [EXCERPT]
18329 [FOLDED]
18330 "
18331 });
18332 cx.simulate_keystroke("up");
18333 cx.assert_excerpts_with_selections(indoc! {"
18334 [EXCERPT]
18335 [FOLDED]
18336 [EXCERPT]
18337 a1
18338 ˇb1
18339 [EXCERPT]
18340 [FOLDED]
18341 [EXCERPT]
18342 [FOLDED]
18343 "
18344 });
18345 cx.simulate_keystroke("up");
18346 cx.assert_excerpts_with_selections(indoc! {"
18347 [EXCERPT]
18348 [FOLDED]
18349 [EXCERPT]
18350 ˇa1
18351 b1
18352 [EXCERPT]
18353 [FOLDED]
18354 [EXCERPT]
18355 [FOLDED]
18356 "
18357 });
18358 for _ in 0..5 {
18359 cx.simulate_keystroke("up");
18360 cx.assert_excerpts_with_selections(indoc! {"
18361 [EXCERPT]
18362 ˇ[FOLDED]
18363 [EXCERPT]
18364 a1
18365 b1
18366 [EXCERPT]
18367 [FOLDED]
18368 [EXCERPT]
18369 [FOLDED]
18370 "
18371 });
18372 }
18373}
18374
18375#[gpui::test]
18376async fn test_inline_completion_text(cx: &mut TestAppContext) {
18377 init_test(cx, |_| {});
18378
18379 // Simple insertion
18380 assert_highlighted_edits(
18381 "Hello, world!",
18382 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
18383 true,
18384 cx,
18385 |highlighted_edits, cx| {
18386 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
18387 assert_eq!(highlighted_edits.highlights.len(), 1);
18388 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
18389 assert_eq!(
18390 highlighted_edits.highlights[0].1.background_color,
18391 Some(cx.theme().status().created_background)
18392 );
18393 },
18394 )
18395 .await;
18396
18397 // Replacement
18398 assert_highlighted_edits(
18399 "This is a test.",
18400 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
18401 false,
18402 cx,
18403 |highlighted_edits, cx| {
18404 assert_eq!(highlighted_edits.text, "That is a test.");
18405 assert_eq!(highlighted_edits.highlights.len(), 1);
18406 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
18407 assert_eq!(
18408 highlighted_edits.highlights[0].1.background_color,
18409 Some(cx.theme().status().created_background)
18410 );
18411 },
18412 )
18413 .await;
18414
18415 // Multiple edits
18416 assert_highlighted_edits(
18417 "Hello, world!",
18418 vec![
18419 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
18420 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
18421 ],
18422 false,
18423 cx,
18424 |highlighted_edits, cx| {
18425 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
18426 assert_eq!(highlighted_edits.highlights.len(), 2);
18427 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
18428 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
18429 assert_eq!(
18430 highlighted_edits.highlights[0].1.background_color,
18431 Some(cx.theme().status().created_background)
18432 );
18433 assert_eq!(
18434 highlighted_edits.highlights[1].1.background_color,
18435 Some(cx.theme().status().created_background)
18436 );
18437 },
18438 )
18439 .await;
18440
18441 // Multiple lines with edits
18442 assert_highlighted_edits(
18443 "First line\nSecond line\nThird line\nFourth line",
18444 vec![
18445 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
18446 (
18447 Point::new(2, 0)..Point::new(2, 10),
18448 "New third line".to_string(),
18449 ),
18450 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
18451 ],
18452 false,
18453 cx,
18454 |highlighted_edits, cx| {
18455 assert_eq!(
18456 highlighted_edits.text,
18457 "Second modified\nNew third line\nFourth updated line"
18458 );
18459 assert_eq!(highlighted_edits.highlights.len(), 3);
18460 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
18461 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
18462 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
18463 for highlight in &highlighted_edits.highlights {
18464 assert_eq!(
18465 highlight.1.background_color,
18466 Some(cx.theme().status().created_background)
18467 );
18468 }
18469 },
18470 )
18471 .await;
18472}
18473
18474#[gpui::test]
18475async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
18476 init_test(cx, |_| {});
18477
18478 // Deletion
18479 assert_highlighted_edits(
18480 "Hello, world!",
18481 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
18482 true,
18483 cx,
18484 |highlighted_edits, cx| {
18485 assert_eq!(highlighted_edits.text, "Hello, world!");
18486 assert_eq!(highlighted_edits.highlights.len(), 1);
18487 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
18488 assert_eq!(
18489 highlighted_edits.highlights[0].1.background_color,
18490 Some(cx.theme().status().deleted_background)
18491 );
18492 },
18493 )
18494 .await;
18495
18496 // Insertion
18497 assert_highlighted_edits(
18498 "Hello, world!",
18499 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
18500 true,
18501 cx,
18502 |highlighted_edits, cx| {
18503 assert_eq!(highlighted_edits.highlights.len(), 1);
18504 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
18505 assert_eq!(
18506 highlighted_edits.highlights[0].1.background_color,
18507 Some(cx.theme().status().created_background)
18508 );
18509 },
18510 )
18511 .await;
18512}
18513
18514async fn assert_highlighted_edits(
18515 text: &str,
18516 edits: Vec<(Range<Point>, String)>,
18517 include_deletions: bool,
18518 cx: &mut TestAppContext,
18519 assertion_fn: impl Fn(HighlightedText, &App),
18520) {
18521 let window = cx.add_window(|window, cx| {
18522 let buffer = MultiBuffer::build_simple(text, cx);
18523 Editor::new(EditorMode::full(), buffer, None, window, cx)
18524 });
18525 let cx = &mut VisualTestContext::from_window(*window, cx);
18526
18527 let (buffer, snapshot) = window
18528 .update(cx, |editor, _window, cx| {
18529 (
18530 editor.buffer().clone(),
18531 editor.buffer().read(cx).snapshot(cx),
18532 )
18533 })
18534 .unwrap();
18535
18536 let edits = edits
18537 .into_iter()
18538 .map(|(range, edit)| {
18539 (
18540 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
18541 edit,
18542 )
18543 })
18544 .collect::<Vec<_>>();
18545
18546 let text_anchor_edits = edits
18547 .clone()
18548 .into_iter()
18549 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
18550 .collect::<Vec<_>>();
18551
18552 let edit_preview = window
18553 .update(cx, |_, _window, cx| {
18554 buffer
18555 .read(cx)
18556 .as_singleton()
18557 .unwrap()
18558 .read(cx)
18559 .preview_edits(text_anchor_edits.into(), cx)
18560 })
18561 .unwrap()
18562 .await;
18563
18564 cx.update(|_window, cx| {
18565 let highlighted_edits = inline_completion_edit_text(
18566 &snapshot.as_singleton().unwrap().2,
18567 &edits,
18568 &edit_preview,
18569 include_deletions,
18570 cx,
18571 );
18572 assertion_fn(highlighted_edits, cx)
18573 });
18574}
18575
18576#[track_caller]
18577fn assert_breakpoint(
18578 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
18579 path: &Arc<Path>,
18580 expected: Vec<(u32, Breakpoint)>,
18581) {
18582 if expected.len() == 0usize {
18583 assert!(!breakpoints.contains_key(path), "{}", path.display());
18584 } else {
18585 let mut breakpoint = breakpoints
18586 .get(path)
18587 .unwrap()
18588 .into_iter()
18589 .map(|breakpoint| {
18590 (
18591 breakpoint.row,
18592 Breakpoint {
18593 message: breakpoint.message.clone(),
18594 state: breakpoint.state,
18595 condition: breakpoint.condition.clone(),
18596 hit_condition: breakpoint.hit_condition.clone(),
18597 },
18598 )
18599 })
18600 .collect::<Vec<_>>();
18601
18602 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
18603
18604 assert_eq!(expected, breakpoint);
18605 }
18606}
18607
18608fn add_log_breakpoint_at_cursor(
18609 editor: &mut Editor,
18610 log_message: &str,
18611 window: &mut Window,
18612 cx: &mut Context<Editor>,
18613) {
18614 let (anchor, bp) = editor
18615 .breakpoints_at_cursors(window, cx)
18616 .first()
18617 .and_then(|(anchor, bp)| {
18618 if let Some(bp) = bp {
18619 Some((*anchor, bp.clone()))
18620 } else {
18621 None
18622 }
18623 })
18624 .unwrap_or_else(|| {
18625 let cursor_position: Point = editor.selections.newest(cx).head();
18626
18627 let breakpoint_position = editor
18628 .snapshot(window, cx)
18629 .display_snapshot
18630 .buffer_snapshot
18631 .anchor_before(Point::new(cursor_position.row, 0));
18632
18633 (breakpoint_position, Breakpoint::new_log(&log_message))
18634 });
18635
18636 editor.edit_breakpoint_at_anchor(
18637 anchor,
18638 bp,
18639 BreakpointEditAction::EditLogMessage(log_message.into()),
18640 cx,
18641 );
18642}
18643
18644#[gpui::test]
18645async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
18646 init_test(cx, |_| {});
18647
18648 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
18649 let fs = FakeFs::new(cx.executor());
18650 fs.insert_tree(
18651 path!("/a"),
18652 json!({
18653 "main.rs": sample_text,
18654 }),
18655 )
18656 .await;
18657 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18658 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18659 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18660
18661 let fs = FakeFs::new(cx.executor());
18662 fs.insert_tree(
18663 path!("/a"),
18664 json!({
18665 "main.rs": sample_text,
18666 }),
18667 )
18668 .await;
18669 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18670 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18671 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18672 let worktree_id = workspace
18673 .update(cx, |workspace, _window, cx| {
18674 workspace.project().update(cx, |project, cx| {
18675 project.worktrees(cx).next().unwrap().read(cx).id()
18676 })
18677 })
18678 .unwrap();
18679
18680 let buffer = project
18681 .update(cx, |project, cx| {
18682 project.open_buffer((worktree_id, "main.rs"), cx)
18683 })
18684 .await
18685 .unwrap();
18686
18687 let (editor, cx) = cx.add_window_view(|window, cx| {
18688 Editor::new(
18689 EditorMode::full(),
18690 MultiBuffer::build_from_buffer(buffer, cx),
18691 Some(project.clone()),
18692 window,
18693 cx,
18694 )
18695 });
18696
18697 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
18698 let abs_path = project.read_with(cx, |project, cx| {
18699 project
18700 .absolute_path(&project_path, cx)
18701 .map(|path_buf| Arc::from(path_buf.to_owned()))
18702 .unwrap()
18703 });
18704
18705 // assert we can add breakpoint on the first line
18706 editor.update_in(cx, |editor, window, cx| {
18707 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18708 editor.move_to_end(&MoveToEnd, window, cx);
18709 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18710 });
18711
18712 let breakpoints = editor.update(cx, |editor, cx| {
18713 editor
18714 .breakpoint_store()
18715 .as_ref()
18716 .unwrap()
18717 .read(cx)
18718 .all_breakpoints(cx)
18719 .clone()
18720 });
18721
18722 assert_eq!(1, breakpoints.len());
18723 assert_breakpoint(
18724 &breakpoints,
18725 &abs_path,
18726 vec![
18727 (0, Breakpoint::new_standard()),
18728 (3, Breakpoint::new_standard()),
18729 ],
18730 );
18731
18732 editor.update_in(cx, |editor, window, cx| {
18733 editor.move_to_beginning(&MoveToBeginning, window, cx);
18734 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18735 });
18736
18737 let breakpoints = editor.update(cx, |editor, cx| {
18738 editor
18739 .breakpoint_store()
18740 .as_ref()
18741 .unwrap()
18742 .read(cx)
18743 .all_breakpoints(cx)
18744 .clone()
18745 });
18746
18747 assert_eq!(1, breakpoints.len());
18748 assert_breakpoint(
18749 &breakpoints,
18750 &abs_path,
18751 vec![(3, Breakpoint::new_standard())],
18752 );
18753
18754 editor.update_in(cx, |editor, window, cx| {
18755 editor.move_to_end(&MoveToEnd, window, cx);
18756 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18757 });
18758
18759 let breakpoints = editor.update(cx, |editor, cx| {
18760 editor
18761 .breakpoint_store()
18762 .as_ref()
18763 .unwrap()
18764 .read(cx)
18765 .all_breakpoints(cx)
18766 .clone()
18767 });
18768
18769 assert_eq!(0, breakpoints.len());
18770 assert_breakpoint(&breakpoints, &abs_path, vec![]);
18771}
18772
18773#[gpui::test]
18774async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
18775 init_test(cx, |_| {});
18776
18777 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
18778
18779 let fs = FakeFs::new(cx.executor());
18780 fs.insert_tree(
18781 path!("/a"),
18782 json!({
18783 "main.rs": sample_text,
18784 }),
18785 )
18786 .await;
18787 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18788 let (workspace, cx) =
18789 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
18790
18791 let worktree_id = workspace.update(cx, |workspace, cx| {
18792 workspace.project().update(cx, |project, cx| {
18793 project.worktrees(cx).next().unwrap().read(cx).id()
18794 })
18795 });
18796
18797 let buffer = project
18798 .update(cx, |project, cx| {
18799 project.open_buffer((worktree_id, "main.rs"), cx)
18800 })
18801 .await
18802 .unwrap();
18803
18804 let (editor, cx) = cx.add_window_view(|window, cx| {
18805 Editor::new(
18806 EditorMode::full(),
18807 MultiBuffer::build_from_buffer(buffer, cx),
18808 Some(project.clone()),
18809 window,
18810 cx,
18811 )
18812 });
18813
18814 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
18815 let abs_path = project.read_with(cx, |project, cx| {
18816 project
18817 .absolute_path(&project_path, cx)
18818 .map(|path_buf| Arc::from(path_buf.to_owned()))
18819 .unwrap()
18820 });
18821
18822 editor.update_in(cx, |editor, window, cx| {
18823 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
18824 });
18825
18826 let breakpoints = editor.update(cx, |editor, cx| {
18827 editor
18828 .breakpoint_store()
18829 .as_ref()
18830 .unwrap()
18831 .read(cx)
18832 .all_breakpoints(cx)
18833 .clone()
18834 });
18835
18836 assert_breakpoint(
18837 &breakpoints,
18838 &abs_path,
18839 vec![(0, Breakpoint::new_log("hello world"))],
18840 );
18841
18842 // Removing a log message from a log breakpoint should remove it
18843 editor.update_in(cx, |editor, window, cx| {
18844 add_log_breakpoint_at_cursor(editor, "", window, cx);
18845 });
18846
18847 let breakpoints = editor.update(cx, |editor, cx| {
18848 editor
18849 .breakpoint_store()
18850 .as_ref()
18851 .unwrap()
18852 .read(cx)
18853 .all_breakpoints(cx)
18854 .clone()
18855 });
18856
18857 assert_breakpoint(&breakpoints, &abs_path, vec![]);
18858
18859 editor.update_in(cx, |editor, window, cx| {
18860 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18861 editor.move_to_end(&MoveToEnd, window, cx);
18862 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18863 // Not adding a log message to a standard breakpoint shouldn't remove it
18864 add_log_breakpoint_at_cursor(editor, "", window, cx);
18865 });
18866
18867 let breakpoints = editor.update(cx, |editor, cx| {
18868 editor
18869 .breakpoint_store()
18870 .as_ref()
18871 .unwrap()
18872 .read(cx)
18873 .all_breakpoints(cx)
18874 .clone()
18875 });
18876
18877 assert_breakpoint(
18878 &breakpoints,
18879 &abs_path,
18880 vec![
18881 (0, Breakpoint::new_standard()),
18882 (3, Breakpoint::new_standard()),
18883 ],
18884 );
18885
18886 editor.update_in(cx, |editor, window, cx| {
18887 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
18888 });
18889
18890 let breakpoints = editor.update(cx, |editor, cx| {
18891 editor
18892 .breakpoint_store()
18893 .as_ref()
18894 .unwrap()
18895 .read(cx)
18896 .all_breakpoints(cx)
18897 .clone()
18898 });
18899
18900 assert_breakpoint(
18901 &breakpoints,
18902 &abs_path,
18903 vec![
18904 (0, Breakpoint::new_standard()),
18905 (3, Breakpoint::new_log("hello world")),
18906 ],
18907 );
18908
18909 editor.update_in(cx, |editor, window, cx| {
18910 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
18911 });
18912
18913 let breakpoints = editor.update(cx, |editor, cx| {
18914 editor
18915 .breakpoint_store()
18916 .as_ref()
18917 .unwrap()
18918 .read(cx)
18919 .all_breakpoints(cx)
18920 .clone()
18921 });
18922
18923 assert_breakpoint(
18924 &breakpoints,
18925 &abs_path,
18926 vec![
18927 (0, Breakpoint::new_standard()),
18928 (3, Breakpoint::new_log("hello Earth!!")),
18929 ],
18930 );
18931}
18932
18933/// This also tests that Editor::breakpoint_at_cursor_head is working properly
18934/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
18935/// or when breakpoints were placed out of order. This tests for a regression too
18936#[gpui::test]
18937async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
18938 init_test(cx, |_| {});
18939
18940 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
18941 let fs = FakeFs::new(cx.executor());
18942 fs.insert_tree(
18943 path!("/a"),
18944 json!({
18945 "main.rs": sample_text,
18946 }),
18947 )
18948 .await;
18949 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18950 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18951 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18952
18953 let fs = FakeFs::new(cx.executor());
18954 fs.insert_tree(
18955 path!("/a"),
18956 json!({
18957 "main.rs": sample_text,
18958 }),
18959 )
18960 .await;
18961 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18962 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18963 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18964 let worktree_id = workspace
18965 .update(cx, |workspace, _window, cx| {
18966 workspace.project().update(cx, |project, cx| {
18967 project.worktrees(cx).next().unwrap().read(cx).id()
18968 })
18969 })
18970 .unwrap();
18971
18972 let buffer = project
18973 .update(cx, |project, cx| {
18974 project.open_buffer((worktree_id, "main.rs"), cx)
18975 })
18976 .await
18977 .unwrap();
18978
18979 let (editor, cx) = cx.add_window_view(|window, cx| {
18980 Editor::new(
18981 EditorMode::full(),
18982 MultiBuffer::build_from_buffer(buffer, cx),
18983 Some(project.clone()),
18984 window,
18985 cx,
18986 )
18987 });
18988
18989 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
18990 let abs_path = project.read_with(cx, |project, cx| {
18991 project
18992 .absolute_path(&project_path, cx)
18993 .map(|path_buf| Arc::from(path_buf.to_owned()))
18994 .unwrap()
18995 });
18996
18997 // assert we can add breakpoint on the first line
18998 editor.update_in(cx, |editor, window, cx| {
18999 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19000 editor.move_to_end(&MoveToEnd, window, cx);
19001 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19002 editor.move_up(&MoveUp, window, cx);
19003 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19004 });
19005
19006 let breakpoints = editor.update(cx, |editor, cx| {
19007 editor
19008 .breakpoint_store()
19009 .as_ref()
19010 .unwrap()
19011 .read(cx)
19012 .all_breakpoints(cx)
19013 .clone()
19014 });
19015
19016 assert_eq!(1, breakpoints.len());
19017 assert_breakpoint(
19018 &breakpoints,
19019 &abs_path,
19020 vec![
19021 (0, Breakpoint::new_standard()),
19022 (2, Breakpoint::new_standard()),
19023 (3, Breakpoint::new_standard()),
19024 ],
19025 );
19026
19027 editor.update_in(cx, |editor, window, cx| {
19028 editor.move_to_beginning(&MoveToBeginning, window, cx);
19029 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
19030 editor.move_to_end(&MoveToEnd, window, cx);
19031 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
19032 // Disabling a breakpoint that doesn't exist should do nothing
19033 editor.move_up(&MoveUp, window, cx);
19034 editor.move_up(&MoveUp, window, cx);
19035 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
19036 });
19037
19038 let breakpoints = editor.update(cx, |editor, cx| {
19039 editor
19040 .breakpoint_store()
19041 .as_ref()
19042 .unwrap()
19043 .read(cx)
19044 .all_breakpoints(cx)
19045 .clone()
19046 });
19047
19048 let disable_breakpoint = {
19049 let mut bp = Breakpoint::new_standard();
19050 bp.state = BreakpointState::Disabled;
19051 bp
19052 };
19053
19054 assert_eq!(1, breakpoints.len());
19055 assert_breakpoint(
19056 &breakpoints,
19057 &abs_path,
19058 vec![
19059 (0, disable_breakpoint.clone()),
19060 (2, Breakpoint::new_standard()),
19061 (3, disable_breakpoint.clone()),
19062 ],
19063 );
19064
19065 editor.update_in(cx, |editor, window, cx| {
19066 editor.move_to_beginning(&MoveToBeginning, window, cx);
19067 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
19068 editor.move_to_end(&MoveToEnd, window, cx);
19069 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
19070 editor.move_up(&MoveUp, window, cx);
19071 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
19072 });
19073
19074 let breakpoints = editor.update(cx, |editor, cx| {
19075 editor
19076 .breakpoint_store()
19077 .as_ref()
19078 .unwrap()
19079 .read(cx)
19080 .all_breakpoints(cx)
19081 .clone()
19082 });
19083
19084 assert_eq!(1, breakpoints.len());
19085 assert_breakpoint(
19086 &breakpoints,
19087 &abs_path,
19088 vec![
19089 (0, Breakpoint::new_standard()),
19090 (2, disable_breakpoint),
19091 (3, Breakpoint::new_standard()),
19092 ],
19093 );
19094}
19095
19096#[gpui::test]
19097async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
19098 init_test(cx, |_| {});
19099 let capabilities = lsp::ServerCapabilities {
19100 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
19101 prepare_provider: Some(true),
19102 work_done_progress_options: Default::default(),
19103 })),
19104 ..Default::default()
19105 };
19106 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
19107
19108 cx.set_state(indoc! {"
19109 struct Fˇoo {}
19110 "});
19111
19112 cx.update_editor(|editor, _, cx| {
19113 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
19114 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
19115 editor.highlight_background::<DocumentHighlightRead>(
19116 &[highlight_range],
19117 |c| c.editor_document_highlight_read_background,
19118 cx,
19119 );
19120 });
19121
19122 let mut prepare_rename_handler = cx
19123 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
19124 move |_, _, _| async move {
19125 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
19126 start: lsp::Position {
19127 line: 0,
19128 character: 7,
19129 },
19130 end: lsp::Position {
19131 line: 0,
19132 character: 10,
19133 },
19134 })))
19135 },
19136 );
19137 let prepare_rename_task = cx
19138 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
19139 .expect("Prepare rename was not started");
19140 prepare_rename_handler.next().await.unwrap();
19141 prepare_rename_task.await.expect("Prepare rename failed");
19142
19143 let mut rename_handler =
19144 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
19145 let edit = lsp::TextEdit {
19146 range: lsp::Range {
19147 start: lsp::Position {
19148 line: 0,
19149 character: 7,
19150 },
19151 end: lsp::Position {
19152 line: 0,
19153 character: 10,
19154 },
19155 },
19156 new_text: "FooRenamed".to_string(),
19157 };
19158 Ok(Some(lsp::WorkspaceEdit::new(
19159 // Specify the same edit twice
19160 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
19161 )))
19162 });
19163 let rename_task = cx
19164 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
19165 .expect("Confirm rename was not started");
19166 rename_handler.next().await.unwrap();
19167 rename_task.await.expect("Confirm rename failed");
19168 cx.run_until_parked();
19169
19170 // Despite two edits, only one is actually applied as those are identical
19171 cx.assert_editor_state(indoc! {"
19172 struct FooRenamedˇ {}
19173 "});
19174}
19175
19176#[gpui::test]
19177async fn test_rename_without_prepare(cx: &mut TestAppContext) {
19178 init_test(cx, |_| {});
19179 // These capabilities indicate that the server does not support prepare rename.
19180 let capabilities = lsp::ServerCapabilities {
19181 rename_provider: Some(lsp::OneOf::Left(true)),
19182 ..Default::default()
19183 };
19184 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
19185
19186 cx.set_state(indoc! {"
19187 struct Fˇoo {}
19188 "});
19189
19190 cx.update_editor(|editor, _window, cx| {
19191 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
19192 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
19193 editor.highlight_background::<DocumentHighlightRead>(
19194 &[highlight_range],
19195 |c| c.editor_document_highlight_read_background,
19196 cx,
19197 );
19198 });
19199
19200 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
19201 .expect("Prepare rename was not started")
19202 .await
19203 .expect("Prepare rename failed");
19204
19205 let mut rename_handler =
19206 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
19207 let edit = lsp::TextEdit {
19208 range: lsp::Range {
19209 start: lsp::Position {
19210 line: 0,
19211 character: 7,
19212 },
19213 end: lsp::Position {
19214 line: 0,
19215 character: 10,
19216 },
19217 },
19218 new_text: "FooRenamed".to_string(),
19219 };
19220 Ok(Some(lsp::WorkspaceEdit::new(
19221 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
19222 )))
19223 });
19224 let rename_task = cx
19225 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
19226 .expect("Confirm rename was not started");
19227 rename_handler.next().await.unwrap();
19228 rename_task.await.expect("Confirm rename failed");
19229 cx.run_until_parked();
19230
19231 // Correct range is renamed, as `surrounding_word` is used to find it.
19232 cx.assert_editor_state(indoc! {"
19233 struct FooRenamedˇ {}
19234 "});
19235}
19236
19237#[gpui::test]
19238async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
19239 init_test(cx, |_| {});
19240 let mut cx = EditorTestContext::new(cx).await;
19241
19242 let language = Arc::new(
19243 Language::new(
19244 LanguageConfig::default(),
19245 Some(tree_sitter_html::LANGUAGE.into()),
19246 )
19247 .with_brackets_query(
19248 r#"
19249 ("<" @open "/>" @close)
19250 ("</" @open ">" @close)
19251 ("<" @open ">" @close)
19252 ("\"" @open "\"" @close)
19253 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
19254 "#,
19255 )
19256 .unwrap(),
19257 );
19258 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
19259
19260 cx.set_state(indoc! {"
19261 <span>ˇ</span>
19262 "});
19263 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
19264 cx.assert_editor_state(indoc! {"
19265 <span>
19266 ˇ
19267 </span>
19268 "});
19269
19270 cx.set_state(indoc! {"
19271 <span><span></span>ˇ</span>
19272 "});
19273 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
19274 cx.assert_editor_state(indoc! {"
19275 <span><span></span>
19276 ˇ</span>
19277 "});
19278
19279 cx.set_state(indoc! {"
19280 <span>ˇ
19281 </span>
19282 "});
19283 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
19284 cx.assert_editor_state(indoc! {"
19285 <span>
19286 ˇ
19287 </span>
19288 "});
19289}
19290
19291#[gpui::test(iterations = 10)]
19292async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
19293 init_test(cx, |_| {});
19294
19295 let fs = FakeFs::new(cx.executor());
19296 fs.insert_tree(
19297 path!("/dir"),
19298 json!({
19299 "a.ts": "a",
19300 }),
19301 )
19302 .await;
19303
19304 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
19305 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19306 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19307
19308 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
19309 language_registry.add(Arc::new(Language::new(
19310 LanguageConfig {
19311 name: "TypeScript".into(),
19312 matcher: LanguageMatcher {
19313 path_suffixes: vec!["ts".to_string()],
19314 ..Default::default()
19315 },
19316 ..Default::default()
19317 },
19318 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
19319 )));
19320 let mut fake_language_servers = language_registry.register_fake_lsp(
19321 "TypeScript",
19322 FakeLspAdapter {
19323 capabilities: lsp::ServerCapabilities {
19324 code_lens_provider: Some(lsp::CodeLensOptions {
19325 resolve_provider: Some(true),
19326 }),
19327 execute_command_provider: Some(lsp::ExecuteCommandOptions {
19328 commands: vec!["_the/command".to_string()],
19329 ..lsp::ExecuteCommandOptions::default()
19330 }),
19331 ..lsp::ServerCapabilities::default()
19332 },
19333 ..FakeLspAdapter::default()
19334 },
19335 );
19336
19337 let (buffer, _handle) = project
19338 .update(cx, |p, cx| {
19339 p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
19340 })
19341 .await
19342 .unwrap();
19343 cx.executor().run_until_parked();
19344
19345 let fake_server = fake_language_servers.next().await.unwrap();
19346
19347 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
19348 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
19349 drop(buffer_snapshot);
19350 let actions = cx
19351 .update_window(*workspace, |_, window, cx| {
19352 project.code_actions(&buffer, anchor..anchor, window, cx)
19353 })
19354 .unwrap();
19355
19356 fake_server
19357 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
19358 Ok(Some(vec![
19359 lsp::CodeLens {
19360 range: lsp::Range::default(),
19361 command: Some(lsp::Command {
19362 title: "Code lens command".to_owned(),
19363 command: "_the/command".to_owned(),
19364 arguments: None,
19365 }),
19366 data: None,
19367 },
19368 lsp::CodeLens {
19369 range: lsp::Range::default(),
19370 command: Some(lsp::Command {
19371 title: "Command not in capabilities".to_owned(),
19372 command: "not in capabilities".to_owned(),
19373 arguments: None,
19374 }),
19375 data: None,
19376 },
19377 lsp::CodeLens {
19378 range: lsp::Range {
19379 start: lsp::Position {
19380 line: 1,
19381 character: 1,
19382 },
19383 end: lsp::Position {
19384 line: 1,
19385 character: 1,
19386 },
19387 },
19388 command: Some(lsp::Command {
19389 title: "Command not in range".to_owned(),
19390 command: "_the/command".to_owned(),
19391 arguments: None,
19392 }),
19393 data: None,
19394 },
19395 ]))
19396 })
19397 .next()
19398 .await;
19399
19400 let actions = actions.await.unwrap();
19401 assert_eq!(
19402 actions.len(),
19403 1,
19404 "Should have only one valid action for the 0..0 range"
19405 );
19406 let action = actions[0].clone();
19407 let apply = project.update(cx, |project, cx| {
19408 project.apply_code_action(buffer.clone(), action, true, cx)
19409 });
19410
19411 // Resolving the code action does not populate its edits. In absence of
19412 // edits, we must execute the given command.
19413 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
19414 |mut lens, _| async move {
19415 let lens_command = lens.command.as_mut().expect("should have a command");
19416 assert_eq!(lens_command.title, "Code lens command");
19417 lens_command.arguments = Some(vec![json!("the-argument")]);
19418 Ok(lens)
19419 },
19420 );
19421
19422 // While executing the command, the language server sends the editor
19423 // a `workspaceEdit` request.
19424 fake_server
19425 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
19426 let fake = fake_server.clone();
19427 move |params, _| {
19428 assert_eq!(params.command, "_the/command");
19429 let fake = fake.clone();
19430 async move {
19431 fake.server
19432 .request::<lsp::request::ApplyWorkspaceEdit>(
19433 lsp::ApplyWorkspaceEditParams {
19434 label: None,
19435 edit: lsp::WorkspaceEdit {
19436 changes: Some(
19437 [(
19438 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
19439 vec![lsp::TextEdit {
19440 range: lsp::Range::new(
19441 lsp::Position::new(0, 0),
19442 lsp::Position::new(0, 0),
19443 ),
19444 new_text: "X".into(),
19445 }],
19446 )]
19447 .into_iter()
19448 .collect(),
19449 ),
19450 ..Default::default()
19451 },
19452 },
19453 )
19454 .await
19455 .into_response()
19456 .unwrap();
19457 Ok(Some(json!(null)))
19458 }
19459 }
19460 })
19461 .next()
19462 .await;
19463
19464 // Applying the code lens command returns a project transaction containing the edits
19465 // sent by the language server in its `workspaceEdit` request.
19466 let transaction = apply.await.unwrap();
19467 assert!(transaction.0.contains_key(&buffer));
19468 buffer.update(cx, |buffer, cx| {
19469 assert_eq!(buffer.text(), "Xa");
19470 buffer.undo(cx);
19471 assert_eq!(buffer.text(), "a");
19472 });
19473}
19474
19475#[gpui::test]
19476async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
19477 init_test(cx, |_| {});
19478
19479 let fs = FakeFs::new(cx.executor());
19480 let main_text = r#"fn main() {
19481println!("1");
19482println!("2");
19483println!("3");
19484println!("4");
19485println!("5");
19486}"#;
19487 let lib_text = "mod foo {}";
19488 fs.insert_tree(
19489 path!("/a"),
19490 json!({
19491 "lib.rs": lib_text,
19492 "main.rs": main_text,
19493 }),
19494 )
19495 .await;
19496
19497 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19498 let (workspace, cx) =
19499 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
19500 let worktree_id = workspace.update(cx, |workspace, cx| {
19501 workspace.project().update(cx, |project, cx| {
19502 project.worktrees(cx).next().unwrap().read(cx).id()
19503 })
19504 });
19505
19506 let expected_ranges = vec![
19507 Point::new(0, 0)..Point::new(0, 0),
19508 Point::new(1, 0)..Point::new(1, 1),
19509 Point::new(2, 0)..Point::new(2, 2),
19510 Point::new(3, 0)..Point::new(3, 3),
19511 ];
19512
19513 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
19514 let editor_1 = workspace
19515 .update_in(cx, |workspace, window, cx| {
19516 workspace.open_path(
19517 (worktree_id, "main.rs"),
19518 Some(pane_1.downgrade()),
19519 true,
19520 window,
19521 cx,
19522 )
19523 })
19524 .unwrap()
19525 .await
19526 .downcast::<Editor>()
19527 .unwrap();
19528 pane_1.update(cx, |pane, cx| {
19529 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19530 open_editor.update(cx, |editor, cx| {
19531 assert_eq!(
19532 editor.display_text(cx),
19533 main_text,
19534 "Original main.rs text on initial open",
19535 );
19536 assert_eq!(
19537 editor
19538 .selections
19539 .all::<Point>(cx)
19540 .into_iter()
19541 .map(|s| s.range())
19542 .collect::<Vec<_>>(),
19543 vec![Point::zero()..Point::zero()],
19544 "Default selections on initial open",
19545 );
19546 })
19547 });
19548 editor_1.update_in(cx, |editor, window, cx| {
19549 editor.change_selections(None, window, cx, |s| {
19550 s.select_ranges(expected_ranges.clone());
19551 });
19552 });
19553
19554 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
19555 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
19556 });
19557 let editor_2 = workspace
19558 .update_in(cx, |workspace, window, cx| {
19559 workspace.open_path(
19560 (worktree_id, "main.rs"),
19561 Some(pane_2.downgrade()),
19562 true,
19563 window,
19564 cx,
19565 )
19566 })
19567 .unwrap()
19568 .await
19569 .downcast::<Editor>()
19570 .unwrap();
19571 pane_2.update(cx, |pane, cx| {
19572 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19573 open_editor.update(cx, |editor, cx| {
19574 assert_eq!(
19575 editor.display_text(cx),
19576 main_text,
19577 "Original main.rs text on initial open in another panel",
19578 );
19579 assert_eq!(
19580 editor
19581 .selections
19582 .all::<Point>(cx)
19583 .into_iter()
19584 .map(|s| s.range())
19585 .collect::<Vec<_>>(),
19586 vec![Point::zero()..Point::zero()],
19587 "Default selections on initial open in another panel",
19588 );
19589 })
19590 });
19591
19592 editor_2.update_in(cx, |editor, window, cx| {
19593 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
19594 });
19595
19596 let _other_editor_1 = workspace
19597 .update_in(cx, |workspace, window, cx| {
19598 workspace.open_path(
19599 (worktree_id, "lib.rs"),
19600 Some(pane_1.downgrade()),
19601 true,
19602 window,
19603 cx,
19604 )
19605 })
19606 .unwrap()
19607 .await
19608 .downcast::<Editor>()
19609 .unwrap();
19610 pane_1
19611 .update_in(cx, |pane, window, cx| {
19612 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
19613 .unwrap()
19614 })
19615 .await
19616 .unwrap();
19617 drop(editor_1);
19618 pane_1.update(cx, |pane, cx| {
19619 pane.active_item()
19620 .unwrap()
19621 .downcast::<Editor>()
19622 .unwrap()
19623 .update(cx, |editor, cx| {
19624 assert_eq!(
19625 editor.display_text(cx),
19626 lib_text,
19627 "Other file should be open and active",
19628 );
19629 });
19630 assert_eq!(pane.items().count(), 1, "No other editors should be open");
19631 });
19632
19633 let _other_editor_2 = workspace
19634 .update_in(cx, |workspace, window, cx| {
19635 workspace.open_path(
19636 (worktree_id, "lib.rs"),
19637 Some(pane_2.downgrade()),
19638 true,
19639 window,
19640 cx,
19641 )
19642 })
19643 .unwrap()
19644 .await
19645 .downcast::<Editor>()
19646 .unwrap();
19647 pane_2
19648 .update_in(cx, |pane, window, cx| {
19649 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
19650 .unwrap()
19651 })
19652 .await
19653 .unwrap();
19654 drop(editor_2);
19655 pane_2.update(cx, |pane, cx| {
19656 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19657 open_editor.update(cx, |editor, cx| {
19658 assert_eq!(
19659 editor.display_text(cx),
19660 lib_text,
19661 "Other file should be open and active in another panel too",
19662 );
19663 });
19664 assert_eq!(
19665 pane.items().count(),
19666 1,
19667 "No other editors should be open in another pane",
19668 );
19669 });
19670
19671 let _editor_1_reopened = workspace
19672 .update_in(cx, |workspace, window, cx| {
19673 workspace.open_path(
19674 (worktree_id, "main.rs"),
19675 Some(pane_1.downgrade()),
19676 true,
19677 window,
19678 cx,
19679 )
19680 })
19681 .unwrap()
19682 .await
19683 .downcast::<Editor>()
19684 .unwrap();
19685 let _editor_2_reopened = workspace
19686 .update_in(cx, |workspace, window, cx| {
19687 workspace.open_path(
19688 (worktree_id, "main.rs"),
19689 Some(pane_2.downgrade()),
19690 true,
19691 window,
19692 cx,
19693 )
19694 })
19695 .unwrap()
19696 .await
19697 .downcast::<Editor>()
19698 .unwrap();
19699 pane_1.update(cx, |pane, cx| {
19700 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19701 open_editor.update(cx, |editor, cx| {
19702 assert_eq!(
19703 editor.display_text(cx),
19704 main_text,
19705 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
19706 );
19707 assert_eq!(
19708 editor
19709 .selections
19710 .all::<Point>(cx)
19711 .into_iter()
19712 .map(|s| s.range())
19713 .collect::<Vec<_>>(),
19714 expected_ranges,
19715 "Previous editor in the 1st panel had selections and should get them restored on reopen",
19716 );
19717 })
19718 });
19719 pane_2.update(cx, |pane, cx| {
19720 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19721 open_editor.update(cx, |editor, cx| {
19722 assert_eq!(
19723 editor.display_text(cx),
19724 r#"fn main() {
19725⋯rintln!("1");
19726⋯intln!("2");
19727⋯ntln!("3");
19728println!("4");
19729println!("5");
19730}"#,
19731 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
19732 );
19733 assert_eq!(
19734 editor
19735 .selections
19736 .all::<Point>(cx)
19737 .into_iter()
19738 .map(|s| s.range())
19739 .collect::<Vec<_>>(),
19740 vec![Point::zero()..Point::zero()],
19741 "Previous editor in the 2nd pane had no selections changed hence should restore none",
19742 );
19743 })
19744 });
19745}
19746
19747#[gpui::test]
19748async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
19749 init_test(cx, |_| {});
19750
19751 let fs = FakeFs::new(cx.executor());
19752 let main_text = r#"fn main() {
19753println!("1");
19754println!("2");
19755println!("3");
19756println!("4");
19757println!("5");
19758}"#;
19759 let lib_text = "mod foo {}";
19760 fs.insert_tree(
19761 path!("/a"),
19762 json!({
19763 "lib.rs": lib_text,
19764 "main.rs": main_text,
19765 }),
19766 )
19767 .await;
19768
19769 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19770 let (workspace, cx) =
19771 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
19772 let worktree_id = workspace.update(cx, |workspace, cx| {
19773 workspace.project().update(cx, |project, cx| {
19774 project.worktrees(cx).next().unwrap().read(cx).id()
19775 })
19776 });
19777
19778 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
19779 let editor = workspace
19780 .update_in(cx, |workspace, window, cx| {
19781 workspace.open_path(
19782 (worktree_id, "main.rs"),
19783 Some(pane.downgrade()),
19784 true,
19785 window,
19786 cx,
19787 )
19788 })
19789 .unwrap()
19790 .await
19791 .downcast::<Editor>()
19792 .unwrap();
19793 pane.update(cx, |pane, cx| {
19794 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19795 open_editor.update(cx, |editor, cx| {
19796 assert_eq!(
19797 editor.display_text(cx),
19798 main_text,
19799 "Original main.rs text on initial open",
19800 );
19801 })
19802 });
19803 editor.update_in(cx, |editor, window, cx| {
19804 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
19805 });
19806
19807 cx.update_global(|store: &mut SettingsStore, cx| {
19808 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
19809 s.restore_on_file_reopen = Some(false);
19810 });
19811 });
19812 editor.update_in(cx, |editor, window, cx| {
19813 editor.fold_ranges(
19814 vec![
19815 Point::new(1, 0)..Point::new(1, 1),
19816 Point::new(2, 0)..Point::new(2, 2),
19817 Point::new(3, 0)..Point::new(3, 3),
19818 ],
19819 false,
19820 window,
19821 cx,
19822 );
19823 });
19824 pane.update_in(cx, |pane, window, cx| {
19825 pane.close_all_items(&CloseAllItems::default(), window, cx)
19826 .unwrap()
19827 })
19828 .await
19829 .unwrap();
19830 pane.update(cx, |pane, _| {
19831 assert!(pane.active_item().is_none());
19832 });
19833 cx.update_global(|store: &mut SettingsStore, cx| {
19834 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
19835 s.restore_on_file_reopen = Some(true);
19836 });
19837 });
19838
19839 let _editor_reopened = workspace
19840 .update_in(cx, |workspace, window, cx| {
19841 workspace.open_path(
19842 (worktree_id, "main.rs"),
19843 Some(pane.downgrade()),
19844 true,
19845 window,
19846 cx,
19847 )
19848 })
19849 .unwrap()
19850 .await
19851 .downcast::<Editor>()
19852 .unwrap();
19853 pane.update(cx, |pane, cx| {
19854 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19855 open_editor.update(cx, |editor, cx| {
19856 assert_eq!(
19857 editor.display_text(cx),
19858 main_text,
19859 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
19860 );
19861 })
19862 });
19863}
19864
19865#[gpui::test]
19866async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
19867 struct EmptyModalView {
19868 focus_handle: gpui::FocusHandle,
19869 }
19870 impl EventEmitter<DismissEvent> for EmptyModalView {}
19871 impl Render for EmptyModalView {
19872 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
19873 div()
19874 }
19875 }
19876 impl Focusable for EmptyModalView {
19877 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
19878 self.focus_handle.clone()
19879 }
19880 }
19881 impl workspace::ModalView for EmptyModalView {}
19882 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
19883 EmptyModalView {
19884 focus_handle: cx.focus_handle(),
19885 }
19886 }
19887
19888 init_test(cx, |_| {});
19889
19890 let fs = FakeFs::new(cx.executor());
19891 let project = Project::test(fs, [], cx).await;
19892 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19893 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
19894 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19895 let editor = cx.new_window_entity(|window, cx| {
19896 Editor::new(
19897 EditorMode::full(),
19898 buffer,
19899 Some(project.clone()),
19900 window,
19901 cx,
19902 )
19903 });
19904 workspace
19905 .update(cx, |workspace, window, cx| {
19906 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
19907 })
19908 .unwrap();
19909 editor.update_in(cx, |editor, window, cx| {
19910 editor.open_context_menu(&OpenContextMenu, window, cx);
19911 assert!(editor.mouse_context_menu.is_some());
19912 });
19913 workspace
19914 .update(cx, |workspace, window, cx| {
19915 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
19916 })
19917 .unwrap();
19918 cx.read(|cx| {
19919 assert!(editor.read(cx).mouse_context_menu.is_none());
19920 });
19921}
19922
19923#[gpui::test]
19924async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
19925 init_test(cx, |_| {});
19926
19927 let fs = FakeFs::new(cx.executor());
19928 fs.insert_file(path!("/file.html"), Default::default())
19929 .await;
19930
19931 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
19932
19933 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
19934 let html_language = Arc::new(Language::new(
19935 LanguageConfig {
19936 name: "HTML".into(),
19937 matcher: LanguageMatcher {
19938 path_suffixes: vec!["html".to_string()],
19939 ..LanguageMatcher::default()
19940 },
19941 brackets: BracketPairConfig {
19942 pairs: vec![BracketPair {
19943 start: "<".into(),
19944 end: ">".into(),
19945 close: true,
19946 ..Default::default()
19947 }],
19948 ..Default::default()
19949 },
19950 ..Default::default()
19951 },
19952 Some(tree_sitter_html::LANGUAGE.into()),
19953 ));
19954 language_registry.add(html_language);
19955 let mut fake_servers = language_registry.register_fake_lsp(
19956 "HTML",
19957 FakeLspAdapter {
19958 capabilities: lsp::ServerCapabilities {
19959 completion_provider: Some(lsp::CompletionOptions {
19960 resolve_provider: Some(true),
19961 ..Default::default()
19962 }),
19963 ..Default::default()
19964 },
19965 ..Default::default()
19966 },
19967 );
19968
19969 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19970 let cx = &mut VisualTestContext::from_window(*workspace, cx);
19971
19972 let worktree_id = workspace
19973 .update(cx, |workspace, _window, cx| {
19974 workspace.project().update(cx, |project, cx| {
19975 project.worktrees(cx).next().unwrap().read(cx).id()
19976 })
19977 })
19978 .unwrap();
19979 project
19980 .update(cx, |project, cx| {
19981 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
19982 })
19983 .await
19984 .unwrap();
19985 let editor = workspace
19986 .update(cx, |workspace, window, cx| {
19987 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
19988 })
19989 .unwrap()
19990 .await
19991 .unwrap()
19992 .downcast::<Editor>()
19993 .unwrap();
19994
19995 let fake_server = fake_servers.next().await.unwrap();
19996 editor.update_in(cx, |editor, window, cx| {
19997 editor.set_text("<ad></ad>", window, cx);
19998 editor.change_selections(None, window, cx, |selections| {
19999 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
20000 });
20001 let Some((buffer, _)) = editor
20002 .buffer
20003 .read(cx)
20004 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
20005 else {
20006 panic!("Failed to get buffer for selection position");
20007 };
20008 let buffer = buffer.read(cx);
20009 let buffer_id = buffer.remote_id();
20010 let opening_range =
20011 buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3));
20012 let closing_range =
20013 buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
20014 let mut linked_ranges = HashMap::default();
20015 linked_ranges.insert(
20016 buffer_id,
20017 vec![(opening_range.clone(), vec![closing_range.clone()])],
20018 );
20019 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
20020 });
20021 let mut completion_handle =
20022 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
20023 Ok(Some(lsp::CompletionResponse::Array(vec![
20024 lsp::CompletionItem {
20025 label: "head".to_string(),
20026 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
20027 lsp::InsertReplaceEdit {
20028 new_text: "head".to_string(),
20029 insert: lsp::Range::new(
20030 lsp::Position::new(0, 1),
20031 lsp::Position::new(0, 3),
20032 ),
20033 replace: lsp::Range::new(
20034 lsp::Position::new(0, 1),
20035 lsp::Position::new(0, 3),
20036 ),
20037 },
20038 )),
20039 ..Default::default()
20040 },
20041 ])))
20042 });
20043 editor.update_in(cx, |editor, window, cx| {
20044 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
20045 });
20046 cx.run_until_parked();
20047 completion_handle.next().await.unwrap();
20048 editor.update(cx, |editor, _| {
20049 assert!(
20050 editor.context_menu_visible(),
20051 "Completion menu should be visible"
20052 );
20053 });
20054 editor.update_in(cx, |editor, window, cx| {
20055 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
20056 });
20057 cx.executor().run_until_parked();
20058 editor.update(cx, |editor, cx| {
20059 assert_eq!(editor.text(cx), "<head></head>");
20060 });
20061}
20062
20063#[gpui::test]
20064async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
20065 init_test(cx, |_| {});
20066
20067 let fs = FakeFs::new(cx.executor());
20068 fs.insert_tree(
20069 path!("/root"),
20070 json!({
20071 "a": {
20072 "main.rs": "fn main() {}",
20073 },
20074 "foo": {
20075 "bar": {
20076 "external_file.rs": "pub mod external {}",
20077 }
20078 }
20079 }),
20080 )
20081 .await;
20082
20083 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
20084 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
20085 language_registry.add(rust_lang());
20086 let _fake_servers = language_registry.register_fake_lsp(
20087 "Rust",
20088 FakeLspAdapter {
20089 ..FakeLspAdapter::default()
20090 },
20091 );
20092 let (workspace, cx) =
20093 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20094 let worktree_id = workspace.update(cx, |workspace, cx| {
20095 workspace.project().update(cx, |project, cx| {
20096 project.worktrees(cx).next().unwrap().read(cx).id()
20097 })
20098 });
20099
20100 let assert_language_servers_count =
20101 |expected: usize, context: &str, cx: &mut VisualTestContext| {
20102 project.update(cx, |project, cx| {
20103 let current = project
20104 .lsp_store()
20105 .read(cx)
20106 .as_local()
20107 .unwrap()
20108 .language_servers
20109 .len();
20110 assert_eq!(expected, current, "{context}");
20111 });
20112 };
20113
20114 assert_language_servers_count(
20115 0,
20116 "No servers should be running before any file is open",
20117 cx,
20118 );
20119 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
20120 let main_editor = workspace
20121 .update_in(cx, |workspace, window, cx| {
20122 workspace.open_path(
20123 (worktree_id, "main.rs"),
20124 Some(pane.downgrade()),
20125 true,
20126 window,
20127 cx,
20128 )
20129 })
20130 .unwrap()
20131 .await
20132 .downcast::<Editor>()
20133 .unwrap();
20134 pane.update(cx, |pane, cx| {
20135 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20136 open_editor.update(cx, |editor, cx| {
20137 assert_eq!(
20138 editor.display_text(cx),
20139 "fn main() {}",
20140 "Original main.rs text on initial open",
20141 );
20142 });
20143 assert_eq!(open_editor, main_editor);
20144 });
20145 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
20146
20147 let external_editor = workspace
20148 .update_in(cx, |workspace, window, cx| {
20149 workspace.open_abs_path(
20150 PathBuf::from("/root/foo/bar/external_file.rs"),
20151 OpenOptions::default(),
20152 window,
20153 cx,
20154 )
20155 })
20156 .await
20157 .expect("opening external file")
20158 .downcast::<Editor>()
20159 .expect("downcasted external file's open element to editor");
20160 pane.update(cx, |pane, cx| {
20161 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20162 open_editor.update(cx, |editor, cx| {
20163 assert_eq!(
20164 editor.display_text(cx),
20165 "pub mod external {}",
20166 "External file is open now",
20167 );
20168 });
20169 assert_eq!(open_editor, external_editor);
20170 });
20171 assert_language_servers_count(
20172 1,
20173 "Second, external, *.rs file should join the existing server",
20174 cx,
20175 );
20176
20177 pane.update_in(cx, |pane, window, cx| {
20178 pane.close_active_item(&CloseActiveItem::default(), window, cx)
20179 })
20180 .unwrap()
20181 .await
20182 .unwrap();
20183 pane.update_in(cx, |pane, window, cx| {
20184 pane.navigate_backward(window, cx);
20185 });
20186 cx.run_until_parked();
20187 pane.update(cx, |pane, cx| {
20188 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20189 open_editor.update(cx, |editor, cx| {
20190 assert_eq!(
20191 editor.display_text(cx),
20192 "pub mod external {}",
20193 "External file is open now",
20194 );
20195 });
20196 });
20197 assert_language_servers_count(
20198 1,
20199 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
20200 cx,
20201 );
20202
20203 cx.update(|_, cx| {
20204 workspace::reload(&workspace::Reload::default(), cx);
20205 });
20206 assert_language_servers_count(
20207 1,
20208 "After reloading the worktree with local and external files opened, only one project should be started",
20209 cx,
20210 );
20211}
20212
20213fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
20214 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
20215 point..point
20216}
20217
20218fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
20219 let (text, ranges) = marked_text_ranges(marked_text, true);
20220 assert_eq!(editor.text(cx), text);
20221 assert_eq!(
20222 editor.selections.ranges(cx),
20223 ranges,
20224 "Assert selections are {}",
20225 marked_text
20226 );
20227}
20228
20229pub fn handle_signature_help_request(
20230 cx: &mut EditorLspTestContext,
20231 mocked_response: lsp::SignatureHelp,
20232) -> impl Future<Output = ()> + use<> {
20233 let mut request =
20234 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
20235 let mocked_response = mocked_response.clone();
20236 async move { Ok(Some(mocked_response)) }
20237 });
20238
20239 async move {
20240 request.next().await;
20241 }
20242}
20243
20244/// Handle completion request passing a marked string specifying where the completion
20245/// should be triggered from using '|' character, what range should be replaced, and what completions
20246/// should be returned using '<' and '>' to delimit the range.
20247///
20248/// Also see `handle_completion_request_with_insert_and_replace`.
20249#[track_caller]
20250pub fn handle_completion_request(
20251 cx: &mut EditorLspTestContext,
20252 marked_string: &str,
20253 completions: Vec<&'static str>,
20254 counter: Arc<AtomicUsize>,
20255) -> impl Future<Output = ()> {
20256 let complete_from_marker: TextRangeMarker = '|'.into();
20257 let replace_range_marker: TextRangeMarker = ('<', '>').into();
20258 let (_, mut marked_ranges) = marked_text_ranges_by(
20259 marked_string,
20260 vec![complete_from_marker.clone(), replace_range_marker.clone()],
20261 );
20262
20263 let complete_from_position =
20264 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
20265 let replace_range =
20266 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
20267
20268 let mut request =
20269 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
20270 let completions = completions.clone();
20271 counter.fetch_add(1, atomic::Ordering::Release);
20272 async move {
20273 assert_eq!(params.text_document_position.text_document.uri, url.clone());
20274 assert_eq!(
20275 params.text_document_position.position,
20276 complete_from_position
20277 );
20278 Ok(Some(lsp::CompletionResponse::Array(
20279 completions
20280 .iter()
20281 .map(|completion_text| lsp::CompletionItem {
20282 label: completion_text.to_string(),
20283 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
20284 range: replace_range,
20285 new_text: completion_text.to_string(),
20286 })),
20287 ..Default::default()
20288 })
20289 .collect(),
20290 )))
20291 }
20292 });
20293
20294 async move {
20295 request.next().await;
20296 }
20297}
20298
20299/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
20300/// given instead, which also contains an `insert` range.
20301///
20302/// This function uses the cursor position to mimic what Rust-Analyzer provides as the `insert` range,
20303/// that is, `replace_range.start..cursor_pos`.
20304pub fn handle_completion_request_with_insert_and_replace(
20305 cx: &mut EditorLspTestContext,
20306 marked_string: &str,
20307 completions: Vec<&'static str>,
20308 counter: Arc<AtomicUsize>,
20309) -> impl Future<Output = ()> {
20310 let complete_from_marker: TextRangeMarker = '|'.into();
20311 let replace_range_marker: TextRangeMarker = ('<', '>').into();
20312 let (_, mut marked_ranges) = marked_text_ranges_by(
20313 marked_string,
20314 vec![complete_from_marker.clone(), replace_range_marker.clone()],
20315 );
20316
20317 let complete_from_position =
20318 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
20319 let replace_range =
20320 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
20321
20322 let mut request =
20323 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
20324 let completions = completions.clone();
20325 counter.fetch_add(1, atomic::Ordering::Release);
20326 async move {
20327 assert_eq!(params.text_document_position.text_document.uri, url.clone());
20328 assert_eq!(
20329 params.text_document_position.position, complete_from_position,
20330 "marker `|` position doesn't match",
20331 );
20332 Ok(Some(lsp::CompletionResponse::Array(
20333 completions
20334 .iter()
20335 .map(|completion_text| lsp::CompletionItem {
20336 label: completion_text.to_string(),
20337 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
20338 lsp::InsertReplaceEdit {
20339 insert: lsp::Range {
20340 start: replace_range.start,
20341 end: complete_from_position,
20342 },
20343 replace: replace_range,
20344 new_text: completion_text.to_string(),
20345 },
20346 )),
20347 ..Default::default()
20348 })
20349 .collect(),
20350 )))
20351 }
20352 });
20353
20354 async move {
20355 request.next().await;
20356 }
20357}
20358
20359fn handle_resolve_completion_request(
20360 cx: &mut EditorLspTestContext,
20361 edits: Option<Vec<(&'static str, &'static str)>>,
20362) -> impl Future<Output = ()> {
20363 let edits = edits.map(|edits| {
20364 edits
20365 .iter()
20366 .map(|(marked_string, new_text)| {
20367 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
20368 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
20369 lsp::TextEdit::new(replace_range, new_text.to_string())
20370 })
20371 .collect::<Vec<_>>()
20372 });
20373
20374 let mut request =
20375 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
20376 let edits = edits.clone();
20377 async move {
20378 Ok(lsp::CompletionItem {
20379 additional_text_edits: edits,
20380 ..Default::default()
20381 })
20382 }
20383 });
20384
20385 async move {
20386 request.next().await;
20387 }
20388}
20389
20390pub(crate) fn update_test_language_settings(
20391 cx: &mut TestAppContext,
20392 f: impl Fn(&mut AllLanguageSettingsContent),
20393) {
20394 cx.update(|cx| {
20395 SettingsStore::update_global(cx, |store, cx| {
20396 store.update_user_settings::<AllLanguageSettings>(cx, f);
20397 });
20398 });
20399}
20400
20401pub(crate) fn update_test_project_settings(
20402 cx: &mut TestAppContext,
20403 f: impl Fn(&mut ProjectSettings),
20404) {
20405 cx.update(|cx| {
20406 SettingsStore::update_global(cx, |store, cx| {
20407 store.update_user_settings::<ProjectSettings>(cx, f);
20408 });
20409 });
20410}
20411
20412pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
20413 cx.update(|cx| {
20414 assets::Assets.load_test_fonts(cx);
20415 let store = SettingsStore::test(cx);
20416 cx.set_global(store);
20417 theme::init(theme::LoadThemes::JustBase, cx);
20418 release_channel::init(SemanticVersion::default(), cx);
20419 client::init_settings(cx);
20420 language::init(cx);
20421 Project::init_settings(cx);
20422 workspace::init_settings(cx);
20423 crate::init(cx);
20424 });
20425
20426 update_test_language_settings(cx, f);
20427}
20428
20429#[track_caller]
20430fn assert_hunk_revert(
20431 not_reverted_text_with_selections: &str,
20432 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
20433 expected_reverted_text_with_selections: &str,
20434 base_text: &str,
20435 cx: &mut EditorLspTestContext,
20436) {
20437 cx.set_state(not_reverted_text_with_selections);
20438 cx.set_head_text(base_text);
20439 cx.executor().run_until_parked();
20440
20441 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
20442 let snapshot = editor.snapshot(window, cx);
20443 let reverted_hunk_statuses = snapshot
20444 .buffer_snapshot
20445 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
20446 .map(|hunk| hunk.status().kind)
20447 .collect::<Vec<_>>();
20448
20449 editor.git_restore(&Default::default(), window, cx);
20450 reverted_hunk_statuses
20451 });
20452 cx.executor().run_until_parked();
20453 cx.assert_editor_state(expected_reverted_text_with_selections);
20454 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
20455}