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_with_folds(cx: &mut TestAppContext) {
16849 let (buffer_id, mut cx) = setup_indent_guides_editor(
16850 &"
16851 fn main() {
16852 if a {
16853 b(
16854 c,
16855 d,
16856 )
16857 } else {
16858 e(
16859 f
16860 )
16861 }
16862 }"
16863 .unindent(),
16864 cx,
16865 )
16866 .await;
16867
16868 assert_indent_guides(
16869 0..11,
16870 vec![
16871 indent_guide(buffer_id, 1, 10, 0),
16872 indent_guide(buffer_id, 2, 5, 1),
16873 indent_guide(buffer_id, 7, 9, 1),
16874 indent_guide(buffer_id, 3, 4, 2),
16875 indent_guide(buffer_id, 8, 8, 2),
16876 ],
16877 None,
16878 &mut cx,
16879 );
16880
16881 cx.update_editor(|editor, window, cx| {
16882 editor.fold_at(MultiBufferRow(2), window, cx);
16883 assert_eq!(
16884 editor.display_text(cx),
16885 "
16886 fn main() {
16887 if a {
16888 b(⋯
16889 )
16890 } else {
16891 e(
16892 f
16893 )
16894 }
16895 }"
16896 .unindent()
16897 );
16898 });
16899
16900 assert_indent_guides(
16901 0..11,
16902 vec![
16903 indent_guide(buffer_id, 1, 10, 0),
16904 indent_guide(buffer_id, 2, 5, 1),
16905 indent_guide(buffer_id, 7, 9, 1),
16906 indent_guide(buffer_id, 8, 8, 2),
16907 ],
16908 None,
16909 &mut cx,
16910 );
16911}
16912
16913#[gpui::test]
16914async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
16915 let (buffer_id, mut cx) = setup_indent_guides_editor(
16916 &"
16917 block1
16918 block2
16919 block3
16920 block4
16921 block2
16922 block1
16923 block1"
16924 .unindent(),
16925 cx,
16926 )
16927 .await;
16928
16929 assert_indent_guides(
16930 1..10,
16931 vec![
16932 indent_guide(buffer_id, 1, 4, 0),
16933 indent_guide(buffer_id, 2, 3, 1),
16934 indent_guide(buffer_id, 3, 3, 2),
16935 ],
16936 None,
16937 &mut cx,
16938 );
16939}
16940
16941#[gpui::test]
16942async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
16943 let (buffer_id, mut cx) = setup_indent_guides_editor(
16944 &"
16945 block1
16946 block2
16947 block3
16948
16949 block1
16950 block1"
16951 .unindent(),
16952 cx,
16953 )
16954 .await;
16955
16956 assert_indent_guides(
16957 0..6,
16958 vec![
16959 indent_guide(buffer_id, 1, 2, 0),
16960 indent_guide(buffer_id, 2, 2, 1),
16961 ],
16962 None,
16963 &mut cx,
16964 );
16965}
16966
16967#[gpui::test]
16968async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
16969 let (buffer_id, mut cx) = setup_indent_guides_editor(
16970 &"
16971 block1
16972
16973
16974
16975 block2
16976 "
16977 .unindent(),
16978 cx,
16979 )
16980 .await;
16981
16982 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
16983}
16984
16985#[gpui::test]
16986async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
16987 let (buffer_id, mut cx) = setup_indent_guides_editor(
16988 &"
16989 def a:
16990 \tb = 3
16991 \tif True:
16992 \t\tc = 4
16993 \t\td = 5
16994 \tprint(b)
16995 "
16996 .unindent(),
16997 cx,
16998 )
16999 .await;
17000
17001 assert_indent_guides(
17002 0..6,
17003 vec![
17004 indent_guide(buffer_id, 1, 5, 0),
17005 indent_guide(buffer_id, 3, 4, 1),
17006 ],
17007 None,
17008 &mut cx,
17009 );
17010}
17011
17012#[gpui::test]
17013async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
17014 let (buffer_id, mut cx) = setup_indent_guides_editor(
17015 &"
17016 fn main() {
17017 let a = 1;
17018 }"
17019 .unindent(),
17020 cx,
17021 )
17022 .await;
17023
17024 cx.update_editor(|editor, window, cx| {
17025 editor.change_selections(None, window, cx, |s| {
17026 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
17027 });
17028 });
17029
17030 assert_indent_guides(
17031 0..3,
17032 vec![indent_guide(buffer_id, 1, 1, 0)],
17033 Some(vec![0]),
17034 &mut cx,
17035 );
17036}
17037
17038#[gpui::test]
17039async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
17040 let (buffer_id, mut cx) = setup_indent_guides_editor(
17041 &"
17042 fn main() {
17043 if 1 == 2 {
17044 let a = 1;
17045 }
17046 }"
17047 .unindent(),
17048 cx,
17049 )
17050 .await;
17051
17052 cx.update_editor(|editor, window, cx| {
17053 editor.change_selections(None, window, cx, |s| {
17054 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
17055 });
17056 });
17057
17058 assert_indent_guides(
17059 0..4,
17060 vec![
17061 indent_guide(buffer_id, 1, 3, 0),
17062 indent_guide(buffer_id, 2, 2, 1),
17063 ],
17064 Some(vec![1]),
17065 &mut cx,
17066 );
17067
17068 cx.update_editor(|editor, window, cx| {
17069 editor.change_selections(None, window, cx, |s| {
17070 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
17071 });
17072 });
17073
17074 assert_indent_guides(
17075 0..4,
17076 vec![
17077 indent_guide(buffer_id, 1, 3, 0),
17078 indent_guide(buffer_id, 2, 2, 1),
17079 ],
17080 Some(vec![1]),
17081 &mut cx,
17082 );
17083
17084 cx.update_editor(|editor, window, cx| {
17085 editor.change_selections(None, window, cx, |s| {
17086 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
17087 });
17088 });
17089
17090 assert_indent_guides(
17091 0..4,
17092 vec![
17093 indent_guide(buffer_id, 1, 3, 0),
17094 indent_guide(buffer_id, 2, 2, 1),
17095 ],
17096 Some(vec![0]),
17097 &mut cx,
17098 );
17099}
17100
17101#[gpui::test]
17102async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
17103 let (buffer_id, mut cx) = setup_indent_guides_editor(
17104 &"
17105 fn main() {
17106 let a = 1;
17107
17108 let b = 2;
17109 }"
17110 .unindent(),
17111 cx,
17112 )
17113 .await;
17114
17115 cx.update_editor(|editor, window, cx| {
17116 editor.change_selections(None, window, cx, |s| {
17117 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
17118 });
17119 });
17120
17121 assert_indent_guides(
17122 0..5,
17123 vec![indent_guide(buffer_id, 1, 3, 0)],
17124 Some(vec![0]),
17125 &mut cx,
17126 );
17127}
17128
17129#[gpui::test]
17130async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
17131 let (buffer_id, mut cx) = setup_indent_guides_editor(
17132 &"
17133 def m:
17134 a = 1
17135 pass"
17136 .unindent(),
17137 cx,
17138 )
17139 .await;
17140
17141 cx.update_editor(|editor, window, cx| {
17142 editor.change_selections(None, window, cx, |s| {
17143 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
17144 });
17145 });
17146
17147 assert_indent_guides(
17148 0..3,
17149 vec![indent_guide(buffer_id, 1, 2, 0)],
17150 Some(vec![0]),
17151 &mut cx,
17152 );
17153}
17154
17155#[gpui::test]
17156async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
17157 init_test(cx, |_| {});
17158 let mut cx = EditorTestContext::new(cx).await;
17159 let text = indoc! {
17160 "
17161 impl A {
17162 fn b() {
17163 0;
17164 3;
17165 5;
17166 6;
17167 7;
17168 }
17169 }
17170 "
17171 };
17172 let base_text = indoc! {
17173 "
17174 impl A {
17175 fn b() {
17176 0;
17177 1;
17178 2;
17179 3;
17180 4;
17181 }
17182 fn c() {
17183 5;
17184 6;
17185 7;
17186 }
17187 }
17188 "
17189 };
17190
17191 cx.update_editor(|editor, window, cx| {
17192 editor.set_text(text, window, cx);
17193
17194 editor.buffer().update(cx, |multibuffer, cx| {
17195 let buffer = multibuffer.as_singleton().unwrap();
17196 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
17197
17198 multibuffer.set_all_diff_hunks_expanded(cx);
17199 multibuffer.add_diff(diff, cx);
17200
17201 buffer.read(cx).remote_id()
17202 })
17203 });
17204 cx.run_until_parked();
17205
17206 cx.assert_state_with_diff(
17207 indoc! { "
17208 impl A {
17209 fn b() {
17210 0;
17211 - 1;
17212 - 2;
17213 3;
17214 - 4;
17215 - }
17216 - fn c() {
17217 5;
17218 6;
17219 7;
17220 }
17221 }
17222 ˇ"
17223 }
17224 .to_string(),
17225 );
17226
17227 let mut actual_guides = cx.update_editor(|editor, window, cx| {
17228 editor
17229 .snapshot(window, cx)
17230 .buffer_snapshot
17231 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
17232 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
17233 .collect::<Vec<_>>()
17234 });
17235 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
17236 assert_eq!(
17237 actual_guides,
17238 vec![
17239 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
17240 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
17241 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
17242 ]
17243 );
17244}
17245
17246#[gpui::test]
17247async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17248 init_test(cx, |_| {});
17249 let mut cx = EditorTestContext::new(cx).await;
17250
17251 let diff_base = r#"
17252 a
17253 b
17254 c
17255 "#
17256 .unindent();
17257
17258 cx.set_state(
17259 &r#"
17260 ˇA
17261 b
17262 C
17263 "#
17264 .unindent(),
17265 );
17266 cx.set_head_text(&diff_base);
17267 cx.update_editor(|editor, window, cx| {
17268 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17269 });
17270 executor.run_until_parked();
17271
17272 let both_hunks_expanded = r#"
17273 - a
17274 + ˇA
17275 b
17276 - c
17277 + C
17278 "#
17279 .unindent();
17280
17281 cx.assert_state_with_diff(both_hunks_expanded.clone());
17282
17283 let hunk_ranges = cx.update_editor(|editor, window, cx| {
17284 let snapshot = editor.snapshot(window, cx);
17285 let hunks = editor
17286 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
17287 .collect::<Vec<_>>();
17288 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
17289 let buffer_id = hunks[0].buffer_id;
17290 hunks
17291 .into_iter()
17292 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
17293 .collect::<Vec<_>>()
17294 });
17295 assert_eq!(hunk_ranges.len(), 2);
17296
17297 cx.update_editor(|editor, _, cx| {
17298 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
17299 });
17300 executor.run_until_parked();
17301
17302 let second_hunk_expanded = r#"
17303 ˇA
17304 b
17305 - c
17306 + C
17307 "#
17308 .unindent();
17309
17310 cx.assert_state_with_diff(second_hunk_expanded);
17311
17312 cx.update_editor(|editor, _, cx| {
17313 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
17314 });
17315 executor.run_until_parked();
17316
17317 cx.assert_state_with_diff(both_hunks_expanded.clone());
17318
17319 cx.update_editor(|editor, _, cx| {
17320 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
17321 });
17322 executor.run_until_parked();
17323
17324 let first_hunk_expanded = r#"
17325 - a
17326 + ˇA
17327 b
17328 C
17329 "#
17330 .unindent();
17331
17332 cx.assert_state_with_diff(first_hunk_expanded);
17333
17334 cx.update_editor(|editor, _, cx| {
17335 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
17336 });
17337 executor.run_until_parked();
17338
17339 cx.assert_state_with_diff(both_hunks_expanded);
17340
17341 cx.set_state(
17342 &r#"
17343 ˇA
17344 b
17345 "#
17346 .unindent(),
17347 );
17348 cx.run_until_parked();
17349
17350 // TODO this cursor position seems bad
17351 cx.assert_state_with_diff(
17352 r#"
17353 - ˇa
17354 + A
17355 b
17356 "#
17357 .unindent(),
17358 );
17359
17360 cx.update_editor(|editor, window, cx| {
17361 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17362 });
17363
17364 cx.assert_state_with_diff(
17365 r#"
17366 - ˇa
17367 + A
17368 b
17369 - c
17370 "#
17371 .unindent(),
17372 );
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(), 2);
17387
17388 cx.update_editor(|editor, _, cx| {
17389 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
17390 });
17391 executor.run_until_parked();
17392
17393 cx.assert_state_with_diff(
17394 r#"
17395 - ˇa
17396 + A
17397 b
17398 "#
17399 .unindent(),
17400 );
17401}
17402
17403#[gpui::test]
17404async fn test_toggle_deletion_hunk_at_start_of_file(
17405 executor: BackgroundExecutor,
17406 cx: &mut TestAppContext,
17407) {
17408 init_test(cx, |_| {});
17409 let mut cx = EditorTestContext::new(cx).await;
17410
17411 let diff_base = r#"
17412 a
17413 b
17414 c
17415 "#
17416 .unindent();
17417
17418 cx.set_state(
17419 &r#"
17420 ˇb
17421 c
17422 "#
17423 .unindent(),
17424 );
17425 cx.set_head_text(&diff_base);
17426 cx.update_editor(|editor, window, cx| {
17427 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17428 });
17429 executor.run_until_parked();
17430
17431 let hunk_expanded = r#"
17432 - a
17433 ˇb
17434 c
17435 "#
17436 .unindent();
17437
17438 cx.assert_state_with_diff(hunk_expanded.clone());
17439
17440 let hunk_ranges = cx.update_editor(|editor, window, cx| {
17441 let snapshot = editor.snapshot(window, cx);
17442 let hunks = editor
17443 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
17444 .collect::<Vec<_>>();
17445 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
17446 let buffer_id = hunks[0].buffer_id;
17447 hunks
17448 .into_iter()
17449 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
17450 .collect::<Vec<_>>()
17451 });
17452 assert_eq!(hunk_ranges.len(), 1);
17453
17454 cx.update_editor(|editor, _, cx| {
17455 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
17456 });
17457 executor.run_until_parked();
17458
17459 let hunk_collapsed = r#"
17460 ˇb
17461 c
17462 "#
17463 .unindent();
17464
17465 cx.assert_state_with_diff(hunk_collapsed);
17466
17467 cx.update_editor(|editor, _, cx| {
17468 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
17469 });
17470 executor.run_until_parked();
17471
17472 cx.assert_state_with_diff(hunk_expanded.clone());
17473}
17474
17475#[gpui::test]
17476async fn test_display_diff_hunks(cx: &mut TestAppContext) {
17477 init_test(cx, |_| {});
17478
17479 let fs = FakeFs::new(cx.executor());
17480 fs.insert_tree(
17481 path!("/test"),
17482 json!({
17483 ".git": {},
17484 "file-1": "ONE\n",
17485 "file-2": "TWO\n",
17486 "file-3": "THREE\n",
17487 }),
17488 )
17489 .await;
17490
17491 fs.set_head_for_repo(
17492 path!("/test/.git").as_ref(),
17493 &[
17494 ("file-1".into(), "one\n".into()),
17495 ("file-2".into(), "two\n".into()),
17496 ("file-3".into(), "three\n".into()),
17497 ],
17498 );
17499
17500 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
17501 let mut buffers = vec![];
17502 for i in 1..=3 {
17503 let buffer = project
17504 .update(cx, |project, cx| {
17505 let path = format!(path!("/test/file-{}"), i);
17506 project.open_local_buffer(path, cx)
17507 })
17508 .await
17509 .unwrap();
17510 buffers.push(buffer);
17511 }
17512
17513 let multibuffer = cx.new(|cx| {
17514 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
17515 multibuffer.set_all_diff_hunks_expanded(cx);
17516 for buffer in &buffers {
17517 let snapshot = buffer.read(cx).snapshot();
17518 multibuffer.set_excerpts_for_path(
17519 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
17520 buffer.clone(),
17521 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
17522 DEFAULT_MULTIBUFFER_CONTEXT,
17523 cx,
17524 );
17525 }
17526 multibuffer
17527 });
17528
17529 let editor = cx.add_window(|window, cx| {
17530 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
17531 });
17532 cx.run_until_parked();
17533
17534 let snapshot = editor
17535 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
17536 .unwrap();
17537 let hunks = snapshot
17538 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
17539 .map(|hunk| match hunk {
17540 DisplayDiffHunk::Unfolded {
17541 display_row_range, ..
17542 } => display_row_range,
17543 DisplayDiffHunk::Folded { .. } => unreachable!(),
17544 })
17545 .collect::<Vec<_>>();
17546 assert_eq!(
17547 hunks,
17548 [
17549 DisplayRow(2)..DisplayRow(4),
17550 DisplayRow(7)..DisplayRow(9),
17551 DisplayRow(12)..DisplayRow(14),
17552 ]
17553 );
17554}
17555
17556#[gpui::test]
17557async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
17558 init_test(cx, |_| {});
17559
17560 let mut cx = EditorTestContext::new(cx).await;
17561 cx.set_head_text(indoc! { "
17562 one
17563 two
17564 three
17565 four
17566 five
17567 "
17568 });
17569 cx.set_index_text(indoc! { "
17570 one
17571 two
17572 three
17573 four
17574 five
17575 "
17576 });
17577 cx.set_state(indoc! {"
17578 one
17579 TWO
17580 ˇTHREE
17581 FOUR
17582 five
17583 "});
17584 cx.run_until_parked();
17585 cx.update_editor(|editor, window, cx| {
17586 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
17587 });
17588 cx.run_until_parked();
17589 cx.assert_index_text(Some(indoc! {"
17590 one
17591 TWO
17592 THREE
17593 FOUR
17594 five
17595 "}));
17596 cx.set_state(indoc! { "
17597 one
17598 TWO
17599 ˇTHREE-HUNDRED
17600 FOUR
17601 five
17602 "});
17603 cx.run_until_parked();
17604 cx.update_editor(|editor, window, cx| {
17605 let snapshot = editor.snapshot(window, cx);
17606 let hunks = editor
17607 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
17608 .collect::<Vec<_>>();
17609 assert_eq!(hunks.len(), 1);
17610 assert_eq!(
17611 hunks[0].status(),
17612 DiffHunkStatus {
17613 kind: DiffHunkStatusKind::Modified,
17614 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
17615 }
17616 );
17617
17618 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
17619 });
17620 cx.run_until_parked();
17621 cx.assert_index_text(Some(indoc! {"
17622 one
17623 TWO
17624 THREE-HUNDRED
17625 FOUR
17626 five
17627 "}));
17628}
17629
17630#[gpui::test]
17631fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
17632 init_test(cx, |_| {});
17633
17634 let editor = cx.add_window(|window, cx| {
17635 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
17636 build_editor(buffer, window, cx)
17637 });
17638
17639 let render_args = Arc::new(Mutex::new(None));
17640 let snapshot = editor
17641 .update(cx, |editor, window, cx| {
17642 let snapshot = editor.buffer().read(cx).snapshot(cx);
17643 let range =
17644 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
17645
17646 struct RenderArgs {
17647 row: MultiBufferRow,
17648 folded: bool,
17649 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
17650 }
17651
17652 let crease = Crease::inline(
17653 range,
17654 FoldPlaceholder::test(),
17655 {
17656 let toggle_callback = render_args.clone();
17657 move |row, folded, callback, _window, _cx| {
17658 *toggle_callback.lock() = Some(RenderArgs {
17659 row,
17660 folded,
17661 callback,
17662 });
17663 div()
17664 }
17665 },
17666 |_row, _folded, _window, _cx| div(),
17667 );
17668
17669 editor.insert_creases(Some(crease), cx);
17670 let snapshot = editor.snapshot(window, cx);
17671 let _div = snapshot.render_crease_toggle(
17672 MultiBufferRow(1),
17673 false,
17674 cx.entity().clone(),
17675 window,
17676 cx,
17677 );
17678 snapshot
17679 })
17680 .unwrap();
17681
17682 let render_args = render_args.lock().take().unwrap();
17683 assert_eq!(render_args.row, MultiBufferRow(1));
17684 assert!(!render_args.folded);
17685 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
17686
17687 cx.update_window(*editor, |_, window, cx| {
17688 (render_args.callback)(true, window, cx)
17689 })
17690 .unwrap();
17691 let snapshot = editor
17692 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
17693 .unwrap();
17694 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
17695
17696 cx.update_window(*editor, |_, window, cx| {
17697 (render_args.callback)(false, window, cx)
17698 })
17699 .unwrap();
17700 let snapshot = editor
17701 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
17702 .unwrap();
17703 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
17704}
17705
17706#[gpui::test]
17707async fn test_input_text(cx: &mut TestAppContext) {
17708 init_test(cx, |_| {});
17709 let mut cx = EditorTestContext::new(cx).await;
17710
17711 cx.set_state(
17712 &r#"ˇone
17713 two
17714
17715 three
17716 fourˇ
17717 five
17718
17719 siˇx"#
17720 .unindent(),
17721 );
17722
17723 cx.dispatch_action(HandleInput(String::new()));
17724 cx.assert_editor_state(
17725 &r#"ˇone
17726 two
17727
17728 three
17729 fourˇ
17730 five
17731
17732 siˇx"#
17733 .unindent(),
17734 );
17735
17736 cx.dispatch_action(HandleInput("AAAA".to_string()));
17737 cx.assert_editor_state(
17738 &r#"AAAAˇone
17739 two
17740
17741 three
17742 fourAAAAˇ
17743 five
17744
17745 siAAAAˇx"#
17746 .unindent(),
17747 );
17748}
17749
17750#[gpui::test]
17751async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
17752 init_test(cx, |_| {});
17753
17754 let mut cx = EditorTestContext::new(cx).await;
17755 cx.set_state(
17756 r#"let foo = 1;
17757let foo = 2;
17758let foo = 3;
17759let fooˇ = 4;
17760let foo = 5;
17761let foo = 6;
17762let foo = 7;
17763let foo = 8;
17764let foo = 9;
17765let foo = 10;
17766let foo = 11;
17767let foo = 12;
17768let foo = 13;
17769let foo = 14;
17770let foo = 15;"#,
17771 );
17772
17773 cx.update_editor(|e, window, cx| {
17774 assert_eq!(
17775 e.next_scroll_position,
17776 NextScrollCursorCenterTopBottom::Center,
17777 "Default next scroll direction is center",
17778 );
17779
17780 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17781 assert_eq!(
17782 e.next_scroll_position,
17783 NextScrollCursorCenterTopBottom::Top,
17784 "After center, next scroll direction should be top",
17785 );
17786
17787 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17788 assert_eq!(
17789 e.next_scroll_position,
17790 NextScrollCursorCenterTopBottom::Bottom,
17791 "After top, next scroll direction should be bottom",
17792 );
17793
17794 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17795 assert_eq!(
17796 e.next_scroll_position,
17797 NextScrollCursorCenterTopBottom::Center,
17798 "After bottom, scrolling should start over",
17799 );
17800
17801 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17802 assert_eq!(
17803 e.next_scroll_position,
17804 NextScrollCursorCenterTopBottom::Top,
17805 "Scrolling continues if retriggered fast enough"
17806 );
17807 });
17808
17809 cx.executor()
17810 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
17811 cx.executor().run_until_parked();
17812 cx.update_editor(|e, _, _| {
17813 assert_eq!(
17814 e.next_scroll_position,
17815 NextScrollCursorCenterTopBottom::Center,
17816 "If scrolling is not triggered fast enough, it should reset"
17817 );
17818 });
17819}
17820
17821#[gpui::test]
17822async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
17823 init_test(cx, |_| {});
17824 let mut cx = EditorLspTestContext::new_rust(
17825 lsp::ServerCapabilities {
17826 definition_provider: Some(lsp::OneOf::Left(true)),
17827 references_provider: Some(lsp::OneOf::Left(true)),
17828 ..lsp::ServerCapabilities::default()
17829 },
17830 cx,
17831 )
17832 .await;
17833
17834 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
17835 let go_to_definition = cx
17836 .lsp
17837 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
17838 move |params, _| async move {
17839 if empty_go_to_definition {
17840 Ok(None)
17841 } else {
17842 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
17843 uri: params.text_document_position_params.text_document.uri,
17844 range: lsp::Range::new(
17845 lsp::Position::new(4, 3),
17846 lsp::Position::new(4, 6),
17847 ),
17848 })))
17849 }
17850 },
17851 );
17852 let references = cx
17853 .lsp
17854 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
17855 Ok(Some(vec![lsp::Location {
17856 uri: params.text_document_position.text_document.uri,
17857 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
17858 }]))
17859 });
17860 (go_to_definition, references)
17861 };
17862
17863 cx.set_state(
17864 &r#"fn one() {
17865 let mut a = ˇtwo();
17866 }
17867
17868 fn two() {}"#
17869 .unindent(),
17870 );
17871 set_up_lsp_handlers(false, &mut cx);
17872 let navigated = cx
17873 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
17874 .await
17875 .expect("Failed to navigate to definition");
17876 assert_eq!(
17877 navigated,
17878 Navigated::Yes,
17879 "Should have navigated to definition from the GetDefinition response"
17880 );
17881 cx.assert_editor_state(
17882 &r#"fn one() {
17883 let mut a = two();
17884 }
17885
17886 fn «twoˇ»() {}"#
17887 .unindent(),
17888 );
17889
17890 let editors = cx.update_workspace(|workspace, _, cx| {
17891 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
17892 });
17893 cx.update_editor(|_, _, test_editor_cx| {
17894 assert_eq!(
17895 editors.len(),
17896 1,
17897 "Initially, only one, test, editor should be open in the workspace"
17898 );
17899 assert_eq!(
17900 test_editor_cx.entity(),
17901 editors.last().expect("Asserted len is 1").clone()
17902 );
17903 });
17904
17905 set_up_lsp_handlers(true, &mut cx);
17906 let navigated = cx
17907 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
17908 .await
17909 .expect("Failed to navigate to lookup references");
17910 assert_eq!(
17911 navigated,
17912 Navigated::Yes,
17913 "Should have navigated to references as a fallback after empty GoToDefinition response"
17914 );
17915 // We should not change the selections in the existing file,
17916 // if opening another milti buffer with the references
17917 cx.assert_editor_state(
17918 &r#"fn one() {
17919 let mut a = two();
17920 }
17921
17922 fn «twoˇ»() {}"#
17923 .unindent(),
17924 );
17925 let editors = cx.update_workspace(|workspace, _, cx| {
17926 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
17927 });
17928 cx.update_editor(|_, _, test_editor_cx| {
17929 assert_eq!(
17930 editors.len(),
17931 2,
17932 "After falling back to references search, we open a new editor with the results"
17933 );
17934 let references_fallback_text = editors
17935 .into_iter()
17936 .find(|new_editor| *new_editor != test_editor_cx.entity())
17937 .expect("Should have one non-test editor now")
17938 .read(test_editor_cx)
17939 .text(test_editor_cx);
17940 assert_eq!(
17941 references_fallback_text, "fn one() {\n let mut a = two();\n}",
17942 "Should use the range from the references response and not the GoToDefinition one"
17943 );
17944 });
17945}
17946
17947#[gpui::test]
17948async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
17949 init_test(cx, |_| {});
17950 cx.update(|cx| {
17951 let mut editor_settings = EditorSettings::get_global(cx).clone();
17952 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
17953 EditorSettings::override_global(editor_settings, cx);
17954 });
17955 let mut cx = EditorLspTestContext::new_rust(
17956 lsp::ServerCapabilities {
17957 definition_provider: Some(lsp::OneOf::Left(true)),
17958 references_provider: Some(lsp::OneOf::Left(true)),
17959 ..lsp::ServerCapabilities::default()
17960 },
17961 cx,
17962 )
17963 .await;
17964 let original_state = r#"fn one() {
17965 let mut a = ˇtwo();
17966 }
17967
17968 fn two() {}"#
17969 .unindent();
17970 cx.set_state(&original_state);
17971
17972 let mut go_to_definition = cx
17973 .lsp
17974 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
17975 move |_, _| async move { Ok(None) },
17976 );
17977 let _references = cx
17978 .lsp
17979 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
17980 panic!("Should not call for references with no go to definition fallback")
17981 });
17982
17983 let navigated = cx
17984 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
17985 .await
17986 .expect("Failed to navigate to lookup references");
17987 go_to_definition
17988 .next()
17989 .await
17990 .expect("Should have called the go_to_definition handler");
17991
17992 assert_eq!(
17993 navigated,
17994 Navigated::No,
17995 "Should have navigated to references as a fallback after empty GoToDefinition response"
17996 );
17997 cx.assert_editor_state(&original_state);
17998 let editors = cx.update_workspace(|workspace, _, cx| {
17999 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
18000 });
18001 cx.update_editor(|_, _, _| {
18002 assert_eq!(
18003 editors.len(),
18004 1,
18005 "After unsuccessful fallback, no other editor should have been opened"
18006 );
18007 });
18008}
18009
18010#[gpui::test]
18011async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
18012 init_test(cx, |_| {});
18013
18014 let language = Arc::new(Language::new(
18015 LanguageConfig::default(),
18016 Some(tree_sitter_rust::LANGUAGE.into()),
18017 ));
18018
18019 let text = r#"
18020 #[cfg(test)]
18021 mod tests() {
18022 #[test]
18023 fn runnable_1() {
18024 let a = 1;
18025 }
18026
18027 #[test]
18028 fn runnable_2() {
18029 let a = 1;
18030 let b = 2;
18031 }
18032 }
18033 "#
18034 .unindent();
18035
18036 let fs = FakeFs::new(cx.executor());
18037 fs.insert_file("/file.rs", Default::default()).await;
18038
18039 let project = Project::test(fs, ["/a".as_ref()], cx).await;
18040 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18041 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18042 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
18043 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
18044
18045 let editor = cx.new_window_entity(|window, cx| {
18046 Editor::new(
18047 EditorMode::full(),
18048 multi_buffer,
18049 Some(project.clone()),
18050 window,
18051 cx,
18052 )
18053 });
18054
18055 editor.update_in(cx, |editor, window, cx| {
18056 let snapshot = editor.buffer().read(cx).snapshot(cx);
18057 editor.tasks.insert(
18058 (buffer.read(cx).remote_id(), 3),
18059 RunnableTasks {
18060 templates: vec![],
18061 offset: snapshot.anchor_before(43),
18062 column: 0,
18063 extra_variables: HashMap::default(),
18064 context_range: BufferOffset(43)..BufferOffset(85),
18065 },
18066 );
18067 editor.tasks.insert(
18068 (buffer.read(cx).remote_id(), 8),
18069 RunnableTasks {
18070 templates: vec![],
18071 offset: snapshot.anchor_before(86),
18072 column: 0,
18073 extra_variables: HashMap::default(),
18074 context_range: BufferOffset(86)..BufferOffset(191),
18075 },
18076 );
18077
18078 // Test finding task when cursor is inside function body
18079 editor.change_selections(None, window, cx, |s| {
18080 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
18081 });
18082 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
18083 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
18084
18085 // Test finding task when cursor is on function name
18086 editor.change_selections(None, window, cx, |s| {
18087 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
18088 });
18089 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
18090 assert_eq!(row, 8, "Should find task when cursor is on function name");
18091 });
18092}
18093
18094#[gpui::test]
18095async fn test_folding_buffers(cx: &mut TestAppContext) {
18096 init_test(cx, |_| {});
18097
18098 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
18099 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
18100 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
18101
18102 let fs = FakeFs::new(cx.executor());
18103 fs.insert_tree(
18104 path!("/a"),
18105 json!({
18106 "first.rs": sample_text_1,
18107 "second.rs": sample_text_2,
18108 "third.rs": sample_text_3,
18109 }),
18110 )
18111 .await;
18112 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18113 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18114 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18115 let worktree = project.update(cx, |project, cx| {
18116 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
18117 assert_eq!(worktrees.len(), 1);
18118 worktrees.pop().unwrap()
18119 });
18120 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
18121
18122 let buffer_1 = project
18123 .update(cx, |project, cx| {
18124 project.open_buffer((worktree_id, "first.rs"), cx)
18125 })
18126 .await
18127 .unwrap();
18128 let buffer_2 = project
18129 .update(cx, |project, cx| {
18130 project.open_buffer((worktree_id, "second.rs"), cx)
18131 })
18132 .await
18133 .unwrap();
18134 let buffer_3 = project
18135 .update(cx, |project, cx| {
18136 project.open_buffer((worktree_id, "third.rs"), cx)
18137 })
18138 .await
18139 .unwrap();
18140
18141 let multi_buffer = cx.new(|cx| {
18142 let mut multi_buffer = MultiBuffer::new(ReadWrite);
18143 multi_buffer.push_excerpts(
18144 buffer_1.clone(),
18145 [
18146 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18147 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18148 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18149 ],
18150 cx,
18151 );
18152 multi_buffer.push_excerpts(
18153 buffer_2.clone(),
18154 [
18155 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18156 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18157 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18158 ],
18159 cx,
18160 );
18161 multi_buffer.push_excerpts(
18162 buffer_3.clone(),
18163 [
18164 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18165 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18166 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18167 ],
18168 cx,
18169 );
18170 multi_buffer
18171 });
18172 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18173 Editor::new(
18174 EditorMode::full(),
18175 multi_buffer.clone(),
18176 Some(project.clone()),
18177 window,
18178 cx,
18179 )
18180 });
18181
18182 assert_eq!(
18183 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18184 "\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",
18185 );
18186
18187 multi_buffer_editor.update(cx, |editor, cx| {
18188 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
18189 });
18190 assert_eq!(
18191 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18192 "\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",
18193 "After folding the first buffer, its text should not be displayed"
18194 );
18195
18196 multi_buffer_editor.update(cx, |editor, cx| {
18197 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
18198 });
18199 assert_eq!(
18200 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18201 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
18202 "After folding the second buffer, its text should not be displayed"
18203 );
18204
18205 multi_buffer_editor.update(cx, |editor, cx| {
18206 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
18207 });
18208 assert_eq!(
18209 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18210 "\n\n\n\n\n",
18211 "After folding the third buffer, its text should not be displayed"
18212 );
18213
18214 // Emulate selection inside the fold logic, that should work
18215 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18216 editor
18217 .snapshot(window, cx)
18218 .next_line_boundary(Point::new(0, 4));
18219 });
18220
18221 multi_buffer_editor.update(cx, |editor, cx| {
18222 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
18223 });
18224 assert_eq!(
18225 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18226 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
18227 "After unfolding the second buffer, its text should be displayed"
18228 );
18229
18230 // Typing inside of buffer 1 causes that buffer to be unfolded.
18231 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18232 assert_eq!(
18233 multi_buffer
18234 .read(cx)
18235 .snapshot(cx)
18236 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
18237 .collect::<String>(),
18238 "bbbb"
18239 );
18240 editor.change_selections(None, window, cx, |selections| {
18241 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
18242 });
18243 editor.handle_input("B", window, cx);
18244 });
18245
18246 assert_eq!(
18247 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18248 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
18249 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
18250 );
18251
18252 multi_buffer_editor.update(cx, |editor, cx| {
18253 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
18254 });
18255 assert_eq!(
18256 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18257 "\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",
18258 "After unfolding the all buffers, all original text should be displayed"
18259 );
18260}
18261
18262#[gpui::test]
18263async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
18264 init_test(cx, |_| {});
18265
18266 let sample_text_1 = "1111\n2222\n3333".to_string();
18267 let sample_text_2 = "4444\n5555\n6666".to_string();
18268 let sample_text_3 = "7777\n8888\n9999".to_string();
18269
18270 let fs = FakeFs::new(cx.executor());
18271 fs.insert_tree(
18272 path!("/a"),
18273 json!({
18274 "first.rs": sample_text_1,
18275 "second.rs": sample_text_2,
18276 "third.rs": sample_text_3,
18277 }),
18278 )
18279 .await;
18280 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18281 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18282 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18283 let worktree = project.update(cx, |project, cx| {
18284 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
18285 assert_eq!(worktrees.len(), 1);
18286 worktrees.pop().unwrap()
18287 });
18288 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
18289
18290 let buffer_1 = project
18291 .update(cx, |project, cx| {
18292 project.open_buffer((worktree_id, "first.rs"), cx)
18293 })
18294 .await
18295 .unwrap();
18296 let buffer_2 = project
18297 .update(cx, |project, cx| {
18298 project.open_buffer((worktree_id, "second.rs"), cx)
18299 })
18300 .await
18301 .unwrap();
18302 let buffer_3 = project
18303 .update(cx, |project, cx| {
18304 project.open_buffer((worktree_id, "third.rs"), cx)
18305 })
18306 .await
18307 .unwrap();
18308
18309 let multi_buffer = cx.new(|cx| {
18310 let mut multi_buffer = MultiBuffer::new(ReadWrite);
18311 multi_buffer.push_excerpts(
18312 buffer_1.clone(),
18313 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
18314 cx,
18315 );
18316 multi_buffer.push_excerpts(
18317 buffer_2.clone(),
18318 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
18319 cx,
18320 );
18321 multi_buffer.push_excerpts(
18322 buffer_3.clone(),
18323 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
18324 cx,
18325 );
18326 multi_buffer
18327 });
18328
18329 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18330 Editor::new(
18331 EditorMode::full(),
18332 multi_buffer,
18333 Some(project.clone()),
18334 window,
18335 cx,
18336 )
18337 });
18338
18339 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
18340 assert_eq!(
18341 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18342 full_text,
18343 );
18344
18345 multi_buffer_editor.update(cx, |editor, cx| {
18346 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
18347 });
18348 assert_eq!(
18349 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18350 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
18351 "After folding the first buffer, its text should not be displayed"
18352 );
18353
18354 multi_buffer_editor.update(cx, |editor, cx| {
18355 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
18356 });
18357
18358 assert_eq!(
18359 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18360 "\n\n\n\n\n\n7777\n8888\n9999",
18361 "After folding the second buffer, its text should not be displayed"
18362 );
18363
18364 multi_buffer_editor.update(cx, |editor, cx| {
18365 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
18366 });
18367 assert_eq!(
18368 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18369 "\n\n\n\n\n",
18370 "After folding the third buffer, its text should not be displayed"
18371 );
18372
18373 multi_buffer_editor.update(cx, |editor, cx| {
18374 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
18375 });
18376 assert_eq!(
18377 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18378 "\n\n\n\n4444\n5555\n6666\n\n",
18379 "After unfolding the second buffer, its text should be displayed"
18380 );
18381
18382 multi_buffer_editor.update(cx, |editor, cx| {
18383 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
18384 });
18385 assert_eq!(
18386 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18387 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
18388 "After unfolding the first buffer, its text should be displayed"
18389 );
18390
18391 multi_buffer_editor.update(cx, |editor, cx| {
18392 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
18393 });
18394 assert_eq!(
18395 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18396 full_text,
18397 "After unfolding all buffers, all original text should be displayed"
18398 );
18399}
18400
18401#[gpui::test]
18402async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
18403 init_test(cx, |_| {});
18404
18405 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
18406
18407 let fs = FakeFs::new(cx.executor());
18408 fs.insert_tree(
18409 path!("/a"),
18410 json!({
18411 "main.rs": sample_text,
18412 }),
18413 )
18414 .await;
18415 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18416 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18417 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18418 let worktree = project.update(cx, |project, cx| {
18419 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
18420 assert_eq!(worktrees.len(), 1);
18421 worktrees.pop().unwrap()
18422 });
18423 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
18424
18425 let buffer_1 = project
18426 .update(cx, |project, cx| {
18427 project.open_buffer((worktree_id, "main.rs"), cx)
18428 })
18429 .await
18430 .unwrap();
18431
18432 let multi_buffer = cx.new(|cx| {
18433 let mut multi_buffer = MultiBuffer::new(ReadWrite);
18434 multi_buffer.push_excerpts(
18435 buffer_1.clone(),
18436 [ExcerptRange::new(
18437 Point::new(0, 0)
18438 ..Point::new(
18439 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
18440 0,
18441 ),
18442 )],
18443 cx,
18444 );
18445 multi_buffer
18446 });
18447 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18448 Editor::new(
18449 EditorMode::full(),
18450 multi_buffer,
18451 Some(project.clone()),
18452 window,
18453 cx,
18454 )
18455 });
18456
18457 let selection_range = Point::new(1, 0)..Point::new(2, 0);
18458 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18459 enum TestHighlight {}
18460 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
18461 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
18462 editor.highlight_text::<TestHighlight>(
18463 vec![highlight_range.clone()],
18464 HighlightStyle::color(Hsla::green()),
18465 cx,
18466 );
18467 editor.change_selections(None, window, cx, |s| s.select_ranges(Some(highlight_range)));
18468 });
18469
18470 let full_text = format!("\n\n{sample_text}");
18471 assert_eq!(
18472 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18473 full_text,
18474 );
18475}
18476
18477#[gpui::test]
18478async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
18479 init_test(cx, |_| {});
18480 cx.update(|cx| {
18481 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
18482 "keymaps/default-linux.json",
18483 cx,
18484 )
18485 .unwrap();
18486 cx.bind_keys(default_key_bindings);
18487 });
18488
18489 let (editor, cx) = cx.add_window_view(|window, cx| {
18490 let multi_buffer = MultiBuffer::build_multi(
18491 [
18492 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
18493 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
18494 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
18495 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
18496 ],
18497 cx,
18498 );
18499 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
18500
18501 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
18502 // fold all but the second buffer, so that we test navigating between two
18503 // adjacent folded buffers, as well as folded buffers at the start and
18504 // end the multibuffer
18505 editor.fold_buffer(buffer_ids[0], cx);
18506 editor.fold_buffer(buffer_ids[2], cx);
18507 editor.fold_buffer(buffer_ids[3], cx);
18508
18509 editor
18510 });
18511 cx.simulate_resize(size(px(1000.), px(1000.)));
18512
18513 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
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 cx.simulate_keystroke("down");
18527 cx.assert_excerpts_with_selections(indoc! {"
18528 [EXCERPT]
18529 [FOLDED]
18530 [EXCERPT]
18531 ˇa1
18532 b1
18533 [EXCERPT]
18534 [FOLDED]
18535 [EXCERPT]
18536 [FOLDED]
18537 "
18538 });
18539 cx.simulate_keystroke("down");
18540 cx.assert_excerpts_with_selections(indoc! {"
18541 [EXCERPT]
18542 [FOLDED]
18543 [EXCERPT]
18544 a1
18545 ˇb1
18546 [EXCERPT]
18547 [FOLDED]
18548 [EXCERPT]
18549 [FOLDED]
18550 "
18551 });
18552 cx.simulate_keystroke("down");
18553 cx.assert_excerpts_with_selections(indoc! {"
18554 [EXCERPT]
18555 [FOLDED]
18556 [EXCERPT]
18557 a1
18558 b1
18559 ˇ[EXCERPT]
18560 [FOLDED]
18561 [EXCERPT]
18562 [FOLDED]
18563 "
18564 });
18565 cx.simulate_keystroke("down");
18566 cx.assert_excerpts_with_selections(indoc! {"
18567 [EXCERPT]
18568 [FOLDED]
18569 [EXCERPT]
18570 a1
18571 b1
18572 [EXCERPT]
18573 ˇ[FOLDED]
18574 [EXCERPT]
18575 [FOLDED]
18576 "
18577 });
18578 for _ in 0..5 {
18579 cx.simulate_keystroke("down");
18580 cx.assert_excerpts_with_selections(indoc! {"
18581 [EXCERPT]
18582 [FOLDED]
18583 [EXCERPT]
18584 a1
18585 b1
18586 [EXCERPT]
18587 [FOLDED]
18588 [EXCERPT]
18589 ˇ[FOLDED]
18590 "
18591 });
18592 }
18593
18594 cx.simulate_keystroke("up");
18595 cx.assert_excerpts_with_selections(indoc! {"
18596 [EXCERPT]
18597 [FOLDED]
18598 [EXCERPT]
18599 a1
18600 b1
18601 [EXCERPT]
18602 ˇ[FOLDED]
18603 [EXCERPT]
18604 [FOLDED]
18605 "
18606 });
18607 cx.simulate_keystroke("up");
18608 cx.assert_excerpts_with_selections(indoc! {"
18609 [EXCERPT]
18610 [FOLDED]
18611 [EXCERPT]
18612 a1
18613 b1
18614 ˇ[EXCERPT]
18615 [FOLDED]
18616 [EXCERPT]
18617 [FOLDED]
18618 "
18619 });
18620 cx.simulate_keystroke("up");
18621 cx.assert_excerpts_with_selections(indoc! {"
18622 [EXCERPT]
18623 [FOLDED]
18624 [EXCERPT]
18625 a1
18626 ˇb1
18627 [EXCERPT]
18628 [FOLDED]
18629 [EXCERPT]
18630 [FOLDED]
18631 "
18632 });
18633 cx.simulate_keystroke("up");
18634 cx.assert_excerpts_with_selections(indoc! {"
18635 [EXCERPT]
18636 [FOLDED]
18637 [EXCERPT]
18638 ˇa1
18639 b1
18640 [EXCERPT]
18641 [FOLDED]
18642 [EXCERPT]
18643 [FOLDED]
18644 "
18645 });
18646 for _ in 0..5 {
18647 cx.simulate_keystroke("up");
18648 cx.assert_excerpts_with_selections(indoc! {"
18649 [EXCERPT]
18650 ˇ[FOLDED]
18651 [EXCERPT]
18652 a1
18653 b1
18654 [EXCERPT]
18655 [FOLDED]
18656 [EXCERPT]
18657 [FOLDED]
18658 "
18659 });
18660 }
18661}
18662
18663#[gpui::test]
18664async fn test_inline_completion_text(cx: &mut TestAppContext) {
18665 init_test(cx, |_| {});
18666
18667 // Simple insertion
18668 assert_highlighted_edits(
18669 "Hello, world!",
18670 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
18671 true,
18672 cx,
18673 |highlighted_edits, cx| {
18674 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
18675 assert_eq!(highlighted_edits.highlights.len(), 1);
18676 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
18677 assert_eq!(
18678 highlighted_edits.highlights[0].1.background_color,
18679 Some(cx.theme().status().created_background)
18680 );
18681 },
18682 )
18683 .await;
18684
18685 // Replacement
18686 assert_highlighted_edits(
18687 "This is a test.",
18688 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
18689 false,
18690 cx,
18691 |highlighted_edits, cx| {
18692 assert_eq!(highlighted_edits.text, "That is a test.");
18693 assert_eq!(highlighted_edits.highlights.len(), 1);
18694 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
18695 assert_eq!(
18696 highlighted_edits.highlights[0].1.background_color,
18697 Some(cx.theme().status().created_background)
18698 );
18699 },
18700 )
18701 .await;
18702
18703 // Multiple edits
18704 assert_highlighted_edits(
18705 "Hello, world!",
18706 vec![
18707 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
18708 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
18709 ],
18710 false,
18711 cx,
18712 |highlighted_edits, cx| {
18713 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
18714 assert_eq!(highlighted_edits.highlights.len(), 2);
18715 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
18716 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
18717 assert_eq!(
18718 highlighted_edits.highlights[0].1.background_color,
18719 Some(cx.theme().status().created_background)
18720 );
18721 assert_eq!(
18722 highlighted_edits.highlights[1].1.background_color,
18723 Some(cx.theme().status().created_background)
18724 );
18725 },
18726 )
18727 .await;
18728
18729 // Multiple lines with edits
18730 assert_highlighted_edits(
18731 "First line\nSecond line\nThird line\nFourth line",
18732 vec![
18733 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
18734 (
18735 Point::new(2, 0)..Point::new(2, 10),
18736 "New third line".to_string(),
18737 ),
18738 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
18739 ],
18740 false,
18741 cx,
18742 |highlighted_edits, cx| {
18743 assert_eq!(
18744 highlighted_edits.text,
18745 "Second modified\nNew third line\nFourth updated line"
18746 );
18747 assert_eq!(highlighted_edits.highlights.len(), 3);
18748 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
18749 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
18750 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
18751 for highlight in &highlighted_edits.highlights {
18752 assert_eq!(
18753 highlight.1.background_color,
18754 Some(cx.theme().status().created_background)
18755 );
18756 }
18757 },
18758 )
18759 .await;
18760}
18761
18762#[gpui::test]
18763async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
18764 init_test(cx, |_| {});
18765
18766 // Deletion
18767 assert_highlighted_edits(
18768 "Hello, world!",
18769 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
18770 true,
18771 cx,
18772 |highlighted_edits, cx| {
18773 assert_eq!(highlighted_edits.text, "Hello, world!");
18774 assert_eq!(highlighted_edits.highlights.len(), 1);
18775 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
18776 assert_eq!(
18777 highlighted_edits.highlights[0].1.background_color,
18778 Some(cx.theme().status().deleted_background)
18779 );
18780 },
18781 )
18782 .await;
18783
18784 // Insertion
18785 assert_highlighted_edits(
18786 "Hello, world!",
18787 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
18788 true,
18789 cx,
18790 |highlighted_edits, cx| {
18791 assert_eq!(highlighted_edits.highlights.len(), 1);
18792 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
18793 assert_eq!(
18794 highlighted_edits.highlights[0].1.background_color,
18795 Some(cx.theme().status().created_background)
18796 );
18797 },
18798 )
18799 .await;
18800}
18801
18802async fn assert_highlighted_edits(
18803 text: &str,
18804 edits: Vec<(Range<Point>, String)>,
18805 include_deletions: bool,
18806 cx: &mut TestAppContext,
18807 assertion_fn: impl Fn(HighlightedText, &App),
18808) {
18809 let window = cx.add_window(|window, cx| {
18810 let buffer = MultiBuffer::build_simple(text, cx);
18811 Editor::new(EditorMode::full(), buffer, None, window, cx)
18812 });
18813 let cx = &mut VisualTestContext::from_window(*window, cx);
18814
18815 let (buffer, snapshot) = window
18816 .update(cx, |editor, _window, cx| {
18817 (
18818 editor.buffer().clone(),
18819 editor.buffer().read(cx).snapshot(cx),
18820 )
18821 })
18822 .unwrap();
18823
18824 let edits = edits
18825 .into_iter()
18826 .map(|(range, edit)| {
18827 (
18828 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
18829 edit,
18830 )
18831 })
18832 .collect::<Vec<_>>();
18833
18834 let text_anchor_edits = edits
18835 .clone()
18836 .into_iter()
18837 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
18838 .collect::<Vec<_>>();
18839
18840 let edit_preview = window
18841 .update(cx, |_, _window, cx| {
18842 buffer
18843 .read(cx)
18844 .as_singleton()
18845 .unwrap()
18846 .read(cx)
18847 .preview_edits(text_anchor_edits.into(), cx)
18848 })
18849 .unwrap()
18850 .await;
18851
18852 cx.update(|_window, cx| {
18853 let highlighted_edits = inline_completion_edit_text(
18854 &snapshot.as_singleton().unwrap().2,
18855 &edits,
18856 &edit_preview,
18857 include_deletions,
18858 cx,
18859 );
18860 assertion_fn(highlighted_edits, cx)
18861 });
18862}
18863
18864#[track_caller]
18865fn assert_breakpoint(
18866 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
18867 path: &Arc<Path>,
18868 expected: Vec<(u32, Breakpoint)>,
18869) {
18870 if expected.len() == 0usize {
18871 assert!(!breakpoints.contains_key(path), "{}", path.display());
18872 } else {
18873 let mut breakpoint = breakpoints
18874 .get(path)
18875 .unwrap()
18876 .into_iter()
18877 .map(|breakpoint| {
18878 (
18879 breakpoint.row,
18880 Breakpoint {
18881 message: breakpoint.message.clone(),
18882 state: breakpoint.state,
18883 condition: breakpoint.condition.clone(),
18884 hit_condition: breakpoint.hit_condition.clone(),
18885 },
18886 )
18887 })
18888 .collect::<Vec<_>>();
18889
18890 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
18891
18892 assert_eq!(expected, breakpoint);
18893 }
18894}
18895
18896fn add_log_breakpoint_at_cursor(
18897 editor: &mut Editor,
18898 log_message: &str,
18899 window: &mut Window,
18900 cx: &mut Context<Editor>,
18901) {
18902 let (anchor, bp) = editor
18903 .breakpoints_at_cursors(window, cx)
18904 .first()
18905 .and_then(|(anchor, bp)| {
18906 if let Some(bp) = bp {
18907 Some((*anchor, bp.clone()))
18908 } else {
18909 None
18910 }
18911 })
18912 .unwrap_or_else(|| {
18913 let cursor_position: Point = editor.selections.newest(cx).head();
18914
18915 let breakpoint_position = editor
18916 .snapshot(window, cx)
18917 .display_snapshot
18918 .buffer_snapshot
18919 .anchor_before(Point::new(cursor_position.row, 0));
18920
18921 (breakpoint_position, Breakpoint::new_log(&log_message))
18922 });
18923
18924 editor.edit_breakpoint_at_anchor(
18925 anchor,
18926 bp,
18927 BreakpointEditAction::EditLogMessage(log_message.into()),
18928 cx,
18929 );
18930}
18931
18932#[gpui::test]
18933async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
18934 init_test(cx, |_| {});
18935
18936 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
18937 let fs = FakeFs::new(cx.executor());
18938 fs.insert_tree(
18939 path!("/a"),
18940 json!({
18941 "main.rs": sample_text,
18942 }),
18943 )
18944 .await;
18945 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18946 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18947 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18948
18949 let fs = FakeFs::new(cx.executor());
18950 fs.insert_tree(
18951 path!("/a"),
18952 json!({
18953 "main.rs": sample_text,
18954 }),
18955 )
18956 .await;
18957 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18958 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18959 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18960 let worktree_id = workspace
18961 .update(cx, |workspace, _window, cx| {
18962 workspace.project().update(cx, |project, cx| {
18963 project.worktrees(cx).next().unwrap().read(cx).id()
18964 })
18965 })
18966 .unwrap();
18967
18968 let buffer = project
18969 .update(cx, |project, cx| {
18970 project.open_buffer((worktree_id, "main.rs"), cx)
18971 })
18972 .await
18973 .unwrap();
18974
18975 let (editor, cx) = cx.add_window_view(|window, cx| {
18976 Editor::new(
18977 EditorMode::full(),
18978 MultiBuffer::build_from_buffer(buffer, cx),
18979 Some(project.clone()),
18980 window,
18981 cx,
18982 )
18983 });
18984
18985 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
18986 let abs_path = project.read_with(cx, |project, cx| {
18987 project
18988 .absolute_path(&project_path, cx)
18989 .map(|path_buf| Arc::from(path_buf.to_owned()))
18990 .unwrap()
18991 });
18992
18993 // assert we can add breakpoint on the first line
18994 editor.update_in(cx, |editor, window, cx| {
18995 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18996 editor.move_to_end(&MoveToEnd, window, cx);
18997 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18998 });
18999
19000 let breakpoints = editor.update(cx, |editor, cx| {
19001 editor
19002 .breakpoint_store()
19003 .as_ref()
19004 .unwrap()
19005 .read(cx)
19006 .all_source_breakpoints(cx)
19007 .clone()
19008 });
19009
19010 assert_eq!(1, breakpoints.len());
19011 assert_breakpoint(
19012 &breakpoints,
19013 &abs_path,
19014 vec![
19015 (0, Breakpoint::new_standard()),
19016 (3, Breakpoint::new_standard()),
19017 ],
19018 );
19019
19020 editor.update_in(cx, |editor, window, cx| {
19021 editor.move_to_beginning(&MoveToBeginning, window, cx);
19022 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19023 });
19024
19025 let breakpoints = editor.update(cx, |editor, cx| {
19026 editor
19027 .breakpoint_store()
19028 .as_ref()
19029 .unwrap()
19030 .read(cx)
19031 .all_source_breakpoints(cx)
19032 .clone()
19033 });
19034
19035 assert_eq!(1, breakpoints.len());
19036 assert_breakpoint(
19037 &breakpoints,
19038 &abs_path,
19039 vec![(3, Breakpoint::new_standard())],
19040 );
19041
19042 editor.update_in(cx, |editor, window, cx| {
19043 editor.move_to_end(&MoveToEnd, window, cx);
19044 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19045 });
19046
19047 let breakpoints = editor.update(cx, |editor, cx| {
19048 editor
19049 .breakpoint_store()
19050 .as_ref()
19051 .unwrap()
19052 .read(cx)
19053 .all_source_breakpoints(cx)
19054 .clone()
19055 });
19056
19057 assert_eq!(0, breakpoints.len());
19058 assert_breakpoint(&breakpoints, &abs_path, vec![]);
19059}
19060
19061#[gpui::test]
19062async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
19063 init_test(cx, |_| {});
19064
19065 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
19066
19067 let fs = FakeFs::new(cx.executor());
19068 fs.insert_tree(
19069 path!("/a"),
19070 json!({
19071 "main.rs": sample_text,
19072 }),
19073 )
19074 .await;
19075 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19076 let (workspace, cx) =
19077 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
19078
19079 let worktree_id = workspace.update(cx, |workspace, cx| {
19080 workspace.project().update(cx, |project, cx| {
19081 project.worktrees(cx).next().unwrap().read(cx).id()
19082 })
19083 });
19084
19085 let buffer = project
19086 .update(cx, |project, cx| {
19087 project.open_buffer((worktree_id, "main.rs"), cx)
19088 })
19089 .await
19090 .unwrap();
19091
19092 let (editor, cx) = cx.add_window_view(|window, cx| {
19093 Editor::new(
19094 EditorMode::full(),
19095 MultiBuffer::build_from_buffer(buffer, cx),
19096 Some(project.clone()),
19097 window,
19098 cx,
19099 )
19100 });
19101
19102 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
19103 let abs_path = project.read_with(cx, |project, cx| {
19104 project
19105 .absolute_path(&project_path, cx)
19106 .map(|path_buf| Arc::from(path_buf.to_owned()))
19107 .unwrap()
19108 });
19109
19110 editor.update_in(cx, |editor, window, cx| {
19111 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
19112 });
19113
19114 let breakpoints = editor.update(cx, |editor, cx| {
19115 editor
19116 .breakpoint_store()
19117 .as_ref()
19118 .unwrap()
19119 .read(cx)
19120 .all_source_breakpoints(cx)
19121 .clone()
19122 });
19123
19124 assert_breakpoint(
19125 &breakpoints,
19126 &abs_path,
19127 vec![(0, Breakpoint::new_log("hello world"))],
19128 );
19129
19130 // Removing a log message from a log breakpoint should remove it
19131 editor.update_in(cx, |editor, window, cx| {
19132 add_log_breakpoint_at_cursor(editor, "", 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(&breakpoints, &abs_path, vec![]);
19146
19147 editor.update_in(cx, |editor, window, cx| {
19148 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19149 editor.move_to_end(&MoveToEnd, window, cx);
19150 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19151 // Not adding a log message to a standard breakpoint shouldn't remove it
19152 add_log_breakpoint_at_cursor(editor, "", window, cx);
19153 });
19154
19155 let breakpoints = editor.update(cx, |editor, cx| {
19156 editor
19157 .breakpoint_store()
19158 .as_ref()
19159 .unwrap()
19160 .read(cx)
19161 .all_source_breakpoints(cx)
19162 .clone()
19163 });
19164
19165 assert_breakpoint(
19166 &breakpoints,
19167 &abs_path,
19168 vec![
19169 (0, Breakpoint::new_standard()),
19170 (3, Breakpoint::new_standard()),
19171 ],
19172 );
19173
19174 editor.update_in(cx, |editor, window, cx| {
19175 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
19176 });
19177
19178 let breakpoints = editor.update(cx, |editor, cx| {
19179 editor
19180 .breakpoint_store()
19181 .as_ref()
19182 .unwrap()
19183 .read(cx)
19184 .all_source_breakpoints(cx)
19185 .clone()
19186 });
19187
19188 assert_breakpoint(
19189 &breakpoints,
19190 &abs_path,
19191 vec![
19192 (0, Breakpoint::new_standard()),
19193 (3, Breakpoint::new_log("hello world")),
19194 ],
19195 );
19196
19197 editor.update_in(cx, |editor, window, cx| {
19198 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
19199 });
19200
19201 let breakpoints = editor.update(cx, |editor, cx| {
19202 editor
19203 .breakpoint_store()
19204 .as_ref()
19205 .unwrap()
19206 .read(cx)
19207 .all_source_breakpoints(cx)
19208 .clone()
19209 });
19210
19211 assert_breakpoint(
19212 &breakpoints,
19213 &abs_path,
19214 vec![
19215 (0, Breakpoint::new_standard()),
19216 (3, Breakpoint::new_log("hello Earth!!")),
19217 ],
19218 );
19219}
19220
19221/// This also tests that Editor::breakpoint_at_cursor_head is working properly
19222/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
19223/// or when breakpoints were placed out of order. This tests for a regression too
19224#[gpui::test]
19225async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
19226 init_test(cx, |_| {});
19227
19228 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
19229 let fs = FakeFs::new(cx.executor());
19230 fs.insert_tree(
19231 path!("/a"),
19232 json!({
19233 "main.rs": sample_text,
19234 }),
19235 )
19236 .await;
19237 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19238 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19239 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19240
19241 let fs = FakeFs::new(cx.executor());
19242 fs.insert_tree(
19243 path!("/a"),
19244 json!({
19245 "main.rs": sample_text,
19246 }),
19247 )
19248 .await;
19249 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19250 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19251 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19252 let worktree_id = workspace
19253 .update(cx, |workspace, _window, cx| {
19254 workspace.project().update(cx, |project, cx| {
19255 project.worktrees(cx).next().unwrap().read(cx).id()
19256 })
19257 })
19258 .unwrap();
19259
19260 let buffer = project
19261 .update(cx, |project, cx| {
19262 project.open_buffer((worktree_id, "main.rs"), cx)
19263 })
19264 .await
19265 .unwrap();
19266
19267 let (editor, cx) = cx.add_window_view(|window, cx| {
19268 Editor::new(
19269 EditorMode::full(),
19270 MultiBuffer::build_from_buffer(buffer, cx),
19271 Some(project.clone()),
19272 window,
19273 cx,
19274 )
19275 });
19276
19277 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
19278 let abs_path = project.read_with(cx, |project, cx| {
19279 project
19280 .absolute_path(&project_path, cx)
19281 .map(|path_buf| Arc::from(path_buf.to_owned()))
19282 .unwrap()
19283 });
19284
19285 // assert we can add breakpoint on the first line
19286 editor.update_in(cx, |editor, window, cx| {
19287 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19288 editor.move_to_end(&MoveToEnd, window, cx);
19289 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19290 editor.move_up(&MoveUp, window, cx);
19291 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19292 });
19293
19294 let breakpoints = editor.update(cx, |editor, cx| {
19295 editor
19296 .breakpoint_store()
19297 .as_ref()
19298 .unwrap()
19299 .read(cx)
19300 .all_source_breakpoints(cx)
19301 .clone()
19302 });
19303
19304 assert_eq!(1, breakpoints.len());
19305 assert_breakpoint(
19306 &breakpoints,
19307 &abs_path,
19308 vec![
19309 (0, Breakpoint::new_standard()),
19310 (2, Breakpoint::new_standard()),
19311 (3, Breakpoint::new_standard()),
19312 ],
19313 );
19314
19315 editor.update_in(cx, |editor, window, cx| {
19316 editor.move_to_beginning(&MoveToBeginning, window, cx);
19317 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
19318 editor.move_to_end(&MoveToEnd, window, cx);
19319 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
19320 // Disabling a breakpoint that doesn't exist should do nothing
19321 editor.move_up(&MoveUp, window, cx);
19322 editor.move_up(&MoveUp, window, cx);
19323 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
19324 });
19325
19326 let breakpoints = editor.update(cx, |editor, cx| {
19327 editor
19328 .breakpoint_store()
19329 .as_ref()
19330 .unwrap()
19331 .read(cx)
19332 .all_source_breakpoints(cx)
19333 .clone()
19334 });
19335
19336 let disable_breakpoint = {
19337 let mut bp = Breakpoint::new_standard();
19338 bp.state = BreakpointState::Disabled;
19339 bp
19340 };
19341
19342 assert_eq!(1, breakpoints.len());
19343 assert_breakpoint(
19344 &breakpoints,
19345 &abs_path,
19346 vec![
19347 (0, disable_breakpoint.clone()),
19348 (2, Breakpoint::new_standard()),
19349 (3, disable_breakpoint.clone()),
19350 ],
19351 );
19352
19353 editor.update_in(cx, |editor, window, cx| {
19354 editor.move_to_beginning(&MoveToBeginning, window, cx);
19355 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
19356 editor.move_to_end(&MoveToEnd, window, cx);
19357 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
19358 editor.move_up(&MoveUp, window, cx);
19359 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
19360 });
19361
19362 let breakpoints = editor.update(cx, |editor, cx| {
19363 editor
19364 .breakpoint_store()
19365 .as_ref()
19366 .unwrap()
19367 .read(cx)
19368 .all_source_breakpoints(cx)
19369 .clone()
19370 });
19371
19372 assert_eq!(1, breakpoints.len());
19373 assert_breakpoint(
19374 &breakpoints,
19375 &abs_path,
19376 vec![
19377 (0, Breakpoint::new_standard()),
19378 (2, disable_breakpoint),
19379 (3, Breakpoint::new_standard()),
19380 ],
19381 );
19382}
19383
19384#[gpui::test]
19385async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
19386 init_test(cx, |_| {});
19387 let capabilities = lsp::ServerCapabilities {
19388 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
19389 prepare_provider: Some(true),
19390 work_done_progress_options: Default::default(),
19391 })),
19392 ..Default::default()
19393 };
19394 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
19395
19396 cx.set_state(indoc! {"
19397 struct Fˇoo {}
19398 "});
19399
19400 cx.update_editor(|editor, _, cx| {
19401 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
19402 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
19403 editor.highlight_background::<DocumentHighlightRead>(
19404 &[highlight_range],
19405 |c| c.editor_document_highlight_read_background,
19406 cx,
19407 );
19408 });
19409
19410 let mut prepare_rename_handler = cx
19411 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
19412 move |_, _, _| async move {
19413 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
19414 start: lsp::Position {
19415 line: 0,
19416 character: 7,
19417 },
19418 end: lsp::Position {
19419 line: 0,
19420 character: 10,
19421 },
19422 })))
19423 },
19424 );
19425 let prepare_rename_task = cx
19426 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
19427 .expect("Prepare rename was not started");
19428 prepare_rename_handler.next().await.unwrap();
19429 prepare_rename_task.await.expect("Prepare rename failed");
19430
19431 let mut rename_handler =
19432 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
19433 let edit = lsp::TextEdit {
19434 range: lsp::Range {
19435 start: lsp::Position {
19436 line: 0,
19437 character: 7,
19438 },
19439 end: lsp::Position {
19440 line: 0,
19441 character: 10,
19442 },
19443 },
19444 new_text: "FooRenamed".to_string(),
19445 };
19446 Ok(Some(lsp::WorkspaceEdit::new(
19447 // Specify the same edit twice
19448 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
19449 )))
19450 });
19451 let rename_task = cx
19452 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
19453 .expect("Confirm rename was not started");
19454 rename_handler.next().await.unwrap();
19455 rename_task.await.expect("Confirm rename failed");
19456 cx.run_until_parked();
19457
19458 // Despite two edits, only one is actually applied as those are identical
19459 cx.assert_editor_state(indoc! {"
19460 struct FooRenamedˇ {}
19461 "});
19462}
19463
19464#[gpui::test]
19465async fn test_rename_without_prepare(cx: &mut TestAppContext) {
19466 init_test(cx, |_| {});
19467 // These capabilities indicate that the server does not support prepare rename.
19468 let capabilities = lsp::ServerCapabilities {
19469 rename_provider: Some(lsp::OneOf::Left(true)),
19470 ..Default::default()
19471 };
19472 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
19473
19474 cx.set_state(indoc! {"
19475 struct Fˇoo {}
19476 "});
19477
19478 cx.update_editor(|editor, _window, cx| {
19479 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
19480 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
19481 editor.highlight_background::<DocumentHighlightRead>(
19482 &[highlight_range],
19483 |c| c.editor_document_highlight_read_background,
19484 cx,
19485 );
19486 });
19487
19488 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
19489 .expect("Prepare rename was not started")
19490 .await
19491 .expect("Prepare rename failed");
19492
19493 let mut rename_handler =
19494 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
19495 let edit = lsp::TextEdit {
19496 range: lsp::Range {
19497 start: lsp::Position {
19498 line: 0,
19499 character: 7,
19500 },
19501 end: lsp::Position {
19502 line: 0,
19503 character: 10,
19504 },
19505 },
19506 new_text: "FooRenamed".to_string(),
19507 };
19508 Ok(Some(lsp::WorkspaceEdit::new(
19509 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
19510 )))
19511 });
19512 let rename_task = cx
19513 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
19514 .expect("Confirm rename was not started");
19515 rename_handler.next().await.unwrap();
19516 rename_task.await.expect("Confirm rename failed");
19517 cx.run_until_parked();
19518
19519 // Correct range is renamed, as `surrounding_word` is used to find it.
19520 cx.assert_editor_state(indoc! {"
19521 struct FooRenamedˇ {}
19522 "});
19523}
19524
19525#[gpui::test]
19526async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
19527 init_test(cx, |_| {});
19528 let mut cx = EditorTestContext::new(cx).await;
19529
19530 let language = Arc::new(
19531 Language::new(
19532 LanguageConfig::default(),
19533 Some(tree_sitter_html::LANGUAGE.into()),
19534 )
19535 .with_brackets_query(
19536 r#"
19537 ("<" @open "/>" @close)
19538 ("</" @open ">" @close)
19539 ("<" @open ">" @close)
19540 ("\"" @open "\"" @close)
19541 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
19542 "#,
19543 )
19544 .unwrap(),
19545 );
19546 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
19547
19548 cx.set_state(indoc! {"
19549 <span>ˇ</span>
19550 "});
19551 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
19552 cx.assert_editor_state(indoc! {"
19553 <span>
19554 ˇ
19555 </span>
19556 "});
19557
19558 cx.set_state(indoc! {"
19559 <span><span></span>ˇ</span>
19560 "});
19561 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
19562 cx.assert_editor_state(indoc! {"
19563 <span><span></span>
19564 ˇ</span>
19565 "});
19566
19567 cx.set_state(indoc! {"
19568 <span>ˇ
19569 </span>
19570 "});
19571 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
19572 cx.assert_editor_state(indoc! {"
19573 <span>
19574 ˇ
19575 </span>
19576 "});
19577}
19578
19579#[gpui::test(iterations = 10)]
19580async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
19581 init_test(cx, |_| {});
19582
19583 let fs = FakeFs::new(cx.executor());
19584 fs.insert_tree(
19585 path!("/dir"),
19586 json!({
19587 "a.ts": "a",
19588 }),
19589 )
19590 .await;
19591
19592 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
19593 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19594 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19595
19596 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
19597 language_registry.add(Arc::new(Language::new(
19598 LanguageConfig {
19599 name: "TypeScript".into(),
19600 matcher: LanguageMatcher {
19601 path_suffixes: vec!["ts".to_string()],
19602 ..Default::default()
19603 },
19604 ..Default::default()
19605 },
19606 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
19607 )));
19608 let mut fake_language_servers = language_registry.register_fake_lsp(
19609 "TypeScript",
19610 FakeLspAdapter {
19611 capabilities: lsp::ServerCapabilities {
19612 code_lens_provider: Some(lsp::CodeLensOptions {
19613 resolve_provider: Some(true),
19614 }),
19615 execute_command_provider: Some(lsp::ExecuteCommandOptions {
19616 commands: vec!["_the/command".to_string()],
19617 ..lsp::ExecuteCommandOptions::default()
19618 }),
19619 ..lsp::ServerCapabilities::default()
19620 },
19621 ..FakeLspAdapter::default()
19622 },
19623 );
19624
19625 let (buffer, _handle) = project
19626 .update(cx, |p, cx| {
19627 p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
19628 })
19629 .await
19630 .unwrap();
19631 cx.executor().run_until_parked();
19632
19633 let fake_server = fake_language_servers.next().await.unwrap();
19634
19635 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
19636 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
19637 drop(buffer_snapshot);
19638 let actions = cx
19639 .update_window(*workspace, |_, window, cx| {
19640 project.code_actions(&buffer, anchor..anchor, window, cx)
19641 })
19642 .unwrap();
19643
19644 fake_server
19645 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
19646 Ok(Some(vec![
19647 lsp::CodeLens {
19648 range: lsp::Range::default(),
19649 command: Some(lsp::Command {
19650 title: "Code lens command".to_owned(),
19651 command: "_the/command".to_owned(),
19652 arguments: None,
19653 }),
19654 data: None,
19655 },
19656 lsp::CodeLens {
19657 range: lsp::Range::default(),
19658 command: Some(lsp::Command {
19659 title: "Command not in capabilities".to_owned(),
19660 command: "not in capabilities".to_owned(),
19661 arguments: None,
19662 }),
19663 data: None,
19664 },
19665 lsp::CodeLens {
19666 range: lsp::Range {
19667 start: lsp::Position {
19668 line: 1,
19669 character: 1,
19670 },
19671 end: lsp::Position {
19672 line: 1,
19673 character: 1,
19674 },
19675 },
19676 command: Some(lsp::Command {
19677 title: "Command not in range".to_owned(),
19678 command: "_the/command".to_owned(),
19679 arguments: None,
19680 }),
19681 data: None,
19682 },
19683 ]))
19684 })
19685 .next()
19686 .await;
19687
19688 let actions = actions.await.unwrap();
19689 assert_eq!(
19690 actions.len(),
19691 1,
19692 "Should have only one valid action for the 0..0 range"
19693 );
19694 let action = actions[0].clone();
19695 let apply = project.update(cx, |project, cx| {
19696 project.apply_code_action(buffer.clone(), action, true, cx)
19697 });
19698
19699 // Resolving the code action does not populate its edits. In absence of
19700 // edits, we must execute the given command.
19701 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
19702 |mut lens, _| async move {
19703 let lens_command = lens.command.as_mut().expect("should have a command");
19704 assert_eq!(lens_command.title, "Code lens command");
19705 lens_command.arguments = Some(vec![json!("the-argument")]);
19706 Ok(lens)
19707 },
19708 );
19709
19710 // While executing the command, the language server sends the editor
19711 // a `workspaceEdit` request.
19712 fake_server
19713 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
19714 let fake = fake_server.clone();
19715 move |params, _| {
19716 assert_eq!(params.command, "_the/command");
19717 let fake = fake.clone();
19718 async move {
19719 fake.server
19720 .request::<lsp::request::ApplyWorkspaceEdit>(
19721 lsp::ApplyWorkspaceEditParams {
19722 label: None,
19723 edit: lsp::WorkspaceEdit {
19724 changes: Some(
19725 [(
19726 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
19727 vec![lsp::TextEdit {
19728 range: lsp::Range::new(
19729 lsp::Position::new(0, 0),
19730 lsp::Position::new(0, 0),
19731 ),
19732 new_text: "X".into(),
19733 }],
19734 )]
19735 .into_iter()
19736 .collect(),
19737 ),
19738 ..Default::default()
19739 },
19740 },
19741 )
19742 .await
19743 .into_response()
19744 .unwrap();
19745 Ok(Some(json!(null)))
19746 }
19747 }
19748 })
19749 .next()
19750 .await;
19751
19752 // Applying the code lens command returns a project transaction containing the edits
19753 // sent by the language server in its `workspaceEdit` request.
19754 let transaction = apply.await.unwrap();
19755 assert!(transaction.0.contains_key(&buffer));
19756 buffer.update(cx, |buffer, cx| {
19757 assert_eq!(buffer.text(), "Xa");
19758 buffer.undo(cx);
19759 assert_eq!(buffer.text(), "a");
19760 });
19761}
19762
19763#[gpui::test]
19764async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
19765 init_test(cx, |_| {});
19766
19767 let fs = FakeFs::new(cx.executor());
19768 let main_text = r#"fn main() {
19769println!("1");
19770println!("2");
19771println!("3");
19772println!("4");
19773println!("5");
19774}"#;
19775 let lib_text = "mod foo {}";
19776 fs.insert_tree(
19777 path!("/a"),
19778 json!({
19779 "lib.rs": lib_text,
19780 "main.rs": main_text,
19781 }),
19782 )
19783 .await;
19784
19785 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19786 let (workspace, cx) =
19787 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
19788 let worktree_id = workspace.update(cx, |workspace, cx| {
19789 workspace.project().update(cx, |project, cx| {
19790 project.worktrees(cx).next().unwrap().read(cx).id()
19791 })
19792 });
19793
19794 let expected_ranges = vec![
19795 Point::new(0, 0)..Point::new(0, 0),
19796 Point::new(1, 0)..Point::new(1, 1),
19797 Point::new(2, 0)..Point::new(2, 2),
19798 Point::new(3, 0)..Point::new(3, 3),
19799 ];
19800
19801 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
19802 let editor_1 = workspace
19803 .update_in(cx, |workspace, window, cx| {
19804 workspace.open_path(
19805 (worktree_id, "main.rs"),
19806 Some(pane_1.downgrade()),
19807 true,
19808 window,
19809 cx,
19810 )
19811 })
19812 .unwrap()
19813 .await
19814 .downcast::<Editor>()
19815 .unwrap();
19816 pane_1.update(cx, |pane, cx| {
19817 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19818 open_editor.update(cx, |editor, cx| {
19819 assert_eq!(
19820 editor.display_text(cx),
19821 main_text,
19822 "Original main.rs text on initial open",
19823 );
19824 assert_eq!(
19825 editor
19826 .selections
19827 .all::<Point>(cx)
19828 .into_iter()
19829 .map(|s| s.range())
19830 .collect::<Vec<_>>(),
19831 vec![Point::zero()..Point::zero()],
19832 "Default selections on initial open",
19833 );
19834 })
19835 });
19836 editor_1.update_in(cx, |editor, window, cx| {
19837 editor.change_selections(None, window, cx, |s| {
19838 s.select_ranges(expected_ranges.clone());
19839 });
19840 });
19841
19842 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
19843 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
19844 });
19845 let editor_2 = workspace
19846 .update_in(cx, |workspace, window, cx| {
19847 workspace.open_path(
19848 (worktree_id, "main.rs"),
19849 Some(pane_2.downgrade()),
19850 true,
19851 window,
19852 cx,
19853 )
19854 })
19855 .unwrap()
19856 .await
19857 .downcast::<Editor>()
19858 .unwrap();
19859 pane_2.update(cx, |pane, cx| {
19860 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19861 open_editor.update(cx, |editor, cx| {
19862 assert_eq!(
19863 editor.display_text(cx),
19864 main_text,
19865 "Original main.rs text on initial open in another panel",
19866 );
19867 assert_eq!(
19868 editor
19869 .selections
19870 .all::<Point>(cx)
19871 .into_iter()
19872 .map(|s| s.range())
19873 .collect::<Vec<_>>(),
19874 vec![Point::zero()..Point::zero()],
19875 "Default selections on initial open in another panel",
19876 );
19877 })
19878 });
19879
19880 editor_2.update_in(cx, |editor, window, cx| {
19881 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
19882 });
19883
19884 let _other_editor_1 = workspace
19885 .update_in(cx, |workspace, window, cx| {
19886 workspace.open_path(
19887 (worktree_id, "lib.rs"),
19888 Some(pane_1.downgrade()),
19889 true,
19890 window,
19891 cx,
19892 )
19893 })
19894 .unwrap()
19895 .await
19896 .downcast::<Editor>()
19897 .unwrap();
19898 pane_1
19899 .update_in(cx, |pane, window, cx| {
19900 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
19901 .unwrap()
19902 })
19903 .await
19904 .unwrap();
19905 drop(editor_1);
19906 pane_1.update(cx, |pane, cx| {
19907 pane.active_item()
19908 .unwrap()
19909 .downcast::<Editor>()
19910 .unwrap()
19911 .update(cx, |editor, cx| {
19912 assert_eq!(
19913 editor.display_text(cx),
19914 lib_text,
19915 "Other file should be open and active",
19916 );
19917 });
19918 assert_eq!(pane.items().count(), 1, "No other editors should be open");
19919 });
19920
19921 let _other_editor_2 = workspace
19922 .update_in(cx, |workspace, window, cx| {
19923 workspace.open_path(
19924 (worktree_id, "lib.rs"),
19925 Some(pane_2.downgrade()),
19926 true,
19927 window,
19928 cx,
19929 )
19930 })
19931 .unwrap()
19932 .await
19933 .downcast::<Editor>()
19934 .unwrap();
19935 pane_2
19936 .update_in(cx, |pane, window, cx| {
19937 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
19938 .unwrap()
19939 })
19940 .await
19941 .unwrap();
19942 drop(editor_2);
19943 pane_2.update(cx, |pane, cx| {
19944 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19945 open_editor.update(cx, |editor, cx| {
19946 assert_eq!(
19947 editor.display_text(cx),
19948 lib_text,
19949 "Other file should be open and active in another panel too",
19950 );
19951 });
19952 assert_eq!(
19953 pane.items().count(),
19954 1,
19955 "No other editors should be open in another pane",
19956 );
19957 });
19958
19959 let _editor_1_reopened = workspace
19960 .update_in(cx, |workspace, window, cx| {
19961 workspace.open_path(
19962 (worktree_id, "main.rs"),
19963 Some(pane_1.downgrade()),
19964 true,
19965 window,
19966 cx,
19967 )
19968 })
19969 .unwrap()
19970 .await
19971 .downcast::<Editor>()
19972 .unwrap();
19973 let _editor_2_reopened = workspace
19974 .update_in(cx, |workspace, window, cx| {
19975 workspace.open_path(
19976 (worktree_id, "main.rs"),
19977 Some(pane_2.downgrade()),
19978 true,
19979 window,
19980 cx,
19981 )
19982 })
19983 .unwrap()
19984 .await
19985 .downcast::<Editor>()
19986 .unwrap();
19987 pane_1.update(cx, |pane, cx| {
19988 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19989 open_editor.update(cx, |editor, cx| {
19990 assert_eq!(
19991 editor.display_text(cx),
19992 main_text,
19993 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
19994 );
19995 assert_eq!(
19996 editor
19997 .selections
19998 .all::<Point>(cx)
19999 .into_iter()
20000 .map(|s| s.range())
20001 .collect::<Vec<_>>(),
20002 expected_ranges,
20003 "Previous editor in the 1st panel had selections and should get them restored on reopen",
20004 );
20005 })
20006 });
20007 pane_2.update(cx, |pane, cx| {
20008 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20009 open_editor.update(cx, |editor, cx| {
20010 assert_eq!(
20011 editor.display_text(cx),
20012 r#"fn main() {
20013⋯rintln!("1");
20014⋯intln!("2");
20015⋯ntln!("3");
20016println!("4");
20017println!("5");
20018}"#,
20019 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
20020 );
20021 assert_eq!(
20022 editor
20023 .selections
20024 .all::<Point>(cx)
20025 .into_iter()
20026 .map(|s| s.range())
20027 .collect::<Vec<_>>(),
20028 vec![Point::zero()..Point::zero()],
20029 "Previous editor in the 2nd pane had no selections changed hence should restore none",
20030 );
20031 })
20032 });
20033}
20034
20035#[gpui::test]
20036async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
20037 init_test(cx, |_| {});
20038
20039 let fs = FakeFs::new(cx.executor());
20040 let main_text = r#"fn main() {
20041println!("1");
20042println!("2");
20043println!("3");
20044println!("4");
20045println!("5");
20046}"#;
20047 let lib_text = "mod foo {}";
20048 fs.insert_tree(
20049 path!("/a"),
20050 json!({
20051 "lib.rs": lib_text,
20052 "main.rs": main_text,
20053 }),
20054 )
20055 .await;
20056
20057 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20058 let (workspace, cx) =
20059 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20060 let worktree_id = workspace.update(cx, |workspace, cx| {
20061 workspace.project().update(cx, |project, cx| {
20062 project.worktrees(cx).next().unwrap().read(cx).id()
20063 })
20064 });
20065
20066 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
20067 let editor = workspace
20068 .update_in(cx, |workspace, window, cx| {
20069 workspace.open_path(
20070 (worktree_id, "main.rs"),
20071 Some(pane.downgrade()),
20072 true,
20073 window,
20074 cx,
20075 )
20076 })
20077 .unwrap()
20078 .await
20079 .downcast::<Editor>()
20080 .unwrap();
20081 pane.update(cx, |pane, cx| {
20082 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20083 open_editor.update(cx, |editor, cx| {
20084 assert_eq!(
20085 editor.display_text(cx),
20086 main_text,
20087 "Original main.rs text on initial open",
20088 );
20089 })
20090 });
20091 editor.update_in(cx, |editor, window, cx| {
20092 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
20093 });
20094
20095 cx.update_global(|store: &mut SettingsStore, cx| {
20096 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
20097 s.restore_on_file_reopen = Some(false);
20098 });
20099 });
20100 editor.update_in(cx, |editor, window, cx| {
20101 editor.fold_ranges(
20102 vec![
20103 Point::new(1, 0)..Point::new(1, 1),
20104 Point::new(2, 0)..Point::new(2, 2),
20105 Point::new(3, 0)..Point::new(3, 3),
20106 ],
20107 false,
20108 window,
20109 cx,
20110 );
20111 });
20112 pane.update_in(cx, |pane, window, cx| {
20113 pane.close_all_items(&CloseAllItems::default(), window, cx)
20114 .unwrap()
20115 })
20116 .await
20117 .unwrap();
20118 pane.update(cx, |pane, _| {
20119 assert!(pane.active_item().is_none());
20120 });
20121 cx.update_global(|store: &mut SettingsStore, cx| {
20122 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
20123 s.restore_on_file_reopen = Some(true);
20124 });
20125 });
20126
20127 let _editor_reopened = workspace
20128 .update_in(cx, |workspace, window, cx| {
20129 workspace.open_path(
20130 (worktree_id, "main.rs"),
20131 Some(pane.downgrade()),
20132 true,
20133 window,
20134 cx,
20135 )
20136 })
20137 .unwrap()
20138 .await
20139 .downcast::<Editor>()
20140 .unwrap();
20141 pane.update(cx, |pane, cx| {
20142 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20143 open_editor.update(cx, |editor, cx| {
20144 assert_eq!(
20145 editor.display_text(cx),
20146 main_text,
20147 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
20148 );
20149 })
20150 });
20151}
20152
20153#[gpui::test]
20154async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
20155 struct EmptyModalView {
20156 focus_handle: gpui::FocusHandle,
20157 }
20158 impl EventEmitter<DismissEvent> for EmptyModalView {}
20159 impl Render for EmptyModalView {
20160 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
20161 div()
20162 }
20163 }
20164 impl Focusable for EmptyModalView {
20165 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
20166 self.focus_handle.clone()
20167 }
20168 }
20169 impl workspace::ModalView for EmptyModalView {}
20170 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
20171 EmptyModalView {
20172 focus_handle: cx.focus_handle(),
20173 }
20174 }
20175
20176 init_test(cx, |_| {});
20177
20178 let fs = FakeFs::new(cx.executor());
20179 let project = Project::test(fs, [], cx).await;
20180 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20181 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
20182 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20183 let editor = cx.new_window_entity(|window, cx| {
20184 Editor::new(
20185 EditorMode::full(),
20186 buffer,
20187 Some(project.clone()),
20188 window,
20189 cx,
20190 )
20191 });
20192 workspace
20193 .update(cx, |workspace, window, cx| {
20194 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
20195 })
20196 .unwrap();
20197 editor.update_in(cx, |editor, window, cx| {
20198 editor.open_context_menu(&OpenContextMenu, window, cx);
20199 assert!(editor.mouse_context_menu.is_some());
20200 });
20201 workspace
20202 .update(cx, |workspace, window, cx| {
20203 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
20204 })
20205 .unwrap();
20206 cx.read(|cx| {
20207 assert!(editor.read(cx).mouse_context_menu.is_none());
20208 });
20209}
20210
20211#[gpui::test]
20212async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
20213 init_test(cx, |_| {});
20214
20215 let fs = FakeFs::new(cx.executor());
20216 fs.insert_file(path!("/file.html"), Default::default())
20217 .await;
20218
20219 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
20220
20221 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
20222 let html_language = Arc::new(Language::new(
20223 LanguageConfig {
20224 name: "HTML".into(),
20225 matcher: LanguageMatcher {
20226 path_suffixes: vec!["html".to_string()],
20227 ..LanguageMatcher::default()
20228 },
20229 brackets: BracketPairConfig {
20230 pairs: vec![BracketPair {
20231 start: "<".into(),
20232 end: ">".into(),
20233 close: true,
20234 ..Default::default()
20235 }],
20236 ..Default::default()
20237 },
20238 ..Default::default()
20239 },
20240 Some(tree_sitter_html::LANGUAGE.into()),
20241 ));
20242 language_registry.add(html_language);
20243 let mut fake_servers = language_registry.register_fake_lsp(
20244 "HTML",
20245 FakeLspAdapter {
20246 capabilities: lsp::ServerCapabilities {
20247 completion_provider: Some(lsp::CompletionOptions {
20248 resolve_provider: Some(true),
20249 ..Default::default()
20250 }),
20251 ..Default::default()
20252 },
20253 ..Default::default()
20254 },
20255 );
20256
20257 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20258 let cx = &mut VisualTestContext::from_window(*workspace, cx);
20259
20260 let worktree_id = workspace
20261 .update(cx, |workspace, _window, cx| {
20262 workspace.project().update(cx, |project, cx| {
20263 project.worktrees(cx).next().unwrap().read(cx).id()
20264 })
20265 })
20266 .unwrap();
20267 project
20268 .update(cx, |project, cx| {
20269 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
20270 })
20271 .await
20272 .unwrap();
20273 let editor = workspace
20274 .update(cx, |workspace, window, cx| {
20275 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
20276 })
20277 .unwrap()
20278 .await
20279 .unwrap()
20280 .downcast::<Editor>()
20281 .unwrap();
20282
20283 let fake_server = fake_servers.next().await.unwrap();
20284 editor.update_in(cx, |editor, window, cx| {
20285 editor.set_text("<ad></ad>", window, cx);
20286 editor.change_selections(None, window, cx, |selections| {
20287 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
20288 });
20289 let Some((buffer, _)) = editor
20290 .buffer
20291 .read(cx)
20292 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
20293 else {
20294 panic!("Failed to get buffer for selection position");
20295 };
20296 let buffer = buffer.read(cx);
20297 let buffer_id = buffer.remote_id();
20298 let opening_range =
20299 buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3));
20300 let closing_range =
20301 buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
20302 let mut linked_ranges = HashMap::default();
20303 linked_ranges.insert(
20304 buffer_id,
20305 vec![(opening_range.clone(), vec![closing_range.clone()])],
20306 );
20307 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
20308 });
20309 let mut completion_handle =
20310 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
20311 Ok(Some(lsp::CompletionResponse::Array(vec![
20312 lsp::CompletionItem {
20313 label: "head".to_string(),
20314 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
20315 lsp::InsertReplaceEdit {
20316 new_text: "head".to_string(),
20317 insert: lsp::Range::new(
20318 lsp::Position::new(0, 1),
20319 lsp::Position::new(0, 3),
20320 ),
20321 replace: lsp::Range::new(
20322 lsp::Position::new(0, 1),
20323 lsp::Position::new(0, 3),
20324 ),
20325 },
20326 )),
20327 ..Default::default()
20328 },
20329 ])))
20330 });
20331 editor.update_in(cx, |editor, window, cx| {
20332 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
20333 });
20334 cx.run_until_parked();
20335 completion_handle.next().await.unwrap();
20336 editor.update(cx, |editor, _| {
20337 assert!(
20338 editor.context_menu_visible(),
20339 "Completion menu should be visible"
20340 );
20341 });
20342 editor.update_in(cx, |editor, window, cx| {
20343 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
20344 });
20345 cx.executor().run_until_parked();
20346 editor.update(cx, |editor, cx| {
20347 assert_eq!(editor.text(cx), "<head></head>");
20348 });
20349}
20350
20351#[gpui::test]
20352async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
20353 init_test(cx, |_| {});
20354
20355 let fs = FakeFs::new(cx.executor());
20356 fs.insert_tree(
20357 path!("/root"),
20358 json!({
20359 "a": {
20360 "main.rs": "fn main() {}",
20361 },
20362 "foo": {
20363 "bar": {
20364 "external_file.rs": "pub mod external {}",
20365 }
20366 }
20367 }),
20368 )
20369 .await;
20370
20371 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
20372 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
20373 language_registry.add(rust_lang());
20374 let _fake_servers = language_registry.register_fake_lsp(
20375 "Rust",
20376 FakeLspAdapter {
20377 ..FakeLspAdapter::default()
20378 },
20379 );
20380 let (workspace, cx) =
20381 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20382 let worktree_id = workspace.update(cx, |workspace, cx| {
20383 workspace.project().update(cx, |project, cx| {
20384 project.worktrees(cx).next().unwrap().read(cx).id()
20385 })
20386 });
20387
20388 let assert_language_servers_count =
20389 |expected: usize, context: &str, cx: &mut VisualTestContext| {
20390 project.update(cx, |project, cx| {
20391 let current = project
20392 .lsp_store()
20393 .read(cx)
20394 .as_local()
20395 .unwrap()
20396 .language_servers
20397 .len();
20398 assert_eq!(expected, current, "{context}");
20399 });
20400 };
20401
20402 assert_language_servers_count(
20403 0,
20404 "No servers should be running before any file is open",
20405 cx,
20406 );
20407 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
20408 let main_editor = workspace
20409 .update_in(cx, |workspace, window, cx| {
20410 workspace.open_path(
20411 (worktree_id, "main.rs"),
20412 Some(pane.downgrade()),
20413 true,
20414 window,
20415 cx,
20416 )
20417 })
20418 .unwrap()
20419 .await
20420 .downcast::<Editor>()
20421 .unwrap();
20422 pane.update(cx, |pane, cx| {
20423 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20424 open_editor.update(cx, |editor, cx| {
20425 assert_eq!(
20426 editor.display_text(cx),
20427 "fn main() {}",
20428 "Original main.rs text on initial open",
20429 );
20430 });
20431 assert_eq!(open_editor, main_editor);
20432 });
20433 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
20434
20435 let external_editor = workspace
20436 .update_in(cx, |workspace, window, cx| {
20437 workspace.open_abs_path(
20438 PathBuf::from("/root/foo/bar/external_file.rs"),
20439 OpenOptions::default(),
20440 window,
20441 cx,
20442 )
20443 })
20444 .await
20445 .expect("opening external file")
20446 .downcast::<Editor>()
20447 .expect("downcasted external file's open element to editor");
20448 pane.update(cx, |pane, cx| {
20449 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20450 open_editor.update(cx, |editor, cx| {
20451 assert_eq!(
20452 editor.display_text(cx),
20453 "pub mod external {}",
20454 "External file is open now",
20455 );
20456 });
20457 assert_eq!(open_editor, external_editor);
20458 });
20459 assert_language_servers_count(
20460 1,
20461 "Second, external, *.rs file should join the existing server",
20462 cx,
20463 );
20464
20465 pane.update_in(cx, |pane, window, cx| {
20466 pane.close_active_item(&CloseActiveItem::default(), window, cx)
20467 })
20468 .unwrap()
20469 .await
20470 .unwrap();
20471 pane.update_in(cx, |pane, window, cx| {
20472 pane.navigate_backward(window, cx);
20473 });
20474 cx.run_until_parked();
20475 pane.update(cx, |pane, cx| {
20476 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20477 open_editor.update(cx, |editor, cx| {
20478 assert_eq!(
20479 editor.display_text(cx),
20480 "pub mod external {}",
20481 "External file is open now",
20482 );
20483 });
20484 });
20485 assert_language_servers_count(
20486 1,
20487 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
20488 cx,
20489 );
20490
20491 cx.update(|_, cx| {
20492 workspace::reload(&workspace::Reload::default(), cx);
20493 });
20494 assert_language_servers_count(
20495 1,
20496 "After reloading the worktree with local and external files opened, only one project should be started",
20497 cx,
20498 );
20499}
20500
20501#[gpui::test]
20502async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
20503 init_test(cx, |_| {});
20504
20505 let mut cx = EditorTestContext::new(cx).await;
20506 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
20507 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
20508
20509 // test cursor move to start of each line on tab
20510 // for `if`, `elif`, `else`, `while`, `with` and `for`
20511 cx.set_state(indoc! {"
20512 def main():
20513 ˇ for item in items:
20514 ˇ while item.active:
20515 ˇ if item.value > 10:
20516 ˇ continue
20517 ˇ elif item.value < 0:
20518 ˇ break
20519 ˇ else:
20520 ˇ with item.context() as ctx:
20521 ˇ yield count
20522 ˇ else:
20523 ˇ log('while else')
20524 ˇ else:
20525 ˇ log('for else')
20526 "});
20527 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
20528 cx.assert_editor_state(indoc! {"
20529 def main():
20530 ˇfor item in items:
20531 ˇwhile item.active:
20532 ˇif item.value > 10:
20533 ˇcontinue
20534 ˇelif item.value < 0:
20535 ˇbreak
20536 ˇelse:
20537 ˇwith item.context() as ctx:
20538 ˇyield count
20539 ˇelse:
20540 ˇlog('while else')
20541 ˇelse:
20542 ˇlog('for else')
20543 "});
20544 // test relative indent is preserved when tab
20545 // for `if`, `elif`, `else`, `while`, `with` and `for`
20546 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
20547 cx.assert_editor_state(indoc! {"
20548 def main():
20549 ˇfor item in items:
20550 ˇwhile item.active:
20551 ˇif item.value > 10:
20552 ˇcontinue
20553 ˇelif item.value < 0:
20554 ˇbreak
20555 ˇelse:
20556 ˇwith item.context() as ctx:
20557 ˇyield count
20558 ˇelse:
20559 ˇlog('while else')
20560 ˇelse:
20561 ˇlog('for else')
20562 "});
20563
20564 // test cursor move to start of each line on tab
20565 // for `try`, `except`, `else`, `finally`, `match` and `def`
20566 cx.set_state(indoc! {"
20567 def main():
20568 ˇ try:
20569 ˇ fetch()
20570 ˇ except ValueError:
20571 ˇ handle_error()
20572 ˇ else:
20573 ˇ match value:
20574 ˇ case _:
20575 ˇ finally:
20576 ˇ def status():
20577 ˇ return 0
20578 "});
20579 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
20580 cx.assert_editor_state(indoc! {"
20581 def main():
20582 ˇtry:
20583 ˇfetch()
20584 ˇexcept ValueError:
20585 ˇhandle_error()
20586 ˇelse:
20587 ˇmatch value:
20588 ˇcase _:
20589 ˇfinally:
20590 ˇdef status():
20591 ˇreturn 0
20592 "});
20593 // test relative indent is preserved when tab
20594 // for `try`, `except`, `else`, `finally`, `match` and `def`
20595 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
20596 cx.assert_editor_state(indoc! {"
20597 def main():
20598 ˇtry:
20599 ˇfetch()
20600 ˇexcept ValueError:
20601 ˇhandle_error()
20602 ˇelse:
20603 ˇmatch value:
20604 ˇcase _:
20605 ˇfinally:
20606 ˇdef status():
20607 ˇreturn 0
20608 "});
20609}
20610
20611#[gpui::test]
20612async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
20613 init_test(cx, |_| {});
20614
20615 let mut cx = EditorTestContext::new(cx).await;
20616 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
20617 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
20618
20619 // test `else` auto outdents when typed inside `if` block
20620 cx.set_state(indoc! {"
20621 def main():
20622 if i == 2:
20623 return
20624 ˇ
20625 "});
20626 cx.update_editor(|editor, window, cx| {
20627 editor.handle_input("else:", window, cx);
20628 });
20629 cx.assert_editor_state(indoc! {"
20630 def main():
20631 if i == 2:
20632 return
20633 else:ˇ
20634 "});
20635
20636 // test `except` auto outdents when typed inside `try` block
20637 cx.set_state(indoc! {"
20638 def main():
20639 try:
20640 i = 2
20641 ˇ
20642 "});
20643 cx.update_editor(|editor, window, cx| {
20644 editor.handle_input("except:", window, cx);
20645 });
20646 cx.assert_editor_state(indoc! {"
20647 def main():
20648 try:
20649 i = 2
20650 except:ˇ
20651 "});
20652
20653 // test `else` auto outdents when typed inside `except` block
20654 cx.set_state(indoc! {"
20655 def main():
20656 try:
20657 i = 2
20658 except:
20659 j = 2
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 j = 2
20671 else:ˇ
20672 "});
20673
20674 // test `finally` auto outdents when typed inside `else` block
20675 cx.set_state(indoc! {"
20676 def main():
20677 try:
20678 i = 2
20679 except:
20680 j = 2
20681 else:
20682 k = 2
20683 ˇ
20684 "});
20685 cx.update_editor(|editor, window, cx| {
20686 editor.handle_input("finally:", window, cx);
20687 });
20688 cx.assert_editor_state(indoc! {"
20689 def main():
20690 try:
20691 i = 2
20692 except:
20693 j = 2
20694 else:
20695 k = 2
20696 finally:ˇ
20697 "});
20698
20699 // TODO: test `except` auto outdents when typed inside `try` block right after for block
20700 // cx.set_state(indoc! {"
20701 // def main():
20702 // try:
20703 // for i in range(n):
20704 // pass
20705 // ˇ
20706 // "});
20707 // cx.update_editor(|editor, window, cx| {
20708 // editor.handle_input("except:", window, cx);
20709 // });
20710 // cx.assert_editor_state(indoc! {"
20711 // def main():
20712 // try:
20713 // for i in range(n):
20714 // pass
20715 // except:ˇ
20716 // "});
20717
20718 // TODO: test `else` auto outdents when typed inside `except` block right after for block
20719 // cx.set_state(indoc! {"
20720 // def main():
20721 // try:
20722 // i = 2
20723 // except:
20724 // for i in range(n):
20725 // pass
20726 // ˇ
20727 // "});
20728 // cx.update_editor(|editor, window, cx| {
20729 // editor.handle_input("else:", window, cx);
20730 // });
20731 // cx.assert_editor_state(indoc! {"
20732 // def main():
20733 // try:
20734 // i = 2
20735 // except:
20736 // for i in range(n):
20737 // pass
20738 // else:ˇ
20739 // "});
20740
20741 // TODO: test `finally` auto outdents when typed inside `else` block right after for block
20742 // cx.set_state(indoc! {"
20743 // def main():
20744 // try:
20745 // i = 2
20746 // except:
20747 // j = 2
20748 // else:
20749 // for i in range(n):
20750 // pass
20751 // ˇ
20752 // "});
20753 // cx.update_editor(|editor, window, cx| {
20754 // editor.handle_input("finally:", window, cx);
20755 // });
20756 // cx.assert_editor_state(indoc! {"
20757 // def main():
20758 // try:
20759 // i = 2
20760 // except:
20761 // j = 2
20762 // else:
20763 // for i in range(n):
20764 // pass
20765 // finally:ˇ
20766 // "});
20767
20768 // test `else` stays at correct indent when typed after `for` block
20769 cx.set_state(indoc! {"
20770 def main():
20771 for i in range(10):
20772 if i == 3:
20773 break
20774 ˇ
20775 "});
20776 cx.update_editor(|editor, window, cx| {
20777 editor.handle_input("else:", window, cx);
20778 });
20779 cx.assert_editor_state(indoc! {"
20780 def main():
20781 for i in range(10):
20782 if i == 3:
20783 break
20784 else:ˇ
20785 "});
20786}
20787
20788#[gpui::test]
20789async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
20790 init_test(cx, |_| {});
20791 update_test_language_settings(cx, |settings| {
20792 settings.defaults.extend_comment_on_newline = Some(false);
20793 });
20794 let mut cx = EditorTestContext::new(cx).await;
20795 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
20796 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
20797
20798 // test correct indent after newline on comment
20799 cx.set_state(indoc! {"
20800 # COMMENT:ˇ
20801 "});
20802 cx.update_editor(|editor, window, cx| {
20803 editor.newline(&Newline, window, cx);
20804 });
20805 cx.assert_editor_state(indoc! {"
20806 # COMMENT:
20807 ˇ
20808 "});
20809
20810 // test correct indent after newline in brackets
20811 cx.set_state(indoc! {"
20812 {ˇ}
20813 "});
20814 cx.update_editor(|editor, window, cx| {
20815 editor.newline(&Newline, window, cx);
20816 });
20817 cx.run_until_parked();
20818 cx.assert_editor_state(indoc! {"
20819 {
20820 ˇ
20821 }
20822 "});
20823
20824 cx.set_state(indoc! {"
20825 (ˇ)
20826 "});
20827 cx.update_editor(|editor, window, cx| {
20828 editor.newline(&Newline, window, cx);
20829 });
20830 cx.run_until_parked();
20831 cx.assert_editor_state(indoc! {"
20832 (
20833 ˇ
20834 )
20835 "});
20836
20837 // do not indent after empty lists or dictionaries
20838 cx.set_state(indoc! {"
20839 a = []ˇ
20840 "});
20841 cx.update_editor(|editor, window, cx| {
20842 editor.newline(&Newline, window, cx);
20843 });
20844 cx.run_until_parked();
20845 cx.assert_editor_state(indoc! {"
20846 a = []
20847 ˇ
20848 "});
20849}
20850
20851fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
20852 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
20853 point..point
20854}
20855
20856fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
20857 let (text, ranges) = marked_text_ranges(marked_text, true);
20858 assert_eq!(editor.text(cx), text);
20859 assert_eq!(
20860 editor.selections.ranges(cx),
20861 ranges,
20862 "Assert selections are {}",
20863 marked_text
20864 );
20865}
20866
20867pub fn handle_signature_help_request(
20868 cx: &mut EditorLspTestContext,
20869 mocked_response: lsp::SignatureHelp,
20870) -> impl Future<Output = ()> + use<> {
20871 let mut request =
20872 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
20873 let mocked_response = mocked_response.clone();
20874 async move { Ok(Some(mocked_response)) }
20875 });
20876
20877 async move {
20878 request.next().await;
20879 }
20880}
20881
20882/// Handle completion request passing a marked string specifying where the completion
20883/// should be triggered from using '|' character, what range should be replaced, and what completions
20884/// should be returned using '<' and '>' to delimit the range.
20885///
20886/// Also see `handle_completion_request_with_insert_and_replace`.
20887#[track_caller]
20888pub fn handle_completion_request(
20889 cx: &mut EditorLspTestContext,
20890 marked_string: &str,
20891 completions: Vec<&'static str>,
20892 counter: Arc<AtomicUsize>,
20893) -> impl Future<Output = ()> {
20894 let complete_from_marker: TextRangeMarker = '|'.into();
20895 let replace_range_marker: TextRangeMarker = ('<', '>').into();
20896 let (_, mut marked_ranges) = marked_text_ranges_by(
20897 marked_string,
20898 vec![complete_from_marker.clone(), replace_range_marker.clone()],
20899 );
20900
20901 let complete_from_position =
20902 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
20903 let replace_range =
20904 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
20905
20906 let mut request =
20907 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
20908 let completions = completions.clone();
20909 counter.fetch_add(1, atomic::Ordering::Release);
20910 async move {
20911 assert_eq!(params.text_document_position.text_document.uri, url.clone());
20912 assert_eq!(
20913 params.text_document_position.position,
20914 complete_from_position
20915 );
20916 Ok(Some(lsp::CompletionResponse::Array(
20917 completions
20918 .iter()
20919 .map(|completion_text| lsp::CompletionItem {
20920 label: completion_text.to_string(),
20921 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
20922 range: replace_range,
20923 new_text: completion_text.to_string(),
20924 })),
20925 ..Default::default()
20926 })
20927 .collect(),
20928 )))
20929 }
20930 });
20931
20932 async move {
20933 request.next().await;
20934 }
20935}
20936
20937/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
20938/// given instead, which also contains an `insert` range.
20939///
20940/// This function uses the cursor position to mimic what Rust-Analyzer provides as the `insert` range,
20941/// that is, `replace_range.start..cursor_pos`.
20942pub fn handle_completion_request_with_insert_and_replace(
20943 cx: &mut EditorLspTestContext,
20944 marked_string: &str,
20945 completions: Vec<&'static str>,
20946 counter: Arc<AtomicUsize>,
20947) -> impl Future<Output = ()> {
20948 let complete_from_marker: TextRangeMarker = '|'.into();
20949 let replace_range_marker: TextRangeMarker = ('<', '>').into();
20950 let (_, mut marked_ranges) = marked_text_ranges_by(
20951 marked_string,
20952 vec![complete_from_marker.clone(), replace_range_marker.clone()],
20953 );
20954
20955 let complete_from_position =
20956 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
20957 let replace_range =
20958 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
20959
20960 let mut request =
20961 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
20962 let completions = completions.clone();
20963 counter.fetch_add(1, atomic::Ordering::Release);
20964 async move {
20965 assert_eq!(params.text_document_position.text_document.uri, url.clone());
20966 assert_eq!(
20967 params.text_document_position.position, complete_from_position,
20968 "marker `|` position doesn't match",
20969 );
20970 Ok(Some(lsp::CompletionResponse::Array(
20971 completions
20972 .iter()
20973 .map(|completion_text| lsp::CompletionItem {
20974 label: completion_text.to_string(),
20975 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
20976 lsp::InsertReplaceEdit {
20977 insert: lsp::Range {
20978 start: replace_range.start,
20979 end: complete_from_position,
20980 },
20981 replace: replace_range,
20982 new_text: completion_text.to_string(),
20983 },
20984 )),
20985 ..Default::default()
20986 })
20987 .collect(),
20988 )))
20989 }
20990 });
20991
20992 async move {
20993 request.next().await;
20994 }
20995}
20996
20997fn handle_resolve_completion_request(
20998 cx: &mut EditorLspTestContext,
20999 edits: Option<Vec<(&'static str, &'static str)>>,
21000) -> impl Future<Output = ()> {
21001 let edits = edits.map(|edits| {
21002 edits
21003 .iter()
21004 .map(|(marked_string, new_text)| {
21005 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
21006 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
21007 lsp::TextEdit::new(replace_range, new_text.to_string())
21008 })
21009 .collect::<Vec<_>>()
21010 });
21011
21012 let mut request =
21013 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
21014 let edits = edits.clone();
21015 async move {
21016 Ok(lsp::CompletionItem {
21017 additional_text_edits: edits,
21018 ..Default::default()
21019 })
21020 }
21021 });
21022
21023 async move {
21024 request.next().await;
21025 }
21026}
21027
21028pub(crate) fn update_test_language_settings(
21029 cx: &mut TestAppContext,
21030 f: impl Fn(&mut AllLanguageSettingsContent),
21031) {
21032 cx.update(|cx| {
21033 SettingsStore::update_global(cx, |store, cx| {
21034 store.update_user_settings::<AllLanguageSettings>(cx, f);
21035 });
21036 });
21037}
21038
21039pub(crate) fn update_test_project_settings(
21040 cx: &mut TestAppContext,
21041 f: impl Fn(&mut ProjectSettings),
21042) {
21043 cx.update(|cx| {
21044 SettingsStore::update_global(cx, |store, cx| {
21045 store.update_user_settings::<ProjectSettings>(cx, f);
21046 });
21047 });
21048}
21049
21050pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
21051 cx.update(|cx| {
21052 assets::Assets.load_test_fonts(cx);
21053 let store = SettingsStore::test(cx);
21054 cx.set_global(store);
21055 theme::init(theme::LoadThemes::JustBase, cx);
21056 release_channel::init(SemanticVersion::default(), cx);
21057 client::init_settings(cx);
21058 language::init(cx);
21059 Project::init_settings(cx);
21060 workspace::init_settings(cx);
21061 crate::init(cx);
21062 });
21063
21064 update_test_language_settings(cx, f);
21065}
21066
21067#[track_caller]
21068fn assert_hunk_revert(
21069 not_reverted_text_with_selections: &str,
21070 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
21071 expected_reverted_text_with_selections: &str,
21072 base_text: &str,
21073 cx: &mut EditorLspTestContext,
21074) {
21075 cx.set_state(not_reverted_text_with_selections);
21076 cx.set_head_text(base_text);
21077 cx.executor().run_until_parked();
21078
21079 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
21080 let snapshot = editor.snapshot(window, cx);
21081 let reverted_hunk_statuses = snapshot
21082 .buffer_snapshot
21083 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
21084 .map(|hunk| hunk.status().kind)
21085 .collect::<Vec<_>>();
21086
21087 editor.git_restore(&Default::default(), window, cx);
21088 reverted_hunk_statuses
21089 });
21090 cx.executor().run_until_parked();
21091 cx.assert_editor_state(expected_reverted_text_with_selections);
21092 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
21093}