1use super::*;
2use crate::{
3 JoinLines,
4 linked_editing_ranges::LinkedEditingRanges,
5 scroll::scroll_amount::ScrollAmount,
6 test::{
7 assert_text_with_selections, build_editor,
8 editor_lsp_test_context::{EditorLspTestContext, git_commit_lang},
9 editor_test_context::EditorTestContext,
10 select_ranges,
11 },
12};
13use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
14use futures::StreamExt;
15use gpui::{
16 BackgroundExecutor, DismissEvent, SemanticVersion, TestAppContext, UpdateGlobal,
17 VisualTestContext, WindowBounds, WindowOptions, div,
18};
19use indoc::indoc;
20use language::{
21 BracketPairConfig,
22 Capability::ReadWrite,
23 FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher, LanguageName,
24 Override, Point,
25 language_settings::{
26 AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings,
27 LanguageSettingsContent, LspInsertMode, PrettierSettings,
28 },
29 tree_sitter_python,
30};
31use language_settings::{Formatter, FormatterList, IndentGuideSettings};
32use lsp::CompletionParams;
33use multi_buffer::{IndentGuide, PathKey};
34use parking_lot::Mutex;
35use pretty_assertions::{assert_eq, assert_ne};
36use project::{
37 FakeFs,
38 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
39 project_settings::{LspSettings, ProjectSettings},
40};
41use serde_json::{self, json};
42use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
43use std::{
44 iter,
45 sync::atomic::{self, AtomicUsize},
46};
47use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
48use text::ToPoint as _;
49use unindent::Unindent;
50use util::{
51 assert_set_eq, path,
52 test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
53 uri,
54};
55use workspace::{
56 CloseActiveItem, CloseAllItems, CloseInactiveItems, NavigationEntry, OpenOptions, ViewId,
57 item::{FollowEvent, FollowableItem, Item, ItemHandle},
58};
59
60#[gpui::test]
61fn test_edit_events(cx: &mut TestAppContext) {
62 init_test(cx, |_| {});
63
64 let buffer = cx.new(|cx| {
65 let mut buffer = language::Buffer::local("123456", cx);
66 buffer.set_group_interval(Duration::from_secs(1));
67 buffer
68 });
69
70 let events = Rc::new(RefCell::new(Vec::new()));
71 let editor1 = cx.add_window({
72 let events = events.clone();
73 |window, cx| {
74 let entity = cx.entity().clone();
75 cx.subscribe_in(
76 &entity,
77 window,
78 move |_, _, event: &EditorEvent, _, _| match event {
79 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
80 EditorEvent::BufferEdited => {
81 events.borrow_mut().push(("editor1", "buffer edited"))
82 }
83 _ => {}
84 },
85 )
86 .detach();
87 Editor::for_buffer(buffer.clone(), None, window, cx)
88 }
89 });
90
91 let editor2 = cx.add_window({
92 let events = events.clone();
93 |window, cx| {
94 cx.subscribe_in(
95 &cx.entity().clone(),
96 window,
97 move |_, _, event: &EditorEvent, _, _| match event {
98 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
99 EditorEvent::BufferEdited => {
100 events.borrow_mut().push(("editor2", "buffer edited"))
101 }
102 _ => {}
103 },
104 )
105 .detach();
106 Editor::for_buffer(buffer.clone(), None, window, cx)
107 }
108 });
109
110 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
111
112 // Mutating editor 1 will emit an `Edited` event only for that editor.
113 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
114 assert_eq!(
115 mem::take(&mut *events.borrow_mut()),
116 [
117 ("editor1", "edited"),
118 ("editor1", "buffer edited"),
119 ("editor2", "buffer edited"),
120 ]
121 );
122
123 // Mutating editor 2 will emit an `Edited` event only for that editor.
124 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
125 assert_eq!(
126 mem::take(&mut *events.borrow_mut()),
127 [
128 ("editor2", "edited"),
129 ("editor1", "buffer edited"),
130 ("editor2", "buffer edited"),
131 ]
132 );
133
134 // Undoing on editor 1 will emit an `Edited` event only for that editor.
135 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
136 assert_eq!(
137 mem::take(&mut *events.borrow_mut()),
138 [
139 ("editor1", "edited"),
140 ("editor1", "buffer edited"),
141 ("editor2", "buffer edited"),
142 ]
143 );
144
145 // Redoing on editor 1 will emit an `Edited` event only for that editor.
146 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
147 assert_eq!(
148 mem::take(&mut *events.borrow_mut()),
149 [
150 ("editor1", "edited"),
151 ("editor1", "buffer edited"),
152 ("editor2", "buffer edited"),
153 ]
154 );
155
156 // Undoing on editor 2 will emit an `Edited` event only for that editor.
157 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
158 assert_eq!(
159 mem::take(&mut *events.borrow_mut()),
160 [
161 ("editor2", "edited"),
162 ("editor1", "buffer edited"),
163 ("editor2", "buffer edited"),
164 ]
165 );
166
167 // Redoing on editor 2 will emit an `Edited` event only for that editor.
168 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
169 assert_eq!(
170 mem::take(&mut *events.borrow_mut()),
171 [
172 ("editor2", "edited"),
173 ("editor1", "buffer edited"),
174 ("editor2", "buffer edited"),
175 ]
176 );
177
178 // No event is emitted when the mutation is a no-op.
179 _ = editor2.update(cx, |editor, window, cx| {
180 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
181
182 editor.backspace(&Backspace, window, cx);
183 });
184 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
185}
186
187#[gpui::test]
188fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
189 init_test(cx, |_| {});
190
191 let mut now = Instant::now();
192 let group_interval = Duration::from_millis(1);
193 let buffer = cx.new(|cx| {
194 let mut buf = language::Buffer::local("123456", cx);
195 buf.set_group_interval(group_interval);
196 buf
197 });
198 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
199 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
200
201 _ = editor.update(cx, |editor, window, cx| {
202 editor.start_transaction_at(now, window, cx);
203 editor.change_selections(None, window, cx, |s| s.select_ranges([2..4]));
204
205 editor.insert("cd", window, cx);
206 editor.end_transaction_at(now, cx);
207 assert_eq!(editor.text(cx), "12cd56");
208 assert_eq!(editor.selections.ranges(cx), vec![4..4]);
209
210 editor.start_transaction_at(now, window, cx);
211 editor.change_selections(None, window, cx, |s| s.select_ranges([4..5]));
212 editor.insert("e", window, cx);
213 editor.end_transaction_at(now, cx);
214 assert_eq!(editor.text(cx), "12cde6");
215 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
216
217 now += group_interval + Duration::from_millis(1);
218 editor.change_selections(None, window, cx, |s| s.select_ranges([2..2]));
219
220 // Simulate an edit in another editor
221 buffer.update(cx, |buffer, cx| {
222 buffer.start_transaction_at(now, cx);
223 buffer.edit([(0..1, "a")], None, cx);
224 buffer.edit([(1..1, "b")], None, cx);
225 buffer.end_transaction_at(now, cx);
226 });
227
228 assert_eq!(editor.text(cx), "ab2cde6");
229 assert_eq!(editor.selections.ranges(cx), vec![3..3]);
230
231 // Last transaction happened past the group interval in a different editor.
232 // Undo it individually and don't restore selections.
233 editor.undo(&Undo, window, cx);
234 assert_eq!(editor.text(cx), "12cde6");
235 assert_eq!(editor.selections.ranges(cx), vec![2..2]);
236
237 // First two transactions happened within the group interval in this editor.
238 // Undo them together and restore selections.
239 editor.undo(&Undo, window, cx);
240 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
241 assert_eq!(editor.text(cx), "123456");
242 assert_eq!(editor.selections.ranges(cx), vec![0..0]);
243
244 // Redo the first two transactions together.
245 editor.redo(&Redo, window, cx);
246 assert_eq!(editor.text(cx), "12cde6");
247 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
248
249 // Redo the last transaction on its own.
250 editor.redo(&Redo, window, cx);
251 assert_eq!(editor.text(cx), "ab2cde6");
252 assert_eq!(editor.selections.ranges(cx), vec![6..6]);
253
254 // Test empty transactions.
255 editor.start_transaction_at(now, window, cx);
256 editor.end_transaction_at(now, cx);
257 editor.undo(&Undo, window, cx);
258 assert_eq!(editor.text(cx), "12cde6");
259 });
260}
261
262#[gpui::test]
263fn test_ime_composition(cx: &mut TestAppContext) {
264 init_test(cx, |_| {});
265
266 let buffer = cx.new(|cx| {
267 let mut buffer = language::Buffer::local("abcde", cx);
268 // Ensure automatic grouping doesn't occur.
269 buffer.set_group_interval(Duration::ZERO);
270 buffer
271 });
272
273 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
274 cx.add_window(|window, cx| {
275 let mut editor = build_editor(buffer.clone(), window, cx);
276
277 // Start a new IME composition.
278 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
279 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
280 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
281 assert_eq!(editor.text(cx), "äbcde");
282 assert_eq!(
283 editor.marked_text_ranges(cx),
284 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
285 );
286
287 // Finalize IME composition.
288 editor.replace_text_in_range(None, "ā", window, cx);
289 assert_eq!(editor.text(cx), "ābcde");
290 assert_eq!(editor.marked_text_ranges(cx), None);
291
292 // IME composition edits are grouped and are undone/redone at once.
293 editor.undo(&Default::default(), window, cx);
294 assert_eq!(editor.text(cx), "abcde");
295 assert_eq!(editor.marked_text_ranges(cx), None);
296 editor.redo(&Default::default(), window, cx);
297 assert_eq!(editor.text(cx), "ābcde");
298 assert_eq!(editor.marked_text_ranges(cx), None);
299
300 // Start a new IME composition.
301 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
302 assert_eq!(
303 editor.marked_text_ranges(cx),
304 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
305 );
306
307 // Undoing during an IME composition cancels it.
308 editor.undo(&Default::default(), window, cx);
309 assert_eq!(editor.text(cx), "ābcde");
310 assert_eq!(editor.marked_text_ranges(cx), None);
311
312 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
313 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
314 assert_eq!(editor.text(cx), "ābcdè");
315 assert_eq!(
316 editor.marked_text_ranges(cx),
317 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
318 );
319
320 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
321 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
322 assert_eq!(editor.text(cx), "ābcdę");
323 assert_eq!(editor.marked_text_ranges(cx), None);
324
325 // Start a new IME composition with multiple cursors.
326 editor.change_selections(None, window, cx, |s| {
327 s.select_ranges([
328 OffsetUtf16(1)..OffsetUtf16(1),
329 OffsetUtf16(3)..OffsetUtf16(3),
330 OffsetUtf16(5)..OffsetUtf16(5),
331 ])
332 });
333 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
334 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
335 assert_eq!(
336 editor.marked_text_ranges(cx),
337 Some(vec![
338 OffsetUtf16(0)..OffsetUtf16(3),
339 OffsetUtf16(4)..OffsetUtf16(7),
340 OffsetUtf16(8)..OffsetUtf16(11)
341 ])
342 );
343
344 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
345 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
346 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
347 assert_eq!(
348 editor.marked_text_ranges(cx),
349 Some(vec![
350 OffsetUtf16(1)..OffsetUtf16(2),
351 OffsetUtf16(5)..OffsetUtf16(6),
352 OffsetUtf16(9)..OffsetUtf16(10)
353 ])
354 );
355
356 // Finalize IME composition with multiple cursors.
357 editor.replace_text_in_range(Some(9..10), "2", window, cx);
358 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
359 assert_eq!(editor.marked_text_ranges(cx), None);
360
361 editor
362 });
363}
364
365#[gpui::test]
366fn test_selection_with_mouse(cx: &mut TestAppContext) {
367 init_test(cx, |_| {});
368
369 let editor = cx.add_window(|window, cx| {
370 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
371 build_editor(buffer, window, cx)
372 });
373
374 _ = editor.update(cx, |editor, window, cx| {
375 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
376 });
377 assert_eq!(
378 editor
379 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
380 .unwrap(),
381 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
382 );
383
384 _ = editor.update(cx, |editor, window, cx| {
385 editor.update_selection(
386 DisplayPoint::new(DisplayRow(3), 3),
387 0,
388 gpui::Point::<f32>::default(),
389 window,
390 cx,
391 );
392 });
393
394 assert_eq!(
395 editor
396 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
397 .unwrap(),
398 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
399 );
400
401 _ = editor.update(cx, |editor, window, cx| {
402 editor.update_selection(
403 DisplayPoint::new(DisplayRow(1), 1),
404 0,
405 gpui::Point::<f32>::default(),
406 window,
407 cx,
408 );
409 });
410
411 assert_eq!(
412 editor
413 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
414 .unwrap(),
415 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
416 );
417
418 _ = editor.update(cx, |editor, window, cx| {
419 editor.end_selection(window, cx);
420 editor.update_selection(
421 DisplayPoint::new(DisplayRow(3), 3),
422 0,
423 gpui::Point::<f32>::default(),
424 window,
425 cx,
426 );
427 });
428
429 assert_eq!(
430 editor
431 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
432 .unwrap(),
433 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
434 );
435
436 _ = editor.update(cx, |editor, window, cx| {
437 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
438 editor.update_selection(
439 DisplayPoint::new(DisplayRow(0), 0),
440 0,
441 gpui::Point::<f32>::default(),
442 window,
443 cx,
444 );
445 });
446
447 assert_eq!(
448 editor
449 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
450 .unwrap(),
451 [
452 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
453 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
454 ]
455 );
456
457 _ = editor.update(cx, |editor, window, cx| {
458 editor.end_selection(window, cx);
459 });
460
461 assert_eq!(
462 editor
463 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
464 .unwrap(),
465 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
466 );
467}
468
469#[gpui::test]
470fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
471 init_test(cx, |_| {});
472
473 let editor = cx.add_window(|window, cx| {
474 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
475 build_editor(buffer, window, cx)
476 });
477
478 _ = editor.update(cx, |editor, window, cx| {
479 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
480 });
481
482 _ = editor.update(cx, |editor, window, cx| {
483 editor.end_selection(window, cx);
484 });
485
486 _ = editor.update(cx, |editor, window, cx| {
487 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
488 });
489
490 _ = editor.update(cx, |editor, window, cx| {
491 editor.end_selection(window, cx);
492 });
493
494 assert_eq!(
495 editor
496 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
497 .unwrap(),
498 [
499 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
500 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
501 ]
502 );
503
504 _ = editor.update(cx, |editor, window, cx| {
505 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
506 });
507
508 _ = editor.update(cx, |editor, window, cx| {
509 editor.end_selection(window, cx);
510 });
511
512 assert_eq!(
513 editor
514 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
515 .unwrap(),
516 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
517 );
518}
519
520#[gpui::test]
521fn test_canceling_pending_selection(cx: &mut TestAppContext) {
522 init_test(cx, |_| {});
523
524 let editor = cx.add_window(|window, cx| {
525 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
526 build_editor(buffer, window, cx)
527 });
528
529 _ = editor.update(cx, |editor, window, cx| {
530 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
531 assert_eq!(
532 editor.selections.display_ranges(cx),
533 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
534 );
535 });
536
537 _ = editor.update(cx, |editor, window, cx| {
538 editor.update_selection(
539 DisplayPoint::new(DisplayRow(3), 3),
540 0,
541 gpui::Point::<f32>::default(),
542 window,
543 cx,
544 );
545 assert_eq!(
546 editor.selections.display_ranges(cx),
547 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
548 );
549 });
550
551 _ = editor.update(cx, |editor, window, cx| {
552 editor.cancel(&Cancel, window, cx);
553 editor.update_selection(
554 DisplayPoint::new(DisplayRow(1), 1),
555 0,
556 gpui::Point::<f32>::default(),
557 window,
558 cx,
559 );
560 assert_eq!(
561 editor.selections.display_ranges(cx),
562 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
563 );
564 });
565}
566
567#[gpui::test]
568fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
569 init_test(cx, |_| {});
570
571 let editor = cx.add_window(|window, cx| {
572 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
573 build_editor(buffer, window, cx)
574 });
575
576 _ = editor.update(cx, |editor, window, cx| {
577 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
578 assert_eq!(
579 editor.selections.display_ranges(cx),
580 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
581 );
582
583 editor.move_down(&Default::default(), window, cx);
584 assert_eq!(
585 editor.selections.display_ranges(cx),
586 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
587 );
588
589 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
590 assert_eq!(
591 editor.selections.display_ranges(cx),
592 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
593 );
594
595 editor.move_up(&Default::default(), window, cx);
596 assert_eq!(
597 editor.selections.display_ranges(cx),
598 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
599 );
600 });
601}
602
603#[gpui::test]
604fn test_clone(cx: &mut TestAppContext) {
605 init_test(cx, |_| {});
606
607 let (text, selection_ranges) = marked_text_ranges(
608 indoc! {"
609 one
610 two
611 threeˇ
612 four
613 fiveˇ
614 "},
615 true,
616 );
617
618 let editor = cx.add_window(|window, cx| {
619 let buffer = MultiBuffer::build_simple(&text, cx);
620 build_editor(buffer, window, cx)
621 });
622
623 _ = editor.update(cx, |editor, window, cx| {
624 editor.change_selections(None, window, cx, |s| {
625 s.select_ranges(selection_ranges.clone())
626 });
627 editor.fold_creases(
628 vec![
629 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
630 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
631 ],
632 true,
633 window,
634 cx,
635 );
636 });
637
638 let cloned_editor = editor
639 .update(cx, |editor, _, cx| {
640 cx.open_window(Default::default(), |window, cx| {
641 cx.new(|cx| editor.clone(window, cx))
642 })
643 })
644 .unwrap()
645 .unwrap();
646
647 let snapshot = editor
648 .update(cx, |e, window, cx| e.snapshot(window, cx))
649 .unwrap();
650 let cloned_snapshot = cloned_editor
651 .update(cx, |e, window, cx| e.snapshot(window, cx))
652 .unwrap();
653
654 assert_eq!(
655 cloned_editor
656 .update(cx, |e, _, cx| e.display_text(cx))
657 .unwrap(),
658 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
659 );
660 assert_eq!(
661 cloned_snapshot
662 .folds_in_range(0..text.len())
663 .collect::<Vec<_>>(),
664 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
665 );
666 assert_set_eq!(
667 cloned_editor
668 .update(cx, |editor, _, cx| editor.selections.ranges::<Point>(cx))
669 .unwrap(),
670 editor
671 .update(cx, |editor, _, cx| editor.selections.ranges(cx))
672 .unwrap()
673 );
674 assert_set_eq!(
675 cloned_editor
676 .update(cx, |e, _window, cx| e.selections.display_ranges(cx))
677 .unwrap(),
678 editor
679 .update(cx, |e, _, cx| e.selections.display_ranges(cx))
680 .unwrap()
681 );
682}
683
684#[gpui::test]
685async fn test_navigation_history(cx: &mut TestAppContext) {
686 init_test(cx, |_| {});
687
688 use workspace::item::Item;
689
690 let fs = FakeFs::new(cx.executor());
691 let project = Project::test(fs, [], cx).await;
692 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
693 let pane = workspace
694 .update(cx, |workspace, _, _| workspace.active_pane().clone())
695 .unwrap();
696
697 _ = workspace.update(cx, |_v, window, cx| {
698 cx.new(|cx| {
699 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
700 let mut editor = build_editor(buffer.clone(), window, cx);
701 let handle = cx.entity();
702 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
703
704 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
705 editor.nav_history.as_mut().unwrap().pop_backward(cx)
706 }
707
708 // Move the cursor a small distance.
709 // Nothing is added to the navigation history.
710 editor.change_selections(None, window, cx, |s| {
711 s.select_display_ranges([
712 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
713 ])
714 });
715 editor.change_selections(None, window, cx, |s| {
716 s.select_display_ranges([
717 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
718 ])
719 });
720 assert!(pop_history(&mut editor, cx).is_none());
721
722 // Move the cursor a large distance.
723 // The history can jump back to the previous position.
724 editor.change_selections(None, window, cx, |s| {
725 s.select_display_ranges([
726 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
727 ])
728 });
729 let nav_entry = pop_history(&mut editor, cx).unwrap();
730 editor.navigate(nav_entry.data.unwrap(), window, cx);
731 assert_eq!(nav_entry.item.id(), cx.entity_id());
732 assert_eq!(
733 editor.selections.display_ranges(cx),
734 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
735 );
736 assert!(pop_history(&mut editor, cx).is_none());
737
738 // Move the cursor a small distance via the mouse.
739 // Nothing is added to the navigation history.
740 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
741 editor.end_selection(window, cx);
742 assert_eq!(
743 editor.selections.display_ranges(cx),
744 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
745 );
746 assert!(pop_history(&mut editor, cx).is_none());
747
748 // Move the cursor a large distance via the mouse.
749 // The history can jump back to the previous position.
750 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
751 editor.end_selection(window, cx);
752 assert_eq!(
753 editor.selections.display_ranges(cx),
754 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
755 );
756 let nav_entry = pop_history(&mut editor, cx).unwrap();
757 editor.navigate(nav_entry.data.unwrap(), window, cx);
758 assert_eq!(nav_entry.item.id(), cx.entity_id());
759 assert_eq!(
760 editor.selections.display_ranges(cx),
761 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
762 );
763 assert!(pop_history(&mut editor, cx).is_none());
764
765 // Set scroll position to check later
766 editor.set_scroll_position(gpui::Point::<f32>::new(5.5, 5.5), window, cx);
767 let original_scroll_position = editor.scroll_manager.anchor();
768
769 // Jump to the end of the document and adjust scroll
770 editor.move_to_end(&MoveToEnd, window, cx);
771 editor.set_scroll_position(gpui::Point::<f32>::new(-2.5, -0.5), window, cx);
772 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
773
774 let nav_entry = pop_history(&mut editor, cx).unwrap();
775 editor.navigate(nav_entry.data.unwrap(), window, cx);
776 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
777
778 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
779 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
780 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
781 let invalid_point = Point::new(9999, 0);
782 editor.navigate(
783 Box::new(NavigationData {
784 cursor_anchor: invalid_anchor,
785 cursor_position: invalid_point,
786 scroll_anchor: ScrollAnchor {
787 anchor: invalid_anchor,
788 offset: Default::default(),
789 },
790 scroll_top_row: invalid_point.row,
791 }),
792 window,
793 cx,
794 );
795 assert_eq!(
796 editor.selections.display_ranges(cx),
797 &[editor.max_point(cx)..editor.max_point(cx)]
798 );
799 assert_eq!(
800 editor.scroll_position(cx),
801 gpui::Point::new(0., editor.max_point(cx).row().as_f32())
802 );
803
804 editor
805 })
806 });
807}
808
809#[gpui::test]
810fn test_cancel(cx: &mut TestAppContext) {
811 init_test(cx, |_| {});
812
813 let editor = cx.add_window(|window, cx| {
814 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
815 build_editor(buffer, window, cx)
816 });
817
818 _ = editor.update(cx, |editor, window, cx| {
819 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
820 editor.update_selection(
821 DisplayPoint::new(DisplayRow(1), 1),
822 0,
823 gpui::Point::<f32>::default(),
824 window,
825 cx,
826 );
827 editor.end_selection(window, cx);
828
829 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
830 editor.update_selection(
831 DisplayPoint::new(DisplayRow(0), 3),
832 0,
833 gpui::Point::<f32>::default(),
834 window,
835 cx,
836 );
837 editor.end_selection(window, cx);
838 assert_eq!(
839 editor.selections.display_ranges(cx),
840 [
841 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
842 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
843 ]
844 );
845 });
846
847 _ = editor.update(cx, |editor, window, cx| {
848 editor.cancel(&Cancel, window, cx);
849 assert_eq!(
850 editor.selections.display_ranges(cx),
851 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
852 );
853 });
854
855 _ = editor.update(cx, |editor, window, cx| {
856 editor.cancel(&Cancel, window, cx);
857 assert_eq!(
858 editor.selections.display_ranges(cx),
859 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
860 );
861 });
862}
863
864#[gpui::test]
865fn test_fold_action(cx: &mut TestAppContext) {
866 init_test(cx, |_| {});
867
868 let editor = cx.add_window(|window, cx| {
869 let buffer = MultiBuffer::build_simple(
870 &"
871 impl Foo {
872 // Hello!
873
874 fn a() {
875 1
876 }
877
878 fn b() {
879 2
880 }
881
882 fn c() {
883 3
884 }
885 }
886 "
887 .unindent(),
888 cx,
889 );
890 build_editor(buffer.clone(), window, cx)
891 });
892
893 _ = editor.update(cx, |editor, window, cx| {
894 editor.change_selections(None, window, cx, |s| {
895 s.select_display_ranges([
896 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
897 ]);
898 });
899 editor.fold(&Fold, window, cx);
900 assert_eq!(
901 editor.display_text(cx),
902 "
903 impl Foo {
904 // Hello!
905
906 fn a() {
907 1
908 }
909
910 fn b() {⋯
911 }
912
913 fn c() {⋯
914 }
915 }
916 "
917 .unindent(),
918 );
919
920 editor.fold(&Fold, window, cx);
921 assert_eq!(
922 editor.display_text(cx),
923 "
924 impl Foo {⋯
925 }
926 "
927 .unindent(),
928 );
929
930 editor.unfold_lines(&UnfoldLines, window, cx);
931 assert_eq!(
932 editor.display_text(cx),
933 "
934 impl Foo {
935 // Hello!
936
937 fn a() {
938 1
939 }
940
941 fn b() {⋯
942 }
943
944 fn c() {⋯
945 }
946 }
947 "
948 .unindent(),
949 );
950
951 editor.unfold_lines(&UnfoldLines, window, cx);
952 assert_eq!(
953 editor.display_text(cx),
954 editor.buffer.read(cx).read(cx).text()
955 );
956 });
957}
958
959#[gpui::test]
960fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
961 init_test(cx, |_| {});
962
963 let editor = cx.add_window(|window, cx| {
964 let buffer = MultiBuffer::build_simple(
965 &"
966 class Foo:
967 # Hello!
968
969 def a():
970 print(1)
971
972 def b():
973 print(2)
974
975 def c():
976 print(3)
977 "
978 .unindent(),
979 cx,
980 );
981 build_editor(buffer.clone(), window, cx)
982 });
983
984 _ = editor.update(cx, |editor, window, cx| {
985 editor.change_selections(None, window, cx, |s| {
986 s.select_display_ranges([
987 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
988 ]);
989 });
990 editor.fold(&Fold, window, cx);
991 assert_eq!(
992 editor.display_text(cx),
993 "
994 class Foo:
995 # Hello!
996
997 def a():
998 print(1)
999
1000 def b():⋯
1001
1002 def c():⋯
1003 "
1004 .unindent(),
1005 );
1006
1007 editor.fold(&Fold, window, cx);
1008 assert_eq!(
1009 editor.display_text(cx),
1010 "
1011 class Foo:⋯
1012 "
1013 .unindent(),
1014 );
1015
1016 editor.unfold_lines(&UnfoldLines, window, cx);
1017 assert_eq!(
1018 editor.display_text(cx),
1019 "
1020 class Foo:
1021 # Hello!
1022
1023 def a():
1024 print(1)
1025
1026 def b():⋯
1027
1028 def c():⋯
1029 "
1030 .unindent(),
1031 );
1032
1033 editor.unfold_lines(&UnfoldLines, window, cx);
1034 assert_eq!(
1035 editor.display_text(cx),
1036 editor.buffer.read(cx).read(cx).text()
1037 );
1038 });
1039}
1040
1041#[gpui::test]
1042fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1043 init_test(cx, |_| {});
1044
1045 let editor = cx.add_window(|window, cx| {
1046 let buffer = MultiBuffer::build_simple(
1047 &"
1048 class Foo:
1049 # Hello!
1050
1051 def a():
1052 print(1)
1053
1054 def b():
1055 print(2)
1056
1057
1058 def c():
1059 print(3)
1060
1061
1062 "
1063 .unindent(),
1064 cx,
1065 );
1066 build_editor(buffer.clone(), window, cx)
1067 });
1068
1069 _ = editor.update(cx, |editor, window, cx| {
1070 editor.change_selections(None, window, cx, |s| {
1071 s.select_display_ranges([
1072 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1073 ]);
1074 });
1075 editor.fold(&Fold, window, cx);
1076 assert_eq!(
1077 editor.display_text(cx),
1078 "
1079 class Foo:
1080 # Hello!
1081
1082 def a():
1083 print(1)
1084
1085 def b():⋯
1086
1087
1088 def c():⋯
1089
1090
1091 "
1092 .unindent(),
1093 );
1094
1095 editor.fold(&Fold, window, cx);
1096 assert_eq!(
1097 editor.display_text(cx),
1098 "
1099 class Foo:⋯
1100
1101
1102 "
1103 .unindent(),
1104 );
1105
1106 editor.unfold_lines(&UnfoldLines, window, cx);
1107 assert_eq!(
1108 editor.display_text(cx),
1109 "
1110 class Foo:
1111 # Hello!
1112
1113 def a():
1114 print(1)
1115
1116 def b():⋯
1117
1118
1119 def c():⋯
1120
1121
1122 "
1123 .unindent(),
1124 );
1125
1126 editor.unfold_lines(&UnfoldLines, window, cx);
1127 assert_eq!(
1128 editor.display_text(cx),
1129 editor.buffer.read(cx).read(cx).text()
1130 );
1131 });
1132}
1133
1134#[gpui::test]
1135fn test_fold_at_level(cx: &mut TestAppContext) {
1136 init_test(cx, |_| {});
1137
1138 let editor = cx.add_window(|window, cx| {
1139 let buffer = MultiBuffer::build_simple(
1140 &"
1141 class Foo:
1142 # Hello!
1143
1144 def a():
1145 print(1)
1146
1147 def b():
1148 print(2)
1149
1150
1151 class Bar:
1152 # World!
1153
1154 def a():
1155 print(1)
1156
1157 def b():
1158 print(2)
1159
1160
1161 "
1162 .unindent(),
1163 cx,
1164 );
1165 build_editor(buffer.clone(), window, cx)
1166 });
1167
1168 _ = editor.update(cx, |editor, window, cx| {
1169 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1170 assert_eq!(
1171 editor.display_text(cx),
1172 "
1173 class Foo:
1174 # Hello!
1175
1176 def a():⋯
1177
1178 def b():⋯
1179
1180
1181 class Bar:
1182 # World!
1183
1184 def a():⋯
1185
1186 def b():⋯
1187
1188
1189 "
1190 .unindent(),
1191 );
1192
1193 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1194 assert_eq!(
1195 editor.display_text(cx),
1196 "
1197 class Foo:⋯
1198
1199
1200 class Bar:⋯
1201
1202
1203 "
1204 .unindent(),
1205 );
1206
1207 editor.unfold_all(&UnfoldAll, window, cx);
1208 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1209 assert_eq!(
1210 editor.display_text(cx),
1211 "
1212 class Foo:
1213 # Hello!
1214
1215 def a():
1216 print(1)
1217
1218 def b():
1219 print(2)
1220
1221
1222 class Bar:
1223 # World!
1224
1225 def a():
1226 print(1)
1227
1228 def b():
1229 print(2)
1230
1231
1232 "
1233 .unindent(),
1234 );
1235
1236 assert_eq!(
1237 editor.display_text(cx),
1238 editor.buffer.read(cx).read(cx).text()
1239 );
1240 });
1241}
1242
1243#[gpui::test]
1244fn test_move_cursor(cx: &mut TestAppContext) {
1245 init_test(cx, |_| {});
1246
1247 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1248 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1249
1250 buffer.update(cx, |buffer, cx| {
1251 buffer.edit(
1252 vec![
1253 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1254 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1255 ],
1256 None,
1257 cx,
1258 );
1259 });
1260 _ = editor.update(cx, |editor, window, cx| {
1261 assert_eq!(
1262 editor.selections.display_ranges(cx),
1263 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1264 );
1265
1266 editor.move_down(&MoveDown, window, cx);
1267 assert_eq!(
1268 editor.selections.display_ranges(cx),
1269 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1270 );
1271
1272 editor.move_right(&MoveRight, window, cx);
1273 assert_eq!(
1274 editor.selections.display_ranges(cx),
1275 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1276 );
1277
1278 editor.move_left(&MoveLeft, window, cx);
1279 assert_eq!(
1280 editor.selections.display_ranges(cx),
1281 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1282 );
1283
1284 editor.move_up(&MoveUp, window, cx);
1285 assert_eq!(
1286 editor.selections.display_ranges(cx),
1287 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1288 );
1289
1290 editor.move_to_end(&MoveToEnd, window, cx);
1291 assert_eq!(
1292 editor.selections.display_ranges(cx),
1293 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1294 );
1295
1296 editor.move_to_beginning(&MoveToBeginning, window, cx);
1297 assert_eq!(
1298 editor.selections.display_ranges(cx),
1299 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1300 );
1301
1302 editor.change_selections(None, window, cx, |s| {
1303 s.select_display_ranges([
1304 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1305 ]);
1306 });
1307 editor.select_to_beginning(&SelectToBeginning, window, cx);
1308 assert_eq!(
1309 editor.selections.display_ranges(cx),
1310 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1311 );
1312
1313 editor.select_to_end(&SelectToEnd, window, cx);
1314 assert_eq!(
1315 editor.selections.display_ranges(cx),
1316 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1317 );
1318 });
1319}
1320
1321#[gpui::test]
1322fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1323 init_test(cx, |_| {});
1324
1325 let editor = cx.add_window(|window, cx| {
1326 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1327 build_editor(buffer.clone(), window, cx)
1328 });
1329
1330 assert_eq!('🟥'.len_utf8(), 4);
1331 assert_eq!('α'.len_utf8(), 2);
1332
1333 _ = editor.update(cx, |editor, window, cx| {
1334 editor.fold_creases(
1335 vec![
1336 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1337 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1338 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1339 ],
1340 true,
1341 window,
1342 cx,
1343 );
1344 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1345
1346 editor.move_right(&MoveRight, window, cx);
1347 assert_eq!(
1348 editor.selections.display_ranges(cx),
1349 &[empty_range(0, "🟥".len())]
1350 );
1351 editor.move_right(&MoveRight, window, cx);
1352 assert_eq!(
1353 editor.selections.display_ranges(cx),
1354 &[empty_range(0, "🟥🟧".len())]
1355 );
1356 editor.move_right(&MoveRight, window, cx);
1357 assert_eq!(
1358 editor.selections.display_ranges(cx),
1359 &[empty_range(0, "🟥🟧⋯".len())]
1360 );
1361
1362 editor.move_down(&MoveDown, window, cx);
1363 assert_eq!(
1364 editor.selections.display_ranges(cx),
1365 &[empty_range(1, "ab⋯e".len())]
1366 );
1367 editor.move_left(&MoveLeft, window, cx);
1368 assert_eq!(
1369 editor.selections.display_ranges(cx),
1370 &[empty_range(1, "ab⋯".len())]
1371 );
1372 editor.move_left(&MoveLeft, window, cx);
1373 assert_eq!(
1374 editor.selections.display_ranges(cx),
1375 &[empty_range(1, "ab".len())]
1376 );
1377 editor.move_left(&MoveLeft, window, cx);
1378 assert_eq!(
1379 editor.selections.display_ranges(cx),
1380 &[empty_range(1, "a".len())]
1381 );
1382
1383 editor.move_down(&MoveDown, window, cx);
1384 assert_eq!(
1385 editor.selections.display_ranges(cx),
1386 &[empty_range(2, "α".len())]
1387 );
1388 editor.move_right(&MoveRight, window, cx);
1389 assert_eq!(
1390 editor.selections.display_ranges(cx),
1391 &[empty_range(2, "αβ".len())]
1392 );
1393 editor.move_right(&MoveRight, window, cx);
1394 assert_eq!(
1395 editor.selections.display_ranges(cx),
1396 &[empty_range(2, "αβ⋯".len())]
1397 );
1398 editor.move_right(&MoveRight, window, cx);
1399 assert_eq!(
1400 editor.selections.display_ranges(cx),
1401 &[empty_range(2, "αβ⋯ε".len())]
1402 );
1403
1404 editor.move_up(&MoveUp, window, cx);
1405 assert_eq!(
1406 editor.selections.display_ranges(cx),
1407 &[empty_range(1, "ab⋯e".len())]
1408 );
1409 editor.move_down(&MoveDown, window, cx);
1410 assert_eq!(
1411 editor.selections.display_ranges(cx),
1412 &[empty_range(2, "αβ⋯ε".len())]
1413 );
1414 editor.move_up(&MoveUp, window, cx);
1415 assert_eq!(
1416 editor.selections.display_ranges(cx),
1417 &[empty_range(1, "ab⋯e".len())]
1418 );
1419
1420 editor.move_up(&MoveUp, window, cx);
1421 assert_eq!(
1422 editor.selections.display_ranges(cx),
1423 &[empty_range(0, "🟥🟧".len())]
1424 );
1425 editor.move_left(&MoveLeft, window, cx);
1426 assert_eq!(
1427 editor.selections.display_ranges(cx),
1428 &[empty_range(0, "🟥".len())]
1429 );
1430 editor.move_left(&MoveLeft, window, cx);
1431 assert_eq!(
1432 editor.selections.display_ranges(cx),
1433 &[empty_range(0, "".len())]
1434 );
1435 });
1436}
1437
1438#[gpui::test]
1439fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1440 init_test(cx, |_| {});
1441
1442 let editor = cx.add_window(|window, cx| {
1443 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1444 build_editor(buffer.clone(), window, cx)
1445 });
1446 _ = editor.update(cx, |editor, window, cx| {
1447 editor.change_selections(None, window, cx, |s| {
1448 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1449 });
1450
1451 // moving above start of document should move selection to start of document,
1452 // but the next move down should still be at the original goal_x
1453 editor.move_up(&MoveUp, window, cx);
1454 assert_eq!(
1455 editor.selections.display_ranges(cx),
1456 &[empty_range(0, "".len())]
1457 );
1458
1459 editor.move_down(&MoveDown, window, cx);
1460 assert_eq!(
1461 editor.selections.display_ranges(cx),
1462 &[empty_range(1, "abcd".len())]
1463 );
1464
1465 editor.move_down(&MoveDown, window, cx);
1466 assert_eq!(
1467 editor.selections.display_ranges(cx),
1468 &[empty_range(2, "αβγ".len())]
1469 );
1470
1471 editor.move_down(&MoveDown, window, cx);
1472 assert_eq!(
1473 editor.selections.display_ranges(cx),
1474 &[empty_range(3, "abcd".len())]
1475 );
1476
1477 editor.move_down(&MoveDown, window, cx);
1478 assert_eq!(
1479 editor.selections.display_ranges(cx),
1480 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1481 );
1482
1483 // moving past end of document should not change goal_x
1484 editor.move_down(&MoveDown, window, cx);
1485 assert_eq!(
1486 editor.selections.display_ranges(cx),
1487 &[empty_range(5, "".len())]
1488 );
1489
1490 editor.move_down(&MoveDown, window, cx);
1491 assert_eq!(
1492 editor.selections.display_ranges(cx),
1493 &[empty_range(5, "".len())]
1494 );
1495
1496 editor.move_up(&MoveUp, window, cx);
1497 assert_eq!(
1498 editor.selections.display_ranges(cx),
1499 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1500 );
1501
1502 editor.move_up(&MoveUp, window, cx);
1503 assert_eq!(
1504 editor.selections.display_ranges(cx),
1505 &[empty_range(3, "abcd".len())]
1506 );
1507
1508 editor.move_up(&MoveUp, window, cx);
1509 assert_eq!(
1510 editor.selections.display_ranges(cx),
1511 &[empty_range(2, "αβγ".len())]
1512 );
1513 });
1514}
1515
1516#[gpui::test]
1517fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1518 init_test(cx, |_| {});
1519 let move_to_beg = MoveToBeginningOfLine {
1520 stop_at_soft_wraps: true,
1521 stop_at_indent: true,
1522 };
1523
1524 let delete_to_beg = DeleteToBeginningOfLine {
1525 stop_at_indent: false,
1526 };
1527
1528 let move_to_end = MoveToEndOfLine {
1529 stop_at_soft_wraps: true,
1530 };
1531
1532 let editor = cx.add_window(|window, cx| {
1533 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1534 build_editor(buffer, window, cx)
1535 });
1536 _ = editor.update(cx, |editor, window, cx| {
1537 editor.change_selections(None, window, cx, |s| {
1538 s.select_display_ranges([
1539 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1540 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1541 ]);
1542 });
1543 });
1544
1545 _ = editor.update(cx, |editor, window, cx| {
1546 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1547 assert_eq!(
1548 editor.selections.display_ranges(cx),
1549 &[
1550 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1551 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1552 ]
1553 );
1554 });
1555
1556 _ = editor.update(cx, |editor, window, cx| {
1557 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1558 assert_eq!(
1559 editor.selections.display_ranges(cx),
1560 &[
1561 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1562 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1563 ]
1564 );
1565 });
1566
1567 _ = editor.update(cx, |editor, window, cx| {
1568 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1569 assert_eq!(
1570 editor.selections.display_ranges(cx),
1571 &[
1572 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1573 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1574 ]
1575 );
1576 });
1577
1578 _ = editor.update(cx, |editor, window, cx| {
1579 editor.move_to_end_of_line(&move_to_end, window, cx);
1580 assert_eq!(
1581 editor.selections.display_ranges(cx),
1582 &[
1583 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1584 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1585 ]
1586 );
1587 });
1588
1589 // Moving to the end of line again is a no-op.
1590 _ = editor.update(cx, |editor, window, cx| {
1591 editor.move_to_end_of_line(&move_to_end, window, cx);
1592 assert_eq!(
1593 editor.selections.display_ranges(cx),
1594 &[
1595 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1596 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1597 ]
1598 );
1599 });
1600
1601 _ = editor.update(cx, |editor, window, cx| {
1602 editor.move_left(&MoveLeft, window, cx);
1603 editor.select_to_beginning_of_line(
1604 &SelectToBeginningOfLine {
1605 stop_at_soft_wraps: true,
1606 stop_at_indent: true,
1607 },
1608 window,
1609 cx,
1610 );
1611 assert_eq!(
1612 editor.selections.display_ranges(cx),
1613 &[
1614 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1615 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1616 ]
1617 );
1618 });
1619
1620 _ = editor.update(cx, |editor, window, cx| {
1621 editor.select_to_beginning_of_line(
1622 &SelectToBeginningOfLine {
1623 stop_at_soft_wraps: true,
1624 stop_at_indent: true,
1625 },
1626 window,
1627 cx,
1628 );
1629 assert_eq!(
1630 editor.selections.display_ranges(cx),
1631 &[
1632 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1633 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1634 ]
1635 );
1636 });
1637
1638 _ = editor.update(cx, |editor, window, cx| {
1639 editor.select_to_beginning_of_line(
1640 &SelectToBeginningOfLine {
1641 stop_at_soft_wraps: true,
1642 stop_at_indent: true,
1643 },
1644 window,
1645 cx,
1646 );
1647 assert_eq!(
1648 editor.selections.display_ranges(cx),
1649 &[
1650 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1651 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1652 ]
1653 );
1654 });
1655
1656 _ = editor.update(cx, |editor, window, cx| {
1657 editor.select_to_end_of_line(
1658 &SelectToEndOfLine {
1659 stop_at_soft_wraps: true,
1660 },
1661 window,
1662 cx,
1663 );
1664 assert_eq!(
1665 editor.selections.display_ranges(cx),
1666 &[
1667 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1668 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1669 ]
1670 );
1671 });
1672
1673 _ = editor.update(cx, |editor, window, cx| {
1674 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1675 assert_eq!(editor.display_text(cx), "ab\n de");
1676 assert_eq!(
1677 editor.selections.display_ranges(cx),
1678 &[
1679 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1680 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1681 ]
1682 );
1683 });
1684
1685 _ = editor.update(cx, |editor, window, cx| {
1686 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1687 assert_eq!(editor.display_text(cx), "\n");
1688 assert_eq!(
1689 editor.selections.display_ranges(cx),
1690 &[
1691 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1692 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1693 ]
1694 );
1695 });
1696}
1697
1698#[gpui::test]
1699fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1700 init_test(cx, |_| {});
1701 let move_to_beg = MoveToBeginningOfLine {
1702 stop_at_soft_wraps: false,
1703 stop_at_indent: false,
1704 };
1705
1706 let move_to_end = MoveToEndOfLine {
1707 stop_at_soft_wraps: false,
1708 };
1709
1710 let editor = cx.add_window(|window, cx| {
1711 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1712 build_editor(buffer, window, cx)
1713 });
1714
1715 _ = editor.update(cx, |editor, window, cx| {
1716 editor.set_wrap_width(Some(140.0.into()), cx);
1717
1718 // We expect the following lines after wrapping
1719 // ```
1720 // thequickbrownfox
1721 // jumpedoverthelazydo
1722 // gs
1723 // ```
1724 // The final `gs` was soft-wrapped onto a new line.
1725 assert_eq!(
1726 "thequickbrownfox\njumpedoverthelaz\nydogs",
1727 editor.display_text(cx),
1728 );
1729
1730 // First, let's assert behavior on the first line, that was not soft-wrapped.
1731 // Start the cursor at the `k` on the first line
1732 editor.change_selections(None, window, cx, |s| {
1733 s.select_display_ranges([
1734 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1735 ]);
1736 });
1737
1738 // Moving to the beginning of the line should put us at the beginning of the line.
1739 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1740 assert_eq!(
1741 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1742 editor.selections.display_ranges(cx)
1743 );
1744
1745 // Moving to the end of the line should put us at the end of the line.
1746 editor.move_to_end_of_line(&move_to_end, window, cx);
1747 assert_eq!(
1748 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1749 editor.selections.display_ranges(cx)
1750 );
1751
1752 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1753 // Start the cursor at the last line (`y` that was wrapped to a new line)
1754 editor.change_selections(None, window, cx, |s| {
1755 s.select_display_ranges([
1756 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1757 ]);
1758 });
1759
1760 // Moving to the beginning of the line should put us at the start of the second line of
1761 // display text, i.e., the `j`.
1762 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1763 assert_eq!(
1764 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1765 editor.selections.display_ranges(cx)
1766 );
1767
1768 // Moving to the beginning of the line again should be a no-op.
1769 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1770 assert_eq!(
1771 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1772 editor.selections.display_ranges(cx)
1773 );
1774
1775 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1776 // next display line.
1777 editor.move_to_end_of_line(&move_to_end, window, cx);
1778 assert_eq!(
1779 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1780 editor.selections.display_ranges(cx)
1781 );
1782
1783 // Moving to the end of the line again should be a no-op.
1784 editor.move_to_end_of_line(&move_to_end, window, cx);
1785 assert_eq!(
1786 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1787 editor.selections.display_ranges(cx)
1788 );
1789 });
1790}
1791
1792#[gpui::test]
1793fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1794 init_test(cx, |_| {});
1795
1796 let move_to_beg = MoveToBeginningOfLine {
1797 stop_at_soft_wraps: true,
1798 stop_at_indent: true,
1799 };
1800
1801 let select_to_beg = SelectToBeginningOfLine {
1802 stop_at_soft_wraps: true,
1803 stop_at_indent: true,
1804 };
1805
1806 let delete_to_beg = DeleteToBeginningOfLine {
1807 stop_at_indent: true,
1808 };
1809
1810 let move_to_end = MoveToEndOfLine {
1811 stop_at_soft_wraps: false,
1812 };
1813
1814 let editor = cx.add_window(|window, cx| {
1815 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1816 build_editor(buffer, window, cx)
1817 });
1818
1819 _ = editor.update(cx, |editor, window, cx| {
1820 editor.change_selections(None, window, cx, |s| {
1821 s.select_display_ranges([
1822 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1823 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1824 ]);
1825 });
1826
1827 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1828 // and the second cursor at the first non-whitespace character in the line.
1829 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1830 assert_eq!(
1831 editor.selections.display_ranges(cx),
1832 &[
1833 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1834 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1835 ]
1836 );
1837
1838 // Moving to the beginning of the line again should be a no-op for the first cursor,
1839 // and should move the second cursor to the beginning of the line.
1840 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1841 assert_eq!(
1842 editor.selections.display_ranges(cx),
1843 &[
1844 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1845 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1846 ]
1847 );
1848
1849 // Moving to the beginning of the line again should still be a no-op for the first cursor,
1850 // and should move the second cursor back to the first non-whitespace character in the line.
1851 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1852 assert_eq!(
1853 editor.selections.display_ranges(cx),
1854 &[
1855 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1856 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1857 ]
1858 );
1859
1860 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
1861 // and to the first non-whitespace character in the line for the second cursor.
1862 editor.move_to_end_of_line(&move_to_end, window, cx);
1863 editor.move_left(&MoveLeft, window, cx);
1864 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1865 assert_eq!(
1866 editor.selections.display_ranges(cx),
1867 &[
1868 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1869 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1870 ]
1871 );
1872
1873 // Selecting to the beginning of the line again should be a no-op for the first cursor,
1874 // and should select to the beginning of the line for the second cursor.
1875 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1876 assert_eq!(
1877 editor.selections.display_ranges(cx),
1878 &[
1879 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1880 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1881 ]
1882 );
1883
1884 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
1885 // and should delete to the first non-whitespace character in the line for the second cursor.
1886 editor.move_to_end_of_line(&move_to_end, window, cx);
1887 editor.move_left(&MoveLeft, window, cx);
1888 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1889 assert_eq!(editor.text(cx), "c\n f");
1890 });
1891}
1892
1893#[gpui::test]
1894fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
1895 init_test(cx, |_| {});
1896
1897 let editor = cx.add_window(|window, cx| {
1898 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
1899 build_editor(buffer, window, cx)
1900 });
1901 _ = editor.update(cx, |editor, window, cx| {
1902 editor.change_selections(None, window, cx, |s| {
1903 s.select_display_ranges([
1904 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
1905 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
1906 ])
1907 });
1908
1909 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1910 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1911
1912 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1913 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\n ˇ{baz.qux()}", editor, cx);
1914
1915 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1916 assert_selection_ranges("use ˇstd::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
1917
1918 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1919 assert_selection_ranges("ˇuse std::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1920
1921 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1922 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1923
1924 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1925 assert_selection_ranges("useˇ std::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
1926
1927 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1928 assert_selection_ranges("use stdˇ::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1929
1930 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1931 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1932
1933 editor.move_right(&MoveRight, window, cx);
1934 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1935 assert_selection_ranges(
1936 "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
1937 editor,
1938 cx,
1939 );
1940
1941 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1942 assert_selection_ranges(
1943 "use std«ˇ::s»tr::{foo, bar}\n\n «ˇ{b»az.qux()}",
1944 editor,
1945 cx,
1946 );
1947
1948 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
1949 assert_selection_ranges(
1950 "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
1951 editor,
1952 cx,
1953 );
1954 });
1955}
1956
1957#[gpui::test]
1958fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
1959 init_test(cx, |_| {});
1960
1961 let editor = cx.add_window(|window, cx| {
1962 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
1963 build_editor(buffer, window, cx)
1964 });
1965
1966 _ = editor.update(cx, |editor, window, cx| {
1967 editor.set_wrap_width(Some(140.0.into()), cx);
1968 assert_eq!(
1969 editor.display_text(cx),
1970 "use one::{\n two::three::\n four::five\n};"
1971 );
1972
1973 editor.change_selections(None, window, cx, |s| {
1974 s.select_display_ranges([
1975 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
1976 ]);
1977 });
1978
1979 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1980 assert_eq!(
1981 editor.selections.display_ranges(cx),
1982 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
1983 );
1984
1985 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1986 assert_eq!(
1987 editor.selections.display_ranges(cx),
1988 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
1989 );
1990
1991 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1992 assert_eq!(
1993 editor.selections.display_ranges(cx),
1994 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
1995 );
1996
1997 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1998 assert_eq!(
1999 editor.selections.display_ranges(cx),
2000 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2001 );
2002
2003 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2004 assert_eq!(
2005 editor.selections.display_ranges(cx),
2006 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2007 );
2008
2009 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2010 assert_eq!(
2011 editor.selections.display_ranges(cx),
2012 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2013 );
2014 });
2015}
2016
2017#[gpui::test]
2018async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2019 init_test(cx, |_| {});
2020 let mut cx = EditorTestContext::new(cx).await;
2021
2022 let line_height = cx.editor(|editor, window, _| {
2023 editor
2024 .style()
2025 .unwrap()
2026 .text
2027 .line_height_in_pixels(window.rem_size())
2028 });
2029 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2030
2031 cx.set_state(
2032 &r#"ˇone
2033 two
2034
2035 three
2036 fourˇ
2037 five
2038
2039 six"#
2040 .unindent(),
2041 );
2042
2043 cx.update_editor(|editor, window, cx| {
2044 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2045 });
2046 cx.assert_editor_state(
2047 &r#"one
2048 two
2049 ˇ
2050 three
2051 four
2052 five
2053 ˇ
2054 six"#
2055 .unindent(),
2056 );
2057
2058 cx.update_editor(|editor, window, cx| {
2059 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2060 });
2061 cx.assert_editor_state(
2062 &r#"one
2063 two
2064
2065 three
2066 four
2067 five
2068 ˇ
2069 sixˇ"#
2070 .unindent(),
2071 );
2072
2073 cx.update_editor(|editor, window, cx| {
2074 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2075 });
2076 cx.assert_editor_state(
2077 &r#"one
2078 two
2079
2080 three
2081 four
2082 five
2083
2084 sixˇ"#
2085 .unindent(),
2086 );
2087
2088 cx.update_editor(|editor, window, cx| {
2089 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2090 });
2091 cx.assert_editor_state(
2092 &r#"one
2093 two
2094
2095 three
2096 four
2097 five
2098 ˇ
2099 six"#
2100 .unindent(),
2101 );
2102
2103 cx.update_editor(|editor, window, cx| {
2104 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2105 });
2106 cx.assert_editor_state(
2107 &r#"one
2108 two
2109 ˇ
2110 three
2111 four
2112 five
2113
2114 six"#
2115 .unindent(),
2116 );
2117
2118 cx.update_editor(|editor, window, cx| {
2119 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2120 });
2121 cx.assert_editor_state(
2122 &r#"ˇone
2123 two
2124
2125 three
2126 four
2127 five
2128
2129 six"#
2130 .unindent(),
2131 );
2132}
2133
2134#[gpui::test]
2135async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2136 init_test(cx, |_| {});
2137 let mut cx = EditorTestContext::new(cx).await;
2138 let line_height = cx.editor(|editor, window, _| {
2139 editor
2140 .style()
2141 .unwrap()
2142 .text
2143 .line_height_in_pixels(window.rem_size())
2144 });
2145 let window = cx.window;
2146 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2147
2148 cx.set_state(
2149 r#"ˇone
2150 two
2151 three
2152 four
2153 five
2154 six
2155 seven
2156 eight
2157 nine
2158 ten
2159 "#,
2160 );
2161
2162 cx.update_editor(|editor, window, cx| {
2163 assert_eq!(
2164 editor.snapshot(window, cx).scroll_position(),
2165 gpui::Point::new(0., 0.)
2166 );
2167 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2168 assert_eq!(
2169 editor.snapshot(window, cx).scroll_position(),
2170 gpui::Point::new(0., 3.)
2171 );
2172 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2173 assert_eq!(
2174 editor.snapshot(window, cx).scroll_position(),
2175 gpui::Point::new(0., 6.)
2176 );
2177 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2178 assert_eq!(
2179 editor.snapshot(window, cx).scroll_position(),
2180 gpui::Point::new(0., 3.)
2181 );
2182
2183 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2184 assert_eq!(
2185 editor.snapshot(window, cx).scroll_position(),
2186 gpui::Point::new(0., 1.)
2187 );
2188 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2189 assert_eq!(
2190 editor.snapshot(window, cx).scroll_position(),
2191 gpui::Point::new(0., 3.)
2192 );
2193 });
2194}
2195
2196#[gpui::test]
2197async fn test_autoscroll(cx: &mut TestAppContext) {
2198 init_test(cx, |_| {});
2199 let mut cx = EditorTestContext::new(cx).await;
2200
2201 let line_height = cx.update_editor(|editor, window, cx| {
2202 editor.set_vertical_scroll_margin(2, cx);
2203 editor
2204 .style()
2205 .unwrap()
2206 .text
2207 .line_height_in_pixels(window.rem_size())
2208 });
2209 let window = cx.window;
2210 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2211
2212 cx.set_state(
2213 r#"ˇone
2214 two
2215 three
2216 four
2217 five
2218 six
2219 seven
2220 eight
2221 nine
2222 ten
2223 "#,
2224 );
2225 cx.update_editor(|editor, window, cx| {
2226 assert_eq!(
2227 editor.snapshot(window, cx).scroll_position(),
2228 gpui::Point::new(0., 0.0)
2229 );
2230 });
2231
2232 // Add a cursor below the visible area. Since both cursors cannot fit
2233 // on screen, the editor autoscrolls to reveal the newest cursor, and
2234 // allows the vertical scroll margin below that cursor.
2235 cx.update_editor(|editor, window, cx| {
2236 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2237 selections.select_ranges([
2238 Point::new(0, 0)..Point::new(0, 0),
2239 Point::new(6, 0)..Point::new(6, 0),
2240 ]);
2241 })
2242 });
2243 cx.update_editor(|editor, window, cx| {
2244 assert_eq!(
2245 editor.snapshot(window, cx).scroll_position(),
2246 gpui::Point::new(0., 3.0)
2247 );
2248 });
2249
2250 // Move down. The editor cursor scrolls down to track the newest cursor.
2251 cx.update_editor(|editor, window, cx| {
2252 editor.move_down(&Default::default(), window, cx);
2253 });
2254 cx.update_editor(|editor, window, cx| {
2255 assert_eq!(
2256 editor.snapshot(window, cx).scroll_position(),
2257 gpui::Point::new(0., 4.0)
2258 );
2259 });
2260
2261 // Add a cursor above the visible area. Since both cursors fit on screen,
2262 // the editor scrolls to show both.
2263 cx.update_editor(|editor, window, cx| {
2264 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2265 selections.select_ranges([
2266 Point::new(1, 0)..Point::new(1, 0),
2267 Point::new(6, 0)..Point::new(6, 0),
2268 ]);
2269 })
2270 });
2271 cx.update_editor(|editor, window, cx| {
2272 assert_eq!(
2273 editor.snapshot(window, cx).scroll_position(),
2274 gpui::Point::new(0., 1.0)
2275 );
2276 });
2277}
2278
2279#[gpui::test]
2280async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2281 init_test(cx, |_| {});
2282 let mut cx = EditorTestContext::new(cx).await;
2283
2284 let line_height = cx.editor(|editor, window, _cx| {
2285 editor
2286 .style()
2287 .unwrap()
2288 .text
2289 .line_height_in_pixels(window.rem_size())
2290 });
2291 let window = cx.window;
2292 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2293 cx.set_state(
2294 &r#"
2295 ˇone
2296 two
2297 threeˇ
2298 four
2299 five
2300 six
2301 seven
2302 eight
2303 nine
2304 ten
2305 "#
2306 .unindent(),
2307 );
2308
2309 cx.update_editor(|editor, window, cx| {
2310 editor.move_page_down(&MovePageDown::default(), window, cx)
2311 });
2312 cx.assert_editor_state(
2313 &r#"
2314 one
2315 two
2316 three
2317 ˇfour
2318 five
2319 sixˇ
2320 seven
2321 eight
2322 nine
2323 ten
2324 "#
2325 .unindent(),
2326 );
2327
2328 cx.update_editor(|editor, window, cx| {
2329 editor.move_page_down(&MovePageDown::default(), window, cx)
2330 });
2331 cx.assert_editor_state(
2332 &r#"
2333 one
2334 two
2335 three
2336 four
2337 five
2338 six
2339 ˇseven
2340 eight
2341 nineˇ
2342 ten
2343 "#
2344 .unindent(),
2345 );
2346
2347 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2348 cx.assert_editor_state(
2349 &r#"
2350 one
2351 two
2352 three
2353 ˇfour
2354 five
2355 sixˇ
2356 seven
2357 eight
2358 nine
2359 ten
2360 "#
2361 .unindent(),
2362 );
2363
2364 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2365 cx.assert_editor_state(
2366 &r#"
2367 ˇone
2368 two
2369 threeˇ
2370 four
2371 five
2372 six
2373 seven
2374 eight
2375 nine
2376 ten
2377 "#
2378 .unindent(),
2379 );
2380
2381 // Test select collapsing
2382 cx.update_editor(|editor, window, cx| {
2383 editor.move_page_down(&MovePageDown::default(), window, cx);
2384 editor.move_page_down(&MovePageDown::default(), window, cx);
2385 editor.move_page_down(&MovePageDown::default(), window, cx);
2386 });
2387 cx.assert_editor_state(
2388 &r#"
2389 one
2390 two
2391 three
2392 four
2393 five
2394 six
2395 seven
2396 eight
2397 nine
2398 ˇten
2399 ˇ"#
2400 .unindent(),
2401 );
2402}
2403
2404#[gpui::test]
2405async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2406 init_test(cx, |_| {});
2407 let mut cx = EditorTestContext::new(cx).await;
2408 cx.set_state("one «two threeˇ» four");
2409 cx.update_editor(|editor, window, cx| {
2410 editor.delete_to_beginning_of_line(
2411 &DeleteToBeginningOfLine {
2412 stop_at_indent: false,
2413 },
2414 window,
2415 cx,
2416 );
2417 assert_eq!(editor.text(cx), " four");
2418 });
2419}
2420
2421#[gpui::test]
2422fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2423 init_test(cx, |_| {});
2424
2425 let editor = cx.add_window(|window, cx| {
2426 let buffer = MultiBuffer::build_simple("one two three four", cx);
2427 build_editor(buffer.clone(), window, cx)
2428 });
2429
2430 _ = editor.update(cx, |editor, window, cx| {
2431 editor.change_selections(None, window, cx, |s| {
2432 s.select_display_ranges([
2433 // an empty selection - the preceding word fragment is deleted
2434 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2435 // characters selected - they are deleted
2436 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 12),
2437 ])
2438 });
2439 editor.delete_to_previous_word_start(
2440 &DeleteToPreviousWordStart {
2441 ignore_newlines: false,
2442 },
2443 window,
2444 cx,
2445 );
2446 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e two te four");
2447 });
2448
2449 _ = editor.update(cx, |editor, window, cx| {
2450 editor.change_selections(None, window, cx, |s| {
2451 s.select_display_ranges([
2452 // an empty selection - the following word fragment is deleted
2453 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
2454 // characters selected - they are deleted
2455 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 10),
2456 ])
2457 });
2458 editor.delete_to_next_word_end(
2459 &DeleteToNextWordEnd {
2460 ignore_newlines: false,
2461 },
2462 window,
2463 cx,
2464 );
2465 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e t te our");
2466 });
2467}
2468
2469#[gpui::test]
2470fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2471 init_test(cx, |_| {});
2472
2473 let editor = cx.add_window(|window, cx| {
2474 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2475 build_editor(buffer.clone(), window, cx)
2476 });
2477 let del_to_prev_word_start = DeleteToPreviousWordStart {
2478 ignore_newlines: false,
2479 };
2480 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2481 ignore_newlines: true,
2482 };
2483
2484 _ = editor.update(cx, |editor, window, cx| {
2485 editor.change_selections(None, window, cx, |s| {
2486 s.select_display_ranges([
2487 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2488 ])
2489 });
2490 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2491 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
2492 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2493 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
2494 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2495 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
2496 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2497 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
2498 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2499 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
2500 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2501 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2502 });
2503}
2504
2505#[gpui::test]
2506fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
2507 init_test(cx, |_| {});
2508
2509 let editor = cx.add_window(|window, cx| {
2510 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
2511 build_editor(buffer.clone(), window, cx)
2512 });
2513 let del_to_next_word_end = DeleteToNextWordEnd {
2514 ignore_newlines: false,
2515 };
2516 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
2517 ignore_newlines: true,
2518 };
2519
2520 _ = editor.update(cx, |editor, window, cx| {
2521 editor.change_selections(None, window, cx, |s| {
2522 s.select_display_ranges([
2523 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2524 ])
2525 });
2526 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2527 assert_eq!(
2528 editor.buffer.read(cx).read(cx).text(),
2529 "one\n two\nthree\n four"
2530 );
2531 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2532 assert_eq!(
2533 editor.buffer.read(cx).read(cx).text(),
2534 "\n two\nthree\n four"
2535 );
2536 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2537 assert_eq!(
2538 editor.buffer.read(cx).read(cx).text(),
2539 "two\nthree\n four"
2540 );
2541 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2542 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
2543 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2544 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
2545 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2546 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2547 });
2548}
2549
2550#[gpui::test]
2551fn test_newline(cx: &mut TestAppContext) {
2552 init_test(cx, |_| {});
2553
2554 let editor = cx.add_window(|window, cx| {
2555 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
2556 build_editor(buffer.clone(), window, cx)
2557 });
2558
2559 _ = editor.update(cx, |editor, window, cx| {
2560 editor.change_selections(None, window, cx, |s| {
2561 s.select_display_ranges([
2562 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2563 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2564 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
2565 ])
2566 });
2567
2568 editor.newline(&Newline, window, cx);
2569 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
2570 });
2571}
2572
2573#[gpui::test]
2574fn test_newline_with_old_selections(cx: &mut TestAppContext) {
2575 init_test(cx, |_| {});
2576
2577 let editor = cx.add_window(|window, cx| {
2578 let buffer = MultiBuffer::build_simple(
2579 "
2580 a
2581 b(
2582 X
2583 )
2584 c(
2585 X
2586 )
2587 "
2588 .unindent()
2589 .as_str(),
2590 cx,
2591 );
2592 let mut editor = build_editor(buffer.clone(), window, cx);
2593 editor.change_selections(None, window, cx, |s| {
2594 s.select_ranges([
2595 Point::new(2, 4)..Point::new(2, 5),
2596 Point::new(5, 4)..Point::new(5, 5),
2597 ])
2598 });
2599 editor
2600 });
2601
2602 _ = editor.update(cx, |editor, window, cx| {
2603 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2604 editor.buffer.update(cx, |buffer, cx| {
2605 buffer.edit(
2606 [
2607 (Point::new(1, 2)..Point::new(3, 0), ""),
2608 (Point::new(4, 2)..Point::new(6, 0), ""),
2609 ],
2610 None,
2611 cx,
2612 );
2613 assert_eq!(
2614 buffer.read(cx).text(),
2615 "
2616 a
2617 b()
2618 c()
2619 "
2620 .unindent()
2621 );
2622 });
2623 assert_eq!(
2624 editor.selections.ranges(cx),
2625 &[
2626 Point::new(1, 2)..Point::new(1, 2),
2627 Point::new(2, 2)..Point::new(2, 2),
2628 ],
2629 );
2630
2631 editor.newline(&Newline, window, cx);
2632 assert_eq!(
2633 editor.text(cx),
2634 "
2635 a
2636 b(
2637 )
2638 c(
2639 )
2640 "
2641 .unindent()
2642 );
2643
2644 // The selections are moved after the inserted newlines
2645 assert_eq!(
2646 editor.selections.ranges(cx),
2647 &[
2648 Point::new(2, 0)..Point::new(2, 0),
2649 Point::new(4, 0)..Point::new(4, 0),
2650 ],
2651 );
2652 });
2653}
2654
2655#[gpui::test]
2656async fn test_newline_above(cx: &mut TestAppContext) {
2657 init_test(cx, |settings| {
2658 settings.defaults.tab_size = NonZeroU32::new(4)
2659 });
2660
2661 let language = Arc::new(
2662 Language::new(
2663 LanguageConfig::default(),
2664 Some(tree_sitter_rust::LANGUAGE.into()),
2665 )
2666 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2667 .unwrap(),
2668 );
2669
2670 let mut cx = EditorTestContext::new(cx).await;
2671 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2672 cx.set_state(indoc! {"
2673 const a: ˇA = (
2674 (ˇ
2675 «const_functionˇ»(ˇ),
2676 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2677 )ˇ
2678 ˇ);ˇ
2679 "});
2680
2681 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
2682 cx.assert_editor_state(indoc! {"
2683 ˇ
2684 const a: A = (
2685 ˇ
2686 (
2687 ˇ
2688 ˇ
2689 const_function(),
2690 ˇ
2691 ˇ
2692 ˇ
2693 ˇ
2694 something_else,
2695 ˇ
2696 )
2697 ˇ
2698 ˇ
2699 );
2700 "});
2701}
2702
2703#[gpui::test]
2704async fn test_newline_below(cx: &mut TestAppContext) {
2705 init_test(cx, |settings| {
2706 settings.defaults.tab_size = NonZeroU32::new(4)
2707 });
2708
2709 let language = Arc::new(
2710 Language::new(
2711 LanguageConfig::default(),
2712 Some(tree_sitter_rust::LANGUAGE.into()),
2713 )
2714 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2715 .unwrap(),
2716 );
2717
2718 let mut cx = EditorTestContext::new(cx).await;
2719 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2720 cx.set_state(indoc! {"
2721 const a: ˇA = (
2722 (ˇ
2723 «const_functionˇ»(ˇ),
2724 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2725 )ˇ
2726 ˇ);ˇ
2727 "});
2728
2729 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
2730 cx.assert_editor_state(indoc! {"
2731 const a: A = (
2732 ˇ
2733 (
2734 ˇ
2735 const_function(),
2736 ˇ
2737 ˇ
2738 something_else,
2739 ˇ
2740 ˇ
2741 ˇ
2742 ˇ
2743 )
2744 ˇ
2745 );
2746 ˇ
2747 ˇ
2748 "});
2749}
2750
2751#[gpui::test]
2752async fn test_newline_comments(cx: &mut TestAppContext) {
2753 init_test(cx, |settings| {
2754 settings.defaults.tab_size = NonZeroU32::new(4)
2755 });
2756
2757 let language = Arc::new(Language::new(
2758 LanguageConfig {
2759 line_comments: vec!["// ".into()],
2760 ..LanguageConfig::default()
2761 },
2762 None,
2763 ));
2764 {
2765 let mut cx = EditorTestContext::new(cx).await;
2766 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2767 cx.set_state(indoc! {"
2768 // Fooˇ
2769 "});
2770
2771 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2772 cx.assert_editor_state(indoc! {"
2773 // Foo
2774 // ˇ
2775 "});
2776 // Ensure that we add comment prefix when existing line contains space
2777 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2778 cx.assert_editor_state(
2779 indoc! {"
2780 // Foo
2781 //s
2782 // ˇ
2783 "}
2784 .replace("s", " ") // s is used as space placeholder to prevent format on save
2785 .as_str(),
2786 );
2787 // Ensure that we add comment prefix when existing line does not contain space
2788 cx.set_state(indoc! {"
2789 // Foo
2790 //ˇ
2791 "});
2792 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2793 cx.assert_editor_state(indoc! {"
2794 // Foo
2795 //
2796 // ˇ
2797 "});
2798 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
2799 cx.set_state(indoc! {"
2800 ˇ// Foo
2801 "});
2802 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2803 cx.assert_editor_state(indoc! {"
2804
2805 ˇ// Foo
2806 "});
2807 }
2808 // Ensure that comment continuations can be disabled.
2809 update_test_language_settings(cx, |settings| {
2810 settings.defaults.extend_comment_on_newline = Some(false);
2811 });
2812 let mut cx = EditorTestContext::new(cx).await;
2813 cx.set_state(indoc! {"
2814 // Fooˇ
2815 "});
2816 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2817 cx.assert_editor_state(indoc! {"
2818 // Foo
2819 ˇ
2820 "});
2821}
2822
2823#[gpui::test]
2824async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
2825 init_test(cx, |settings| {
2826 settings.defaults.tab_size = NonZeroU32::new(4)
2827 });
2828
2829 let language = Arc::new(Language::new(
2830 LanguageConfig {
2831 line_comments: vec!["// ".into(), "/// ".into()],
2832 ..LanguageConfig::default()
2833 },
2834 None,
2835 ));
2836 {
2837 let mut cx = EditorTestContext::new(cx).await;
2838 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2839 cx.set_state(indoc! {"
2840 //ˇ
2841 "});
2842 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2843 cx.assert_editor_state(indoc! {"
2844 //
2845 // ˇ
2846 "});
2847
2848 cx.set_state(indoc! {"
2849 ///ˇ
2850 "});
2851 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2852 cx.assert_editor_state(indoc! {"
2853 ///
2854 /// ˇ
2855 "});
2856 }
2857}
2858
2859#[gpui::test]
2860async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
2861 init_test(cx, |settings| {
2862 settings.defaults.tab_size = NonZeroU32::new(4)
2863 });
2864
2865 let language = Arc::new(
2866 Language::new(
2867 LanguageConfig {
2868 documentation: Some(language::DocumentationConfig {
2869 start: "/**".into(),
2870 end: "*/".into(),
2871 prefix: "* ".into(),
2872 tab_size: NonZeroU32::new(1).unwrap(),
2873 }),
2874
2875 ..LanguageConfig::default()
2876 },
2877 Some(tree_sitter_rust::LANGUAGE.into()),
2878 )
2879 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
2880 .unwrap(),
2881 );
2882
2883 {
2884 let mut cx = EditorTestContext::new(cx).await;
2885 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2886 cx.set_state(indoc! {"
2887 /**ˇ
2888 "});
2889
2890 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2891 cx.assert_editor_state(indoc! {"
2892 /**
2893 * ˇ
2894 "});
2895 // Ensure that if cursor is before the comment start,
2896 // we do not actually insert a comment prefix.
2897 cx.set_state(indoc! {"
2898 ˇ/**
2899 "});
2900 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2901 cx.assert_editor_state(indoc! {"
2902
2903 ˇ/**
2904 "});
2905 // Ensure that if cursor is between it doesn't add comment prefix.
2906 cx.set_state(indoc! {"
2907 /*ˇ*
2908 "});
2909 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2910 cx.assert_editor_state(indoc! {"
2911 /*
2912 ˇ*
2913 "});
2914 // Ensure that if suffix exists on same line after cursor it adds new line.
2915 cx.set_state(indoc! {"
2916 /**ˇ*/
2917 "});
2918 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2919 cx.assert_editor_state(indoc! {"
2920 /**
2921 * ˇ
2922 */
2923 "});
2924 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2925 cx.set_state(indoc! {"
2926 /**ˇ */
2927 "});
2928 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2929 cx.assert_editor_state(indoc! {"
2930 /**
2931 * ˇ
2932 */
2933 "});
2934 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2935 cx.set_state(indoc! {"
2936 /** ˇ*/
2937 "});
2938 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2939 cx.assert_editor_state(
2940 indoc! {"
2941 /**s
2942 * ˇ
2943 */
2944 "}
2945 .replace("s", " ") // s is used as space placeholder to prevent format on save
2946 .as_str(),
2947 );
2948 // Ensure that delimiter space is preserved when newline on already
2949 // spaced delimiter.
2950 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2951 cx.assert_editor_state(
2952 indoc! {"
2953 /**s
2954 *s
2955 * ˇ
2956 */
2957 "}
2958 .replace("s", " ") // s is used as space placeholder to prevent format on save
2959 .as_str(),
2960 );
2961 // Ensure that delimiter space is preserved when space is not
2962 // on existing delimiter.
2963 cx.set_state(indoc! {"
2964 /**
2965 *ˇ
2966 */
2967 "});
2968 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2969 cx.assert_editor_state(indoc! {"
2970 /**
2971 *
2972 * ˇ
2973 */
2974 "});
2975 // Ensure that if suffix exists on same line after cursor it
2976 // doesn't add extra new line if prefix is not on same line.
2977 cx.set_state(indoc! {"
2978 /**
2979 ˇ*/
2980 "});
2981 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2982 cx.assert_editor_state(indoc! {"
2983 /**
2984
2985 ˇ*/
2986 "});
2987 // Ensure that it detects suffix after existing prefix.
2988 cx.set_state(indoc! {"
2989 /**ˇ/
2990 "});
2991 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2992 cx.assert_editor_state(indoc! {"
2993 /**
2994 ˇ/
2995 "});
2996 // Ensure that if suffix exists on same line before
2997 // cursor it does not add comment prefix.
2998 cx.set_state(indoc! {"
2999 /** */ˇ
3000 "});
3001 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3002 cx.assert_editor_state(indoc! {"
3003 /** */
3004 ˇ
3005 "});
3006 // Ensure that if suffix exists on same line before
3007 // cursor it does not add comment prefix.
3008 cx.set_state(indoc! {"
3009 /**
3010 *
3011 */ˇ
3012 "});
3013 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3014 cx.assert_editor_state(indoc! {"
3015 /**
3016 *
3017 */
3018 ˇ
3019 "});
3020
3021 // Ensure that inline comment followed by code
3022 // doesn't add comment prefix on newline
3023 cx.set_state(indoc! {"
3024 /** */ textˇ
3025 "});
3026 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3027 cx.assert_editor_state(indoc! {"
3028 /** */ text
3029 ˇ
3030 "});
3031
3032 // Ensure that text after comment end tag
3033 // doesn't add comment prefix on newline
3034 cx.set_state(indoc! {"
3035 /**
3036 *
3037 */ˇtext
3038 "});
3039 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3040 cx.assert_editor_state(indoc! {"
3041 /**
3042 *
3043 */
3044 ˇtext
3045 "});
3046
3047 // Ensure if not comment block it doesn't
3048 // add comment prefix on newline
3049 cx.set_state(indoc! {"
3050 * textˇ
3051 "});
3052 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3053 cx.assert_editor_state(indoc! {"
3054 * text
3055 ˇ
3056 "});
3057 }
3058 // Ensure that comment continuations can be disabled.
3059 update_test_language_settings(cx, |settings| {
3060 settings.defaults.extend_comment_on_newline = Some(false);
3061 });
3062 let mut cx = EditorTestContext::new(cx).await;
3063 cx.set_state(indoc! {"
3064 /**ˇ
3065 "});
3066 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3067 cx.assert_editor_state(indoc! {"
3068 /**
3069 ˇ
3070 "});
3071}
3072
3073#[gpui::test]
3074fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3075 init_test(cx, |_| {});
3076
3077 let editor = cx.add_window(|window, cx| {
3078 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3079 let mut editor = build_editor(buffer.clone(), window, cx);
3080 editor.change_selections(None, window, cx, |s| {
3081 s.select_ranges([3..4, 11..12, 19..20])
3082 });
3083 editor
3084 });
3085
3086 _ = editor.update(cx, |editor, window, cx| {
3087 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3088 editor.buffer.update(cx, |buffer, cx| {
3089 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
3090 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3091 });
3092 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
3093
3094 editor.insert("Z", window, cx);
3095 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3096
3097 // The selections are moved after the inserted characters
3098 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
3099 });
3100}
3101
3102#[gpui::test]
3103async fn test_tab(cx: &mut TestAppContext) {
3104 init_test(cx, |settings| {
3105 settings.defaults.tab_size = NonZeroU32::new(3)
3106 });
3107
3108 let mut cx = EditorTestContext::new(cx).await;
3109 cx.set_state(indoc! {"
3110 ˇabˇc
3111 ˇ🏀ˇ🏀ˇefg
3112 dˇ
3113 "});
3114 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3115 cx.assert_editor_state(indoc! {"
3116 ˇab ˇc
3117 ˇ🏀 ˇ🏀 ˇefg
3118 d ˇ
3119 "});
3120
3121 cx.set_state(indoc! {"
3122 a
3123 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3124 "});
3125 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3126 cx.assert_editor_state(indoc! {"
3127 a
3128 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3129 "});
3130}
3131
3132#[gpui::test]
3133async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3134 init_test(cx, |_| {});
3135
3136 let mut cx = EditorTestContext::new(cx).await;
3137 let language = Arc::new(
3138 Language::new(
3139 LanguageConfig::default(),
3140 Some(tree_sitter_rust::LANGUAGE.into()),
3141 )
3142 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3143 .unwrap(),
3144 );
3145 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3146
3147 // test when all cursors are not at suggested indent
3148 // then simply move to their suggested indent location
3149 cx.set_state(indoc! {"
3150 const a: B = (
3151 c(
3152 ˇ
3153 ˇ )
3154 );
3155 "});
3156 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3157 cx.assert_editor_state(indoc! {"
3158 const a: B = (
3159 c(
3160 ˇ
3161 ˇ)
3162 );
3163 "});
3164
3165 // test cursor already at suggested indent not moving when
3166 // other cursors are yet to reach their suggested indents
3167 cx.set_state(indoc! {"
3168 ˇ
3169 const a: B = (
3170 c(
3171 d(
3172 ˇ
3173 )
3174 ˇ
3175 ˇ )
3176 );
3177 "});
3178 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3179 cx.assert_editor_state(indoc! {"
3180 ˇ
3181 const a: B = (
3182 c(
3183 d(
3184 ˇ
3185 )
3186 ˇ
3187 ˇ)
3188 );
3189 "});
3190 // test when all cursors are at suggested indent then tab is inserted
3191 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3192 cx.assert_editor_state(indoc! {"
3193 ˇ
3194 const a: B = (
3195 c(
3196 d(
3197 ˇ
3198 )
3199 ˇ
3200 ˇ)
3201 );
3202 "});
3203
3204 // test when current indent is less than suggested indent,
3205 // we adjust line to match suggested indent and move cursor to it
3206 //
3207 // when no other cursor is at word boundary, all of them should move
3208 cx.set_state(indoc! {"
3209 const a: B = (
3210 c(
3211 d(
3212 ˇ
3213 ˇ )
3214 ˇ )
3215 );
3216 "});
3217 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3218 cx.assert_editor_state(indoc! {"
3219 const a: B = (
3220 c(
3221 d(
3222 ˇ
3223 ˇ)
3224 ˇ)
3225 );
3226 "});
3227
3228 // test when current indent is less than suggested indent,
3229 // we adjust line to match suggested indent and move cursor to it
3230 //
3231 // when some other cursor is at word boundary, it should not move
3232 cx.set_state(indoc! {"
3233 const a: B = (
3234 c(
3235 d(
3236 ˇ
3237 ˇ )
3238 ˇ)
3239 );
3240 "});
3241 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3242 cx.assert_editor_state(indoc! {"
3243 const a: B = (
3244 c(
3245 d(
3246 ˇ
3247 ˇ)
3248 ˇ)
3249 );
3250 "});
3251
3252 // test when current indent is more than suggested indent,
3253 // we just move cursor to current indent instead of suggested indent
3254 //
3255 // when no other cursor is at word boundary, all of them should move
3256 cx.set_state(indoc! {"
3257 const a: B = (
3258 c(
3259 d(
3260 ˇ
3261 ˇ )
3262 ˇ )
3263 );
3264 "});
3265 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3266 cx.assert_editor_state(indoc! {"
3267 const a: B = (
3268 c(
3269 d(
3270 ˇ
3271 ˇ)
3272 ˇ)
3273 );
3274 "});
3275 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3276 cx.assert_editor_state(indoc! {"
3277 const a: B = (
3278 c(
3279 d(
3280 ˇ
3281 ˇ)
3282 ˇ)
3283 );
3284 "});
3285
3286 // test when current indent is more than suggested indent,
3287 // we just move cursor to current indent instead of suggested indent
3288 //
3289 // when some other cursor is at word boundary, it doesn't move
3290 cx.set_state(indoc! {"
3291 const a: B = (
3292 c(
3293 d(
3294 ˇ
3295 ˇ )
3296 ˇ)
3297 );
3298 "});
3299 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3300 cx.assert_editor_state(indoc! {"
3301 const a: B = (
3302 c(
3303 d(
3304 ˇ
3305 ˇ)
3306 ˇ)
3307 );
3308 "});
3309
3310 // handle auto-indent when there are multiple cursors on the same line
3311 cx.set_state(indoc! {"
3312 const a: B = (
3313 c(
3314 ˇ ˇ
3315 ˇ )
3316 );
3317 "});
3318 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3319 cx.assert_editor_state(indoc! {"
3320 const a: B = (
3321 c(
3322 ˇ
3323 ˇ)
3324 );
3325 "});
3326}
3327
3328#[gpui::test]
3329async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3330 init_test(cx, |settings| {
3331 settings.defaults.tab_size = NonZeroU32::new(3)
3332 });
3333
3334 let mut cx = EditorTestContext::new(cx).await;
3335 cx.set_state(indoc! {"
3336 ˇ
3337 \t ˇ
3338 \t ˇ
3339 \t ˇ
3340 \t \t\t \t \t\t \t\t \t \t ˇ
3341 "});
3342
3343 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3344 cx.assert_editor_state(indoc! {"
3345 ˇ
3346 \t ˇ
3347 \t ˇ
3348 \t ˇ
3349 \t \t\t \t \t\t \t\t \t \t ˇ
3350 "});
3351}
3352
3353#[gpui::test]
3354async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3355 init_test(cx, |settings| {
3356 settings.defaults.tab_size = NonZeroU32::new(4)
3357 });
3358
3359 let language = Arc::new(
3360 Language::new(
3361 LanguageConfig::default(),
3362 Some(tree_sitter_rust::LANGUAGE.into()),
3363 )
3364 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3365 .unwrap(),
3366 );
3367
3368 let mut cx = EditorTestContext::new(cx).await;
3369 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3370 cx.set_state(indoc! {"
3371 fn a() {
3372 if b {
3373 \t ˇc
3374 }
3375 }
3376 "});
3377
3378 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3379 cx.assert_editor_state(indoc! {"
3380 fn a() {
3381 if b {
3382 ˇc
3383 }
3384 }
3385 "});
3386}
3387
3388#[gpui::test]
3389async fn test_indent_outdent(cx: &mut TestAppContext) {
3390 init_test(cx, |settings| {
3391 settings.defaults.tab_size = NonZeroU32::new(4);
3392 });
3393
3394 let mut cx = EditorTestContext::new(cx).await;
3395
3396 cx.set_state(indoc! {"
3397 «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 «oneˇ» «twoˇ»
3404 three
3405 four
3406 "});
3407
3408 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3409 cx.assert_editor_state(indoc! {"
3410 «oneˇ» «twoˇ»
3411 three
3412 four
3413 "});
3414
3415 // select across line ending
3416 cx.set_state(indoc! {"
3417 one two
3418 t«hree
3419 ˇ» four
3420 "});
3421 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3422 cx.assert_editor_state(indoc! {"
3423 one two
3424 t«hree
3425 ˇ» four
3426 "});
3427
3428 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3429 cx.assert_editor_state(indoc! {"
3430 one two
3431 t«hree
3432 ˇ» four
3433 "});
3434
3435 // Ensure that indenting/outdenting works when the cursor is at column 0.
3436 cx.set_state(indoc! {"
3437 one two
3438 ˇthree
3439 four
3440 "});
3441 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3442 cx.assert_editor_state(indoc! {"
3443 one two
3444 ˇthree
3445 four
3446 "});
3447
3448 cx.set_state(indoc! {"
3449 one two
3450 ˇ three
3451 four
3452 "});
3453 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3454 cx.assert_editor_state(indoc! {"
3455 one two
3456 ˇthree
3457 four
3458 "});
3459}
3460
3461#[gpui::test]
3462async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
3463 init_test(cx, |settings| {
3464 settings.defaults.hard_tabs = Some(true);
3465 });
3466
3467 let mut cx = EditorTestContext::new(cx).await;
3468
3469 // select two ranges on one line
3470 cx.set_state(indoc! {"
3471 «oneˇ» «twoˇ»
3472 three
3473 four
3474 "});
3475 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3476 cx.assert_editor_state(indoc! {"
3477 \t«oneˇ» «twoˇ»
3478 three
3479 four
3480 "});
3481 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3482 cx.assert_editor_state(indoc! {"
3483 \t\t«oneˇ» «twoˇ»
3484 three
3485 four
3486 "});
3487 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3488 cx.assert_editor_state(indoc! {"
3489 \t«oneˇ» «twoˇ»
3490 three
3491 four
3492 "});
3493 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3494 cx.assert_editor_state(indoc! {"
3495 «oneˇ» «twoˇ»
3496 three
3497 four
3498 "});
3499
3500 // select across a line ending
3501 cx.set_state(indoc! {"
3502 one two
3503 t«hree
3504 ˇ»four
3505 "});
3506 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3507 cx.assert_editor_state(indoc! {"
3508 one two
3509 \tt«hree
3510 ˇ»four
3511 "});
3512 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3513 cx.assert_editor_state(indoc! {"
3514 one two
3515 \t\tt«hree
3516 ˇ»four
3517 "});
3518 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3519 cx.assert_editor_state(indoc! {"
3520 one two
3521 \tt«hree
3522 ˇ»four
3523 "});
3524 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3525 cx.assert_editor_state(indoc! {"
3526 one two
3527 t«hree
3528 ˇ»four
3529 "});
3530
3531 // Ensure that indenting/outdenting works when the cursor is at column 0.
3532 cx.set_state(indoc! {"
3533 one two
3534 ˇthree
3535 four
3536 "});
3537 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3538 cx.assert_editor_state(indoc! {"
3539 one two
3540 ˇthree
3541 four
3542 "});
3543 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3544 cx.assert_editor_state(indoc! {"
3545 one two
3546 \tˇthree
3547 four
3548 "});
3549 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3550 cx.assert_editor_state(indoc! {"
3551 one two
3552 ˇthree
3553 four
3554 "});
3555}
3556
3557#[gpui::test]
3558fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
3559 init_test(cx, |settings| {
3560 settings.languages.extend([
3561 (
3562 "TOML".into(),
3563 LanguageSettingsContent {
3564 tab_size: NonZeroU32::new(2),
3565 ..Default::default()
3566 },
3567 ),
3568 (
3569 "Rust".into(),
3570 LanguageSettingsContent {
3571 tab_size: NonZeroU32::new(4),
3572 ..Default::default()
3573 },
3574 ),
3575 ]);
3576 });
3577
3578 let toml_language = Arc::new(Language::new(
3579 LanguageConfig {
3580 name: "TOML".into(),
3581 ..Default::default()
3582 },
3583 None,
3584 ));
3585 let rust_language = Arc::new(Language::new(
3586 LanguageConfig {
3587 name: "Rust".into(),
3588 ..Default::default()
3589 },
3590 None,
3591 ));
3592
3593 let toml_buffer =
3594 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
3595 let rust_buffer =
3596 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
3597 let multibuffer = cx.new(|cx| {
3598 let mut multibuffer = MultiBuffer::new(ReadWrite);
3599 multibuffer.push_excerpts(
3600 toml_buffer.clone(),
3601 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
3602 cx,
3603 );
3604 multibuffer.push_excerpts(
3605 rust_buffer.clone(),
3606 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
3607 cx,
3608 );
3609 multibuffer
3610 });
3611
3612 cx.add_window(|window, cx| {
3613 let mut editor = build_editor(multibuffer, window, cx);
3614
3615 assert_eq!(
3616 editor.text(cx),
3617 indoc! {"
3618 a = 1
3619 b = 2
3620
3621 const c: usize = 3;
3622 "}
3623 );
3624
3625 select_ranges(
3626 &mut editor,
3627 indoc! {"
3628 «aˇ» = 1
3629 b = 2
3630
3631 «const c:ˇ» usize = 3;
3632 "},
3633 window,
3634 cx,
3635 );
3636
3637 editor.tab(&Tab, window, cx);
3638 assert_text_with_selections(
3639 &mut editor,
3640 indoc! {"
3641 «aˇ» = 1
3642 b = 2
3643
3644 «const c:ˇ» usize = 3;
3645 "},
3646 cx,
3647 );
3648 editor.backtab(&Backtab, window, cx);
3649 assert_text_with_selections(
3650 &mut editor,
3651 indoc! {"
3652 «aˇ» = 1
3653 b = 2
3654
3655 «const c:ˇ» usize = 3;
3656 "},
3657 cx,
3658 );
3659
3660 editor
3661 });
3662}
3663
3664#[gpui::test]
3665async fn test_backspace(cx: &mut TestAppContext) {
3666 init_test(cx, |_| {});
3667
3668 let mut cx = EditorTestContext::new(cx).await;
3669
3670 // Basic backspace
3671 cx.set_state(indoc! {"
3672 onˇe two three
3673 fou«rˇ» five six
3674 seven «ˇeight nine
3675 »ten
3676 "});
3677 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3678 cx.assert_editor_state(indoc! {"
3679 oˇe two three
3680 fouˇ five six
3681 seven ˇten
3682 "});
3683
3684 // Test backspace inside and around indents
3685 cx.set_state(indoc! {"
3686 zero
3687 ˇone
3688 ˇtwo
3689 ˇ ˇ ˇ three
3690 ˇ ˇ four
3691 "});
3692 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3693 cx.assert_editor_state(indoc! {"
3694 zero
3695 ˇone
3696 ˇtwo
3697 ˇ threeˇ four
3698 "});
3699}
3700
3701#[gpui::test]
3702async fn test_delete(cx: &mut TestAppContext) {
3703 init_test(cx, |_| {});
3704
3705 let mut cx = EditorTestContext::new(cx).await;
3706 cx.set_state(indoc! {"
3707 onˇe two three
3708 fou«rˇ» five six
3709 seven «ˇeight nine
3710 »ten
3711 "});
3712 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
3713 cx.assert_editor_state(indoc! {"
3714 onˇ two three
3715 fouˇ five six
3716 seven ˇten
3717 "});
3718}
3719
3720#[gpui::test]
3721fn test_delete_line(cx: &mut TestAppContext) {
3722 init_test(cx, |_| {});
3723
3724 let editor = cx.add_window(|window, cx| {
3725 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3726 build_editor(buffer, window, cx)
3727 });
3728 _ = editor.update(cx, |editor, window, cx| {
3729 editor.change_selections(None, window, cx, |s| {
3730 s.select_display_ranges([
3731 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
3732 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3733 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3734 ])
3735 });
3736 editor.delete_line(&DeleteLine, window, cx);
3737 assert_eq!(editor.display_text(cx), "ghi");
3738 assert_eq!(
3739 editor.selections.display_ranges(cx),
3740 vec![
3741 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
3742 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
3743 ]
3744 );
3745 });
3746
3747 let editor = cx.add_window(|window, cx| {
3748 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3749 build_editor(buffer, window, cx)
3750 });
3751 _ = editor.update(cx, |editor, window, cx| {
3752 editor.change_selections(None, window, cx, |s| {
3753 s.select_display_ranges([
3754 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
3755 ])
3756 });
3757 editor.delete_line(&DeleteLine, window, cx);
3758 assert_eq!(editor.display_text(cx), "ghi\n");
3759 assert_eq!(
3760 editor.selections.display_ranges(cx),
3761 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
3762 );
3763 });
3764}
3765
3766#[gpui::test]
3767fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
3768 init_test(cx, |_| {});
3769
3770 cx.add_window(|window, cx| {
3771 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3772 let mut editor = build_editor(buffer.clone(), window, cx);
3773 let buffer = buffer.read(cx).as_singleton().unwrap();
3774
3775 assert_eq!(
3776 editor.selections.ranges::<Point>(cx),
3777 &[Point::new(0, 0)..Point::new(0, 0)]
3778 );
3779
3780 // When on single line, replace newline at end by space
3781 editor.join_lines(&JoinLines, window, cx);
3782 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3783 assert_eq!(
3784 editor.selections.ranges::<Point>(cx),
3785 &[Point::new(0, 3)..Point::new(0, 3)]
3786 );
3787
3788 // When multiple lines are selected, remove newlines that are spanned by the selection
3789 editor.change_selections(None, window, cx, |s| {
3790 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
3791 });
3792 editor.join_lines(&JoinLines, window, cx);
3793 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
3794 assert_eq!(
3795 editor.selections.ranges::<Point>(cx),
3796 &[Point::new(0, 11)..Point::new(0, 11)]
3797 );
3798
3799 // Undo should be transactional
3800 editor.undo(&Undo, window, cx);
3801 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3802 assert_eq!(
3803 editor.selections.ranges::<Point>(cx),
3804 &[Point::new(0, 5)..Point::new(2, 2)]
3805 );
3806
3807 // When joining an empty line don't insert a space
3808 editor.change_selections(None, window, cx, |s| {
3809 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
3810 });
3811 editor.join_lines(&JoinLines, window, cx);
3812 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
3813 assert_eq!(
3814 editor.selections.ranges::<Point>(cx),
3815 [Point::new(2, 3)..Point::new(2, 3)]
3816 );
3817
3818 // We can remove trailing newlines
3819 editor.join_lines(&JoinLines, window, cx);
3820 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3821 assert_eq!(
3822 editor.selections.ranges::<Point>(cx),
3823 [Point::new(2, 3)..Point::new(2, 3)]
3824 );
3825
3826 // We don't blow up on the last line
3827 editor.join_lines(&JoinLines, window, cx);
3828 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3829 assert_eq!(
3830 editor.selections.ranges::<Point>(cx),
3831 [Point::new(2, 3)..Point::new(2, 3)]
3832 );
3833
3834 // reset to test indentation
3835 editor.buffer.update(cx, |buffer, cx| {
3836 buffer.edit(
3837 [
3838 (Point::new(1, 0)..Point::new(1, 2), " "),
3839 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
3840 ],
3841 None,
3842 cx,
3843 )
3844 });
3845
3846 // We remove any leading spaces
3847 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
3848 editor.change_selections(None, window, cx, |s| {
3849 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
3850 });
3851 editor.join_lines(&JoinLines, window, cx);
3852 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
3853
3854 // We don't insert a space for a line containing only spaces
3855 editor.join_lines(&JoinLines, window, cx);
3856 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
3857
3858 // We ignore any leading tabs
3859 editor.join_lines(&JoinLines, window, cx);
3860 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
3861
3862 editor
3863 });
3864}
3865
3866#[gpui::test]
3867fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
3868 init_test(cx, |_| {});
3869
3870 cx.add_window(|window, cx| {
3871 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3872 let mut editor = build_editor(buffer.clone(), window, cx);
3873 let buffer = buffer.read(cx).as_singleton().unwrap();
3874
3875 editor.change_selections(None, window, cx, |s| {
3876 s.select_ranges([
3877 Point::new(0, 2)..Point::new(1, 1),
3878 Point::new(1, 2)..Point::new(1, 2),
3879 Point::new(3, 1)..Point::new(3, 2),
3880 ])
3881 });
3882
3883 editor.join_lines(&JoinLines, window, cx);
3884 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
3885
3886 assert_eq!(
3887 editor.selections.ranges::<Point>(cx),
3888 [
3889 Point::new(0, 7)..Point::new(0, 7),
3890 Point::new(1, 3)..Point::new(1, 3)
3891 ]
3892 );
3893 editor
3894 });
3895}
3896
3897#[gpui::test]
3898async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
3899 init_test(cx, |_| {});
3900
3901 let mut cx = EditorTestContext::new(cx).await;
3902
3903 let diff_base = r#"
3904 Line 0
3905 Line 1
3906 Line 2
3907 Line 3
3908 "#
3909 .unindent();
3910
3911 cx.set_state(
3912 &r#"
3913 ˇLine 0
3914 Line 1
3915 Line 2
3916 Line 3
3917 "#
3918 .unindent(),
3919 );
3920
3921 cx.set_head_text(&diff_base);
3922 executor.run_until_parked();
3923
3924 // Join lines
3925 cx.update_editor(|editor, window, cx| {
3926 editor.join_lines(&JoinLines, window, cx);
3927 });
3928 executor.run_until_parked();
3929
3930 cx.assert_editor_state(
3931 &r#"
3932 Line 0ˇ Line 1
3933 Line 2
3934 Line 3
3935 "#
3936 .unindent(),
3937 );
3938 // Join again
3939 cx.update_editor(|editor, window, cx| {
3940 editor.join_lines(&JoinLines, window, cx);
3941 });
3942 executor.run_until_parked();
3943
3944 cx.assert_editor_state(
3945 &r#"
3946 Line 0 Line 1ˇ Line 2
3947 Line 3
3948 "#
3949 .unindent(),
3950 );
3951}
3952
3953#[gpui::test]
3954async fn test_custom_newlines_cause_no_false_positive_diffs(
3955 executor: BackgroundExecutor,
3956 cx: &mut TestAppContext,
3957) {
3958 init_test(cx, |_| {});
3959 let mut cx = EditorTestContext::new(cx).await;
3960 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
3961 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
3962 executor.run_until_parked();
3963
3964 cx.update_editor(|editor, window, cx| {
3965 let snapshot = editor.snapshot(window, cx);
3966 assert_eq!(
3967 snapshot
3968 .buffer_snapshot
3969 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
3970 .collect::<Vec<_>>(),
3971 Vec::new(),
3972 "Should not have any diffs for files with custom newlines"
3973 );
3974 });
3975}
3976
3977#[gpui::test]
3978async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) {
3979 init_test(cx, |_| {});
3980
3981 let mut cx = EditorTestContext::new(cx).await;
3982
3983 // Test sort_lines_case_insensitive()
3984 cx.set_state(indoc! {"
3985 «z
3986 y
3987 x
3988 Z
3989 Y
3990 Xˇ»
3991 "});
3992 cx.update_editor(|e, window, cx| {
3993 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
3994 });
3995 cx.assert_editor_state(indoc! {"
3996 «x
3997 X
3998 y
3999 Y
4000 z
4001 Zˇ»
4002 "});
4003
4004 // Test reverse_lines()
4005 cx.set_state(indoc! {"
4006 «5
4007 4
4008 3
4009 2
4010 1ˇ»
4011 "});
4012 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4013 cx.assert_editor_state(indoc! {"
4014 «1
4015 2
4016 3
4017 4
4018 5ˇ»
4019 "});
4020
4021 // Skip testing shuffle_line()
4022
4023 // From here on out, test more complex cases of manipulate_lines() with a single driver method: sort_lines_case_sensitive()
4024 // Since all methods calling manipulate_lines() are doing the exact same general thing (reordering lines)
4025
4026 // Don't manipulate when cursor is on single line, but expand the selection
4027 cx.set_state(indoc! {"
4028 ddˇdd
4029 ccc
4030 bb
4031 a
4032 "});
4033 cx.update_editor(|e, window, cx| {
4034 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4035 });
4036 cx.assert_editor_state(indoc! {"
4037 «ddddˇ»
4038 ccc
4039 bb
4040 a
4041 "});
4042
4043 // Basic manipulate case
4044 // Start selection moves to column 0
4045 // End of selection shrinks to fit shorter line
4046 cx.set_state(indoc! {"
4047 dd«d
4048 ccc
4049 bb
4050 aaaaaˇ»
4051 "});
4052 cx.update_editor(|e, window, cx| {
4053 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4054 });
4055 cx.assert_editor_state(indoc! {"
4056 «aaaaa
4057 bb
4058 ccc
4059 dddˇ»
4060 "});
4061
4062 // Manipulate case with newlines
4063 cx.set_state(indoc! {"
4064 dd«d
4065 ccc
4066
4067 bb
4068 aaaaa
4069
4070 ˇ»
4071 "});
4072 cx.update_editor(|e, window, cx| {
4073 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4074 });
4075 cx.assert_editor_state(indoc! {"
4076 «
4077
4078 aaaaa
4079 bb
4080 ccc
4081 dddˇ»
4082
4083 "});
4084
4085 // Adding new line
4086 cx.set_state(indoc! {"
4087 aa«a
4088 bbˇ»b
4089 "});
4090 cx.update_editor(|e, window, cx| {
4091 e.manipulate_lines(window, cx, |lines| lines.push("added_line"))
4092 });
4093 cx.assert_editor_state(indoc! {"
4094 «aaa
4095 bbb
4096 added_lineˇ»
4097 "});
4098
4099 // Removing line
4100 cx.set_state(indoc! {"
4101 aa«a
4102 bbbˇ»
4103 "});
4104 cx.update_editor(|e, window, cx| {
4105 e.manipulate_lines(window, cx, |lines| {
4106 lines.pop();
4107 })
4108 });
4109 cx.assert_editor_state(indoc! {"
4110 «aaaˇ»
4111 "});
4112
4113 // Removing all lines
4114 cx.set_state(indoc! {"
4115 aa«a
4116 bbbˇ»
4117 "});
4118 cx.update_editor(|e, window, cx| {
4119 e.manipulate_lines(window, cx, |lines| {
4120 lines.drain(..);
4121 })
4122 });
4123 cx.assert_editor_state(indoc! {"
4124 ˇ
4125 "});
4126}
4127
4128#[gpui::test]
4129async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4130 init_test(cx, |_| {});
4131
4132 let mut cx = EditorTestContext::new(cx).await;
4133
4134 // Consider continuous selection as single selection
4135 cx.set_state(indoc! {"
4136 Aaa«aa
4137 cˇ»c«c
4138 bb
4139 aaaˇ»aa
4140 "});
4141 cx.update_editor(|e, window, cx| {
4142 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4143 });
4144 cx.assert_editor_state(indoc! {"
4145 «Aaaaa
4146 ccc
4147 bb
4148 aaaaaˇ»
4149 "});
4150
4151 cx.set_state(indoc! {"
4152 Aaa«aa
4153 cˇ»c«c
4154 bb
4155 aaaˇ»aa
4156 "});
4157 cx.update_editor(|e, window, cx| {
4158 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4159 });
4160 cx.assert_editor_state(indoc! {"
4161 «Aaaaa
4162 ccc
4163 bbˇ»
4164 "});
4165
4166 // Consider non continuous selection as distinct dedup operations
4167 cx.set_state(indoc! {"
4168 «aaaaa
4169 bb
4170 aaaaa
4171 aaaaaˇ»
4172
4173 aaa«aaˇ»
4174 "});
4175 cx.update_editor(|e, window, cx| {
4176 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4177 });
4178 cx.assert_editor_state(indoc! {"
4179 «aaaaa
4180 bbˇ»
4181
4182 «aaaaaˇ»
4183 "});
4184}
4185
4186#[gpui::test]
4187async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4188 init_test(cx, |_| {});
4189
4190 let mut cx = EditorTestContext::new(cx).await;
4191
4192 cx.set_state(indoc! {"
4193 «Aaa
4194 aAa
4195 Aaaˇ»
4196 "});
4197 cx.update_editor(|e, window, cx| {
4198 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4199 });
4200 cx.assert_editor_state(indoc! {"
4201 «Aaa
4202 aAaˇ»
4203 "});
4204
4205 cx.set_state(indoc! {"
4206 «Aaa
4207 aAa
4208 aaAˇ»
4209 "});
4210 cx.update_editor(|e, window, cx| {
4211 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4212 });
4213 cx.assert_editor_state(indoc! {"
4214 «Aaaˇ»
4215 "});
4216}
4217
4218#[gpui::test]
4219async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) {
4220 init_test(cx, |_| {});
4221
4222 let mut cx = EditorTestContext::new(cx).await;
4223
4224 // Manipulate with multiple selections on a single line
4225 cx.set_state(indoc! {"
4226 dd«dd
4227 cˇ»c«c
4228 bb
4229 aaaˇ»aa
4230 "});
4231 cx.update_editor(|e, window, cx| {
4232 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4233 });
4234 cx.assert_editor_state(indoc! {"
4235 «aaaaa
4236 bb
4237 ccc
4238 ddddˇ»
4239 "});
4240
4241 // Manipulate with multiple disjoin selections
4242 cx.set_state(indoc! {"
4243 5«
4244 4
4245 3
4246 2
4247 1ˇ»
4248
4249 dd«dd
4250 ccc
4251 bb
4252 aaaˇ»aa
4253 "});
4254 cx.update_editor(|e, window, cx| {
4255 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4256 });
4257 cx.assert_editor_state(indoc! {"
4258 «1
4259 2
4260 3
4261 4
4262 5ˇ»
4263
4264 «aaaaa
4265 bb
4266 ccc
4267 ddddˇ»
4268 "});
4269
4270 // Adding lines on each selection
4271 cx.set_state(indoc! {"
4272 2«
4273 1ˇ»
4274
4275 bb«bb
4276 aaaˇ»aa
4277 "});
4278 cx.update_editor(|e, window, cx| {
4279 e.manipulate_lines(window, cx, |lines| lines.push("added line"))
4280 });
4281 cx.assert_editor_state(indoc! {"
4282 «2
4283 1
4284 added lineˇ»
4285
4286 «bbbb
4287 aaaaa
4288 added lineˇ»
4289 "});
4290
4291 // Removing lines on each selection
4292 cx.set_state(indoc! {"
4293 2«
4294 1ˇ»
4295
4296 bb«bb
4297 aaaˇ»aa
4298 "});
4299 cx.update_editor(|e, window, cx| {
4300 e.manipulate_lines(window, cx, |lines| {
4301 lines.pop();
4302 })
4303 });
4304 cx.assert_editor_state(indoc! {"
4305 «2ˇ»
4306
4307 «bbbbˇ»
4308 "});
4309}
4310
4311#[gpui::test]
4312async fn test_toggle_case(cx: &mut TestAppContext) {
4313 init_test(cx, |_| {});
4314
4315 let mut cx = EditorTestContext::new(cx).await;
4316
4317 // If all lower case -> upper case
4318 cx.set_state(indoc! {"
4319 «hello worldˇ»
4320 "});
4321 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4322 cx.assert_editor_state(indoc! {"
4323 «HELLO WORLDˇ»
4324 "});
4325
4326 // If all upper case -> lower case
4327 cx.set_state(indoc! {"
4328 «HELLO WORLDˇ»
4329 "});
4330 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4331 cx.assert_editor_state(indoc! {"
4332 «hello worldˇ»
4333 "});
4334
4335 // If any upper case characters are identified -> lower case
4336 // This matches JetBrains IDEs
4337 cx.set_state(indoc! {"
4338 «hEllo worldˇ»
4339 "});
4340 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4341 cx.assert_editor_state(indoc! {"
4342 «hello worldˇ»
4343 "});
4344}
4345
4346#[gpui::test]
4347async fn test_manipulate_text(cx: &mut TestAppContext) {
4348 init_test(cx, |_| {});
4349
4350 let mut cx = EditorTestContext::new(cx).await;
4351
4352 // Test convert_to_upper_case()
4353 cx.set_state(indoc! {"
4354 «hello worldˇ»
4355 "});
4356 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4357 cx.assert_editor_state(indoc! {"
4358 «HELLO WORLDˇ»
4359 "});
4360
4361 // Test convert_to_lower_case()
4362 cx.set_state(indoc! {"
4363 «HELLO WORLDˇ»
4364 "});
4365 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
4366 cx.assert_editor_state(indoc! {"
4367 «hello worldˇ»
4368 "});
4369
4370 // Test multiple line, single selection case
4371 cx.set_state(indoc! {"
4372 «The quick brown
4373 fox jumps over
4374 the lazy dogˇ»
4375 "});
4376 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
4377 cx.assert_editor_state(indoc! {"
4378 «The Quick Brown
4379 Fox Jumps Over
4380 The Lazy Dogˇ»
4381 "});
4382
4383 // Test multiple line, single selection case
4384 cx.set_state(indoc! {"
4385 «The quick brown
4386 fox jumps over
4387 the lazy dogˇ»
4388 "});
4389 cx.update_editor(|e, window, cx| {
4390 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
4391 });
4392 cx.assert_editor_state(indoc! {"
4393 «TheQuickBrown
4394 FoxJumpsOver
4395 TheLazyDogˇ»
4396 "});
4397
4398 // From here on out, test more complex cases of manipulate_text()
4399
4400 // Test no selection case - should affect words cursors are in
4401 // Cursor at beginning, middle, and end of word
4402 cx.set_state(indoc! {"
4403 ˇhello big beauˇtiful worldˇ
4404 "});
4405 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4406 cx.assert_editor_state(indoc! {"
4407 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
4408 "});
4409
4410 // Test multiple selections on a single line and across multiple lines
4411 cx.set_state(indoc! {"
4412 «Theˇ» quick «brown
4413 foxˇ» jumps «overˇ»
4414 the «lazyˇ» dog
4415 "});
4416 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4417 cx.assert_editor_state(indoc! {"
4418 «THEˇ» quick «BROWN
4419 FOXˇ» jumps «OVERˇ»
4420 the «LAZYˇ» dog
4421 "});
4422
4423 // Test case where text length grows
4424 cx.set_state(indoc! {"
4425 «tschüߡ»
4426 "});
4427 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4428 cx.assert_editor_state(indoc! {"
4429 «TSCHÜSSˇ»
4430 "});
4431
4432 // Test to make sure we don't crash when text shrinks
4433 cx.set_state(indoc! {"
4434 aaa_bbbˇ
4435 "});
4436 cx.update_editor(|e, window, cx| {
4437 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4438 });
4439 cx.assert_editor_state(indoc! {"
4440 «aaaBbbˇ»
4441 "});
4442
4443 // Test to make sure we all aware of the fact that each word can grow and shrink
4444 // Final selections should be aware of this fact
4445 cx.set_state(indoc! {"
4446 aaa_bˇbb bbˇb_ccc ˇccc_ddd
4447 "});
4448 cx.update_editor(|e, window, cx| {
4449 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4450 });
4451 cx.assert_editor_state(indoc! {"
4452 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
4453 "});
4454
4455 cx.set_state(indoc! {"
4456 «hElLo, WoRld!ˇ»
4457 "});
4458 cx.update_editor(|e, window, cx| {
4459 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
4460 });
4461 cx.assert_editor_state(indoc! {"
4462 «HeLlO, wOrLD!ˇ»
4463 "});
4464}
4465
4466#[gpui::test]
4467fn test_duplicate_line(cx: &mut TestAppContext) {
4468 init_test(cx, |_| {});
4469
4470 let editor = cx.add_window(|window, cx| {
4471 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4472 build_editor(buffer, window, cx)
4473 });
4474 _ = editor.update(cx, |editor, window, cx| {
4475 editor.change_selections(None, window, cx, |s| {
4476 s.select_display_ranges([
4477 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4478 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4479 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4480 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4481 ])
4482 });
4483 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4484 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4485 assert_eq!(
4486 editor.selections.display_ranges(cx),
4487 vec![
4488 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4489 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
4490 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4491 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4492 ]
4493 );
4494 });
4495
4496 let editor = cx.add_window(|window, cx| {
4497 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4498 build_editor(buffer, window, cx)
4499 });
4500 _ = editor.update(cx, |editor, window, cx| {
4501 editor.change_selections(None, window, cx, |s| {
4502 s.select_display_ranges([
4503 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4504 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4505 ])
4506 });
4507 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4508 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4509 assert_eq!(
4510 editor.selections.display_ranges(cx),
4511 vec![
4512 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
4513 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
4514 ]
4515 );
4516 });
4517
4518 // With `move_upwards` the selections stay in place, except for
4519 // the lines inserted above them
4520 let editor = cx.add_window(|window, cx| {
4521 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4522 build_editor(buffer, window, cx)
4523 });
4524 _ = editor.update(cx, |editor, window, cx| {
4525 editor.change_selections(None, window, cx, |s| {
4526 s.select_display_ranges([
4527 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4528 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4529 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4530 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4531 ])
4532 });
4533 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4534 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4535 assert_eq!(
4536 editor.selections.display_ranges(cx),
4537 vec![
4538 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4539 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4540 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
4541 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4542 ]
4543 );
4544 });
4545
4546 let editor = cx.add_window(|window, cx| {
4547 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4548 build_editor(buffer, window, cx)
4549 });
4550 _ = editor.update(cx, |editor, window, cx| {
4551 editor.change_selections(None, window, cx, |s| {
4552 s.select_display_ranges([
4553 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4554 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4555 ])
4556 });
4557 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4558 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4559 assert_eq!(
4560 editor.selections.display_ranges(cx),
4561 vec![
4562 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4563 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4564 ]
4565 );
4566 });
4567
4568 let editor = cx.add_window(|window, cx| {
4569 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4570 build_editor(buffer, window, cx)
4571 });
4572 _ = editor.update(cx, |editor, window, cx| {
4573 editor.change_selections(None, window, cx, |s| {
4574 s.select_display_ranges([
4575 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4576 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4577 ])
4578 });
4579 editor.duplicate_selection(&DuplicateSelection, window, cx);
4580 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
4581 assert_eq!(
4582 editor.selections.display_ranges(cx),
4583 vec![
4584 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4585 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
4586 ]
4587 );
4588 });
4589}
4590
4591#[gpui::test]
4592fn test_move_line_up_down(cx: &mut TestAppContext) {
4593 init_test(cx, |_| {});
4594
4595 let editor = cx.add_window(|window, cx| {
4596 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4597 build_editor(buffer, window, cx)
4598 });
4599 _ = editor.update(cx, |editor, window, cx| {
4600 editor.fold_creases(
4601 vec![
4602 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
4603 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
4604 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
4605 ],
4606 true,
4607 window,
4608 cx,
4609 );
4610 editor.change_selections(None, window, cx, |s| {
4611 s.select_display_ranges([
4612 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4613 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4614 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4615 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
4616 ])
4617 });
4618 assert_eq!(
4619 editor.display_text(cx),
4620 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
4621 );
4622
4623 editor.move_line_up(&MoveLineUp, window, cx);
4624 assert_eq!(
4625 editor.display_text(cx),
4626 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
4627 );
4628 assert_eq!(
4629 editor.selections.display_ranges(cx),
4630 vec![
4631 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4632 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4633 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4634 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4635 ]
4636 );
4637 });
4638
4639 _ = editor.update(cx, |editor, window, cx| {
4640 editor.move_line_down(&MoveLineDown, window, cx);
4641 assert_eq!(
4642 editor.display_text(cx),
4643 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
4644 );
4645 assert_eq!(
4646 editor.selections.display_ranges(cx),
4647 vec![
4648 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4649 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4650 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4651 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4652 ]
4653 );
4654 });
4655
4656 _ = editor.update(cx, |editor, window, cx| {
4657 editor.move_line_down(&MoveLineDown, window, cx);
4658 assert_eq!(
4659 editor.display_text(cx),
4660 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
4661 );
4662 assert_eq!(
4663 editor.selections.display_ranges(cx),
4664 vec![
4665 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4666 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4667 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4668 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4669 ]
4670 );
4671 });
4672
4673 _ = editor.update(cx, |editor, window, cx| {
4674 editor.move_line_up(&MoveLineUp, window, cx);
4675 assert_eq!(
4676 editor.display_text(cx),
4677 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
4678 );
4679 assert_eq!(
4680 editor.selections.display_ranges(cx),
4681 vec![
4682 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4683 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4684 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4685 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4686 ]
4687 );
4688 });
4689}
4690
4691#[gpui::test]
4692fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
4693 init_test(cx, |_| {});
4694
4695 let editor = cx.add_window(|window, cx| {
4696 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4697 build_editor(buffer, window, cx)
4698 });
4699 _ = editor.update(cx, |editor, window, cx| {
4700 let snapshot = editor.buffer.read(cx).snapshot(cx);
4701 editor.insert_blocks(
4702 [BlockProperties {
4703 style: BlockStyle::Fixed,
4704 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
4705 height: Some(1),
4706 render: Arc::new(|_| div().into_any()),
4707 priority: 0,
4708 render_in_minimap: true,
4709 }],
4710 Some(Autoscroll::fit()),
4711 cx,
4712 );
4713 editor.change_selections(None, window, cx, |s| {
4714 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
4715 });
4716 editor.move_line_down(&MoveLineDown, window, cx);
4717 });
4718}
4719
4720#[gpui::test]
4721async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
4722 init_test(cx, |_| {});
4723
4724 let mut cx = EditorTestContext::new(cx).await;
4725 cx.set_state(
4726 &"
4727 ˇzero
4728 one
4729 two
4730 three
4731 four
4732 five
4733 "
4734 .unindent(),
4735 );
4736
4737 // Create a four-line block that replaces three lines of text.
4738 cx.update_editor(|editor, window, cx| {
4739 let snapshot = editor.snapshot(window, cx);
4740 let snapshot = &snapshot.buffer_snapshot;
4741 let placement = BlockPlacement::Replace(
4742 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
4743 );
4744 editor.insert_blocks(
4745 [BlockProperties {
4746 placement,
4747 height: Some(4),
4748 style: BlockStyle::Sticky,
4749 render: Arc::new(|_| gpui::div().into_any_element()),
4750 priority: 0,
4751 render_in_minimap: true,
4752 }],
4753 None,
4754 cx,
4755 );
4756 });
4757
4758 // Move down so that the cursor touches the block.
4759 cx.update_editor(|editor, window, cx| {
4760 editor.move_down(&Default::default(), window, cx);
4761 });
4762 cx.assert_editor_state(
4763 &"
4764 zero
4765 «one
4766 two
4767 threeˇ»
4768 four
4769 five
4770 "
4771 .unindent(),
4772 );
4773
4774 // Move down past the block.
4775 cx.update_editor(|editor, window, cx| {
4776 editor.move_down(&Default::default(), window, cx);
4777 });
4778 cx.assert_editor_state(
4779 &"
4780 zero
4781 one
4782 two
4783 three
4784 ˇfour
4785 five
4786 "
4787 .unindent(),
4788 );
4789}
4790
4791#[gpui::test]
4792fn test_transpose(cx: &mut TestAppContext) {
4793 init_test(cx, |_| {});
4794
4795 _ = cx.add_window(|window, cx| {
4796 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
4797 editor.set_style(EditorStyle::default(), window, cx);
4798 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
4799 editor.transpose(&Default::default(), window, cx);
4800 assert_eq!(editor.text(cx), "bac");
4801 assert_eq!(editor.selections.ranges(cx), [2..2]);
4802
4803 editor.transpose(&Default::default(), window, cx);
4804 assert_eq!(editor.text(cx), "bca");
4805 assert_eq!(editor.selections.ranges(cx), [3..3]);
4806
4807 editor.transpose(&Default::default(), window, cx);
4808 assert_eq!(editor.text(cx), "bac");
4809 assert_eq!(editor.selections.ranges(cx), [3..3]);
4810
4811 editor
4812 });
4813
4814 _ = cx.add_window(|window, cx| {
4815 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4816 editor.set_style(EditorStyle::default(), window, cx);
4817 editor.change_selections(None, window, cx, |s| s.select_ranges([3..3]));
4818 editor.transpose(&Default::default(), window, cx);
4819 assert_eq!(editor.text(cx), "acb\nde");
4820 assert_eq!(editor.selections.ranges(cx), [3..3]);
4821
4822 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4823 editor.transpose(&Default::default(), window, cx);
4824 assert_eq!(editor.text(cx), "acbd\ne");
4825 assert_eq!(editor.selections.ranges(cx), [5..5]);
4826
4827 editor.transpose(&Default::default(), window, cx);
4828 assert_eq!(editor.text(cx), "acbde\n");
4829 assert_eq!(editor.selections.ranges(cx), [6..6]);
4830
4831 editor.transpose(&Default::default(), window, cx);
4832 assert_eq!(editor.text(cx), "acbd\ne");
4833 assert_eq!(editor.selections.ranges(cx), [6..6]);
4834
4835 editor
4836 });
4837
4838 _ = cx.add_window(|window, cx| {
4839 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4840 editor.set_style(EditorStyle::default(), window, cx);
4841 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1, 2..2, 4..4]));
4842 editor.transpose(&Default::default(), window, cx);
4843 assert_eq!(editor.text(cx), "bacd\ne");
4844 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
4845
4846 editor.transpose(&Default::default(), window, cx);
4847 assert_eq!(editor.text(cx), "bcade\n");
4848 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
4849
4850 editor.transpose(&Default::default(), window, cx);
4851 assert_eq!(editor.text(cx), "bcda\ne");
4852 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4853
4854 editor.transpose(&Default::default(), window, cx);
4855 assert_eq!(editor.text(cx), "bcade\n");
4856 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4857
4858 editor.transpose(&Default::default(), window, cx);
4859 assert_eq!(editor.text(cx), "bcaed\n");
4860 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
4861
4862 editor
4863 });
4864
4865 _ = cx.add_window(|window, cx| {
4866 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
4867 editor.set_style(EditorStyle::default(), window, cx);
4868 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4869 editor.transpose(&Default::default(), window, cx);
4870 assert_eq!(editor.text(cx), "🏀🍐✋");
4871 assert_eq!(editor.selections.ranges(cx), [8..8]);
4872
4873 editor.transpose(&Default::default(), window, cx);
4874 assert_eq!(editor.text(cx), "🏀✋🍐");
4875 assert_eq!(editor.selections.ranges(cx), [11..11]);
4876
4877 editor.transpose(&Default::default(), window, cx);
4878 assert_eq!(editor.text(cx), "🏀🍐✋");
4879 assert_eq!(editor.selections.ranges(cx), [11..11]);
4880
4881 editor
4882 });
4883}
4884
4885#[gpui::test]
4886async fn test_rewrap(cx: &mut TestAppContext) {
4887 init_test(cx, |settings| {
4888 settings.languages.extend([
4889 (
4890 "Markdown".into(),
4891 LanguageSettingsContent {
4892 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4893 ..Default::default()
4894 },
4895 ),
4896 (
4897 "Plain Text".into(),
4898 LanguageSettingsContent {
4899 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4900 ..Default::default()
4901 },
4902 ),
4903 ])
4904 });
4905
4906 let mut cx = EditorTestContext::new(cx).await;
4907
4908 let language_with_c_comments = Arc::new(Language::new(
4909 LanguageConfig {
4910 line_comments: vec!["// ".into()],
4911 ..LanguageConfig::default()
4912 },
4913 None,
4914 ));
4915 let language_with_pound_comments = Arc::new(Language::new(
4916 LanguageConfig {
4917 line_comments: vec!["# ".into()],
4918 ..LanguageConfig::default()
4919 },
4920 None,
4921 ));
4922 let markdown_language = Arc::new(Language::new(
4923 LanguageConfig {
4924 name: "Markdown".into(),
4925 ..LanguageConfig::default()
4926 },
4927 None,
4928 ));
4929 let language_with_doc_comments = Arc::new(Language::new(
4930 LanguageConfig {
4931 line_comments: vec!["// ".into(), "/// ".into()],
4932 ..LanguageConfig::default()
4933 },
4934 Some(tree_sitter_rust::LANGUAGE.into()),
4935 ));
4936
4937 let plaintext_language = Arc::new(Language::new(
4938 LanguageConfig {
4939 name: "Plain Text".into(),
4940 ..LanguageConfig::default()
4941 },
4942 None,
4943 ));
4944
4945 assert_rewrap(
4946 indoc! {"
4947 // ˇ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.
4948 "},
4949 indoc! {"
4950 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4951 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4952 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4953 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4954 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4955 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4956 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4957 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4958 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4959 // porttitor id. Aliquam id accumsan eros.
4960 "},
4961 language_with_c_comments.clone(),
4962 &mut cx,
4963 );
4964
4965 // Test that rewrapping works inside of a selection
4966 assert_rewrap(
4967 indoc! {"
4968 «// 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.ˇ»
4969 "},
4970 indoc! {"
4971 «// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4972 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4973 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4974 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4975 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4976 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4977 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4978 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4979 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4980 // porttitor id. Aliquam id accumsan eros.ˇ»
4981 "},
4982 language_with_c_comments.clone(),
4983 &mut cx,
4984 );
4985
4986 // Test that cursors that expand to the same region are collapsed.
4987 assert_rewrap(
4988 indoc! {"
4989 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4990 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4991 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4992 // ˇ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.
4993 "},
4994 indoc! {"
4995 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4996 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4997 // auctor, eu lacinia sapien scelerisque. ˇVivamus sit amet neque et quam
4998 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4999 // Pellentesque odio lectus, iaculis ac volutpat et, ˇblandit quis urna. Sed
5000 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
5001 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
5002 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
5003 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
5004 // porttitor id. Aliquam id accumsan eros.
5005 "},
5006 language_with_c_comments.clone(),
5007 &mut cx,
5008 );
5009
5010 // Test that non-contiguous selections are treated separately.
5011 assert_rewrap(
5012 indoc! {"
5013 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
5014 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
5015 //
5016 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
5017 // ˇ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.
5018 "},
5019 indoc! {"
5020 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
5021 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
5022 // auctor, eu lacinia sapien scelerisque.
5023 //
5024 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas
5025 // tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
5026 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec
5027 // molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque
5028 // nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas
5029 // porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id
5030 // vulputate turpis porttitor id. Aliquam id accumsan eros.
5031 "},
5032 language_with_c_comments.clone(),
5033 &mut cx,
5034 );
5035
5036 // Test that different comment prefixes are supported.
5037 assert_rewrap(
5038 indoc! {"
5039 # ˇ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.
5040 "},
5041 indoc! {"
5042 # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
5043 # purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
5044 # eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
5045 # hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
5046 # lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit
5047 # amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet
5048 # in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur
5049 # adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis.
5050 # Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id
5051 # accumsan eros.
5052 "},
5053 language_with_pound_comments.clone(),
5054 &mut cx,
5055 );
5056
5057 // Test that rewrapping is ignored outside of comments in most languages.
5058 assert_rewrap(
5059 indoc! {"
5060 /// Adds two numbers.
5061 /// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
5062 fn add(a: u32, b: u32) -> u32 {
5063 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ˇ
5064 }
5065 "},
5066 indoc! {"
5067 /// Adds two numbers. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
5068 /// Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
5069 fn add(a: u32, b: u32) -> u32 {
5070 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ˇ
5071 }
5072 "},
5073 language_with_doc_comments.clone(),
5074 &mut cx,
5075 );
5076
5077 // Test that rewrapping works in Markdown and Plain Text languages.
5078 assert_rewrap(
5079 indoc! {"
5080 # Hello
5081
5082 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.
5083 "},
5084 indoc! {"
5085 # Hello
5086
5087 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
5088 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
5089 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
5090 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
5091 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
5092 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
5093 Integer sit amet scelerisque nisi.
5094 "},
5095 markdown_language,
5096 &mut cx,
5097 );
5098
5099 assert_rewrap(
5100 indoc! {"
5101 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.
5102 "},
5103 indoc! {"
5104 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
5105 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
5106 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
5107 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
5108 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
5109 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
5110 Integer sit amet scelerisque nisi.
5111 "},
5112 plaintext_language,
5113 &mut cx,
5114 );
5115
5116 // Test rewrapping unaligned comments in a selection.
5117 assert_rewrap(
5118 indoc! {"
5119 fn foo() {
5120 if true {
5121 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
5122 // Praesent semper egestas tellus id dignissim.ˇ»
5123 do_something();
5124 } else {
5125 //
5126 }
5127 }
5128 "},
5129 indoc! {"
5130 fn foo() {
5131 if true {
5132 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
5133 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
5134 // egestas tellus id dignissim.ˇ»
5135 do_something();
5136 } else {
5137 //
5138 }
5139 }
5140 "},
5141 language_with_doc_comments.clone(),
5142 &mut cx,
5143 );
5144
5145 assert_rewrap(
5146 indoc! {"
5147 fn foo() {
5148 if true {
5149 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
5150 // Praesent semper egestas tellus id dignissim.»
5151 do_something();
5152 } else {
5153 //
5154 }
5155
5156 }
5157 "},
5158 indoc! {"
5159 fn foo() {
5160 if true {
5161 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
5162 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
5163 // egestas tellus id dignissim.»
5164 do_something();
5165 } else {
5166 //
5167 }
5168
5169 }
5170 "},
5171 language_with_doc_comments.clone(),
5172 &mut cx,
5173 );
5174
5175 #[track_caller]
5176 fn assert_rewrap(
5177 unwrapped_text: &str,
5178 wrapped_text: &str,
5179 language: Arc<Language>,
5180 cx: &mut EditorTestContext,
5181 ) {
5182 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
5183 cx.set_state(unwrapped_text);
5184 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
5185 cx.assert_editor_state(wrapped_text);
5186 }
5187}
5188
5189#[gpui::test]
5190async fn test_hard_wrap(cx: &mut TestAppContext) {
5191 init_test(cx, |_| {});
5192 let mut cx = EditorTestContext::new(cx).await;
5193
5194 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
5195 cx.update_editor(|editor, _, cx| {
5196 editor.set_hard_wrap(Some(14), cx);
5197 });
5198
5199 cx.set_state(indoc!(
5200 "
5201 one two three ˇ
5202 "
5203 ));
5204 cx.simulate_input("four");
5205 cx.run_until_parked();
5206
5207 cx.assert_editor_state(indoc!(
5208 "
5209 one two three
5210 fourˇ
5211 "
5212 ));
5213
5214 cx.update_editor(|editor, window, cx| {
5215 editor.newline(&Default::default(), window, cx);
5216 });
5217 cx.run_until_parked();
5218 cx.assert_editor_state(indoc!(
5219 "
5220 one two three
5221 four
5222 ˇ
5223 "
5224 ));
5225
5226 cx.simulate_input("five");
5227 cx.run_until_parked();
5228 cx.assert_editor_state(indoc!(
5229 "
5230 one two three
5231 four
5232 fiveˇ
5233 "
5234 ));
5235
5236 cx.update_editor(|editor, window, cx| {
5237 editor.newline(&Default::default(), window, cx);
5238 });
5239 cx.run_until_parked();
5240 cx.simulate_input("# ");
5241 cx.run_until_parked();
5242 cx.assert_editor_state(indoc!(
5243 "
5244 one two three
5245 four
5246 five
5247 # ˇ
5248 "
5249 ));
5250
5251 cx.update_editor(|editor, window, cx| {
5252 editor.newline(&Default::default(), window, cx);
5253 });
5254 cx.run_until_parked();
5255 cx.assert_editor_state(indoc!(
5256 "
5257 one two three
5258 four
5259 five
5260 #\x20
5261 #ˇ
5262 "
5263 ));
5264
5265 cx.simulate_input(" 6");
5266 cx.run_until_parked();
5267 cx.assert_editor_state(indoc!(
5268 "
5269 one two three
5270 four
5271 five
5272 #
5273 # 6ˇ
5274 "
5275 ));
5276}
5277
5278#[gpui::test]
5279async fn test_clipboard(cx: &mut TestAppContext) {
5280 init_test(cx, |_| {});
5281
5282 let mut cx = EditorTestContext::new(cx).await;
5283
5284 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
5285 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5286 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
5287
5288 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
5289 cx.set_state("two ˇfour ˇsix ˇ");
5290 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5291 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
5292
5293 // Paste again but with only two cursors. Since the number of cursors doesn't
5294 // match the number of slices in the clipboard, the entire clipboard text
5295 // is pasted at each cursor.
5296 cx.set_state("ˇtwo one✅ four three six five ˇ");
5297 cx.update_editor(|e, window, cx| {
5298 e.handle_input("( ", window, cx);
5299 e.paste(&Paste, window, cx);
5300 e.handle_input(") ", window, cx);
5301 });
5302 cx.assert_editor_state(
5303 &([
5304 "( one✅ ",
5305 "three ",
5306 "five ) ˇtwo one✅ four three six five ( one✅ ",
5307 "three ",
5308 "five ) ˇ",
5309 ]
5310 .join("\n")),
5311 );
5312
5313 // Cut with three selections, one of which is full-line.
5314 cx.set_state(indoc! {"
5315 1«2ˇ»3
5316 4ˇ567
5317 «8ˇ»9"});
5318 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5319 cx.assert_editor_state(indoc! {"
5320 1ˇ3
5321 ˇ9"});
5322
5323 // Paste with three selections, noticing how the copied selection that was full-line
5324 // gets inserted before the second cursor.
5325 cx.set_state(indoc! {"
5326 1ˇ3
5327 9ˇ
5328 «oˇ»ne"});
5329 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5330 cx.assert_editor_state(indoc! {"
5331 12ˇ3
5332 4567
5333 9ˇ
5334 8ˇne"});
5335
5336 // Copy with a single cursor only, which writes the whole line into the clipboard.
5337 cx.set_state(indoc! {"
5338 The quick brown
5339 fox juˇmps over
5340 the lazy dog"});
5341 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5342 assert_eq!(
5343 cx.read_from_clipboard()
5344 .and_then(|item| item.text().as_deref().map(str::to_string)),
5345 Some("fox jumps over\n".to_string())
5346 );
5347
5348 // Paste with three selections, noticing how the copied full-line selection is inserted
5349 // before the empty selections but replaces the selection that is non-empty.
5350 cx.set_state(indoc! {"
5351 Tˇhe quick brown
5352 «foˇ»x jumps over
5353 tˇhe lazy dog"});
5354 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5355 cx.assert_editor_state(indoc! {"
5356 fox jumps over
5357 Tˇhe quick brown
5358 fox jumps over
5359 ˇx jumps over
5360 fox jumps over
5361 tˇhe lazy dog"});
5362}
5363
5364#[gpui::test]
5365async fn test_copy_trim(cx: &mut TestAppContext) {
5366 init_test(cx, |_| {});
5367
5368 let mut cx = EditorTestContext::new(cx).await;
5369 cx.set_state(
5370 r#" «for selection in selections.iter() {
5371 let mut start = selection.start;
5372 let mut end = selection.end;
5373 let is_entire_line = selection.is_empty();
5374 if is_entire_line {
5375 start = Point::new(start.row, 0);ˇ»
5376 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5377 }
5378 "#,
5379 );
5380 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5381 assert_eq!(
5382 cx.read_from_clipboard()
5383 .and_then(|item| item.text().as_deref().map(str::to_string)),
5384 Some(
5385 "for selection in selections.iter() {
5386 let mut start = selection.start;
5387 let mut end = selection.end;
5388 let is_entire_line = selection.is_empty();
5389 if is_entire_line {
5390 start = Point::new(start.row, 0);"
5391 .to_string()
5392 ),
5393 "Regular copying preserves all indentation selected",
5394 );
5395 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5396 assert_eq!(
5397 cx.read_from_clipboard()
5398 .and_then(|item| item.text().as_deref().map(str::to_string)),
5399 Some(
5400 "for selection in selections.iter() {
5401let mut start = selection.start;
5402let mut end = selection.end;
5403let is_entire_line = selection.is_empty();
5404if is_entire_line {
5405 start = Point::new(start.row, 0);"
5406 .to_string()
5407 ),
5408 "Copying with stripping should strip all leading whitespaces"
5409 );
5410
5411 cx.set_state(
5412 r#" « for selection in selections.iter() {
5413 let mut start = selection.start;
5414 let mut end = selection.end;
5415 let is_entire_line = selection.is_empty();
5416 if is_entire_line {
5417 start = Point::new(start.row, 0);ˇ»
5418 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5419 }
5420 "#,
5421 );
5422 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5423 assert_eq!(
5424 cx.read_from_clipboard()
5425 .and_then(|item| item.text().as_deref().map(str::to_string)),
5426 Some(
5427 " for selection in selections.iter() {
5428 let mut start = selection.start;
5429 let mut end = selection.end;
5430 let is_entire_line = selection.is_empty();
5431 if is_entire_line {
5432 start = Point::new(start.row, 0);"
5433 .to_string()
5434 ),
5435 "Regular copying preserves all indentation selected",
5436 );
5437 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5438 assert_eq!(
5439 cx.read_from_clipboard()
5440 .and_then(|item| item.text().as_deref().map(str::to_string)),
5441 Some(
5442 "for selection in selections.iter() {
5443let mut start = selection.start;
5444let mut end = selection.end;
5445let is_entire_line = selection.is_empty();
5446if is_entire_line {
5447 start = Point::new(start.row, 0);"
5448 .to_string()
5449 ),
5450 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
5451 );
5452
5453 cx.set_state(
5454 r#" «ˇ for selection in selections.iter() {
5455 let mut start = selection.start;
5456 let mut end = selection.end;
5457 let is_entire_line = selection.is_empty();
5458 if is_entire_line {
5459 start = Point::new(start.row, 0);»
5460 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5461 }
5462 "#,
5463 );
5464 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5465 assert_eq!(
5466 cx.read_from_clipboard()
5467 .and_then(|item| item.text().as_deref().map(str::to_string)),
5468 Some(
5469 " for selection in selections.iter() {
5470 let mut start = selection.start;
5471 let mut end = selection.end;
5472 let is_entire_line = selection.is_empty();
5473 if is_entire_line {
5474 start = Point::new(start.row, 0);"
5475 .to_string()
5476 ),
5477 "Regular copying for reverse selection works the same",
5478 );
5479 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5480 assert_eq!(
5481 cx.read_from_clipboard()
5482 .and_then(|item| item.text().as_deref().map(str::to_string)),
5483 Some(
5484 "for selection in selections.iter() {
5485let mut start = selection.start;
5486let mut end = selection.end;
5487let is_entire_line = selection.is_empty();
5488if is_entire_line {
5489 start = Point::new(start.row, 0);"
5490 .to_string()
5491 ),
5492 "Copying with stripping for reverse selection works the same"
5493 );
5494
5495 cx.set_state(
5496 r#" for selection «in selections.iter() {
5497 let mut start = selection.start;
5498 let mut end = selection.end;
5499 let is_entire_line = selection.is_empty();
5500 if is_entire_line {
5501 start = Point::new(start.row, 0);ˇ»
5502 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5503 }
5504 "#,
5505 );
5506 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5507 assert_eq!(
5508 cx.read_from_clipboard()
5509 .and_then(|item| item.text().as_deref().map(str::to_string)),
5510 Some(
5511 "in selections.iter() {
5512 let mut start = selection.start;
5513 let mut end = selection.end;
5514 let is_entire_line = selection.is_empty();
5515 if is_entire_line {
5516 start = Point::new(start.row, 0);"
5517 .to_string()
5518 ),
5519 "When selecting past the indent, the copying works as usual",
5520 );
5521 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5522 assert_eq!(
5523 cx.read_from_clipboard()
5524 .and_then(|item| item.text().as_deref().map(str::to_string)),
5525 Some(
5526 "in selections.iter() {
5527 let mut start = selection.start;
5528 let mut end = selection.end;
5529 let is_entire_line = selection.is_empty();
5530 if is_entire_line {
5531 start = Point::new(start.row, 0);"
5532 .to_string()
5533 ),
5534 "When selecting past the indent, nothing is trimmed"
5535 );
5536
5537 cx.set_state(
5538 r#" «for selection in selections.iter() {
5539 let mut start = selection.start;
5540
5541 let mut end = selection.end;
5542 let is_entire_line = selection.is_empty();
5543 if is_entire_line {
5544 start = Point::new(start.row, 0);
5545ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
5546 }
5547 "#,
5548 );
5549 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5550 assert_eq!(
5551 cx.read_from_clipboard()
5552 .and_then(|item| item.text().as_deref().map(str::to_string)),
5553 Some(
5554 "for selection in selections.iter() {
5555let mut start = selection.start;
5556
5557let mut end = selection.end;
5558let is_entire_line = selection.is_empty();
5559if is_entire_line {
5560 start = Point::new(start.row, 0);
5561"
5562 .to_string()
5563 ),
5564 "Copying with stripping should ignore empty lines"
5565 );
5566}
5567
5568#[gpui::test]
5569async fn test_paste_multiline(cx: &mut TestAppContext) {
5570 init_test(cx, |_| {});
5571
5572 let mut cx = EditorTestContext::new(cx).await;
5573 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5574
5575 // Cut an indented block, without the leading whitespace.
5576 cx.set_state(indoc! {"
5577 const a: B = (
5578 c(),
5579 «d(
5580 e,
5581 f
5582 )ˇ»
5583 );
5584 "});
5585 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5586 cx.assert_editor_state(indoc! {"
5587 const a: B = (
5588 c(),
5589 ˇ
5590 );
5591 "});
5592
5593 // Paste it at the same position.
5594 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5595 cx.assert_editor_state(indoc! {"
5596 const a: B = (
5597 c(),
5598 d(
5599 e,
5600 f
5601 )ˇ
5602 );
5603 "});
5604
5605 // Paste it at a line with a lower indent level.
5606 cx.set_state(indoc! {"
5607 ˇ
5608 const a: B = (
5609 c(),
5610 );
5611 "});
5612 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5613 cx.assert_editor_state(indoc! {"
5614 d(
5615 e,
5616 f
5617 )ˇ
5618 const a: B = (
5619 c(),
5620 );
5621 "});
5622
5623 // Cut an indented block, with the leading whitespace.
5624 cx.set_state(indoc! {"
5625 const a: B = (
5626 c(),
5627 « d(
5628 e,
5629 f
5630 )
5631 ˇ»);
5632 "});
5633 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5634 cx.assert_editor_state(indoc! {"
5635 const a: B = (
5636 c(),
5637 ˇ);
5638 "});
5639
5640 // Paste it at the same position.
5641 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5642 cx.assert_editor_state(indoc! {"
5643 const a: B = (
5644 c(),
5645 d(
5646 e,
5647 f
5648 )
5649 ˇ);
5650 "});
5651
5652 // Paste it at a line with a higher indent level.
5653 cx.set_state(indoc! {"
5654 const a: B = (
5655 c(),
5656 d(
5657 e,
5658 fˇ
5659 )
5660 );
5661 "});
5662 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5663 cx.assert_editor_state(indoc! {"
5664 const a: B = (
5665 c(),
5666 d(
5667 e,
5668 f d(
5669 e,
5670 f
5671 )
5672 ˇ
5673 )
5674 );
5675 "});
5676
5677 // Copy an indented block, starting mid-line
5678 cx.set_state(indoc! {"
5679 const a: B = (
5680 c(),
5681 somethin«g(
5682 e,
5683 f
5684 )ˇ»
5685 );
5686 "});
5687 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5688
5689 // Paste it on a line with a lower indent level
5690 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
5691 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5692 cx.assert_editor_state(indoc! {"
5693 const a: B = (
5694 c(),
5695 something(
5696 e,
5697 f
5698 )
5699 );
5700 g(
5701 e,
5702 f
5703 )ˇ"});
5704}
5705
5706#[gpui::test]
5707async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
5708 init_test(cx, |_| {});
5709
5710 cx.write_to_clipboard(ClipboardItem::new_string(
5711 " d(\n e\n );\n".into(),
5712 ));
5713
5714 let mut cx = EditorTestContext::new(cx).await;
5715 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5716
5717 cx.set_state(indoc! {"
5718 fn a() {
5719 b();
5720 if c() {
5721 ˇ
5722 }
5723 }
5724 "});
5725
5726 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5727 cx.assert_editor_state(indoc! {"
5728 fn a() {
5729 b();
5730 if c() {
5731 d(
5732 e
5733 );
5734 ˇ
5735 }
5736 }
5737 "});
5738
5739 cx.set_state(indoc! {"
5740 fn a() {
5741 b();
5742 ˇ
5743 }
5744 "});
5745
5746 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5747 cx.assert_editor_state(indoc! {"
5748 fn a() {
5749 b();
5750 d(
5751 e
5752 );
5753 ˇ
5754 }
5755 "});
5756}
5757
5758#[gpui::test]
5759fn test_select_all(cx: &mut TestAppContext) {
5760 init_test(cx, |_| {});
5761
5762 let editor = cx.add_window(|window, cx| {
5763 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
5764 build_editor(buffer, window, cx)
5765 });
5766 _ = editor.update(cx, |editor, window, cx| {
5767 editor.select_all(&SelectAll, window, cx);
5768 assert_eq!(
5769 editor.selections.display_ranges(cx),
5770 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
5771 );
5772 });
5773}
5774
5775#[gpui::test]
5776fn test_select_line(cx: &mut TestAppContext) {
5777 init_test(cx, |_| {});
5778
5779 let editor = cx.add_window(|window, cx| {
5780 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
5781 build_editor(buffer, window, cx)
5782 });
5783 _ = editor.update(cx, |editor, window, cx| {
5784 editor.change_selections(None, window, cx, |s| {
5785 s.select_display_ranges([
5786 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5787 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5788 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5789 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
5790 ])
5791 });
5792 editor.select_line(&SelectLine, window, cx);
5793 assert_eq!(
5794 editor.selections.display_ranges(cx),
5795 vec![
5796 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
5797 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
5798 ]
5799 );
5800 });
5801
5802 _ = editor.update(cx, |editor, window, cx| {
5803 editor.select_line(&SelectLine, window, cx);
5804 assert_eq!(
5805 editor.selections.display_ranges(cx),
5806 vec![
5807 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
5808 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
5809 ]
5810 );
5811 });
5812
5813 _ = editor.update(cx, |editor, window, cx| {
5814 editor.select_line(&SelectLine, window, cx);
5815 assert_eq!(
5816 editor.selections.display_ranges(cx),
5817 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
5818 );
5819 });
5820}
5821
5822#[gpui::test]
5823async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
5824 init_test(cx, |_| {});
5825 let mut cx = EditorTestContext::new(cx).await;
5826
5827 #[track_caller]
5828 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
5829 cx.set_state(initial_state);
5830 cx.update_editor(|e, window, cx| {
5831 e.split_selection_into_lines(&SplitSelectionIntoLines, window, cx)
5832 });
5833 cx.assert_editor_state(expected_state);
5834 }
5835
5836 // Selection starts and ends at the middle of lines, left-to-right
5837 test(
5838 &mut cx,
5839 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
5840 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5841 );
5842 // Same thing, right-to-left
5843 test(
5844 &mut cx,
5845 "aa\nb«b\ncc\ndd\neˇ»e\nff",
5846 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5847 );
5848
5849 // Whole buffer, left-to-right, last line *doesn't* end with newline
5850 test(
5851 &mut cx,
5852 "«ˇaa\nbb\ncc\ndd\nee\nff»",
5853 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5854 );
5855 // Same thing, right-to-left
5856 test(
5857 &mut cx,
5858 "«aa\nbb\ncc\ndd\nee\nffˇ»",
5859 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5860 );
5861
5862 // Whole buffer, left-to-right, last line ends with newline
5863 test(
5864 &mut cx,
5865 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
5866 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5867 );
5868 // Same thing, right-to-left
5869 test(
5870 &mut cx,
5871 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
5872 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5873 );
5874
5875 // Starts at the end of a line, ends at the start of another
5876 test(
5877 &mut cx,
5878 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
5879 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
5880 );
5881}
5882
5883#[gpui::test]
5884async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
5885 init_test(cx, |_| {});
5886
5887 let editor = cx.add_window(|window, cx| {
5888 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
5889 build_editor(buffer, window, cx)
5890 });
5891
5892 // setup
5893 _ = editor.update(cx, |editor, window, cx| {
5894 editor.fold_creases(
5895 vec![
5896 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5897 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5898 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5899 ],
5900 true,
5901 window,
5902 cx,
5903 );
5904 assert_eq!(
5905 editor.display_text(cx),
5906 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5907 );
5908 });
5909
5910 _ = editor.update(cx, |editor, window, cx| {
5911 editor.change_selections(None, window, cx, |s| {
5912 s.select_display_ranges([
5913 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5914 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5915 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5916 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
5917 ])
5918 });
5919 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5920 assert_eq!(
5921 editor.display_text(cx),
5922 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5923 );
5924 });
5925 EditorTestContext::for_editor(editor, cx)
5926 .await
5927 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
5928
5929 _ = editor.update(cx, |editor, window, cx| {
5930 editor.change_selections(None, window, cx, |s| {
5931 s.select_display_ranges([
5932 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
5933 ])
5934 });
5935 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5936 assert_eq!(
5937 editor.display_text(cx),
5938 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
5939 );
5940 assert_eq!(
5941 editor.selections.display_ranges(cx),
5942 [
5943 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
5944 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
5945 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
5946 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
5947 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
5948 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
5949 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
5950 ]
5951 );
5952 });
5953 EditorTestContext::for_editor(editor, cx)
5954 .await
5955 .assert_editor_state(
5956 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
5957 );
5958}
5959
5960#[gpui::test]
5961async fn test_add_selection_above_below(cx: &mut TestAppContext) {
5962 init_test(cx, |_| {});
5963
5964 let mut cx = EditorTestContext::new(cx).await;
5965
5966 cx.set_state(indoc!(
5967 r#"abc
5968 defˇghi
5969
5970 jk
5971 nlmo
5972 "#
5973 ));
5974
5975 cx.update_editor(|editor, window, cx| {
5976 editor.add_selection_above(&Default::default(), window, cx);
5977 });
5978
5979 cx.assert_editor_state(indoc!(
5980 r#"abcˇ
5981 defˇghi
5982
5983 jk
5984 nlmo
5985 "#
5986 ));
5987
5988 cx.update_editor(|editor, window, cx| {
5989 editor.add_selection_above(&Default::default(), window, cx);
5990 });
5991
5992 cx.assert_editor_state(indoc!(
5993 r#"abcˇ
5994 defˇghi
5995
5996 jk
5997 nlmo
5998 "#
5999 ));
6000
6001 cx.update_editor(|editor, window, cx| {
6002 editor.add_selection_below(&Default::default(), window, cx);
6003 });
6004
6005 cx.assert_editor_state(indoc!(
6006 r#"abc
6007 defˇghi
6008
6009 jk
6010 nlmo
6011 "#
6012 ));
6013
6014 cx.update_editor(|editor, window, cx| {
6015 editor.undo_selection(&Default::default(), window, cx);
6016 });
6017
6018 cx.assert_editor_state(indoc!(
6019 r#"abcˇ
6020 defˇghi
6021
6022 jk
6023 nlmo
6024 "#
6025 ));
6026
6027 cx.update_editor(|editor, window, cx| {
6028 editor.redo_selection(&Default::default(), window, cx);
6029 });
6030
6031 cx.assert_editor_state(indoc!(
6032 r#"abc
6033 defˇghi
6034
6035 jk
6036 nlmo
6037 "#
6038 ));
6039
6040 cx.update_editor(|editor, window, cx| {
6041 editor.add_selection_below(&Default::default(), window, cx);
6042 });
6043
6044 cx.assert_editor_state(indoc!(
6045 r#"abc
6046 defˇghi
6047
6048 jk
6049 nlmˇo
6050 "#
6051 ));
6052
6053 cx.update_editor(|editor, window, cx| {
6054 editor.add_selection_below(&Default::default(), window, cx);
6055 });
6056
6057 cx.assert_editor_state(indoc!(
6058 r#"abc
6059 defˇghi
6060
6061 jk
6062 nlmˇo
6063 "#
6064 ));
6065
6066 // change selections
6067 cx.set_state(indoc!(
6068 r#"abc
6069 def«ˇg»hi
6070
6071 jk
6072 nlmo
6073 "#
6074 ));
6075
6076 cx.update_editor(|editor, window, cx| {
6077 editor.add_selection_below(&Default::default(), window, cx);
6078 });
6079
6080 cx.assert_editor_state(indoc!(
6081 r#"abc
6082 def«ˇg»hi
6083
6084 jk
6085 nlm«ˇo»
6086 "#
6087 ));
6088
6089 cx.update_editor(|editor, window, cx| {
6090 editor.add_selection_below(&Default::default(), window, cx);
6091 });
6092
6093 cx.assert_editor_state(indoc!(
6094 r#"abc
6095 def«ˇg»hi
6096
6097 jk
6098 nlm«ˇo»
6099 "#
6100 ));
6101
6102 cx.update_editor(|editor, window, cx| {
6103 editor.add_selection_above(&Default::default(), window, cx);
6104 });
6105
6106 cx.assert_editor_state(indoc!(
6107 r#"abc
6108 def«ˇg»hi
6109
6110 jk
6111 nlmo
6112 "#
6113 ));
6114
6115 cx.update_editor(|editor, window, cx| {
6116 editor.add_selection_above(&Default::default(), window, cx);
6117 });
6118
6119 cx.assert_editor_state(indoc!(
6120 r#"abc
6121 def«ˇg»hi
6122
6123 jk
6124 nlmo
6125 "#
6126 ));
6127
6128 // Change selections again
6129 cx.set_state(indoc!(
6130 r#"a«bc
6131 defgˇ»hi
6132
6133 jk
6134 nlmo
6135 "#
6136 ));
6137
6138 cx.update_editor(|editor, window, cx| {
6139 editor.add_selection_below(&Default::default(), window, cx);
6140 });
6141
6142 cx.assert_editor_state(indoc!(
6143 r#"a«bcˇ»
6144 d«efgˇ»hi
6145
6146 j«kˇ»
6147 nlmo
6148 "#
6149 ));
6150
6151 cx.update_editor(|editor, window, cx| {
6152 editor.add_selection_below(&Default::default(), window, cx);
6153 });
6154 cx.assert_editor_state(indoc!(
6155 r#"a«bcˇ»
6156 d«efgˇ»hi
6157
6158 j«kˇ»
6159 n«lmoˇ»
6160 "#
6161 ));
6162 cx.update_editor(|editor, window, cx| {
6163 editor.add_selection_above(&Default::default(), window, cx);
6164 });
6165
6166 cx.assert_editor_state(indoc!(
6167 r#"a«bcˇ»
6168 d«efgˇ»hi
6169
6170 j«kˇ»
6171 nlmo
6172 "#
6173 ));
6174
6175 // Change selections again
6176 cx.set_state(indoc!(
6177 r#"abc
6178 d«ˇefghi
6179
6180 jk
6181 nlm»o
6182 "#
6183 ));
6184
6185 cx.update_editor(|editor, window, cx| {
6186 editor.add_selection_above(&Default::default(), window, cx);
6187 });
6188
6189 cx.assert_editor_state(indoc!(
6190 r#"a«ˇbc»
6191 d«ˇef»ghi
6192
6193 j«ˇk»
6194 n«ˇlm»o
6195 "#
6196 ));
6197
6198 cx.update_editor(|editor, window, cx| {
6199 editor.add_selection_below(&Default::default(), window, cx);
6200 });
6201
6202 cx.assert_editor_state(indoc!(
6203 r#"abc
6204 d«ˇef»ghi
6205
6206 j«ˇk»
6207 n«ˇlm»o
6208 "#
6209 ));
6210}
6211
6212#[gpui::test]
6213async fn test_select_next(cx: &mut TestAppContext) {
6214 init_test(cx, |_| {});
6215
6216 let mut cx = EditorTestContext::new(cx).await;
6217 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6218
6219 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6220 .unwrap();
6221 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6222
6223 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6224 .unwrap();
6225 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
6226
6227 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6228 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6229
6230 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6231 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
6232
6233 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6234 .unwrap();
6235 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6236
6237 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6238 .unwrap();
6239 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6240
6241 // Test selection direction should be preserved
6242 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
6243
6244 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6245 .unwrap();
6246 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
6247}
6248
6249#[gpui::test]
6250async fn test_select_all_matches(cx: &mut TestAppContext) {
6251 init_test(cx, |_| {});
6252
6253 let mut cx = EditorTestContext::new(cx).await;
6254
6255 // Test caret-only selections
6256 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6257 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6258 .unwrap();
6259 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6260
6261 // Test left-to-right selections
6262 cx.set_state("abc\n«abcˇ»\nabc");
6263 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6264 .unwrap();
6265 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
6266
6267 // Test right-to-left selections
6268 cx.set_state("abc\n«ˇabc»\nabc");
6269 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6270 .unwrap();
6271 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
6272
6273 // Test selecting whitespace with caret selection
6274 cx.set_state("abc\nˇ abc\nabc");
6275 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6276 .unwrap();
6277 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
6278
6279 // Test selecting whitespace with left-to-right selection
6280 cx.set_state("abc\n«ˇ »abc\nabc");
6281 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6282 .unwrap();
6283 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
6284
6285 // Test no matches with right-to-left selection
6286 cx.set_state("abc\n« ˇ»abc\nabc");
6287 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6288 .unwrap();
6289 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
6290}
6291
6292#[gpui::test]
6293async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
6294 init_test(cx, |_| {});
6295
6296 let mut cx = EditorTestContext::new(cx).await;
6297
6298 let large_body_1 = "\nd".repeat(200);
6299 let large_body_2 = "\ne".repeat(200);
6300
6301 cx.set_state(&format!(
6302 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
6303 ));
6304 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
6305 let scroll_position = editor.scroll_position(cx);
6306 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
6307 scroll_position
6308 });
6309
6310 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6311 .unwrap();
6312 cx.assert_editor_state(&format!(
6313 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
6314 ));
6315 let scroll_position_after_selection =
6316 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
6317 assert_eq!(
6318 initial_scroll_position, scroll_position_after_selection,
6319 "Scroll position should not change after selecting all matches"
6320 );
6321}
6322
6323#[gpui::test]
6324async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
6325 init_test(cx, |_| {});
6326
6327 let mut cx = EditorLspTestContext::new_rust(
6328 lsp::ServerCapabilities {
6329 document_formatting_provider: Some(lsp::OneOf::Left(true)),
6330 ..Default::default()
6331 },
6332 cx,
6333 )
6334 .await;
6335
6336 cx.set_state(indoc! {"
6337 line 1
6338 line 2
6339 linˇe 3
6340 line 4
6341 line 5
6342 "});
6343
6344 // Make an edit
6345 cx.update_editor(|editor, window, cx| {
6346 editor.handle_input("X", window, cx);
6347 });
6348
6349 // Move cursor to a different position
6350 cx.update_editor(|editor, window, cx| {
6351 editor.change_selections(None, window, cx, |s| {
6352 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
6353 });
6354 });
6355
6356 cx.assert_editor_state(indoc! {"
6357 line 1
6358 line 2
6359 linXe 3
6360 line 4
6361 liˇne 5
6362 "});
6363
6364 cx.lsp
6365 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
6366 Ok(Some(vec![lsp::TextEdit::new(
6367 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
6368 "PREFIX ".to_string(),
6369 )]))
6370 });
6371
6372 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
6373 .unwrap()
6374 .await
6375 .unwrap();
6376
6377 cx.assert_editor_state(indoc! {"
6378 PREFIX line 1
6379 line 2
6380 linXe 3
6381 line 4
6382 liˇne 5
6383 "});
6384
6385 // Undo formatting
6386 cx.update_editor(|editor, window, cx| {
6387 editor.undo(&Default::default(), window, cx);
6388 });
6389
6390 // Verify cursor moved back to position after edit
6391 cx.assert_editor_state(indoc! {"
6392 line 1
6393 line 2
6394 linXˇe 3
6395 line 4
6396 line 5
6397 "});
6398}
6399
6400#[gpui::test]
6401async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
6402 init_test(cx, |_| {});
6403
6404 let mut cx = EditorTestContext::new(cx).await;
6405 cx.set_state(
6406 r#"let foo = 2;
6407lˇet foo = 2;
6408let fooˇ = 2;
6409let foo = 2;
6410let foo = ˇ2;"#,
6411 );
6412
6413 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6414 .unwrap();
6415 cx.assert_editor_state(
6416 r#"let foo = 2;
6417«letˇ» foo = 2;
6418let «fooˇ» = 2;
6419let foo = 2;
6420let foo = «2ˇ»;"#,
6421 );
6422
6423 // noop for multiple selections with different contents
6424 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6425 .unwrap();
6426 cx.assert_editor_state(
6427 r#"let foo = 2;
6428«letˇ» foo = 2;
6429let «fooˇ» = 2;
6430let foo = 2;
6431let foo = «2ˇ»;"#,
6432 );
6433
6434 // Test last selection direction should be preserved
6435 cx.set_state(
6436 r#"let foo = 2;
6437let foo = 2;
6438let «fooˇ» = 2;
6439let «ˇfoo» = 2;
6440let foo = 2;"#,
6441 );
6442
6443 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6444 .unwrap();
6445 cx.assert_editor_state(
6446 r#"let foo = 2;
6447let foo = 2;
6448let «fooˇ» = 2;
6449let «ˇfoo» = 2;
6450let «ˇfoo» = 2;"#,
6451 );
6452}
6453
6454#[gpui::test]
6455async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
6456 init_test(cx, |_| {});
6457
6458 let mut cx =
6459 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
6460
6461 cx.assert_editor_state(indoc! {"
6462 ˇbbb
6463 ccc
6464
6465 bbb
6466 ccc
6467 "});
6468 cx.dispatch_action(SelectPrevious::default());
6469 cx.assert_editor_state(indoc! {"
6470 «bbbˇ»
6471 ccc
6472
6473 bbb
6474 ccc
6475 "});
6476 cx.dispatch_action(SelectPrevious::default());
6477 cx.assert_editor_state(indoc! {"
6478 «bbbˇ»
6479 ccc
6480
6481 «bbbˇ»
6482 ccc
6483 "});
6484}
6485
6486#[gpui::test]
6487async fn test_select_previous_with_single_caret(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 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6496
6497 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6498 .unwrap();
6499 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
6500
6501 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6502 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6503
6504 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6505 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
6506
6507 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6508 .unwrap();
6509 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
6510
6511 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6512 .unwrap();
6513 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6514}
6515
6516#[gpui::test]
6517async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
6518 init_test(cx, |_| {});
6519
6520 let mut cx = EditorTestContext::new(cx).await;
6521 cx.set_state("aˇ");
6522
6523 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6524 .unwrap();
6525 cx.assert_editor_state("«aˇ»");
6526 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6527 .unwrap();
6528 cx.assert_editor_state("«aˇ»");
6529}
6530
6531#[gpui::test]
6532async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
6533 init_test(cx, |_| {});
6534
6535 let mut cx = EditorTestContext::new(cx).await;
6536 cx.set_state(
6537 r#"let foo = 2;
6538lˇet foo = 2;
6539let fooˇ = 2;
6540let foo = 2;
6541let foo = ˇ2;"#,
6542 );
6543
6544 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6545 .unwrap();
6546 cx.assert_editor_state(
6547 r#"let foo = 2;
6548«letˇ» foo = 2;
6549let «fooˇ» = 2;
6550let foo = 2;
6551let foo = «2ˇ»;"#,
6552 );
6553
6554 // noop for multiple selections with different contents
6555 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6556 .unwrap();
6557 cx.assert_editor_state(
6558 r#"let foo = 2;
6559«letˇ» foo = 2;
6560let «fooˇ» = 2;
6561let foo = 2;
6562let foo = «2ˇ»;"#,
6563 );
6564}
6565
6566#[gpui::test]
6567async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
6568 init_test(cx, |_| {});
6569
6570 let mut cx = EditorTestContext::new(cx).await;
6571 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
6572
6573 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6574 .unwrap();
6575 // selection direction is preserved
6576 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
6577
6578 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6579 .unwrap();
6580 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
6581
6582 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6583 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
6584
6585 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6586 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
6587
6588 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6589 .unwrap();
6590 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
6591
6592 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6593 .unwrap();
6594 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
6595}
6596
6597#[gpui::test]
6598async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
6599 init_test(cx, |_| {});
6600
6601 let language = Arc::new(Language::new(
6602 LanguageConfig::default(),
6603 Some(tree_sitter_rust::LANGUAGE.into()),
6604 ));
6605
6606 let text = r#"
6607 use mod1::mod2::{mod3, mod4};
6608
6609 fn fn_1(param1: bool, param2: &str) {
6610 let var1 = "text";
6611 }
6612 "#
6613 .unindent();
6614
6615 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6616 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6617 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6618
6619 editor
6620 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6621 .await;
6622
6623 editor.update_in(cx, |editor, window, cx| {
6624 editor.change_selections(None, window, cx, |s| {
6625 s.select_display_ranges([
6626 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
6627 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
6628 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
6629 ]);
6630 });
6631 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6632 });
6633 editor.update(cx, |editor, cx| {
6634 assert_text_with_selections(
6635 editor,
6636 indoc! {r#"
6637 use mod1::mod2::{mod3, «mod4ˇ»};
6638
6639 fn fn_1«ˇ(param1: bool, param2: &str)» {
6640 let var1 = "«ˇtext»";
6641 }
6642 "#},
6643 cx,
6644 );
6645 });
6646
6647 editor.update_in(cx, |editor, window, cx| {
6648 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6649 });
6650 editor.update(cx, |editor, cx| {
6651 assert_text_with_selections(
6652 editor,
6653 indoc! {r#"
6654 use mod1::mod2::«{mod3, mod4}ˇ»;
6655
6656 «ˇfn fn_1(param1: bool, param2: &str) {
6657 let var1 = "text";
6658 }»
6659 "#},
6660 cx,
6661 );
6662 });
6663
6664 editor.update_in(cx, |editor, window, cx| {
6665 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6666 });
6667 assert_eq!(
6668 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
6669 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
6670 );
6671
6672 // Trying to expand the selected syntax node one more time has no effect.
6673 editor.update_in(cx, |editor, window, cx| {
6674 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6675 });
6676 assert_eq!(
6677 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
6678 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
6679 );
6680
6681 editor.update_in(cx, |editor, window, cx| {
6682 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6683 });
6684 editor.update(cx, |editor, cx| {
6685 assert_text_with_selections(
6686 editor,
6687 indoc! {r#"
6688 use mod1::mod2::«{mod3, mod4}ˇ»;
6689
6690 «ˇfn fn_1(param1: bool, param2: &str) {
6691 let var1 = "text";
6692 }»
6693 "#},
6694 cx,
6695 );
6696 });
6697
6698 editor.update_in(cx, |editor, window, cx| {
6699 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6700 });
6701 editor.update(cx, |editor, cx| {
6702 assert_text_with_selections(
6703 editor,
6704 indoc! {r#"
6705 use mod1::mod2::{mod3, «mod4ˇ»};
6706
6707 fn fn_1«ˇ(param1: bool, param2: &str)» {
6708 let var1 = "«ˇtext»";
6709 }
6710 "#},
6711 cx,
6712 );
6713 });
6714
6715 editor.update_in(cx, |editor, window, cx| {
6716 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6717 });
6718 editor.update(cx, |editor, cx| {
6719 assert_text_with_selections(
6720 editor,
6721 indoc! {r#"
6722 use mod1::mod2::{mod3, mo«ˇ»d4};
6723
6724 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
6725 let var1 = "te«ˇ»xt";
6726 }
6727 "#},
6728 cx,
6729 );
6730 });
6731
6732 // Trying to shrink the selected syntax node one more time has no effect.
6733 editor.update_in(cx, |editor, window, cx| {
6734 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6735 });
6736 editor.update_in(cx, |editor, _, cx| {
6737 assert_text_with_selections(
6738 editor,
6739 indoc! {r#"
6740 use mod1::mod2::{mod3, mo«ˇ»d4};
6741
6742 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
6743 let var1 = "te«ˇ»xt";
6744 }
6745 "#},
6746 cx,
6747 );
6748 });
6749
6750 // Ensure that we keep expanding the selection if the larger selection starts or ends within
6751 // a fold.
6752 editor.update_in(cx, |editor, window, cx| {
6753 editor.fold_creases(
6754 vec![
6755 Crease::simple(
6756 Point::new(0, 21)..Point::new(0, 24),
6757 FoldPlaceholder::test(),
6758 ),
6759 Crease::simple(
6760 Point::new(3, 20)..Point::new(3, 22),
6761 FoldPlaceholder::test(),
6762 ),
6763 ],
6764 true,
6765 window,
6766 cx,
6767 );
6768 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6769 });
6770 editor.update(cx, |editor, cx| {
6771 assert_text_with_selections(
6772 editor,
6773 indoc! {r#"
6774 use mod1::mod2::«{mod3, mod4}ˇ»;
6775
6776 fn fn_1«ˇ(param1: bool, param2: &str)» {
6777 let var1 = "«ˇtext»";
6778 }
6779 "#},
6780 cx,
6781 );
6782 });
6783}
6784
6785#[gpui::test]
6786async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
6787 init_test(cx, |_| {});
6788
6789 let language = Arc::new(Language::new(
6790 LanguageConfig::default(),
6791 Some(tree_sitter_rust::LANGUAGE.into()),
6792 ));
6793
6794 let text = "let a = 2;";
6795
6796 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6797 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6798 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6799
6800 editor
6801 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6802 .await;
6803
6804 // Test case 1: Cursor at end of word
6805 editor.update_in(cx, |editor, window, cx| {
6806 editor.change_selections(None, window, cx, |s| {
6807 s.select_display_ranges([
6808 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
6809 ]);
6810 });
6811 });
6812 editor.update(cx, |editor, cx| {
6813 assert_text_with_selections(editor, "let aˇ = 2;", cx);
6814 });
6815 editor.update_in(cx, |editor, window, cx| {
6816 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6817 });
6818 editor.update(cx, |editor, cx| {
6819 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
6820 });
6821 editor.update_in(cx, |editor, window, cx| {
6822 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6823 });
6824 editor.update(cx, |editor, cx| {
6825 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
6826 });
6827
6828 // Test case 2: Cursor at end of statement
6829 editor.update_in(cx, |editor, window, cx| {
6830 editor.change_selections(None, window, cx, |s| {
6831 s.select_display_ranges([
6832 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
6833 ]);
6834 });
6835 });
6836 editor.update(cx, |editor, cx| {
6837 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
6838 });
6839 editor.update_in(cx, |editor, window, cx| {
6840 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6841 });
6842 editor.update(cx, |editor, cx| {
6843 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
6844 });
6845}
6846
6847#[gpui::test]
6848async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
6849 init_test(cx, |_| {});
6850
6851 let language = Arc::new(Language::new(
6852 LanguageConfig::default(),
6853 Some(tree_sitter_rust::LANGUAGE.into()),
6854 ));
6855
6856 let text = r#"
6857 use mod1::mod2::{mod3, mod4};
6858
6859 fn fn_1(param1: bool, param2: &str) {
6860 let var1 = "hello world";
6861 }
6862 "#
6863 .unindent();
6864
6865 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6866 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6867 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6868
6869 editor
6870 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6871 .await;
6872
6873 // Test 1: Cursor on a letter of a string word
6874 editor.update_in(cx, |editor, window, cx| {
6875 editor.change_selections(None, window, cx, |s| {
6876 s.select_display_ranges([
6877 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
6878 ]);
6879 });
6880 });
6881 editor.update_in(cx, |editor, 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 = "hˇello world";
6889 }
6890 "#},
6891 cx,
6892 );
6893 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6894 assert_text_with_selections(
6895 editor,
6896 indoc! {r#"
6897 use mod1::mod2::{mod3, mod4};
6898
6899 fn fn_1(param1: bool, param2: &str) {
6900 let var1 = "«ˇhello» world";
6901 }
6902 "#},
6903 cx,
6904 );
6905 });
6906
6907 // Test 2: Partial selection within a word
6908 editor.update_in(cx, |editor, window, cx| {
6909 editor.change_selections(None, window, cx, |s| {
6910 s.select_display_ranges([
6911 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
6912 ]);
6913 });
6914 });
6915 editor.update_in(cx, |editor, 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 = "h«elˇ»lo world";
6923 }
6924 "#},
6925 cx,
6926 );
6927 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6928 assert_text_with_selections(
6929 editor,
6930 indoc! {r#"
6931 use mod1::mod2::{mod3, mod4};
6932
6933 fn fn_1(param1: bool, param2: &str) {
6934 let var1 = "«ˇhello» world";
6935 }
6936 "#},
6937 cx,
6938 );
6939 });
6940
6941 // Test 3: Complete word already selected
6942 editor.update_in(cx, |editor, window, cx| {
6943 editor.change_selections(None, window, cx, |s| {
6944 s.select_display_ranges([
6945 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
6946 ]);
6947 });
6948 });
6949 editor.update_in(cx, |editor, window, cx| {
6950 assert_text_with_selections(
6951 editor,
6952 indoc! {r#"
6953 use mod1::mod2::{mod3, mod4};
6954
6955 fn fn_1(param1: bool, param2: &str) {
6956 let var1 = "«helloˇ» world";
6957 }
6958 "#},
6959 cx,
6960 );
6961 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6962 assert_text_with_selections(
6963 editor,
6964 indoc! {r#"
6965 use mod1::mod2::{mod3, mod4};
6966
6967 fn fn_1(param1: bool, param2: &str) {
6968 let var1 = "«hello worldˇ»";
6969 }
6970 "#},
6971 cx,
6972 );
6973 });
6974
6975 // Test 4: Selection spanning across words
6976 editor.update_in(cx, |editor, window, cx| {
6977 editor.change_selections(None, window, cx, |s| {
6978 s.select_display_ranges([
6979 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
6980 ]);
6981 });
6982 });
6983 editor.update_in(cx, |editor, window, cx| {
6984 assert_text_with_selections(
6985 editor,
6986 indoc! {r#"
6987 use mod1::mod2::{mod3, mod4};
6988
6989 fn fn_1(param1: bool, param2: &str) {
6990 let var1 = "hel«lo woˇ»rld";
6991 }
6992 "#},
6993 cx,
6994 );
6995 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6996 assert_text_with_selections(
6997 editor,
6998 indoc! {r#"
6999 use mod1::mod2::{mod3, mod4};
7000
7001 fn fn_1(param1: bool, param2: &str) {
7002 let var1 = "«ˇhello world»";
7003 }
7004 "#},
7005 cx,
7006 );
7007 });
7008
7009 // Test 5: Expansion beyond string
7010 editor.update_in(cx, |editor, window, cx| {
7011 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7012 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7013 assert_text_with_selections(
7014 editor,
7015 indoc! {r#"
7016 use mod1::mod2::{mod3, mod4};
7017
7018 fn fn_1(param1: bool, param2: &str) {
7019 «ˇlet var1 = "hello world";»
7020 }
7021 "#},
7022 cx,
7023 );
7024 });
7025}
7026
7027#[gpui::test]
7028async fn test_fold_function_bodies(cx: &mut TestAppContext) {
7029 init_test(cx, |_| {});
7030
7031 let base_text = r#"
7032 impl A {
7033 // this is an uncommitted comment
7034
7035 fn b() {
7036 c();
7037 }
7038
7039 // this is another uncommitted comment
7040
7041 fn d() {
7042 // e
7043 // f
7044 }
7045 }
7046
7047 fn g() {
7048 // h
7049 }
7050 "#
7051 .unindent();
7052
7053 let text = r#"
7054 ˇimpl A {
7055
7056 fn b() {
7057 c();
7058 }
7059
7060 fn d() {
7061 // e
7062 // f
7063 }
7064 }
7065
7066 fn g() {
7067 // h
7068 }
7069 "#
7070 .unindent();
7071
7072 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
7073 cx.set_state(&text);
7074 cx.set_head_text(&base_text);
7075 cx.update_editor(|editor, window, cx| {
7076 editor.expand_all_diff_hunks(&Default::default(), window, cx);
7077 });
7078
7079 cx.assert_state_with_diff(
7080 "
7081 ˇimpl A {
7082 - // this is an uncommitted comment
7083
7084 fn b() {
7085 c();
7086 }
7087
7088 - // this is another uncommitted comment
7089 -
7090 fn d() {
7091 // e
7092 // f
7093 }
7094 }
7095
7096 fn g() {
7097 // h
7098 }
7099 "
7100 .unindent(),
7101 );
7102
7103 let expected_display_text = "
7104 impl A {
7105 // this is an uncommitted comment
7106
7107 fn b() {
7108 ⋯
7109 }
7110
7111 // this is another uncommitted comment
7112
7113 fn d() {
7114 ⋯
7115 }
7116 }
7117
7118 fn g() {
7119 ⋯
7120 }
7121 "
7122 .unindent();
7123
7124 cx.update_editor(|editor, window, cx| {
7125 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
7126 assert_eq!(editor.display_text(cx), expected_display_text);
7127 });
7128}
7129
7130#[gpui::test]
7131async fn test_autoindent(cx: &mut TestAppContext) {
7132 init_test(cx, |_| {});
7133
7134 let language = Arc::new(
7135 Language::new(
7136 LanguageConfig {
7137 brackets: BracketPairConfig {
7138 pairs: vec![
7139 BracketPair {
7140 start: "{".to_string(),
7141 end: "}".to_string(),
7142 close: false,
7143 surround: false,
7144 newline: true,
7145 },
7146 BracketPair {
7147 start: "(".to_string(),
7148 end: ")".to_string(),
7149 close: false,
7150 surround: false,
7151 newline: true,
7152 },
7153 ],
7154 ..Default::default()
7155 },
7156 ..Default::default()
7157 },
7158 Some(tree_sitter_rust::LANGUAGE.into()),
7159 )
7160 .with_indents_query(
7161 r#"
7162 (_ "(" ")" @end) @indent
7163 (_ "{" "}" @end) @indent
7164 "#,
7165 )
7166 .unwrap(),
7167 );
7168
7169 let text = "fn a() {}";
7170
7171 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7172 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7173 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7174 editor
7175 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7176 .await;
7177
7178 editor.update_in(cx, |editor, window, cx| {
7179 editor.change_selections(None, window, cx, |s| s.select_ranges([5..5, 8..8, 9..9]));
7180 editor.newline(&Newline, window, cx);
7181 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
7182 assert_eq!(
7183 editor.selections.ranges(cx),
7184 &[
7185 Point::new(1, 4)..Point::new(1, 4),
7186 Point::new(3, 4)..Point::new(3, 4),
7187 Point::new(5, 0)..Point::new(5, 0)
7188 ]
7189 );
7190 });
7191}
7192
7193#[gpui::test]
7194async fn test_autoindent_selections(cx: &mut TestAppContext) {
7195 init_test(cx, |_| {});
7196
7197 {
7198 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
7199 cx.set_state(indoc! {"
7200 impl A {
7201
7202 fn b() {}
7203
7204 «fn c() {
7205
7206 }ˇ»
7207 }
7208 "});
7209
7210 cx.update_editor(|editor, window, cx| {
7211 editor.autoindent(&Default::default(), window, cx);
7212 });
7213
7214 cx.assert_editor_state(indoc! {"
7215 impl A {
7216
7217 fn b() {}
7218
7219 «fn c() {
7220
7221 }ˇ»
7222 }
7223 "});
7224 }
7225
7226 {
7227 let mut cx = EditorTestContext::new_multibuffer(
7228 cx,
7229 [indoc! { "
7230 impl A {
7231 «
7232 // a
7233 fn b(){}
7234 »
7235 «
7236 }
7237 fn c(){}
7238 »
7239 "}],
7240 );
7241
7242 let buffer = cx.update_editor(|editor, _, cx| {
7243 let buffer = editor.buffer().update(cx, |buffer, _| {
7244 buffer.all_buffers().iter().next().unwrap().clone()
7245 });
7246 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7247 buffer
7248 });
7249
7250 cx.run_until_parked();
7251 cx.update_editor(|editor, window, cx| {
7252 editor.select_all(&Default::default(), window, cx);
7253 editor.autoindent(&Default::default(), window, cx)
7254 });
7255 cx.run_until_parked();
7256
7257 cx.update(|_, cx| {
7258 assert_eq!(
7259 buffer.read(cx).text(),
7260 indoc! { "
7261 impl A {
7262
7263 // a
7264 fn b(){}
7265
7266
7267 }
7268 fn c(){}
7269
7270 " }
7271 )
7272 });
7273 }
7274}
7275
7276#[gpui::test]
7277async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
7278 init_test(cx, |_| {});
7279
7280 let mut cx = EditorTestContext::new(cx).await;
7281
7282 let language = Arc::new(Language::new(
7283 LanguageConfig {
7284 brackets: BracketPairConfig {
7285 pairs: vec![
7286 BracketPair {
7287 start: "{".to_string(),
7288 end: "}".to_string(),
7289 close: true,
7290 surround: true,
7291 newline: true,
7292 },
7293 BracketPair {
7294 start: "(".to_string(),
7295 end: ")".to_string(),
7296 close: true,
7297 surround: true,
7298 newline: true,
7299 },
7300 BracketPair {
7301 start: "/*".to_string(),
7302 end: " */".to_string(),
7303 close: true,
7304 surround: true,
7305 newline: true,
7306 },
7307 BracketPair {
7308 start: "[".to_string(),
7309 end: "]".to_string(),
7310 close: false,
7311 surround: false,
7312 newline: true,
7313 },
7314 BracketPair {
7315 start: "\"".to_string(),
7316 end: "\"".to_string(),
7317 close: true,
7318 surround: true,
7319 newline: false,
7320 },
7321 BracketPair {
7322 start: "<".to_string(),
7323 end: ">".to_string(),
7324 close: false,
7325 surround: true,
7326 newline: true,
7327 },
7328 ],
7329 ..Default::default()
7330 },
7331 autoclose_before: "})]".to_string(),
7332 ..Default::default()
7333 },
7334 Some(tree_sitter_rust::LANGUAGE.into()),
7335 ));
7336
7337 cx.language_registry().add(language.clone());
7338 cx.update_buffer(|buffer, cx| {
7339 buffer.set_language(Some(language), cx);
7340 });
7341
7342 cx.set_state(
7343 &r#"
7344 🏀ˇ
7345 εˇ
7346 ❤️ˇ
7347 "#
7348 .unindent(),
7349 );
7350
7351 // autoclose multiple nested brackets at multiple cursors
7352 cx.update_editor(|editor, window, cx| {
7353 editor.handle_input("{", window, cx);
7354 editor.handle_input("{", window, cx);
7355 editor.handle_input("{", window, cx);
7356 });
7357 cx.assert_editor_state(
7358 &"
7359 🏀{{{ˇ}}}
7360 ε{{{ˇ}}}
7361 ❤️{{{ˇ}}}
7362 "
7363 .unindent(),
7364 );
7365
7366 // insert a different closing bracket
7367 cx.update_editor(|editor, window, cx| {
7368 editor.handle_input(")", window, cx);
7369 });
7370 cx.assert_editor_state(
7371 &"
7372 🏀{{{)ˇ}}}
7373 ε{{{)ˇ}}}
7374 ❤️{{{)ˇ}}}
7375 "
7376 .unindent(),
7377 );
7378
7379 // skip over the auto-closed brackets when typing a closing bracket
7380 cx.update_editor(|editor, window, cx| {
7381 editor.move_right(&MoveRight, window, cx);
7382 editor.handle_input("}", window, cx);
7383 editor.handle_input("}", window, cx);
7384 editor.handle_input("}", window, cx);
7385 });
7386 cx.assert_editor_state(
7387 &"
7388 🏀{{{)}}}}ˇ
7389 ε{{{)}}}}ˇ
7390 ❤️{{{)}}}}ˇ
7391 "
7392 .unindent(),
7393 );
7394
7395 // autoclose multi-character pairs
7396 cx.set_state(
7397 &"
7398 ˇ
7399 ˇ
7400 "
7401 .unindent(),
7402 );
7403 cx.update_editor(|editor, window, cx| {
7404 editor.handle_input("/", window, cx);
7405 editor.handle_input("*", window, cx);
7406 });
7407 cx.assert_editor_state(
7408 &"
7409 /*ˇ */
7410 /*ˇ */
7411 "
7412 .unindent(),
7413 );
7414
7415 // one cursor autocloses a multi-character pair, one cursor
7416 // does not autoclose.
7417 cx.set_state(
7418 &"
7419 /ˇ
7420 ˇ
7421 "
7422 .unindent(),
7423 );
7424 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
7425 cx.assert_editor_state(
7426 &"
7427 /*ˇ */
7428 *ˇ
7429 "
7430 .unindent(),
7431 );
7432
7433 // Don't autoclose if the next character isn't whitespace and isn't
7434 // listed in the language's "autoclose_before" section.
7435 cx.set_state("ˇa b");
7436 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7437 cx.assert_editor_state("{ˇa b");
7438
7439 // Don't autoclose if `close` is false for the bracket pair
7440 cx.set_state("ˇ");
7441 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
7442 cx.assert_editor_state("[ˇ");
7443
7444 // Surround with brackets if text is selected
7445 cx.set_state("«aˇ» b");
7446 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7447 cx.assert_editor_state("{«aˇ»} b");
7448
7449 // Autoclose when not immediately after a word character
7450 cx.set_state("a ˇ");
7451 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7452 cx.assert_editor_state("a \"ˇ\"");
7453
7454 // Autoclose pair where the start and end characters are the same
7455 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7456 cx.assert_editor_state("a \"\"ˇ");
7457
7458 // Don't autoclose when immediately after a word character
7459 cx.set_state("aˇ");
7460 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7461 cx.assert_editor_state("a\"ˇ");
7462
7463 // Do autoclose when after a non-word character
7464 cx.set_state("{ˇ");
7465 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7466 cx.assert_editor_state("{\"ˇ\"");
7467
7468 // Non identical pairs autoclose regardless of preceding character
7469 cx.set_state("aˇ");
7470 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7471 cx.assert_editor_state("a{ˇ}");
7472
7473 // Don't autoclose pair if autoclose is disabled
7474 cx.set_state("ˇ");
7475 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
7476 cx.assert_editor_state("<ˇ");
7477
7478 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
7479 cx.set_state("«aˇ» b");
7480 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
7481 cx.assert_editor_state("<«aˇ»> b");
7482}
7483
7484#[gpui::test]
7485async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
7486 init_test(cx, |settings| {
7487 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
7488 });
7489
7490 let mut cx = EditorTestContext::new(cx).await;
7491
7492 let language = Arc::new(Language::new(
7493 LanguageConfig {
7494 brackets: BracketPairConfig {
7495 pairs: vec![
7496 BracketPair {
7497 start: "{".to_string(),
7498 end: "}".to_string(),
7499 close: true,
7500 surround: true,
7501 newline: true,
7502 },
7503 BracketPair {
7504 start: "(".to_string(),
7505 end: ")".to_string(),
7506 close: true,
7507 surround: true,
7508 newline: true,
7509 },
7510 BracketPair {
7511 start: "[".to_string(),
7512 end: "]".to_string(),
7513 close: false,
7514 surround: false,
7515 newline: true,
7516 },
7517 ],
7518 ..Default::default()
7519 },
7520 autoclose_before: "})]".to_string(),
7521 ..Default::default()
7522 },
7523 Some(tree_sitter_rust::LANGUAGE.into()),
7524 ));
7525
7526 cx.language_registry().add(language.clone());
7527 cx.update_buffer(|buffer, cx| {
7528 buffer.set_language(Some(language), cx);
7529 });
7530
7531 cx.set_state(
7532 &"
7533 ˇ
7534 ˇ
7535 ˇ
7536 "
7537 .unindent(),
7538 );
7539
7540 // ensure only matching closing brackets are skipped over
7541 cx.update_editor(|editor, window, cx| {
7542 editor.handle_input("}", window, cx);
7543 editor.move_left(&MoveLeft, window, cx);
7544 editor.handle_input(")", window, cx);
7545 editor.move_left(&MoveLeft, window, cx);
7546 });
7547 cx.assert_editor_state(
7548 &"
7549 ˇ)}
7550 ˇ)}
7551 ˇ)}
7552 "
7553 .unindent(),
7554 );
7555
7556 // skip-over closing brackets at multiple cursors
7557 cx.update_editor(|editor, window, cx| {
7558 editor.handle_input(")", window, cx);
7559 editor.handle_input("}", window, cx);
7560 });
7561 cx.assert_editor_state(
7562 &"
7563 )}ˇ
7564 )}ˇ
7565 )}ˇ
7566 "
7567 .unindent(),
7568 );
7569
7570 // ignore non-close brackets
7571 cx.update_editor(|editor, window, cx| {
7572 editor.handle_input("]", window, cx);
7573 editor.move_left(&MoveLeft, window, cx);
7574 editor.handle_input("]", window, cx);
7575 });
7576 cx.assert_editor_state(
7577 &"
7578 )}]ˇ]
7579 )}]ˇ]
7580 )}]ˇ]
7581 "
7582 .unindent(),
7583 );
7584}
7585
7586#[gpui::test]
7587async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
7588 init_test(cx, |_| {});
7589
7590 let mut cx = EditorTestContext::new(cx).await;
7591
7592 let html_language = Arc::new(
7593 Language::new(
7594 LanguageConfig {
7595 name: "HTML".into(),
7596 brackets: BracketPairConfig {
7597 pairs: vec![
7598 BracketPair {
7599 start: "<".into(),
7600 end: ">".into(),
7601 close: true,
7602 ..Default::default()
7603 },
7604 BracketPair {
7605 start: "{".into(),
7606 end: "}".into(),
7607 close: true,
7608 ..Default::default()
7609 },
7610 BracketPair {
7611 start: "(".into(),
7612 end: ")".into(),
7613 close: true,
7614 ..Default::default()
7615 },
7616 ],
7617 ..Default::default()
7618 },
7619 autoclose_before: "})]>".into(),
7620 ..Default::default()
7621 },
7622 Some(tree_sitter_html::LANGUAGE.into()),
7623 )
7624 .with_injection_query(
7625 r#"
7626 (script_element
7627 (raw_text) @injection.content
7628 (#set! injection.language "javascript"))
7629 "#,
7630 )
7631 .unwrap(),
7632 );
7633
7634 let javascript_language = Arc::new(Language::new(
7635 LanguageConfig {
7636 name: "JavaScript".into(),
7637 brackets: BracketPairConfig {
7638 pairs: vec![
7639 BracketPair {
7640 start: "/*".into(),
7641 end: " */".into(),
7642 close: true,
7643 ..Default::default()
7644 },
7645 BracketPair {
7646 start: "{".into(),
7647 end: "}".into(),
7648 close: true,
7649 ..Default::default()
7650 },
7651 BracketPair {
7652 start: "(".into(),
7653 end: ")".into(),
7654 close: true,
7655 ..Default::default()
7656 },
7657 ],
7658 ..Default::default()
7659 },
7660 autoclose_before: "})]>".into(),
7661 ..Default::default()
7662 },
7663 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
7664 ));
7665
7666 cx.language_registry().add(html_language.clone());
7667 cx.language_registry().add(javascript_language.clone());
7668
7669 cx.update_buffer(|buffer, cx| {
7670 buffer.set_language(Some(html_language), cx);
7671 });
7672
7673 cx.set_state(
7674 &r#"
7675 <body>ˇ
7676 <script>
7677 var x = 1;ˇ
7678 </script>
7679 </body>ˇ
7680 "#
7681 .unindent(),
7682 );
7683
7684 // Precondition: different languages are active at different locations.
7685 cx.update_editor(|editor, window, cx| {
7686 let snapshot = editor.snapshot(window, cx);
7687 let cursors = editor.selections.ranges::<usize>(cx);
7688 let languages = cursors
7689 .iter()
7690 .map(|c| snapshot.language_at(c.start).unwrap().name())
7691 .collect::<Vec<_>>();
7692 assert_eq!(
7693 languages,
7694 &["HTML".into(), "JavaScript".into(), "HTML".into()]
7695 );
7696 });
7697
7698 // Angle brackets autoclose in HTML, but not JavaScript.
7699 cx.update_editor(|editor, window, cx| {
7700 editor.handle_input("<", window, cx);
7701 editor.handle_input("a", window, cx);
7702 });
7703 cx.assert_editor_state(
7704 &r#"
7705 <body><aˇ>
7706 <script>
7707 var x = 1;<aˇ
7708 </script>
7709 </body><aˇ>
7710 "#
7711 .unindent(),
7712 );
7713
7714 // Curly braces and parens autoclose in both HTML and JavaScript.
7715 cx.update_editor(|editor, window, cx| {
7716 editor.handle_input(" b=", window, cx);
7717 editor.handle_input("{", window, cx);
7718 editor.handle_input("c", window, cx);
7719 editor.handle_input("(", window, cx);
7720 });
7721 cx.assert_editor_state(
7722 &r#"
7723 <body><a b={c(ˇ)}>
7724 <script>
7725 var x = 1;<a b={c(ˇ)}
7726 </script>
7727 </body><a b={c(ˇ)}>
7728 "#
7729 .unindent(),
7730 );
7731
7732 // Brackets that were already autoclosed are skipped.
7733 cx.update_editor(|editor, window, cx| {
7734 editor.handle_input(")", window, cx);
7735 editor.handle_input("d", window, cx);
7736 editor.handle_input("}", window, cx);
7737 });
7738 cx.assert_editor_state(
7739 &r#"
7740 <body><a b={c()d}ˇ>
7741 <script>
7742 var x = 1;<a b={c()d}ˇ
7743 </script>
7744 </body><a b={c()d}ˇ>
7745 "#
7746 .unindent(),
7747 );
7748 cx.update_editor(|editor, window, cx| {
7749 editor.handle_input(">", window, cx);
7750 });
7751 cx.assert_editor_state(
7752 &r#"
7753 <body><a b={c()d}>ˇ
7754 <script>
7755 var x = 1;<a b={c()d}>ˇ
7756 </script>
7757 </body><a b={c()d}>ˇ
7758 "#
7759 .unindent(),
7760 );
7761
7762 // Reset
7763 cx.set_state(
7764 &r#"
7765 <body>ˇ
7766 <script>
7767 var x = 1;ˇ
7768 </script>
7769 </body>ˇ
7770 "#
7771 .unindent(),
7772 );
7773
7774 cx.update_editor(|editor, window, cx| {
7775 editor.handle_input("<", window, cx);
7776 });
7777 cx.assert_editor_state(
7778 &r#"
7779 <body><ˇ>
7780 <script>
7781 var x = 1;<ˇ
7782 </script>
7783 </body><ˇ>
7784 "#
7785 .unindent(),
7786 );
7787
7788 // When backspacing, the closing angle brackets are removed.
7789 cx.update_editor(|editor, window, cx| {
7790 editor.backspace(&Backspace, window, cx);
7791 });
7792 cx.assert_editor_state(
7793 &r#"
7794 <body>ˇ
7795 <script>
7796 var x = 1;ˇ
7797 </script>
7798 </body>ˇ
7799 "#
7800 .unindent(),
7801 );
7802
7803 // Block comments autoclose in JavaScript, but not HTML.
7804 cx.update_editor(|editor, window, cx| {
7805 editor.handle_input("/", window, cx);
7806 editor.handle_input("*", window, cx);
7807 });
7808 cx.assert_editor_state(
7809 &r#"
7810 <body>/*ˇ
7811 <script>
7812 var x = 1;/*ˇ */
7813 </script>
7814 </body>/*ˇ
7815 "#
7816 .unindent(),
7817 );
7818}
7819
7820#[gpui::test]
7821async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
7822 init_test(cx, |_| {});
7823
7824 let mut cx = EditorTestContext::new(cx).await;
7825
7826 let rust_language = Arc::new(
7827 Language::new(
7828 LanguageConfig {
7829 name: "Rust".into(),
7830 brackets: serde_json::from_value(json!([
7831 { "start": "{", "end": "}", "close": true, "newline": true },
7832 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
7833 ]))
7834 .unwrap(),
7835 autoclose_before: "})]>".into(),
7836 ..Default::default()
7837 },
7838 Some(tree_sitter_rust::LANGUAGE.into()),
7839 )
7840 .with_override_query("(string_literal) @string")
7841 .unwrap(),
7842 );
7843
7844 cx.language_registry().add(rust_language.clone());
7845 cx.update_buffer(|buffer, cx| {
7846 buffer.set_language(Some(rust_language), cx);
7847 });
7848
7849 cx.set_state(
7850 &r#"
7851 let x = ˇ
7852 "#
7853 .unindent(),
7854 );
7855
7856 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
7857 cx.update_editor(|editor, window, cx| {
7858 editor.handle_input("\"", window, cx);
7859 });
7860 cx.assert_editor_state(
7861 &r#"
7862 let x = "ˇ"
7863 "#
7864 .unindent(),
7865 );
7866
7867 // Inserting another quotation mark. The cursor moves across the existing
7868 // automatically-inserted quotation mark.
7869 cx.update_editor(|editor, window, cx| {
7870 editor.handle_input("\"", window, cx);
7871 });
7872 cx.assert_editor_state(
7873 &r#"
7874 let x = ""ˇ
7875 "#
7876 .unindent(),
7877 );
7878
7879 // Reset
7880 cx.set_state(
7881 &r#"
7882 let x = ˇ
7883 "#
7884 .unindent(),
7885 );
7886
7887 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
7888 cx.update_editor(|editor, window, cx| {
7889 editor.handle_input("\"", window, cx);
7890 editor.handle_input(" ", window, cx);
7891 editor.move_left(&Default::default(), window, cx);
7892 editor.handle_input("\\", window, cx);
7893 editor.handle_input("\"", window, cx);
7894 });
7895 cx.assert_editor_state(
7896 &r#"
7897 let x = "\"ˇ "
7898 "#
7899 .unindent(),
7900 );
7901
7902 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
7903 // mark. Nothing is inserted.
7904 cx.update_editor(|editor, window, cx| {
7905 editor.move_right(&Default::default(), window, cx);
7906 editor.handle_input("\"", window, cx);
7907 });
7908 cx.assert_editor_state(
7909 &r#"
7910 let x = "\" "ˇ
7911 "#
7912 .unindent(),
7913 );
7914}
7915
7916#[gpui::test]
7917async fn test_surround_with_pair(cx: &mut TestAppContext) {
7918 init_test(cx, |_| {});
7919
7920 let language = Arc::new(Language::new(
7921 LanguageConfig {
7922 brackets: BracketPairConfig {
7923 pairs: vec![
7924 BracketPair {
7925 start: "{".to_string(),
7926 end: "}".to_string(),
7927 close: true,
7928 surround: true,
7929 newline: true,
7930 },
7931 BracketPair {
7932 start: "/* ".to_string(),
7933 end: "*/".to_string(),
7934 close: true,
7935 surround: true,
7936 ..Default::default()
7937 },
7938 ],
7939 ..Default::default()
7940 },
7941 ..Default::default()
7942 },
7943 Some(tree_sitter_rust::LANGUAGE.into()),
7944 ));
7945
7946 let text = r#"
7947 a
7948 b
7949 c
7950 "#
7951 .unindent();
7952
7953 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7954 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7955 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7956 editor
7957 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7958 .await;
7959
7960 editor.update_in(cx, |editor, window, cx| {
7961 editor.change_selections(None, window, cx, |s| {
7962 s.select_display_ranges([
7963 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7964 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7965 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
7966 ])
7967 });
7968
7969 editor.handle_input("{", window, cx);
7970 editor.handle_input("{", window, cx);
7971 editor.handle_input("{", window, cx);
7972 assert_eq!(
7973 editor.text(cx),
7974 "
7975 {{{a}}}
7976 {{{b}}}
7977 {{{c}}}
7978 "
7979 .unindent()
7980 );
7981 assert_eq!(
7982 editor.selections.display_ranges(cx),
7983 [
7984 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
7985 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
7986 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
7987 ]
7988 );
7989
7990 editor.undo(&Undo, window, cx);
7991 editor.undo(&Undo, window, cx);
7992 editor.undo(&Undo, window, cx);
7993 assert_eq!(
7994 editor.text(cx),
7995 "
7996 a
7997 b
7998 c
7999 "
8000 .unindent()
8001 );
8002 assert_eq!(
8003 editor.selections.display_ranges(cx),
8004 [
8005 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8006 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8007 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
8008 ]
8009 );
8010
8011 // Ensure inserting the first character of a multi-byte bracket pair
8012 // doesn't surround the selections with the bracket.
8013 editor.handle_input("/", window, cx);
8014 assert_eq!(
8015 editor.text(cx),
8016 "
8017 /
8018 /
8019 /
8020 "
8021 .unindent()
8022 );
8023 assert_eq!(
8024 editor.selections.display_ranges(cx),
8025 [
8026 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
8027 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
8028 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
8029 ]
8030 );
8031
8032 editor.undo(&Undo, window, cx);
8033 assert_eq!(
8034 editor.text(cx),
8035 "
8036 a
8037 b
8038 c
8039 "
8040 .unindent()
8041 );
8042 assert_eq!(
8043 editor.selections.display_ranges(cx),
8044 [
8045 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8046 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8047 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
8048 ]
8049 );
8050
8051 // Ensure inserting the last character of a multi-byte bracket pair
8052 // doesn't surround the selections with the bracket.
8053 editor.handle_input("*", window, cx);
8054 assert_eq!(
8055 editor.text(cx),
8056 "
8057 *
8058 *
8059 *
8060 "
8061 .unindent()
8062 );
8063 assert_eq!(
8064 editor.selections.display_ranges(cx),
8065 [
8066 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
8067 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
8068 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
8069 ]
8070 );
8071 });
8072}
8073
8074#[gpui::test]
8075async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
8076 init_test(cx, |_| {});
8077
8078 let language = Arc::new(Language::new(
8079 LanguageConfig {
8080 brackets: BracketPairConfig {
8081 pairs: vec![BracketPair {
8082 start: "{".to_string(),
8083 end: "}".to_string(),
8084 close: true,
8085 surround: true,
8086 newline: true,
8087 }],
8088 ..Default::default()
8089 },
8090 autoclose_before: "}".to_string(),
8091 ..Default::default()
8092 },
8093 Some(tree_sitter_rust::LANGUAGE.into()),
8094 ));
8095
8096 let text = r#"
8097 a
8098 b
8099 c
8100 "#
8101 .unindent();
8102
8103 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8104 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8105 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8106 editor
8107 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8108 .await;
8109
8110 editor.update_in(cx, |editor, window, cx| {
8111 editor.change_selections(None, window, cx, |s| {
8112 s.select_ranges([
8113 Point::new(0, 1)..Point::new(0, 1),
8114 Point::new(1, 1)..Point::new(1, 1),
8115 Point::new(2, 1)..Point::new(2, 1),
8116 ])
8117 });
8118
8119 editor.handle_input("{", window, cx);
8120 editor.handle_input("{", window, cx);
8121 editor.handle_input("_", window, cx);
8122 assert_eq!(
8123 editor.text(cx),
8124 "
8125 a{{_}}
8126 b{{_}}
8127 c{{_}}
8128 "
8129 .unindent()
8130 );
8131 assert_eq!(
8132 editor.selections.ranges::<Point>(cx),
8133 [
8134 Point::new(0, 4)..Point::new(0, 4),
8135 Point::new(1, 4)..Point::new(1, 4),
8136 Point::new(2, 4)..Point::new(2, 4)
8137 ]
8138 );
8139
8140 editor.backspace(&Default::default(), window, cx);
8141 editor.backspace(&Default::default(), window, cx);
8142 assert_eq!(
8143 editor.text(cx),
8144 "
8145 a{}
8146 b{}
8147 c{}
8148 "
8149 .unindent()
8150 );
8151 assert_eq!(
8152 editor.selections.ranges::<Point>(cx),
8153 [
8154 Point::new(0, 2)..Point::new(0, 2),
8155 Point::new(1, 2)..Point::new(1, 2),
8156 Point::new(2, 2)..Point::new(2, 2)
8157 ]
8158 );
8159
8160 editor.delete_to_previous_word_start(&Default::default(), window, cx);
8161 assert_eq!(
8162 editor.text(cx),
8163 "
8164 a
8165 b
8166 c
8167 "
8168 .unindent()
8169 );
8170 assert_eq!(
8171 editor.selections.ranges::<Point>(cx),
8172 [
8173 Point::new(0, 1)..Point::new(0, 1),
8174 Point::new(1, 1)..Point::new(1, 1),
8175 Point::new(2, 1)..Point::new(2, 1)
8176 ]
8177 );
8178 });
8179}
8180
8181#[gpui::test]
8182async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
8183 init_test(cx, |settings| {
8184 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
8185 });
8186
8187 let mut cx = EditorTestContext::new(cx).await;
8188
8189 let language = Arc::new(Language::new(
8190 LanguageConfig {
8191 brackets: BracketPairConfig {
8192 pairs: vec![
8193 BracketPair {
8194 start: "{".to_string(),
8195 end: "}".to_string(),
8196 close: true,
8197 surround: true,
8198 newline: true,
8199 },
8200 BracketPair {
8201 start: "(".to_string(),
8202 end: ")".to_string(),
8203 close: true,
8204 surround: true,
8205 newline: true,
8206 },
8207 BracketPair {
8208 start: "[".to_string(),
8209 end: "]".to_string(),
8210 close: false,
8211 surround: true,
8212 newline: true,
8213 },
8214 ],
8215 ..Default::default()
8216 },
8217 autoclose_before: "})]".to_string(),
8218 ..Default::default()
8219 },
8220 Some(tree_sitter_rust::LANGUAGE.into()),
8221 ));
8222
8223 cx.language_registry().add(language.clone());
8224 cx.update_buffer(|buffer, cx| {
8225 buffer.set_language(Some(language), cx);
8226 });
8227
8228 cx.set_state(
8229 &"
8230 {(ˇ)}
8231 [[ˇ]]
8232 {(ˇ)}
8233 "
8234 .unindent(),
8235 );
8236
8237 cx.update_editor(|editor, window, cx| {
8238 editor.backspace(&Default::default(), window, cx);
8239 editor.backspace(&Default::default(), window, cx);
8240 });
8241
8242 cx.assert_editor_state(
8243 &"
8244 ˇ
8245 ˇ]]
8246 ˇ
8247 "
8248 .unindent(),
8249 );
8250
8251 cx.update_editor(|editor, window, cx| {
8252 editor.handle_input("{", window, cx);
8253 editor.handle_input("{", window, cx);
8254 editor.move_right(&MoveRight, window, cx);
8255 editor.move_right(&MoveRight, window, cx);
8256 editor.move_left(&MoveLeft, window, cx);
8257 editor.move_left(&MoveLeft, window, cx);
8258 editor.backspace(&Default::default(), window, cx);
8259 });
8260
8261 cx.assert_editor_state(
8262 &"
8263 {ˇ}
8264 {ˇ}]]
8265 {ˇ}
8266 "
8267 .unindent(),
8268 );
8269
8270 cx.update_editor(|editor, window, cx| {
8271 editor.backspace(&Default::default(), window, cx);
8272 });
8273
8274 cx.assert_editor_state(
8275 &"
8276 ˇ
8277 ˇ]]
8278 ˇ
8279 "
8280 .unindent(),
8281 );
8282}
8283
8284#[gpui::test]
8285async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
8286 init_test(cx, |_| {});
8287
8288 let language = Arc::new(Language::new(
8289 LanguageConfig::default(),
8290 Some(tree_sitter_rust::LANGUAGE.into()),
8291 ));
8292
8293 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
8294 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8295 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8296 editor
8297 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8298 .await;
8299
8300 editor.update_in(cx, |editor, window, cx| {
8301 editor.set_auto_replace_emoji_shortcode(true);
8302
8303 editor.handle_input("Hello ", window, cx);
8304 editor.handle_input(":wave", window, cx);
8305 assert_eq!(editor.text(cx), "Hello :wave".unindent());
8306
8307 editor.handle_input(":", window, cx);
8308 assert_eq!(editor.text(cx), "Hello 👋".unindent());
8309
8310 editor.handle_input(" :smile", window, cx);
8311 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
8312
8313 editor.handle_input(":", window, cx);
8314 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
8315
8316 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
8317 editor.handle_input(":wave", window, cx);
8318 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
8319
8320 editor.handle_input(":", window, cx);
8321 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
8322
8323 editor.handle_input(":1", window, cx);
8324 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
8325
8326 editor.handle_input(":", window, cx);
8327 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
8328
8329 // Ensure shortcode does not get replaced when it is part of a word
8330 editor.handle_input(" Test:wave", window, cx);
8331 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
8332
8333 editor.handle_input(":", window, cx);
8334 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
8335
8336 editor.set_auto_replace_emoji_shortcode(false);
8337
8338 // Ensure shortcode does not get replaced when auto replace is off
8339 editor.handle_input(" :wave", window, cx);
8340 assert_eq!(
8341 editor.text(cx),
8342 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
8343 );
8344
8345 editor.handle_input(":", window, cx);
8346 assert_eq!(
8347 editor.text(cx),
8348 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
8349 );
8350 });
8351}
8352
8353#[gpui::test]
8354async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
8355 init_test(cx, |_| {});
8356
8357 let (text, insertion_ranges) = marked_text_ranges(
8358 indoc! {"
8359 ˇ
8360 "},
8361 false,
8362 );
8363
8364 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
8365 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8366
8367 _ = editor.update_in(cx, |editor, window, cx| {
8368 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
8369
8370 editor
8371 .insert_snippet(&insertion_ranges, snippet, window, cx)
8372 .unwrap();
8373
8374 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
8375 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
8376 assert_eq!(editor.text(cx), expected_text);
8377 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
8378 }
8379
8380 assert(
8381 editor,
8382 cx,
8383 indoc! {"
8384 type «» =•
8385 "},
8386 );
8387
8388 assert!(editor.context_menu_visible(), "There should be a matches");
8389 });
8390}
8391
8392#[gpui::test]
8393async fn test_snippets(cx: &mut TestAppContext) {
8394 init_test(cx, |_| {});
8395
8396 let (text, insertion_ranges) = marked_text_ranges(
8397 indoc! {"
8398 a.ˇ b
8399 a.ˇ b
8400 a.ˇ b
8401 "},
8402 false,
8403 );
8404
8405 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
8406 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8407
8408 editor.update_in(cx, |editor, window, cx| {
8409 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
8410
8411 editor
8412 .insert_snippet(&insertion_ranges, snippet, window, cx)
8413 .unwrap();
8414
8415 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
8416 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
8417 assert_eq!(editor.text(cx), expected_text);
8418 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
8419 }
8420
8421 assert(
8422 editor,
8423 cx,
8424 indoc! {"
8425 a.f(«one», two, «three») b
8426 a.f(«one», two, «three») b
8427 a.f(«one», two, «three») b
8428 "},
8429 );
8430
8431 // Can't move earlier than the first tab stop
8432 assert!(!editor.move_to_prev_snippet_tabstop(window, cx));
8433 assert(
8434 editor,
8435 cx,
8436 indoc! {"
8437 a.f(«one», two, «three») b
8438 a.f(«one», two, «three») b
8439 a.f(«one», two, «three») b
8440 "},
8441 );
8442
8443 assert!(editor.move_to_next_snippet_tabstop(window, cx));
8444 assert(
8445 editor,
8446 cx,
8447 indoc! {"
8448 a.f(one, «two», three) b
8449 a.f(one, «two», three) b
8450 a.f(one, «two», three) b
8451 "},
8452 );
8453
8454 editor.move_to_prev_snippet_tabstop(window, cx);
8455 assert(
8456 editor,
8457 cx,
8458 indoc! {"
8459 a.f(«one», two, «three») b
8460 a.f(«one», two, «three») b
8461 a.f(«one», two, «three») b
8462 "},
8463 );
8464
8465 assert!(editor.move_to_next_snippet_tabstop(window, cx));
8466 assert(
8467 editor,
8468 cx,
8469 indoc! {"
8470 a.f(one, «two», three) b
8471 a.f(one, «two», three) b
8472 a.f(one, «two», three) b
8473 "},
8474 );
8475 assert!(editor.move_to_next_snippet_tabstop(window, cx));
8476 assert(
8477 editor,
8478 cx,
8479 indoc! {"
8480 a.f(one, two, three)ˇ b
8481 a.f(one, two, three)ˇ b
8482 a.f(one, two, three)ˇ b
8483 "},
8484 );
8485
8486 // As soon as the last tab stop is reached, snippet state is gone
8487 editor.move_to_prev_snippet_tabstop(window, cx);
8488 assert(
8489 editor,
8490 cx,
8491 indoc! {"
8492 a.f(one, two, three)ˇ b
8493 a.f(one, two, three)ˇ b
8494 a.f(one, two, three)ˇ b
8495 "},
8496 );
8497 });
8498}
8499
8500#[gpui::test]
8501async fn test_document_format_during_save(cx: &mut TestAppContext) {
8502 init_test(cx, |_| {});
8503
8504 let fs = FakeFs::new(cx.executor());
8505 fs.insert_file(path!("/file.rs"), Default::default()).await;
8506
8507 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
8508
8509 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8510 language_registry.add(rust_lang());
8511 let mut fake_servers = language_registry.register_fake_lsp(
8512 "Rust",
8513 FakeLspAdapter {
8514 capabilities: lsp::ServerCapabilities {
8515 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8516 ..Default::default()
8517 },
8518 ..Default::default()
8519 },
8520 );
8521
8522 let buffer = project
8523 .update(cx, |project, cx| {
8524 project.open_local_buffer(path!("/file.rs"), cx)
8525 })
8526 .await
8527 .unwrap();
8528
8529 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8530 let (editor, cx) = cx.add_window_view(|window, cx| {
8531 build_editor_with_project(project.clone(), buffer, window, cx)
8532 });
8533 editor.update_in(cx, |editor, window, cx| {
8534 editor.set_text("one\ntwo\nthree\n", window, cx)
8535 });
8536 assert!(cx.read(|cx| editor.is_dirty(cx)));
8537
8538 cx.executor().start_waiting();
8539 let fake_server = fake_servers.next().await.unwrap();
8540
8541 {
8542 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8543 move |params, _| async move {
8544 assert_eq!(
8545 params.text_document.uri,
8546 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8547 );
8548 assert_eq!(params.options.tab_size, 4);
8549 Ok(Some(vec![lsp::TextEdit::new(
8550 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8551 ", ".to_string(),
8552 )]))
8553 },
8554 );
8555 let save = editor
8556 .update_in(cx, |editor, window, cx| {
8557 editor.save(true, project.clone(), window, cx)
8558 })
8559 .unwrap();
8560 cx.executor().start_waiting();
8561 save.await;
8562
8563 assert_eq!(
8564 editor.update(cx, |editor, cx| editor.text(cx)),
8565 "one, two\nthree\n"
8566 );
8567 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8568 }
8569
8570 {
8571 editor.update_in(cx, |editor, window, cx| {
8572 editor.set_text("one\ntwo\nthree\n", window, cx)
8573 });
8574 assert!(cx.read(|cx| editor.is_dirty(cx)));
8575
8576 // Ensure we can still save even if formatting hangs.
8577 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8578 move |params, _| async move {
8579 assert_eq!(
8580 params.text_document.uri,
8581 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8582 );
8583 futures::future::pending::<()>().await;
8584 unreachable!()
8585 },
8586 );
8587 let save = editor
8588 .update_in(cx, |editor, window, cx| {
8589 editor.save(true, project.clone(), window, cx)
8590 })
8591 .unwrap();
8592 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8593 cx.executor().start_waiting();
8594 save.await;
8595 assert_eq!(
8596 editor.update(cx, |editor, cx| editor.text(cx)),
8597 "one\ntwo\nthree\n"
8598 );
8599 }
8600
8601 // For non-dirty buffer, no formatting request should be sent
8602 {
8603 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8604
8605 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
8606 panic!("Should not be invoked on non-dirty buffer");
8607 });
8608 let save = editor
8609 .update_in(cx, |editor, window, cx| {
8610 editor.save(true, project.clone(), window, cx)
8611 })
8612 .unwrap();
8613 cx.executor().start_waiting();
8614 save.await;
8615 }
8616
8617 // Set rust language override and assert overridden tabsize is sent to language server
8618 update_test_language_settings(cx, |settings| {
8619 settings.languages.insert(
8620 "Rust".into(),
8621 LanguageSettingsContent {
8622 tab_size: NonZeroU32::new(8),
8623 ..Default::default()
8624 },
8625 );
8626 });
8627
8628 {
8629 editor.update_in(cx, |editor, window, cx| {
8630 editor.set_text("somehting_new\n", window, cx)
8631 });
8632 assert!(cx.read(|cx| editor.is_dirty(cx)));
8633 let _formatting_request_signal = fake_server
8634 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
8635 assert_eq!(
8636 params.text_document.uri,
8637 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8638 );
8639 assert_eq!(params.options.tab_size, 8);
8640 Ok(Some(vec![]))
8641 });
8642 let save = editor
8643 .update_in(cx, |editor, window, cx| {
8644 editor.save(true, project.clone(), window, cx)
8645 })
8646 .unwrap();
8647 cx.executor().start_waiting();
8648 save.await;
8649 }
8650}
8651
8652#[gpui::test]
8653async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
8654 init_test(cx, |_| {});
8655
8656 let cols = 4;
8657 let rows = 10;
8658 let sample_text_1 = sample_text(rows, cols, 'a');
8659 assert_eq!(
8660 sample_text_1,
8661 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
8662 );
8663 let sample_text_2 = sample_text(rows, cols, 'l');
8664 assert_eq!(
8665 sample_text_2,
8666 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
8667 );
8668 let sample_text_3 = sample_text(rows, cols, 'v');
8669 assert_eq!(
8670 sample_text_3,
8671 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
8672 );
8673
8674 let fs = FakeFs::new(cx.executor());
8675 fs.insert_tree(
8676 path!("/a"),
8677 json!({
8678 "main.rs": sample_text_1,
8679 "other.rs": sample_text_2,
8680 "lib.rs": sample_text_3,
8681 }),
8682 )
8683 .await;
8684
8685 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
8686 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
8687 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
8688
8689 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8690 language_registry.add(rust_lang());
8691 let mut fake_servers = language_registry.register_fake_lsp(
8692 "Rust",
8693 FakeLspAdapter {
8694 capabilities: lsp::ServerCapabilities {
8695 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8696 ..Default::default()
8697 },
8698 ..Default::default()
8699 },
8700 );
8701
8702 let worktree = project.update(cx, |project, cx| {
8703 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
8704 assert_eq!(worktrees.len(), 1);
8705 worktrees.pop().unwrap()
8706 });
8707 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
8708
8709 let buffer_1 = project
8710 .update(cx, |project, cx| {
8711 project.open_buffer((worktree_id, "main.rs"), cx)
8712 })
8713 .await
8714 .unwrap();
8715 let buffer_2 = project
8716 .update(cx, |project, cx| {
8717 project.open_buffer((worktree_id, "other.rs"), cx)
8718 })
8719 .await
8720 .unwrap();
8721 let buffer_3 = project
8722 .update(cx, |project, cx| {
8723 project.open_buffer((worktree_id, "lib.rs"), cx)
8724 })
8725 .await
8726 .unwrap();
8727
8728 let multi_buffer = cx.new(|cx| {
8729 let mut multi_buffer = MultiBuffer::new(ReadWrite);
8730 multi_buffer.push_excerpts(
8731 buffer_1.clone(),
8732 [
8733 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8734 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8735 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8736 ],
8737 cx,
8738 );
8739 multi_buffer.push_excerpts(
8740 buffer_2.clone(),
8741 [
8742 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8743 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8744 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8745 ],
8746 cx,
8747 );
8748 multi_buffer.push_excerpts(
8749 buffer_3.clone(),
8750 [
8751 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8752 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8753 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8754 ],
8755 cx,
8756 );
8757 multi_buffer
8758 });
8759 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
8760 Editor::new(
8761 EditorMode::full(),
8762 multi_buffer,
8763 Some(project.clone()),
8764 window,
8765 cx,
8766 )
8767 });
8768
8769 multi_buffer_editor.update_in(cx, |editor, window, cx| {
8770 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
8771 s.select_ranges(Some(1..2))
8772 });
8773 editor.insert("|one|two|three|", window, cx);
8774 });
8775 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
8776 multi_buffer_editor.update_in(cx, |editor, window, cx| {
8777 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
8778 s.select_ranges(Some(60..70))
8779 });
8780 editor.insert("|four|five|six|", window, cx);
8781 });
8782 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
8783
8784 // First two buffers should be edited, but not the third one.
8785 assert_eq!(
8786 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
8787 "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}",
8788 );
8789 buffer_1.update(cx, |buffer, _| {
8790 assert!(buffer.is_dirty());
8791 assert_eq!(
8792 buffer.text(),
8793 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
8794 )
8795 });
8796 buffer_2.update(cx, |buffer, _| {
8797 assert!(buffer.is_dirty());
8798 assert_eq!(
8799 buffer.text(),
8800 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
8801 )
8802 });
8803 buffer_3.update(cx, |buffer, _| {
8804 assert!(!buffer.is_dirty());
8805 assert_eq!(buffer.text(), sample_text_3,)
8806 });
8807 cx.executor().run_until_parked();
8808
8809 cx.executor().start_waiting();
8810 let save = multi_buffer_editor
8811 .update_in(cx, |editor, window, cx| {
8812 editor.save(true, project.clone(), window, cx)
8813 })
8814 .unwrap();
8815
8816 let fake_server = fake_servers.next().await.unwrap();
8817 fake_server
8818 .server
8819 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
8820 Ok(Some(vec![lsp::TextEdit::new(
8821 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8822 format!("[{} formatted]", params.text_document.uri),
8823 )]))
8824 })
8825 .detach();
8826 save.await;
8827
8828 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
8829 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
8830 assert_eq!(
8831 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
8832 uri!(
8833 "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}"
8834 ),
8835 );
8836 buffer_1.update(cx, |buffer, _| {
8837 assert!(!buffer.is_dirty());
8838 assert_eq!(
8839 buffer.text(),
8840 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
8841 )
8842 });
8843 buffer_2.update(cx, |buffer, _| {
8844 assert!(!buffer.is_dirty());
8845 assert_eq!(
8846 buffer.text(),
8847 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
8848 )
8849 });
8850 buffer_3.update(cx, |buffer, _| {
8851 assert!(!buffer.is_dirty());
8852 assert_eq!(buffer.text(), sample_text_3,)
8853 });
8854}
8855
8856#[gpui::test]
8857async fn test_range_format_during_save(cx: &mut TestAppContext) {
8858 init_test(cx, |_| {});
8859
8860 let fs = FakeFs::new(cx.executor());
8861 fs.insert_file(path!("/file.rs"), Default::default()).await;
8862
8863 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8864
8865 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8866 language_registry.add(rust_lang());
8867 let mut fake_servers = language_registry.register_fake_lsp(
8868 "Rust",
8869 FakeLspAdapter {
8870 capabilities: lsp::ServerCapabilities {
8871 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
8872 ..Default::default()
8873 },
8874 ..Default::default()
8875 },
8876 );
8877
8878 let buffer = project
8879 .update(cx, |project, cx| {
8880 project.open_local_buffer(path!("/file.rs"), cx)
8881 })
8882 .await
8883 .unwrap();
8884
8885 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8886 let (editor, cx) = cx.add_window_view(|window, cx| {
8887 build_editor_with_project(project.clone(), buffer, window, cx)
8888 });
8889 editor.update_in(cx, |editor, window, cx| {
8890 editor.set_text("one\ntwo\nthree\n", window, cx)
8891 });
8892 assert!(cx.read(|cx| editor.is_dirty(cx)));
8893
8894 cx.executor().start_waiting();
8895 let fake_server = fake_servers.next().await.unwrap();
8896
8897 let save = editor
8898 .update_in(cx, |editor, window, cx| {
8899 editor.save(true, project.clone(), window, cx)
8900 })
8901 .unwrap();
8902 fake_server
8903 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
8904 assert_eq!(
8905 params.text_document.uri,
8906 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8907 );
8908 assert_eq!(params.options.tab_size, 4);
8909 Ok(Some(vec![lsp::TextEdit::new(
8910 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8911 ", ".to_string(),
8912 )]))
8913 })
8914 .next()
8915 .await;
8916 cx.executor().start_waiting();
8917 save.await;
8918 assert_eq!(
8919 editor.update(cx, |editor, cx| editor.text(cx)),
8920 "one, two\nthree\n"
8921 );
8922 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8923
8924 editor.update_in(cx, |editor, window, cx| {
8925 editor.set_text("one\ntwo\nthree\n", window, cx)
8926 });
8927 assert!(cx.read(|cx| editor.is_dirty(cx)));
8928
8929 // Ensure we can still save even if formatting hangs.
8930 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
8931 move |params, _| async move {
8932 assert_eq!(
8933 params.text_document.uri,
8934 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8935 );
8936 futures::future::pending::<()>().await;
8937 unreachable!()
8938 },
8939 );
8940 let save = editor
8941 .update_in(cx, |editor, window, cx| {
8942 editor.save(true, project.clone(), window, cx)
8943 })
8944 .unwrap();
8945 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8946 cx.executor().start_waiting();
8947 save.await;
8948 assert_eq!(
8949 editor.update(cx, |editor, cx| editor.text(cx)),
8950 "one\ntwo\nthree\n"
8951 );
8952 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8953
8954 // For non-dirty buffer, no formatting request should be sent
8955 let save = editor
8956 .update_in(cx, |editor, window, cx| {
8957 editor.save(true, project.clone(), window, cx)
8958 })
8959 .unwrap();
8960 let _pending_format_request = fake_server
8961 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
8962 panic!("Should not be invoked on non-dirty buffer");
8963 })
8964 .next();
8965 cx.executor().start_waiting();
8966 save.await;
8967
8968 // Set Rust language override and assert overridden tabsize is sent to language server
8969 update_test_language_settings(cx, |settings| {
8970 settings.languages.insert(
8971 "Rust".into(),
8972 LanguageSettingsContent {
8973 tab_size: NonZeroU32::new(8),
8974 ..Default::default()
8975 },
8976 );
8977 });
8978
8979 editor.update_in(cx, |editor, window, cx| {
8980 editor.set_text("somehting_new\n", window, cx)
8981 });
8982 assert!(cx.read(|cx| editor.is_dirty(cx)));
8983 let save = editor
8984 .update_in(cx, |editor, window, cx| {
8985 editor.save(true, project.clone(), window, cx)
8986 })
8987 .unwrap();
8988 fake_server
8989 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
8990 assert_eq!(
8991 params.text_document.uri,
8992 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8993 );
8994 assert_eq!(params.options.tab_size, 8);
8995 Ok(Some(vec![]))
8996 })
8997 .next()
8998 .await;
8999 cx.executor().start_waiting();
9000 save.await;
9001}
9002
9003#[gpui::test]
9004async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
9005 init_test(cx, |settings| {
9006 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
9007 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
9008 ))
9009 });
9010
9011 let fs = FakeFs::new(cx.executor());
9012 fs.insert_file(path!("/file.rs"), Default::default()).await;
9013
9014 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9015
9016 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9017 language_registry.add(Arc::new(Language::new(
9018 LanguageConfig {
9019 name: "Rust".into(),
9020 matcher: LanguageMatcher {
9021 path_suffixes: vec!["rs".to_string()],
9022 ..Default::default()
9023 },
9024 ..LanguageConfig::default()
9025 },
9026 Some(tree_sitter_rust::LANGUAGE.into()),
9027 )));
9028 update_test_language_settings(cx, |settings| {
9029 // Enable Prettier formatting for the same buffer, and ensure
9030 // LSP is called instead of Prettier.
9031 settings.defaults.prettier = Some(PrettierSettings {
9032 allowed: true,
9033 ..PrettierSettings::default()
9034 });
9035 });
9036 let mut fake_servers = language_registry.register_fake_lsp(
9037 "Rust",
9038 FakeLspAdapter {
9039 capabilities: lsp::ServerCapabilities {
9040 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9041 ..Default::default()
9042 },
9043 ..Default::default()
9044 },
9045 );
9046
9047 let buffer = project
9048 .update(cx, |project, cx| {
9049 project.open_local_buffer(path!("/file.rs"), cx)
9050 })
9051 .await
9052 .unwrap();
9053
9054 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9055 let (editor, cx) = cx.add_window_view(|window, cx| {
9056 build_editor_with_project(project.clone(), buffer, window, cx)
9057 });
9058 editor.update_in(cx, |editor, window, cx| {
9059 editor.set_text("one\ntwo\nthree\n", window, cx)
9060 });
9061
9062 cx.executor().start_waiting();
9063 let fake_server = fake_servers.next().await.unwrap();
9064
9065 let format = editor
9066 .update_in(cx, |editor, window, cx| {
9067 editor.perform_format(
9068 project.clone(),
9069 FormatTrigger::Manual,
9070 FormatTarget::Buffers,
9071 window,
9072 cx,
9073 )
9074 })
9075 .unwrap();
9076 fake_server
9077 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
9078 assert_eq!(
9079 params.text_document.uri,
9080 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9081 );
9082 assert_eq!(params.options.tab_size, 4);
9083 Ok(Some(vec![lsp::TextEdit::new(
9084 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9085 ", ".to_string(),
9086 )]))
9087 })
9088 .next()
9089 .await;
9090 cx.executor().start_waiting();
9091 format.await;
9092 assert_eq!(
9093 editor.update(cx, |editor, cx| editor.text(cx)),
9094 "one, two\nthree\n"
9095 );
9096
9097 editor.update_in(cx, |editor, window, cx| {
9098 editor.set_text("one\ntwo\nthree\n", window, cx)
9099 });
9100 // Ensure we don't lock if formatting hangs.
9101 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9102 move |params, _| async move {
9103 assert_eq!(
9104 params.text_document.uri,
9105 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9106 );
9107 futures::future::pending::<()>().await;
9108 unreachable!()
9109 },
9110 );
9111 let format = editor
9112 .update_in(cx, |editor, window, cx| {
9113 editor.perform_format(
9114 project,
9115 FormatTrigger::Manual,
9116 FormatTarget::Buffers,
9117 window,
9118 cx,
9119 )
9120 })
9121 .unwrap();
9122 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9123 cx.executor().start_waiting();
9124 format.await;
9125 assert_eq!(
9126 editor.update(cx, |editor, cx| editor.text(cx)),
9127 "one\ntwo\nthree\n"
9128 );
9129}
9130
9131#[gpui::test]
9132async fn test_multiple_formatters(cx: &mut TestAppContext) {
9133 init_test(cx, |settings| {
9134 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
9135 settings.defaults.formatter =
9136 Some(language_settings::SelectedFormatter::List(FormatterList(
9137 vec![
9138 Formatter::LanguageServer { name: None },
9139 Formatter::CodeActions(
9140 [
9141 ("code-action-1".into(), true),
9142 ("code-action-2".into(), true),
9143 ]
9144 .into_iter()
9145 .collect(),
9146 ),
9147 ]
9148 .into(),
9149 )))
9150 });
9151
9152 let fs = FakeFs::new(cx.executor());
9153 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
9154 .await;
9155
9156 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9157 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9158 language_registry.add(rust_lang());
9159
9160 let mut fake_servers = language_registry.register_fake_lsp(
9161 "Rust",
9162 FakeLspAdapter {
9163 capabilities: lsp::ServerCapabilities {
9164 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9165 execute_command_provider: Some(lsp::ExecuteCommandOptions {
9166 commands: vec!["the-command-for-code-action-1".into()],
9167 ..Default::default()
9168 }),
9169 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
9170 ..Default::default()
9171 },
9172 ..Default::default()
9173 },
9174 );
9175
9176 let buffer = project
9177 .update(cx, |project, cx| {
9178 project.open_local_buffer(path!("/file.rs"), cx)
9179 })
9180 .await
9181 .unwrap();
9182
9183 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9184 let (editor, cx) = cx.add_window_view(|window, cx| {
9185 build_editor_with_project(project.clone(), buffer, window, cx)
9186 });
9187
9188 cx.executor().start_waiting();
9189
9190 let fake_server = fake_servers.next().await.unwrap();
9191 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9192 move |_params, _| async move {
9193 Ok(Some(vec![lsp::TextEdit::new(
9194 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9195 "applied-formatting\n".to_string(),
9196 )]))
9197 },
9198 );
9199 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
9200 move |params, _| async move {
9201 assert_eq!(
9202 params.context.only,
9203 Some(vec!["code-action-1".into(), "code-action-2".into()])
9204 );
9205 let uri = lsp::Url::from_file_path(path!("/file.rs")).unwrap();
9206 Ok(Some(vec![
9207 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
9208 kind: Some("code-action-1".into()),
9209 edit: Some(lsp::WorkspaceEdit::new(
9210 [(
9211 uri.clone(),
9212 vec![lsp::TextEdit::new(
9213 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9214 "applied-code-action-1-edit\n".to_string(),
9215 )],
9216 )]
9217 .into_iter()
9218 .collect(),
9219 )),
9220 command: Some(lsp::Command {
9221 command: "the-command-for-code-action-1".into(),
9222 ..Default::default()
9223 }),
9224 ..Default::default()
9225 }),
9226 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
9227 kind: Some("code-action-2".into()),
9228 edit: Some(lsp::WorkspaceEdit::new(
9229 [(
9230 uri.clone(),
9231 vec![lsp::TextEdit::new(
9232 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9233 "applied-code-action-2-edit\n".to_string(),
9234 )],
9235 )]
9236 .into_iter()
9237 .collect(),
9238 )),
9239 ..Default::default()
9240 }),
9241 ]))
9242 },
9243 );
9244
9245 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
9246 move |params, _| async move { Ok(params) }
9247 });
9248
9249 let command_lock = Arc::new(futures::lock::Mutex::new(()));
9250 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
9251 let fake = fake_server.clone();
9252 let lock = command_lock.clone();
9253 move |params, _| {
9254 assert_eq!(params.command, "the-command-for-code-action-1");
9255 let fake = fake.clone();
9256 let lock = lock.clone();
9257 async move {
9258 lock.lock().await;
9259 fake.server
9260 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
9261 label: None,
9262 edit: lsp::WorkspaceEdit {
9263 changes: Some(
9264 [(
9265 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
9266 vec![lsp::TextEdit {
9267 range: lsp::Range::new(
9268 lsp::Position::new(0, 0),
9269 lsp::Position::new(0, 0),
9270 ),
9271 new_text: "applied-code-action-1-command\n".into(),
9272 }],
9273 )]
9274 .into_iter()
9275 .collect(),
9276 ),
9277 ..Default::default()
9278 },
9279 })
9280 .await
9281 .into_response()
9282 .unwrap();
9283 Ok(Some(json!(null)))
9284 }
9285 }
9286 });
9287
9288 cx.executor().start_waiting();
9289 editor
9290 .update_in(cx, |editor, window, cx| {
9291 editor.perform_format(
9292 project.clone(),
9293 FormatTrigger::Manual,
9294 FormatTarget::Buffers,
9295 window,
9296 cx,
9297 )
9298 })
9299 .unwrap()
9300 .await;
9301 editor.update(cx, |editor, cx| {
9302 assert_eq!(
9303 editor.text(cx),
9304 r#"
9305 applied-code-action-2-edit
9306 applied-code-action-1-command
9307 applied-code-action-1-edit
9308 applied-formatting
9309 one
9310 two
9311 three
9312 "#
9313 .unindent()
9314 );
9315 });
9316
9317 editor.update_in(cx, |editor, window, cx| {
9318 editor.undo(&Default::default(), window, cx);
9319 assert_eq!(editor.text(cx), "one \ntwo \nthree");
9320 });
9321
9322 // Perform a manual edit while waiting for an LSP command
9323 // that's being run as part of a formatting code action.
9324 let lock_guard = command_lock.lock().await;
9325 let format = editor
9326 .update_in(cx, |editor, window, cx| {
9327 editor.perform_format(
9328 project.clone(),
9329 FormatTrigger::Manual,
9330 FormatTarget::Buffers,
9331 window,
9332 cx,
9333 )
9334 })
9335 .unwrap();
9336 cx.run_until_parked();
9337 editor.update(cx, |editor, cx| {
9338 assert_eq!(
9339 editor.text(cx),
9340 r#"
9341 applied-code-action-1-edit
9342 applied-formatting
9343 one
9344 two
9345 three
9346 "#
9347 .unindent()
9348 );
9349
9350 editor.buffer.update(cx, |buffer, cx| {
9351 let ix = buffer.len(cx);
9352 buffer.edit([(ix..ix, "edited\n")], None, cx);
9353 });
9354 });
9355
9356 // Allow the LSP command to proceed. Because the buffer was edited,
9357 // the second code action will not be run.
9358 drop(lock_guard);
9359 format.await;
9360 editor.update_in(cx, |editor, window, cx| {
9361 assert_eq!(
9362 editor.text(cx),
9363 r#"
9364 applied-code-action-1-command
9365 applied-code-action-1-edit
9366 applied-formatting
9367 one
9368 two
9369 three
9370 edited
9371 "#
9372 .unindent()
9373 );
9374
9375 // The manual edit is undone first, because it is the last thing the user did
9376 // (even though the command completed afterwards).
9377 editor.undo(&Default::default(), window, cx);
9378 assert_eq!(
9379 editor.text(cx),
9380 r#"
9381 applied-code-action-1-command
9382 applied-code-action-1-edit
9383 applied-formatting
9384 one
9385 two
9386 three
9387 "#
9388 .unindent()
9389 );
9390
9391 // All the formatting (including the command, which completed after the manual edit)
9392 // is undone together.
9393 editor.undo(&Default::default(), window, cx);
9394 assert_eq!(editor.text(cx), "one \ntwo \nthree");
9395 });
9396}
9397
9398#[gpui::test]
9399async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
9400 init_test(cx, |settings| {
9401 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
9402 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
9403 ))
9404 });
9405
9406 let fs = FakeFs::new(cx.executor());
9407 fs.insert_file(path!("/file.ts"), Default::default()).await;
9408
9409 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9410
9411 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9412 language_registry.add(Arc::new(Language::new(
9413 LanguageConfig {
9414 name: "TypeScript".into(),
9415 matcher: LanguageMatcher {
9416 path_suffixes: vec!["ts".to_string()],
9417 ..Default::default()
9418 },
9419 ..LanguageConfig::default()
9420 },
9421 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
9422 )));
9423 update_test_language_settings(cx, |settings| {
9424 settings.defaults.prettier = Some(PrettierSettings {
9425 allowed: true,
9426 ..PrettierSettings::default()
9427 });
9428 });
9429 let mut fake_servers = language_registry.register_fake_lsp(
9430 "TypeScript",
9431 FakeLspAdapter {
9432 capabilities: lsp::ServerCapabilities {
9433 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
9434 ..Default::default()
9435 },
9436 ..Default::default()
9437 },
9438 );
9439
9440 let buffer = project
9441 .update(cx, |project, cx| {
9442 project.open_local_buffer(path!("/file.ts"), cx)
9443 })
9444 .await
9445 .unwrap();
9446
9447 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9448 let (editor, cx) = cx.add_window_view(|window, cx| {
9449 build_editor_with_project(project.clone(), buffer, window, cx)
9450 });
9451 editor.update_in(cx, |editor, window, cx| {
9452 editor.set_text(
9453 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
9454 window,
9455 cx,
9456 )
9457 });
9458
9459 cx.executor().start_waiting();
9460 let fake_server = fake_servers.next().await.unwrap();
9461
9462 let format = editor
9463 .update_in(cx, |editor, window, cx| {
9464 editor.perform_code_action_kind(
9465 project.clone(),
9466 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
9467 window,
9468 cx,
9469 )
9470 })
9471 .unwrap();
9472 fake_server
9473 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
9474 assert_eq!(
9475 params.text_document.uri,
9476 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
9477 );
9478 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
9479 lsp::CodeAction {
9480 title: "Organize Imports".to_string(),
9481 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
9482 edit: Some(lsp::WorkspaceEdit {
9483 changes: Some(
9484 [(
9485 params.text_document.uri.clone(),
9486 vec![lsp::TextEdit::new(
9487 lsp::Range::new(
9488 lsp::Position::new(1, 0),
9489 lsp::Position::new(2, 0),
9490 ),
9491 "".to_string(),
9492 )],
9493 )]
9494 .into_iter()
9495 .collect(),
9496 ),
9497 ..Default::default()
9498 }),
9499 ..Default::default()
9500 },
9501 )]))
9502 })
9503 .next()
9504 .await;
9505 cx.executor().start_waiting();
9506 format.await;
9507 assert_eq!(
9508 editor.update(cx, |editor, cx| editor.text(cx)),
9509 "import { a } from 'module';\n\nconst x = a;\n"
9510 );
9511
9512 editor.update_in(cx, |editor, window, cx| {
9513 editor.set_text(
9514 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
9515 window,
9516 cx,
9517 )
9518 });
9519 // Ensure we don't lock if code action hangs.
9520 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
9521 move |params, _| async move {
9522 assert_eq!(
9523 params.text_document.uri,
9524 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
9525 );
9526 futures::future::pending::<()>().await;
9527 unreachable!()
9528 },
9529 );
9530 let format = editor
9531 .update_in(cx, |editor, window, cx| {
9532 editor.perform_code_action_kind(
9533 project,
9534 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
9535 window,
9536 cx,
9537 )
9538 })
9539 .unwrap();
9540 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
9541 cx.executor().start_waiting();
9542 format.await;
9543 assert_eq!(
9544 editor.update(cx, |editor, cx| editor.text(cx)),
9545 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
9546 );
9547}
9548
9549#[gpui::test]
9550async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
9551 init_test(cx, |_| {});
9552
9553 let mut cx = EditorLspTestContext::new_rust(
9554 lsp::ServerCapabilities {
9555 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9556 ..Default::default()
9557 },
9558 cx,
9559 )
9560 .await;
9561
9562 cx.set_state(indoc! {"
9563 one.twoˇ
9564 "});
9565
9566 // The format request takes a long time. When it completes, it inserts
9567 // a newline and an indent before the `.`
9568 cx.lsp
9569 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
9570 let executor = cx.background_executor().clone();
9571 async move {
9572 executor.timer(Duration::from_millis(100)).await;
9573 Ok(Some(vec![lsp::TextEdit {
9574 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
9575 new_text: "\n ".into(),
9576 }]))
9577 }
9578 });
9579
9580 // Submit a format request.
9581 let format_1 = cx
9582 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
9583 .unwrap();
9584 cx.executor().run_until_parked();
9585
9586 // Submit a second format request.
9587 let format_2 = cx
9588 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
9589 .unwrap();
9590 cx.executor().run_until_parked();
9591
9592 // Wait for both format requests to complete
9593 cx.executor().advance_clock(Duration::from_millis(200));
9594 cx.executor().start_waiting();
9595 format_1.await.unwrap();
9596 cx.executor().start_waiting();
9597 format_2.await.unwrap();
9598
9599 // The formatting edits only happens once.
9600 cx.assert_editor_state(indoc! {"
9601 one
9602 .twoˇ
9603 "});
9604}
9605
9606#[gpui::test]
9607async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
9608 init_test(cx, |settings| {
9609 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
9610 });
9611
9612 let mut cx = EditorLspTestContext::new_rust(
9613 lsp::ServerCapabilities {
9614 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9615 ..Default::default()
9616 },
9617 cx,
9618 )
9619 .await;
9620
9621 // Set up a buffer white some trailing whitespace and no trailing newline.
9622 cx.set_state(
9623 &[
9624 "one ", //
9625 "twoˇ", //
9626 "three ", //
9627 "four", //
9628 ]
9629 .join("\n"),
9630 );
9631
9632 // Submit a format request.
9633 let format = cx
9634 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
9635 .unwrap();
9636
9637 // Record which buffer changes have been sent to the language server
9638 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
9639 cx.lsp
9640 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
9641 let buffer_changes = buffer_changes.clone();
9642 move |params, _| {
9643 buffer_changes.lock().extend(
9644 params
9645 .content_changes
9646 .into_iter()
9647 .map(|e| (e.range.unwrap(), e.text)),
9648 );
9649 }
9650 });
9651
9652 // Handle formatting requests to the language server.
9653 cx.lsp
9654 .set_request_handler::<lsp::request::Formatting, _, _>({
9655 let buffer_changes = buffer_changes.clone();
9656 move |_, _| {
9657 // When formatting is requested, trailing whitespace has already been stripped,
9658 // and the trailing newline has already been added.
9659 assert_eq!(
9660 &buffer_changes.lock()[1..],
9661 &[
9662 (
9663 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
9664 "".into()
9665 ),
9666 (
9667 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
9668 "".into()
9669 ),
9670 (
9671 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
9672 "\n".into()
9673 ),
9674 ]
9675 );
9676
9677 // Insert blank lines between each line of the buffer.
9678 async move {
9679 Ok(Some(vec![
9680 lsp::TextEdit {
9681 range: lsp::Range::new(
9682 lsp::Position::new(1, 0),
9683 lsp::Position::new(1, 0),
9684 ),
9685 new_text: "\n".into(),
9686 },
9687 lsp::TextEdit {
9688 range: lsp::Range::new(
9689 lsp::Position::new(2, 0),
9690 lsp::Position::new(2, 0),
9691 ),
9692 new_text: "\n".into(),
9693 },
9694 ]))
9695 }
9696 }
9697 });
9698
9699 // After formatting the buffer, the trailing whitespace is stripped,
9700 // a newline is appended, and the edits provided by the language server
9701 // have been applied.
9702 format.await.unwrap();
9703 cx.assert_editor_state(
9704 &[
9705 "one", //
9706 "", //
9707 "twoˇ", //
9708 "", //
9709 "three", //
9710 "four", //
9711 "", //
9712 ]
9713 .join("\n"),
9714 );
9715
9716 // Undoing the formatting undoes the trailing whitespace removal, the
9717 // trailing newline, and the LSP edits.
9718 cx.update_buffer(|buffer, cx| buffer.undo(cx));
9719 cx.assert_editor_state(
9720 &[
9721 "one ", //
9722 "twoˇ", //
9723 "three ", //
9724 "four", //
9725 ]
9726 .join("\n"),
9727 );
9728}
9729
9730#[gpui::test]
9731async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
9732 cx: &mut TestAppContext,
9733) {
9734 init_test(cx, |_| {});
9735
9736 cx.update(|cx| {
9737 cx.update_global::<SettingsStore, _>(|settings, cx| {
9738 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9739 settings.auto_signature_help = Some(true);
9740 });
9741 });
9742 });
9743
9744 let mut cx = EditorLspTestContext::new_rust(
9745 lsp::ServerCapabilities {
9746 signature_help_provider: Some(lsp::SignatureHelpOptions {
9747 ..Default::default()
9748 }),
9749 ..Default::default()
9750 },
9751 cx,
9752 )
9753 .await;
9754
9755 let language = Language::new(
9756 LanguageConfig {
9757 name: "Rust".into(),
9758 brackets: BracketPairConfig {
9759 pairs: vec![
9760 BracketPair {
9761 start: "{".to_string(),
9762 end: "}".to_string(),
9763 close: true,
9764 surround: true,
9765 newline: true,
9766 },
9767 BracketPair {
9768 start: "(".to_string(),
9769 end: ")".to_string(),
9770 close: true,
9771 surround: true,
9772 newline: true,
9773 },
9774 BracketPair {
9775 start: "/*".to_string(),
9776 end: " */".to_string(),
9777 close: true,
9778 surround: true,
9779 newline: true,
9780 },
9781 BracketPair {
9782 start: "[".to_string(),
9783 end: "]".to_string(),
9784 close: false,
9785 surround: false,
9786 newline: true,
9787 },
9788 BracketPair {
9789 start: "\"".to_string(),
9790 end: "\"".to_string(),
9791 close: true,
9792 surround: true,
9793 newline: false,
9794 },
9795 BracketPair {
9796 start: "<".to_string(),
9797 end: ">".to_string(),
9798 close: false,
9799 surround: true,
9800 newline: true,
9801 },
9802 ],
9803 ..Default::default()
9804 },
9805 autoclose_before: "})]".to_string(),
9806 ..Default::default()
9807 },
9808 Some(tree_sitter_rust::LANGUAGE.into()),
9809 );
9810 let language = Arc::new(language);
9811
9812 cx.language_registry().add(language.clone());
9813 cx.update_buffer(|buffer, cx| {
9814 buffer.set_language(Some(language), cx);
9815 });
9816
9817 cx.set_state(
9818 &r#"
9819 fn main() {
9820 sampleˇ
9821 }
9822 "#
9823 .unindent(),
9824 );
9825
9826 cx.update_editor(|editor, window, cx| {
9827 editor.handle_input("(", window, cx);
9828 });
9829 cx.assert_editor_state(
9830 &"
9831 fn main() {
9832 sample(ˇ)
9833 }
9834 "
9835 .unindent(),
9836 );
9837
9838 let mocked_response = lsp::SignatureHelp {
9839 signatures: vec![lsp::SignatureInformation {
9840 label: "fn sample(param1: u8, param2: u8)".to_string(),
9841 documentation: None,
9842 parameters: Some(vec![
9843 lsp::ParameterInformation {
9844 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9845 documentation: None,
9846 },
9847 lsp::ParameterInformation {
9848 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9849 documentation: None,
9850 },
9851 ]),
9852 active_parameter: None,
9853 }],
9854 active_signature: Some(0),
9855 active_parameter: Some(0),
9856 };
9857 handle_signature_help_request(&mut cx, mocked_response).await;
9858
9859 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9860 .await;
9861
9862 cx.editor(|editor, _, _| {
9863 let signature_help_state = editor.signature_help_state.popover().cloned();
9864 assert_eq!(
9865 signature_help_state.unwrap().label,
9866 "param1: u8, param2: u8"
9867 );
9868 });
9869}
9870
9871#[gpui::test]
9872async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
9873 init_test(cx, |_| {});
9874
9875 cx.update(|cx| {
9876 cx.update_global::<SettingsStore, _>(|settings, cx| {
9877 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9878 settings.auto_signature_help = Some(false);
9879 settings.show_signature_help_after_edits = Some(false);
9880 });
9881 });
9882 });
9883
9884 let mut cx = EditorLspTestContext::new_rust(
9885 lsp::ServerCapabilities {
9886 signature_help_provider: Some(lsp::SignatureHelpOptions {
9887 ..Default::default()
9888 }),
9889 ..Default::default()
9890 },
9891 cx,
9892 )
9893 .await;
9894
9895 let language = Language::new(
9896 LanguageConfig {
9897 name: "Rust".into(),
9898 brackets: BracketPairConfig {
9899 pairs: vec![
9900 BracketPair {
9901 start: "{".to_string(),
9902 end: "}".to_string(),
9903 close: true,
9904 surround: true,
9905 newline: true,
9906 },
9907 BracketPair {
9908 start: "(".to_string(),
9909 end: ")".to_string(),
9910 close: true,
9911 surround: true,
9912 newline: true,
9913 },
9914 BracketPair {
9915 start: "/*".to_string(),
9916 end: " */".to_string(),
9917 close: true,
9918 surround: true,
9919 newline: true,
9920 },
9921 BracketPair {
9922 start: "[".to_string(),
9923 end: "]".to_string(),
9924 close: false,
9925 surround: false,
9926 newline: true,
9927 },
9928 BracketPair {
9929 start: "\"".to_string(),
9930 end: "\"".to_string(),
9931 close: true,
9932 surround: true,
9933 newline: false,
9934 },
9935 BracketPair {
9936 start: "<".to_string(),
9937 end: ">".to_string(),
9938 close: false,
9939 surround: true,
9940 newline: true,
9941 },
9942 ],
9943 ..Default::default()
9944 },
9945 autoclose_before: "})]".to_string(),
9946 ..Default::default()
9947 },
9948 Some(tree_sitter_rust::LANGUAGE.into()),
9949 );
9950 let language = Arc::new(language);
9951
9952 cx.language_registry().add(language.clone());
9953 cx.update_buffer(|buffer, cx| {
9954 buffer.set_language(Some(language), cx);
9955 });
9956
9957 // Ensure that signature_help is not called when no signature help is enabled.
9958 cx.set_state(
9959 &r#"
9960 fn main() {
9961 sampleˇ
9962 }
9963 "#
9964 .unindent(),
9965 );
9966 cx.update_editor(|editor, window, cx| {
9967 editor.handle_input("(", window, cx);
9968 });
9969 cx.assert_editor_state(
9970 &"
9971 fn main() {
9972 sample(ˇ)
9973 }
9974 "
9975 .unindent(),
9976 );
9977 cx.editor(|editor, _, _| {
9978 assert!(editor.signature_help_state.task().is_none());
9979 });
9980
9981 let mocked_response = lsp::SignatureHelp {
9982 signatures: vec![lsp::SignatureInformation {
9983 label: "fn sample(param1: u8, param2: u8)".to_string(),
9984 documentation: None,
9985 parameters: Some(vec![
9986 lsp::ParameterInformation {
9987 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9988 documentation: None,
9989 },
9990 lsp::ParameterInformation {
9991 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9992 documentation: None,
9993 },
9994 ]),
9995 active_parameter: None,
9996 }],
9997 active_signature: Some(0),
9998 active_parameter: Some(0),
9999 };
10000
10001 // Ensure that signature_help is called when enabled afte edits
10002 cx.update(|_, cx| {
10003 cx.update_global::<SettingsStore, _>(|settings, cx| {
10004 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10005 settings.auto_signature_help = Some(false);
10006 settings.show_signature_help_after_edits = Some(true);
10007 });
10008 });
10009 });
10010 cx.set_state(
10011 &r#"
10012 fn main() {
10013 sampleˇ
10014 }
10015 "#
10016 .unindent(),
10017 );
10018 cx.update_editor(|editor, window, cx| {
10019 editor.handle_input("(", window, cx);
10020 });
10021 cx.assert_editor_state(
10022 &"
10023 fn main() {
10024 sample(ˇ)
10025 }
10026 "
10027 .unindent(),
10028 );
10029 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10030 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10031 .await;
10032 cx.update_editor(|editor, _, _| {
10033 let signature_help_state = editor.signature_help_state.popover().cloned();
10034 assert!(signature_help_state.is_some());
10035 assert_eq!(
10036 signature_help_state.unwrap().label,
10037 "param1: u8, param2: u8"
10038 );
10039 editor.signature_help_state = SignatureHelpState::default();
10040 });
10041
10042 // Ensure that signature_help is called when auto signature help override is enabled
10043 cx.update(|_, cx| {
10044 cx.update_global::<SettingsStore, _>(|settings, cx| {
10045 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10046 settings.auto_signature_help = Some(true);
10047 settings.show_signature_help_after_edits = Some(false);
10048 });
10049 });
10050 });
10051 cx.set_state(
10052 &r#"
10053 fn main() {
10054 sampleˇ
10055 }
10056 "#
10057 .unindent(),
10058 );
10059 cx.update_editor(|editor, window, cx| {
10060 editor.handle_input("(", window, cx);
10061 });
10062 cx.assert_editor_state(
10063 &"
10064 fn main() {
10065 sample(ˇ)
10066 }
10067 "
10068 .unindent(),
10069 );
10070 handle_signature_help_request(&mut cx, mocked_response).await;
10071 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10072 .await;
10073 cx.editor(|editor, _, _| {
10074 let signature_help_state = editor.signature_help_state.popover().cloned();
10075 assert!(signature_help_state.is_some());
10076 assert_eq!(
10077 signature_help_state.unwrap().label,
10078 "param1: u8, param2: u8"
10079 );
10080 });
10081}
10082
10083#[gpui::test]
10084async fn test_signature_help(cx: &mut TestAppContext) {
10085 init_test(cx, |_| {});
10086 cx.update(|cx| {
10087 cx.update_global::<SettingsStore, _>(|settings, cx| {
10088 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10089 settings.auto_signature_help = Some(true);
10090 });
10091 });
10092 });
10093
10094 let mut cx = EditorLspTestContext::new_rust(
10095 lsp::ServerCapabilities {
10096 signature_help_provider: Some(lsp::SignatureHelpOptions {
10097 ..Default::default()
10098 }),
10099 ..Default::default()
10100 },
10101 cx,
10102 )
10103 .await;
10104
10105 // A test that directly calls `show_signature_help`
10106 cx.update_editor(|editor, window, cx| {
10107 editor.show_signature_help(&ShowSignatureHelp, window, cx);
10108 });
10109
10110 let mocked_response = lsp::SignatureHelp {
10111 signatures: vec![lsp::SignatureInformation {
10112 label: "fn sample(param1: u8, param2: u8)".to_string(),
10113 documentation: None,
10114 parameters: Some(vec![
10115 lsp::ParameterInformation {
10116 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10117 documentation: None,
10118 },
10119 lsp::ParameterInformation {
10120 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10121 documentation: None,
10122 },
10123 ]),
10124 active_parameter: None,
10125 }],
10126 active_signature: Some(0),
10127 active_parameter: Some(0),
10128 };
10129 handle_signature_help_request(&mut cx, mocked_response).await;
10130
10131 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10132 .await;
10133
10134 cx.editor(|editor, _, _| {
10135 let signature_help_state = editor.signature_help_state.popover().cloned();
10136 assert!(signature_help_state.is_some());
10137 assert_eq!(
10138 signature_help_state.unwrap().label,
10139 "param1: u8, param2: u8"
10140 );
10141 });
10142
10143 // When exiting outside from inside the brackets, `signature_help` is closed.
10144 cx.set_state(indoc! {"
10145 fn main() {
10146 sample(ˇ);
10147 }
10148
10149 fn sample(param1: u8, param2: u8) {}
10150 "});
10151
10152 cx.update_editor(|editor, window, cx| {
10153 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
10154 });
10155
10156 let mocked_response = lsp::SignatureHelp {
10157 signatures: Vec::new(),
10158 active_signature: None,
10159 active_parameter: None,
10160 };
10161 handle_signature_help_request(&mut cx, mocked_response).await;
10162
10163 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
10164 .await;
10165
10166 cx.editor(|editor, _, _| {
10167 assert!(!editor.signature_help_state.is_shown());
10168 });
10169
10170 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
10171 cx.set_state(indoc! {"
10172 fn main() {
10173 sample(ˇ);
10174 }
10175
10176 fn sample(param1: u8, param2: u8) {}
10177 "});
10178
10179 let mocked_response = lsp::SignatureHelp {
10180 signatures: vec![lsp::SignatureInformation {
10181 label: "fn sample(param1: u8, param2: u8)".to_string(),
10182 documentation: None,
10183 parameters: Some(vec![
10184 lsp::ParameterInformation {
10185 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10186 documentation: None,
10187 },
10188 lsp::ParameterInformation {
10189 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10190 documentation: None,
10191 },
10192 ]),
10193 active_parameter: None,
10194 }],
10195 active_signature: Some(0),
10196 active_parameter: Some(0),
10197 };
10198 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10199 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10200 .await;
10201 cx.editor(|editor, _, _| {
10202 assert!(editor.signature_help_state.is_shown());
10203 });
10204
10205 // Restore the popover with more parameter input
10206 cx.set_state(indoc! {"
10207 fn main() {
10208 sample(param1, param2ˇ);
10209 }
10210
10211 fn sample(param1: u8, param2: u8) {}
10212 "});
10213
10214 let mocked_response = lsp::SignatureHelp {
10215 signatures: vec![lsp::SignatureInformation {
10216 label: "fn sample(param1: u8, param2: u8)".to_string(),
10217 documentation: None,
10218 parameters: Some(vec![
10219 lsp::ParameterInformation {
10220 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10221 documentation: None,
10222 },
10223 lsp::ParameterInformation {
10224 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10225 documentation: None,
10226 },
10227 ]),
10228 active_parameter: None,
10229 }],
10230 active_signature: Some(0),
10231 active_parameter: Some(1),
10232 };
10233 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10234 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10235 .await;
10236
10237 // When selecting a range, the popover is gone.
10238 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
10239 cx.update_editor(|editor, window, cx| {
10240 editor.change_selections(None, window, cx, |s| {
10241 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
10242 })
10243 });
10244 cx.assert_editor_state(indoc! {"
10245 fn main() {
10246 sample(param1, «ˇparam2»);
10247 }
10248
10249 fn sample(param1: u8, param2: u8) {}
10250 "});
10251 cx.editor(|editor, _, _| {
10252 assert!(!editor.signature_help_state.is_shown());
10253 });
10254
10255 // When unselecting again, the popover is back if within the brackets.
10256 cx.update_editor(|editor, window, cx| {
10257 editor.change_selections(None, window, cx, |s| {
10258 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
10259 })
10260 });
10261 cx.assert_editor_state(indoc! {"
10262 fn main() {
10263 sample(param1, ˇparam2);
10264 }
10265
10266 fn sample(param1: u8, param2: u8) {}
10267 "});
10268 handle_signature_help_request(&mut cx, mocked_response).await;
10269 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10270 .await;
10271 cx.editor(|editor, _, _| {
10272 assert!(editor.signature_help_state.is_shown());
10273 });
10274
10275 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
10276 cx.update_editor(|editor, window, cx| {
10277 editor.change_selections(None, window, cx, |s| {
10278 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
10279 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
10280 })
10281 });
10282 cx.assert_editor_state(indoc! {"
10283 fn main() {
10284 sample(param1, ˇparam2);
10285 }
10286
10287 fn sample(param1: u8, param2: u8) {}
10288 "});
10289
10290 let mocked_response = lsp::SignatureHelp {
10291 signatures: vec![lsp::SignatureInformation {
10292 label: "fn sample(param1: u8, param2: u8)".to_string(),
10293 documentation: None,
10294 parameters: Some(vec![
10295 lsp::ParameterInformation {
10296 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10297 documentation: None,
10298 },
10299 lsp::ParameterInformation {
10300 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10301 documentation: None,
10302 },
10303 ]),
10304 active_parameter: None,
10305 }],
10306 active_signature: Some(0),
10307 active_parameter: Some(1),
10308 };
10309 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10310 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10311 .await;
10312 cx.update_editor(|editor, _, cx| {
10313 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
10314 });
10315 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
10316 .await;
10317 cx.update_editor(|editor, window, cx| {
10318 editor.change_selections(None, window, cx, |s| {
10319 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
10320 })
10321 });
10322 cx.assert_editor_state(indoc! {"
10323 fn main() {
10324 sample(param1, «ˇparam2»);
10325 }
10326
10327 fn sample(param1: u8, param2: u8) {}
10328 "});
10329 cx.update_editor(|editor, window, cx| {
10330 editor.change_selections(None, window, cx, |s| {
10331 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
10332 })
10333 });
10334 cx.assert_editor_state(indoc! {"
10335 fn main() {
10336 sample(param1, ˇparam2);
10337 }
10338
10339 fn sample(param1: u8, param2: u8) {}
10340 "});
10341 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
10342 .await;
10343}
10344
10345#[gpui::test]
10346async fn test_completion_mode(cx: &mut TestAppContext) {
10347 init_test(cx, |_| {});
10348 let mut cx = EditorLspTestContext::new_rust(
10349 lsp::ServerCapabilities {
10350 completion_provider: Some(lsp::CompletionOptions {
10351 resolve_provider: Some(true),
10352 ..Default::default()
10353 }),
10354 ..Default::default()
10355 },
10356 cx,
10357 )
10358 .await;
10359
10360 struct Run {
10361 run_description: &'static str,
10362 initial_state: String,
10363 buffer_marked_text: String,
10364 completion_text: &'static str,
10365 expected_with_insert_mode: String,
10366 expected_with_replace_mode: String,
10367 expected_with_replace_subsequence_mode: String,
10368 expected_with_replace_suffix_mode: String,
10369 }
10370
10371 let runs = [
10372 Run {
10373 run_description: "Start of word matches completion text",
10374 initial_state: "before ediˇ after".into(),
10375 buffer_marked_text: "before <edi|> after".into(),
10376 completion_text: "editor",
10377 expected_with_insert_mode: "before editorˇ after".into(),
10378 expected_with_replace_mode: "before editorˇ after".into(),
10379 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10380 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10381 },
10382 Run {
10383 run_description: "Accept same text at the middle of the word",
10384 initial_state: "before ediˇtor after".into(),
10385 buffer_marked_text: "before <edi|tor> after".into(),
10386 completion_text: "editor",
10387 expected_with_insert_mode: "before editorˇtor after".into(),
10388 expected_with_replace_mode: "before editorˇ after".into(),
10389 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10390 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10391 },
10392 Run {
10393 run_description: "End of word matches completion text -- cursor at end",
10394 initial_state: "before torˇ after".into(),
10395 buffer_marked_text: "before <tor|> after".into(),
10396 completion_text: "editor",
10397 expected_with_insert_mode: "before editorˇ after".into(),
10398 expected_with_replace_mode: "before editorˇ after".into(),
10399 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10400 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10401 },
10402 Run {
10403 run_description: "End of word matches completion text -- cursor at start",
10404 initial_state: "before ˇtor after".into(),
10405 buffer_marked_text: "before <|tor> after".into(),
10406 completion_text: "editor",
10407 expected_with_insert_mode: "before editorˇtor after".into(),
10408 expected_with_replace_mode: "before editorˇ after".into(),
10409 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10410 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10411 },
10412 Run {
10413 run_description: "Prepend text containing whitespace",
10414 initial_state: "pˇfield: bool".into(),
10415 buffer_marked_text: "<p|field>: bool".into(),
10416 completion_text: "pub ",
10417 expected_with_insert_mode: "pub ˇfield: bool".into(),
10418 expected_with_replace_mode: "pub ˇ: bool".into(),
10419 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
10420 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
10421 },
10422 Run {
10423 run_description: "Add element to start of list",
10424 initial_state: "[element_ˇelement_2]".into(),
10425 buffer_marked_text: "[<element_|element_2>]".into(),
10426 completion_text: "element_1",
10427 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
10428 expected_with_replace_mode: "[element_1ˇ]".into(),
10429 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
10430 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
10431 },
10432 Run {
10433 run_description: "Add element to start of list -- first and second elements are equal",
10434 initial_state: "[elˇelement]".into(),
10435 buffer_marked_text: "[<el|element>]".into(),
10436 completion_text: "element",
10437 expected_with_insert_mode: "[elementˇelement]".into(),
10438 expected_with_replace_mode: "[elementˇ]".into(),
10439 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
10440 expected_with_replace_suffix_mode: "[elementˇ]".into(),
10441 },
10442 Run {
10443 run_description: "Ends with matching suffix",
10444 initial_state: "SubˇError".into(),
10445 buffer_marked_text: "<Sub|Error>".into(),
10446 completion_text: "SubscriptionError",
10447 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
10448 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10449 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10450 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
10451 },
10452 Run {
10453 run_description: "Suffix is a subsequence -- contiguous",
10454 initial_state: "SubˇErr".into(),
10455 buffer_marked_text: "<Sub|Err>".into(),
10456 completion_text: "SubscriptionError",
10457 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
10458 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10459 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10460 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
10461 },
10462 Run {
10463 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
10464 initial_state: "Suˇscrirr".into(),
10465 buffer_marked_text: "<Su|scrirr>".into(),
10466 completion_text: "SubscriptionError",
10467 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
10468 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10469 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10470 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
10471 },
10472 Run {
10473 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
10474 initial_state: "foo(indˇix)".into(),
10475 buffer_marked_text: "foo(<ind|ix>)".into(),
10476 completion_text: "node_index",
10477 expected_with_insert_mode: "foo(node_indexˇix)".into(),
10478 expected_with_replace_mode: "foo(node_indexˇ)".into(),
10479 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
10480 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
10481 },
10482 ];
10483
10484 for run in runs {
10485 let run_variations = [
10486 (LspInsertMode::Insert, run.expected_with_insert_mode),
10487 (LspInsertMode::Replace, run.expected_with_replace_mode),
10488 (
10489 LspInsertMode::ReplaceSubsequence,
10490 run.expected_with_replace_subsequence_mode,
10491 ),
10492 (
10493 LspInsertMode::ReplaceSuffix,
10494 run.expected_with_replace_suffix_mode,
10495 ),
10496 ];
10497
10498 for (lsp_insert_mode, expected_text) in run_variations {
10499 eprintln!(
10500 "run = {:?}, mode = {lsp_insert_mode:.?}",
10501 run.run_description,
10502 );
10503
10504 update_test_language_settings(&mut cx, |settings| {
10505 settings.defaults.completions = Some(CompletionSettings {
10506 lsp_insert_mode,
10507 words: WordsCompletionMode::Disabled,
10508 lsp: true,
10509 lsp_fetch_timeout_ms: 0,
10510 });
10511 });
10512
10513 cx.set_state(&run.initial_state);
10514 cx.update_editor(|editor, window, cx| {
10515 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10516 });
10517
10518 let counter = Arc::new(AtomicUsize::new(0));
10519 handle_completion_request_with_insert_and_replace(
10520 &mut cx,
10521 &run.buffer_marked_text,
10522 vec![run.completion_text],
10523 counter.clone(),
10524 )
10525 .await;
10526 cx.condition(|editor, _| editor.context_menu_visible())
10527 .await;
10528 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
10529
10530 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10531 editor
10532 .confirm_completion(&ConfirmCompletion::default(), window, cx)
10533 .unwrap()
10534 });
10535 cx.assert_editor_state(&expected_text);
10536 handle_resolve_completion_request(&mut cx, None).await;
10537 apply_additional_edits.await.unwrap();
10538 }
10539 }
10540}
10541
10542#[gpui::test]
10543async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
10544 init_test(cx, |_| {});
10545 let mut cx = EditorLspTestContext::new_rust(
10546 lsp::ServerCapabilities {
10547 completion_provider: Some(lsp::CompletionOptions {
10548 resolve_provider: Some(true),
10549 ..Default::default()
10550 }),
10551 ..Default::default()
10552 },
10553 cx,
10554 )
10555 .await;
10556
10557 let initial_state = "SubˇError";
10558 let buffer_marked_text = "<Sub|Error>";
10559 let completion_text = "SubscriptionError";
10560 let expected_with_insert_mode = "SubscriptionErrorˇError";
10561 let expected_with_replace_mode = "SubscriptionErrorˇ";
10562
10563 update_test_language_settings(&mut cx, |settings| {
10564 settings.defaults.completions = Some(CompletionSettings {
10565 words: WordsCompletionMode::Disabled,
10566 // set the opposite here to ensure that the action is overriding the default behavior
10567 lsp_insert_mode: LspInsertMode::Insert,
10568 lsp: true,
10569 lsp_fetch_timeout_ms: 0,
10570 });
10571 });
10572
10573 cx.set_state(initial_state);
10574 cx.update_editor(|editor, window, cx| {
10575 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10576 });
10577
10578 let counter = Arc::new(AtomicUsize::new(0));
10579 handle_completion_request_with_insert_and_replace(
10580 &mut cx,
10581 &buffer_marked_text,
10582 vec![completion_text],
10583 counter.clone(),
10584 )
10585 .await;
10586 cx.condition(|editor, _| editor.context_menu_visible())
10587 .await;
10588 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
10589
10590 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10591 editor
10592 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10593 .unwrap()
10594 });
10595 cx.assert_editor_state(&expected_with_replace_mode);
10596 handle_resolve_completion_request(&mut cx, None).await;
10597 apply_additional_edits.await.unwrap();
10598
10599 update_test_language_settings(&mut cx, |settings| {
10600 settings.defaults.completions = Some(CompletionSettings {
10601 words: WordsCompletionMode::Disabled,
10602 // set the opposite here to ensure that the action is overriding the default behavior
10603 lsp_insert_mode: LspInsertMode::Replace,
10604 lsp: true,
10605 lsp_fetch_timeout_ms: 0,
10606 });
10607 });
10608
10609 cx.set_state(initial_state);
10610 cx.update_editor(|editor, window, cx| {
10611 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10612 });
10613 handle_completion_request_with_insert_and_replace(
10614 &mut cx,
10615 &buffer_marked_text,
10616 vec![completion_text],
10617 counter.clone(),
10618 )
10619 .await;
10620 cx.condition(|editor, _| editor.context_menu_visible())
10621 .await;
10622 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
10623
10624 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10625 editor
10626 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
10627 .unwrap()
10628 });
10629 cx.assert_editor_state(&expected_with_insert_mode);
10630 handle_resolve_completion_request(&mut cx, None).await;
10631 apply_additional_edits.await.unwrap();
10632}
10633
10634#[gpui::test]
10635async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
10636 init_test(cx, |_| {});
10637 let mut cx = EditorLspTestContext::new_rust(
10638 lsp::ServerCapabilities {
10639 completion_provider: Some(lsp::CompletionOptions {
10640 resolve_provider: Some(true),
10641 ..Default::default()
10642 }),
10643 ..Default::default()
10644 },
10645 cx,
10646 )
10647 .await;
10648
10649 // scenario: surrounding text matches completion text
10650 let completion_text = "to_offset";
10651 let initial_state = indoc! {"
10652 1. buf.to_offˇsuffix
10653 2. buf.to_offˇsuf
10654 3. buf.to_offˇfix
10655 4. buf.to_offˇ
10656 5. into_offˇensive
10657 6. ˇsuffix
10658 7. let ˇ //
10659 8. aaˇzz
10660 9. buf.to_off«zzzzzˇ»suffix
10661 10. buf.«ˇzzzzz»suffix
10662 11. to_off«ˇzzzzz»
10663
10664 buf.to_offˇsuffix // newest cursor
10665 "};
10666 let completion_marked_buffer = indoc! {"
10667 1. buf.to_offsuffix
10668 2. buf.to_offsuf
10669 3. buf.to_offfix
10670 4. buf.to_off
10671 5. into_offensive
10672 6. suffix
10673 7. let //
10674 8. aazz
10675 9. buf.to_offzzzzzsuffix
10676 10. buf.zzzzzsuffix
10677 11. to_offzzzzz
10678
10679 buf.<to_off|suffix> // newest cursor
10680 "};
10681 let expected = indoc! {"
10682 1. buf.to_offsetˇ
10683 2. buf.to_offsetˇsuf
10684 3. buf.to_offsetˇfix
10685 4. buf.to_offsetˇ
10686 5. into_offsetˇensive
10687 6. to_offsetˇsuffix
10688 7. let to_offsetˇ //
10689 8. aato_offsetˇzz
10690 9. buf.to_offsetˇ
10691 10. buf.to_offsetˇsuffix
10692 11. to_offsetˇ
10693
10694 buf.to_offsetˇ // newest cursor
10695 "};
10696 cx.set_state(initial_state);
10697 cx.update_editor(|editor, window, cx| {
10698 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10699 });
10700 handle_completion_request_with_insert_and_replace(
10701 &mut cx,
10702 completion_marked_buffer,
10703 vec![completion_text],
10704 Arc::new(AtomicUsize::new(0)),
10705 )
10706 .await;
10707 cx.condition(|editor, _| editor.context_menu_visible())
10708 .await;
10709 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10710 editor
10711 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10712 .unwrap()
10713 });
10714 cx.assert_editor_state(expected);
10715 handle_resolve_completion_request(&mut cx, None).await;
10716 apply_additional_edits.await.unwrap();
10717
10718 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
10719 let completion_text = "foo_and_bar";
10720 let initial_state = indoc! {"
10721 1. ooanbˇ
10722 2. zooanbˇ
10723 3. ooanbˇz
10724 4. zooanbˇz
10725 5. ooanˇ
10726 6. oanbˇ
10727
10728 ooanbˇ
10729 "};
10730 let completion_marked_buffer = indoc! {"
10731 1. ooanb
10732 2. zooanb
10733 3. ooanbz
10734 4. zooanbz
10735 5. ooan
10736 6. oanb
10737
10738 <ooanb|>
10739 "};
10740 let expected = indoc! {"
10741 1. foo_and_barˇ
10742 2. zfoo_and_barˇ
10743 3. foo_and_barˇz
10744 4. zfoo_and_barˇz
10745 5. ooanfoo_and_barˇ
10746 6. oanbfoo_and_barˇ
10747
10748 foo_and_barˇ
10749 "};
10750 cx.set_state(initial_state);
10751 cx.update_editor(|editor, window, cx| {
10752 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10753 });
10754 handle_completion_request_with_insert_and_replace(
10755 &mut cx,
10756 completion_marked_buffer,
10757 vec![completion_text],
10758 Arc::new(AtomicUsize::new(0)),
10759 )
10760 .await;
10761 cx.condition(|editor, _| editor.context_menu_visible())
10762 .await;
10763 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10764 editor
10765 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10766 .unwrap()
10767 });
10768 cx.assert_editor_state(expected);
10769 handle_resolve_completion_request(&mut cx, None).await;
10770 apply_additional_edits.await.unwrap();
10771
10772 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
10773 // (expects the same as if it was inserted at the end)
10774 let completion_text = "foo_and_bar";
10775 let initial_state = indoc! {"
10776 1. ooˇanb
10777 2. zooˇanb
10778 3. ooˇanbz
10779 4. zooˇanbz
10780
10781 ooˇanb
10782 "};
10783 let completion_marked_buffer = indoc! {"
10784 1. ooanb
10785 2. zooanb
10786 3. ooanbz
10787 4. zooanbz
10788
10789 <oo|anb>
10790 "};
10791 let expected = indoc! {"
10792 1. foo_and_barˇ
10793 2. zfoo_and_barˇ
10794 3. foo_and_barˇz
10795 4. zfoo_and_barˇz
10796
10797 foo_and_barˇ
10798 "};
10799 cx.set_state(initial_state);
10800 cx.update_editor(|editor, window, cx| {
10801 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10802 });
10803 handle_completion_request_with_insert_and_replace(
10804 &mut cx,
10805 completion_marked_buffer,
10806 vec![completion_text],
10807 Arc::new(AtomicUsize::new(0)),
10808 )
10809 .await;
10810 cx.condition(|editor, _| editor.context_menu_visible())
10811 .await;
10812 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10813 editor
10814 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10815 .unwrap()
10816 });
10817 cx.assert_editor_state(expected);
10818 handle_resolve_completion_request(&mut cx, None).await;
10819 apply_additional_edits.await.unwrap();
10820}
10821
10822// This used to crash
10823#[gpui::test]
10824async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
10825 init_test(cx, |_| {});
10826
10827 let buffer_text = indoc! {"
10828 fn main() {
10829 10.satu;
10830
10831 //
10832 // separate cursors so they open in different excerpts (manually reproducible)
10833 //
10834
10835 10.satu20;
10836 }
10837 "};
10838 let multibuffer_text_with_selections = indoc! {"
10839 fn main() {
10840 10.satuˇ;
10841
10842 //
10843
10844 //
10845
10846 10.satuˇ20;
10847 }
10848 "};
10849 let expected_multibuffer = indoc! {"
10850 fn main() {
10851 10.saturating_sub()ˇ;
10852
10853 //
10854
10855 //
10856
10857 10.saturating_sub()ˇ;
10858 }
10859 "};
10860
10861 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
10862 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
10863
10864 let fs = FakeFs::new(cx.executor());
10865 fs.insert_tree(
10866 path!("/a"),
10867 json!({
10868 "main.rs": buffer_text,
10869 }),
10870 )
10871 .await;
10872
10873 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
10874 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10875 language_registry.add(rust_lang());
10876 let mut fake_servers = language_registry.register_fake_lsp(
10877 "Rust",
10878 FakeLspAdapter {
10879 capabilities: lsp::ServerCapabilities {
10880 completion_provider: Some(lsp::CompletionOptions {
10881 resolve_provider: None,
10882 ..lsp::CompletionOptions::default()
10883 }),
10884 ..lsp::ServerCapabilities::default()
10885 },
10886 ..FakeLspAdapter::default()
10887 },
10888 );
10889 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
10890 let cx = &mut VisualTestContext::from_window(*workspace, cx);
10891 let buffer = project
10892 .update(cx, |project, cx| {
10893 project.open_local_buffer(path!("/a/main.rs"), cx)
10894 })
10895 .await
10896 .unwrap();
10897
10898 let multi_buffer = cx.new(|cx| {
10899 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
10900 multi_buffer.push_excerpts(
10901 buffer.clone(),
10902 [ExcerptRange::new(0..first_excerpt_end)],
10903 cx,
10904 );
10905 multi_buffer.push_excerpts(
10906 buffer.clone(),
10907 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
10908 cx,
10909 );
10910 multi_buffer
10911 });
10912
10913 let editor = workspace
10914 .update(cx, |_, window, cx| {
10915 cx.new(|cx| {
10916 Editor::new(
10917 EditorMode::Full {
10918 scale_ui_elements_with_buffer_font_size: false,
10919 show_active_line_background: false,
10920 sized_by_content: false,
10921 },
10922 multi_buffer.clone(),
10923 Some(project.clone()),
10924 window,
10925 cx,
10926 )
10927 })
10928 })
10929 .unwrap();
10930
10931 let pane = workspace
10932 .update(cx, |workspace, _, _| workspace.active_pane().clone())
10933 .unwrap();
10934 pane.update_in(cx, |pane, window, cx| {
10935 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
10936 });
10937
10938 let fake_server = fake_servers.next().await.unwrap();
10939
10940 editor.update_in(cx, |editor, window, cx| {
10941 editor.change_selections(None, window, cx, |s| {
10942 s.select_ranges([
10943 Point::new(1, 11)..Point::new(1, 11),
10944 Point::new(7, 11)..Point::new(7, 11),
10945 ])
10946 });
10947
10948 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
10949 });
10950
10951 editor.update_in(cx, |editor, window, cx| {
10952 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10953 });
10954
10955 fake_server
10956 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
10957 let completion_item = lsp::CompletionItem {
10958 label: "saturating_sub()".into(),
10959 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
10960 lsp::InsertReplaceEdit {
10961 new_text: "saturating_sub()".to_owned(),
10962 insert: lsp::Range::new(
10963 lsp::Position::new(7, 7),
10964 lsp::Position::new(7, 11),
10965 ),
10966 replace: lsp::Range::new(
10967 lsp::Position::new(7, 7),
10968 lsp::Position::new(7, 13),
10969 ),
10970 },
10971 )),
10972 ..lsp::CompletionItem::default()
10973 };
10974
10975 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
10976 })
10977 .next()
10978 .await
10979 .unwrap();
10980
10981 cx.condition(&editor, |editor, _| editor.context_menu_visible())
10982 .await;
10983
10984 editor
10985 .update_in(cx, |editor, window, cx| {
10986 editor
10987 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10988 .unwrap()
10989 })
10990 .await
10991 .unwrap();
10992
10993 editor.update(cx, |editor, cx| {
10994 assert_text_with_selections(editor, expected_multibuffer, cx);
10995 })
10996}
10997
10998#[gpui::test]
10999async fn test_completion(cx: &mut TestAppContext) {
11000 init_test(cx, |_| {});
11001
11002 let mut cx = EditorLspTestContext::new_rust(
11003 lsp::ServerCapabilities {
11004 completion_provider: Some(lsp::CompletionOptions {
11005 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11006 resolve_provider: Some(true),
11007 ..Default::default()
11008 }),
11009 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11010 ..Default::default()
11011 },
11012 cx,
11013 )
11014 .await;
11015 let counter = Arc::new(AtomicUsize::new(0));
11016
11017 cx.set_state(indoc! {"
11018 oneˇ
11019 two
11020 three
11021 "});
11022 cx.simulate_keystroke(".");
11023 handle_completion_request(
11024 &mut cx,
11025 indoc! {"
11026 one.|<>
11027 two
11028 three
11029 "},
11030 vec!["first_completion", "second_completion"],
11031 counter.clone(),
11032 )
11033 .await;
11034 cx.condition(|editor, _| editor.context_menu_visible())
11035 .await;
11036 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11037
11038 let _handler = handle_signature_help_request(
11039 &mut cx,
11040 lsp::SignatureHelp {
11041 signatures: vec![lsp::SignatureInformation {
11042 label: "test signature".to_string(),
11043 documentation: None,
11044 parameters: Some(vec![lsp::ParameterInformation {
11045 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
11046 documentation: None,
11047 }]),
11048 active_parameter: None,
11049 }],
11050 active_signature: None,
11051 active_parameter: None,
11052 },
11053 );
11054 cx.update_editor(|editor, window, cx| {
11055 assert!(
11056 !editor.signature_help_state.is_shown(),
11057 "No signature help was called for"
11058 );
11059 editor.show_signature_help(&ShowSignatureHelp, window, cx);
11060 });
11061 cx.run_until_parked();
11062 cx.update_editor(|editor, _, _| {
11063 assert!(
11064 !editor.signature_help_state.is_shown(),
11065 "No signature help should be shown when completions menu is open"
11066 );
11067 });
11068
11069 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11070 editor.context_menu_next(&Default::default(), window, cx);
11071 editor
11072 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11073 .unwrap()
11074 });
11075 cx.assert_editor_state(indoc! {"
11076 one.second_completionˇ
11077 two
11078 three
11079 "});
11080
11081 handle_resolve_completion_request(
11082 &mut cx,
11083 Some(vec![
11084 (
11085 //This overlaps with the primary completion edit which is
11086 //misbehavior from the LSP spec, test that we filter it out
11087 indoc! {"
11088 one.second_ˇcompletion
11089 two
11090 threeˇ
11091 "},
11092 "overlapping additional edit",
11093 ),
11094 (
11095 indoc! {"
11096 one.second_completion
11097 two
11098 threeˇ
11099 "},
11100 "\nadditional edit",
11101 ),
11102 ]),
11103 )
11104 .await;
11105 apply_additional_edits.await.unwrap();
11106 cx.assert_editor_state(indoc! {"
11107 one.second_completionˇ
11108 two
11109 three
11110 additional edit
11111 "});
11112
11113 cx.set_state(indoc! {"
11114 one.second_completion
11115 twoˇ
11116 threeˇ
11117 additional edit
11118 "});
11119 cx.simulate_keystroke(" ");
11120 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11121 cx.simulate_keystroke("s");
11122 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11123
11124 cx.assert_editor_state(indoc! {"
11125 one.second_completion
11126 two sˇ
11127 three sˇ
11128 additional edit
11129 "});
11130 handle_completion_request(
11131 &mut cx,
11132 indoc! {"
11133 one.second_completion
11134 two s
11135 three <s|>
11136 additional edit
11137 "},
11138 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
11139 counter.clone(),
11140 )
11141 .await;
11142 cx.condition(|editor, _| editor.context_menu_visible())
11143 .await;
11144 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
11145
11146 cx.simulate_keystroke("i");
11147
11148 handle_completion_request(
11149 &mut cx,
11150 indoc! {"
11151 one.second_completion
11152 two si
11153 three <si|>
11154 additional edit
11155 "},
11156 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
11157 counter.clone(),
11158 )
11159 .await;
11160 cx.condition(|editor, _| editor.context_menu_visible())
11161 .await;
11162 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
11163
11164 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11165 editor
11166 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11167 .unwrap()
11168 });
11169 cx.assert_editor_state(indoc! {"
11170 one.second_completion
11171 two sixth_completionˇ
11172 three sixth_completionˇ
11173 additional edit
11174 "});
11175
11176 apply_additional_edits.await.unwrap();
11177
11178 update_test_language_settings(&mut cx, |settings| {
11179 settings.defaults.show_completions_on_input = Some(false);
11180 });
11181 cx.set_state("editorˇ");
11182 cx.simulate_keystroke(".");
11183 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11184 cx.simulate_keystrokes("c l o");
11185 cx.assert_editor_state("editor.cloˇ");
11186 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11187 cx.update_editor(|editor, window, cx| {
11188 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11189 });
11190 handle_completion_request(
11191 &mut cx,
11192 "editor.<clo|>",
11193 vec!["close", "clobber"],
11194 counter.clone(),
11195 )
11196 .await;
11197 cx.condition(|editor, _| editor.context_menu_visible())
11198 .await;
11199 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
11200
11201 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11202 editor
11203 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11204 .unwrap()
11205 });
11206 cx.assert_editor_state("editor.closeˇ");
11207 handle_resolve_completion_request(&mut cx, None).await;
11208 apply_additional_edits.await.unwrap();
11209}
11210
11211#[gpui::test]
11212async fn test_word_completion(cx: &mut TestAppContext) {
11213 let lsp_fetch_timeout_ms = 10;
11214 init_test(cx, |language_settings| {
11215 language_settings.defaults.completions = Some(CompletionSettings {
11216 words: WordsCompletionMode::Fallback,
11217 lsp: true,
11218 lsp_fetch_timeout_ms: 10,
11219 lsp_insert_mode: LspInsertMode::Insert,
11220 });
11221 });
11222
11223 let mut cx = EditorLspTestContext::new_rust(
11224 lsp::ServerCapabilities {
11225 completion_provider: Some(lsp::CompletionOptions {
11226 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11227 ..lsp::CompletionOptions::default()
11228 }),
11229 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11230 ..lsp::ServerCapabilities::default()
11231 },
11232 cx,
11233 )
11234 .await;
11235
11236 let throttle_completions = Arc::new(AtomicBool::new(false));
11237
11238 let lsp_throttle_completions = throttle_completions.clone();
11239 let _completion_requests_handler =
11240 cx.lsp
11241 .server
11242 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
11243 let lsp_throttle_completions = lsp_throttle_completions.clone();
11244 let cx = cx.clone();
11245 async move {
11246 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
11247 cx.background_executor()
11248 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
11249 .await;
11250 }
11251 Ok(Some(lsp::CompletionResponse::Array(vec![
11252 lsp::CompletionItem {
11253 label: "first".into(),
11254 ..lsp::CompletionItem::default()
11255 },
11256 lsp::CompletionItem {
11257 label: "last".into(),
11258 ..lsp::CompletionItem::default()
11259 },
11260 ])))
11261 }
11262 });
11263
11264 cx.set_state(indoc! {"
11265 oneˇ
11266 two
11267 three
11268 "});
11269 cx.simulate_keystroke(".");
11270 cx.executor().run_until_parked();
11271 cx.condition(|editor, _| editor.context_menu_visible())
11272 .await;
11273 cx.update_editor(|editor, window, cx| {
11274 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11275 {
11276 assert_eq!(
11277 completion_menu_entries(&menu),
11278 &["first", "last"],
11279 "When LSP server is fast to reply, no fallback word completions are used"
11280 );
11281 } else {
11282 panic!("expected completion menu to be open");
11283 }
11284 editor.cancel(&Cancel, window, cx);
11285 });
11286 cx.executor().run_until_parked();
11287 cx.condition(|editor, _| !editor.context_menu_visible())
11288 .await;
11289
11290 throttle_completions.store(true, atomic::Ordering::Release);
11291 cx.simulate_keystroke(".");
11292 cx.executor()
11293 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
11294 cx.executor().run_until_parked();
11295 cx.condition(|editor, _| editor.context_menu_visible())
11296 .await;
11297 cx.update_editor(|editor, _, _| {
11298 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11299 {
11300 assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
11301 "When LSP server is slow, document words can be shown instead, if configured accordingly");
11302 } else {
11303 panic!("expected completion menu to be open");
11304 }
11305 });
11306}
11307
11308#[gpui::test]
11309async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
11310 init_test(cx, |language_settings| {
11311 language_settings.defaults.completions = Some(CompletionSettings {
11312 words: WordsCompletionMode::Enabled,
11313 lsp: true,
11314 lsp_fetch_timeout_ms: 0,
11315 lsp_insert_mode: LspInsertMode::Insert,
11316 });
11317 });
11318
11319 let mut cx = EditorLspTestContext::new_rust(
11320 lsp::ServerCapabilities {
11321 completion_provider: Some(lsp::CompletionOptions {
11322 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11323 ..lsp::CompletionOptions::default()
11324 }),
11325 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11326 ..lsp::ServerCapabilities::default()
11327 },
11328 cx,
11329 )
11330 .await;
11331
11332 let _completion_requests_handler =
11333 cx.lsp
11334 .server
11335 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
11336 Ok(Some(lsp::CompletionResponse::Array(vec![
11337 lsp::CompletionItem {
11338 label: "first".into(),
11339 ..lsp::CompletionItem::default()
11340 },
11341 lsp::CompletionItem {
11342 label: "last".into(),
11343 ..lsp::CompletionItem::default()
11344 },
11345 ])))
11346 });
11347
11348 cx.set_state(indoc! {"ˇ
11349 first
11350 last
11351 second
11352 "});
11353 cx.simulate_keystroke(".");
11354 cx.executor().run_until_parked();
11355 cx.condition(|editor, _| editor.context_menu_visible())
11356 .await;
11357 cx.update_editor(|editor, _, _| {
11358 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11359 {
11360 assert_eq!(
11361 completion_menu_entries(&menu),
11362 &["first", "last", "second"],
11363 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
11364 );
11365 } else {
11366 panic!("expected completion menu to be open");
11367 }
11368 });
11369}
11370
11371#[gpui::test]
11372async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
11373 init_test(cx, |language_settings| {
11374 language_settings.defaults.completions = Some(CompletionSettings {
11375 words: WordsCompletionMode::Disabled,
11376 lsp: true,
11377 lsp_fetch_timeout_ms: 0,
11378 lsp_insert_mode: LspInsertMode::Insert,
11379 });
11380 });
11381
11382 let mut cx = EditorLspTestContext::new_rust(
11383 lsp::ServerCapabilities {
11384 completion_provider: Some(lsp::CompletionOptions {
11385 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11386 ..lsp::CompletionOptions::default()
11387 }),
11388 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11389 ..lsp::ServerCapabilities::default()
11390 },
11391 cx,
11392 )
11393 .await;
11394
11395 let _completion_requests_handler =
11396 cx.lsp
11397 .server
11398 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
11399 panic!("LSP completions should not be queried when dealing with word completions")
11400 });
11401
11402 cx.set_state(indoc! {"ˇ
11403 first
11404 last
11405 second
11406 "});
11407 cx.update_editor(|editor, window, cx| {
11408 editor.show_word_completions(&ShowWordCompletions, window, cx);
11409 });
11410 cx.executor().run_until_parked();
11411 cx.condition(|editor, _| editor.context_menu_visible())
11412 .await;
11413 cx.update_editor(|editor, _, _| {
11414 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11415 {
11416 assert_eq!(
11417 completion_menu_entries(&menu),
11418 &["first", "last", "second"],
11419 "`ShowWordCompletions` action should show word completions"
11420 );
11421 } else {
11422 panic!("expected completion menu to be open");
11423 }
11424 });
11425
11426 cx.simulate_keystroke("l");
11427 cx.executor().run_until_parked();
11428 cx.condition(|editor, _| editor.context_menu_visible())
11429 .await;
11430 cx.update_editor(|editor, _, _| {
11431 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11432 {
11433 assert_eq!(
11434 completion_menu_entries(&menu),
11435 &["last"],
11436 "After showing word completions, further editing should filter them and not query the LSP"
11437 );
11438 } else {
11439 panic!("expected completion menu to be open");
11440 }
11441 });
11442}
11443
11444#[gpui::test]
11445async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
11446 init_test(cx, |language_settings| {
11447 language_settings.defaults.completions = Some(CompletionSettings {
11448 words: WordsCompletionMode::Fallback,
11449 lsp: false,
11450 lsp_fetch_timeout_ms: 0,
11451 lsp_insert_mode: LspInsertMode::Insert,
11452 });
11453 });
11454
11455 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
11456
11457 cx.set_state(indoc! {"ˇ
11458 0_usize
11459 let
11460 33
11461 4.5f32
11462 "});
11463 cx.update_editor(|editor, window, cx| {
11464 editor.show_completions(&ShowCompletions::default(), window, cx);
11465 });
11466 cx.executor().run_until_parked();
11467 cx.condition(|editor, _| editor.context_menu_visible())
11468 .await;
11469 cx.update_editor(|editor, window, cx| {
11470 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11471 {
11472 assert_eq!(
11473 completion_menu_entries(&menu),
11474 &["let"],
11475 "With no digits in the completion query, no digits should be in the word completions"
11476 );
11477 } else {
11478 panic!("expected completion menu to be open");
11479 }
11480 editor.cancel(&Cancel, window, cx);
11481 });
11482
11483 cx.set_state(indoc! {"3ˇ
11484 0_usize
11485 let
11486 3
11487 33.35f32
11488 "});
11489 cx.update_editor(|editor, window, cx| {
11490 editor.show_completions(&ShowCompletions::default(), window, cx);
11491 });
11492 cx.executor().run_until_parked();
11493 cx.condition(|editor, _| editor.context_menu_visible())
11494 .await;
11495 cx.update_editor(|editor, _, _| {
11496 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11497 {
11498 assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
11499 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
11500 } else {
11501 panic!("expected completion menu to be open");
11502 }
11503 });
11504}
11505
11506fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
11507 let position = || lsp::Position {
11508 line: params.text_document_position.position.line,
11509 character: params.text_document_position.position.character,
11510 };
11511 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11512 range: lsp::Range {
11513 start: position(),
11514 end: position(),
11515 },
11516 new_text: text.to_string(),
11517 }))
11518}
11519
11520#[gpui::test]
11521async fn test_multiline_completion(cx: &mut TestAppContext) {
11522 init_test(cx, |_| {});
11523
11524 let fs = FakeFs::new(cx.executor());
11525 fs.insert_tree(
11526 path!("/a"),
11527 json!({
11528 "main.ts": "a",
11529 }),
11530 )
11531 .await;
11532
11533 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11534 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11535 let typescript_language = Arc::new(Language::new(
11536 LanguageConfig {
11537 name: "TypeScript".into(),
11538 matcher: LanguageMatcher {
11539 path_suffixes: vec!["ts".to_string()],
11540 ..LanguageMatcher::default()
11541 },
11542 line_comments: vec!["// ".into()],
11543 ..LanguageConfig::default()
11544 },
11545 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
11546 ));
11547 language_registry.add(typescript_language.clone());
11548 let mut fake_servers = language_registry.register_fake_lsp(
11549 "TypeScript",
11550 FakeLspAdapter {
11551 capabilities: lsp::ServerCapabilities {
11552 completion_provider: Some(lsp::CompletionOptions {
11553 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11554 ..lsp::CompletionOptions::default()
11555 }),
11556 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11557 ..lsp::ServerCapabilities::default()
11558 },
11559 // Emulate vtsls label generation
11560 label_for_completion: Some(Box::new(|item, _| {
11561 let text = if let Some(description) = item
11562 .label_details
11563 .as_ref()
11564 .and_then(|label_details| label_details.description.as_ref())
11565 {
11566 format!("{} {}", item.label, description)
11567 } else if let Some(detail) = &item.detail {
11568 format!("{} {}", item.label, detail)
11569 } else {
11570 item.label.clone()
11571 };
11572 let len = text.len();
11573 Some(language::CodeLabel {
11574 text,
11575 runs: Vec::new(),
11576 filter_range: 0..len,
11577 })
11578 })),
11579 ..FakeLspAdapter::default()
11580 },
11581 );
11582 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11583 let cx = &mut VisualTestContext::from_window(*workspace, cx);
11584 let worktree_id = workspace
11585 .update(cx, |workspace, _window, cx| {
11586 workspace.project().update(cx, |project, cx| {
11587 project.worktrees(cx).next().unwrap().read(cx).id()
11588 })
11589 })
11590 .unwrap();
11591 let _buffer = project
11592 .update(cx, |project, cx| {
11593 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
11594 })
11595 .await
11596 .unwrap();
11597 let editor = workspace
11598 .update(cx, |workspace, window, cx| {
11599 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
11600 })
11601 .unwrap()
11602 .await
11603 .unwrap()
11604 .downcast::<Editor>()
11605 .unwrap();
11606 let fake_server = fake_servers.next().await.unwrap();
11607
11608 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
11609 let multiline_label_2 = "a\nb\nc\n";
11610 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
11611 let multiline_description = "d\ne\nf\n";
11612 let multiline_detail_2 = "g\nh\ni\n";
11613
11614 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
11615 move |params, _| async move {
11616 Ok(Some(lsp::CompletionResponse::Array(vec![
11617 lsp::CompletionItem {
11618 label: multiline_label.to_string(),
11619 text_edit: gen_text_edit(¶ms, "new_text_1"),
11620 ..lsp::CompletionItem::default()
11621 },
11622 lsp::CompletionItem {
11623 label: "single line label 1".to_string(),
11624 detail: Some(multiline_detail.to_string()),
11625 text_edit: gen_text_edit(¶ms, "new_text_2"),
11626 ..lsp::CompletionItem::default()
11627 },
11628 lsp::CompletionItem {
11629 label: "single line label 2".to_string(),
11630 label_details: Some(lsp::CompletionItemLabelDetails {
11631 description: Some(multiline_description.to_string()),
11632 detail: None,
11633 }),
11634 text_edit: gen_text_edit(¶ms, "new_text_2"),
11635 ..lsp::CompletionItem::default()
11636 },
11637 lsp::CompletionItem {
11638 label: multiline_label_2.to_string(),
11639 detail: Some(multiline_detail_2.to_string()),
11640 text_edit: gen_text_edit(¶ms, "new_text_3"),
11641 ..lsp::CompletionItem::default()
11642 },
11643 lsp::CompletionItem {
11644 label: "Label with many spaces and \t but without newlines".to_string(),
11645 detail: Some(
11646 "Details with many spaces and \t but without newlines".to_string(),
11647 ),
11648 text_edit: gen_text_edit(¶ms, "new_text_4"),
11649 ..lsp::CompletionItem::default()
11650 },
11651 ])))
11652 },
11653 );
11654
11655 editor.update_in(cx, |editor, window, cx| {
11656 cx.focus_self(window);
11657 editor.move_to_end(&MoveToEnd, window, cx);
11658 editor.handle_input(".", window, cx);
11659 });
11660 cx.run_until_parked();
11661 completion_handle.next().await.unwrap();
11662
11663 editor.update(cx, |editor, _| {
11664 assert!(editor.context_menu_visible());
11665 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11666 {
11667 let completion_labels = menu
11668 .completions
11669 .borrow()
11670 .iter()
11671 .map(|c| c.label.text.clone())
11672 .collect::<Vec<_>>();
11673 assert_eq!(
11674 completion_labels,
11675 &[
11676 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
11677 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
11678 "single line label 2 d e f ",
11679 "a b c g h i ",
11680 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
11681 ],
11682 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
11683 );
11684
11685 for completion in menu
11686 .completions
11687 .borrow()
11688 .iter() {
11689 assert_eq!(
11690 completion.label.filter_range,
11691 0..completion.label.text.len(),
11692 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
11693 );
11694 }
11695 } else {
11696 panic!("expected completion menu to be open");
11697 }
11698 });
11699}
11700
11701#[gpui::test]
11702async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
11703 init_test(cx, |_| {});
11704 let mut cx = EditorLspTestContext::new_rust(
11705 lsp::ServerCapabilities {
11706 completion_provider: Some(lsp::CompletionOptions {
11707 trigger_characters: Some(vec![".".to_string()]),
11708 ..Default::default()
11709 }),
11710 ..Default::default()
11711 },
11712 cx,
11713 )
11714 .await;
11715 cx.lsp
11716 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
11717 Ok(Some(lsp::CompletionResponse::Array(vec![
11718 lsp::CompletionItem {
11719 label: "first".into(),
11720 ..Default::default()
11721 },
11722 lsp::CompletionItem {
11723 label: "last".into(),
11724 ..Default::default()
11725 },
11726 ])))
11727 });
11728 cx.set_state("variableˇ");
11729 cx.simulate_keystroke(".");
11730 cx.executor().run_until_parked();
11731
11732 cx.update_editor(|editor, _, _| {
11733 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11734 {
11735 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
11736 } else {
11737 panic!("expected completion menu to be open");
11738 }
11739 });
11740
11741 cx.update_editor(|editor, window, cx| {
11742 editor.move_page_down(&MovePageDown::default(), window, cx);
11743 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11744 {
11745 assert!(
11746 menu.selected_item == 1,
11747 "expected PageDown to select the last item from the context menu"
11748 );
11749 } else {
11750 panic!("expected completion menu to stay open after PageDown");
11751 }
11752 });
11753
11754 cx.update_editor(|editor, window, cx| {
11755 editor.move_page_up(&MovePageUp::default(), window, cx);
11756 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11757 {
11758 assert!(
11759 menu.selected_item == 0,
11760 "expected PageUp to select the first item from the context menu"
11761 );
11762 } else {
11763 panic!("expected completion menu to stay open after PageUp");
11764 }
11765 });
11766}
11767
11768#[gpui::test]
11769async fn test_as_is_completions(cx: &mut TestAppContext) {
11770 init_test(cx, |_| {});
11771 let mut cx = EditorLspTestContext::new_rust(
11772 lsp::ServerCapabilities {
11773 completion_provider: Some(lsp::CompletionOptions {
11774 ..Default::default()
11775 }),
11776 ..Default::default()
11777 },
11778 cx,
11779 )
11780 .await;
11781 cx.lsp
11782 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
11783 Ok(Some(lsp::CompletionResponse::Array(vec![
11784 lsp::CompletionItem {
11785 label: "unsafe".into(),
11786 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11787 range: lsp::Range {
11788 start: lsp::Position {
11789 line: 1,
11790 character: 2,
11791 },
11792 end: lsp::Position {
11793 line: 1,
11794 character: 3,
11795 },
11796 },
11797 new_text: "unsafe".to_string(),
11798 })),
11799 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
11800 ..Default::default()
11801 },
11802 ])))
11803 });
11804 cx.set_state("fn a() {}\n nˇ");
11805 cx.executor().run_until_parked();
11806 cx.update_editor(|editor, window, cx| {
11807 editor.show_completions(
11808 &ShowCompletions {
11809 trigger: Some("\n".into()),
11810 },
11811 window,
11812 cx,
11813 );
11814 });
11815 cx.executor().run_until_parked();
11816
11817 cx.update_editor(|editor, window, cx| {
11818 editor.confirm_completion(&Default::default(), window, cx)
11819 });
11820 cx.executor().run_until_parked();
11821 cx.assert_editor_state("fn a() {}\n unsafeˇ");
11822}
11823
11824#[gpui::test]
11825async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
11826 init_test(cx, |_| {});
11827
11828 let mut cx = EditorLspTestContext::new_rust(
11829 lsp::ServerCapabilities {
11830 completion_provider: Some(lsp::CompletionOptions {
11831 trigger_characters: Some(vec![".".to_string()]),
11832 resolve_provider: Some(true),
11833 ..Default::default()
11834 }),
11835 ..Default::default()
11836 },
11837 cx,
11838 )
11839 .await;
11840
11841 cx.set_state("fn main() { let a = 2ˇ; }");
11842 cx.simulate_keystroke(".");
11843 let completion_item = lsp::CompletionItem {
11844 label: "Some".into(),
11845 kind: Some(lsp::CompletionItemKind::SNIPPET),
11846 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
11847 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
11848 kind: lsp::MarkupKind::Markdown,
11849 value: "```rust\nSome(2)\n```".to_string(),
11850 })),
11851 deprecated: Some(false),
11852 sort_text: Some("Some".to_string()),
11853 filter_text: Some("Some".to_string()),
11854 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
11855 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11856 range: lsp::Range {
11857 start: lsp::Position {
11858 line: 0,
11859 character: 22,
11860 },
11861 end: lsp::Position {
11862 line: 0,
11863 character: 22,
11864 },
11865 },
11866 new_text: "Some(2)".to_string(),
11867 })),
11868 additional_text_edits: Some(vec![lsp::TextEdit {
11869 range: lsp::Range {
11870 start: lsp::Position {
11871 line: 0,
11872 character: 20,
11873 },
11874 end: lsp::Position {
11875 line: 0,
11876 character: 22,
11877 },
11878 },
11879 new_text: "".to_string(),
11880 }]),
11881 ..Default::default()
11882 };
11883
11884 let closure_completion_item = completion_item.clone();
11885 let counter = Arc::new(AtomicUsize::new(0));
11886 let counter_clone = counter.clone();
11887 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
11888 let task_completion_item = closure_completion_item.clone();
11889 counter_clone.fetch_add(1, atomic::Ordering::Release);
11890 async move {
11891 Ok(Some(lsp::CompletionResponse::Array(vec![
11892 task_completion_item,
11893 ])))
11894 }
11895 });
11896
11897 cx.condition(|editor, _| editor.context_menu_visible())
11898 .await;
11899 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
11900 assert!(request.next().await.is_some());
11901 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11902
11903 cx.simulate_keystrokes("S o m");
11904 cx.condition(|editor, _| editor.context_menu_visible())
11905 .await;
11906 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
11907 assert!(request.next().await.is_some());
11908 assert!(request.next().await.is_some());
11909 assert!(request.next().await.is_some());
11910 request.close();
11911 assert!(request.next().await.is_none());
11912 assert_eq!(
11913 counter.load(atomic::Ordering::Acquire),
11914 4,
11915 "With the completions menu open, only one LSP request should happen per input"
11916 );
11917}
11918
11919#[gpui::test]
11920async fn test_toggle_comment(cx: &mut TestAppContext) {
11921 init_test(cx, |_| {});
11922 let mut cx = EditorTestContext::new(cx).await;
11923 let language = Arc::new(Language::new(
11924 LanguageConfig {
11925 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
11926 ..Default::default()
11927 },
11928 Some(tree_sitter_rust::LANGUAGE.into()),
11929 ));
11930 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
11931
11932 // If multiple selections intersect a line, the line is only toggled once.
11933 cx.set_state(indoc! {"
11934 fn a() {
11935 «//b();
11936 ˇ»// «c();
11937 //ˇ» d();
11938 }
11939 "});
11940
11941 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11942
11943 cx.assert_editor_state(indoc! {"
11944 fn a() {
11945 «b();
11946 c();
11947 ˇ» d();
11948 }
11949 "});
11950
11951 // The comment prefix is inserted at the same column for every line in a
11952 // selection.
11953 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11954
11955 cx.assert_editor_state(indoc! {"
11956 fn a() {
11957 // «b();
11958 // c();
11959 ˇ»// d();
11960 }
11961 "});
11962
11963 // If a selection ends at the beginning of a line, that line is not toggled.
11964 cx.set_selections_state(indoc! {"
11965 fn a() {
11966 // b();
11967 «// c();
11968 ˇ» // d();
11969 }
11970 "});
11971
11972 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11973
11974 cx.assert_editor_state(indoc! {"
11975 fn a() {
11976 // b();
11977 «c();
11978 ˇ» // d();
11979 }
11980 "});
11981
11982 // If a selection span a single line and is empty, the line is toggled.
11983 cx.set_state(indoc! {"
11984 fn a() {
11985 a();
11986 b();
11987 ˇ
11988 }
11989 "});
11990
11991 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11992
11993 cx.assert_editor_state(indoc! {"
11994 fn a() {
11995 a();
11996 b();
11997 //•ˇ
11998 }
11999 "});
12000
12001 // If a selection span multiple lines, empty lines are not toggled.
12002 cx.set_state(indoc! {"
12003 fn a() {
12004 «a();
12005
12006 c();ˇ»
12007 }
12008 "});
12009
12010 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12011
12012 cx.assert_editor_state(indoc! {"
12013 fn a() {
12014 // «a();
12015
12016 // c();ˇ»
12017 }
12018 "});
12019
12020 // If a selection includes multiple comment prefixes, all lines are uncommented.
12021 cx.set_state(indoc! {"
12022 fn a() {
12023 «// a();
12024 /// b();
12025 //! c();ˇ»
12026 }
12027 "});
12028
12029 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12030
12031 cx.assert_editor_state(indoc! {"
12032 fn a() {
12033 «a();
12034 b();
12035 c();ˇ»
12036 }
12037 "});
12038}
12039
12040#[gpui::test]
12041async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
12042 init_test(cx, |_| {});
12043 let mut cx = EditorTestContext::new(cx).await;
12044 let language = Arc::new(Language::new(
12045 LanguageConfig {
12046 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
12047 ..Default::default()
12048 },
12049 Some(tree_sitter_rust::LANGUAGE.into()),
12050 ));
12051 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
12052
12053 let toggle_comments = &ToggleComments {
12054 advance_downwards: false,
12055 ignore_indent: true,
12056 };
12057
12058 // If multiple selections intersect a line, the line is only toggled once.
12059 cx.set_state(indoc! {"
12060 fn a() {
12061 // «b();
12062 // c();
12063 // ˇ» d();
12064 }
12065 "});
12066
12067 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12068
12069 cx.assert_editor_state(indoc! {"
12070 fn a() {
12071 «b();
12072 c();
12073 ˇ» d();
12074 }
12075 "});
12076
12077 // The comment prefix is inserted at the beginning of each line
12078 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12079
12080 cx.assert_editor_state(indoc! {"
12081 fn a() {
12082 // «b();
12083 // c();
12084 // ˇ» d();
12085 }
12086 "});
12087
12088 // If a selection ends at the beginning of a line, that line is not toggled.
12089 cx.set_selections_state(indoc! {"
12090 fn a() {
12091 // b();
12092 // «c();
12093 ˇ»// d();
12094 }
12095 "});
12096
12097 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12098
12099 cx.assert_editor_state(indoc! {"
12100 fn a() {
12101 // b();
12102 «c();
12103 ˇ»// d();
12104 }
12105 "});
12106
12107 // If a selection span a single line and is empty, the line is toggled.
12108 cx.set_state(indoc! {"
12109 fn a() {
12110 a();
12111 b();
12112 ˇ
12113 }
12114 "});
12115
12116 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12117
12118 cx.assert_editor_state(indoc! {"
12119 fn a() {
12120 a();
12121 b();
12122 //ˇ
12123 }
12124 "});
12125
12126 // If a selection span multiple lines, empty lines are not toggled.
12127 cx.set_state(indoc! {"
12128 fn a() {
12129 «a();
12130
12131 c();ˇ»
12132 }
12133 "});
12134
12135 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12136
12137 cx.assert_editor_state(indoc! {"
12138 fn a() {
12139 // «a();
12140
12141 // c();ˇ»
12142 }
12143 "});
12144
12145 // If a selection includes multiple comment prefixes, all lines are uncommented.
12146 cx.set_state(indoc! {"
12147 fn a() {
12148 // «a();
12149 /// b();
12150 //! c();ˇ»
12151 }
12152 "});
12153
12154 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12155
12156 cx.assert_editor_state(indoc! {"
12157 fn a() {
12158 «a();
12159 b();
12160 c();ˇ»
12161 }
12162 "});
12163}
12164
12165#[gpui::test]
12166async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
12167 init_test(cx, |_| {});
12168
12169 let language = Arc::new(Language::new(
12170 LanguageConfig {
12171 line_comments: vec!["// ".into()],
12172 ..Default::default()
12173 },
12174 Some(tree_sitter_rust::LANGUAGE.into()),
12175 ));
12176
12177 let mut cx = EditorTestContext::new(cx).await;
12178
12179 cx.language_registry().add(language.clone());
12180 cx.update_buffer(|buffer, cx| {
12181 buffer.set_language(Some(language), cx);
12182 });
12183
12184 let toggle_comments = &ToggleComments {
12185 advance_downwards: true,
12186 ignore_indent: false,
12187 };
12188
12189 // Single cursor on one line -> advance
12190 // Cursor moves horizontally 3 characters as well on non-blank line
12191 cx.set_state(indoc!(
12192 "fn a() {
12193 ˇdog();
12194 cat();
12195 }"
12196 ));
12197 cx.update_editor(|editor, window, cx| {
12198 editor.toggle_comments(toggle_comments, window, cx);
12199 });
12200 cx.assert_editor_state(indoc!(
12201 "fn a() {
12202 // dog();
12203 catˇ();
12204 }"
12205 ));
12206
12207 // Single selection on one line -> don't advance
12208 cx.set_state(indoc!(
12209 "fn a() {
12210 «dog()ˇ»;
12211 cat();
12212 }"
12213 ));
12214 cx.update_editor(|editor, window, cx| {
12215 editor.toggle_comments(toggle_comments, window, cx);
12216 });
12217 cx.assert_editor_state(indoc!(
12218 "fn a() {
12219 // «dog()ˇ»;
12220 cat();
12221 }"
12222 ));
12223
12224 // Multiple cursors on one line -> advance
12225 cx.set_state(indoc!(
12226 "fn a() {
12227 ˇdˇog();
12228 cat();
12229 }"
12230 ));
12231 cx.update_editor(|editor, window, cx| {
12232 editor.toggle_comments(toggle_comments, window, cx);
12233 });
12234 cx.assert_editor_state(indoc!(
12235 "fn a() {
12236 // dog();
12237 catˇ(ˇ);
12238 }"
12239 ));
12240
12241 // Multiple cursors on one line, with selection -> don't advance
12242 cx.set_state(indoc!(
12243 "fn a() {
12244 ˇdˇog«()ˇ»;
12245 cat();
12246 }"
12247 ));
12248 cx.update_editor(|editor, window, cx| {
12249 editor.toggle_comments(toggle_comments, window, cx);
12250 });
12251 cx.assert_editor_state(indoc!(
12252 "fn a() {
12253 // ˇdˇog«()ˇ»;
12254 cat();
12255 }"
12256 ));
12257
12258 // Single cursor on one line -> advance
12259 // Cursor moves to column 0 on blank line
12260 cx.set_state(indoc!(
12261 "fn a() {
12262 ˇdog();
12263
12264 cat();
12265 }"
12266 ));
12267 cx.update_editor(|editor, window, cx| {
12268 editor.toggle_comments(toggle_comments, window, cx);
12269 });
12270 cx.assert_editor_state(indoc!(
12271 "fn a() {
12272 // dog();
12273 ˇ
12274 cat();
12275 }"
12276 ));
12277
12278 // Single cursor on one line -> advance
12279 // Cursor starts and ends at column 0
12280 cx.set_state(indoc!(
12281 "fn a() {
12282 ˇ dog();
12283 cat();
12284 }"
12285 ));
12286 cx.update_editor(|editor, window, cx| {
12287 editor.toggle_comments(toggle_comments, window, cx);
12288 });
12289 cx.assert_editor_state(indoc!(
12290 "fn a() {
12291 // dog();
12292 ˇ cat();
12293 }"
12294 ));
12295}
12296
12297#[gpui::test]
12298async fn test_toggle_block_comment(cx: &mut TestAppContext) {
12299 init_test(cx, |_| {});
12300
12301 let mut cx = EditorTestContext::new(cx).await;
12302
12303 let html_language = Arc::new(
12304 Language::new(
12305 LanguageConfig {
12306 name: "HTML".into(),
12307 block_comment: Some(("<!-- ".into(), " -->".into())),
12308 ..Default::default()
12309 },
12310 Some(tree_sitter_html::LANGUAGE.into()),
12311 )
12312 .with_injection_query(
12313 r#"
12314 (script_element
12315 (raw_text) @injection.content
12316 (#set! injection.language "javascript"))
12317 "#,
12318 )
12319 .unwrap(),
12320 );
12321
12322 let javascript_language = Arc::new(Language::new(
12323 LanguageConfig {
12324 name: "JavaScript".into(),
12325 line_comments: vec!["// ".into()],
12326 ..Default::default()
12327 },
12328 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
12329 ));
12330
12331 cx.language_registry().add(html_language.clone());
12332 cx.language_registry().add(javascript_language.clone());
12333 cx.update_buffer(|buffer, cx| {
12334 buffer.set_language(Some(html_language), cx);
12335 });
12336
12337 // Toggle comments for empty selections
12338 cx.set_state(
12339 &r#"
12340 <p>A</p>ˇ
12341 <p>B</p>ˇ
12342 <p>C</p>ˇ
12343 "#
12344 .unindent(),
12345 );
12346 cx.update_editor(|editor, window, cx| {
12347 editor.toggle_comments(&ToggleComments::default(), window, cx)
12348 });
12349 cx.assert_editor_state(
12350 &r#"
12351 <!-- <p>A</p>ˇ -->
12352 <!-- <p>B</p>ˇ -->
12353 <!-- <p>C</p>ˇ -->
12354 "#
12355 .unindent(),
12356 );
12357 cx.update_editor(|editor, window, cx| {
12358 editor.toggle_comments(&ToggleComments::default(), window, cx)
12359 });
12360 cx.assert_editor_state(
12361 &r#"
12362 <p>A</p>ˇ
12363 <p>B</p>ˇ
12364 <p>C</p>ˇ
12365 "#
12366 .unindent(),
12367 );
12368
12369 // Toggle comments for mixture of empty and non-empty selections, where
12370 // multiple selections occupy a given line.
12371 cx.set_state(
12372 &r#"
12373 <p>A«</p>
12374 <p>ˇ»B</p>ˇ
12375 <p>C«</p>
12376 <p>ˇ»D</p>ˇ
12377 "#
12378 .unindent(),
12379 );
12380
12381 cx.update_editor(|editor, window, cx| {
12382 editor.toggle_comments(&ToggleComments::default(), window, cx)
12383 });
12384 cx.assert_editor_state(
12385 &r#"
12386 <!-- <p>A«</p>
12387 <p>ˇ»B</p>ˇ -->
12388 <!-- <p>C«</p>
12389 <p>ˇ»D</p>ˇ -->
12390 "#
12391 .unindent(),
12392 );
12393 cx.update_editor(|editor, window, cx| {
12394 editor.toggle_comments(&ToggleComments::default(), window, cx)
12395 });
12396 cx.assert_editor_state(
12397 &r#"
12398 <p>A«</p>
12399 <p>ˇ»B</p>ˇ
12400 <p>C«</p>
12401 <p>ˇ»D</p>ˇ
12402 "#
12403 .unindent(),
12404 );
12405
12406 // Toggle comments when different languages are active for different
12407 // selections.
12408 cx.set_state(
12409 &r#"
12410 ˇ<script>
12411 ˇvar x = new Y();
12412 ˇ</script>
12413 "#
12414 .unindent(),
12415 );
12416 cx.executor().run_until_parked();
12417 cx.update_editor(|editor, window, cx| {
12418 editor.toggle_comments(&ToggleComments::default(), window, cx)
12419 });
12420 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
12421 // Uncommenting and commenting from this position brings in even more wrong artifacts.
12422 cx.assert_editor_state(
12423 &r#"
12424 <!-- ˇ<script> -->
12425 // ˇvar x = new Y();
12426 <!-- ˇ</script> -->
12427 "#
12428 .unindent(),
12429 );
12430}
12431
12432#[gpui::test]
12433fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
12434 init_test(cx, |_| {});
12435
12436 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
12437 let multibuffer = cx.new(|cx| {
12438 let mut multibuffer = MultiBuffer::new(ReadWrite);
12439 multibuffer.push_excerpts(
12440 buffer.clone(),
12441 [
12442 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
12443 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
12444 ],
12445 cx,
12446 );
12447 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
12448 multibuffer
12449 });
12450
12451 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
12452 editor.update_in(cx, |editor, window, cx| {
12453 assert_eq!(editor.text(cx), "aaaa\nbbbb");
12454 editor.change_selections(None, window, cx, |s| {
12455 s.select_ranges([
12456 Point::new(0, 0)..Point::new(0, 0),
12457 Point::new(1, 0)..Point::new(1, 0),
12458 ])
12459 });
12460
12461 editor.handle_input("X", window, cx);
12462 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
12463 assert_eq!(
12464 editor.selections.ranges(cx),
12465 [
12466 Point::new(0, 1)..Point::new(0, 1),
12467 Point::new(1, 1)..Point::new(1, 1),
12468 ]
12469 );
12470
12471 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
12472 editor.change_selections(None, window, cx, |s| {
12473 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
12474 });
12475 editor.backspace(&Default::default(), window, cx);
12476 assert_eq!(editor.text(cx), "Xa\nbbb");
12477 assert_eq!(
12478 editor.selections.ranges(cx),
12479 [Point::new(1, 0)..Point::new(1, 0)]
12480 );
12481
12482 editor.change_selections(None, window, cx, |s| {
12483 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
12484 });
12485 editor.backspace(&Default::default(), window, cx);
12486 assert_eq!(editor.text(cx), "X\nbb");
12487 assert_eq!(
12488 editor.selections.ranges(cx),
12489 [Point::new(0, 1)..Point::new(0, 1)]
12490 );
12491 });
12492}
12493
12494#[gpui::test]
12495fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
12496 init_test(cx, |_| {});
12497
12498 let markers = vec![('[', ']').into(), ('(', ')').into()];
12499 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
12500 indoc! {"
12501 [aaaa
12502 (bbbb]
12503 cccc)",
12504 },
12505 markers.clone(),
12506 );
12507 let excerpt_ranges = markers.into_iter().map(|marker| {
12508 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
12509 ExcerptRange::new(context.clone())
12510 });
12511 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
12512 let multibuffer = cx.new(|cx| {
12513 let mut multibuffer = MultiBuffer::new(ReadWrite);
12514 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
12515 multibuffer
12516 });
12517
12518 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
12519 editor.update_in(cx, |editor, window, cx| {
12520 let (expected_text, selection_ranges) = marked_text_ranges(
12521 indoc! {"
12522 aaaa
12523 bˇbbb
12524 bˇbbˇb
12525 cccc"
12526 },
12527 true,
12528 );
12529 assert_eq!(editor.text(cx), expected_text);
12530 editor.change_selections(None, window, cx, |s| s.select_ranges(selection_ranges));
12531
12532 editor.handle_input("X", window, cx);
12533
12534 let (expected_text, expected_selections) = marked_text_ranges(
12535 indoc! {"
12536 aaaa
12537 bXˇbbXb
12538 bXˇbbXˇb
12539 cccc"
12540 },
12541 false,
12542 );
12543 assert_eq!(editor.text(cx), expected_text);
12544 assert_eq!(editor.selections.ranges(cx), expected_selections);
12545
12546 editor.newline(&Newline, window, cx);
12547 let (expected_text, expected_selections) = marked_text_ranges(
12548 indoc! {"
12549 aaaa
12550 bX
12551 ˇbbX
12552 b
12553 bX
12554 ˇbbX
12555 ˇb
12556 cccc"
12557 },
12558 false,
12559 );
12560 assert_eq!(editor.text(cx), expected_text);
12561 assert_eq!(editor.selections.ranges(cx), expected_selections);
12562 });
12563}
12564
12565#[gpui::test]
12566fn test_refresh_selections(cx: &mut TestAppContext) {
12567 init_test(cx, |_| {});
12568
12569 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
12570 let mut excerpt1_id = None;
12571 let multibuffer = cx.new(|cx| {
12572 let mut multibuffer = MultiBuffer::new(ReadWrite);
12573 excerpt1_id = multibuffer
12574 .push_excerpts(
12575 buffer.clone(),
12576 [
12577 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
12578 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
12579 ],
12580 cx,
12581 )
12582 .into_iter()
12583 .next();
12584 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
12585 multibuffer
12586 });
12587
12588 let editor = cx.add_window(|window, cx| {
12589 let mut editor = build_editor(multibuffer.clone(), window, cx);
12590 let snapshot = editor.snapshot(window, cx);
12591 editor.change_selections(None, window, cx, |s| {
12592 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
12593 });
12594 editor.begin_selection(
12595 Point::new(2, 1).to_display_point(&snapshot),
12596 true,
12597 1,
12598 window,
12599 cx,
12600 );
12601 assert_eq!(
12602 editor.selections.ranges(cx),
12603 [
12604 Point::new(1, 3)..Point::new(1, 3),
12605 Point::new(2, 1)..Point::new(2, 1),
12606 ]
12607 );
12608 editor
12609 });
12610
12611 // Refreshing selections is a no-op when excerpts haven't changed.
12612 _ = editor.update(cx, |editor, window, cx| {
12613 editor.change_selections(None, window, cx, |s| s.refresh());
12614 assert_eq!(
12615 editor.selections.ranges(cx),
12616 [
12617 Point::new(1, 3)..Point::new(1, 3),
12618 Point::new(2, 1)..Point::new(2, 1),
12619 ]
12620 );
12621 });
12622
12623 multibuffer.update(cx, |multibuffer, cx| {
12624 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
12625 });
12626 _ = editor.update(cx, |editor, window, cx| {
12627 // Removing an excerpt causes the first selection to become degenerate.
12628 assert_eq!(
12629 editor.selections.ranges(cx),
12630 [
12631 Point::new(0, 0)..Point::new(0, 0),
12632 Point::new(0, 1)..Point::new(0, 1)
12633 ]
12634 );
12635
12636 // Refreshing selections will relocate the first selection to the original buffer
12637 // location.
12638 editor.change_selections(None, window, cx, |s| s.refresh());
12639 assert_eq!(
12640 editor.selections.ranges(cx),
12641 [
12642 Point::new(0, 1)..Point::new(0, 1),
12643 Point::new(0, 3)..Point::new(0, 3)
12644 ]
12645 );
12646 assert!(editor.selections.pending_anchor().is_some());
12647 });
12648}
12649
12650#[gpui::test]
12651fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
12652 init_test(cx, |_| {});
12653
12654 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
12655 let mut excerpt1_id = None;
12656 let multibuffer = cx.new(|cx| {
12657 let mut multibuffer = MultiBuffer::new(ReadWrite);
12658 excerpt1_id = multibuffer
12659 .push_excerpts(
12660 buffer.clone(),
12661 [
12662 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
12663 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
12664 ],
12665 cx,
12666 )
12667 .into_iter()
12668 .next();
12669 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
12670 multibuffer
12671 });
12672
12673 let editor = cx.add_window(|window, cx| {
12674 let mut editor = build_editor(multibuffer.clone(), window, cx);
12675 let snapshot = editor.snapshot(window, cx);
12676 editor.begin_selection(
12677 Point::new(1, 3).to_display_point(&snapshot),
12678 false,
12679 1,
12680 window,
12681 cx,
12682 );
12683 assert_eq!(
12684 editor.selections.ranges(cx),
12685 [Point::new(1, 3)..Point::new(1, 3)]
12686 );
12687 editor
12688 });
12689
12690 multibuffer.update(cx, |multibuffer, cx| {
12691 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
12692 });
12693 _ = editor.update(cx, |editor, window, cx| {
12694 assert_eq!(
12695 editor.selections.ranges(cx),
12696 [Point::new(0, 0)..Point::new(0, 0)]
12697 );
12698
12699 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
12700 editor.change_selections(None, window, cx, |s| s.refresh());
12701 assert_eq!(
12702 editor.selections.ranges(cx),
12703 [Point::new(0, 3)..Point::new(0, 3)]
12704 );
12705 assert!(editor.selections.pending_anchor().is_some());
12706 });
12707}
12708
12709#[gpui::test]
12710async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
12711 init_test(cx, |_| {});
12712
12713 let language = Arc::new(
12714 Language::new(
12715 LanguageConfig {
12716 brackets: BracketPairConfig {
12717 pairs: vec![
12718 BracketPair {
12719 start: "{".to_string(),
12720 end: "}".to_string(),
12721 close: true,
12722 surround: true,
12723 newline: true,
12724 },
12725 BracketPair {
12726 start: "/* ".to_string(),
12727 end: " */".to_string(),
12728 close: true,
12729 surround: true,
12730 newline: true,
12731 },
12732 ],
12733 ..Default::default()
12734 },
12735 ..Default::default()
12736 },
12737 Some(tree_sitter_rust::LANGUAGE.into()),
12738 )
12739 .with_indents_query("")
12740 .unwrap(),
12741 );
12742
12743 let text = concat!(
12744 "{ }\n", //
12745 " x\n", //
12746 " /* */\n", //
12747 "x\n", //
12748 "{{} }\n", //
12749 );
12750
12751 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
12752 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12753 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
12754 editor
12755 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
12756 .await;
12757
12758 editor.update_in(cx, |editor, window, cx| {
12759 editor.change_selections(None, window, cx, |s| {
12760 s.select_display_ranges([
12761 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
12762 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
12763 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
12764 ])
12765 });
12766 editor.newline(&Newline, window, cx);
12767
12768 assert_eq!(
12769 editor.buffer().read(cx).read(cx).text(),
12770 concat!(
12771 "{ \n", // Suppress rustfmt
12772 "\n", //
12773 "}\n", //
12774 " x\n", //
12775 " /* \n", //
12776 " \n", //
12777 " */\n", //
12778 "x\n", //
12779 "{{} \n", //
12780 "}\n", //
12781 )
12782 );
12783 });
12784}
12785
12786#[gpui::test]
12787fn test_highlighted_ranges(cx: &mut TestAppContext) {
12788 init_test(cx, |_| {});
12789
12790 let editor = cx.add_window(|window, cx| {
12791 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
12792 build_editor(buffer.clone(), window, cx)
12793 });
12794
12795 _ = editor.update(cx, |editor, window, cx| {
12796 struct Type1;
12797 struct Type2;
12798
12799 let buffer = editor.buffer.read(cx).snapshot(cx);
12800
12801 let anchor_range =
12802 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
12803
12804 editor.highlight_background::<Type1>(
12805 &[
12806 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
12807 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
12808 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
12809 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
12810 ],
12811 |_| Hsla::red(),
12812 cx,
12813 );
12814 editor.highlight_background::<Type2>(
12815 &[
12816 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
12817 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
12818 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
12819 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
12820 ],
12821 |_| Hsla::green(),
12822 cx,
12823 );
12824
12825 let snapshot = editor.snapshot(window, cx);
12826 let mut highlighted_ranges = editor.background_highlights_in_range(
12827 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
12828 &snapshot,
12829 cx.theme().colors(),
12830 );
12831 // Enforce a consistent ordering based on color without relying on the ordering of the
12832 // highlight's `TypeId` which is non-executor.
12833 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
12834 assert_eq!(
12835 highlighted_ranges,
12836 &[
12837 (
12838 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
12839 Hsla::red(),
12840 ),
12841 (
12842 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
12843 Hsla::red(),
12844 ),
12845 (
12846 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
12847 Hsla::green(),
12848 ),
12849 (
12850 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
12851 Hsla::green(),
12852 ),
12853 ]
12854 );
12855 assert_eq!(
12856 editor.background_highlights_in_range(
12857 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
12858 &snapshot,
12859 cx.theme().colors(),
12860 ),
12861 &[(
12862 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
12863 Hsla::red(),
12864 )]
12865 );
12866 });
12867}
12868
12869#[gpui::test]
12870async fn test_following(cx: &mut TestAppContext) {
12871 init_test(cx, |_| {});
12872
12873 let fs = FakeFs::new(cx.executor());
12874 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
12875
12876 let buffer = project.update(cx, |project, cx| {
12877 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
12878 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
12879 });
12880 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
12881 let follower = cx.update(|cx| {
12882 cx.open_window(
12883 WindowOptions {
12884 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
12885 gpui::Point::new(px(0.), px(0.)),
12886 gpui::Point::new(px(10.), px(80.)),
12887 ))),
12888 ..Default::default()
12889 },
12890 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
12891 )
12892 .unwrap()
12893 });
12894
12895 let is_still_following = Rc::new(RefCell::new(true));
12896 let follower_edit_event_count = Rc::new(RefCell::new(0));
12897 let pending_update = Rc::new(RefCell::new(None));
12898 let leader_entity = leader.root(cx).unwrap();
12899 let follower_entity = follower.root(cx).unwrap();
12900 _ = follower.update(cx, {
12901 let update = pending_update.clone();
12902 let is_still_following = is_still_following.clone();
12903 let follower_edit_event_count = follower_edit_event_count.clone();
12904 |_, window, cx| {
12905 cx.subscribe_in(
12906 &leader_entity,
12907 window,
12908 move |_, leader, event, window, cx| {
12909 leader.read(cx).add_event_to_update_proto(
12910 event,
12911 &mut update.borrow_mut(),
12912 window,
12913 cx,
12914 );
12915 },
12916 )
12917 .detach();
12918
12919 cx.subscribe_in(
12920 &follower_entity,
12921 window,
12922 move |_, _, event: &EditorEvent, _window, _cx| {
12923 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
12924 *is_still_following.borrow_mut() = false;
12925 }
12926
12927 if let EditorEvent::BufferEdited = event {
12928 *follower_edit_event_count.borrow_mut() += 1;
12929 }
12930 },
12931 )
12932 .detach();
12933 }
12934 });
12935
12936 // Update the selections only
12937 _ = leader.update(cx, |leader, window, cx| {
12938 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
12939 });
12940 follower
12941 .update(cx, |follower, window, cx| {
12942 follower.apply_update_proto(
12943 &project,
12944 pending_update.borrow_mut().take().unwrap(),
12945 window,
12946 cx,
12947 )
12948 })
12949 .unwrap()
12950 .await
12951 .unwrap();
12952 _ = follower.update(cx, |follower, _, cx| {
12953 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
12954 });
12955 assert!(*is_still_following.borrow());
12956 assert_eq!(*follower_edit_event_count.borrow(), 0);
12957
12958 // Update the scroll position only
12959 _ = leader.update(cx, |leader, window, cx| {
12960 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
12961 });
12962 follower
12963 .update(cx, |follower, window, cx| {
12964 follower.apply_update_proto(
12965 &project,
12966 pending_update.borrow_mut().take().unwrap(),
12967 window,
12968 cx,
12969 )
12970 })
12971 .unwrap()
12972 .await
12973 .unwrap();
12974 assert_eq!(
12975 follower
12976 .update(cx, |follower, _, cx| follower.scroll_position(cx))
12977 .unwrap(),
12978 gpui::Point::new(1.5, 3.5)
12979 );
12980 assert!(*is_still_following.borrow());
12981 assert_eq!(*follower_edit_event_count.borrow(), 0);
12982
12983 // Update the selections and scroll position. The follower's scroll position is updated
12984 // via autoscroll, not via the leader's exact scroll position.
12985 _ = leader.update(cx, |leader, window, cx| {
12986 leader.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
12987 leader.request_autoscroll(Autoscroll::newest(), cx);
12988 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
12989 });
12990 follower
12991 .update(cx, |follower, window, cx| {
12992 follower.apply_update_proto(
12993 &project,
12994 pending_update.borrow_mut().take().unwrap(),
12995 window,
12996 cx,
12997 )
12998 })
12999 .unwrap()
13000 .await
13001 .unwrap();
13002 _ = follower.update(cx, |follower, _, cx| {
13003 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
13004 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
13005 });
13006 assert!(*is_still_following.borrow());
13007
13008 // Creating a pending selection that precedes another selection
13009 _ = leader.update(cx, |leader, window, cx| {
13010 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
13011 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
13012 });
13013 follower
13014 .update(cx, |follower, window, cx| {
13015 follower.apply_update_proto(
13016 &project,
13017 pending_update.borrow_mut().take().unwrap(),
13018 window,
13019 cx,
13020 )
13021 })
13022 .unwrap()
13023 .await
13024 .unwrap();
13025 _ = follower.update(cx, |follower, _, cx| {
13026 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
13027 });
13028 assert!(*is_still_following.borrow());
13029
13030 // Extend the pending selection so that it surrounds another selection
13031 _ = leader.update(cx, |leader, window, cx| {
13032 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
13033 });
13034 follower
13035 .update(cx, |follower, window, cx| {
13036 follower.apply_update_proto(
13037 &project,
13038 pending_update.borrow_mut().take().unwrap(),
13039 window,
13040 cx,
13041 )
13042 })
13043 .unwrap()
13044 .await
13045 .unwrap();
13046 _ = follower.update(cx, |follower, _, cx| {
13047 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
13048 });
13049
13050 // Scrolling locally breaks the follow
13051 _ = follower.update(cx, |follower, window, cx| {
13052 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
13053 follower.set_scroll_anchor(
13054 ScrollAnchor {
13055 anchor: top_anchor,
13056 offset: gpui::Point::new(0.0, 0.5),
13057 },
13058 window,
13059 cx,
13060 );
13061 });
13062 assert!(!(*is_still_following.borrow()));
13063}
13064
13065#[gpui::test]
13066async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
13067 init_test(cx, |_| {});
13068
13069 let fs = FakeFs::new(cx.executor());
13070 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
13071 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13072 let pane = workspace
13073 .update(cx, |workspace, _, _| workspace.active_pane().clone())
13074 .unwrap();
13075
13076 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
13077
13078 let leader = pane.update_in(cx, |_, window, cx| {
13079 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
13080 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
13081 });
13082
13083 // Start following the editor when it has no excerpts.
13084 let mut state_message =
13085 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
13086 let workspace_entity = workspace.root(cx).unwrap();
13087 let follower_1 = cx
13088 .update_window(*workspace.deref(), |_, window, cx| {
13089 Editor::from_state_proto(
13090 workspace_entity,
13091 ViewId {
13092 creator: CollaboratorId::PeerId(PeerId::default()),
13093 id: 0,
13094 },
13095 &mut state_message,
13096 window,
13097 cx,
13098 )
13099 })
13100 .unwrap()
13101 .unwrap()
13102 .await
13103 .unwrap();
13104
13105 let update_message = Rc::new(RefCell::new(None));
13106 follower_1.update_in(cx, {
13107 let update = update_message.clone();
13108 |_, window, cx| {
13109 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
13110 leader.read(cx).add_event_to_update_proto(
13111 event,
13112 &mut update.borrow_mut(),
13113 window,
13114 cx,
13115 );
13116 })
13117 .detach();
13118 }
13119 });
13120
13121 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
13122 (
13123 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
13124 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
13125 )
13126 });
13127
13128 // Insert some excerpts.
13129 leader.update(cx, |leader, cx| {
13130 leader.buffer.update(cx, |multibuffer, cx| {
13131 multibuffer.set_excerpts_for_path(
13132 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
13133 buffer_1.clone(),
13134 vec![
13135 Point::row_range(0..3),
13136 Point::row_range(1..6),
13137 Point::row_range(12..15),
13138 ],
13139 0,
13140 cx,
13141 );
13142 multibuffer.set_excerpts_for_path(
13143 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
13144 buffer_2.clone(),
13145 vec![Point::row_range(0..6), Point::row_range(8..12)],
13146 0,
13147 cx,
13148 );
13149 });
13150 });
13151
13152 // Apply the update of adding the excerpts.
13153 follower_1
13154 .update_in(cx, |follower, window, cx| {
13155 follower.apply_update_proto(
13156 &project,
13157 update_message.borrow().clone().unwrap(),
13158 window,
13159 cx,
13160 )
13161 })
13162 .await
13163 .unwrap();
13164 assert_eq!(
13165 follower_1.update(cx, |editor, cx| editor.text(cx)),
13166 leader.update(cx, |editor, cx| editor.text(cx))
13167 );
13168 update_message.borrow_mut().take();
13169
13170 // Start following separately after it already has excerpts.
13171 let mut state_message =
13172 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
13173 let workspace_entity = workspace.root(cx).unwrap();
13174 let follower_2 = cx
13175 .update_window(*workspace.deref(), |_, window, cx| {
13176 Editor::from_state_proto(
13177 workspace_entity,
13178 ViewId {
13179 creator: CollaboratorId::PeerId(PeerId::default()),
13180 id: 0,
13181 },
13182 &mut state_message,
13183 window,
13184 cx,
13185 )
13186 })
13187 .unwrap()
13188 .unwrap()
13189 .await
13190 .unwrap();
13191 assert_eq!(
13192 follower_2.update(cx, |editor, cx| editor.text(cx)),
13193 leader.update(cx, |editor, cx| editor.text(cx))
13194 );
13195
13196 // Remove some excerpts.
13197 leader.update(cx, |leader, cx| {
13198 leader.buffer.update(cx, |multibuffer, cx| {
13199 let excerpt_ids = multibuffer.excerpt_ids();
13200 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
13201 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
13202 });
13203 });
13204
13205 // Apply the update of removing the excerpts.
13206 follower_1
13207 .update_in(cx, |follower, window, cx| {
13208 follower.apply_update_proto(
13209 &project,
13210 update_message.borrow().clone().unwrap(),
13211 window,
13212 cx,
13213 )
13214 })
13215 .await
13216 .unwrap();
13217 follower_2
13218 .update_in(cx, |follower, window, cx| {
13219 follower.apply_update_proto(
13220 &project,
13221 update_message.borrow().clone().unwrap(),
13222 window,
13223 cx,
13224 )
13225 })
13226 .await
13227 .unwrap();
13228 update_message.borrow_mut().take();
13229 assert_eq!(
13230 follower_1.update(cx, |editor, cx| editor.text(cx)),
13231 leader.update(cx, |editor, cx| editor.text(cx))
13232 );
13233}
13234
13235#[gpui::test]
13236async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
13237 init_test(cx, |_| {});
13238
13239 let mut cx = EditorTestContext::new(cx).await;
13240 let lsp_store =
13241 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
13242
13243 cx.set_state(indoc! {"
13244 ˇfn func(abc def: i32) -> u32 {
13245 }
13246 "});
13247
13248 cx.update(|_, cx| {
13249 lsp_store.update(cx, |lsp_store, cx| {
13250 lsp_store
13251 .update_diagnostics(
13252 LanguageServerId(0),
13253 lsp::PublishDiagnosticsParams {
13254 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
13255 version: None,
13256 diagnostics: vec![
13257 lsp::Diagnostic {
13258 range: lsp::Range::new(
13259 lsp::Position::new(0, 11),
13260 lsp::Position::new(0, 12),
13261 ),
13262 severity: Some(lsp::DiagnosticSeverity::ERROR),
13263 ..Default::default()
13264 },
13265 lsp::Diagnostic {
13266 range: lsp::Range::new(
13267 lsp::Position::new(0, 12),
13268 lsp::Position::new(0, 15),
13269 ),
13270 severity: Some(lsp::DiagnosticSeverity::ERROR),
13271 ..Default::default()
13272 },
13273 lsp::Diagnostic {
13274 range: lsp::Range::new(
13275 lsp::Position::new(0, 25),
13276 lsp::Position::new(0, 28),
13277 ),
13278 severity: Some(lsp::DiagnosticSeverity::ERROR),
13279 ..Default::default()
13280 },
13281 ],
13282 },
13283 &[],
13284 cx,
13285 )
13286 .unwrap()
13287 });
13288 });
13289
13290 executor.run_until_parked();
13291
13292 cx.update_editor(|editor, window, cx| {
13293 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13294 });
13295
13296 cx.assert_editor_state(indoc! {"
13297 fn func(abc def: i32) -> ˇu32 {
13298 }
13299 "});
13300
13301 cx.update_editor(|editor, window, cx| {
13302 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13303 });
13304
13305 cx.assert_editor_state(indoc! {"
13306 fn func(abc ˇdef: i32) -> u32 {
13307 }
13308 "});
13309
13310 cx.update_editor(|editor, window, cx| {
13311 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13312 });
13313
13314 cx.assert_editor_state(indoc! {"
13315 fn func(abcˇ def: i32) -> u32 {
13316 }
13317 "});
13318
13319 cx.update_editor(|editor, window, cx| {
13320 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13321 });
13322
13323 cx.assert_editor_state(indoc! {"
13324 fn func(abc def: i32) -> ˇu32 {
13325 }
13326 "});
13327}
13328
13329#[gpui::test]
13330async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
13331 init_test(cx, |_| {});
13332
13333 let mut cx = EditorTestContext::new(cx).await;
13334
13335 let diff_base = r#"
13336 use some::mod;
13337
13338 const A: u32 = 42;
13339
13340 fn main() {
13341 println!("hello");
13342
13343 println!("world");
13344 }
13345 "#
13346 .unindent();
13347
13348 // Edits are modified, removed, modified, added
13349 cx.set_state(
13350 &r#"
13351 use some::modified;
13352
13353 ˇ
13354 fn main() {
13355 println!("hello there");
13356
13357 println!("around the");
13358 println!("world");
13359 }
13360 "#
13361 .unindent(),
13362 );
13363
13364 cx.set_head_text(&diff_base);
13365 executor.run_until_parked();
13366
13367 cx.update_editor(|editor, window, cx| {
13368 //Wrap around the bottom of the buffer
13369 for _ in 0..3 {
13370 editor.go_to_next_hunk(&GoToHunk, window, cx);
13371 }
13372 });
13373
13374 cx.assert_editor_state(
13375 &r#"
13376 ˇuse some::modified;
13377
13378
13379 fn main() {
13380 println!("hello there");
13381
13382 println!("around the");
13383 println!("world");
13384 }
13385 "#
13386 .unindent(),
13387 );
13388
13389 cx.update_editor(|editor, window, cx| {
13390 //Wrap around the top of the buffer
13391 for _ in 0..2 {
13392 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13393 }
13394 });
13395
13396 cx.assert_editor_state(
13397 &r#"
13398 use some::modified;
13399
13400
13401 fn main() {
13402 ˇ println!("hello there");
13403
13404 println!("around the");
13405 println!("world");
13406 }
13407 "#
13408 .unindent(),
13409 );
13410
13411 cx.update_editor(|editor, window, cx| {
13412 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13413 });
13414
13415 cx.assert_editor_state(
13416 &r#"
13417 use some::modified;
13418
13419 ˇ
13420 fn main() {
13421 println!("hello there");
13422
13423 println!("around the");
13424 println!("world");
13425 }
13426 "#
13427 .unindent(),
13428 );
13429
13430 cx.update_editor(|editor, window, cx| {
13431 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13432 });
13433
13434 cx.assert_editor_state(
13435 &r#"
13436 ˇuse some::modified;
13437
13438
13439 fn main() {
13440 println!("hello there");
13441
13442 println!("around the");
13443 println!("world");
13444 }
13445 "#
13446 .unindent(),
13447 );
13448
13449 cx.update_editor(|editor, window, cx| {
13450 for _ in 0..2 {
13451 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13452 }
13453 });
13454
13455 cx.assert_editor_state(
13456 &r#"
13457 use some::modified;
13458
13459
13460 fn main() {
13461 ˇ println!("hello there");
13462
13463 println!("around the");
13464 println!("world");
13465 }
13466 "#
13467 .unindent(),
13468 );
13469
13470 cx.update_editor(|editor, window, cx| {
13471 editor.fold(&Fold, window, cx);
13472 });
13473
13474 cx.update_editor(|editor, window, cx| {
13475 editor.go_to_next_hunk(&GoToHunk, window, cx);
13476 });
13477
13478 cx.assert_editor_state(
13479 &r#"
13480 ˇuse some::modified;
13481
13482
13483 fn main() {
13484 println!("hello there");
13485
13486 println!("around the");
13487 println!("world");
13488 }
13489 "#
13490 .unindent(),
13491 );
13492}
13493
13494#[test]
13495fn test_split_words() {
13496 fn split(text: &str) -> Vec<&str> {
13497 split_words(text).collect()
13498 }
13499
13500 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
13501 assert_eq!(split("hello_world"), &["hello_", "world"]);
13502 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
13503 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
13504 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
13505 assert_eq!(split("helloworld"), &["helloworld"]);
13506
13507 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
13508}
13509
13510#[gpui::test]
13511async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
13512 init_test(cx, |_| {});
13513
13514 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
13515 let mut assert = |before, after| {
13516 let _state_context = cx.set_state(before);
13517 cx.run_until_parked();
13518 cx.update_editor(|editor, window, cx| {
13519 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
13520 });
13521 cx.run_until_parked();
13522 cx.assert_editor_state(after);
13523 };
13524
13525 // Outside bracket jumps to outside of matching bracket
13526 assert("console.logˇ(var);", "console.log(var)ˇ;");
13527 assert("console.log(var)ˇ;", "console.logˇ(var);");
13528
13529 // Inside bracket jumps to inside of matching bracket
13530 assert("console.log(ˇvar);", "console.log(varˇ);");
13531 assert("console.log(varˇ);", "console.log(ˇvar);");
13532
13533 // When outside a bracket and inside, favor jumping to the inside bracket
13534 assert(
13535 "console.log('foo', [1, 2, 3]ˇ);",
13536 "console.log(ˇ'foo', [1, 2, 3]);",
13537 );
13538 assert(
13539 "console.log(ˇ'foo', [1, 2, 3]);",
13540 "console.log('foo', [1, 2, 3]ˇ);",
13541 );
13542
13543 // Bias forward if two options are equally likely
13544 assert(
13545 "let result = curried_fun()ˇ();",
13546 "let result = curried_fun()()ˇ;",
13547 );
13548
13549 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
13550 assert(
13551 indoc! {"
13552 function test() {
13553 console.log('test')ˇ
13554 }"},
13555 indoc! {"
13556 function test() {
13557 console.logˇ('test')
13558 }"},
13559 );
13560}
13561
13562#[gpui::test]
13563async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
13564 init_test(cx, |_| {});
13565
13566 let fs = FakeFs::new(cx.executor());
13567 fs.insert_tree(
13568 path!("/a"),
13569 json!({
13570 "main.rs": "fn main() { let a = 5; }",
13571 "other.rs": "// Test file",
13572 }),
13573 )
13574 .await;
13575 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13576
13577 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13578 language_registry.add(Arc::new(Language::new(
13579 LanguageConfig {
13580 name: "Rust".into(),
13581 matcher: LanguageMatcher {
13582 path_suffixes: vec!["rs".to_string()],
13583 ..Default::default()
13584 },
13585 brackets: BracketPairConfig {
13586 pairs: vec![BracketPair {
13587 start: "{".to_string(),
13588 end: "}".to_string(),
13589 close: true,
13590 surround: true,
13591 newline: true,
13592 }],
13593 disabled_scopes_by_bracket_ix: Vec::new(),
13594 },
13595 ..Default::default()
13596 },
13597 Some(tree_sitter_rust::LANGUAGE.into()),
13598 )));
13599 let mut fake_servers = language_registry.register_fake_lsp(
13600 "Rust",
13601 FakeLspAdapter {
13602 capabilities: lsp::ServerCapabilities {
13603 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
13604 first_trigger_character: "{".to_string(),
13605 more_trigger_character: None,
13606 }),
13607 ..Default::default()
13608 },
13609 ..Default::default()
13610 },
13611 );
13612
13613 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13614
13615 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13616
13617 let worktree_id = workspace
13618 .update(cx, |workspace, _, cx| {
13619 workspace.project().update(cx, |project, cx| {
13620 project.worktrees(cx).next().unwrap().read(cx).id()
13621 })
13622 })
13623 .unwrap();
13624
13625 let buffer = project
13626 .update(cx, |project, cx| {
13627 project.open_local_buffer(path!("/a/main.rs"), cx)
13628 })
13629 .await
13630 .unwrap();
13631 let editor_handle = workspace
13632 .update(cx, |workspace, window, cx| {
13633 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
13634 })
13635 .unwrap()
13636 .await
13637 .unwrap()
13638 .downcast::<Editor>()
13639 .unwrap();
13640
13641 cx.executor().start_waiting();
13642 let fake_server = fake_servers.next().await.unwrap();
13643
13644 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
13645 |params, _| async move {
13646 assert_eq!(
13647 params.text_document_position.text_document.uri,
13648 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
13649 );
13650 assert_eq!(
13651 params.text_document_position.position,
13652 lsp::Position::new(0, 21),
13653 );
13654
13655 Ok(Some(vec![lsp::TextEdit {
13656 new_text: "]".to_string(),
13657 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13658 }]))
13659 },
13660 );
13661
13662 editor_handle.update_in(cx, |editor, window, cx| {
13663 window.focus(&editor.focus_handle(cx));
13664 editor.change_selections(None, window, cx, |s| {
13665 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
13666 });
13667 editor.handle_input("{", window, cx);
13668 });
13669
13670 cx.executor().run_until_parked();
13671
13672 buffer.update(cx, |buffer, _| {
13673 assert_eq!(
13674 buffer.text(),
13675 "fn main() { let a = {5}; }",
13676 "No extra braces from on type formatting should appear in the buffer"
13677 )
13678 });
13679}
13680
13681#[gpui::test]
13682async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
13683 init_test(cx, |_| {});
13684
13685 let fs = FakeFs::new(cx.executor());
13686 fs.insert_tree(
13687 path!("/a"),
13688 json!({
13689 "main.rs": "fn main() { let a = 5; }",
13690 "other.rs": "// Test file",
13691 }),
13692 )
13693 .await;
13694
13695 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13696
13697 let server_restarts = Arc::new(AtomicUsize::new(0));
13698 let closure_restarts = Arc::clone(&server_restarts);
13699 let language_server_name = "test language server";
13700 let language_name: LanguageName = "Rust".into();
13701
13702 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13703 language_registry.add(Arc::new(Language::new(
13704 LanguageConfig {
13705 name: language_name.clone(),
13706 matcher: LanguageMatcher {
13707 path_suffixes: vec!["rs".to_string()],
13708 ..Default::default()
13709 },
13710 ..Default::default()
13711 },
13712 Some(tree_sitter_rust::LANGUAGE.into()),
13713 )));
13714 let mut fake_servers = language_registry.register_fake_lsp(
13715 "Rust",
13716 FakeLspAdapter {
13717 name: language_server_name,
13718 initialization_options: Some(json!({
13719 "testOptionValue": true
13720 })),
13721 initializer: Some(Box::new(move |fake_server| {
13722 let task_restarts = Arc::clone(&closure_restarts);
13723 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
13724 task_restarts.fetch_add(1, atomic::Ordering::Release);
13725 futures::future::ready(Ok(()))
13726 });
13727 })),
13728 ..Default::default()
13729 },
13730 );
13731
13732 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13733 let _buffer = project
13734 .update(cx, |project, cx| {
13735 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
13736 })
13737 .await
13738 .unwrap();
13739 let _fake_server = fake_servers.next().await.unwrap();
13740 update_test_language_settings(cx, |language_settings| {
13741 language_settings.languages.insert(
13742 language_name.clone(),
13743 LanguageSettingsContent {
13744 tab_size: NonZeroU32::new(8),
13745 ..Default::default()
13746 },
13747 );
13748 });
13749 cx.executor().run_until_parked();
13750 assert_eq!(
13751 server_restarts.load(atomic::Ordering::Acquire),
13752 0,
13753 "Should not restart LSP server on an unrelated change"
13754 );
13755
13756 update_test_project_settings(cx, |project_settings| {
13757 project_settings.lsp.insert(
13758 "Some other server name".into(),
13759 LspSettings {
13760 binary: None,
13761 settings: None,
13762 initialization_options: Some(json!({
13763 "some other init value": false
13764 })),
13765 enable_lsp_tasks: false,
13766 },
13767 );
13768 });
13769 cx.executor().run_until_parked();
13770 assert_eq!(
13771 server_restarts.load(atomic::Ordering::Acquire),
13772 0,
13773 "Should not restart LSP server on an unrelated LSP settings change"
13774 );
13775
13776 update_test_project_settings(cx, |project_settings| {
13777 project_settings.lsp.insert(
13778 language_server_name.into(),
13779 LspSettings {
13780 binary: None,
13781 settings: None,
13782 initialization_options: Some(json!({
13783 "anotherInitValue": false
13784 })),
13785 enable_lsp_tasks: false,
13786 },
13787 );
13788 });
13789 cx.executor().run_until_parked();
13790 assert_eq!(
13791 server_restarts.load(atomic::Ordering::Acquire),
13792 1,
13793 "Should restart LSP server on a related LSP settings change"
13794 );
13795
13796 update_test_project_settings(cx, |project_settings| {
13797 project_settings.lsp.insert(
13798 language_server_name.into(),
13799 LspSettings {
13800 binary: None,
13801 settings: None,
13802 initialization_options: Some(json!({
13803 "anotherInitValue": false
13804 })),
13805 enable_lsp_tasks: false,
13806 },
13807 );
13808 });
13809 cx.executor().run_until_parked();
13810 assert_eq!(
13811 server_restarts.load(atomic::Ordering::Acquire),
13812 1,
13813 "Should not restart LSP server on a related LSP settings change that is the same"
13814 );
13815
13816 update_test_project_settings(cx, |project_settings| {
13817 project_settings.lsp.insert(
13818 language_server_name.into(),
13819 LspSettings {
13820 binary: None,
13821 settings: None,
13822 initialization_options: None,
13823 enable_lsp_tasks: false,
13824 },
13825 );
13826 });
13827 cx.executor().run_until_parked();
13828 assert_eq!(
13829 server_restarts.load(atomic::Ordering::Acquire),
13830 2,
13831 "Should restart LSP server on another related LSP settings change"
13832 );
13833}
13834
13835#[gpui::test]
13836async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
13837 init_test(cx, |_| {});
13838
13839 let mut cx = EditorLspTestContext::new_rust(
13840 lsp::ServerCapabilities {
13841 completion_provider: Some(lsp::CompletionOptions {
13842 trigger_characters: Some(vec![".".to_string()]),
13843 resolve_provider: Some(true),
13844 ..Default::default()
13845 }),
13846 ..Default::default()
13847 },
13848 cx,
13849 )
13850 .await;
13851
13852 cx.set_state("fn main() { let a = 2ˇ; }");
13853 cx.simulate_keystroke(".");
13854 let completion_item = lsp::CompletionItem {
13855 label: "some".into(),
13856 kind: Some(lsp::CompletionItemKind::SNIPPET),
13857 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
13858 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
13859 kind: lsp::MarkupKind::Markdown,
13860 value: "```rust\nSome(2)\n```".to_string(),
13861 })),
13862 deprecated: Some(false),
13863 sort_text: Some("fffffff2".to_string()),
13864 filter_text: Some("some".to_string()),
13865 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
13866 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13867 range: lsp::Range {
13868 start: lsp::Position {
13869 line: 0,
13870 character: 22,
13871 },
13872 end: lsp::Position {
13873 line: 0,
13874 character: 22,
13875 },
13876 },
13877 new_text: "Some(2)".to_string(),
13878 })),
13879 additional_text_edits: Some(vec![lsp::TextEdit {
13880 range: lsp::Range {
13881 start: lsp::Position {
13882 line: 0,
13883 character: 20,
13884 },
13885 end: lsp::Position {
13886 line: 0,
13887 character: 22,
13888 },
13889 },
13890 new_text: "".to_string(),
13891 }]),
13892 ..Default::default()
13893 };
13894
13895 let closure_completion_item = completion_item.clone();
13896 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13897 let task_completion_item = closure_completion_item.clone();
13898 async move {
13899 Ok(Some(lsp::CompletionResponse::Array(vec![
13900 task_completion_item,
13901 ])))
13902 }
13903 });
13904
13905 request.next().await;
13906
13907 cx.condition(|editor, _| editor.context_menu_visible())
13908 .await;
13909 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13910 editor
13911 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13912 .unwrap()
13913 });
13914 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
13915
13916 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
13917 let task_completion_item = completion_item.clone();
13918 async move { Ok(task_completion_item) }
13919 })
13920 .next()
13921 .await
13922 .unwrap();
13923 apply_additional_edits.await.unwrap();
13924 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
13925}
13926
13927#[gpui::test]
13928async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
13929 init_test(cx, |_| {});
13930
13931 let mut cx = EditorLspTestContext::new_rust(
13932 lsp::ServerCapabilities {
13933 completion_provider: Some(lsp::CompletionOptions {
13934 trigger_characters: Some(vec![".".to_string()]),
13935 resolve_provider: Some(true),
13936 ..Default::default()
13937 }),
13938 ..Default::default()
13939 },
13940 cx,
13941 )
13942 .await;
13943
13944 cx.set_state("fn main() { let a = 2ˇ; }");
13945 cx.simulate_keystroke(".");
13946
13947 let item1 = lsp::CompletionItem {
13948 label: "method id()".to_string(),
13949 filter_text: Some("id".to_string()),
13950 detail: None,
13951 documentation: None,
13952 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13953 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13954 new_text: ".id".to_string(),
13955 })),
13956 ..lsp::CompletionItem::default()
13957 };
13958
13959 let item2 = lsp::CompletionItem {
13960 label: "other".to_string(),
13961 filter_text: Some("other".to_string()),
13962 detail: None,
13963 documentation: None,
13964 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13965 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13966 new_text: ".other".to_string(),
13967 })),
13968 ..lsp::CompletionItem::default()
13969 };
13970
13971 let item1 = item1.clone();
13972 cx.set_request_handler::<lsp::request::Completion, _, _>({
13973 let item1 = item1.clone();
13974 move |_, _, _| {
13975 let item1 = item1.clone();
13976 let item2 = item2.clone();
13977 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
13978 }
13979 })
13980 .next()
13981 .await;
13982
13983 cx.condition(|editor, _| editor.context_menu_visible())
13984 .await;
13985 cx.update_editor(|editor, _, _| {
13986 let context_menu = editor.context_menu.borrow_mut();
13987 let context_menu = context_menu
13988 .as_ref()
13989 .expect("Should have the context menu deployed");
13990 match context_menu {
13991 CodeContextMenu::Completions(completions_menu) => {
13992 let completions = completions_menu.completions.borrow_mut();
13993 assert_eq!(
13994 completions
13995 .iter()
13996 .map(|completion| &completion.label.text)
13997 .collect::<Vec<_>>(),
13998 vec!["method id()", "other"]
13999 )
14000 }
14001 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
14002 }
14003 });
14004
14005 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
14006 let item1 = item1.clone();
14007 move |_, item_to_resolve, _| {
14008 let item1 = item1.clone();
14009 async move {
14010 if item1 == item_to_resolve {
14011 Ok(lsp::CompletionItem {
14012 label: "method id()".to_string(),
14013 filter_text: Some("id".to_string()),
14014 detail: Some("Now resolved!".to_string()),
14015 documentation: Some(lsp::Documentation::String("Docs".to_string())),
14016 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14017 range: lsp::Range::new(
14018 lsp::Position::new(0, 22),
14019 lsp::Position::new(0, 22),
14020 ),
14021 new_text: ".id".to_string(),
14022 })),
14023 ..lsp::CompletionItem::default()
14024 })
14025 } else {
14026 Ok(item_to_resolve)
14027 }
14028 }
14029 }
14030 })
14031 .next()
14032 .await
14033 .unwrap();
14034 cx.run_until_parked();
14035
14036 cx.update_editor(|editor, window, cx| {
14037 editor.context_menu_next(&Default::default(), window, cx);
14038 });
14039
14040 cx.update_editor(|editor, _, _| {
14041 let context_menu = editor.context_menu.borrow_mut();
14042 let context_menu = context_menu
14043 .as_ref()
14044 .expect("Should have the context menu deployed");
14045 match context_menu {
14046 CodeContextMenu::Completions(completions_menu) => {
14047 let completions = completions_menu.completions.borrow_mut();
14048 assert_eq!(
14049 completions
14050 .iter()
14051 .map(|completion| &completion.label.text)
14052 .collect::<Vec<_>>(),
14053 vec!["method id() Now resolved!", "other"],
14054 "Should update first completion label, but not second as the filter text did not match."
14055 );
14056 }
14057 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
14058 }
14059 });
14060}
14061
14062#[gpui::test]
14063async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
14064 init_test(cx, |_| {});
14065 let mut cx = EditorLspTestContext::new_rust(
14066 lsp::ServerCapabilities {
14067 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
14068 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
14069 completion_provider: Some(lsp::CompletionOptions {
14070 resolve_provider: Some(true),
14071 ..Default::default()
14072 }),
14073 ..Default::default()
14074 },
14075 cx,
14076 )
14077 .await;
14078 cx.set_state(indoc! {"
14079 struct TestStruct {
14080 field: i32
14081 }
14082
14083 fn mainˇ() {
14084 let unused_var = 42;
14085 let test_struct = TestStruct { field: 42 };
14086 }
14087 "});
14088 let symbol_range = cx.lsp_range(indoc! {"
14089 struct TestStruct {
14090 field: i32
14091 }
14092
14093 «fn main»() {
14094 let unused_var = 42;
14095 let test_struct = TestStruct { field: 42 };
14096 }
14097 "});
14098 let mut hover_requests =
14099 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
14100 Ok(Some(lsp::Hover {
14101 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
14102 kind: lsp::MarkupKind::Markdown,
14103 value: "Function documentation".to_string(),
14104 }),
14105 range: Some(symbol_range),
14106 }))
14107 });
14108
14109 // Case 1: Test that code action menu hide hover popover
14110 cx.dispatch_action(Hover);
14111 hover_requests.next().await;
14112 cx.condition(|editor, _| editor.hover_state.visible()).await;
14113 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
14114 move |_, _, _| async move {
14115 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
14116 lsp::CodeAction {
14117 title: "Remove unused variable".to_string(),
14118 kind: Some(CodeActionKind::QUICKFIX),
14119 edit: Some(lsp::WorkspaceEdit {
14120 changes: Some(
14121 [(
14122 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
14123 vec![lsp::TextEdit {
14124 range: lsp::Range::new(
14125 lsp::Position::new(5, 4),
14126 lsp::Position::new(5, 27),
14127 ),
14128 new_text: "".to_string(),
14129 }],
14130 )]
14131 .into_iter()
14132 .collect(),
14133 ),
14134 ..Default::default()
14135 }),
14136 ..Default::default()
14137 },
14138 )]))
14139 },
14140 );
14141 cx.update_editor(|editor, window, cx| {
14142 editor.toggle_code_actions(
14143 &ToggleCodeActions {
14144 deployed_from: None,
14145 quick_launch: false,
14146 },
14147 window,
14148 cx,
14149 );
14150 });
14151 code_action_requests.next().await;
14152 cx.run_until_parked();
14153 cx.condition(|editor, _| editor.context_menu_visible())
14154 .await;
14155 cx.update_editor(|editor, _, _| {
14156 assert!(
14157 !editor.hover_state.visible(),
14158 "Hover popover should be hidden when code action menu is shown"
14159 );
14160 // Hide code actions
14161 editor.context_menu.take();
14162 });
14163
14164 // Case 2: Test that code completions hide hover popover
14165 cx.dispatch_action(Hover);
14166 hover_requests.next().await;
14167 cx.condition(|editor, _| editor.hover_state.visible()).await;
14168 let counter = Arc::new(AtomicUsize::new(0));
14169 let mut completion_requests =
14170 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14171 let counter = counter.clone();
14172 async move {
14173 counter.fetch_add(1, atomic::Ordering::Release);
14174 Ok(Some(lsp::CompletionResponse::Array(vec![
14175 lsp::CompletionItem {
14176 label: "main".into(),
14177 kind: Some(lsp::CompletionItemKind::FUNCTION),
14178 detail: Some("() -> ()".to_string()),
14179 ..Default::default()
14180 },
14181 lsp::CompletionItem {
14182 label: "TestStruct".into(),
14183 kind: Some(lsp::CompletionItemKind::STRUCT),
14184 detail: Some("struct TestStruct".to_string()),
14185 ..Default::default()
14186 },
14187 ])))
14188 }
14189 });
14190 cx.update_editor(|editor, window, cx| {
14191 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14192 });
14193 completion_requests.next().await;
14194 cx.condition(|editor, _| editor.context_menu_visible())
14195 .await;
14196 cx.update_editor(|editor, _, _| {
14197 assert!(
14198 !editor.hover_state.visible(),
14199 "Hover popover should be hidden when completion menu is shown"
14200 );
14201 });
14202}
14203
14204#[gpui::test]
14205async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
14206 init_test(cx, |_| {});
14207
14208 let mut cx = EditorLspTestContext::new_rust(
14209 lsp::ServerCapabilities {
14210 completion_provider: Some(lsp::CompletionOptions {
14211 trigger_characters: Some(vec![".".to_string()]),
14212 resolve_provider: Some(true),
14213 ..Default::default()
14214 }),
14215 ..Default::default()
14216 },
14217 cx,
14218 )
14219 .await;
14220
14221 cx.set_state("fn main() { let a = 2ˇ; }");
14222 cx.simulate_keystroke(".");
14223
14224 let unresolved_item_1 = lsp::CompletionItem {
14225 label: "id".to_string(),
14226 filter_text: Some("id".to_string()),
14227 detail: None,
14228 documentation: None,
14229 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14230 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14231 new_text: ".id".to_string(),
14232 })),
14233 ..lsp::CompletionItem::default()
14234 };
14235 let resolved_item_1 = lsp::CompletionItem {
14236 additional_text_edits: Some(vec![lsp::TextEdit {
14237 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
14238 new_text: "!!".to_string(),
14239 }]),
14240 ..unresolved_item_1.clone()
14241 };
14242 let unresolved_item_2 = lsp::CompletionItem {
14243 label: "other".to_string(),
14244 filter_text: Some("other".to_string()),
14245 detail: None,
14246 documentation: None,
14247 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14248 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14249 new_text: ".other".to_string(),
14250 })),
14251 ..lsp::CompletionItem::default()
14252 };
14253 let resolved_item_2 = lsp::CompletionItem {
14254 additional_text_edits: Some(vec![lsp::TextEdit {
14255 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
14256 new_text: "??".to_string(),
14257 }]),
14258 ..unresolved_item_2.clone()
14259 };
14260
14261 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
14262 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
14263 cx.lsp
14264 .server
14265 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
14266 let unresolved_item_1 = unresolved_item_1.clone();
14267 let resolved_item_1 = resolved_item_1.clone();
14268 let unresolved_item_2 = unresolved_item_2.clone();
14269 let resolved_item_2 = resolved_item_2.clone();
14270 let resolve_requests_1 = resolve_requests_1.clone();
14271 let resolve_requests_2 = resolve_requests_2.clone();
14272 move |unresolved_request, _| {
14273 let unresolved_item_1 = unresolved_item_1.clone();
14274 let resolved_item_1 = resolved_item_1.clone();
14275 let unresolved_item_2 = unresolved_item_2.clone();
14276 let resolved_item_2 = resolved_item_2.clone();
14277 let resolve_requests_1 = resolve_requests_1.clone();
14278 let resolve_requests_2 = resolve_requests_2.clone();
14279 async move {
14280 if unresolved_request == unresolved_item_1 {
14281 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
14282 Ok(resolved_item_1.clone())
14283 } else if unresolved_request == unresolved_item_2 {
14284 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
14285 Ok(resolved_item_2.clone())
14286 } else {
14287 panic!("Unexpected completion item {unresolved_request:?}")
14288 }
14289 }
14290 }
14291 })
14292 .detach();
14293
14294 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14295 let unresolved_item_1 = unresolved_item_1.clone();
14296 let unresolved_item_2 = unresolved_item_2.clone();
14297 async move {
14298 Ok(Some(lsp::CompletionResponse::Array(vec![
14299 unresolved_item_1,
14300 unresolved_item_2,
14301 ])))
14302 }
14303 })
14304 .next()
14305 .await;
14306
14307 cx.condition(|editor, _| editor.context_menu_visible())
14308 .await;
14309 cx.update_editor(|editor, _, _| {
14310 let context_menu = editor.context_menu.borrow_mut();
14311 let context_menu = context_menu
14312 .as_ref()
14313 .expect("Should have the context menu deployed");
14314 match context_menu {
14315 CodeContextMenu::Completions(completions_menu) => {
14316 let completions = completions_menu.completions.borrow_mut();
14317 assert_eq!(
14318 completions
14319 .iter()
14320 .map(|completion| &completion.label.text)
14321 .collect::<Vec<_>>(),
14322 vec!["id", "other"]
14323 )
14324 }
14325 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
14326 }
14327 });
14328 cx.run_until_parked();
14329
14330 cx.update_editor(|editor, window, cx| {
14331 editor.context_menu_next(&ContextMenuNext, window, cx);
14332 });
14333 cx.run_until_parked();
14334 cx.update_editor(|editor, window, cx| {
14335 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
14336 });
14337 cx.run_until_parked();
14338 cx.update_editor(|editor, window, cx| {
14339 editor.context_menu_next(&ContextMenuNext, window, cx);
14340 });
14341 cx.run_until_parked();
14342 cx.update_editor(|editor, window, cx| {
14343 editor
14344 .compose_completion(&ComposeCompletion::default(), window, cx)
14345 .expect("No task returned")
14346 })
14347 .await
14348 .expect("Completion failed");
14349 cx.run_until_parked();
14350
14351 cx.update_editor(|editor, _, cx| {
14352 assert_eq!(
14353 resolve_requests_1.load(atomic::Ordering::Acquire),
14354 1,
14355 "Should always resolve once despite multiple selections"
14356 );
14357 assert_eq!(
14358 resolve_requests_2.load(atomic::Ordering::Acquire),
14359 1,
14360 "Should always resolve once after multiple selections and applying the completion"
14361 );
14362 assert_eq!(
14363 editor.text(cx),
14364 "fn main() { let a = ??.other; }",
14365 "Should use resolved data when applying the completion"
14366 );
14367 });
14368}
14369
14370#[gpui::test]
14371async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
14372 init_test(cx, |_| {});
14373
14374 let item_0 = lsp::CompletionItem {
14375 label: "abs".into(),
14376 insert_text: Some("abs".into()),
14377 data: Some(json!({ "very": "special"})),
14378 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
14379 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
14380 lsp::InsertReplaceEdit {
14381 new_text: "abs".to_string(),
14382 insert: lsp::Range::default(),
14383 replace: lsp::Range::default(),
14384 },
14385 )),
14386 ..lsp::CompletionItem::default()
14387 };
14388 let items = iter::once(item_0.clone())
14389 .chain((11..51).map(|i| lsp::CompletionItem {
14390 label: format!("item_{}", i),
14391 insert_text: Some(format!("item_{}", i)),
14392 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
14393 ..lsp::CompletionItem::default()
14394 }))
14395 .collect::<Vec<_>>();
14396
14397 let default_commit_characters = vec!["?".to_string()];
14398 let default_data = json!({ "default": "data"});
14399 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
14400 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
14401 let default_edit_range = lsp::Range {
14402 start: lsp::Position {
14403 line: 0,
14404 character: 5,
14405 },
14406 end: lsp::Position {
14407 line: 0,
14408 character: 5,
14409 },
14410 };
14411
14412 let mut cx = EditorLspTestContext::new_rust(
14413 lsp::ServerCapabilities {
14414 completion_provider: Some(lsp::CompletionOptions {
14415 trigger_characters: Some(vec![".".to_string()]),
14416 resolve_provider: Some(true),
14417 ..Default::default()
14418 }),
14419 ..Default::default()
14420 },
14421 cx,
14422 )
14423 .await;
14424
14425 cx.set_state("fn main() { let a = 2ˇ; }");
14426 cx.simulate_keystroke(".");
14427
14428 let completion_data = default_data.clone();
14429 let completion_characters = default_commit_characters.clone();
14430 let completion_items = items.clone();
14431 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14432 let default_data = completion_data.clone();
14433 let default_commit_characters = completion_characters.clone();
14434 let items = completion_items.clone();
14435 async move {
14436 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
14437 items,
14438 item_defaults: Some(lsp::CompletionListItemDefaults {
14439 data: Some(default_data.clone()),
14440 commit_characters: Some(default_commit_characters.clone()),
14441 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
14442 default_edit_range,
14443 )),
14444 insert_text_format: Some(default_insert_text_format),
14445 insert_text_mode: Some(default_insert_text_mode),
14446 }),
14447 ..lsp::CompletionList::default()
14448 })))
14449 }
14450 })
14451 .next()
14452 .await;
14453
14454 let resolved_items = Arc::new(Mutex::new(Vec::new()));
14455 cx.lsp
14456 .server
14457 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
14458 let closure_resolved_items = resolved_items.clone();
14459 move |item_to_resolve, _| {
14460 let closure_resolved_items = closure_resolved_items.clone();
14461 async move {
14462 closure_resolved_items.lock().push(item_to_resolve.clone());
14463 Ok(item_to_resolve)
14464 }
14465 }
14466 })
14467 .detach();
14468
14469 cx.condition(|editor, _| editor.context_menu_visible())
14470 .await;
14471 cx.run_until_parked();
14472 cx.update_editor(|editor, _, _| {
14473 let menu = editor.context_menu.borrow_mut();
14474 match menu.as_ref().expect("should have the completions menu") {
14475 CodeContextMenu::Completions(completions_menu) => {
14476 assert_eq!(
14477 completions_menu
14478 .entries
14479 .borrow()
14480 .iter()
14481 .map(|mat| mat.string.clone())
14482 .collect::<Vec<String>>(),
14483 items
14484 .iter()
14485 .map(|completion| completion.label.clone())
14486 .collect::<Vec<String>>()
14487 );
14488 }
14489 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
14490 }
14491 });
14492 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
14493 // with 4 from the end.
14494 assert_eq!(
14495 *resolved_items.lock(),
14496 [&items[0..16], &items[items.len() - 4..items.len()]]
14497 .concat()
14498 .iter()
14499 .cloned()
14500 .map(|mut item| {
14501 if item.data.is_none() {
14502 item.data = Some(default_data.clone());
14503 }
14504 item
14505 })
14506 .collect::<Vec<lsp::CompletionItem>>(),
14507 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
14508 );
14509 resolved_items.lock().clear();
14510
14511 cx.update_editor(|editor, window, cx| {
14512 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
14513 });
14514 cx.run_until_parked();
14515 // Completions that have already been resolved are skipped.
14516 assert_eq!(
14517 *resolved_items.lock(),
14518 items[items.len() - 16..items.len() - 4]
14519 .iter()
14520 .cloned()
14521 .map(|mut item| {
14522 if item.data.is_none() {
14523 item.data = Some(default_data.clone());
14524 }
14525 item
14526 })
14527 .collect::<Vec<lsp::CompletionItem>>()
14528 );
14529 resolved_items.lock().clear();
14530}
14531
14532#[gpui::test]
14533async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
14534 init_test(cx, |_| {});
14535
14536 let mut cx = EditorLspTestContext::new(
14537 Language::new(
14538 LanguageConfig {
14539 matcher: LanguageMatcher {
14540 path_suffixes: vec!["jsx".into()],
14541 ..Default::default()
14542 },
14543 overrides: [(
14544 "element".into(),
14545 LanguageConfigOverride {
14546 completion_query_characters: Override::Set(['-'].into_iter().collect()),
14547 ..Default::default()
14548 },
14549 )]
14550 .into_iter()
14551 .collect(),
14552 ..Default::default()
14553 },
14554 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
14555 )
14556 .with_override_query("(jsx_self_closing_element) @element")
14557 .unwrap(),
14558 lsp::ServerCapabilities {
14559 completion_provider: Some(lsp::CompletionOptions {
14560 trigger_characters: Some(vec![":".to_string()]),
14561 ..Default::default()
14562 }),
14563 ..Default::default()
14564 },
14565 cx,
14566 )
14567 .await;
14568
14569 cx.lsp
14570 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14571 Ok(Some(lsp::CompletionResponse::Array(vec![
14572 lsp::CompletionItem {
14573 label: "bg-blue".into(),
14574 ..Default::default()
14575 },
14576 lsp::CompletionItem {
14577 label: "bg-red".into(),
14578 ..Default::default()
14579 },
14580 lsp::CompletionItem {
14581 label: "bg-yellow".into(),
14582 ..Default::default()
14583 },
14584 ])))
14585 });
14586
14587 cx.set_state(r#"<p class="bgˇ" />"#);
14588
14589 // Trigger completion when typing a dash, because the dash is an extra
14590 // word character in the 'element' scope, which contains the cursor.
14591 cx.simulate_keystroke("-");
14592 cx.executor().run_until_parked();
14593 cx.update_editor(|editor, _, _| {
14594 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14595 {
14596 assert_eq!(
14597 completion_menu_entries(&menu),
14598 &["bg-red", "bg-blue", "bg-yellow"]
14599 );
14600 } else {
14601 panic!("expected completion menu to be open");
14602 }
14603 });
14604
14605 cx.simulate_keystroke("l");
14606 cx.executor().run_until_parked();
14607 cx.update_editor(|editor, _, _| {
14608 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14609 {
14610 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
14611 } else {
14612 panic!("expected completion menu to be open");
14613 }
14614 });
14615
14616 // When filtering completions, consider the character after the '-' to
14617 // be the start of a subword.
14618 cx.set_state(r#"<p class="yelˇ" />"#);
14619 cx.simulate_keystroke("l");
14620 cx.executor().run_until_parked();
14621 cx.update_editor(|editor, _, _| {
14622 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14623 {
14624 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
14625 } else {
14626 panic!("expected completion menu to be open");
14627 }
14628 });
14629}
14630
14631fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
14632 let entries = menu.entries.borrow();
14633 entries.iter().map(|mat| mat.string.clone()).collect()
14634}
14635
14636#[gpui::test]
14637async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
14638 init_test(cx, |settings| {
14639 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
14640 FormatterList(vec![Formatter::Prettier].into()),
14641 ))
14642 });
14643
14644 let fs = FakeFs::new(cx.executor());
14645 fs.insert_file(path!("/file.ts"), Default::default()).await;
14646
14647 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
14648 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14649
14650 language_registry.add(Arc::new(Language::new(
14651 LanguageConfig {
14652 name: "TypeScript".into(),
14653 matcher: LanguageMatcher {
14654 path_suffixes: vec!["ts".to_string()],
14655 ..Default::default()
14656 },
14657 ..Default::default()
14658 },
14659 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
14660 )));
14661 update_test_language_settings(cx, |settings| {
14662 settings.defaults.prettier = Some(PrettierSettings {
14663 allowed: true,
14664 ..PrettierSettings::default()
14665 });
14666 });
14667
14668 let test_plugin = "test_plugin";
14669 let _ = language_registry.register_fake_lsp(
14670 "TypeScript",
14671 FakeLspAdapter {
14672 prettier_plugins: vec![test_plugin],
14673 ..Default::default()
14674 },
14675 );
14676
14677 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
14678 let buffer = project
14679 .update(cx, |project, cx| {
14680 project.open_local_buffer(path!("/file.ts"), cx)
14681 })
14682 .await
14683 .unwrap();
14684
14685 let buffer_text = "one\ntwo\nthree\n";
14686 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
14687 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
14688 editor.update_in(cx, |editor, window, cx| {
14689 editor.set_text(buffer_text, window, cx)
14690 });
14691
14692 editor
14693 .update_in(cx, |editor, window, cx| {
14694 editor.perform_format(
14695 project.clone(),
14696 FormatTrigger::Manual,
14697 FormatTarget::Buffers,
14698 window,
14699 cx,
14700 )
14701 })
14702 .unwrap()
14703 .await;
14704 assert_eq!(
14705 editor.update(cx, |editor, cx| editor.text(cx)),
14706 buffer_text.to_string() + prettier_format_suffix,
14707 "Test prettier formatting was not applied to the original buffer text",
14708 );
14709
14710 update_test_language_settings(cx, |settings| {
14711 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
14712 });
14713 let format = editor.update_in(cx, |editor, window, cx| {
14714 editor.perform_format(
14715 project.clone(),
14716 FormatTrigger::Manual,
14717 FormatTarget::Buffers,
14718 window,
14719 cx,
14720 )
14721 });
14722 format.await.unwrap();
14723 assert_eq!(
14724 editor.update(cx, |editor, cx| editor.text(cx)),
14725 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
14726 "Autoformatting (via test prettier) was not applied to the original buffer text",
14727 );
14728}
14729
14730#[gpui::test]
14731async fn test_addition_reverts(cx: &mut TestAppContext) {
14732 init_test(cx, |_| {});
14733 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14734 let base_text = indoc! {r#"
14735 struct Row;
14736 struct Row1;
14737 struct Row2;
14738
14739 struct Row4;
14740 struct Row5;
14741 struct Row6;
14742
14743 struct Row8;
14744 struct Row9;
14745 struct Row10;"#};
14746
14747 // When addition hunks are not adjacent to carets, no hunk revert is performed
14748 assert_hunk_revert(
14749 indoc! {r#"struct Row;
14750 struct Row1;
14751 struct Row1.1;
14752 struct Row1.2;
14753 struct Row2;ˇ
14754
14755 struct Row4;
14756 struct Row5;
14757 struct Row6;
14758
14759 struct Row8;
14760 ˇstruct Row9;
14761 struct Row9.1;
14762 struct Row9.2;
14763 struct Row9.3;
14764 struct Row10;"#},
14765 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
14766 indoc! {r#"struct Row;
14767 struct Row1;
14768 struct Row1.1;
14769 struct Row1.2;
14770 struct Row2;ˇ
14771
14772 struct Row4;
14773 struct Row5;
14774 struct Row6;
14775
14776 struct Row8;
14777 ˇstruct Row9;
14778 struct Row9.1;
14779 struct Row9.2;
14780 struct Row9.3;
14781 struct Row10;"#},
14782 base_text,
14783 &mut cx,
14784 );
14785 // Same for selections
14786 assert_hunk_revert(
14787 indoc! {r#"struct Row;
14788 struct Row1;
14789 struct Row2;
14790 struct Row2.1;
14791 struct Row2.2;
14792 «ˇ
14793 struct Row4;
14794 struct» Row5;
14795 «struct Row6;
14796 ˇ»
14797 struct Row9.1;
14798 struct Row9.2;
14799 struct Row9.3;
14800 struct Row8;
14801 struct Row9;
14802 struct Row10;"#},
14803 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
14804 indoc! {r#"struct Row;
14805 struct Row1;
14806 struct Row2;
14807 struct Row2.1;
14808 struct Row2.2;
14809 «ˇ
14810 struct Row4;
14811 struct» Row5;
14812 «struct Row6;
14813 ˇ»
14814 struct Row9.1;
14815 struct Row9.2;
14816 struct Row9.3;
14817 struct Row8;
14818 struct Row9;
14819 struct Row10;"#},
14820 base_text,
14821 &mut cx,
14822 );
14823
14824 // When carets and selections intersect the addition hunks, those are reverted.
14825 // Adjacent carets got merged.
14826 assert_hunk_revert(
14827 indoc! {r#"struct Row;
14828 ˇ// something on the top
14829 struct Row1;
14830 struct Row2;
14831 struct Roˇw3.1;
14832 struct Row2.2;
14833 struct Row2.3;ˇ
14834
14835 struct Row4;
14836 struct ˇRow5.1;
14837 struct Row5.2;
14838 struct «Rowˇ»5.3;
14839 struct Row5;
14840 struct Row6;
14841 ˇ
14842 struct Row9.1;
14843 struct «Rowˇ»9.2;
14844 struct «ˇRow»9.3;
14845 struct Row8;
14846 struct Row9;
14847 «ˇ// something on bottom»
14848 struct Row10;"#},
14849 vec![
14850 DiffHunkStatusKind::Added,
14851 DiffHunkStatusKind::Added,
14852 DiffHunkStatusKind::Added,
14853 DiffHunkStatusKind::Added,
14854 DiffHunkStatusKind::Added,
14855 ],
14856 indoc! {r#"struct Row;
14857 ˇstruct Row1;
14858 struct Row2;
14859 ˇ
14860 struct Row4;
14861 ˇstruct Row5;
14862 struct Row6;
14863 ˇ
14864 ˇstruct Row8;
14865 struct Row9;
14866 ˇstruct Row10;"#},
14867 base_text,
14868 &mut cx,
14869 );
14870}
14871
14872#[gpui::test]
14873async fn test_modification_reverts(cx: &mut TestAppContext) {
14874 init_test(cx, |_| {});
14875 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14876 let base_text = indoc! {r#"
14877 struct Row;
14878 struct Row1;
14879 struct Row2;
14880
14881 struct Row4;
14882 struct Row5;
14883 struct Row6;
14884
14885 struct Row8;
14886 struct Row9;
14887 struct Row10;"#};
14888
14889 // Modification hunks behave the same as the addition ones.
14890 assert_hunk_revert(
14891 indoc! {r#"struct Row;
14892 struct Row1;
14893 struct Row33;
14894 ˇ
14895 struct Row4;
14896 struct Row5;
14897 struct Row6;
14898 ˇ
14899 struct Row99;
14900 struct Row9;
14901 struct Row10;"#},
14902 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
14903 indoc! {r#"struct Row;
14904 struct Row1;
14905 struct Row33;
14906 ˇ
14907 struct Row4;
14908 struct Row5;
14909 struct Row6;
14910 ˇ
14911 struct Row99;
14912 struct Row9;
14913 struct Row10;"#},
14914 base_text,
14915 &mut cx,
14916 );
14917 assert_hunk_revert(
14918 indoc! {r#"struct Row;
14919 struct Row1;
14920 struct Row33;
14921 «ˇ
14922 struct Row4;
14923 struct» Row5;
14924 «struct Row6;
14925 ˇ»
14926 struct Row99;
14927 struct Row9;
14928 struct Row10;"#},
14929 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
14930 indoc! {r#"struct Row;
14931 struct Row1;
14932 struct Row33;
14933 «ˇ
14934 struct Row4;
14935 struct» Row5;
14936 «struct Row6;
14937 ˇ»
14938 struct Row99;
14939 struct Row9;
14940 struct Row10;"#},
14941 base_text,
14942 &mut cx,
14943 );
14944
14945 assert_hunk_revert(
14946 indoc! {r#"ˇstruct Row1.1;
14947 struct Row1;
14948 «ˇstr»uct Row22;
14949
14950 struct ˇRow44;
14951 struct Row5;
14952 struct «Rˇ»ow66;ˇ
14953
14954 «struˇ»ct Row88;
14955 struct Row9;
14956 struct Row1011;ˇ"#},
14957 vec![
14958 DiffHunkStatusKind::Modified,
14959 DiffHunkStatusKind::Modified,
14960 DiffHunkStatusKind::Modified,
14961 DiffHunkStatusKind::Modified,
14962 DiffHunkStatusKind::Modified,
14963 DiffHunkStatusKind::Modified,
14964 ],
14965 indoc! {r#"struct Row;
14966 ˇstruct Row1;
14967 struct Row2;
14968 ˇ
14969 struct Row4;
14970 ˇstruct Row5;
14971 struct Row6;
14972 ˇ
14973 struct Row8;
14974 ˇstruct Row9;
14975 struct Row10;ˇ"#},
14976 base_text,
14977 &mut cx,
14978 );
14979}
14980
14981#[gpui::test]
14982async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
14983 init_test(cx, |_| {});
14984 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14985 let base_text = indoc! {r#"
14986 one
14987
14988 two
14989 three
14990 "#};
14991
14992 cx.set_head_text(base_text);
14993 cx.set_state("\nˇ\n");
14994 cx.executor().run_until_parked();
14995 cx.update_editor(|editor, _window, cx| {
14996 editor.expand_selected_diff_hunks(cx);
14997 });
14998 cx.executor().run_until_parked();
14999 cx.update_editor(|editor, window, cx| {
15000 editor.backspace(&Default::default(), window, cx);
15001 });
15002 cx.run_until_parked();
15003 cx.assert_state_with_diff(
15004 indoc! {r#"
15005
15006 - two
15007 - threeˇ
15008 +
15009 "#}
15010 .to_string(),
15011 );
15012}
15013
15014#[gpui::test]
15015async fn test_deletion_reverts(cx: &mut TestAppContext) {
15016 init_test(cx, |_| {});
15017 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15018 let base_text = indoc! {r#"struct Row;
15019struct Row1;
15020struct Row2;
15021
15022struct Row4;
15023struct Row5;
15024struct Row6;
15025
15026struct Row8;
15027struct Row9;
15028struct Row10;"#};
15029
15030 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
15031 assert_hunk_revert(
15032 indoc! {r#"struct Row;
15033 struct Row2;
15034
15035 ˇstruct Row4;
15036 struct Row5;
15037 struct Row6;
15038 ˇ
15039 struct Row8;
15040 struct Row10;"#},
15041 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
15042 indoc! {r#"struct Row;
15043 struct Row2;
15044
15045 ˇstruct Row4;
15046 struct Row5;
15047 struct Row6;
15048 ˇ
15049 struct Row8;
15050 struct Row10;"#},
15051 base_text,
15052 &mut cx,
15053 );
15054 assert_hunk_revert(
15055 indoc! {r#"struct Row;
15056 struct Row2;
15057
15058 «ˇstruct Row4;
15059 struct» Row5;
15060 «struct Row6;
15061 ˇ»
15062 struct Row8;
15063 struct Row10;"#},
15064 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
15065 indoc! {r#"struct Row;
15066 struct Row2;
15067
15068 «ˇstruct Row4;
15069 struct» Row5;
15070 «struct Row6;
15071 ˇ»
15072 struct Row8;
15073 struct Row10;"#},
15074 base_text,
15075 &mut cx,
15076 );
15077
15078 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
15079 assert_hunk_revert(
15080 indoc! {r#"struct Row;
15081 ˇstruct Row2;
15082
15083 struct Row4;
15084 struct Row5;
15085 struct Row6;
15086
15087 struct Row8;ˇ
15088 struct Row10;"#},
15089 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
15090 indoc! {r#"struct Row;
15091 struct Row1;
15092 ˇstruct Row2;
15093
15094 struct Row4;
15095 struct Row5;
15096 struct Row6;
15097
15098 struct Row8;ˇ
15099 struct Row9;
15100 struct Row10;"#},
15101 base_text,
15102 &mut cx,
15103 );
15104 assert_hunk_revert(
15105 indoc! {r#"struct Row;
15106 struct Row2«ˇ;
15107 struct Row4;
15108 struct» Row5;
15109 «struct Row6;
15110
15111 struct Row8;ˇ»
15112 struct Row10;"#},
15113 vec![
15114 DiffHunkStatusKind::Deleted,
15115 DiffHunkStatusKind::Deleted,
15116 DiffHunkStatusKind::Deleted,
15117 ],
15118 indoc! {r#"struct Row;
15119 struct Row1;
15120 struct Row2«ˇ;
15121
15122 struct Row4;
15123 struct» Row5;
15124 «struct Row6;
15125
15126 struct Row8;ˇ»
15127 struct Row9;
15128 struct Row10;"#},
15129 base_text,
15130 &mut cx,
15131 );
15132}
15133
15134#[gpui::test]
15135async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
15136 init_test(cx, |_| {});
15137
15138 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
15139 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
15140 let base_text_3 =
15141 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
15142
15143 let text_1 = edit_first_char_of_every_line(base_text_1);
15144 let text_2 = edit_first_char_of_every_line(base_text_2);
15145 let text_3 = edit_first_char_of_every_line(base_text_3);
15146
15147 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
15148 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
15149 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
15150
15151 let multibuffer = cx.new(|cx| {
15152 let mut multibuffer = MultiBuffer::new(ReadWrite);
15153 multibuffer.push_excerpts(
15154 buffer_1.clone(),
15155 [
15156 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15157 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15158 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15159 ],
15160 cx,
15161 );
15162 multibuffer.push_excerpts(
15163 buffer_2.clone(),
15164 [
15165 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15166 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15167 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15168 ],
15169 cx,
15170 );
15171 multibuffer.push_excerpts(
15172 buffer_3.clone(),
15173 [
15174 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15175 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15176 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15177 ],
15178 cx,
15179 );
15180 multibuffer
15181 });
15182
15183 let fs = FakeFs::new(cx.executor());
15184 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
15185 let (editor, cx) = cx
15186 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
15187 editor.update_in(cx, |editor, _window, cx| {
15188 for (buffer, diff_base) in [
15189 (buffer_1.clone(), base_text_1),
15190 (buffer_2.clone(), base_text_2),
15191 (buffer_3.clone(), base_text_3),
15192 ] {
15193 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
15194 editor
15195 .buffer
15196 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
15197 }
15198 });
15199 cx.executor().run_until_parked();
15200
15201 editor.update_in(cx, |editor, window, cx| {
15202 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}");
15203 editor.select_all(&SelectAll, window, cx);
15204 editor.git_restore(&Default::default(), window, cx);
15205 });
15206 cx.executor().run_until_parked();
15207
15208 // When all ranges are selected, all buffer hunks are reverted.
15209 editor.update(cx, |editor, cx| {
15210 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");
15211 });
15212 buffer_1.update(cx, |buffer, _| {
15213 assert_eq!(buffer.text(), base_text_1);
15214 });
15215 buffer_2.update(cx, |buffer, _| {
15216 assert_eq!(buffer.text(), base_text_2);
15217 });
15218 buffer_3.update(cx, |buffer, _| {
15219 assert_eq!(buffer.text(), base_text_3);
15220 });
15221
15222 editor.update_in(cx, |editor, window, cx| {
15223 editor.undo(&Default::default(), window, cx);
15224 });
15225
15226 editor.update_in(cx, |editor, window, cx| {
15227 editor.change_selections(None, window, cx, |s| {
15228 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
15229 });
15230 editor.git_restore(&Default::default(), window, cx);
15231 });
15232
15233 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
15234 // but not affect buffer_2 and its related excerpts.
15235 editor.update(cx, |editor, cx| {
15236 assert_eq!(
15237 editor.text(cx),
15238 "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}"
15239 );
15240 });
15241 buffer_1.update(cx, |buffer, _| {
15242 assert_eq!(buffer.text(), base_text_1);
15243 });
15244 buffer_2.update(cx, |buffer, _| {
15245 assert_eq!(
15246 buffer.text(),
15247 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
15248 );
15249 });
15250 buffer_3.update(cx, |buffer, _| {
15251 assert_eq!(
15252 buffer.text(),
15253 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
15254 );
15255 });
15256
15257 fn edit_first_char_of_every_line(text: &str) -> String {
15258 text.split('\n')
15259 .map(|line| format!("X{}", &line[1..]))
15260 .collect::<Vec<_>>()
15261 .join("\n")
15262 }
15263}
15264
15265#[gpui::test]
15266async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) {
15267 init_test(cx, |_| {});
15268
15269 let cols = 4;
15270 let rows = 10;
15271 let sample_text_1 = sample_text(rows, cols, 'a');
15272 assert_eq!(
15273 sample_text_1,
15274 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
15275 );
15276 let sample_text_2 = sample_text(rows, cols, 'l');
15277 assert_eq!(
15278 sample_text_2,
15279 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
15280 );
15281 let sample_text_3 = sample_text(rows, cols, 'v');
15282 assert_eq!(
15283 sample_text_3,
15284 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
15285 );
15286
15287 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
15288 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
15289 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
15290
15291 let multi_buffer = cx.new(|cx| {
15292 let mut multibuffer = MultiBuffer::new(ReadWrite);
15293 multibuffer.push_excerpts(
15294 buffer_1.clone(),
15295 [
15296 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15297 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15298 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15299 ],
15300 cx,
15301 );
15302 multibuffer.push_excerpts(
15303 buffer_2.clone(),
15304 [
15305 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15306 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15307 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15308 ],
15309 cx,
15310 );
15311 multibuffer.push_excerpts(
15312 buffer_3.clone(),
15313 [
15314 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15315 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15316 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15317 ],
15318 cx,
15319 );
15320 multibuffer
15321 });
15322
15323 let fs = FakeFs::new(cx.executor());
15324 fs.insert_tree(
15325 "/a",
15326 json!({
15327 "main.rs": sample_text_1,
15328 "other.rs": sample_text_2,
15329 "lib.rs": sample_text_3,
15330 }),
15331 )
15332 .await;
15333 let project = Project::test(fs, ["/a".as_ref()], cx).await;
15334 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15335 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
15336 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
15337 Editor::new(
15338 EditorMode::full(),
15339 multi_buffer,
15340 Some(project.clone()),
15341 window,
15342 cx,
15343 )
15344 });
15345 let multibuffer_item_id = workspace
15346 .update(cx, |workspace, window, cx| {
15347 assert!(
15348 workspace.active_item(cx).is_none(),
15349 "active item should be None before the first item is added"
15350 );
15351 workspace.add_item_to_active_pane(
15352 Box::new(multi_buffer_editor.clone()),
15353 None,
15354 true,
15355 window,
15356 cx,
15357 );
15358 let active_item = workspace
15359 .active_item(cx)
15360 .expect("should have an active item after adding the multi buffer");
15361 assert!(
15362 !active_item.is_singleton(cx),
15363 "A multi buffer was expected to active after adding"
15364 );
15365 active_item.item_id()
15366 })
15367 .unwrap();
15368 cx.executor().run_until_parked();
15369
15370 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15371 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
15372 s.select_ranges(Some(1..2))
15373 });
15374 editor.open_excerpts(&OpenExcerpts, window, cx);
15375 });
15376 cx.executor().run_until_parked();
15377 let first_item_id = workspace
15378 .update(cx, |workspace, window, cx| {
15379 let active_item = workspace
15380 .active_item(cx)
15381 .expect("should have an active item after navigating into the 1st buffer");
15382 let first_item_id = active_item.item_id();
15383 assert_ne!(
15384 first_item_id, multibuffer_item_id,
15385 "Should navigate into the 1st buffer and activate it"
15386 );
15387 assert!(
15388 active_item.is_singleton(cx),
15389 "New active item should be a singleton buffer"
15390 );
15391 assert_eq!(
15392 active_item
15393 .act_as::<Editor>(cx)
15394 .expect("should have navigated into an editor for the 1st buffer")
15395 .read(cx)
15396 .text(cx),
15397 sample_text_1
15398 );
15399
15400 workspace
15401 .go_back(workspace.active_pane().downgrade(), window, cx)
15402 .detach_and_log_err(cx);
15403
15404 first_item_id
15405 })
15406 .unwrap();
15407 cx.executor().run_until_parked();
15408 workspace
15409 .update(cx, |workspace, _, cx| {
15410 let active_item = workspace
15411 .active_item(cx)
15412 .expect("should have an active item after navigating back");
15413 assert_eq!(
15414 active_item.item_id(),
15415 multibuffer_item_id,
15416 "Should navigate back to the multi buffer"
15417 );
15418 assert!(!active_item.is_singleton(cx));
15419 })
15420 .unwrap();
15421
15422 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15423 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
15424 s.select_ranges(Some(39..40))
15425 });
15426 editor.open_excerpts(&OpenExcerpts, window, cx);
15427 });
15428 cx.executor().run_until_parked();
15429 let second_item_id = workspace
15430 .update(cx, |workspace, window, cx| {
15431 let active_item = workspace
15432 .active_item(cx)
15433 .expect("should have an active item after navigating into the 2nd buffer");
15434 let second_item_id = active_item.item_id();
15435 assert_ne!(
15436 second_item_id, multibuffer_item_id,
15437 "Should navigate away from the multibuffer"
15438 );
15439 assert_ne!(
15440 second_item_id, first_item_id,
15441 "Should navigate into the 2nd buffer and activate it"
15442 );
15443 assert!(
15444 active_item.is_singleton(cx),
15445 "New active item should be a singleton buffer"
15446 );
15447 assert_eq!(
15448 active_item
15449 .act_as::<Editor>(cx)
15450 .expect("should have navigated into an editor")
15451 .read(cx)
15452 .text(cx),
15453 sample_text_2
15454 );
15455
15456 workspace
15457 .go_back(workspace.active_pane().downgrade(), window, cx)
15458 .detach_and_log_err(cx);
15459
15460 second_item_id
15461 })
15462 .unwrap();
15463 cx.executor().run_until_parked();
15464 workspace
15465 .update(cx, |workspace, _, cx| {
15466 let active_item = workspace
15467 .active_item(cx)
15468 .expect("should have an active item after navigating back from the 2nd buffer");
15469 assert_eq!(
15470 active_item.item_id(),
15471 multibuffer_item_id,
15472 "Should navigate back from the 2nd buffer to the multi buffer"
15473 );
15474 assert!(!active_item.is_singleton(cx));
15475 })
15476 .unwrap();
15477
15478 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15479 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
15480 s.select_ranges(Some(70..70))
15481 });
15482 editor.open_excerpts(&OpenExcerpts, window, cx);
15483 });
15484 cx.executor().run_until_parked();
15485 workspace
15486 .update(cx, |workspace, window, cx| {
15487 let active_item = workspace
15488 .active_item(cx)
15489 .expect("should have an active item after navigating into the 3rd buffer");
15490 let third_item_id = active_item.item_id();
15491 assert_ne!(
15492 third_item_id, multibuffer_item_id,
15493 "Should navigate into the 3rd buffer and activate it"
15494 );
15495 assert_ne!(third_item_id, first_item_id);
15496 assert_ne!(third_item_id, second_item_id);
15497 assert!(
15498 active_item.is_singleton(cx),
15499 "New active item should be a singleton buffer"
15500 );
15501 assert_eq!(
15502 active_item
15503 .act_as::<Editor>(cx)
15504 .expect("should have navigated into an editor")
15505 .read(cx)
15506 .text(cx),
15507 sample_text_3
15508 );
15509
15510 workspace
15511 .go_back(workspace.active_pane().downgrade(), window, cx)
15512 .detach_and_log_err(cx);
15513 })
15514 .unwrap();
15515 cx.executor().run_until_parked();
15516 workspace
15517 .update(cx, |workspace, _, cx| {
15518 let active_item = workspace
15519 .active_item(cx)
15520 .expect("should have an active item after navigating back from the 3rd buffer");
15521 assert_eq!(
15522 active_item.item_id(),
15523 multibuffer_item_id,
15524 "Should navigate back from the 3rd buffer to the multi buffer"
15525 );
15526 assert!(!active_item.is_singleton(cx));
15527 })
15528 .unwrap();
15529}
15530
15531#[gpui::test]
15532async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
15533 init_test(cx, |_| {});
15534
15535 let mut cx = EditorTestContext::new(cx).await;
15536
15537 let diff_base = r#"
15538 use some::mod;
15539
15540 const A: u32 = 42;
15541
15542 fn main() {
15543 println!("hello");
15544
15545 println!("world");
15546 }
15547 "#
15548 .unindent();
15549
15550 cx.set_state(
15551 &r#"
15552 use some::modified;
15553
15554 ˇ
15555 fn main() {
15556 println!("hello there");
15557
15558 println!("around the");
15559 println!("world");
15560 }
15561 "#
15562 .unindent(),
15563 );
15564
15565 cx.set_head_text(&diff_base);
15566 executor.run_until_parked();
15567
15568 cx.update_editor(|editor, window, cx| {
15569 editor.go_to_next_hunk(&GoToHunk, window, cx);
15570 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15571 });
15572 executor.run_until_parked();
15573 cx.assert_state_with_diff(
15574 r#"
15575 use some::modified;
15576
15577
15578 fn main() {
15579 - println!("hello");
15580 + ˇ println!("hello there");
15581
15582 println!("around the");
15583 println!("world");
15584 }
15585 "#
15586 .unindent(),
15587 );
15588
15589 cx.update_editor(|editor, window, cx| {
15590 for _ in 0..2 {
15591 editor.go_to_next_hunk(&GoToHunk, window, cx);
15592 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15593 }
15594 });
15595 executor.run_until_parked();
15596 cx.assert_state_with_diff(
15597 r#"
15598 - use some::mod;
15599 + ˇuse some::modified;
15600
15601
15602 fn main() {
15603 - println!("hello");
15604 + println!("hello there");
15605
15606 + println!("around the");
15607 println!("world");
15608 }
15609 "#
15610 .unindent(),
15611 );
15612
15613 cx.update_editor(|editor, window, cx| {
15614 editor.go_to_next_hunk(&GoToHunk, window, cx);
15615 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15616 });
15617 executor.run_until_parked();
15618 cx.assert_state_with_diff(
15619 r#"
15620 - use some::mod;
15621 + use some::modified;
15622
15623 - const A: u32 = 42;
15624 ˇ
15625 fn main() {
15626 - println!("hello");
15627 + println!("hello there");
15628
15629 + println!("around the");
15630 println!("world");
15631 }
15632 "#
15633 .unindent(),
15634 );
15635
15636 cx.update_editor(|editor, window, cx| {
15637 editor.cancel(&Cancel, window, cx);
15638 });
15639
15640 cx.assert_state_with_diff(
15641 r#"
15642 use some::modified;
15643
15644 ˇ
15645 fn main() {
15646 println!("hello there");
15647
15648 println!("around the");
15649 println!("world");
15650 }
15651 "#
15652 .unindent(),
15653 );
15654}
15655
15656#[gpui::test]
15657async fn test_diff_base_change_with_expanded_diff_hunks(
15658 executor: BackgroundExecutor,
15659 cx: &mut TestAppContext,
15660) {
15661 init_test(cx, |_| {});
15662
15663 let mut cx = EditorTestContext::new(cx).await;
15664
15665 let diff_base = r#"
15666 use some::mod1;
15667 use some::mod2;
15668
15669 const A: u32 = 42;
15670 const B: u32 = 42;
15671 const C: u32 = 42;
15672
15673 fn main() {
15674 println!("hello");
15675
15676 println!("world");
15677 }
15678 "#
15679 .unindent();
15680
15681 cx.set_state(
15682 &r#"
15683 use some::mod2;
15684
15685 const A: u32 = 42;
15686 const C: u32 = 42;
15687
15688 fn main(ˇ) {
15689 //println!("hello");
15690
15691 println!("world");
15692 //
15693 //
15694 }
15695 "#
15696 .unindent(),
15697 );
15698
15699 cx.set_head_text(&diff_base);
15700 executor.run_until_parked();
15701
15702 cx.update_editor(|editor, window, cx| {
15703 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15704 });
15705 executor.run_until_parked();
15706 cx.assert_state_with_diff(
15707 r#"
15708 - use some::mod1;
15709 use some::mod2;
15710
15711 const A: u32 = 42;
15712 - const B: u32 = 42;
15713 const C: u32 = 42;
15714
15715 fn main(ˇ) {
15716 - println!("hello");
15717 + //println!("hello");
15718
15719 println!("world");
15720 + //
15721 + //
15722 }
15723 "#
15724 .unindent(),
15725 );
15726
15727 cx.set_head_text("new diff base!");
15728 executor.run_until_parked();
15729 cx.assert_state_with_diff(
15730 r#"
15731 - new diff base!
15732 + use some::mod2;
15733 +
15734 + const A: u32 = 42;
15735 + const C: u32 = 42;
15736 +
15737 + fn main(ˇ) {
15738 + //println!("hello");
15739 +
15740 + println!("world");
15741 + //
15742 + //
15743 + }
15744 "#
15745 .unindent(),
15746 );
15747}
15748
15749#[gpui::test]
15750async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
15751 init_test(cx, |_| {});
15752
15753 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
15754 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
15755 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
15756 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
15757 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
15758 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
15759
15760 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
15761 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
15762 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
15763
15764 let multi_buffer = cx.new(|cx| {
15765 let mut multibuffer = MultiBuffer::new(ReadWrite);
15766 multibuffer.push_excerpts(
15767 buffer_1.clone(),
15768 [
15769 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15770 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15771 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
15772 ],
15773 cx,
15774 );
15775 multibuffer.push_excerpts(
15776 buffer_2.clone(),
15777 [
15778 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15779 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15780 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
15781 ],
15782 cx,
15783 );
15784 multibuffer.push_excerpts(
15785 buffer_3.clone(),
15786 [
15787 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15788 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15789 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
15790 ],
15791 cx,
15792 );
15793 multibuffer
15794 });
15795
15796 let editor =
15797 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
15798 editor
15799 .update(cx, |editor, _window, cx| {
15800 for (buffer, diff_base) in [
15801 (buffer_1.clone(), file_1_old),
15802 (buffer_2.clone(), file_2_old),
15803 (buffer_3.clone(), file_3_old),
15804 ] {
15805 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
15806 editor
15807 .buffer
15808 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
15809 }
15810 })
15811 .unwrap();
15812
15813 let mut cx = EditorTestContext::for_editor(editor, cx).await;
15814 cx.run_until_parked();
15815
15816 cx.assert_editor_state(
15817 &"
15818 ˇaaa
15819 ccc
15820 ddd
15821
15822 ggg
15823 hhh
15824
15825
15826 lll
15827 mmm
15828 NNN
15829
15830 qqq
15831 rrr
15832
15833 uuu
15834 111
15835 222
15836 333
15837
15838 666
15839 777
15840
15841 000
15842 !!!"
15843 .unindent(),
15844 );
15845
15846 cx.update_editor(|editor, window, cx| {
15847 editor.select_all(&SelectAll, window, cx);
15848 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15849 });
15850 cx.executor().run_until_parked();
15851
15852 cx.assert_state_with_diff(
15853 "
15854 «aaa
15855 - bbb
15856 ccc
15857 ddd
15858
15859 ggg
15860 hhh
15861
15862
15863 lll
15864 mmm
15865 - nnn
15866 + NNN
15867
15868 qqq
15869 rrr
15870
15871 uuu
15872 111
15873 222
15874 333
15875
15876 + 666
15877 777
15878
15879 000
15880 !!!ˇ»"
15881 .unindent(),
15882 );
15883}
15884
15885#[gpui::test]
15886async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
15887 init_test(cx, |_| {});
15888
15889 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
15890 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
15891
15892 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
15893 let multi_buffer = cx.new(|cx| {
15894 let mut multibuffer = MultiBuffer::new(ReadWrite);
15895 multibuffer.push_excerpts(
15896 buffer.clone(),
15897 [
15898 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
15899 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
15900 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
15901 ],
15902 cx,
15903 );
15904 multibuffer
15905 });
15906
15907 let editor =
15908 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
15909 editor
15910 .update(cx, |editor, _window, cx| {
15911 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
15912 editor
15913 .buffer
15914 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
15915 })
15916 .unwrap();
15917
15918 let mut cx = EditorTestContext::for_editor(editor, cx).await;
15919 cx.run_until_parked();
15920
15921 cx.update_editor(|editor, window, cx| {
15922 editor.expand_all_diff_hunks(&Default::default(), window, cx)
15923 });
15924 cx.executor().run_until_parked();
15925
15926 // When the start of a hunk coincides with the start of its excerpt,
15927 // the hunk is expanded. When the start of a a hunk is earlier than
15928 // the start of its excerpt, the hunk is not expanded.
15929 cx.assert_state_with_diff(
15930 "
15931 ˇaaa
15932 - bbb
15933 + BBB
15934
15935 - ddd
15936 - eee
15937 + DDD
15938 + EEE
15939 fff
15940
15941 iii
15942 "
15943 .unindent(),
15944 );
15945}
15946
15947#[gpui::test]
15948async fn test_edits_around_expanded_insertion_hunks(
15949 executor: BackgroundExecutor,
15950 cx: &mut TestAppContext,
15951) {
15952 init_test(cx, |_| {});
15953
15954 let mut cx = EditorTestContext::new(cx).await;
15955
15956 let diff_base = r#"
15957 use some::mod1;
15958 use some::mod2;
15959
15960 const A: u32 = 42;
15961
15962 fn main() {
15963 println!("hello");
15964
15965 println!("world");
15966 }
15967 "#
15968 .unindent();
15969 executor.run_until_parked();
15970 cx.set_state(
15971 &r#"
15972 use some::mod1;
15973 use some::mod2;
15974
15975 const A: u32 = 42;
15976 const B: u32 = 42;
15977 const C: u32 = 42;
15978 ˇ
15979
15980 fn main() {
15981 println!("hello");
15982
15983 println!("world");
15984 }
15985 "#
15986 .unindent(),
15987 );
15988
15989 cx.set_head_text(&diff_base);
15990 executor.run_until_parked();
15991
15992 cx.update_editor(|editor, window, cx| {
15993 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15994 });
15995 executor.run_until_parked();
15996
15997 cx.assert_state_with_diff(
15998 r#"
15999 use some::mod1;
16000 use some::mod2;
16001
16002 const A: u32 = 42;
16003 + const B: u32 = 42;
16004 + const C: u32 = 42;
16005 + ˇ
16006
16007 fn main() {
16008 println!("hello");
16009
16010 println!("world");
16011 }
16012 "#
16013 .unindent(),
16014 );
16015
16016 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
16017 executor.run_until_parked();
16018
16019 cx.assert_state_with_diff(
16020 r#"
16021 use some::mod1;
16022 use some::mod2;
16023
16024 const A: u32 = 42;
16025 + const B: u32 = 42;
16026 + const C: u32 = 42;
16027 + const D: u32 = 42;
16028 + ˇ
16029
16030 fn main() {
16031 println!("hello");
16032
16033 println!("world");
16034 }
16035 "#
16036 .unindent(),
16037 );
16038
16039 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
16040 executor.run_until_parked();
16041
16042 cx.assert_state_with_diff(
16043 r#"
16044 use some::mod1;
16045 use some::mod2;
16046
16047 const A: u32 = 42;
16048 + const B: u32 = 42;
16049 + const C: u32 = 42;
16050 + const D: u32 = 42;
16051 + const E: u32 = 42;
16052 + ˇ
16053
16054 fn main() {
16055 println!("hello");
16056
16057 println!("world");
16058 }
16059 "#
16060 .unindent(),
16061 );
16062
16063 cx.update_editor(|editor, window, cx| {
16064 editor.delete_line(&DeleteLine, window, cx);
16065 });
16066 executor.run_until_parked();
16067
16068 cx.assert_state_with_diff(
16069 r#"
16070 use some::mod1;
16071 use some::mod2;
16072
16073 const A: u32 = 42;
16074 + const B: u32 = 42;
16075 + const C: u32 = 42;
16076 + const D: u32 = 42;
16077 + const E: u32 = 42;
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.move_up(&MoveUp, window, cx);
16090 editor.delete_line(&DeleteLine, window, cx);
16091 editor.move_up(&MoveUp, window, cx);
16092 editor.delete_line(&DeleteLine, window, cx);
16093 editor.move_up(&MoveUp, window, cx);
16094 editor.delete_line(&DeleteLine, window, cx);
16095 });
16096 executor.run_until_parked();
16097 cx.assert_state_with_diff(
16098 r#"
16099 use some::mod1;
16100 use some::mod2;
16101
16102 const A: u32 = 42;
16103 + const B: u32 = 42;
16104 ˇ
16105 fn main() {
16106 println!("hello");
16107
16108 println!("world");
16109 }
16110 "#
16111 .unindent(),
16112 );
16113
16114 cx.update_editor(|editor, window, cx| {
16115 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
16116 editor.delete_line(&DeleteLine, window, cx);
16117 });
16118 executor.run_until_parked();
16119 cx.assert_state_with_diff(
16120 r#"
16121 ˇ
16122 fn main() {
16123 println!("hello");
16124
16125 println!("world");
16126 }
16127 "#
16128 .unindent(),
16129 );
16130}
16131
16132#[gpui::test]
16133async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
16134 init_test(cx, |_| {});
16135
16136 let mut cx = EditorTestContext::new(cx).await;
16137 cx.set_head_text(indoc! { "
16138 one
16139 two
16140 three
16141 four
16142 five
16143 "
16144 });
16145 cx.set_state(indoc! { "
16146 one
16147 ˇthree
16148 five
16149 "});
16150 cx.run_until_parked();
16151 cx.update_editor(|editor, window, cx| {
16152 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
16153 });
16154 cx.assert_state_with_diff(
16155 indoc! { "
16156 one
16157 - two
16158 ˇthree
16159 - four
16160 five
16161 "}
16162 .to_string(),
16163 );
16164 cx.update_editor(|editor, window, cx| {
16165 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
16166 });
16167
16168 cx.assert_state_with_diff(
16169 indoc! { "
16170 one
16171 ˇthree
16172 five
16173 "}
16174 .to_string(),
16175 );
16176
16177 cx.set_state(indoc! { "
16178 one
16179 ˇTWO
16180 three
16181 four
16182 five
16183 "});
16184 cx.run_until_parked();
16185 cx.update_editor(|editor, window, cx| {
16186 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
16187 });
16188
16189 cx.assert_state_with_diff(
16190 indoc! { "
16191 one
16192 - two
16193 + ˇTWO
16194 three
16195 four
16196 five
16197 "}
16198 .to_string(),
16199 );
16200 cx.update_editor(|editor, window, cx| {
16201 editor.move_up(&Default::default(), window, cx);
16202 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
16203 });
16204 cx.assert_state_with_diff(
16205 indoc! { "
16206 one
16207 ˇTWO
16208 three
16209 four
16210 five
16211 "}
16212 .to_string(),
16213 );
16214}
16215
16216#[gpui::test]
16217async fn test_edits_around_expanded_deletion_hunks(
16218 executor: BackgroundExecutor,
16219 cx: &mut TestAppContext,
16220) {
16221 init_test(cx, |_| {});
16222
16223 let mut cx = EditorTestContext::new(cx).await;
16224
16225 let diff_base = r#"
16226 use some::mod1;
16227 use some::mod2;
16228
16229 const A: u32 = 42;
16230 const B: u32 = 42;
16231 const C: u32 = 42;
16232
16233
16234 fn main() {
16235 println!("hello");
16236
16237 println!("world");
16238 }
16239 "#
16240 .unindent();
16241 executor.run_until_parked();
16242 cx.set_state(
16243 &r#"
16244 use some::mod1;
16245 use some::mod2;
16246
16247 ˇconst B: u32 = 42;
16248 const C: u32 = 42;
16249
16250
16251 fn main() {
16252 println!("hello");
16253
16254 println!("world");
16255 }
16256 "#
16257 .unindent(),
16258 );
16259
16260 cx.set_head_text(&diff_base);
16261 executor.run_until_parked();
16262
16263 cx.update_editor(|editor, window, cx| {
16264 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16265 });
16266 executor.run_until_parked();
16267
16268 cx.assert_state_with_diff(
16269 r#"
16270 use some::mod1;
16271 use some::mod2;
16272
16273 - const A: u32 = 42;
16274 ˇconst B: u32 = 42;
16275 const C: u32 = 42;
16276
16277
16278 fn main() {
16279 println!("hello");
16280
16281 println!("world");
16282 }
16283 "#
16284 .unindent(),
16285 );
16286
16287 cx.update_editor(|editor, window, cx| {
16288 editor.delete_line(&DeleteLine, window, cx);
16289 });
16290 executor.run_until_parked();
16291 cx.assert_state_with_diff(
16292 r#"
16293 use some::mod1;
16294 use some::mod2;
16295
16296 - const A: u32 = 42;
16297 - const B: u32 = 42;
16298 ˇconst C: u32 = 42;
16299
16300
16301 fn main() {
16302 println!("hello");
16303
16304 println!("world");
16305 }
16306 "#
16307 .unindent(),
16308 );
16309
16310 cx.update_editor(|editor, window, cx| {
16311 editor.delete_line(&DeleteLine, window, cx);
16312 });
16313 executor.run_until_parked();
16314 cx.assert_state_with_diff(
16315 r#"
16316 use some::mod1;
16317 use some::mod2;
16318
16319 - const A: u32 = 42;
16320 - const B: u32 = 42;
16321 - const C: u32 = 42;
16322 ˇ
16323
16324 fn main() {
16325 println!("hello");
16326
16327 println!("world");
16328 }
16329 "#
16330 .unindent(),
16331 );
16332
16333 cx.update_editor(|editor, window, cx| {
16334 editor.handle_input("replacement", window, cx);
16335 });
16336 executor.run_until_parked();
16337 cx.assert_state_with_diff(
16338 r#"
16339 use some::mod1;
16340 use some::mod2;
16341
16342 - const A: u32 = 42;
16343 - const B: u32 = 42;
16344 - const C: u32 = 42;
16345 -
16346 + replacementˇ
16347
16348 fn main() {
16349 println!("hello");
16350
16351 println!("world");
16352 }
16353 "#
16354 .unindent(),
16355 );
16356}
16357
16358#[gpui::test]
16359async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16360 init_test(cx, |_| {});
16361
16362 let mut cx = EditorTestContext::new(cx).await;
16363
16364 let base_text = r#"
16365 one
16366 two
16367 three
16368 four
16369 five
16370 "#
16371 .unindent();
16372 executor.run_until_parked();
16373 cx.set_state(
16374 &r#"
16375 one
16376 two
16377 fˇour
16378 five
16379 "#
16380 .unindent(),
16381 );
16382
16383 cx.set_head_text(&base_text);
16384 executor.run_until_parked();
16385
16386 cx.update_editor(|editor, window, cx| {
16387 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16388 });
16389 executor.run_until_parked();
16390
16391 cx.assert_state_with_diff(
16392 r#"
16393 one
16394 two
16395 - three
16396 fˇour
16397 five
16398 "#
16399 .unindent(),
16400 );
16401
16402 cx.update_editor(|editor, window, cx| {
16403 editor.backspace(&Backspace, window, cx);
16404 editor.backspace(&Backspace, window, cx);
16405 });
16406 executor.run_until_parked();
16407 cx.assert_state_with_diff(
16408 r#"
16409 one
16410 two
16411 - threeˇ
16412 - four
16413 + our
16414 five
16415 "#
16416 .unindent(),
16417 );
16418}
16419
16420#[gpui::test]
16421async fn test_edit_after_expanded_modification_hunk(
16422 executor: BackgroundExecutor,
16423 cx: &mut TestAppContext,
16424) {
16425 init_test(cx, |_| {});
16426
16427 let mut cx = EditorTestContext::new(cx).await;
16428
16429 let diff_base = r#"
16430 use some::mod1;
16431 use some::mod2;
16432
16433 const A: u32 = 42;
16434 const B: u32 = 42;
16435 const C: u32 = 42;
16436 const D: u32 = 42;
16437
16438
16439 fn main() {
16440 println!("hello");
16441
16442 println!("world");
16443 }"#
16444 .unindent();
16445
16446 cx.set_state(
16447 &r#"
16448 use some::mod1;
16449 use some::mod2;
16450
16451 const A: u32 = 42;
16452 const B: u32 = 42;
16453 const C: u32 = 43ˇ
16454 const D: u32 = 42;
16455
16456
16457 fn main() {
16458 println!("hello");
16459
16460 println!("world");
16461 }"#
16462 .unindent(),
16463 );
16464
16465 cx.set_head_text(&diff_base);
16466 executor.run_until_parked();
16467 cx.update_editor(|editor, window, cx| {
16468 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16469 });
16470 executor.run_until_parked();
16471
16472 cx.assert_state_with_diff(
16473 r#"
16474 use some::mod1;
16475 use some::mod2;
16476
16477 const A: u32 = 42;
16478 const B: u32 = 42;
16479 - const C: u32 = 42;
16480 + const C: u32 = 43ˇ
16481 const D: u32 = 42;
16482
16483
16484 fn main() {
16485 println!("hello");
16486
16487 println!("world");
16488 }"#
16489 .unindent(),
16490 );
16491
16492 cx.update_editor(|editor, window, cx| {
16493 editor.handle_input("\nnew_line\n", window, cx);
16494 });
16495 executor.run_until_parked();
16496
16497 cx.assert_state_with_diff(
16498 r#"
16499 use some::mod1;
16500 use some::mod2;
16501
16502 const A: u32 = 42;
16503 const B: u32 = 42;
16504 - const C: u32 = 42;
16505 + const C: u32 = 43
16506 + new_line
16507 + ˇ
16508 const D: u32 = 42;
16509
16510
16511 fn main() {
16512 println!("hello");
16513
16514 println!("world");
16515 }"#
16516 .unindent(),
16517 );
16518}
16519
16520#[gpui::test]
16521async fn test_stage_and_unstage_added_file_hunk(
16522 executor: BackgroundExecutor,
16523 cx: &mut TestAppContext,
16524) {
16525 init_test(cx, |_| {});
16526
16527 let mut cx = EditorTestContext::new(cx).await;
16528 cx.update_editor(|editor, _, cx| {
16529 editor.set_expand_all_diff_hunks(cx);
16530 });
16531
16532 let working_copy = r#"
16533 ˇfn main() {
16534 println!("hello, world!");
16535 }
16536 "#
16537 .unindent();
16538
16539 cx.set_state(&working_copy);
16540 executor.run_until_parked();
16541
16542 cx.assert_state_with_diff(
16543 r#"
16544 + ˇfn main() {
16545 + println!("hello, world!");
16546 + }
16547 "#
16548 .unindent(),
16549 );
16550 cx.assert_index_text(None);
16551
16552 cx.update_editor(|editor, window, cx| {
16553 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16554 });
16555 executor.run_until_parked();
16556 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
16557 cx.assert_state_with_diff(
16558 r#"
16559 + ˇfn main() {
16560 + println!("hello, world!");
16561 + }
16562 "#
16563 .unindent(),
16564 );
16565
16566 cx.update_editor(|editor, window, cx| {
16567 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16568 });
16569 executor.run_until_parked();
16570 cx.assert_index_text(None);
16571}
16572
16573async fn setup_indent_guides_editor(
16574 text: &str,
16575 cx: &mut TestAppContext,
16576) -> (BufferId, EditorTestContext) {
16577 init_test(cx, |_| {});
16578
16579 let mut cx = EditorTestContext::new(cx).await;
16580
16581 let buffer_id = cx.update_editor(|editor, window, cx| {
16582 editor.set_text(text, window, cx);
16583 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
16584
16585 buffer_ids[0]
16586 });
16587
16588 (buffer_id, cx)
16589}
16590
16591fn assert_indent_guides(
16592 range: Range<u32>,
16593 expected: Vec<IndentGuide>,
16594 active_indices: Option<Vec<usize>>,
16595 cx: &mut EditorTestContext,
16596) {
16597 let indent_guides = cx.update_editor(|editor, window, cx| {
16598 let snapshot = editor.snapshot(window, cx).display_snapshot;
16599 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
16600 editor,
16601 MultiBufferRow(range.start)..MultiBufferRow(range.end),
16602 true,
16603 &snapshot,
16604 cx,
16605 );
16606
16607 indent_guides.sort_by(|a, b| {
16608 a.depth.cmp(&b.depth).then(
16609 a.start_row
16610 .cmp(&b.start_row)
16611 .then(a.end_row.cmp(&b.end_row)),
16612 )
16613 });
16614 indent_guides
16615 });
16616
16617 if let Some(expected) = active_indices {
16618 let active_indices = cx.update_editor(|editor, window, cx| {
16619 let snapshot = editor.snapshot(window, cx).display_snapshot;
16620 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
16621 });
16622
16623 assert_eq!(
16624 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
16625 expected,
16626 "Active indent guide indices do not match"
16627 );
16628 }
16629
16630 assert_eq!(indent_guides, expected, "Indent guides do not match");
16631}
16632
16633fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
16634 IndentGuide {
16635 buffer_id,
16636 start_row: MultiBufferRow(start_row),
16637 end_row: MultiBufferRow(end_row),
16638 depth,
16639 tab_size: 4,
16640 settings: IndentGuideSettings {
16641 enabled: true,
16642 line_width: 1,
16643 active_line_width: 1,
16644 ..Default::default()
16645 },
16646 }
16647}
16648
16649#[gpui::test]
16650async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
16651 let (buffer_id, mut cx) = setup_indent_guides_editor(
16652 &"
16653 fn main() {
16654 let a = 1;
16655 }"
16656 .unindent(),
16657 cx,
16658 )
16659 .await;
16660
16661 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
16662}
16663
16664#[gpui::test]
16665async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
16666 let (buffer_id, mut cx) = setup_indent_guides_editor(
16667 &"
16668 fn main() {
16669 let a = 1;
16670 let b = 2;
16671 }"
16672 .unindent(),
16673 cx,
16674 )
16675 .await;
16676
16677 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
16678}
16679
16680#[gpui::test]
16681async fn test_indent_guide_nested(cx: &mut TestAppContext) {
16682 let (buffer_id, mut cx) = setup_indent_guides_editor(
16683 &"
16684 fn main() {
16685 let a = 1;
16686 if a == 3 {
16687 let b = 2;
16688 } else {
16689 let c = 3;
16690 }
16691 }"
16692 .unindent(),
16693 cx,
16694 )
16695 .await;
16696
16697 assert_indent_guides(
16698 0..8,
16699 vec![
16700 indent_guide(buffer_id, 1, 6, 0),
16701 indent_guide(buffer_id, 3, 3, 1),
16702 indent_guide(buffer_id, 5, 5, 1),
16703 ],
16704 None,
16705 &mut cx,
16706 );
16707}
16708
16709#[gpui::test]
16710async fn test_indent_guide_tab(cx: &mut TestAppContext) {
16711 let (buffer_id, mut cx) = setup_indent_guides_editor(
16712 &"
16713 fn main() {
16714 let a = 1;
16715 let b = 2;
16716 let c = 3;
16717 }"
16718 .unindent(),
16719 cx,
16720 )
16721 .await;
16722
16723 assert_indent_guides(
16724 0..5,
16725 vec![
16726 indent_guide(buffer_id, 1, 3, 0),
16727 indent_guide(buffer_id, 2, 2, 1),
16728 ],
16729 None,
16730 &mut cx,
16731 );
16732}
16733
16734#[gpui::test]
16735async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
16736 let (buffer_id, mut cx) = setup_indent_guides_editor(
16737 &"
16738 fn main() {
16739 let a = 1;
16740
16741 let c = 3;
16742 }"
16743 .unindent(),
16744 cx,
16745 )
16746 .await;
16747
16748 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
16749}
16750
16751#[gpui::test]
16752async fn test_indent_guide_complex(cx: &mut TestAppContext) {
16753 let (buffer_id, mut cx) = setup_indent_guides_editor(
16754 &"
16755 fn main() {
16756 let a = 1;
16757
16758 let c = 3;
16759
16760 if a == 3 {
16761 let b = 2;
16762 } else {
16763 let c = 3;
16764 }
16765 }"
16766 .unindent(),
16767 cx,
16768 )
16769 .await;
16770
16771 assert_indent_guides(
16772 0..11,
16773 vec![
16774 indent_guide(buffer_id, 1, 9, 0),
16775 indent_guide(buffer_id, 6, 6, 1),
16776 indent_guide(buffer_id, 8, 8, 1),
16777 ],
16778 None,
16779 &mut cx,
16780 );
16781}
16782
16783#[gpui::test]
16784async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
16785 let (buffer_id, mut cx) = setup_indent_guides_editor(
16786 &"
16787 fn main() {
16788 let a = 1;
16789
16790 let c = 3;
16791
16792 if a == 3 {
16793 let b = 2;
16794 } else {
16795 let c = 3;
16796 }
16797 }"
16798 .unindent(),
16799 cx,
16800 )
16801 .await;
16802
16803 assert_indent_guides(
16804 1..11,
16805 vec![
16806 indent_guide(buffer_id, 1, 9, 0),
16807 indent_guide(buffer_id, 6, 6, 1),
16808 indent_guide(buffer_id, 8, 8, 1),
16809 ],
16810 None,
16811 &mut cx,
16812 );
16813}
16814
16815#[gpui::test]
16816async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
16817 let (buffer_id, mut cx) = setup_indent_guides_editor(
16818 &"
16819 fn main() {
16820 let a = 1;
16821
16822 let c = 3;
16823
16824 if a == 3 {
16825 let b = 2;
16826 } else {
16827 let c = 3;
16828 }
16829 }"
16830 .unindent(),
16831 cx,
16832 )
16833 .await;
16834
16835 assert_indent_guides(
16836 1..10,
16837 vec![
16838 indent_guide(buffer_id, 1, 9, 0),
16839 indent_guide(buffer_id, 6, 6, 1),
16840 indent_guide(buffer_id, 8, 8, 1),
16841 ],
16842 None,
16843 &mut cx,
16844 );
16845}
16846
16847#[gpui::test]
16848async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
16849 let (buffer_id, mut cx) = setup_indent_guides_editor(
16850 &"
16851 block1
16852 block2
16853 block3
16854 block4
16855 block2
16856 block1
16857 block1"
16858 .unindent(),
16859 cx,
16860 )
16861 .await;
16862
16863 assert_indent_guides(
16864 1..10,
16865 vec![
16866 indent_guide(buffer_id, 1, 4, 0),
16867 indent_guide(buffer_id, 2, 3, 1),
16868 indent_guide(buffer_id, 3, 3, 2),
16869 ],
16870 None,
16871 &mut cx,
16872 );
16873}
16874
16875#[gpui::test]
16876async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
16877 let (buffer_id, mut cx) = setup_indent_guides_editor(
16878 &"
16879 block1
16880 block2
16881 block3
16882
16883 block1
16884 block1"
16885 .unindent(),
16886 cx,
16887 )
16888 .await;
16889
16890 assert_indent_guides(
16891 0..6,
16892 vec![
16893 indent_guide(buffer_id, 1, 2, 0),
16894 indent_guide(buffer_id, 2, 2, 1),
16895 ],
16896 None,
16897 &mut cx,
16898 );
16899}
16900
16901#[gpui::test]
16902async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
16903 let (buffer_id, mut cx) = setup_indent_guides_editor(
16904 &"
16905 block1
16906
16907
16908
16909 block2
16910 "
16911 .unindent(),
16912 cx,
16913 )
16914 .await;
16915
16916 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
16917}
16918
16919#[gpui::test]
16920async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
16921 let (buffer_id, mut cx) = setup_indent_guides_editor(
16922 &"
16923 def a:
16924 \tb = 3
16925 \tif True:
16926 \t\tc = 4
16927 \t\td = 5
16928 \tprint(b)
16929 "
16930 .unindent(),
16931 cx,
16932 )
16933 .await;
16934
16935 assert_indent_guides(
16936 0..6,
16937 vec![
16938 indent_guide(buffer_id, 1, 5, 0),
16939 indent_guide(buffer_id, 3, 4, 1),
16940 ],
16941 None,
16942 &mut cx,
16943 );
16944}
16945
16946#[gpui::test]
16947async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
16948 let (buffer_id, mut cx) = setup_indent_guides_editor(
16949 &"
16950 fn main() {
16951 let a = 1;
16952 }"
16953 .unindent(),
16954 cx,
16955 )
16956 .await;
16957
16958 cx.update_editor(|editor, window, cx| {
16959 editor.change_selections(None, window, cx, |s| {
16960 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
16961 });
16962 });
16963
16964 assert_indent_guides(
16965 0..3,
16966 vec![indent_guide(buffer_id, 1, 1, 0)],
16967 Some(vec![0]),
16968 &mut cx,
16969 );
16970}
16971
16972#[gpui::test]
16973async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
16974 let (buffer_id, mut cx) = setup_indent_guides_editor(
16975 &"
16976 fn main() {
16977 if 1 == 2 {
16978 let a = 1;
16979 }
16980 }"
16981 .unindent(),
16982 cx,
16983 )
16984 .await;
16985
16986 cx.update_editor(|editor, window, cx| {
16987 editor.change_selections(None, window, cx, |s| {
16988 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
16989 });
16990 });
16991
16992 assert_indent_guides(
16993 0..4,
16994 vec![
16995 indent_guide(buffer_id, 1, 3, 0),
16996 indent_guide(buffer_id, 2, 2, 1),
16997 ],
16998 Some(vec![1]),
16999 &mut cx,
17000 );
17001
17002 cx.update_editor(|editor, window, cx| {
17003 editor.change_selections(None, window, cx, |s| {
17004 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
17005 });
17006 });
17007
17008 assert_indent_guides(
17009 0..4,
17010 vec![
17011 indent_guide(buffer_id, 1, 3, 0),
17012 indent_guide(buffer_id, 2, 2, 1),
17013 ],
17014 Some(vec![1]),
17015 &mut cx,
17016 );
17017
17018 cx.update_editor(|editor, window, cx| {
17019 editor.change_selections(None, window, cx, |s| {
17020 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
17021 });
17022 });
17023
17024 assert_indent_guides(
17025 0..4,
17026 vec![
17027 indent_guide(buffer_id, 1, 3, 0),
17028 indent_guide(buffer_id, 2, 2, 1),
17029 ],
17030 Some(vec![0]),
17031 &mut cx,
17032 );
17033}
17034
17035#[gpui::test]
17036async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
17037 let (buffer_id, mut cx) = setup_indent_guides_editor(
17038 &"
17039 fn main() {
17040 let a = 1;
17041
17042 let b = 2;
17043 }"
17044 .unindent(),
17045 cx,
17046 )
17047 .await;
17048
17049 cx.update_editor(|editor, window, cx| {
17050 editor.change_selections(None, window, cx, |s| {
17051 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
17052 });
17053 });
17054
17055 assert_indent_guides(
17056 0..5,
17057 vec![indent_guide(buffer_id, 1, 3, 0)],
17058 Some(vec![0]),
17059 &mut cx,
17060 );
17061}
17062
17063#[gpui::test]
17064async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
17065 let (buffer_id, mut cx) = setup_indent_guides_editor(
17066 &"
17067 def m:
17068 a = 1
17069 pass"
17070 .unindent(),
17071 cx,
17072 )
17073 .await;
17074
17075 cx.update_editor(|editor, window, cx| {
17076 editor.change_selections(None, window, cx, |s| {
17077 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
17078 });
17079 });
17080
17081 assert_indent_guides(
17082 0..3,
17083 vec![indent_guide(buffer_id, 1, 2, 0)],
17084 Some(vec![0]),
17085 &mut cx,
17086 );
17087}
17088
17089#[gpui::test]
17090async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
17091 init_test(cx, |_| {});
17092 let mut cx = EditorTestContext::new(cx).await;
17093 let text = indoc! {
17094 "
17095 impl A {
17096 fn b() {
17097 0;
17098 3;
17099 5;
17100 6;
17101 7;
17102 }
17103 }
17104 "
17105 };
17106 let base_text = indoc! {
17107 "
17108 impl A {
17109 fn b() {
17110 0;
17111 1;
17112 2;
17113 3;
17114 4;
17115 }
17116 fn c() {
17117 5;
17118 6;
17119 7;
17120 }
17121 }
17122 "
17123 };
17124
17125 cx.update_editor(|editor, window, cx| {
17126 editor.set_text(text, window, cx);
17127
17128 editor.buffer().update(cx, |multibuffer, cx| {
17129 let buffer = multibuffer.as_singleton().unwrap();
17130 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
17131
17132 multibuffer.set_all_diff_hunks_expanded(cx);
17133 multibuffer.add_diff(diff, cx);
17134
17135 buffer.read(cx).remote_id()
17136 })
17137 });
17138 cx.run_until_parked();
17139
17140 cx.assert_state_with_diff(
17141 indoc! { "
17142 impl A {
17143 fn b() {
17144 0;
17145 - 1;
17146 - 2;
17147 3;
17148 - 4;
17149 - }
17150 - fn c() {
17151 5;
17152 6;
17153 7;
17154 }
17155 }
17156 ˇ"
17157 }
17158 .to_string(),
17159 );
17160
17161 let mut actual_guides = cx.update_editor(|editor, window, cx| {
17162 editor
17163 .snapshot(window, cx)
17164 .buffer_snapshot
17165 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
17166 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
17167 .collect::<Vec<_>>()
17168 });
17169 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
17170 assert_eq!(
17171 actual_guides,
17172 vec![
17173 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
17174 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
17175 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
17176 ]
17177 );
17178}
17179
17180#[gpui::test]
17181async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17182 init_test(cx, |_| {});
17183 let mut cx = EditorTestContext::new(cx).await;
17184
17185 let diff_base = r#"
17186 a
17187 b
17188 c
17189 "#
17190 .unindent();
17191
17192 cx.set_state(
17193 &r#"
17194 ˇA
17195 b
17196 C
17197 "#
17198 .unindent(),
17199 );
17200 cx.set_head_text(&diff_base);
17201 cx.update_editor(|editor, window, cx| {
17202 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17203 });
17204 executor.run_until_parked();
17205
17206 let both_hunks_expanded = r#"
17207 - a
17208 + ˇA
17209 b
17210 - c
17211 + C
17212 "#
17213 .unindent();
17214
17215 cx.assert_state_with_diff(both_hunks_expanded.clone());
17216
17217 let hunk_ranges = cx.update_editor(|editor, window, cx| {
17218 let snapshot = editor.snapshot(window, cx);
17219 let hunks = editor
17220 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
17221 .collect::<Vec<_>>();
17222 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
17223 let buffer_id = hunks[0].buffer_id;
17224 hunks
17225 .into_iter()
17226 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
17227 .collect::<Vec<_>>()
17228 });
17229 assert_eq!(hunk_ranges.len(), 2);
17230
17231 cx.update_editor(|editor, _, cx| {
17232 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
17233 });
17234 executor.run_until_parked();
17235
17236 let second_hunk_expanded = r#"
17237 ˇA
17238 b
17239 - c
17240 + C
17241 "#
17242 .unindent();
17243
17244 cx.assert_state_with_diff(second_hunk_expanded);
17245
17246 cx.update_editor(|editor, _, cx| {
17247 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
17248 });
17249 executor.run_until_parked();
17250
17251 cx.assert_state_with_diff(both_hunks_expanded.clone());
17252
17253 cx.update_editor(|editor, _, cx| {
17254 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
17255 });
17256 executor.run_until_parked();
17257
17258 let first_hunk_expanded = r#"
17259 - a
17260 + ˇA
17261 b
17262 C
17263 "#
17264 .unindent();
17265
17266 cx.assert_state_with_diff(first_hunk_expanded);
17267
17268 cx.update_editor(|editor, _, cx| {
17269 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
17270 });
17271 executor.run_until_parked();
17272
17273 cx.assert_state_with_diff(both_hunks_expanded);
17274
17275 cx.set_state(
17276 &r#"
17277 ˇA
17278 b
17279 "#
17280 .unindent(),
17281 );
17282 cx.run_until_parked();
17283
17284 // TODO this cursor position seems bad
17285 cx.assert_state_with_diff(
17286 r#"
17287 - ˇa
17288 + A
17289 b
17290 "#
17291 .unindent(),
17292 );
17293
17294 cx.update_editor(|editor, window, cx| {
17295 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17296 });
17297
17298 cx.assert_state_with_diff(
17299 r#"
17300 - ˇa
17301 + A
17302 b
17303 - c
17304 "#
17305 .unindent(),
17306 );
17307
17308 let hunk_ranges = cx.update_editor(|editor, window, cx| {
17309 let snapshot = editor.snapshot(window, cx);
17310 let hunks = editor
17311 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
17312 .collect::<Vec<_>>();
17313 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
17314 let buffer_id = hunks[0].buffer_id;
17315 hunks
17316 .into_iter()
17317 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
17318 .collect::<Vec<_>>()
17319 });
17320 assert_eq!(hunk_ranges.len(), 2);
17321
17322 cx.update_editor(|editor, _, cx| {
17323 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
17324 });
17325 executor.run_until_parked();
17326
17327 cx.assert_state_with_diff(
17328 r#"
17329 - ˇa
17330 + A
17331 b
17332 "#
17333 .unindent(),
17334 );
17335}
17336
17337#[gpui::test]
17338async fn test_toggle_deletion_hunk_at_start_of_file(
17339 executor: BackgroundExecutor,
17340 cx: &mut TestAppContext,
17341) {
17342 init_test(cx, |_| {});
17343 let mut cx = EditorTestContext::new(cx).await;
17344
17345 let diff_base = r#"
17346 a
17347 b
17348 c
17349 "#
17350 .unindent();
17351
17352 cx.set_state(
17353 &r#"
17354 ˇb
17355 c
17356 "#
17357 .unindent(),
17358 );
17359 cx.set_head_text(&diff_base);
17360 cx.update_editor(|editor, window, cx| {
17361 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17362 });
17363 executor.run_until_parked();
17364
17365 let hunk_expanded = r#"
17366 - a
17367 ˇb
17368 c
17369 "#
17370 .unindent();
17371
17372 cx.assert_state_with_diff(hunk_expanded.clone());
17373
17374 let hunk_ranges = cx.update_editor(|editor, window, cx| {
17375 let snapshot = editor.snapshot(window, cx);
17376 let hunks = editor
17377 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
17378 .collect::<Vec<_>>();
17379 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
17380 let buffer_id = hunks[0].buffer_id;
17381 hunks
17382 .into_iter()
17383 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
17384 .collect::<Vec<_>>()
17385 });
17386 assert_eq!(hunk_ranges.len(), 1);
17387
17388 cx.update_editor(|editor, _, cx| {
17389 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
17390 });
17391 executor.run_until_parked();
17392
17393 let hunk_collapsed = r#"
17394 ˇb
17395 c
17396 "#
17397 .unindent();
17398
17399 cx.assert_state_with_diff(hunk_collapsed);
17400
17401 cx.update_editor(|editor, _, cx| {
17402 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
17403 });
17404 executor.run_until_parked();
17405
17406 cx.assert_state_with_diff(hunk_expanded.clone());
17407}
17408
17409#[gpui::test]
17410async fn test_display_diff_hunks(cx: &mut TestAppContext) {
17411 init_test(cx, |_| {});
17412
17413 let fs = FakeFs::new(cx.executor());
17414 fs.insert_tree(
17415 path!("/test"),
17416 json!({
17417 ".git": {},
17418 "file-1": "ONE\n",
17419 "file-2": "TWO\n",
17420 "file-3": "THREE\n",
17421 }),
17422 )
17423 .await;
17424
17425 fs.set_head_for_repo(
17426 path!("/test/.git").as_ref(),
17427 &[
17428 ("file-1".into(), "one\n".into()),
17429 ("file-2".into(), "two\n".into()),
17430 ("file-3".into(), "three\n".into()),
17431 ],
17432 );
17433
17434 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
17435 let mut buffers = vec![];
17436 for i in 1..=3 {
17437 let buffer = project
17438 .update(cx, |project, cx| {
17439 let path = format!(path!("/test/file-{}"), i);
17440 project.open_local_buffer(path, cx)
17441 })
17442 .await
17443 .unwrap();
17444 buffers.push(buffer);
17445 }
17446
17447 let multibuffer = cx.new(|cx| {
17448 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
17449 multibuffer.set_all_diff_hunks_expanded(cx);
17450 for buffer in &buffers {
17451 let snapshot = buffer.read(cx).snapshot();
17452 multibuffer.set_excerpts_for_path(
17453 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
17454 buffer.clone(),
17455 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
17456 DEFAULT_MULTIBUFFER_CONTEXT,
17457 cx,
17458 );
17459 }
17460 multibuffer
17461 });
17462
17463 let editor = cx.add_window(|window, cx| {
17464 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
17465 });
17466 cx.run_until_parked();
17467
17468 let snapshot = editor
17469 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
17470 .unwrap();
17471 let hunks = snapshot
17472 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
17473 .map(|hunk| match hunk {
17474 DisplayDiffHunk::Unfolded {
17475 display_row_range, ..
17476 } => display_row_range,
17477 DisplayDiffHunk::Folded { .. } => unreachable!(),
17478 })
17479 .collect::<Vec<_>>();
17480 assert_eq!(
17481 hunks,
17482 [
17483 DisplayRow(2)..DisplayRow(4),
17484 DisplayRow(7)..DisplayRow(9),
17485 DisplayRow(12)..DisplayRow(14),
17486 ]
17487 );
17488}
17489
17490#[gpui::test]
17491async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
17492 init_test(cx, |_| {});
17493
17494 let mut cx = EditorTestContext::new(cx).await;
17495 cx.set_head_text(indoc! { "
17496 one
17497 two
17498 three
17499 four
17500 five
17501 "
17502 });
17503 cx.set_index_text(indoc! { "
17504 one
17505 two
17506 three
17507 four
17508 five
17509 "
17510 });
17511 cx.set_state(indoc! {"
17512 one
17513 TWO
17514 ˇTHREE
17515 FOUR
17516 five
17517 "});
17518 cx.run_until_parked();
17519 cx.update_editor(|editor, window, cx| {
17520 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
17521 });
17522 cx.run_until_parked();
17523 cx.assert_index_text(Some(indoc! {"
17524 one
17525 TWO
17526 THREE
17527 FOUR
17528 five
17529 "}));
17530 cx.set_state(indoc! { "
17531 one
17532 TWO
17533 ˇTHREE-HUNDRED
17534 FOUR
17535 five
17536 "});
17537 cx.run_until_parked();
17538 cx.update_editor(|editor, window, cx| {
17539 let snapshot = editor.snapshot(window, cx);
17540 let hunks = editor
17541 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
17542 .collect::<Vec<_>>();
17543 assert_eq!(hunks.len(), 1);
17544 assert_eq!(
17545 hunks[0].status(),
17546 DiffHunkStatus {
17547 kind: DiffHunkStatusKind::Modified,
17548 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
17549 }
17550 );
17551
17552 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
17553 });
17554 cx.run_until_parked();
17555 cx.assert_index_text(Some(indoc! {"
17556 one
17557 TWO
17558 THREE-HUNDRED
17559 FOUR
17560 five
17561 "}));
17562}
17563
17564#[gpui::test]
17565fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
17566 init_test(cx, |_| {});
17567
17568 let editor = cx.add_window(|window, cx| {
17569 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
17570 build_editor(buffer, window, cx)
17571 });
17572
17573 let render_args = Arc::new(Mutex::new(None));
17574 let snapshot = editor
17575 .update(cx, |editor, window, cx| {
17576 let snapshot = editor.buffer().read(cx).snapshot(cx);
17577 let range =
17578 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
17579
17580 struct RenderArgs {
17581 row: MultiBufferRow,
17582 folded: bool,
17583 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
17584 }
17585
17586 let crease = Crease::inline(
17587 range,
17588 FoldPlaceholder::test(),
17589 {
17590 let toggle_callback = render_args.clone();
17591 move |row, folded, callback, _window, _cx| {
17592 *toggle_callback.lock() = Some(RenderArgs {
17593 row,
17594 folded,
17595 callback,
17596 });
17597 div()
17598 }
17599 },
17600 |_row, _folded, _window, _cx| div(),
17601 );
17602
17603 editor.insert_creases(Some(crease), cx);
17604 let snapshot = editor.snapshot(window, cx);
17605 let _div = snapshot.render_crease_toggle(
17606 MultiBufferRow(1),
17607 false,
17608 cx.entity().clone(),
17609 window,
17610 cx,
17611 );
17612 snapshot
17613 })
17614 .unwrap();
17615
17616 let render_args = render_args.lock().take().unwrap();
17617 assert_eq!(render_args.row, MultiBufferRow(1));
17618 assert!(!render_args.folded);
17619 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
17620
17621 cx.update_window(*editor, |_, window, cx| {
17622 (render_args.callback)(true, window, cx)
17623 })
17624 .unwrap();
17625 let snapshot = editor
17626 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
17627 .unwrap();
17628 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
17629
17630 cx.update_window(*editor, |_, window, cx| {
17631 (render_args.callback)(false, window, cx)
17632 })
17633 .unwrap();
17634 let snapshot = editor
17635 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
17636 .unwrap();
17637 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
17638}
17639
17640#[gpui::test]
17641async fn test_input_text(cx: &mut TestAppContext) {
17642 init_test(cx, |_| {});
17643 let mut cx = EditorTestContext::new(cx).await;
17644
17645 cx.set_state(
17646 &r#"ˇone
17647 two
17648
17649 three
17650 fourˇ
17651 five
17652
17653 siˇx"#
17654 .unindent(),
17655 );
17656
17657 cx.dispatch_action(HandleInput(String::new()));
17658 cx.assert_editor_state(
17659 &r#"ˇone
17660 two
17661
17662 three
17663 fourˇ
17664 five
17665
17666 siˇx"#
17667 .unindent(),
17668 );
17669
17670 cx.dispatch_action(HandleInput("AAAA".to_string()));
17671 cx.assert_editor_state(
17672 &r#"AAAAˇone
17673 two
17674
17675 three
17676 fourAAAAˇ
17677 five
17678
17679 siAAAAˇx"#
17680 .unindent(),
17681 );
17682}
17683
17684#[gpui::test]
17685async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
17686 init_test(cx, |_| {});
17687
17688 let mut cx = EditorTestContext::new(cx).await;
17689 cx.set_state(
17690 r#"let foo = 1;
17691let foo = 2;
17692let foo = 3;
17693let fooˇ = 4;
17694let foo = 5;
17695let foo = 6;
17696let foo = 7;
17697let foo = 8;
17698let foo = 9;
17699let foo = 10;
17700let foo = 11;
17701let foo = 12;
17702let foo = 13;
17703let foo = 14;
17704let foo = 15;"#,
17705 );
17706
17707 cx.update_editor(|e, window, cx| {
17708 assert_eq!(
17709 e.next_scroll_position,
17710 NextScrollCursorCenterTopBottom::Center,
17711 "Default next scroll direction is center",
17712 );
17713
17714 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17715 assert_eq!(
17716 e.next_scroll_position,
17717 NextScrollCursorCenterTopBottom::Top,
17718 "After center, next scroll direction should be top",
17719 );
17720
17721 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17722 assert_eq!(
17723 e.next_scroll_position,
17724 NextScrollCursorCenterTopBottom::Bottom,
17725 "After top, next scroll direction should be bottom",
17726 );
17727
17728 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17729 assert_eq!(
17730 e.next_scroll_position,
17731 NextScrollCursorCenterTopBottom::Center,
17732 "After bottom, scrolling should start over",
17733 );
17734
17735 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17736 assert_eq!(
17737 e.next_scroll_position,
17738 NextScrollCursorCenterTopBottom::Top,
17739 "Scrolling continues if retriggered fast enough"
17740 );
17741 });
17742
17743 cx.executor()
17744 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
17745 cx.executor().run_until_parked();
17746 cx.update_editor(|e, _, _| {
17747 assert_eq!(
17748 e.next_scroll_position,
17749 NextScrollCursorCenterTopBottom::Center,
17750 "If scrolling is not triggered fast enough, it should reset"
17751 );
17752 });
17753}
17754
17755#[gpui::test]
17756async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
17757 init_test(cx, |_| {});
17758 let mut cx = EditorLspTestContext::new_rust(
17759 lsp::ServerCapabilities {
17760 definition_provider: Some(lsp::OneOf::Left(true)),
17761 references_provider: Some(lsp::OneOf::Left(true)),
17762 ..lsp::ServerCapabilities::default()
17763 },
17764 cx,
17765 )
17766 .await;
17767
17768 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
17769 let go_to_definition = cx
17770 .lsp
17771 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
17772 move |params, _| async move {
17773 if empty_go_to_definition {
17774 Ok(None)
17775 } else {
17776 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
17777 uri: params.text_document_position_params.text_document.uri,
17778 range: lsp::Range::new(
17779 lsp::Position::new(4, 3),
17780 lsp::Position::new(4, 6),
17781 ),
17782 })))
17783 }
17784 },
17785 );
17786 let references = cx
17787 .lsp
17788 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
17789 Ok(Some(vec![lsp::Location {
17790 uri: params.text_document_position.text_document.uri,
17791 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
17792 }]))
17793 });
17794 (go_to_definition, references)
17795 };
17796
17797 cx.set_state(
17798 &r#"fn one() {
17799 let mut a = ˇtwo();
17800 }
17801
17802 fn two() {}"#
17803 .unindent(),
17804 );
17805 set_up_lsp_handlers(false, &mut cx);
17806 let navigated = cx
17807 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
17808 .await
17809 .expect("Failed to navigate to definition");
17810 assert_eq!(
17811 navigated,
17812 Navigated::Yes,
17813 "Should have navigated to definition from the GetDefinition response"
17814 );
17815 cx.assert_editor_state(
17816 &r#"fn one() {
17817 let mut a = two();
17818 }
17819
17820 fn «twoˇ»() {}"#
17821 .unindent(),
17822 );
17823
17824 let editors = cx.update_workspace(|workspace, _, cx| {
17825 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
17826 });
17827 cx.update_editor(|_, _, test_editor_cx| {
17828 assert_eq!(
17829 editors.len(),
17830 1,
17831 "Initially, only one, test, editor should be open in the workspace"
17832 );
17833 assert_eq!(
17834 test_editor_cx.entity(),
17835 editors.last().expect("Asserted len is 1").clone()
17836 );
17837 });
17838
17839 set_up_lsp_handlers(true, &mut cx);
17840 let navigated = cx
17841 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
17842 .await
17843 .expect("Failed to navigate to lookup references");
17844 assert_eq!(
17845 navigated,
17846 Navigated::Yes,
17847 "Should have navigated to references as a fallback after empty GoToDefinition response"
17848 );
17849 // We should not change the selections in the existing file,
17850 // if opening another milti buffer with the references
17851 cx.assert_editor_state(
17852 &r#"fn one() {
17853 let mut a = two();
17854 }
17855
17856 fn «twoˇ»() {}"#
17857 .unindent(),
17858 );
17859 let editors = cx.update_workspace(|workspace, _, cx| {
17860 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
17861 });
17862 cx.update_editor(|_, _, test_editor_cx| {
17863 assert_eq!(
17864 editors.len(),
17865 2,
17866 "After falling back to references search, we open a new editor with the results"
17867 );
17868 let references_fallback_text = editors
17869 .into_iter()
17870 .find(|new_editor| *new_editor != test_editor_cx.entity())
17871 .expect("Should have one non-test editor now")
17872 .read(test_editor_cx)
17873 .text(test_editor_cx);
17874 assert_eq!(
17875 references_fallback_text, "fn one() {\n let mut a = two();\n}",
17876 "Should use the range from the references response and not the GoToDefinition one"
17877 );
17878 });
17879}
17880
17881#[gpui::test]
17882async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
17883 init_test(cx, |_| {});
17884 cx.update(|cx| {
17885 let mut editor_settings = EditorSettings::get_global(cx).clone();
17886 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
17887 EditorSettings::override_global(editor_settings, cx);
17888 });
17889 let mut cx = EditorLspTestContext::new_rust(
17890 lsp::ServerCapabilities {
17891 definition_provider: Some(lsp::OneOf::Left(true)),
17892 references_provider: Some(lsp::OneOf::Left(true)),
17893 ..lsp::ServerCapabilities::default()
17894 },
17895 cx,
17896 )
17897 .await;
17898 let original_state = r#"fn one() {
17899 let mut a = ˇtwo();
17900 }
17901
17902 fn two() {}"#
17903 .unindent();
17904 cx.set_state(&original_state);
17905
17906 let mut go_to_definition = cx
17907 .lsp
17908 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
17909 move |_, _| async move { Ok(None) },
17910 );
17911 let _references = cx
17912 .lsp
17913 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
17914 panic!("Should not call for references with no go to definition fallback")
17915 });
17916
17917 let navigated = cx
17918 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
17919 .await
17920 .expect("Failed to navigate to lookup references");
17921 go_to_definition
17922 .next()
17923 .await
17924 .expect("Should have called the go_to_definition handler");
17925
17926 assert_eq!(
17927 navigated,
17928 Navigated::No,
17929 "Should have navigated to references as a fallback after empty GoToDefinition response"
17930 );
17931 cx.assert_editor_state(&original_state);
17932 let editors = cx.update_workspace(|workspace, _, cx| {
17933 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
17934 });
17935 cx.update_editor(|_, _, _| {
17936 assert_eq!(
17937 editors.len(),
17938 1,
17939 "After unsuccessful fallback, no other editor should have been opened"
17940 );
17941 });
17942}
17943
17944#[gpui::test]
17945async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
17946 init_test(cx, |_| {});
17947
17948 let language = Arc::new(Language::new(
17949 LanguageConfig::default(),
17950 Some(tree_sitter_rust::LANGUAGE.into()),
17951 ));
17952
17953 let text = r#"
17954 #[cfg(test)]
17955 mod tests() {
17956 #[test]
17957 fn runnable_1() {
17958 let a = 1;
17959 }
17960
17961 #[test]
17962 fn runnable_2() {
17963 let a = 1;
17964 let b = 2;
17965 }
17966 }
17967 "#
17968 .unindent();
17969
17970 let fs = FakeFs::new(cx.executor());
17971 fs.insert_file("/file.rs", Default::default()).await;
17972
17973 let project = Project::test(fs, ["/a".as_ref()], cx).await;
17974 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17975 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17976 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
17977 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
17978
17979 let editor = cx.new_window_entity(|window, cx| {
17980 Editor::new(
17981 EditorMode::full(),
17982 multi_buffer,
17983 Some(project.clone()),
17984 window,
17985 cx,
17986 )
17987 });
17988
17989 editor.update_in(cx, |editor, window, cx| {
17990 let snapshot = editor.buffer().read(cx).snapshot(cx);
17991 editor.tasks.insert(
17992 (buffer.read(cx).remote_id(), 3),
17993 RunnableTasks {
17994 templates: vec![],
17995 offset: snapshot.anchor_before(43),
17996 column: 0,
17997 extra_variables: HashMap::default(),
17998 context_range: BufferOffset(43)..BufferOffset(85),
17999 },
18000 );
18001 editor.tasks.insert(
18002 (buffer.read(cx).remote_id(), 8),
18003 RunnableTasks {
18004 templates: vec![],
18005 offset: snapshot.anchor_before(86),
18006 column: 0,
18007 extra_variables: HashMap::default(),
18008 context_range: BufferOffset(86)..BufferOffset(191),
18009 },
18010 );
18011
18012 // Test finding task when cursor is inside function body
18013 editor.change_selections(None, window, cx, |s| {
18014 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
18015 });
18016 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
18017 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
18018
18019 // Test finding task when cursor is on function name
18020 editor.change_selections(None, window, cx, |s| {
18021 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
18022 });
18023 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
18024 assert_eq!(row, 8, "Should find task when cursor is on function name");
18025 });
18026}
18027
18028#[gpui::test]
18029async fn test_folding_buffers(cx: &mut TestAppContext) {
18030 init_test(cx, |_| {});
18031
18032 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
18033 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
18034 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
18035
18036 let fs = FakeFs::new(cx.executor());
18037 fs.insert_tree(
18038 path!("/a"),
18039 json!({
18040 "first.rs": sample_text_1,
18041 "second.rs": sample_text_2,
18042 "third.rs": sample_text_3,
18043 }),
18044 )
18045 .await;
18046 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18047 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18048 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18049 let worktree = project.update(cx, |project, cx| {
18050 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
18051 assert_eq!(worktrees.len(), 1);
18052 worktrees.pop().unwrap()
18053 });
18054 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
18055
18056 let buffer_1 = project
18057 .update(cx, |project, cx| {
18058 project.open_buffer((worktree_id, "first.rs"), cx)
18059 })
18060 .await
18061 .unwrap();
18062 let buffer_2 = project
18063 .update(cx, |project, cx| {
18064 project.open_buffer((worktree_id, "second.rs"), cx)
18065 })
18066 .await
18067 .unwrap();
18068 let buffer_3 = project
18069 .update(cx, |project, cx| {
18070 project.open_buffer((worktree_id, "third.rs"), cx)
18071 })
18072 .await
18073 .unwrap();
18074
18075 let multi_buffer = cx.new(|cx| {
18076 let mut multi_buffer = MultiBuffer::new(ReadWrite);
18077 multi_buffer.push_excerpts(
18078 buffer_1.clone(),
18079 [
18080 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18081 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18082 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18083 ],
18084 cx,
18085 );
18086 multi_buffer.push_excerpts(
18087 buffer_2.clone(),
18088 [
18089 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18090 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18091 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18092 ],
18093 cx,
18094 );
18095 multi_buffer.push_excerpts(
18096 buffer_3.clone(),
18097 [
18098 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18099 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18100 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18101 ],
18102 cx,
18103 );
18104 multi_buffer
18105 });
18106 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18107 Editor::new(
18108 EditorMode::full(),
18109 multi_buffer.clone(),
18110 Some(project.clone()),
18111 window,
18112 cx,
18113 )
18114 });
18115
18116 assert_eq!(
18117 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18118 "\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",
18119 );
18120
18121 multi_buffer_editor.update(cx, |editor, cx| {
18122 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
18123 });
18124 assert_eq!(
18125 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18126 "\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",
18127 "After folding the first buffer, its text should not be displayed"
18128 );
18129
18130 multi_buffer_editor.update(cx, |editor, cx| {
18131 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
18132 });
18133 assert_eq!(
18134 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18135 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
18136 "After folding the second buffer, its text should not be displayed"
18137 );
18138
18139 multi_buffer_editor.update(cx, |editor, cx| {
18140 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
18141 });
18142 assert_eq!(
18143 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18144 "\n\n\n\n\n",
18145 "After folding the third buffer, its text should not be displayed"
18146 );
18147
18148 // Emulate selection inside the fold logic, that should work
18149 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18150 editor
18151 .snapshot(window, cx)
18152 .next_line_boundary(Point::new(0, 4));
18153 });
18154
18155 multi_buffer_editor.update(cx, |editor, cx| {
18156 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
18157 });
18158 assert_eq!(
18159 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18160 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
18161 "After unfolding the second buffer, its text should be displayed"
18162 );
18163
18164 // Typing inside of buffer 1 causes that buffer to be unfolded.
18165 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18166 assert_eq!(
18167 multi_buffer
18168 .read(cx)
18169 .snapshot(cx)
18170 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
18171 .collect::<String>(),
18172 "bbbb"
18173 );
18174 editor.change_selections(None, window, cx, |selections| {
18175 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
18176 });
18177 editor.handle_input("B", window, cx);
18178 });
18179
18180 assert_eq!(
18181 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18182 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
18183 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
18184 );
18185
18186 multi_buffer_editor.update(cx, |editor, cx| {
18187 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
18188 });
18189 assert_eq!(
18190 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18191 "\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",
18192 "After unfolding the all buffers, all original text should be displayed"
18193 );
18194}
18195
18196#[gpui::test]
18197async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
18198 init_test(cx, |_| {});
18199
18200 let sample_text_1 = "1111\n2222\n3333".to_string();
18201 let sample_text_2 = "4444\n5555\n6666".to_string();
18202 let sample_text_3 = "7777\n8888\n9999".to_string();
18203
18204 let fs = FakeFs::new(cx.executor());
18205 fs.insert_tree(
18206 path!("/a"),
18207 json!({
18208 "first.rs": sample_text_1,
18209 "second.rs": sample_text_2,
18210 "third.rs": sample_text_3,
18211 }),
18212 )
18213 .await;
18214 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18215 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18216 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18217 let worktree = project.update(cx, |project, cx| {
18218 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
18219 assert_eq!(worktrees.len(), 1);
18220 worktrees.pop().unwrap()
18221 });
18222 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
18223
18224 let buffer_1 = project
18225 .update(cx, |project, cx| {
18226 project.open_buffer((worktree_id, "first.rs"), cx)
18227 })
18228 .await
18229 .unwrap();
18230 let buffer_2 = project
18231 .update(cx, |project, cx| {
18232 project.open_buffer((worktree_id, "second.rs"), cx)
18233 })
18234 .await
18235 .unwrap();
18236 let buffer_3 = project
18237 .update(cx, |project, cx| {
18238 project.open_buffer((worktree_id, "third.rs"), cx)
18239 })
18240 .await
18241 .unwrap();
18242
18243 let multi_buffer = cx.new(|cx| {
18244 let mut multi_buffer = MultiBuffer::new(ReadWrite);
18245 multi_buffer.push_excerpts(
18246 buffer_1.clone(),
18247 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
18248 cx,
18249 );
18250 multi_buffer.push_excerpts(
18251 buffer_2.clone(),
18252 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
18253 cx,
18254 );
18255 multi_buffer.push_excerpts(
18256 buffer_3.clone(),
18257 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
18258 cx,
18259 );
18260 multi_buffer
18261 });
18262
18263 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18264 Editor::new(
18265 EditorMode::full(),
18266 multi_buffer,
18267 Some(project.clone()),
18268 window,
18269 cx,
18270 )
18271 });
18272
18273 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
18274 assert_eq!(
18275 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18276 full_text,
18277 );
18278
18279 multi_buffer_editor.update(cx, |editor, cx| {
18280 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
18281 });
18282 assert_eq!(
18283 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18284 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
18285 "After folding the first buffer, its text should not be displayed"
18286 );
18287
18288 multi_buffer_editor.update(cx, |editor, cx| {
18289 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
18290 });
18291
18292 assert_eq!(
18293 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18294 "\n\n\n\n\n\n7777\n8888\n9999",
18295 "After folding the second buffer, its text should not be displayed"
18296 );
18297
18298 multi_buffer_editor.update(cx, |editor, cx| {
18299 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
18300 });
18301 assert_eq!(
18302 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18303 "\n\n\n\n\n",
18304 "After folding the third buffer, its text should not be displayed"
18305 );
18306
18307 multi_buffer_editor.update(cx, |editor, cx| {
18308 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
18309 });
18310 assert_eq!(
18311 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18312 "\n\n\n\n4444\n5555\n6666\n\n",
18313 "After unfolding the second buffer, its text should be displayed"
18314 );
18315
18316 multi_buffer_editor.update(cx, |editor, cx| {
18317 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
18318 });
18319 assert_eq!(
18320 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18321 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
18322 "After unfolding the first buffer, its text should be displayed"
18323 );
18324
18325 multi_buffer_editor.update(cx, |editor, cx| {
18326 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
18327 });
18328 assert_eq!(
18329 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18330 full_text,
18331 "After unfolding all buffers, all original text should be displayed"
18332 );
18333}
18334
18335#[gpui::test]
18336async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
18337 init_test(cx, |_| {});
18338
18339 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
18340
18341 let fs = FakeFs::new(cx.executor());
18342 fs.insert_tree(
18343 path!("/a"),
18344 json!({
18345 "main.rs": sample_text,
18346 }),
18347 )
18348 .await;
18349 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18350 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18351 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18352 let worktree = project.update(cx, |project, cx| {
18353 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
18354 assert_eq!(worktrees.len(), 1);
18355 worktrees.pop().unwrap()
18356 });
18357 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
18358
18359 let buffer_1 = project
18360 .update(cx, |project, cx| {
18361 project.open_buffer((worktree_id, "main.rs"), cx)
18362 })
18363 .await
18364 .unwrap();
18365
18366 let multi_buffer = cx.new(|cx| {
18367 let mut multi_buffer = MultiBuffer::new(ReadWrite);
18368 multi_buffer.push_excerpts(
18369 buffer_1.clone(),
18370 [ExcerptRange::new(
18371 Point::new(0, 0)
18372 ..Point::new(
18373 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
18374 0,
18375 ),
18376 )],
18377 cx,
18378 );
18379 multi_buffer
18380 });
18381 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18382 Editor::new(
18383 EditorMode::full(),
18384 multi_buffer,
18385 Some(project.clone()),
18386 window,
18387 cx,
18388 )
18389 });
18390
18391 let selection_range = Point::new(1, 0)..Point::new(2, 0);
18392 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18393 enum TestHighlight {}
18394 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
18395 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
18396 editor.highlight_text::<TestHighlight>(
18397 vec![highlight_range.clone()],
18398 HighlightStyle::color(Hsla::green()),
18399 cx,
18400 );
18401 editor.change_selections(None, window, cx, |s| s.select_ranges(Some(highlight_range)));
18402 });
18403
18404 let full_text = format!("\n\n{sample_text}");
18405 assert_eq!(
18406 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18407 full_text,
18408 );
18409}
18410
18411#[gpui::test]
18412async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
18413 init_test(cx, |_| {});
18414 cx.update(|cx| {
18415 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
18416 "keymaps/default-linux.json",
18417 cx,
18418 )
18419 .unwrap();
18420 cx.bind_keys(default_key_bindings);
18421 });
18422
18423 let (editor, cx) = cx.add_window_view(|window, cx| {
18424 let multi_buffer = MultiBuffer::build_multi(
18425 [
18426 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
18427 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
18428 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
18429 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
18430 ],
18431 cx,
18432 );
18433 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
18434
18435 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
18436 // fold all but the second buffer, so that we test navigating between two
18437 // adjacent folded buffers, as well as folded buffers at the start and
18438 // end the multibuffer
18439 editor.fold_buffer(buffer_ids[0], cx);
18440 editor.fold_buffer(buffer_ids[2], cx);
18441 editor.fold_buffer(buffer_ids[3], cx);
18442
18443 editor
18444 });
18445 cx.simulate_resize(size(px(1000.), px(1000.)));
18446
18447 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
18448 cx.assert_excerpts_with_selections(indoc! {"
18449 [EXCERPT]
18450 ˇ[FOLDED]
18451 [EXCERPT]
18452 a1
18453 b1
18454 [EXCERPT]
18455 [FOLDED]
18456 [EXCERPT]
18457 [FOLDED]
18458 "
18459 });
18460 cx.simulate_keystroke("down");
18461 cx.assert_excerpts_with_selections(indoc! {"
18462 [EXCERPT]
18463 [FOLDED]
18464 [EXCERPT]
18465 ˇa1
18466 b1
18467 [EXCERPT]
18468 [FOLDED]
18469 [EXCERPT]
18470 [FOLDED]
18471 "
18472 });
18473 cx.simulate_keystroke("down");
18474 cx.assert_excerpts_with_selections(indoc! {"
18475 [EXCERPT]
18476 [FOLDED]
18477 [EXCERPT]
18478 a1
18479 ˇb1
18480 [EXCERPT]
18481 [FOLDED]
18482 [EXCERPT]
18483 [FOLDED]
18484 "
18485 });
18486 cx.simulate_keystroke("down");
18487 cx.assert_excerpts_with_selections(indoc! {"
18488 [EXCERPT]
18489 [FOLDED]
18490 [EXCERPT]
18491 a1
18492 b1
18493 ˇ[EXCERPT]
18494 [FOLDED]
18495 [EXCERPT]
18496 [FOLDED]
18497 "
18498 });
18499 cx.simulate_keystroke("down");
18500 cx.assert_excerpts_with_selections(indoc! {"
18501 [EXCERPT]
18502 [FOLDED]
18503 [EXCERPT]
18504 a1
18505 b1
18506 [EXCERPT]
18507 ˇ[FOLDED]
18508 [EXCERPT]
18509 [FOLDED]
18510 "
18511 });
18512 for _ in 0..5 {
18513 cx.simulate_keystroke("down");
18514 cx.assert_excerpts_with_selections(indoc! {"
18515 [EXCERPT]
18516 [FOLDED]
18517 [EXCERPT]
18518 a1
18519 b1
18520 [EXCERPT]
18521 [FOLDED]
18522 [EXCERPT]
18523 ˇ[FOLDED]
18524 "
18525 });
18526 }
18527
18528 cx.simulate_keystroke("up");
18529 cx.assert_excerpts_with_selections(indoc! {"
18530 [EXCERPT]
18531 [FOLDED]
18532 [EXCERPT]
18533 a1
18534 b1
18535 [EXCERPT]
18536 ˇ[FOLDED]
18537 [EXCERPT]
18538 [FOLDED]
18539 "
18540 });
18541 cx.simulate_keystroke("up");
18542 cx.assert_excerpts_with_selections(indoc! {"
18543 [EXCERPT]
18544 [FOLDED]
18545 [EXCERPT]
18546 a1
18547 b1
18548 ˇ[EXCERPT]
18549 [FOLDED]
18550 [EXCERPT]
18551 [FOLDED]
18552 "
18553 });
18554 cx.simulate_keystroke("up");
18555 cx.assert_excerpts_with_selections(indoc! {"
18556 [EXCERPT]
18557 [FOLDED]
18558 [EXCERPT]
18559 a1
18560 ˇb1
18561 [EXCERPT]
18562 [FOLDED]
18563 [EXCERPT]
18564 [FOLDED]
18565 "
18566 });
18567 cx.simulate_keystroke("up");
18568 cx.assert_excerpts_with_selections(indoc! {"
18569 [EXCERPT]
18570 [FOLDED]
18571 [EXCERPT]
18572 ˇa1
18573 b1
18574 [EXCERPT]
18575 [FOLDED]
18576 [EXCERPT]
18577 [FOLDED]
18578 "
18579 });
18580 for _ in 0..5 {
18581 cx.simulate_keystroke("up");
18582 cx.assert_excerpts_with_selections(indoc! {"
18583 [EXCERPT]
18584 ˇ[FOLDED]
18585 [EXCERPT]
18586 a1
18587 b1
18588 [EXCERPT]
18589 [FOLDED]
18590 [EXCERPT]
18591 [FOLDED]
18592 "
18593 });
18594 }
18595}
18596
18597#[gpui::test]
18598async fn test_inline_completion_text(cx: &mut TestAppContext) {
18599 init_test(cx, |_| {});
18600
18601 // Simple insertion
18602 assert_highlighted_edits(
18603 "Hello, world!",
18604 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
18605 true,
18606 cx,
18607 |highlighted_edits, cx| {
18608 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
18609 assert_eq!(highlighted_edits.highlights.len(), 1);
18610 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
18611 assert_eq!(
18612 highlighted_edits.highlights[0].1.background_color,
18613 Some(cx.theme().status().created_background)
18614 );
18615 },
18616 )
18617 .await;
18618
18619 // Replacement
18620 assert_highlighted_edits(
18621 "This is a test.",
18622 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
18623 false,
18624 cx,
18625 |highlighted_edits, cx| {
18626 assert_eq!(highlighted_edits.text, "That is a test.");
18627 assert_eq!(highlighted_edits.highlights.len(), 1);
18628 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
18629 assert_eq!(
18630 highlighted_edits.highlights[0].1.background_color,
18631 Some(cx.theme().status().created_background)
18632 );
18633 },
18634 )
18635 .await;
18636
18637 // Multiple edits
18638 assert_highlighted_edits(
18639 "Hello, world!",
18640 vec![
18641 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
18642 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
18643 ],
18644 false,
18645 cx,
18646 |highlighted_edits, cx| {
18647 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
18648 assert_eq!(highlighted_edits.highlights.len(), 2);
18649 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
18650 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
18651 assert_eq!(
18652 highlighted_edits.highlights[0].1.background_color,
18653 Some(cx.theme().status().created_background)
18654 );
18655 assert_eq!(
18656 highlighted_edits.highlights[1].1.background_color,
18657 Some(cx.theme().status().created_background)
18658 );
18659 },
18660 )
18661 .await;
18662
18663 // Multiple lines with edits
18664 assert_highlighted_edits(
18665 "First line\nSecond line\nThird line\nFourth line",
18666 vec![
18667 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
18668 (
18669 Point::new(2, 0)..Point::new(2, 10),
18670 "New third line".to_string(),
18671 ),
18672 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
18673 ],
18674 false,
18675 cx,
18676 |highlighted_edits, cx| {
18677 assert_eq!(
18678 highlighted_edits.text,
18679 "Second modified\nNew third line\nFourth updated line"
18680 );
18681 assert_eq!(highlighted_edits.highlights.len(), 3);
18682 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
18683 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
18684 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
18685 for highlight in &highlighted_edits.highlights {
18686 assert_eq!(
18687 highlight.1.background_color,
18688 Some(cx.theme().status().created_background)
18689 );
18690 }
18691 },
18692 )
18693 .await;
18694}
18695
18696#[gpui::test]
18697async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
18698 init_test(cx, |_| {});
18699
18700 // Deletion
18701 assert_highlighted_edits(
18702 "Hello, world!",
18703 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
18704 true,
18705 cx,
18706 |highlighted_edits, cx| {
18707 assert_eq!(highlighted_edits.text, "Hello, world!");
18708 assert_eq!(highlighted_edits.highlights.len(), 1);
18709 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
18710 assert_eq!(
18711 highlighted_edits.highlights[0].1.background_color,
18712 Some(cx.theme().status().deleted_background)
18713 );
18714 },
18715 )
18716 .await;
18717
18718 // Insertion
18719 assert_highlighted_edits(
18720 "Hello, world!",
18721 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
18722 true,
18723 cx,
18724 |highlighted_edits, cx| {
18725 assert_eq!(highlighted_edits.highlights.len(), 1);
18726 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
18727 assert_eq!(
18728 highlighted_edits.highlights[0].1.background_color,
18729 Some(cx.theme().status().created_background)
18730 );
18731 },
18732 )
18733 .await;
18734}
18735
18736async fn assert_highlighted_edits(
18737 text: &str,
18738 edits: Vec<(Range<Point>, String)>,
18739 include_deletions: bool,
18740 cx: &mut TestAppContext,
18741 assertion_fn: impl Fn(HighlightedText, &App),
18742) {
18743 let window = cx.add_window(|window, cx| {
18744 let buffer = MultiBuffer::build_simple(text, cx);
18745 Editor::new(EditorMode::full(), buffer, None, window, cx)
18746 });
18747 let cx = &mut VisualTestContext::from_window(*window, cx);
18748
18749 let (buffer, snapshot) = window
18750 .update(cx, |editor, _window, cx| {
18751 (
18752 editor.buffer().clone(),
18753 editor.buffer().read(cx).snapshot(cx),
18754 )
18755 })
18756 .unwrap();
18757
18758 let edits = edits
18759 .into_iter()
18760 .map(|(range, edit)| {
18761 (
18762 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
18763 edit,
18764 )
18765 })
18766 .collect::<Vec<_>>();
18767
18768 let text_anchor_edits = edits
18769 .clone()
18770 .into_iter()
18771 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
18772 .collect::<Vec<_>>();
18773
18774 let edit_preview = window
18775 .update(cx, |_, _window, cx| {
18776 buffer
18777 .read(cx)
18778 .as_singleton()
18779 .unwrap()
18780 .read(cx)
18781 .preview_edits(text_anchor_edits.into(), cx)
18782 })
18783 .unwrap()
18784 .await;
18785
18786 cx.update(|_window, cx| {
18787 let highlighted_edits = inline_completion_edit_text(
18788 &snapshot.as_singleton().unwrap().2,
18789 &edits,
18790 &edit_preview,
18791 include_deletions,
18792 cx,
18793 );
18794 assertion_fn(highlighted_edits, cx)
18795 });
18796}
18797
18798#[track_caller]
18799fn assert_breakpoint(
18800 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
18801 path: &Arc<Path>,
18802 expected: Vec<(u32, Breakpoint)>,
18803) {
18804 if expected.len() == 0usize {
18805 assert!(!breakpoints.contains_key(path), "{}", path.display());
18806 } else {
18807 let mut breakpoint = breakpoints
18808 .get(path)
18809 .unwrap()
18810 .into_iter()
18811 .map(|breakpoint| {
18812 (
18813 breakpoint.row,
18814 Breakpoint {
18815 message: breakpoint.message.clone(),
18816 state: breakpoint.state,
18817 condition: breakpoint.condition.clone(),
18818 hit_condition: breakpoint.hit_condition.clone(),
18819 },
18820 )
18821 })
18822 .collect::<Vec<_>>();
18823
18824 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
18825
18826 assert_eq!(expected, breakpoint);
18827 }
18828}
18829
18830fn add_log_breakpoint_at_cursor(
18831 editor: &mut Editor,
18832 log_message: &str,
18833 window: &mut Window,
18834 cx: &mut Context<Editor>,
18835) {
18836 let (anchor, bp) = editor
18837 .breakpoints_at_cursors(window, cx)
18838 .first()
18839 .and_then(|(anchor, bp)| {
18840 if let Some(bp) = bp {
18841 Some((*anchor, bp.clone()))
18842 } else {
18843 None
18844 }
18845 })
18846 .unwrap_or_else(|| {
18847 let cursor_position: Point = editor.selections.newest(cx).head();
18848
18849 let breakpoint_position = editor
18850 .snapshot(window, cx)
18851 .display_snapshot
18852 .buffer_snapshot
18853 .anchor_before(Point::new(cursor_position.row, 0));
18854
18855 (breakpoint_position, Breakpoint::new_log(&log_message))
18856 });
18857
18858 editor.edit_breakpoint_at_anchor(
18859 anchor,
18860 bp,
18861 BreakpointEditAction::EditLogMessage(log_message.into()),
18862 cx,
18863 );
18864}
18865
18866#[gpui::test]
18867async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
18868 init_test(cx, |_| {});
18869
18870 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
18871 let fs = FakeFs::new(cx.executor());
18872 fs.insert_tree(
18873 path!("/a"),
18874 json!({
18875 "main.rs": sample_text,
18876 }),
18877 )
18878 .await;
18879 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18880 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18881 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18882
18883 let fs = FakeFs::new(cx.executor());
18884 fs.insert_tree(
18885 path!("/a"),
18886 json!({
18887 "main.rs": sample_text,
18888 }),
18889 )
18890 .await;
18891 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18892 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18893 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18894 let worktree_id = workspace
18895 .update(cx, |workspace, _window, cx| {
18896 workspace.project().update(cx, |project, cx| {
18897 project.worktrees(cx).next().unwrap().read(cx).id()
18898 })
18899 })
18900 .unwrap();
18901
18902 let buffer = project
18903 .update(cx, |project, cx| {
18904 project.open_buffer((worktree_id, "main.rs"), cx)
18905 })
18906 .await
18907 .unwrap();
18908
18909 let (editor, cx) = cx.add_window_view(|window, cx| {
18910 Editor::new(
18911 EditorMode::full(),
18912 MultiBuffer::build_from_buffer(buffer, cx),
18913 Some(project.clone()),
18914 window,
18915 cx,
18916 )
18917 });
18918
18919 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
18920 let abs_path = project.read_with(cx, |project, cx| {
18921 project
18922 .absolute_path(&project_path, cx)
18923 .map(|path_buf| Arc::from(path_buf.to_owned()))
18924 .unwrap()
18925 });
18926
18927 // assert we can add breakpoint on the first line
18928 editor.update_in(cx, |editor, window, cx| {
18929 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18930 editor.move_to_end(&MoveToEnd, window, cx);
18931 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18932 });
18933
18934 let breakpoints = editor.update(cx, |editor, cx| {
18935 editor
18936 .breakpoint_store()
18937 .as_ref()
18938 .unwrap()
18939 .read(cx)
18940 .all_source_breakpoints(cx)
18941 .clone()
18942 });
18943
18944 assert_eq!(1, breakpoints.len());
18945 assert_breakpoint(
18946 &breakpoints,
18947 &abs_path,
18948 vec![
18949 (0, Breakpoint::new_standard()),
18950 (3, Breakpoint::new_standard()),
18951 ],
18952 );
18953
18954 editor.update_in(cx, |editor, window, cx| {
18955 editor.move_to_beginning(&MoveToBeginning, window, cx);
18956 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18957 });
18958
18959 let breakpoints = editor.update(cx, |editor, cx| {
18960 editor
18961 .breakpoint_store()
18962 .as_ref()
18963 .unwrap()
18964 .read(cx)
18965 .all_source_breakpoints(cx)
18966 .clone()
18967 });
18968
18969 assert_eq!(1, breakpoints.len());
18970 assert_breakpoint(
18971 &breakpoints,
18972 &abs_path,
18973 vec![(3, Breakpoint::new_standard())],
18974 );
18975
18976 editor.update_in(cx, |editor, window, cx| {
18977 editor.move_to_end(&MoveToEnd, window, cx);
18978 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18979 });
18980
18981 let breakpoints = editor.update(cx, |editor, cx| {
18982 editor
18983 .breakpoint_store()
18984 .as_ref()
18985 .unwrap()
18986 .read(cx)
18987 .all_source_breakpoints(cx)
18988 .clone()
18989 });
18990
18991 assert_eq!(0, breakpoints.len());
18992 assert_breakpoint(&breakpoints, &abs_path, vec![]);
18993}
18994
18995#[gpui::test]
18996async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
18997 init_test(cx, |_| {});
18998
18999 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
19000
19001 let fs = FakeFs::new(cx.executor());
19002 fs.insert_tree(
19003 path!("/a"),
19004 json!({
19005 "main.rs": sample_text,
19006 }),
19007 )
19008 .await;
19009 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19010 let (workspace, cx) =
19011 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
19012
19013 let worktree_id = workspace.update(cx, |workspace, cx| {
19014 workspace.project().update(cx, |project, cx| {
19015 project.worktrees(cx).next().unwrap().read(cx).id()
19016 })
19017 });
19018
19019 let buffer = project
19020 .update(cx, |project, cx| {
19021 project.open_buffer((worktree_id, "main.rs"), cx)
19022 })
19023 .await
19024 .unwrap();
19025
19026 let (editor, cx) = cx.add_window_view(|window, cx| {
19027 Editor::new(
19028 EditorMode::full(),
19029 MultiBuffer::build_from_buffer(buffer, cx),
19030 Some(project.clone()),
19031 window,
19032 cx,
19033 )
19034 });
19035
19036 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
19037 let abs_path = project.read_with(cx, |project, cx| {
19038 project
19039 .absolute_path(&project_path, cx)
19040 .map(|path_buf| Arc::from(path_buf.to_owned()))
19041 .unwrap()
19042 });
19043
19044 editor.update_in(cx, |editor, window, cx| {
19045 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
19046 });
19047
19048 let breakpoints = editor.update(cx, |editor, cx| {
19049 editor
19050 .breakpoint_store()
19051 .as_ref()
19052 .unwrap()
19053 .read(cx)
19054 .all_source_breakpoints(cx)
19055 .clone()
19056 });
19057
19058 assert_breakpoint(
19059 &breakpoints,
19060 &abs_path,
19061 vec![(0, Breakpoint::new_log("hello world"))],
19062 );
19063
19064 // Removing a log message from a log breakpoint should remove it
19065 editor.update_in(cx, |editor, window, cx| {
19066 add_log_breakpoint_at_cursor(editor, "", window, cx);
19067 });
19068
19069 let breakpoints = editor.update(cx, |editor, cx| {
19070 editor
19071 .breakpoint_store()
19072 .as_ref()
19073 .unwrap()
19074 .read(cx)
19075 .all_source_breakpoints(cx)
19076 .clone()
19077 });
19078
19079 assert_breakpoint(&breakpoints, &abs_path, vec![]);
19080
19081 editor.update_in(cx, |editor, window, cx| {
19082 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19083 editor.move_to_end(&MoveToEnd, window, cx);
19084 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19085 // Not adding a log message to a standard breakpoint shouldn't remove it
19086 add_log_breakpoint_at_cursor(editor, "", window, cx);
19087 });
19088
19089 let breakpoints = editor.update(cx, |editor, cx| {
19090 editor
19091 .breakpoint_store()
19092 .as_ref()
19093 .unwrap()
19094 .read(cx)
19095 .all_source_breakpoints(cx)
19096 .clone()
19097 });
19098
19099 assert_breakpoint(
19100 &breakpoints,
19101 &abs_path,
19102 vec![
19103 (0, Breakpoint::new_standard()),
19104 (3, Breakpoint::new_standard()),
19105 ],
19106 );
19107
19108 editor.update_in(cx, |editor, window, cx| {
19109 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
19110 });
19111
19112 let breakpoints = editor.update(cx, |editor, cx| {
19113 editor
19114 .breakpoint_store()
19115 .as_ref()
19116 .unwrap()
19117 .read(cx)
19118 .all_source_breakpoints(cx)
19119 .clone()
19120 });
19121
19122 assert_breakpoint(
19123 &breakpoints,
19124 &abs_path,
19125 vec![
19126 (0, Breakpoint::new_standard()),
19127 (3, Breakpoint::new_log("hello world")),
19128 ],
19129 );
19130
19131 editor.update_in(cx, |editor, window, cx| {
19132 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
19133 });
19134
19135 let breakpoints = editor.update(cx, |editor, cx| {
19136 editor
19137 .breakpoint_store()
19138 .as_ref()
19139 .unwrap()
19140 .read(cx)
19141 .all_source_breakpoints(cx)
19142 .clone()
19143 });
19144
19145 assert_breakpoint(
19146 &breakpoints,
19147 &abs_path,
19148 vec![
19149 (0, Breakpoint::new_standard()),
19150 (3, Breakpoint::new_log("hello Earth!!")),
19151 ],
19152 );
19153}
19154
19155/// This also tests that Editor::breakpoint_at_cursor_head is working properly
19156/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
19157/// or when breakpoints were placed out of order. This tests for a regression too
19158#[gpui::test]
19159async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
19160 init_test(cx, |_| {});
19161
19162 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
19163 let fs = FakeFs::new(cx.executor());
19164 fs.insert_tree(
19165 path!("/a"),
19166 json!({
19167 "main.rs": sample_text,
19168 }),
19169 )
19170 .await;
19171 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19172 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19173 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19174
19175 let fs = FakeFs::new(cx.executor());
19176 fs.insert_tree(
19177 path!("/a"),
19178 json!({
19179 "main.rs": sample_text,
19180 }),
19181 )
19182 .await;
19183 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19184 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19185 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19186 let worktree_id = workspace
19187 .update(cx, |workspace, _window, cx| {
19188 workspace.project().update(cx, |project, cx| {
19189 project.worktrees(cx).next().unwrap().read(cx).id()
19190 })
19191 })
19192 .unwrap();
19193
19194 let buffer = project
19195 .update(cx, |project, cx| {
19196 project.open_buffer((worktree_id, "main.rs"), cx)
19197 })
19198 .await
19199 .unwrap();
19200
19201 let (editor, cx) = cx.add_window_view(|window, cx| {
19202 Editor::new(
19203 EditorMode::full(),
19204 MultiBuffer::build_from_buffer(buffer, cx),
19205 Some(project.clone()),
19206 window,
19207 cx,
19208 )
19209 });
19210
19211 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
19212 let abs_path = project.read_with(cx, |project, cx| {
19213 project
19214 .absolute_path(&project_path, cx)
19215 .map(|path_buf| Arc::from(path_buf.to_owned()))
19216 .unwrap()
19217 });
19218
19219 // assert we can add breakpoint on the first line
19220 editor.update_in(cx, |editor, window, cx| {
19221 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19222 editor.move_to_end(&MoveToEnd, window, cx);
19223 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19224 editor.move_up(&MoveUp, window, cx);
19225 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19226 });
19227
19228 let breakpoints = editor.update(cx, |editor, cx| {
19229 editor
19230 .breakpoint_store()
19231 .as_ref()
19232 .unwrap()
19233 .read(cx)
19234 .all_source_breakpoints(cx)
19235 .clone()
19236 });
19237
19238 assert_eq!(1, breakpoints.len());
19239 assert_breakpoint(
19240 &breakpoints,
19241 &abs_path,
19242 vec![
19243 (0, Breakpoint::new_standard()),
19244 (2, Breakpoint::new_standard()),
19245 (3, Breakpoint::new_standard()),
19246 ],
19247 );
19248
19249 editor.update_in(cx, |editor, window, cx| {
19250 editor.move_to_beginning(&MoveToBeginning, window, cx);
19251 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
19252 editor.move_to_end(&MoveToEnd, window, cx);
19253 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
19254 // Disabling a breakpoint that doesn't exist should do nothing
19255 editor.move_up(&MoveUp, window, cx);
19256 editor.move_up(&MoveUp, window, cx);
19257 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
19258 });
19259
19260 let breakpoints = editor.update(cx, |editor, cx| {
19261 editor
19262 .breakpoint_store()
19263 .as_ref()
19264 .unwrap()
19265 .read(cx)
19266 .all_source_breakpoints(cx)
19267 .clone()
19268 });
19269
19270 let disable_breakpoint = {
19271 let mut bp = Breakpoint::new_standard();
19272 bp.state = BreakpointState::Disabled;
19273 bp
19274 };
19275
19276 assert_eq!(1, breakpoints.len());
19277 assert_breakpoint(
19278 &breakpoints,
19279 &abs_path,
19280 vec![
19281 (0, disable_breakpoint.clone()),
19282 (2, Breakpoint::new_standard()),
19283 (3, disable_breakpoint.clone()),
19284 ],
19285 );
19286
19287 editor.update_in(cx, |editor, window, cx| {
19288 editor.move_to_beginning(&MoveToBeginning, window, cx);
19289 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
19290 editor.move_to_end(&MoveToEnd, window, cx);
19291 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
19292 editor.move_up(&MoveUp, window, cx);
19293 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
19294 });
19295
19296 let breakpoints = editor.update(cx, |editor, cx| {
19297 editor
19298 .breakpoint_store()
19299 .as_ref()
19300 .unwrap()
19301 .read(cx)
19302 .all_source_breakpoints(cx)
19303 .clone()
19304 });
19305
19306 assert_eq!(1, breakpoints.len());
19307 assert_breakpoint(
19308 &breakpoints,
19309 &abs_path,
19310 vec![
19311 (0, Breakpoint::new_standard()),
19312 (2, disable_breakpoint),
19313 (3, Breakpoint::new_standard()),
19314 ],
19315 );
19316}
19317
19318#[gpui::test]
19319async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
19320 init_test(cx, |_| {});
19321 let capabilities = lsp::ServerCapabilities {
19322 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
19323 prepare_provider: Some(true),
19324 work_done_progress_options: Default::default(),
19325 })),
19326 ..Default::default()
19327 };
19328 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
19329
19330 cx.set_state(indoc! {"
19331 struct Fˇoo {}
19332 "});
19333
19334 cx.update_editor(|editor, _, cx| {
19335 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
19336 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
19337 editor.highlight_background::<DocumentHighlightRead>(
19338 &[highlight_range],
19339 |c| c.editor_document_highlight_read_background,
19340 cx,
19341 );
19342 });
19343
19344 let mut prepare_rename_handler = cx
19345 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
19346 move |_, _, _| async move {
19347 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
19348 start: lsp::Position {
19349 line: 0,
19350 character: 7,
19351 },
19352 end: lsp::Position {
19353 line: 0,
19354 character: 10,
19355 },
19356 })))
19357 },
19358 );
19359 let prepare_rename_task = cx
19360 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
19361 .expect("Prepare rename was not started");
19362 prepare_rename_handler.next().await.unwrap();
19363 prepare_rename_task.await.expect("Prepare rename failed");
19364
19365 let mut rename_handler =
19366 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
19367 let edit = lsp::TextEdit {
19368 range: lsp::Range {
19369 start: lsp::Position {
19370 line: 0,
19371 character: 7,
19372 },
19373 end: lsp::Position {
19374 line: 0,
19375 character: 10,
19376 },
19377 },
19378 new_text: "FooRenamed".to_string(),
19379 };
19380 Ok(Some(lsp::WorkspaceEdit::new(
19381 // Specify the same edit twice
19382 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
19383 )))
19384 });
19385 let rename_task = cx
19386 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
19387 .expect("Confirm rename was not started");
19388 rename_handler.next().await.unwrap();
19389 rename_task.await.expect("Confirm rename failed");
19390 cx.run_until_parked();
19391
19392 // Despite two edits, only one is actually applied as those are identical
19393 cx.assert_editor_state(indoc! {"
19394 struct FooRenamedˇ {}
19395 "});
19396}
19397
19398#[gpui::test]
19399async fn test_rename_without_prepare(cx: &mut TestAppContext) {
19400 init_test(cx, |_| {});
19401 // These capabilities indicate that the server does not support prepare rename.
19402 let capabilities = lsp::ServerCapabilities {
19403 rename_provider: Some(lsp::OneOf::Left(true)),
19404 ..Default::default()
19405 };
19406 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
19407
19408 cx.set_state(indoc! {"
19409 struct Fˇoo {}
19410 "});
19411
19412 cx.update_editor(|editor, _window, cx| {
19413 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
19414 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
19415 editor.highlight_background::<DocumentHighlightRead>(
19416 &[highlight_range],
19417 |c| c.editor_document_highlight_read_background,
19418 cx,
19419 );
19420 });
19421
19422 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
19423 .expect("Prepare rename was not started")
19424 .await
19425 .expect("Prepare rename failed");
19426
19427 let mut rename_handler =
19428 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
19429 let edit = lsp::TextEdit {
19430 range: lsp::Range {
19431 start: lsp::Position {
19432 line: 0,
19433 character: 7,
19434 },
19435 end: lsp::Position {
19436 line: 0,
19437 character: 10,
19438 },
19439 },
19440 new_text: "FooRenamed".to_string(),
19441 };
19442 Ok(Some(lsp::WorkspaceEdit::new(
19443 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
19444 )))
19445 });
19446 let rename_task = cx
19447 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
19448 .expect("Confirm rename was not started");
19449 rename_handler.next().await.unwrap();
19450 rename_task.await.expect("Confirm rename failed");
19451 cx.run_until_parked();
19452
19453 // Correct range is renamed, as `surrounding_word` is used to find it.
19454 cx.assert_editor_state(indoc! {"
19455 struct FooRenamedˇ {}
19456 "});
19457}
19458
19459#[gpui::test]
19460async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
19461 init_test(cx, |_| {});
19462 let mut cx = EditorTestContext::new(cx).await;
19463
19464 let language = Arc::new(
19465 Language::new(
19466 LanguageConfig::default(),
19467 Some(tree_sitter_html::LANGUAGE.into()),
19468 )
19469 .with_brackets_query(
19470 r#"
19471 ("<" @open "/>" @close)
19472 ("</" @open ">" @close)
19473 ("<" @open ">" @close)
19474 ("\"" @open "\"" @close)
19475 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
19476 "#,
19477 )
19478 .unwrap(),
19479 );
19480 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
19481
19482 cx.set_state(indoc! {"
19483 <span>ˇ</span>
19484 "});
19485 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
19486 cx.assert_editor_state(indoc! {"
19487 <span>
19488 ˇ
19489 </span>
19490 "});
19491
19492 cx.set_state(indoc! {"
19493 <span><span></span>ˇ</span>
19494 "});
19495 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
19496 cx.assert_editor_state(indoc! {"
19497 <span><span></span>
19498 ˇ</span>
19499 "});
19500
19501 cx.set_state(indoc! {"
19502 <span>ˇ
19503 </span>
19504 "});
19505 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
19506 cx.assert_editor_state(indoc! {"
19507 <span>
19508 ˇ
19509 </span>
19510 "});
19511}
19512
19513#[gpui::test(iterations = 10)]
19514async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
19515 init_test(cx, |_| {});
19516
19517 let fs = FakeFs::new(cx.executor());
19518 fs.insert_tree(
19519 path!("/dir"),
19520 json!({
19521 "a.ts": "a",
19522 }),
19523 )
19524 .await;
19525
19526 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
19527 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19528 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19529
19530 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
19531 language_registry.add(Arc::new(Language::new(
19532 LanguageConfig {
19533 name: "TypeScript".into(),
19534 matcher: LanguageMatcher {
19535 path_suffixes: vec!["ts".to_string()],
19536 ..Default::default()
19537 },
19538 ..Default::default()
19539 },
19540 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
19541 )));
19542 let mut fake_language_servers = language_registry.register_fake_lsp(
19543 "TypeScript",
19544 FakeLspAdapter {
19545 capabilities: lsp::ServerCapabilities {
19546 code_lens_provider: Some(lsp::CodeLensOptions {
19547 resolve_provider: Some(true),
19548 }),
19549 execute_command_provider: Some(lsp::ExecuteCommandOptions {
19550 commands: vec!["_the/command".to_string()],
19551 ..lsp::ExecuteCommandOptions::default()
19552 }),
19553 ..lsp::ServerCapabilities::default()
19554 },
19555 ..FakeLspAdapter::default()
19556 },
19557 );
19558
19559 let (buffer, _handle) = project
19560 .update(cx, |p, cx| {
19561 p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
19562 })
19563 .await
19564 .unwrap();
19565 cx.executor().run_until_parked();
19566
19567 let fake_server = fake_language_servers.next().await.unwrap();
19568
19569 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
19570 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
19571 drop(buffer_snapshot);
19572 let actions = cx
19573 .update_window(*workspace, |_, window, cx| {
19574 project.code_actions(&buffer, anchor..anchor, window, cx)
19575 })
19576 .unwrap();
19577
19578 fake_server
19579 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
19580 Ok(Some(vec![
19581 lsp::CodeLens {
19582 range: lsp::Range::default(),
19583 command: Some(lsp::Command {
19584 title: "Code lens command".to_owned(),
19585 command: "_the/command".to_owned(),
19586 arguments: None,
19587 }),
19588 data: None,
19589 },
19590 lsp::CodeLens {
19591 range: lsp::Range::default(),
19592 command: Some(lsp::Command {
19593 title: "Command not in capabilities".to_owned(),
19594 command: "not in capabilities".to_owned(),
19595 arguments: None,
19596 }),
19597 data: None,
19598 },
19599 lsp::CodeLens {
19600 range: lsp::Range {
19601 start: lsp::Position {
19602 line: 1,
19603 character: 1,
19604 },
19605 end: lsp::Position {
19606 line: 1,
19607 character: 1,
19608 },
19609 },
19610 command: Some(lsp::Command {
19611 title: "Command not in range".to_owned(),
19612 command: "_the/command".to_owned(),
19613 arguments: None,
19614 }),
19615 data: None,
19616 },
19617 ]))
19618 })
19619 .next()
19620 .await;
19621
19622 let actions = actions.await.unwrap();
19623 assert_eq!(
19624 actions.len(),
19625 1,
19626 "Should have only one valid action for the 0..0 range"
19627 );
19628 let action = actions[0].clone();
19629 let apply = project.update(cx, |project, cx| {
19630 project.apply_code_action(buffer.clone(), action, true, cx)
19631 });
19632
19633 // Resolving the code action does not populate its edits. In absence of
19634 // edits, we must execute the given command.
19635 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
19636 |mut lens, _| async move {
19637 let lens_command = lens.command.as_mut().expect("should have a command");
19638 assert_eq!(lens_command.title, "Code lens command");
19639 lens_command.arguments = Some(vec![json!("the-argument")]);
19640 Ok(lens)
19641 },
19642 );
19643
19644 // While executing the command, the language server sends the editor
19645 // a `workspaceEdit` request.
19646 fake_server
19647 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
19648 let fake = fake_server.clone();
19649 move |params, _| {
19650 assert_eq!(params.command, "_the/command");
19651 let fake = fake.clone();
19652 async move {
19653 fake.server
19654 .request::<lsp::request::ApplyWorkspaceEdit>(
19655 lsp::ApplyWorkspaceEditParams {
19656 label: None,
19657 edit: lsp::WorkspaceEdit {
19658 changes: Some(
19659 [(
19660 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
19661 vec![lsp::TextEdit {
19662 range: lsp::Range::new(
19663 lsp::Position::new(0, 0),
19664 lsp::Position::new(0, 0),
19665 ),
19666 new_text: "X".into(),
19667 }],
19668 )]
19669 .into_iter()
19670 .collect(),
19671 ),
19672 ..Default::default()
19673 },
19674 },
19675 )
19676 .await
19677 .into_response()
19678 .unwrap();
19679 Ok(Some(json!(null)))
19680 }
19681 }
19682 })
19683 .next()
19684 .await;
19685
19686 // Applying the code lens command returns a project transaction containing the edits
19687 // sent by the language server in its `workspaceEdit` request.
19688 let transaction = apply.await.unwrap();
19689 assert!(transaction.0.contains_key(&buffer));
19690 buffer.update(cx, |buffer, cx| {
19691 assert_eq!(buffer.text(), "Xa");
19692 buffer.undo(cx);
19693 assert_eq!(buffer.text(), "a");
19694 });
19695}
19696
19697#[gpui::test]
19698async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
19699 init_test(cx, |_| {});
19700
19701 let fs = FakeFs::new(cx.executor());
19702 let main_text = r#"fn main() {
19703println!("1");
19704println!("2");
19705println!("3");
19706println!("4");
19707println!("5");
19708}"#;
19709 let lib_text = "mod foo {}";
19710 fs.insert_tree(
19711 path!("/a"),
19712 json!({
19713 "lib.rs": lib_text,
19714 "main.rs": main_text,
19715 }),
19716 )
19717 .await;
19718
19719 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19720 let (workspace, cx) =
19721 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
19722 let worktree_id = workspace.update(cx, |workspace, cx| {
19723 workspace.project().update(cx, |project, cx| {
19724 project.worktrees(cx).next().unwrap().read(cx).id()
19725 })
19726 });
19727
19728 let expected_ranges = vec![
19729 Point::new(0, 0)..Point::new(0, 0),
19730 Point::new(1, 0)..Point::new(1, 1),
19731 Point::new(2, 0)..Point::new(2, 2),
19732 Point::new(3, 0)..Point::new(3, 3),
19733 ];
19734
19735 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
19736 let editor_1 = workspace
19737 .update_in(cx, |workspace, window, cx| {
19738 workspace.open_path(
19739 (worktree_id, "main.rs"),
19740 Some(pane_1.downgrade()),
19741 true,
19742 window,
19743 cx,
19744 )
19745 })
19746 .unwrap()
19747 .await
19748 .downcast::<Editor>()
19749 .unwrap();
19750 pane_1.update(cx, |pane, cx| {
19751 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19752 open_editor.update(cx, |editor, cx| {
19753 assert_eq!(
19754 editor.display_text(cx),
19755 main_text,
19756 "Original main.rs text on initial open",
19757 );
19758 assert_eq!(
19759 editor
19760 .selections
19761 .all::<Point>(cx)
19762 .into_iter()
19763 .map(|s| s.range())
19764 .collect::<Vec<_>>(),
19765 vec![Point::zero()..Point::zero()],
19766 "Default selections on initial open",
19767 );
19768 })
19769 });
19770 editor_1.update_in(cx, |editor, window, cx| {
19771 editor.change_selections(None, window, cx, |s| {
19772 s.select_ranges(expected_ranges.clone());
19773 });
19774 });
19775
19776 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
19777 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
19778 });
19779 let editor_2 = workspace
19780 .update_in(cx, |workspace, window, cx| {
19781 workspace.open_path(
19782 (worktree_id, "main.rs"),
19783 Some(pane_2.downgrade()),
19784 true,
19785 window,
19786 cx,
19787 )
19788 })
19789 .unwrap()
19790 .await
19791 .downcast::<Editor>()
19792 .unwrap();
19793 pane_2.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 in another panel",
19800 );
19801 assert_eq!(
19802 editor
19803 .selections
19804 .all::<Point>(cx)
19805 .into_iter()
19806 .map(|s| s.range())
19807 .collect::<Vec<_>>(),
19808 vec![Point::zero()..Point::zero()],
19809 "Default selections on initial open in another panel",
19810 );
19811 })
19812 });
19813
19814 editor_2.update_in(cx, |editor, window, cx| {
19815 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
19816 });
19817
19818 let _other_editor_1 = workspace
19819 .update_in(cx, |workspace, window, cx| {
19820 workspace.open_path(
19821 (worktree_id, "lib.rs"),
19822 Some(pane_1.downgrade()),
19823 true,
19824 window,
19825 cx,
19826 )
19827 })
19828 .unwrap()
19829 .await
19830 .downcast::<Editor>()
19831 .unwrap();
19832 pane_1
19833 .update_in(cx, |pane, window, cx| {
19834 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
19835 .unwrap()
19836 })
19837 .await
19838 .unwrap();
19839 drop(editor_1);
19840 pane_1.update(cx, |pane, cx| {
19841 pane.active_item()
19842 .unwrap()
19843 .downcast::<Editor>()
19844 .unwrap()
19845 .update(cx, |editor, cx| {
19846 assert_eq!(
19847 editor.display_text(cx),
19848 lib_text,
19849 "Other file should be open and active",
19850 );
19851 });
19852 assert_eq!(pane.items().count(), 1, "No other editors should be open");
19853 });
19854
19855 let _other_editor_2 = workspace
19856 .update_in(cx, |workspace, window, cx| {
19857 workspace.open_path(
19858 (worktree_id, "lib.rs"),
19859 Some(pane_2.downgrade()),
19860 true,
19861 window,
19862 cx,
19863 )
19864 })
19865 .unwrap()
19866 .await
19867 .downcast::<Editor>()
19868 .unwrap();
19869 pane_2
19870 .update_in(cx, |pane, window, cx| {
19871 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
19872 .unwrap()
19873 })
19874 .await
19875 .unwrap();
19876 drop(editor_2);
19877 pane_2.update(cx, |pane, cx| {
19878 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19879 open_editor.update(cx, |editor, cx| {
19880 assert_eq!(
19881 editor.display_text(cx),
19882 lib_text,
19883 "Other file should be open and active in another panel too",
19884 );
19885 });
19886 assert_eq!(
19887 pane.items().count(),
19888 1,
19889 "No other editors should be open in another pane",
19890 );
19891 });
19892
19893 let _editor_1_reopened = workspace
19894 .update_in(cx, |workspace, window, cx| {
19895 workspace.open_path(
19896 (worktree_id, "main.rs"),
19897 Some(pane_1.downgrade()),
19898 true,
19899 window,
19900 cx,
19901 )
19902 })
19903 .unwrap()
19904 .await
19905 .downcast::<Editor>()
19906 .unwrap();
19907 let _editor_2_reopened = workspace
19908 .update_in(cx, |workspace, window, cx| {
19909 workspace.open_path(
19910 (worktree_id, "main.rs"),
19911 Some(pane_2.downgrade()),
19912 true,
19913 window,
19914 cx,
19915 )
19916 })
19917 .unwrap()
19918 .await
19919 .downcast::<Editor>()
19920 .unwrap();
19921 pane_1.update(cx, |pane, cx| {
19922 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19923 open_editor.update(cx, |editor, cx| {
19924 assert_eq!(
19925 editor.display_text(cx),
19926 main_text,
19927 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
19928 );
19929 assert_eq!(
19930 editor
19931 .selections
19932 .all::<Point>(cx)
19933 .into_iter()
19934 .map(|s| s.range())
19935 .collect::<Vec<_>>(),
19936 expected_ranges,
19937 "Previous editor in the 1st panel had selections and should get them restored on reopen",
19938 );
19939 })
19940 });
19941 pane_2.update(cx, |pane, cx| {
19942 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19943 open_editor.update(cx, |editor, cx| {
19944 assert_eq!(
19945 editor.display_text(cx),
19946 r#"fn main() {
19947⋯rintln!("1");
19948⋯intln!("2");
19949⋯ntln!("3");
19950println!("4");
19951println!("5");
19952}"#,
19953 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
19954 );
19955 assert_eq!(
19956 editor
19957 .selections
19958 .all::<Point>(cx)
19959 .into_iter()
19960 .map(|s| s.range())
19961 .collect::<Vec<_>>(),
19962 vec![Point::zero()..Point::zero()],
19963 "Previous editor in the 2nd pane had no selections changed hence should restore none",
19964 );
19965 })
19966 });
19967}
19968
19969#[gpui::test]
19970async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
19971 init_test(cx, |_| {});
19972
19973 let fs = FakeFs::new(cx.executor());
19974 let main_text = r#"fn main() {
19975println!("1");
19976println!("2");
19977println!("3");
19978println!("4");
19979println!("5");
19980}"#;
19981 let lib_text = "mod foo {}";
19982 fs.insert_tree(
19983 path!("/a"),
19984 json!({
19985 "lib.rs": lib_text,
19986 "main.rs": main_text,
19987 }),
19988 )
19989 .await;
19990
19991 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19992 let (workspace, cx) =
19993 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
19994 let worktree_id = workspace.update(cx, |workspace, cx| {
19995 workspace.project().update(cx, |project, cx| {
19996 project.worktrees(cx).next().unwrap().read(cx).id()
19997 })
19998 });
19999
20000 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
20001 let editor = workspace
20002 .update_in(cx, |workspace, window, cx| {
20003 workspace.open_path(
20004 (worktree_id, "main.rs"),
20005 Some(pane.downgrade()),
20006 true,
20007 window,
20008 cx,
20009 )
20010 })
20011 .unwrap()
20012 .await
20013 .downcast::<Editor>()
20014 .unwrap();
20015 pane.update(cx, |pane, cx| {
20016 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20017 open_editor.update(cx, |editor, cx| {
20018 assert_eq!(
20019 editor.display_text(cx),
20020 main_text,
20021 "Original main.rs text on initial open",
20022 );
20023 })
20024 });
20025 editor.update_in(cx, |editor, window, cx| {
20026 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
20027 });
20028
20029 cx.update_global(|store: &mut SettingsStore, cx| {
20030 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
20031 s.restore_on_file_reopen = Some(false);
20032 });
20033 });
20034 editor.update_in(cx, |editor, window, cx| {
20035 editor.fold_ranges(
20036 vec![
20037 Point::new(1, 0)..Point::new(1, 1),
20038 Point::new(2, 0)..Point::new(2, 2),
20039 Point::new(3, 0)..Point::new(3, 3),
20040 ],
20041 false,
20042 window,
20043 cx,
20044 );
20045 });
20046 pane.update_in(cx, |pane, window, cx| {
20047 pane.close_all_items(&CloseAllItems::default(), window, cx)
20048 .unwrap()
20049 })
20050 .await
20051 .unwrap();
20052 pane.update(cx, |pane, _| {
20053 assert!(pane.active_item().is_none());
20054 });
20055 cx.update_global(|store: &mut SettingsStore, cx| {
20056 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
20057 s.restore_on_file_reopen = Some(true);
20058 });
20059 });
20060
20061 let _editor_reopened = workspace
20062 .update_in(cx, |workspace, window, cx| {
20063 workspace.open_path(
20064 (worktree_id, "main.rs"),
20065 Some(pane.downgrade()),
20066 true,
20067 window,
20068 cx,
20069 )
20070 })
20071 .unwrap()
20072 .await
20073 .downcast::<Editor>()
20074 .unwrap();
20075 pane.update(cx, |pane, cx| {
20076 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20077 open_editor.update(cx, |editor, cx| {
20078 assert_eq!(
20079 editor.display_text(cx),
20080 main_text,
20081 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
20082 );
20083 })
20084 });
20085}
20086
20087#[gpui::test]
20088async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
20089 struct EmptyModalView {
20090 focus_handle: gpui::FocusHandle,
20091 }
20092 impl EventEmitter<DismissEvent> for EmptyModalView {}
20093 impl Render for EmptyModalView {
20094 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
20095 div()
20096 }
20097 }
20098 impl Focusable for EmptyModalView {
20099 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
20100 self.focus_handle.clone()
20101 }
20102 }
20103 impl workspace::ModalView for EmptyModalView {}
20104 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
20105 EmptyModalView {
20106 focus_handle: cx.focus_handle(),
20107 }
20108 }
20109
20110 init_test(cx, |_| {});
20111
20112 let fs = FakeFs::new(cx.executor());
20113 let project = Project::test(fs, [], cx).await;
20114 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20115 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
20116 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20117 let editor = cx.new_window_entity(|window, cx| {
20118 Editor::new(
20119 EditorMode::full(),
20120 buffer,
20121 Some(project.clone()),
20122 window,
20123 cx,
20124 )
20125 });
20126 workspace
20127 .update(cx, |workspace, window, cx| {
20128 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
20129 })
20130 .unwrap();
20131 editor.update_in(cx, |editor, window, cx| {
20132 editor.open_context_menu(&OpenContextMenu, window, cx);
20133 assert!(editor.mouse_context_menu.is_some());
20134 });
20135 workspace
20136 .update(cx, |workspace, window, cx| {
20137 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
20138 })
20139 .unwrap();
20140 cx.read(|cx| {
20141 assert!(editor.read(cx).mouse_context_menu.is_none());
20142 });
20143}
20144
20145#[gpui::test]
20146async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
20147 init_test(cx, |_| {});
20148
20149 let fs = FakeFs::new(cx.executor());
20150 fs.insert_file(path!("/file.html"), Default::default())
20151 .await;
20152
20153 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
20154
20155 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
20156 let html_language = Arc::new(Language::new(
20157 LanguageConfig {
20158 name: "HTML".into(),
20159 matcher: LanguageMatcher {
20160 path_suffixes: vec!["html".to_string()],
20161 ..LanguageMatcher::default()
20162 },
20163 brackets: BracketPairConfig {
20164 pairs: vec![BracketPair {
20165 start: "<".into(),
20166 end: ">".into(),
20167 close: true,
20168 ..Default::default()
20169 }],
20170 ..Default::default()
20171 },
20172 ..Default::default()
20173 },
20174 Some(tree_sitter_html::LANGUAGE.into()),
20175 ));
20176 language_registry.add(html_language);
20177 let mut fake_servers = language_registry.register_fake_lsp(
20178 "HTML",
20179 FakeLspAdapter {
20180 capabilities: lsp::ServerCapabilities {
20181 completion_provider: Some(lsp::CompletionOptions {
20182 resolve_provider: Some(true),
20183 ..Default::default()
20184 }),
20185 ..Default::default()
20186 },
20187 ..Default::default()
20188 },
20189 );
20190
20191 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20192 let cx = &mut VisualTestContext::from_window(*workspace, cx);
20193
20194 let worktree_id = workspace
20195 .update(cx, |workspace, _window, cx| {
20196 workspace.project().update(cx, |project, cx| {
20197 project.worktrees(cx).next().unwrap().read(cx).id()
20198 })
20199 })
20200 .unwrap();
20201 project
20202 .update(cx, |project, cx| {
20203 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
20204 })
20205 .await
20206 .unwrap();
20207 let editor = workspace
20208 .update(cx, |workspace, window, cx| {
20209 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
20210 })
20211 .unwrap()
20212 .await
20213 .unwrap()
20214 .downcast::<Editor>()
20215 .unwrap();
20216
20217 let fake_server = fake_servers.next().await.unwrap();
20218 editor.update_in(cx, |editor, window, cx| {
20219 editor.set_text("<ad></ad>", window, cx);
20220 editor.change_selections(None, window, cx, |selections| {
20221 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
20222 });
20223 let Some((buffer, _)) = editor
20224 .buffer
20225 .read(cx)
20226 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
20227 else {
20228 panic!("Failed to get buffer for selection position");
20229 };
20230 let buffer = buffer.read(cx);
20231 let buffer_id = buffer.remote_id();
20232 let opening_range =
20233 buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3));
20234 let closing_range =
20235 buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
20236 let mut linked_ranges = HashMap::default();
20237 linked_ranges.insert(
20238 buffer_id,
20239 vec![(opening_range.clone(), vec![closing_range.clone()])],
20240 );
20241 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
20242 });
20243 let mut completion_handle =
20244 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
20245 Ok(Some(lsp::CompletionResponse::Array(vec![
20246 lsp::CompletionItem {
20247 label: "head".to_string(),
20248 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
20249 lsp::InsertReplaceEdit {
20250 new_text: "head".to_string(),
20251 insert: lsp::Range::new(
20252 lsp::Position::new(0, 1),
20253 lsp::Position::new(0, 3),
20254 ),
20255 replace: lsp::Range::new(
20256 lsp::Position::new(0, 1),
20257 lsp::Position::new(0, 3),
20258 ),
20259 },
20260 )),
20261 ..Default::default()
20262 },
20263 ])))
20264 });
20265 editor.update_in(cx, |editor, window, cx| {
20266 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
20267 });
20268 cx.run_until_parked();
20269 completion_handle.next().await.unwrap();
20270 editor.update(cx, |editor, _| {
20271 assert!(
20272 editor.context_menu_visible(),
20273 "Completion menu should be visible"
20274 );
20275 });
20276 editor.update_in(cx, |editor, window, cx| {
20277 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
20278 });
20279 cx.executor().run_until_parked();
20280 editor.update(cx, |editor, cx| {
20281 assert_eq!(editor.text(cx), "<head></head>");
20282 });
20283}
20284
20285#[gpui::test]
20286async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
20287 init_test(cx, |_| {});
20288
20289 let fs = FakeFs::new(cx.executor());
20290 fs.insert_tree(
20291 path!("/root"),
20292 json!({
20293 "a": {
20294 "main.rs": "fn main() {}",
20295 },
20296 "foo": {
20297 "bar": {
20298 "external_file.rs": "pub mod external {}",
20299 }
20300 }
20301 }),
20302 )
20303 .await;
20304
20305 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
20306 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
20307 language_registry.add(rust_lang());
20308 let _fake_servers = language_registry.register_fake_lsp(
20309 "Rust",
20310 FakeLspAdapter {
20311 ..FakeLspAdapter::default()
20312 },
20313 );
20314 let (workspace, cx) =
20315 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20316 let worktree_id = workspace.update(cx, |workspace, cx| {
20317 workspace.project().update(cx, |project, cx| {
20318 project.worktrees(cx).next().unwrap().read(cx).id()
20319 })
20320 });
20321
20322 let assert_language_servers_count =
20323 |expected: usize, context: &str, cx: &mut VisualTestContext| {
20324 project.update(cx, |project, cx| {
20325 let current = project
20326 .lsp_store()
20327 .read(cx)
20328 .as_local()
20329 .unwrap()
20330 .language_servers
20331 .len();
20332 assert_eq!(expected, current, "{context}");
20333 });
20334 };
20335
20336 assert_language_servers_count(
20337 0,
20338 "No servers should be running before any file is open",
20339 cx,
20340 );
20341 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
20342 let main_editor = workspace
20343 .update_in(cx, |workspace, window, cx| {
20344 workspace.open_path(
20345 (worktree_id, "main.rs"),
20346 Some(pane.downgrade()),
20347 true,
20348 window,
20349 cx,
20350 )
20351 })
20352 .unwrap()
20353 .await
20354 .downcast::<Editor>()
20355 .unwrap();
20356 pane.update(cx, |pane, cx| {
20357 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20358 open_editor.update(cx, |editor, cx| {
20359 assert_eq!(
20360 editor.display_text(cx),
20361 "fn main() {}",
20362 "Original main.rs text on initial open",
20363 );
20364 });
20365 assert_eq!(open_editor, main_editor);
20366 });
20367 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
20368
20369 let external_editor = workspace
20370 .update_in(cx, |workspace, window, cx| {
20371 workspace.open_abs_path(
20372 PathBuf::from("/root/foo/bar/external_file.rs"),
20373 OpenOptions::default(),
20374 window,
20375 cx,
20376 )
20377 })
20378 .await
20379 .expect("opening external file")
20380 .downcast::<Editor>()
20381 .expect("downcasted external file's open element to editor");
20382 pane.update(cx, |pane, cx| {
20383 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20384 open_editor.update(cx, |editor, cx| {
20385 assert_eq!(
20386 editor.display_text(cx),
20387 "pub mod external {}",
20388 "External file is open now",
20389 );
20390 });
20391 assert_eq!(open_editor, external_editor);
20392 });
20393 assert_language_servers_count(
20394 1,
20395 "Second, external, *.rs file should join the existing server",
20396 cx,
20397 );
20398
20399 pane.update_in(cx, |pane, window, cx| {
20400 pane.close_active_item(&CloseActiveItem::default(), window, cx)
20401 })
20402 .unwrap()
20403 .await
20404 .unwrap();
20405 pane.update_in(cx, |pane, window, cx| {
20406 pane.navigate_backward(window, cx);
20407 });
20408 cx.run_until_parked();
20409 pane.update(cx, |pane, cx| {
20410 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20411 open_editor.update(cx, |editor, cx| {
20412 assert_eq!(
20413 editor.display_text(cx),
20414 "pub mod external {}",
20415 "External file is open now",
20416 );
20417 });
20418 });
20419 assert_language_servers_count(
20420 1,
20421 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
20422 cx,
20423 );
20424
20425 cx.update(|_, cx| {
20426 workspace::reload(&workspace::Reload::default(), cx);
20427 });
20428 assert_language_servers_count(
20429 1,
20430 "After reloading the worktree with local and external files opened, only one project should be started",
20431 cx,
20432 );
20433}
20434
20435#[gpui::test]
20436async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
20437 init_test(cx, |_| {});
20438
20439 let mut cx = EditorTestContext::new(cx).await;
20440 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
20441 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
20442
20443 // test cursor move to start of each line on tab
20444 // for `if`, `elif`, `else`, `while`, `with` and `for`
20445 cx.set_state(indoc! {"
20446 def main():
20447 ˇ for item in items:
20448 ˇ while item.active:
20449 ˇ if item.value > 10:
20450 ˇ continue
20451 ˇ elif item.value < 0:
20452 ˇ break
20453 ˇ else:
20454 ˇ with item.context() as ctx:
20455 ˇ yield count
20456 ˇ else:
20457 ˇ log('while else')
20458 ˇ else:
20459 ˇ log('for else')
20460 "});
20461 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
20462 cx.assert_editor_state(indoc! {"
20463 def main():
20464 ˇfor item in items:
20465 ˇwhile item.active:
20466 ˇif item.value > 10:
20467 ˇcontinue
20468 ˇelif item.value < 0:
20469 ˇbreak
20470 ˇelse:
20471 ˇwith item.context() as ctx:
20472 ˇyield count
20473 ˇelse:
20474 ˇlog('while else')
20475 ˇelse:
20476 ˇlog('for else')
20477 "});
20478 // test relative indent is preserved when tab
20479 // for `if`, `elif`, `else`, `while`, `with` and `for`
20480 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
20481 cx.assert_editor_state(indoc! {"
20482 def main():
20483 ˇfor item in items:
20484 ˇwhile item.active:
20485 ˇif item.value > 10:
20486 ˇcontinue
20487 ˇelif item.value < 0:
20488 ˇbreak
20489 ˇelse:
20490 ˇwith item.context() as ctx:
20491 ˇyield count
20492 ˇelse:
20493 ˇlog('while else')
20494 ˇelse:
20495 ˇlog('for else')
20496 "});
20497
20498 // test cursor move to start of each line on tab
20499 // for `try`, `except`, `else`, `finally`, `match` and `def`
20500 cx.set_state(indoc! {"
20501 def main():
20502 ˇ try:
20503 ˇ fetch()
20504 ˇ except ValueError:
20505 ˇ handle_error()
20506 ˇ else:
20507 ˇ match value:
20508 ˇ case _:
20509 ˇ finally:
20510 ˇ def status():
20511 ˇ return 0
20512 "});
20513 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
20514 cx.assert_editor_state(indoc! {"
20515 def main():
20516 ˇtry:
20517 ˇfetch()
20518 ˇexcept ValueError:
20519 ˇhandle_error()
20520 ˇelse:
20521 ˇmatch value:
20522 ˇcase _:
20523 ˇfinally:
20524 ˇdef status():
20525 ˇreturn 0
20526 "});
20527 // test relative indent is preserved when tab
20528 // for `try`, `except`, `else`, `finally`, `match` and `def`
20529 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
20530 cx.assert_editor_state(indoc! {"
20531 def main():
20532 ˇtry:
20533 ˇfetch()
20534 ˇexcept ValueError:
20535 ˇhandle_error()
20536 ˇelse:
20537 ˇmatch value:
20538 ˇcase _:
20539 ˇfinally:
20540 ˇdef status():
20541 ˇreturn 0
20542 "});
20543}
20544
20545#[gpui::test]
20546async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
20547 init_test(cx, |_| {});
20548
20549 let mut cx = EditorTestContext::new(cx).await;
20550 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
20551 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
20552
20553 // test `else` auto outdents when typed inside `if` block
20554 cx.set_state(indoc! {"
20555 def main():
20556 if i == 2:
20557 return
20558 ˇ
20559 "});
20560 cx.update_editor(|editor, window, cx| {
20561 editor.handle_input("else:", window, cx);
20562 });
20563 cx.assert_editor_state(indoc! {"
20564 def main():
20565 if i == 2:
20566 return
20567 else:ˇ
20568 "});
20569
20570 // test `except` auto outdents when typed inside `try` block
20571 cx.set_state(indoc! {"
20572 def main():
20573 try:
20574 i = 2
20575 ˇ
20576 "});
20577 cx.update_editor(|editor, window, cx| {
20578 editor.handle_input("except:", window, cx);
20579 });
20580 cx.assert_editor_state(indoc! {"
20581 def main():
20582 try:
20583 i = 2
20584 except:ˇ
20585 "});
20586
20587 // test `else` auto outdents when typed inside `except` block
20588 cx.set_state(indoc! {"
20589 def main():
20590 try:
20591 i = 2
20592 except:
20593 j = 2
20594 ˇ
20595 "});
20596 cx.update_editor(|editor, window, cx| {
20597 editor.handle_input("else:", window, cx);
20598 });
20599 cx.assert_editor_state(indoc! {"
20600 def main():
20601 try:
20602 i = 2
20603 except:
20604 j = 2
20605 else:ˇ
20606 "});
20607
20608 // test `finally` auto outdents when typed inside `else` block
20609 cx.set_state(indoc! {"
20610 def main():
20611 try:
20612 i = 2
20613 except:
20614 j = 2
20615 else:
20616 k = 2
20617 ˇ
20618 "});
20619 cx.update_editor(|editor, window, cx| {
20620 editor.handle_input("finally:", window, cx);
20621 });
20622 cx.assert_editor_state(indoc! {"
20623 def main():
20624 try:
20625 i = 2
20626 except:
20627 j = 2
20628 else:
20629 k = 2
20630 finally:ˇ
20631 "});
20632
20633 // TODO: test `except` auto outdents when typed inside `try` block right after for block
20634 // cx.set_state(indoc! {"
20635 // def main():
20636 // try:
20637 // for i in range(n):
20638 // pass
20639 // ˇ
20640 // "});
20641 // cx.update_editor(|editor, window, cx| {
20642 // editor.handle_input("except:", window, cx);
20643 // });
20644 // cx.assert_editor_state(indoc! {"
20645 // def main():
20646 // try:
20647 // for i in range(n):
20648 // pass
20649 // except:ˇ
20650 // "});
20651
20652 // TODO: test `else` auto outdents when typed inside `except` block right after for block
20653 // cx.set_state(indoc! {"
20654 // def main():
20655 // try:
20656 // i = 2
20657 // except:
20658 // for i in range(n):
20659 // pass
20660 // ˇ
20661 // "});
20662 // cx.update_editor(|editor, window, cx| {
20663 // editor.handle_input("else:", window, cx);
20664 // });
20665 // cx.assert_editor_state(indoc! {"
20666 // def main():
20667 // try:
20668 // i = 2
20669 // except:
20670 // for i in range(n):
20671 // pass
20672 // else:ˇ
20673 // "});
20674
20675 // TODO: test `finally` auto outdents when typed inside `else` block right after for block
20676 // cx.set_state(indoc! {"
20677 // def main():
20678 // try:
20679 // i = 2
20680 // except:
20681 // j = 2
20682 // else:
20683 // for i in range(n):
20684 // pass
20685 // ˇ
20686 // "});
20687 // cx.update_editor(|editor, window, cx| {
20688 // editor.handle_input("finally:", window, cx);
20689 // });
20690 // cx.assert_editor_state(indoc! {"
20691 // def main():
20692 // try:
20693 // i = 2
20694 // except:
20695 // j = 2
20696 // else:
20697 // for i in range(n):
20698 // pass
20699 // finally:ˇ
20700 // "});
20701
20702 // test `else` stays at correct indent when typed after `for` block
20703 cx.set_state(indoc! {"
20704 def main():
20705 for i in range(10):
20706 if i == 3:
20707 break
20708 ˇ
20709 "});
20710 cx.update_editor(|editor, window, cx| {
20711 editor.handle_input("else:", window, cx);
20712 });
20713 cx.assert_editor_state(indoc! {"
20714 def main():
20715 for i in range(10):
20716 if i == 3:
20717 break
20718 else:ˇ
20719 "});
20720}
20721
20722#[gpui::test]
20723async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
20724 init_test(cx, |_| {});
20725 update_test_language_settings(cx, |settings| {
20726 settings.defaults.extend_comment_on_newline = Some(false);
20727 });
20728 let mut cx = EditorTestContext::new(cx).await;
20729 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
20730 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
20731
20732 // test correct indent after newline on comment
20733 cx.set_state(indoc! {"
20734 # COMMENT:ˇ
20735 "});
20736 cx.update_editor(|editor, window, cx| {
20737 editor.newline(&Newline, window, cx);
20738 });
20739 cx.assert_editor_state(indoc! {"
20740 # COMMENT:
20741 ˇ
20742 "});
20743
20744 // test correct indent after newline in brackets
20745 cx.set_state(indoc! {"
20746 {ˇ}
20747 "});
20748 cx.update_editor(|editor, window, cx| {
20749 editor.newline(&Newline, window, cx);
20750 });
20751 cx.run_until_parked();
20752 cx.assert_editor_state(indoc! {"
20753 {
20754 ˇ
20755 }
20756 "});
20757
20758 cx.set_state(indoc! {"
20759 (ˇ)
20760 "});
20761 cx.update_editor(|editor, window, cx| {
20762 editor.newline(&Newline, window, cx);
20763 });
20764 cx.run_until_parked();
20765 cx.assert_editor_state(indoc! {"
20766 (
20767 ˇ
20768 )
20769 "});
20770
20771 // do not indent after empty lists or dictionaries
20772 cx.set_state(indoc! {"
20773 a = []ˇ
20774 "});
20775 cx.update_editor(|editor, window, cx| {
20776 editor.newline(&Newline, window, cx);
20777 });
20778 cx.run_until_parked();
20779 cx.assert_editor_state(indoc! {"
20780 a = []
20781 ˇ
20782 "});
20783}
20784
20785fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
20786 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
20787 point..point
20788}
20789
20790fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
20791 let (text, ranges) = marked_text_ranges(marked_text, true);
20792 assert_eq!(editor.text(cx), text);
20793 assert_eq!(
20794 editor.selections.ranges(cx),
20795 ranges,
20796 "Assert selections are {}",
20797 marked_text
20798 );
20799}
20800
20801pub fn handle_signature_help_request(
20802 cx: &mut EditorLspTestContext,
20803 mocked_response: lsp::SignatureHelp,
20804) -> impl Future<Output = ()> + use<> {
20805 let mut request =
20806 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
20807 let mocked_response = mocked_response.clone();
20808 async move { Ok(Some(mocked_response)) }
20809 });
20810
20811 async move {
20812 request.next().await;
20813 }
20814}
20815
20816/// Handle completion request passing a marked string specifying where the completion
20817/// should be triggered from using '|' character, what range should be replaced, and what completions
20818/// should be returned using '<' and '>' to delimit the range.
20819///
20820/// Also see `handle_completion_request_with_insert_and_replace`.
20821#[track_caller]
20822pub fn handle_completion_request(
20823 cx: &mut EditorLspTestContext,
20824 marked_string: &str,
20825 completions: Vec<&'static str>,
20826 counter: Arc<AtomicUsize>,
20827) -> impl Future<Output = ()> {
20828 let complete_from_marker: TextRangeMarker = '|'.into();
20829 let replace_range_marker: TextRangeMarker = ('<', '>').into();
20830 let (_, mut marked_ranges) = marked_text_ranges_by(
20831 marked_string,
20832 vec![complete_from_marker.clone(), replace_range_marker.clone()],
20833 );
20834
20835 let complete_from_position =
20836 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
20837 let replace_range =
20838 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
20839
20840 let mut request =
20841 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
20842 let completions = completions.clone();
20843 counter.fetch_add(1, atomic::Ordering::Release);
20844 async move {
20845 assert_eq!(params.text_document_position.text_document.uri, url.clone());
20846 assert_eq!(
20847 params.text_document_position.position,
20848 complete_from_position
20849 );
20850 Ok(Some(lsp::CompletionResponse::Array(
20851 completions
20852 .iter()
20853 .map(|completion_text| lsp::CompletionItem {
20854 label: completion_text.to_string(),
20855 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
20856 range: replace_range,
20857 new_text: completion_text.to_string(),
20858 })),
20859 ..Default::default()
20860 })
20861 .collect(),
20862 )))
20863 }
20864 });
20865
20866 async move {
20867 request.next().await;
20868 }
20869}
20870
20871/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
20872/// given instead, which also contains an `insert` range.
20873///
20874/// This function uses the cursor position to mimic what Rust-Analyzer provides as the `insert` range,
20875/// that is, `replace_range.start..cursor_pos`.
20876pub fn handle_completion_request_with_insert_and_replace(
20877 cx: &mut EditorLspTestContext,
20878 marked_string: &str,
20879 completions: Vec<&'static str>,
20880 counter: Arc<AtomicUsize>,
20881) -> impl Future<Output = ()> {
20882 let complete_from_marker: TextRangeMarker = '|'.into();
20883 let replace_range_marker: TextRangeMarker = ('<', '>').into();
20884 let (_, mut marked_ranges) = marked_text_ranges_by(
20885 marked_string,
20886 vec![complete_from_marker.clone(), replace_range_marker.clone()],
20887 );
20888
20889 let complete_from_position =
20890 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
20891 let replace_range =
20892 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
20893
20894 let mut request =
20895 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
20896 let completions = completions.clone();
20897 counter.fetch_add(1, atomic::Ordering::Release);
20898 async move {
20899 assert_eq!(params.text_document_position.text_document.uri, url.clone());
20900 assert_eq!(
20901 params.text_document_position.position, complete_from_position,
20902 "marker `|` position doesn't match",
20903 );
20904 Ok(Some(lsp::CompletionResponse::Array(
20905 completions
20906 .iter()
20907 .map(|completion_text| lsp::CompletionItem {
20908 label: completion_text.to_string(),
20909 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
20910 lsp::InsertReplaceEdit {
20911 insert: lsp::Range {
20912 start: replace_range.start,
20913 end: complete_from_position,
20914 },
20915 replace: replace_range,
20916 new_text: completion_text.to_string(),
20917 },
20918 )),
20919 ..Default::default()
20920 })
20921 .collect(),
20922 )))
20923 }
20924 });
20925
20926 async move {
20927 request.next().await;
20928 }
20929}
20930
20931fn handle_resolve_completion_request(
20932 cx: &mut EditorLspTestContext,
20933 edits: Option<Vec<(&'static str, &'static str)>>,
20934) -> impl Future<Output = ()> {
20935 let edits = edits.map(|edits| {
20936 edits
20937 .iter()
20938 .map(|(marked_string, new_text)| {
20939 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
20940 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
20941 lsp::TextEdit::new(replace_range, new_text.to_string())
20942 })
20943 .collect::<Vec<_>>()
20944 });
20945
20946 let mut request =
20947 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
20948 let edits = edits.clone();
20949 async move {
20950 Ok(lsp::CompletionItem {
20951 additional_text_edits: edits,
20952 ..Default::default()
20953 })
20954 }
20955 });
20956
20957 async move {
20958 request.next().await;
20959 }
20960}
20961
20962pub(crate) fn update_test_language_settings(
20963 cx: &mut TestAppContext,
20964 f: impl Fn(&mut AllLanguageSettingsContent),
20965) {
20966 cx.update(|cx| {
20967 SettingsStore::update_global(cx, |store, cx| {
20968 store.update_user_settings::<AllLanguageSettings>(cx, f);
20969 });
20970 });
20971}
20972
20973pub(crate) fn update_test_project_settings(
20974 cx: &mut TestAppContext,
20975 f: impl Fn(&mut ProjectSettings),
20976) {
20977 cx.update(|cx| {
20978 SettingsStore::update_global(cx, |store, cx| {
20979 store.update_user_settings::<ProjectSettings>(cx, f);
20980 });
20981 });
20982}
20983
20984pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
20985 cx.update(|cx| {
20986 assets::Assets.load_test_fonts(cx);
20987 let store = SettingsStore::test(cx);
20988 cx.set_global(store);
20989 theme::init(theme::LoadThemes::JustBase, cx);
20990 release_channel::init(SemanticVersion::default(), cx);
20991 client::init_settings(cx);
20992 language::init(cx);
20993 Project::init_settings(cx);
20994 workspace::init_settings(cx);
20995 crate::init(cx);
20996 });
20997
20998 update_test_language_settings(cx, f);
20999}
21000
21001#[track_caller]
21002fn assert_hunk_revert(
21003 not_reverted_text_with_selections: &str,
21004 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
21005 expected_reverted_text_with_selections: &str,
21006 base_text: &str,
21007 cx: &mut EditorLspTestContext,
21008) {
21009 cx.set_state(not_reverted_text_with_selections);
21010 cx.set_head_text(base_text);
21011 cx.executor().run_until_parked();
21012
21013 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
21014 let snapshot = editor.snapshot(window, cx);
21015 let reverted_hunk_statuses = snapshot
21016 .buffer_snapshot
21017 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
21018 .map(|hunk| hunk.status().kind)
21019 .collect::<Vec<_>>();
21020
21021 editor.git_restore(&Default::default(), window, cx);
21022 reverted_hunk_statuses
21023 });
21024 cx.executor().run_until_parked();
21025 cx.assert_editor_state(expected_reverted_text_with_selections);
21026 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
21027}