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(Language::new(
2866 LanguageConfig {
2867 documentation: Some(language::DocumentationConfig {
2868 start: "/**".into(),
2869 end: "*/".into(),
2870 prefix: "* ".into(),
2871 tab_size: NonZeroU32::new(1).unwrap(),
2872 }),
2873 ..LanguageConfig::default()
2874 },
2875 None,
2876 ));
2877 {
2878 let mut cx = EditorTestContext::new(cx).await;
2879 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2880 cx.set_state(indoc! {"
2881 /**ˇ
2882 "});
2883
2884 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2885 cx.assert_editor_state(indoc! {"
2886 /**
2887 * ˇ
2888 "});
2889 // Ensure that if cursor is before the comment start,
2890 // we do not actually insert a comment prefix.
2891 cx.set_state(indoc! {"
2892 ˇ/**
2893 "});
2894 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2895 cx.assert_editor_state(indoc! {"
2896
2897 ˇ/**
2898 "});
2899 // Ensure that if cursor is between it doesn't add comment prefix.
2900 cx.set_state(indoc! {"
2901 /*ˇ*
2902 "});
2903 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2904 cx.assert_editor_state(indoc! {"
2905 /*
2906 ˇ*
2907 "});
2908 // Ensure that if suffix exists on same line after cursor it adds new line.
2909 cx.set_state(indoc! {"
2910 /**ˇ*/
2911 "});
2912 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2913 cx.assert_editor_state(indoc! {"
2914 /**
2915 * ˇ
2916 */
2917 "});
2918 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2919 cx.set_state(indoc! {"
2920 /**ˇ */
2921 "});
2922 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2923 cx.assert_editor_state(indoc! {"
2924 /**
2925 * ˇ
2926 */
2927 "});
2928 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2929 cx.set_state(indoc! {"
2930 /** ˇ*/
2931 "});
2932 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2933 cx.assert_editor_state(
2934 indoc! {"
2935 /**s
2936 * ˇ
2937 */
2938 "}
2939 .replace("s", " ") // s is used as space placeholder to prevent format on save
2940 .as_str(),
2941 );
2942 // Ensure that delimiter space is preserved when newline on already
2943 // spaced delimiter.
2944 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2945 cx.assert_editor_state(
2946 indoc! {"
2947 /**s
2948 *s
2949 * ˇ
2950 */
2951 "}
2952 .replace("s", " ") // s is used as space placeholder to prevent format on save
2953 .as_str(),
2954 );
2955 // Ensure that delimiter space is preserved when space is not
2956 // on existing delimiter.
2957 cx.set_state(indoc! {"
2958 /**
2959 *ˇ
2960 */
2961 "});
2962 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2963 cx.assert_editor_state(indoc! {"
2964 /**
2965 *
2966 * ˇ
2967 */
2968 "});
2969 // Ensure that if suffix exists on same line after cursor it
2970 // doesn't add extra new line if prefix is not on same line.
2971 cx.set_state(indoc! {"
2972 /**
2973 ˇ*/
2974 "});
2975 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2976 cx.assert_editor_state(indoc! {"
2977 /**
2978
2979 ˇ*/
2980 "});
2981 // Ensure that it detects suffix after existing prefix.
2982 cx.set_state(indoc! {"
2983 /**ˇ/
2984 "});
2985 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2986 cx.assert_editor_state(indoc! {"
2987 /**
2988 ˇ/
2989 "});
2990 // Ensure that if suffix exists on same line before
2991 // cursor it does not add comment prefix.
2992 cx.set_state(indoc! {"
2993 /** */ˇ
2994 "});
2995 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2996 cx.assert_editor_state(indoc! {"
2997 /** */
2998 ˇ
2999 "});
3000 // Ensure that if suffix exists on same line before
3001 // cursor it does not add comment prefix.
3002 cx.set_state(indoc! {"
3003 /**
3004 *
3005 */ˇ
3006 "});
3007 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3008 cx.assert_editor_state(indoc! {"
3009 /**
3010 *
3011 */
3012 ˇ
3013 "});
3014
3015 // Ensure that inline comment followed by code
3016 // doesn't add comment prefix on newline
3017 cx.set_state(indoc! {"
3018 /** */ textˇ
3019 "});
3020 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3021 cx.assert_editor_state(indoc! {"
3022 /** */ text
3023 ˇ
3024 "});
3025
3026 // Ensure that text after comment end tag
3027 // doesn't add comment prefix on newline
3028 cx.set_state(indoc! {"
3029 /**
3030 *
3031 */ˇtext
3032 "});
3033 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3034 cx.assert_editor_state(indoc! {"
3035 /**
3036 *
3037 */
3038 ˇtext
3039 "});
3040 }
3041 // Ensure that comment continuations can be disabled.
3042 update_test_language_settings(cx, |settings| {
3043 settings.defaults.extend_comment_on_newline = Some(false);
3044 });
3045 let mut cx = EditorTestContext::new(cx).await;
3046 cx.set_state(indoc! {"
3047 /**ˇ
3048 "});
3049 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3050 cx.assert_editor_state(indoc! {"
3051 /**
3052 ˇ
3053 "});
3054}
3055
3056#[gpui::test]
3057fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3058 init_test(cx, |_| {});
3059
3060 let editor = cx.add_window(|window, cx| {
3061 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3062 let mut editor = build_editor(buffer.clone(), window, cx);
3063 editor.change_selections(None, window, cx, |s| {
3064 s.select_ranges([3..4, 11..12, 19..20])
3065 });
3066 editor
3067 });
3068
3069 _ = editor.update(cx, |editor, window, cx| {
3070 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3071 editor.buffer.update(cx, |buffer, cx| {
3072 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
3073 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3074 });
3075 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
3076
3077 editor.insert("Z", window, cx);
3078 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3079
3080 // The selections are moved after the inserted characters
3081 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
3082 });
3083}
3084
3085#[gpui::test]
3086async fn test_tab(cx: &mut TestAppContext) {
3087 init_test(cx, |settings| {
3088 settings.defaults.tab_size = NonZeroU32::new(3)
3089 });
3090
3091 let mut cx = EditorTestContext::new(cx).await;
3092 cx.set_state(indoc! {"
3093 ˇabˇc
3094 ˇ🏀ˇ🏀ˇefg
3095 dˇ
3096 "});
3097 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3098 cx.assert_editor_state(indoc! {"
3099 ˇab ˇc
3100 ˇ🏀 ˇ🏀 ˇefg
3101 d ˇ
3102 "});
3103
3104 cx.set_state(indoc! {"
3105 a
3106 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3107 "});
3108 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3109 cx.assert_editor_state(indoc! {"
3110 a
3111 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3112 "});
3113}
3114
3115#[gpui::test]
3116async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3117 init_test(cx, |_| {});
3118
3119 let mut cx = EditorTestContext::new(cx).await;
3120 let language = Arc::new(
3121 Language::new(
3122 LanguageConfig::default(),
3123 Some(tree_sitter_rust::LANGUAGE.into()),
3124 )
3125 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3126 .unwrap(),
3127 );
3128 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3129
3130 // test when all cursors are not at suggested indent
3131 // then simply move to their suggested indent location
3132 cx.set_state(indoc! {"
3133 const a: B = (
3134 c(
3135 ˇ
3136 ˇ )
3137 );
3138 "});
3139 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3140 cx.assert_editor_state(indoc! {"
3141 const a: B = (
3142 c(
3143 ˇ
3144 ˇ)
3145 );
3146 "});
3147
3148 // test cursor already at suggested indent not moving when
3149 // other cursors are yet to reach their suggested indents
3150 cx.set_state(indoc! {"
3151 ˇ
3152 const a: B = (
3153 c(
3154 d(
3155 ˇ
3156 )
3157 ˇ
3158 ˇ )
3159 );
3160 "});
3161 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3162 cx.assert_editor_state(indoc! {"
3163 ˇ
3164 const a: B = (
3165 c(
3166 d(
3167 ˇ
3168 )
3169 ˇ
3170 ˇ)
3171 );
3172 "});
3173 // test when all cursors are at suggested indent then tab is inserted
3174 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3175 cx.assert_editor_state(indoc! {"
3176 ˇ
3177 const a: B = (
3178 c(
3179 d(
3180 ˇ
3181 )
3182 ˇ
3183 ˇ)
3184 );
3185 "});
3186
3187 // test when current indent is less than suggested indent,
3188 // we adjust line to match suggested indent and move cursor to it
3189 //
3190 // when no other cursor is at word boundary, all of them should move
3191 cx.set_state(indoc! {"
3192 const a: B = (
3193 c(
3194 d(
3195 ˇ
3196 ˇ )
3197 ˇ )
3198 );
3199 "});
3200 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3201 cx.assert_editor_state(indoc! {"
3202 const a: B = (
3203 c(
3204 d(
3205 ˇ
3206 ˇ)
3207 ˇ)
3208 );
3209 "});
3210
3211 // test when current indent is less than suggested indent,
3212 // we adjust line to match suggested indent and move cursor to it
3213 //
3214 // when some other cursor is at word boundary, it should not move
3215 cx.set_state(indoc! {"
3216 const a: B = (
3217 c(
3218 d(
3219 ˇ
3220 ˇ )
3221 ˇ)
3222 );
3223 "});
3224 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3225 cx.assert_editor_state(indoc! {"
3226 const a: B = (
3227 c(
3228 d(
3229 ˇ
3230 ˇ)
3231 ˇ)
3232 );
3233 "});
3234
3235 // test when current indent is more than suggested indent,
3236 // we just move cursor to current indent instead of suggested indent
3237 //
3238 // when no other cursor is at word boundary, all of them should move
3239 cx.set_state(indoc! {"
3240 const a: B = (
3241 c(
3242 d(
3243 ˇ
3244 ˇ )
3245 ˇ )
3246 );
3247 "});
3248 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3249 cx.assert_editor_state(indoc! {"
3250 const a: B = (
3251 c(
3252 d(
3253 ˇ
3254 ˇ)
3255 ˇ)
3256 );
3257 "});
3258 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3259 cx.assert_editor_state(indoc! {"
3260 const a: B = (
3261 c(
3262 d(
3263 ˇ
3264 ˇ)
3265 ˇ)
3266 );
3267 "});
3268
3269 // test when current indent is more than suggested indent,
3270 // we just move cursor to current indent instead of suggested indent
3271 //
3272 // when some other cursor is at word boundary, it doesn't move
3273 cx.set_state(indoc! {"
3274 const a: B = (
3275 c(
3276 d(
3277 ˇ
3278 ˇ )
3279 ˇ)
3280 );
3281 "});
3282 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3283 cx.assert_editor_state(indoc! {"
3284 const a: B = (
3285 c(
3286 d(
3287 ˇ
3288 ˇ)
3289 ˇ)
3290 );
3291 "});
3292
3293 // handle auto-indent when there are multiple cursors on the same line
3294 cx.set_state(indoc! {"
3295 const a: B = (
3296 c(
3297 ˇ ˇ
3298 ˇ )
3299 );
3300 "});
3301 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3302 cx.assert_editor_state(indoc! {"
3303 const a: B = (
3304 c(
3305 ˇ
3306 ˇ)
3307 );
3308 "});
3309}
3310
3311#[gpui::test]
3312async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3313 init_test(cx, |settings| {
3314 settings.defaults.tab_size = NonZeroU32::new(3)
3315 });
3316
3317 let mut cx = EditorTestContext::new(cx).await;
3318 cx.set_state(indoc! {"
3319 ˇ
3320 \t ˇ
3321 \t ˇ
3322 \t ˇ
3323 \t \t\t \t \t\t \t\t \t \t ˇ
3324 "});
3325
3326 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3327 cx.assert_editor_state(indoc! {"
3328 ˇ
3329 \t ˇ
3330 \t ˇ
3331 \t ˇ
3332 \t \t\t \t \t\t \t\t \t \t ˇ
3333 "});
3334}
3335
3336#[gpui::test]
3337async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3338 init_test(cx, |settings| {
3339 settings.defaults.tab_size = NonZeroU32::new(4)
3340 });
3341
3342 let language = Arc::new(
3343 Language::new(
3344 LanguageConfig::default(),
3345 Some(tree_sitter_rust::LANGUAGE.into()),
3346 )
3347 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3348 .unwrap(),
3349 );
3350
3351 let mut cx = EditorTestContext::new(cx).await;
3352 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3353 cx.set_state(indoc! {"
3354 fn a() {
3355 if b {
3356 \t ˇc
3357 }
3358 }
3359 "});
3360
3361 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3362 cx.assert_editor_state(indoc! {"
3363 fn a() {
3364 if b {
3365 ˇc
3366 }
3367 }
3368 "});
3369}
3370
3371#[gpui::test]
3372async fn test_indent_outdent(cx: &mut TestAppContext) {
3373 init_test(cx, |settings| {
3374 settings.defaults.tab_size = NonZeroU32::new(4);
3375 });
3376
3377 let mut cx = EditorTestContext::new(cx).await;
3378
3379 cx.set_state(indoc! {"
3380 «oneˇ» «twoˇ»
3381 three
3382 four
3383 "});
3384 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3385 cx.assert_editor_state(indoc! {"
3386 «oneˇ» «twoˇ»
3387 three
3388 four
3389 "});
3390
3391 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3392 cx.assert_editor_state(indoc! {"
3393 «oneˇ» «twoˇ»
3394 three
3395 four
3396 "});
3397
3398 // select across line ending
3399 cx.set_state(indoc! {"
3400 one two
3401 t«hree
3402 ˇ» four
3403 "});
3404 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3405 cx.assert_editor_state(indoc! {"
3406 one two
3407 t«hree
3408 ˇ» four
3409 "});
3410
3411 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3412 cx.assert_editor_state(indoc! {"
3413 one two
3414 t«hree
3415 ˇ» four
3416 "});
3417
3418 // Ensure that indenting/outdenting works when the cursor is at column 0.
3419 cx.set_state(indoc! {"
3420 one two
3421 ˇthree
3422 four
3423 "});
3424 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3425 cx.assert_editor_state(indoc! {"
3426 one two
3427 ˇthree
3428 four
3429 "});
3430
3431 cx.set_state(indoc! {"
3432 one two
3433 ˇ three
3434 four
3435 "});
3436 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3437 cx.assert_editor_state(indoc! {"
3438 one two
3439 ˇthree
3440 four
3441 "});
3442}
3443
3444#[gpui::test]
3445async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
3446 init_test(cx, |settings| {
3447 settings.defaults.hard_tabs = Some(true);
3448 });
3449
3450 let mut cx = EditorTestContext::new(cx).await;
3451
3452 // select two ranges on one line
3453 cx.set_state(indoc! {"
3454 «oneˇ» «twoˇ»
3455 three
3456 four
3457 "});
3458 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3459 cx.assert_editor_state(indoc! {"
3460 \t«oneˇ» «twoˇ»
3461 three
3462 four
3463 "});
3464 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3465 cx.assert_editor_state(indoc! {"
3466 \t\t«oneˇ» «twoˇ»
3467 three
3468 four
3469 "});
3470 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3471 cx.assert_editor_state(indoc! {"
3472 \t«oneˇ» «twoˇ»
3473 three
3474 four
3475 "});
3476 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3477 cx.assert_editor_state(indoc! {"
3478 «oneˇ» «twoˇ»
3479 three
3480 four
3481 "});
3482
3483 // select across a line ending
3484 cx.set_state(indoc! {"
3485 one two
3486 t«hree
3487 ˇ»four
3488 "});
3489 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3490 cx.assert_editor_state(indoc! {"
3491 one two
3492 \tt«hree
3493 ˇ»four
3494 "});
3495 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3496 cx.assert_editor_state(indoc! {"
3497 one two
3498 \t\tt«hree
3499 ˇ»four
3500 "});
3501 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3502 cx.assert_editor_state(indoc! {"
3503 one two
3504 \tt«hree
3505 ˇ»four
3506 "});
3507 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3508 cx.assert_editor_state(indoc! {"
3509 one two
3510 t«hree
3511 ˇ»four
3512 "});
3513
3514 // Ensure that indenting/outdenting works when the cursor is at column 0.
3515 cx.set_state(indoc! {"
3516 one two
3517 ˇthree
3518 four
3519 "});
3520 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3521 cx.assert_editor_state(indoc! {"
3522 one two
3523 ˇthree
3524 four
3525 "});
3526 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3527 cx.assert_editor_state(indoc! {"
3528 one two
3529 \tˇthree
3530 four
3531 "});
3532 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3533 cx.assert_editor_state(indoc! {"
3534 one two
3535 ˇthree
3536 four
3537 "});
3538}
3539
3540#[gpui::test]
3541fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
3542 init_test(cx, |settings| {
3543 settings.languages.extend([
3544 (
3545 "TOML".into(),
3546 LanguageSettingsContent {
3547 tab_size: NonZeroU32::new(2),
3548 ..Default::default()
3549 },
3550 ),
3551 (
3552 "Rust".into(),
3553 LanguageSettingsContent {
3554 tab_size: NonZeroU32::new(4),
3555 ..Default::default()
3556 },
3557 ),
3558 ]);
3559 });
3560
3561 let toml_language = Arc::new(Language::new(
3562 LanguageConfig {
3563 name: "TOML".into(),
3564 ..Default::default()
3565 },
3566 None,
3567 ));
3568 let rust_language = Arc::new(Language::new(
3569 LanguageConfig {
3570 name: "Rust".into(),
3571 ..Default::default()
3572 },
3573 None,
3574 ));
3575
3576 let toml_buffer =
3577 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
3578 let rust_buffer =
3579 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
3580 let multibuffer = cx.new(|cx| {
3581 let mut multibuffer = MultiBuffer::new(ReadWrite);
3582 multibuffer.push_excerpts(
3583 toml_buffer.clone(),
3584 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
3585 cx,
3586 );
3587 multibuffer.push_excerpts(
3588 rust_buffer.clone(),
3589 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
3590 cx,
3591 );
3592 multibuffer
3593 });
3594
3595 cx.add_window(|window, cx| {
3596 let mut editor = build_editor(multibuffer, window, cx);
3597
3598 assert_eq!(
3599 editor.text(cx),
3600 indoc! {"
3601 a = 1
3602 b = 2
3603
3604 const c: usize = 3;
3605 "}
3606 );
3607
3608 select_ranges(
3609 &mut editor,
3610 indoc! {"
3611 «aˇ» = 1
3612 b = 2
3613
3614 «const c:ˇ» usize = 3;
3615 "},
3616 window,
3617 cx,
3618 );
3619
3620 editor.tab(&Tab, window, cx);
3621 assert_text_with_selections(
3622 &mut editor,
3623 indoc! {"
3624 «aˇ» = 1
3625 b = 2
3626
3627 «const c:ˇ» usize = 3;
3628 "},
3629 cx,
3630 );
3631 editor.backtab(&Backtab, window, cx);
3632 assert_text_with_selections(
3633 &mut editor,
3634 indoc! {"
3635 «aˇ» = 1
3636 b = 2
3637
3638 «const c:ˇ» usize = 3;
3639 "},
3640 cx,
3641 );
3642
3643 editor
3644 });
3645}
3646
3647#[gpui::test]
3648async fn test_backspace(cx: &mut TestAppContext) {
3649 init_test(cx, |_| {});
3650
3651 let mut cx = EditorTestContext::new(cx).await;
3652
3653 // Basic backspace
3654 cx.set_state(indoc! {"
3655 onˇe two three
3656 fou«rˇ» five six
3657 seven «ˇeight nine
3658 »ten
3659 "});
3660 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3661 cx.assert_editor_state(indoc! {"
3662 oˇe two three
3663 fouˇ five six
3664 seven ˇten
3665 "});
3666
3667 // Test backspace inside and around indents
3668 cx.set_state(indoc! {"
3669 zero
3670 ˇone
3671 ˇtwo
3672 ˇ ˇ ˇ three
3673 ˇ ˇ four
3674 "});
3675 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3676 cx.assert_editor_state(indoc! {"
3677 zero
3678 ˇone
3679 ˇtwo
3680 ˇ threeˇ four
3681 "});
3682}
3683
3684#[gpui::test]
3685async fn test_delete(cx: &mut TestAppContext) {
3686 init_test(cx, |_| {});
3687
3688 let mut cx = EditorTestContext::new(cx).await;
3689 cx.set_state(indoc! {"
3690 onˇe two three
3691 fou«rˇ» five six
3692 seven «ˇeight nine
3693 »ten
3694 "});
3695 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
3696 cx.assert_editor_state(indoc! {"
3697 onˇ two three
3698 fouˇ five six
3699 seven ˇten
3700 "});
3701}
3702
3703#[gpui::test]
3704fn test_delete_line(cx: &mut TestAppContext) {
3705 init_test(cx, |_| {});
3706
3707 let editor = cx.add_window(|window, cx| {
3708 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3709 build_editor(buffer, window, cx)
3710 });
3711 _ = editor.update(cx, |editor, window, cx| {
3712 editor.change_selections(None, window, cx, |s| {
3713 s.select_display_ranges([
3714 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
3715 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3716 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3717 ])
3718 });
3719 editor.delete_line(&DeleteLine, window, cx);
3720 assert_eq!(editor.display_text(cx), "ghi");
3721 assert_eq!(
3722 editor.selections.display_ranges(cx),
3723 vec![
3724 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
3725 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
3726 ]
3727 );
3728 });
3729
3730 let editor = cx.add_window(|window, cx| {
3731 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3732 build_editor(buffer, window, cx)
3733 });
3734 _ = editor.update(cx, |editor, window, cx| {
3735 editor.change_selections(None, window, cx, |s| {
3736 s.select_display_ranges([
3737 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
3738 ])
3739 });
3740 editor.delete_line(&DeleteLine, window, cx);
3741 assert_eq!(editor.display_text(cx), "ghi\n");
3742 assert_eq!(
3743 editor.selections.display_ranges(cx),
3744 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
3745 );
3746 });
3747}
3748
3749#[gpui::test]
3750fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
3751 init_test(cx, |_| {});
3752
3753 cx.add_window(|window, cx| {
3754 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3755 let mut editor = build_editor(buffer.clone(), window, cx);
3756 let buffer = buffer.read(cx).as_singleton().unwrap();
3757
3758 assert_eq!(
3759 editor.selections.ranges::<Point>(cx),
3760 &[Point::new(0, 0)..Point::new(0, 0)]
3761 );
3762
3763 // When on single line, replace newline at end by space
3764 editor.join_lines(&JoinLines, window, cx);
3765 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3766 assert_eq!(
3767 editor.selections.ranges::<Point>(cx),
3768 &[Point::new(0, 3)..Point::new(0, 3)]
3769 );
3770
3771 // When multiple lines are selected, remove newlines that are spanned by the selection
3772 editor.change_selections(None, window, cx, |s| {
3773 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
3774 });
3775 editor.join_lines(&JoinLines, window, cx);
3776 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
3777 assert_eq!(
3778 editor.selections.ranges::<Point>(cx),
3779 &[Point::new(0, 11)..Point::new(0, 11)]
3780 );
3781
3782 // Undo should be transactional
3783 editor.undo(&Undo, window, cx);
3784 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3785 assert_eq!(
3786 editor.selections.ranges::<Point>(cx),
3787 &[Point::new(0, 5)..Point::new(2, 2)]
3788 );
3789
3790 // When joining an empty line don't insert a space
3791 editor.change_selections(None, window, cx, |s| {
3792 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
3793 });
3794 editor.join_lines(&JoinLines, window, cx);
3795 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
3796 assert_eq!(
3797 editor.selections.ranges::<Point>(cx),
3798 [Point::new(2, 3)..Point::new(2, 3)]
3799 );
3800
3801 // We can remove trailing newlines
3802 editor.join_lines(&JoinLines, window, cx);
3803 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3804 assert_eq!(
3805 editor.selections.ranges::<Point>(cx),
3806 [Point::new(2, 3)..Point::new(2, 3)]
3807 );
3808
3809 // We don't blow up on the last line
3810 editor.join_lines(&JoinLines, window, cx);
3811 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3812 assert_eq!(
3813 editor.selections.ranges::<Point>(cx),
3814 [Point::new(2, 3)..Point::new(2, 3)]
3815 );
3816
3817 // reset to test indentation
3818 editor.buffer.update(cx, |buffer, cx| {
3819 buffer.edit(
3820 [
3821 (Point::new(1, 0)..Point::new(1, 2), " "),
3822 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
3823 ],
3824 None,
3825 cx,
3826 )
3827 });
3828
3829 // We remove any leading spaces
3830 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
3831 editor.change_selections(None, window, cx, |s| {
3832 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
3833 });
3834 editor.join_lines(&JoinLines, window, cx);
3835 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
3836
3837 // We don't insert a space for a line containing only spaces
3838 editor.join_lines(&JoinLines, window, cx);
3839 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
3840
3841 // We ignore any leading tabs
3842 editor.join_lines(&JoinLines, window, cx);
3843 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
3844
3845 editor
3846 });
3847}
3848
3849#[gpui::test]
3850fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
3851 init_test(cx, |_| {});
3852
3853 cx.add_window(|window, cx| {
3854 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3855 let mut editor = build_editor(buffer.clone(), window, cx);
3856 let buffer = buffer.read(cx).as_singleton().unwrap();
3857
3858 editor.change_selections(None, window, cx, |s| {
3859 s.select_ranges([
3860 Point::new(0, 2)..Point::new(1, 1),
3861 Point::new(1, 2)..Point::new(1, 2),
3862 Point::new(3, 1)..Point::new(3, 2),
3863 ])
3864 });
3865
3866 editor.join_lines(&JoinLines, window, cx);
3867 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
3868
3869 assert_eq!(
3870 editor.selections.ranges::<Point>(cx),
3871 [
3872 Point::new(0, 7)..Point::new(0, 7),
3873 Point::new(1, 3)..Point::new(1, 3)
3874 ]
3875 );
3876 editor
3877 });
3878}
3879
3880#[gpui::test]
3881async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
3882 init_test(cx, |_| {});
3883
3884 let mut cx = EditorTestContext::new(cx).await;
3885
3886 let diff_base = r#"
3887 Line 0
3888 Line 1
3889 Line 2
3890 Line 3
3891 "#
3892 .unindent();
3893
3894 cx.set_state(
3895 &r#"
3896 ˇLine 0
3897 Line 1
3898 Line 2
3899 Line 3
3900 "#
3901 .unindent(),
3902 );
3903
3904 cx.set_head_text(&diff_base);
3905 executor.run_until_parked();
3906
3907 // Join lines
3908 cx.update_editor(|editor, window, cx| {
3909 editor.join_lines(&JoinLines, window, cx);
3910 });
3911 executor.run_until_parked();
3912
3913 cx.assert_editor_state(
3914 &r#"
3915 Line 0ˇ Line 1
3916 Line 2
3917 Line 3
3918 "#
3919 .unindent(),
3920 );
3921 // Join again
3922 cx.update_editor(|editor, window, cx| {
3923 editor.join_lines(&JoinLines, window, cx);
3924 });
3925 executor.run_until_parked();
3926
3927 cx.assert_editor_state(
3928 &r#"
3929 Line 0 Line 1ˇ Line 2
3930 Line 3
3931 "#
3932 .unindent(),
3933 );
3934}
3935
3936#[gpui::test]
3937async fn test_custom_newlines_cause_no_false_positive_diffs(
3938 executor: BackgroundExecutor,
3939 cx: &mut TestAppContext,
3940) {
3941 init_test(cx, |_| {});
3942 let mut cx = EditorTestContext::new(cx).await;
3943 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
3944 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
3945 executor.run_until_parked();
3946
3947 cx.update_editor(|editor, window, cx| {
3948 let snapshot = editor.snapshot(window, cx);
3949 assert_eq!(
3950 snapshot
3951 .buffer_snapshot
3952 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
3953 .collect::<Vec<_>>(),
3954 Vec::new(),
3955 "Should not have any diffs for files with custom newlines"
3956 );
3957 });
3958}
3959
3960#[gpui::test]
3961async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) {
3962 init_test(cx, |_| {});
3963
3964 let mut cx = EditorTestContext::new(cx).await;
3965
3966 // Test sort_lines_case_insensitive()
3967 cx.set_state(indoc! {"
3968 «z
3969 y
3970 x
3971 Z
3972 Y
3973 Xˇ»
3974 "});
3975 cx.update_editor(|e, window, cx| {
3976 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
3977 });
3978 cx.assert_editor_state(indoc! {"
3979 «x
3980 X
3981 y
3982 Y
3983 z
3984 Zˇ»
3985 "});
3986
3987 // Test reverse_lines()
3988 cx.set_state(indoc! {"
3989 «5
3990 4
3991 3
3992 2
3993 1ˇ»
3994 "});
3995 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
3996 cx.assert_editor_state(indoc! {"
3997 «1
3998 2
3999 3
4000 4
4001 5ˇ»
4002 "});
4003
4004 // Skip testing shuffle_line()
4005
4006 // From here on out, test more complex cases of manipulate_lines() with a single driver method: sort_lines_case_sensitive()
4007 // Since all methods calling manipulate_lines() are doing the exact same general thing (reordering lines)
4008
4009 // Don't manipulate when cursor is on single line, but expand the selection
4010 cx.set_state(indoc! {"
4011 ddˇdd
4012 ccc
4013 bb
4014 a
4015 "});
4016 cx.update_editor(|e, window, cx| {
4017 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4018 });
4019 cx.assert_editor_state(indoc! {"
4020 «ddddˇ»
4021 ccc
4022 bb
4023 a
4024 "});
4025
4026 // Basic manipulate case
4027 // Start selection moves to column 0
4028 // End of selection shrinks to fit shorter line
4029 cx.set_state(indoc! {"
4030 dd«d
4031 ccc
4032 bb
4033 aaaaaˇ»
4034 "});
4035 cx.update_editor(|e, window, cx| {
4036 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4037 });
4038 cx.assert_editor_state(indoc! {"
4039 «aaaaa
4040 bb
4041 ccc
4042 dddˇ»
4043 "});
4044
4045 // Manipulate case with newlines
4046 cx.set_state(indoc! {"
4047 dd«d
4048 ccc
4049
4050 bb
4051 aaaaa
4052
4053 ˇ»
4054 "});
4055 cx.update_editor(|e, window, cx| {
4056 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4057 });
4058 cx.assert_editor_state(indoc! {"
4059 «
4060
4061 aaaaa
4062 bb
4063 ccc
4064 dddˇ»
4065
4066 "});
4067
4068 // Adding new line
4069 cx.set_state(indoc! {"
4070 aa«a
4071 bbˇ»b
4072 "});
4073 cx.update_editor(|e, window, cx| {
4074 e.manipulate_lines(window, cx, |lines| lines.push("added_line"))
4075 });
4076 cx.assert_editor_state(indoc! {"
4077 «aaa
4078 bbb
4079 added_lineˇ»
4080 "});
4081
4082 // Removing line
4083 cx.set_state(indoc! {"
4084 aa«a
4085 bbbˇ»
4086 "});
4087 cx.update_editor(|e, window, cx| {
4088 e.manipulate_lines(window, cx, |lines| {
4089 lines.pop();
4090 })
4091 });
4092 cx.assert_editor_state(indoc! {"
4093 «aaaˇ»
4094 "});
4095
4096 // Removing all lines
4097 cx.set_state(indoc! {"
4098 aa«a
4099 bbbˇ»
4100 "});
4101 cx.update_editor(|e, window, cx| {
4102 e.manipulate_lines(window, cx, |lines| {
4103 lines.drain(..);
4104 })
4105 });
4106 cx.assert_editor_state(indoc! {"
4107 ˇ
4108 "});
4109}
4110
4111#[gpui::test]
4112async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4113 init_test(cx, |_| {});
4114
4115 let mut cx = EditorTestContext::new(cx).await;
4116
4117 // Consider continuous selection as single selection
4118 cx.set_state(indoc! {"
4119 Aaa«aa
4120 cˇ»c«c
4121 bb
4122 aaaˇ»aa
4123 "});
4124 cx.update_editor(|e, window, cx| {
4125 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4126 });
4127 cx.assert_editor_state(indoc! {"
4128 «Aaaaa
4129 ccc
4130 bb
4131 aaaaaˇ»
4132 "});
4133
4134 cx.set_state(indoc! {"
4135 Aaa«aa
4136 cˇ»c«c
4137 bb
4138 aaaˇ»aa
4139 "});
4140 cx.update_editor(|e, window, cx| {
4141 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4142 });
4143 cx.assert_editor_state(indoc! {"
4144 «Aaaaa
4145 ccc
4146 bbˇ»
4147 "});
4148
4149 // Consider non continuous selection as distinct dedup operations
4150 cx.set_state(indoc! {"
4151 «aaaaa
4152 bb
4153 aaaaa
4154 aaaaaˇ»
4155
4156 aaa«aaˇ»
4157 "});
4158 cx.update_editor(|e, window, cx| {
4159 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4160 });
4161 cx.assert_editor_state(indoc! {"
4162 «aaaaa
4163 bbˇ»
4164
4165 «aaaaaˇ»
4166 "});
4167}
4168
4169#[gpui::test]
4170async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4171 init_test(cx, |_| {});
4172
4173 let mut cx = EditorTestContext::new(cx).await;
4174
4175 cx.set_state(indoc! {"
4176 «Aaa
4177 aAa
4178 Aaaˇ»
4179 "});
4180 cx.update_editor(|e, window, cx| {
4181 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4182 });
4183 cx.assert_editor_state(indoc! {"
4184 «Aaa
4185 aAaˇ»
4186 "});
4187
4188 cx.set_state(indoc! {"
4189 «Aaa
4190 aAa
4191 aaAˇ»
4192 "});
4193 cx.update_editor(|e, window, cx| {
4194 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4195 });
4196 cx.assert_editor_state(indoc! {"
4197 «Aaaˇ»
4198 "});
4199}
4200
4201#[gpui::test]
4202async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) {
4203 init_test(cx, |_| {});
4204
4205 let mut cx = EditorTestContext::new(cx).await;
4206
4207 // Manipulate with multiple selections on a single line
4208 cx.set_state(indoc! {"
4209 dd«dd
4210 cˇ»c«c
4211 bb
4212 aaaˇ»aa
4213 "});
4214 cx.update_editor(|e, window, cx| {
4215 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4216 });
4217 cx.assert_editor_state(indoc! {"
4218 «aaaaa
4219 bb
4220 ccc
4221 ddddˇ»
4222 "});
4223
4224 // Manipulate with multiple disjoin selections
4225 cx.set_state(indoc! {"
4226 5«
4227 4
4228 3
4229 2
4230 1ˇ»
4231
4232 dd«dd
4233 ccc
4234 bb
4235 aaaˇ»aa
4236 "});
4237 cx.update_editor(|e, window, cx| {
4238 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4239 });
4240 cx.assert_editor_state(indoc! {"
4241 «1
4242 2
4243 3
4244 4
4245 5ˇ»
4246
4247 «aaaaa
4248 bb
4249 ccc
4250 ddddˇ»
4251 "});
4252
4253 // Adding lines on each selection
4254 cx.set_state(indoc! {"
4255 2«
4256 1ˇ»
4257
4258 bb«bb
4259 aaaˇ»aa
4260 "});
4261 cx.update_editor(|e, window, cx| {
4262 e.manipulate_lines(window, cx, |lines| lines.push("added line"))
4263 });
4264 cx.assert_editor_state(indoc! {"
4265 «2
4266 1
4267 added lineˇ»
4268
4269 «bbbb
4270 aaaaa
4271 added lineˇ»
4272 "});
4273
4274 // Removing lines on each selection
4275 cx.set_state(indoc! {"
4276 2«
4277 1ˇ»
4278
4279 bb«bb
4280 aaaˇ»aa
4281 "});
4282 cx.update_editor(|e, window, cx| {
4283 e.manipulate_lines(window, cx, |lines| {
4284 lines.pop();
4285 })
4286 });
4287 cx.assert_editor_state(indoc! {"
4288 «2ˇ»
4289
4290 «bbbbˇ»
4291 "});
4292}
4293
4294#[gpui::test]
4295async fn test_toggle_case(cx: &mut TestAppContext) {
4296 init_test(cx, |_| {});
4297
4298 let mut cx = EditorTestContext::new(cx).await;
4299
4300 // If all lower case -> upper case
4301 cx.set_state(indoc! {"
4302 «hello worldˇ»
4303 "});
4304 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4305 cx.assert_editor_state(indoc! {"
4306 «HELLO WORLDˇ»
4307 "});
4308
4309 // If all upper case -> lower case
4310 cx.set_state(indoc! {"
4311 «HELLO WORLDˇ»
4312 "});
4313 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4314 cx.assert_editor_state(indoc! {"
4315 «hello worldˇ»
4316 "});
4317
4318 // If any upper case characters are identified -> lower case
4319 // This matches JetBrains IDEs
4320 cx.set_state(indoc! {"
4321 «hEllo worldˇ»
4322 "});
4323 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4324 cx.assert_editor_state(indoc! {"
4325 «hello worldˇ»
4326 "});
4327}
4328
4329#[gpui::test]
4330async fn test_manipulate_text(cx: &mut TestAppContext) {
4331 init_test(cx, |_| {});
4332
4333 let mut cx = EditorTestContext::new(cx).await;
4334
4335 // Test convert_to_upper_case()
4336 cx.set_state(indoc! {"
4337 «hello worldˇ»
4338 "});
4339 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4340 cx.assert_editor_state(indoc! {"
4341 «HELLO WORLDˇ»
4342 "});
4343
4344 // Test convert_to_lower_case()
4345 cx.set_state(indoc! {"
4346 «HELLO WORLDˇ»
4347 "});
4348 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
4349 cx.assert_editor_state(indoc! {"
4350 «hello worldˇ»
4351 "});
4352
4353 // Test multiple line, single selection case
4354 cx.set_state(indoc! {"
4355 «The quick brown
4356 fox jumps over
4357 the lazy dogˇ»
4358 "});
4359 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
4360 cx.assert_editor_state(indoc! {"
4361 «The Quick Brown
4362 Fox Jumps Over
4363 The Lazy Dogˇ»
4364 "});
4365
4366 // Test multiple line, single selection case
4367 cx.set_state(indoc! {"
4368 «The quick brown
4369 fox jumps over
4370 the lazy dogˇ»
4371 "});
4372 cx.update_editor(|e, window, cx| {
4373 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
4374 });
4375 cx.assert_editor_state(indoc! {"
4376 «TheQuickBrown
4377 FoxJumpsOver
4378 TheLazyDogˇ»
4379 "});
4380
4381 // From here on out, test more complex cases of manipulate_text()
4382
4383 // Test no selection case - should affect words cursors are in
4384 // Cursor at beginning, middle, and end of word
4385 cx.set_state(indoc! {"
4386 ˇhello big beauˇtiful worldˇ
4387 "});
4388 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4389 cx.assert_editor_state(indoc! {"
4390 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
4391 "});
4392
4393 // Test multiple selections on a single line and across multiple lines
4394 cx.set_state(indoc! {"
4395 «Theˇ» quick «brown
4396 foxˇ» jumps «overˇ»
4397 the «lazyˇ» dog
4398 "});
4399 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4400 cx.assert_editor_state(indoc! {"
4401 «THEˇ» quick «BROWN
4402 FOXˇ» jumps «OVERˇ»
4403 the «LAZYˇ» dog
4404 "});
4405
4406 // Test case where text length grows
4407 cx.set_state(indoc! {"
4408 «tschüߡ»
4409 "});
4410 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4411 cx.assert_editor_state(indoc! {"
4412 «TSCHÜSSˇ»
4413 "});
4414
4415 // Test to make sure we don't crash when text shrinks
4416 cx.set_state(indoc! {"
4417 aaa_bbbˇ
4418 "});
4419 cx.update_editor(|e, window, cx| {
4420 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4421 });
4422 cx.assert_editor_state(indoc! {"
4423 «aaaBbbˇ»
4424 "});
4425
4426 // Test to make sure we all aware of the fact that each word can grow and shrink
4427 // Final selections should be aware of this fact
4428 cx.set_state(indoc! {"
4429 aaa_bˇbb bbˇb_ccc ˇccc_ddd
4430 "});
4431 cx.update_editor(|e, window, cx| {
4432 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4433 });
4434 cx.assert_editor_state(indoc! {"
4435 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
4436 "});
4437
4438 cx.set_state(indoc! {"
4439 «hElLo, WoRld!ˇ»
4440 "});
4441 cx.update_editor(|e, window, cx| {
4442 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
4443 });
4444 cx.assert_editor_state(indoc! {"
4445 «HeLlO, wOrLD!ˇ»
4446 "});
4447}
4448
4449#[gpui::test]
4450fn test_duplicate_line(cx: &mut TestAppContext) {
4451 init_test(cx, |_| {});
4452
4453 let editor = cx.add_window(|window, cx| {
4454 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4455 build_editor(buffer, window, cx)
4456 });
4457 _ = editor.update(cx, |editor, window, cx| {
4458 editor.change_selections(None, window, cx, |s| {
4459 s.select_display_ranges([
4460 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4461 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4462 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4463 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4464 ])
4465 });
4466 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4467 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4468 assert_eq!(
4469 editor.selections.display_ranges(cx),
4470 vec![
4471 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4472 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
4473 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4474 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4475 ]
4476 );
4477 });
4478
4479 let editor = cx.add_window(|window, cx| {
4480 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4481 build_editor(buffer, window, cx)
4482 });
4483 _ = editor.update(cx, |editor, window, cx| {
4484 editor.change_selections(None, window, cx, |s| {
4485 s.select_display_ranges([
4486 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4487 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4488 ])
4489 });
4490 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4491 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4492 assert_eq!(
4493 editor.selections.display_ranges(cx),
4494 vec![
4495 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
4496 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
4497 ]
4498 );
4499 });
4500
4501 // With `move_upwards` the selections stay in place, except for
4502 // the lines inserted above them
4503 let editor = cx.add_window(|window, cx| {
4504 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4505 build_editor(buffer, window, cx)
4506 });
4507 _ = editor.update(cx, |editor, window, cx| {
4508 editor.change_selections(None, window, cx, |s| {
4509 s.select_display_ranges([
4510 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4511 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4512 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4513 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4514 ])
4515 });
4516 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4517 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4518 assert_eq!(
4519 editor.selections.display_ranges(cx),
4520 vec![
4521 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4522 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4523 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
4524 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4525 ]
4526 );
4527 });
4528
4529 let editor = cx.add_window(|window, cx| {
4530 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4531 build_editor(buffer, window, cx)
4532 });
4533 _ = editor.update(cx, |editor, window, cx| {
4534 editor.change_selections(None, window, cx, |s| {
4535 s.select_display_ranges([
4536 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4537 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4538 ])
4539 });
4540 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4541 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4542 assert_eq!(
4543 editor.selections.display_ranges(cx),
4544 vec![
4545 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4546 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4547 ]
4548 );
4549 });
4550
4551 let editor = cx.add_window(|window, cx| {
4552 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4553 build_editor(buffer, window, cx)
4554 });
4555 _ = editor.update(cx, |editor, window, cx| {
4556 editor.change_selections(None, window, cx, |s| {
4557 s.select_display_ranges([
4558 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4559 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4560 ])
4561 });
4562 editor.duplicate_selection(&DuplicateSelection, window, cx);
4563 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
4564 assert_eq!(
4565 editor.selections.display_ranges(cx),
4566 vec![
4567 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4568 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
4569 ]
4570 );
4571 });
4572}
4573
4574#[gpui::test]
4575fn test_move_line_up_down(cx: &mut TestAppContext) {
4576 init_test(cx, |_| {});
4577
4578 let editor = cx.add_window(|window, cx| {
4579 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4580 build_editor(buffer, window, cx)
4581 });
4582 _ = editor.update(cx, |editor, window, cx| {
4583 editor.fold_creases(
4584 vec![
4585 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
4586 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
4587 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
4588 ],
4589 true,
4590 window,
4591 cx,
4592 );
4593 editor.change_selections(None, window, cx, |s| {
4594 s.select_display_ranges([
4595 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4596 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4597 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4598 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
4599 ])
4600 });
4601 assert_eq!(
4602 editor.display_text(cx),
4603 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
4604 );
4605
4606 editor.move_line_up(&MoveLineUp, window, cx);
4607 assert_eq!(
4608 editor.display_text(cx),
4609 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
4610 );
4611 assert_eq!(
4612 editor.selections.display_ranges(cx),
4613 vec![
4614 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4615 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4616 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4617 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4618 ]
4619 );
4620 });
4621
4622 _ = editor.update(cx, |editor, window, cx| {
4623 editor.move_line_down(&MoveLineDown, window, cx);
4624 assert_eq!(
4625 editor.display_text(cx),
4626 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
4627 );
4628 assert_eq!(
4629 editor.selections.display_ranges(cx),
4630 vec![
4631 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4632 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4633 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4634 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 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\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
4644 );
4645 assert_eq!(
4646 editor.selections.display_ranges(cx),
4647 vec![
4648 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 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_up(&MoveLineUp, window, cx);
4658 assert_eq!(
4659 editor.display_text(cx),
4660 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
4661 );
4662 assert_eq!(
4663 editor.selections.display_ranges(cx),
4664 vec![
4665 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4666 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4667 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4668 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4669 ]
4670 );
4671 });
4672}
4673
4674#[gpui::test]
4675fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
4676 init_test(cx, |_| {});
4677
4678 let editor = cx.add_window(|window, cx| {
4679 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4680 build_editor(buffer, window, cx)
4681 });
4682 _ = editor.update(cx, |editor, window, cx| {
4683 let snapshot = editor.buffer.read(cx).snapshot(cx);
4684 editor.insert_blocks(
4685 [BlockProperties {
4686 style: BlockStyle::Fixed,
4687 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
4688 height: Some(1),
4689 render: Arc::new(|_| div().into_any()),
4690 priority: 0,
4691 render_in_minimap: true,
4692 }],
4693 Some(Autoscroll::fit()),
4694 cx,
4695 );
4696 editor.change_selections(None, window, cx, |s| {
4697 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
4698 });
4699 editor.move_line_down(&MoveLineDown, window, cx);
4700 });
4701}
4702
4703#[gpui::test]
4704async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
4705 init_test(cx, |_| {});
4706
4707 let mut cx = EditorTestContext::new(cx).await;
4708 cx.set_state(
4709 &"
4710 ˇzero
4711 one
4712 two
4713 three
4714 four
4715 five
4716 "
4717 .unindent(),
4718 );
4719
4720 // Create a four-line block that replaces three lines of text.
4721 cx.update_editor(|editor, window, cx| {
4722 let snapshot = editor.snapshot(window, cx);
4723 let snapshot = &snapshot.buffer_snapshot;
4724 let placement = BlockPlacement::Replace(
4725 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
4726 );
4727 editor.insert_blocks(
4728 [BlockProperties {
4729 placement,
4730 height: Some(4),
4731 style: BlockStyle::Sticky,
4732 render: Arc::new(|_| gpui::div().into_any_element()),
4733 priority: 0,
4734 render_in_minimap: true,
4735 }],
4736 None,
4737 cx,
4738 );
4739 });
4740
4741 // Move down so that the cursor touches the block.
4742 cx.update_editor(|editor, window, cx| {
4743 editor.move_down(&Default::default(), window, cx);
4744 });
4745 cx.assert_editor_state(
4746 &"
4747 zero
4748 «one
4749 two
4750 threeˇ»
4751 four
4752 five
4753 "
4754 .unindent(),
4755 );
4756
4757 // Move down past the block.
4758 cx.update_editor(|editor, window, cx| {
4759 editor.move_down(&Default::default(), window, cx);
4760 });
4761 cx.assert_editor_state(
4762 &"
4763 zero
4764 one
4765 two
4766 three
4767 ˇfour
4768 five
4769 "
4770 .unindent(),
4771 );
4772}
4773
4774#[gpui::test]
4775fn test_transpose(cx: &mut TestAppContext) {
4776 init_test(cx, |_| {});
4777
4778 _ = cx.add_window(|window, cx| {
4779 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
4780 editor.set_style(EditorStyle::default(), window, cx);
4781 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
4782 editor.transpose(&Default::default(), window, cx);
4783 assert_eq!(editor.text(cx), "bac");
4784 assert_eq!(editor.selections.ranges(cx), [2..2]);
4785
4786 editor.transpose(&Default::default(), window, cx);
4787 assert_eq!(editor.text(cx), "bca");
4788 assert_eq!(editor.selections.ranges(cx), [3..3]);
4789
4790 editor.transpose(&Default::default(), window, cx);
4791 assert_eq!(editor.text(cx), "bac");
4792 assert_eq!(editor.selections.ranges(cx), [3..3]);
4793
4794 editor
4795 });
4796
4797 _ = cx.add_window(|window, cx| {
4798 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4799 editor.set_style(EditorStyle::default(), window, cx);
4800 editor.change_selections(None, window, cx, |s| s.select_ranges([3..3]));
4801 editor.transpose(&Default::default(), window, cx);
4802 assert_eq!(editor.text(cx), "acb\nde");
4803 assert_eq!(editor.selections.ranges(cx), [3..3]);
4804
4805 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4806 editor.transpose(&Default::default(), window, cx);
4807 assert_eq!(editor.text(cx), "acbd\ne");
4808 assert_eq!(editor.selections.ranges(cx), [5..5]);
4809
4810 editor.transpose(&Default::default(), window, cx);
4811 assert_eq!(editor.text(cx), "acbde\n");
4812 assert_eq!(editor.selections.ranges(cx), [6..6]);
4813
4814 editor.transpose(&Default::default(), window, cx);
4815 assert_eq!(editor.text(cx), "acbd\ne");
4816 assert_eq!(editor.selections.ranges(cx), [6..6]);
4817
4818 editor
4819 });
4820
4821 _ = cx.add_window(|window, cx| {
4822 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4823 editor.set_style(EditorStyle::default(), window, cx);
4824 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1, 2..2, 4..4]));
4825 editor.transpose(&Default::default(), window, cx);
4826 assert_eq!(editor.text(cx), "bacd\ne");
4827 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
4828
4829 editor.transpose(&Default::default(), window, cx);
4830 assert_eq!(editor.text(cx), "bcade\n");
4831 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
4832
4833 editor.transpose(&Default::default(), window, cx);
4834 assert_eq!(editor.text(cx), "bcda\ne");
4835 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4836
4837 editor.transpose(&Default::default(), window, cx);
4838 assert_eq!(editor.text(cx), "bcade\n");
4839 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4840
4841 editor.transpose(&Default::default(), window, cx);
4842 assert_eq!(editor.text(cx), "bcaed\n");
4843 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
4844
4845 editor
4846 });
4847
4848 _ = cx.add_window(|window, cx| {
4849 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
4850 editor.set_style(EditorStyle::default(), window, cx);
4851 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4852 editor.transpose(&Default::default(), window, cx);
4853 assert_eq!(editor.text(cx), "🏀🍐✋");
4854 assert_eq!(editor.selections.ranges(cx), [8..8]);
4855
4856 editor.transpose(&Default::default(), window, cx);
4857 assert_eq!(editor.text(cx), "🏀✋🍐");
4858 assert_eq!(editor.selections.ranges(cx), [11..11]);
4859
4860 editor.transpose(&Default::default(), window, cx);
4861 assert_eq!(editor.text(cx), "🏀🍐✋");
4862 assert_eq!(editor.selections.ranges(cx), [11..11]);
4863
4864 editor
4865 });
4866}
4867
4868#[gpui::test]
4869async fn test_rewrap(cx: &mut TestAppContext) {
4870 init_test(cx, |settings| {
4871 settings.languages.extend([
4872 (
4873 "Markdown".into(),
4874 LanguageSettingsContent {
4875 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4876 ..Default::default()
4877 },
4878 ),
4879 (
4880 "Plain Text".into(),
4881 LanguageSettingsContent {
4882 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4883 ..Default::default()
4884 },
4885 ),
4886 ])
4887 });
4888
4889 let mut cx = EditorTestContext::new(cx).await;
4890
4891 let language_with_c_comments = Arc::new(Language::new(
4892 LanguageConfig {
4893 line_comments: vec!["// ".into()],
4894 ..LanguageConfig::default()
4895 },
4896 None,
4897 ));
4898 let language_with_pound_comments = Arc::new(Language::new(
4899 LanguageConfig {
4900 line_comments: vec!["# ".into()],
4901 ..LanguageConfig::default()
4902 },
4903 None,
4904 ));
4905 let markdown_language = Arc::new(Language::new(
4906 LanguageConfig {
4907 name: "Markdown".into(),
4908 ..LanguageConfig::default()
4909 },
4910 None,
4911 ));
4912 let language_with_doc_comments = Arc::new(Language::new(
4913 LanguageConfig {
4914 line_comments: vec!["// ".into(), "/// ".into()],
4915 ..LanguageConfig::default()
4916 },
4917 Some(tree_sitter_rust::LANGUAGE.into()),
4918 ));
4919
4920 let plaintext_language = Arc::new(Language::new(
4921 LanguageConfig {
4922 name: "Plain Text".into(),
4923 ..LanguageConfig::default()
4924 },
4925 None,
4926 ));
4927
4928 assert_rewrap(
4929 indoc! {"
4930 // ˇ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.
4931 "},
4932 indoc! {"
4933 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4934 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4935 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4936 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4937 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4938 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4939 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4940 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4941 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4942 // porttitor id. Aliquam id accumsan eros.
4943 "},
4944 language_with_c_comments.clone(),
4945 &mut cx,
4946 );
4947
4948 // Test that rewrapping works inside of a selection
4949 assert_rewrap(
4950 indoc! {"
4951 «// 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.ˇ»
4952 "},
4953 indoc! {"
4954 «// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4955 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4956 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4957 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4958 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4959 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4960 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4961 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4962 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4963 // porttitor id. Aliquam id accumsan eros.ˇ»
4964 "},
4965 language_with_c_comments.clone(),
4966 &mut cx,
4967 );
4968
4969 // Test that cursors that expand to the same region are collapsed.
4970 assert_rewrap(
4971 indoc! {"
4972 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4973 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4974 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4975 // ˇ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.
4976 "},
4977 indoc! {"
4978 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4979 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4980 // auctor, eu lacinia sapien scelerisque. ˇVivamus sit amet neque et quam
4981 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4982 // Pellentesque odio lectus, iaculis ac volutpat et, ˇblandit quis urna. Sed
4983 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4984 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4985 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4986 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4987 // porttitor id. Aliquam id accumsan eros.
4988 "},
4989 language_with_c_comments.clone(),
4990 &mut cx,
4991 );
4992
4993 // Test that non-contiguous selections are treated separately.
4994 assert_rewrap(
4995 indoc! {"
4996 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4997 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4998 //
4999 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
5000 // ˇ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.
5001 "},
5002 indoc! {"
5003 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
5004 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
5005 // auctor, eu lacinia sapien scelerisque.
5006 //
5007 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas
5008 // tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
5009 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec
5010 // molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque
5011 // nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas
5012 // porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id
5013 // vulputate turpis porttitor id. Aliquam id accumsan eros.
5014 "},
5015 language_with_c_comments.clone(),
5016 &mut cx,
5017 );
5018
5019 // Test that different comment prefixes are supported.
5020 assert_rewrap(
5021 indoc! {"
5022 # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. 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.
5023 "},
5024 indoc! {"
5025 # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
5026 # purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
5027 # eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
5028 # hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
5029 # lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit
5030 # amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet
5031 # in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur
5032 # adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis.
5033 # Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id
5034 # accumsan eros.
5035 "},
5036 language_with_pound_comments.clone(),
5037 &mut cx,
5038 );
5039
5040 // Test that rewrapping is ignored outside of comments in most languages.
5041 assert_rewrap(
5042 indoc! {"
5043 /// Adds two numbers.
5044 /// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
5045 fn add(a: u32, b: u32) -> u32 {
5046 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ˇ
5047 }
5048 "},
5049 indoc! {"
5050 /// Adds two numbers. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
5051 /// Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
5052 fn add(a: u32, b: u32) -> u32 {
5053 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ˇ
5054 }
5055 "},
5056 language_with_doc_comments.clone(),
5057 &mut cx,
5058 );
5059
5060 // Test that rewrapping works in Markdown and Plain Text languages.
5061 assert_rewrap(
5062 indoc! {"
5063 # Hello
5064
5065 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.
5066 "},
5067 indoc! {"
5068 # Hello
5069
5070 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
5071 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
5072 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
5073 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
5074 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
5075 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
5076 Integer sit amet scelerisque nisi.
5077 "},
5078 markdown_language,
5079 &mut cx,
5080 );
5081
5082 assert_rewrap(
5083 indoc! {"
5084 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.
5085 "},
5086 indoc! {"
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 plaintext_language,
5096 &mut cx,
5097 );
5098
5099 // Test rewrapping unaligned comments in a selection.
5100 assert_rewrap(
5101 indoc! {"
5102 fn foo() {
5103 if true {
5104 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
5105 // Praesent semper egestas tellus id dignissim.ˇ»
5106 do_something();
5107 } else {
5108 //
5109 }
5110 }
5111 "},
5112 indoc! {"
5113 fn foo() {
5114 if true {
5115 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
5116 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
5117 // egestas tellus id dignissim.ˇ»
5118 do_something();
5119 } else {
5120 //
5121 }
5122 }
5123 "},
5124 language_with_doc_comments.clone(),
5125 &mut cx,
5126 );
5127
5128 assert_rewrap(
5129 indoc! {"
5130 fn foo() {
5131 if true {
5132 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
5133 // Praesent semper egestas tellus id dignissim.»
5134 do_something();
5135 } else {
5136 //
5137 }
5138
5139 }
5140 "},
5141 indoc! {"
5142 fn foo() {
5143 if true {
5144 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
5145 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
5146 // egestas tellus id dignissim.»
5147 do_something();
5148 } else {
5149 //
5150 }
5151
5152 }
5153 "},
5154 language_with_doc_comments.clone(),
5155 &mut cx,
5156 );
5157
5158 #[track_caller]
5159 fn assert_rewrap(
5160 unwrapped_text: &str,
5161 wrapped_text: &str,
5162 language: Arc<Language>,
5163 cx: &mut EditorTestContext,
5164 ) {
5165 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
5166 cx.set_state(unwrapped_text);
5167 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
5168 cx.assert_editor_state(wrapped_text);
5169 }
5170}
5171
5172#[gpui::test]
5173async fn test_hard_wrap(cx: &mut TestAppContext) {
5174 init_test(cx, |_| {});
5175 let mut cx = EditorTestContext::new(cx).await;
5176
5177 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
5178 cx.update_editor(|editor, _, cx| {
5179 editor.set_hard_wrap(Some(14), cx);
5180 });
5181
5182 cx.set_state(indoc!(
5183 "
5184 one two three ˇ
5185 "
5186 ));
5187 cx.simulate_input("four");
5188 cx.run_until_parked();
5189
5190 cx.assert_editor_state(indoc!(
5191 "
5192 one two three
5193 fourˇ
5194 "
5195 ));
5196
5197 cx.update_editor(|editor, window, cx| {
5198 editor.newline(&Default::default(), window, cx);
5199 });
5200 cx.run_until_parked();
5201 cx.assert_editor_state(indoc!(
5202 "
5203 one two three
5204 four
5205 ˇ
5206 "
5207 ));
5208
5209 cx.simulate_input("five");
5210 cx.run_until_parked();
5211 cx.assert_editor_state(indoc!(
5212 "
5213 one two three
5214 four
5215 fiveˇ
5216 "
5217 ));
5218
5219 cx.update_editor(|editor, window, cx| {
5220 editor.newline(&Default::default(), window, cx);
5221 });
5222 cx.run_until_parked();
5223 cx.simulate_input("# ");
5224 cx.run_until_parked();
5225 cx.assert_editor_state(indoc!(
5226 "
5227 one two three
5228 four
5229 five
5230 # ˇ
5231 "
5232 ));
5233
5234 cx.update_editor(|editor, window, cx| {
5235 editor.newline(&Default::default(), window, cx);
5236 });
5237 cx.run_until_parked();
5238 cx.assert_editor_state(indoc!(
5239 "
5240 one two three
5241 four
5242 five
5243 #\x20
5244 #ˇ
5245 "
5246 ));
5247
5248 cx.simulate_input(" 6");
5249 cx.run_until_parked();
5250 cx.assert_editor_state(indoc!(
5251 "
5252 one two three
5253 four
5254 five
5255 #
5256 # 6ˇ
5257 "
5258 ));
5259}
5260
5261#[gpui::test]
5262async fn test_clipboard(cx: &mut TestAppContext) {
5263 init_test(cx, |_| {});
5264
5265 let mut cx = EditorTestContext::new(cx).await;
5266
5267 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
5268 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5269 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
5270
5271 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
5272 cx.set_state("two ˇfour ˇsix ˇ");
5273 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5274 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
5275
5276 // Paste again but with only two cursors. Since the number of cursors doesn't
5277 // match the number of slices in the clipboard, the entire clipboard text
5278 // is pasted at each cursor.
5279 cx.set_state("ˇtwo one✅ four three six five ˇ");
5280 cx.update_editor(|e, window, cx| {
5281 e.handle_input("( ", window, cx);
5282 e.paste(&Paste, window, cx);
5283 e.handle_input(") ", window, cx);
5284 });
5285 cx.assert_editor_state(
5286 &([
5287 "( one✅ ",
5288 "three ",
5289 "five ) ˇtwo one✅ four three six five ( one✅ ",
5290 "three ",
5291 "five ) ˇ",
5292 ]
5293 .join("\n")),
5294 );
5295
5296 // Cut with three selections, one of which is full-line.
5297 cx.set_state(indoc! {"
5298 1«2ˇ»3
5299 4ˇ567
5300 «8ˇ»9"});
5301 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5302 cx.assert_editor_state(indoc! {"
5303 1ˇ3
5304 ˇ9"});
5305
5306 // Paste with three selections, noticing how the copied selection that was full-line
5307 // gets inserted before the second cursor.
5308 cx.set_state(indoc! {"
5309 1ˇ3
5310 9ˇ
5311 «oˇ»ne"});
5312 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5313 cx.assert_editor_state(indoc! {"
5314 12ˇ3
5315 4567
5316 9ˇ
5317 8ˇne"});
5318
5319 // Copy with a single cursor only, which writes the whole line into the clipboard.
5320 cx.set_state(indoc! {"
5321 The quick brown
5322 fox juˇmps over
5323 the lazy dog"});
5324 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5325 assert_eq!(
5326 cx.read_from_clipboard()
5327 .and_then(|item| item.text().as_deref().map(str::to_string)),
5328 Some("fox jumps over\n".to_string())
5329 );
5330
5331 // Paste with three selections, noticing how the copied full-line selection is inserted
5332 // before the empty selections but replaces the selection that is non-empty.
5333 cx.set_state(indoc! {"
5334 Tˇhe quick brown
5335 «foˇ»x jumps over
5336 tˇhe lazy dog"});
5337 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5338 cx.assert_editor_state(indoc! {"
5339 fox jumps over
5340 Tˇhe quick brown
5341 fox jumps over
5342 ˇx jumps over
5343 fox jumps over
5344 tˇhe lazy dog"});
5345}
5346
5347#[gpui::test]
5348async fn test_copy_trim(cx: &mut TestAppContext) {
5349 init_test(cx, |_| {});
5350
5351 let mut cx = EditorTestContext::new(cx).await;
5352 cx.set_state(
5353 r#" «for selection in selections.iter() {
5354 let mut start = selection.start;
5355 let mut end = selection.end;
5356 let is_entire_line = selection.is_empty();
5357 if is_entire_line {
5358 start = Point::new(start.row, 0);ˇ»
5359 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5360 }
5361 "#,
5362 );
5363 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5364 assert_eq!(
5365 cx.read_from_clipboard()
5366 .and_then(|item| item.text().as_deref().map(str::to_string)),
5367 Some(
5368 "for selection in selections.iter() {
5369 let mut start = selection.start;
5370 let mut end = selection.end;
5371 let is_entire_line = selection.is_empty();
5372 if is_entire_line {
5373 start = Point::new(start.row, 0);"
5374 .to_string()
5375 ),
5376 "Regular copying preserves all indentation selected",
5377 );
5378 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5379 assert_eq!(
5380 cx.read_from_clipboard()
5381 .and_then(|item| item.text().as_deref().map(str::to_string)),
5382 Some(
5383 "for selection in selections.iter() {
5384let mut start = selection.start;
5385let mut end = selection.end;
5386let is_entire_line = selection.is_empty();
5387if is_entire_line {
5388 start = Point::new(start.row, 0);"
5389 .to_string()
5390 ),
5391 "Copying with stripping should strip all leading whitespaces"
5392 );
5393
5394 cx.set_state(
5395 r#" « for selection in selections.iter() {
5396 let mut start = selection.start;
5397 let mut end = selection.end;
5398 let is_entire_line = selection.is_empty();
5399 if is_entire_line {
5400 start = Point::new(start.row, 0);ˇ»
5401 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5402 }
5403 "#,
5404 );
5405 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5406 assert_eq!(
5407 cx.read_from_clipboard()
5408 .and_then(|item| item.text().as_deref().map(str::to_string)),
5409 Some(
5410 " for selection in selections.iter() {
5411 let mut start = selection.start;
5412 let mut end = selection.end;
5413 let is_entire_line = selection.is_empty();
5414 if is_entire_line {
5415 start = Point::new(start.row, 0);"
5416 .to_string()
5417 ),
5418 "Regular copying preserves all indentation selected",
5419 );
5420 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5421 assert_eq!(
5422 cx.read_from_clipboard()
5423 .and_then(|item| item.text().as_deref().map(str::to_string)),
5424 Some(
5425 "for selection in selections.iter() {
5426let mut start = selection.start;
5427let mut end = selection.end;
5428let is_entire_line = selection.is_empty();
5429if is_entire_line {
5430 start = Point::new(start.row, 0);"
5431 .to_string()
5432 ),
5433 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
5434 );
5435
5436 cx.set_state(
5437 r#" «ˇ for selection in selections.iter() {
5438 let mut start = selection.start;
5439 let mut end = selection.end;
5440 let is_entire_line = selection.is_empty();
5441 if is_entire_line {
5442 start = Point::new(start.row, 0);»
5443 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5444 }
5445 "#,
5446 );
5447 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5448 assert_eq!(
5449 cx.read_from_clipboard()
5450 .and_then(|item| item.text().as_deref().map(str::to_string)),
5451 Some(
5452 " for selection in selections.iter() {
5453 let mut start = selection.start;
5454 let mut end = selection.end;
5455 let is_entire_line = selection.is_empty();
5456 if is_entire_line {
5457 start = Point::new(start.row, 0);"
5458 .to_string()
5459 ),
5460 "Regular copying for reverse selection works the same",
5461 );
5462 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5463 assert_eq!(
5464 cx.read_from_clipboard()
5465 .and_then(|item| item.text().as_deref().map(str::to_string)),
5466 Some(
5467 "for selection in selections.iter() {
5468let mut start = selection.start;
5469let mut end = selection.end;
5470let is_entire_line = selection.is_empty();
5471if is_entire_line {
5472 start = Point::new(start.row, 0);"
5473 .to_string()
5474 ),
5475 "Copying with stripping for reverse selection works the same"
5476 );
5477
5478 cx.set_state(
5479 r#" for selection «in selections.iter() {
5480 let mut start = selection.start;
5481 let mut end = selection.end;
5482 let is_entire_line = selection.is_empty();
5483 if is_entire_line {
5484 start = Point::new(start.row, 0);ˇ»
5485 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5486 }
5487 "#,
5488 );
5489 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5490 assert_eq!(
5491 cx.read_from_clipboard()
5492 .and_then(|item| item.text().as_deref().map(str::to_string)),
5493 Some(
5494 "in selections.iter() {
5495 let mut start = selection.start;
5496 let mut end = selection.end;
5497 let is_entire_line = selection.is_empty();
5498 if is_entire_line {
5499 start = Point::new(start.row, 0);"
5500 .to_string()
5501 ),
5502 "When selecting past the indent, the copying works as usual",
5503 );
5504 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5505 assert_eq!(
5506 cx.read_from_clipboard()
5507 .and_then(|item| item.text().as_deref().map(str::to_string)),
5508 Some(
5509 "in selections.iter() {
5510 let mut start = selection.start;
5511 let mut end = selection.end;
5512 let is_entire_line = selection.is_empty();
5513 if is_entire_line {
5514 start = Point::new(start.row, 0);"
5515 .to_string()
5516 ),
5517 "When selecting past the indent, nothing is trimmed"
5518 );
5519
5520 cx.set_state(
5521 r#" «for selection in selections.iter() {
5522 let mut start = selection.start;
5523
5524 let mut end = selection.end;
5525 let is_entire_line = selection.is_empty();
5526 if is_entire_line {
5527 start = Point::new(start.row, 0);
5528ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
5529 }
5530 "#,
5531 );
5532 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5533 assert_eq!(
5534 cx.read_from_clipboard()
5535 .and_then(|item| item.text().as_deref().map(str::to_string)),
5536 Some(
5537 "for selection in selections.iter() {
5538let mut start = selection.start;
5539
5540let mut end = selection.end;
5541let is_entire_line = selection.is_empty();
5542if is_entire_line {
5543 start = Point::new(start.row, 0);
5544"
5545 .to_string()
5546 ),
5547 "Copying with stripping should ignore empty lines"
5548 );
5549}
5550
5551#[gpui::test]
5552async fn test_paste_multiline(cx: &mut TestAppContext) {
5553 init_test(cx, |_| {});
5554
5555 let mut cx = EditorTestContext::new(cx).await;
5556 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5557
5558 // Cut an indented block, without the leading whitespace.
5559 cx.set_state(indoc! {"
5560 const a: B = (
5561 c(),
5562 «d(
5563 e,
5564 f
5565 )ˇ»
5566 );
5567 "});
5568 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5569 cx.assert_editor_state(indoc! {"
5570 const a: B = (
5571 c(),
5572 ˇ
5573 );
5574 "});
5575
5576 // Paste it at the same position.
5577 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5578 cx.assert_editor_state(indoc! {"
5579 const a: B = (
5580 c(),
5581 d(
5582 e,
5583 f
5584 )ˇ
5585 );
5586 "});
5587
5588 // Paste it at a line with a lower indent level.
5589 cx.set_state(indoc! {"
5590 ˇ
5591 const a: B = (
5592 c(),
5593 );
5594 "});
5595 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5596 cx.assert_editor_state(indoc! {"
5597 d(
5598 e,
5599 f
5600 )ˇ
5601 const a: B = (
5602 c(),
5603 );
5604 "});
5605
5606 // Cut an indented block, with the leading whitespace.
5607 cx.set_state(indoc! {"
5608 const a: B = (
5609 c(),
5610 « d(
5611 e,
5612 f
5613 )
5614 ˇ»);
5615 "});
5616 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5617 cx.assert_editor_state(indoc! {"
5618 const a: B = (
5619 c(),
5620 ˇ);
5621 "});
5622
5623 // Paste it at the same position.
5624 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5625 cx.assert_editor_state(indoc! {"
5626 const a: B = (
5627 c(),
5628 d(
5629 e,
5630 f
5631 )
5632 ˇ);
5633 "});
5634
5635 // Paste it at a line with a higher indent level.
5636 cx.set_state(indoc! {"
5637 const a: B = (
5638 c(),
5639 d(
5640 e,
5641 fˇ
5642 )
5643 );
5644 "});
5645 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5646 cx.assert_editor_state(indoc! {"
5647 const a: B = (
5648 c(),
5649 d(
5650 e,
5651 f d(
5652 e,
5653 f
5654 )
5655 ˇ
5656 )
5657 );
5658 "});
5659
5660 // Copy an indented block, starting mid-line
5661 cx.set_state(indoc! {"
5662 const a: B = (
5663 c(),
5664 somethin«g(
5665 e,
5666 f
5667 )ˇ»
5668 );
5669 "});
5670 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5671
5672 // Paste it on a line with a lower indent level
5673 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
5674 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5675 cx.assert_editor_state(indoc! {"
5676 const a: B = (
5677 c(),
5678 something(
5679 e,
5680 f
5681 )
5682 );
5683 g(
5684 e,
5685 f
5686 )ˇ"});
5687}
5688
5689#[gpui::test]
5690async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
5691 init_test(cx, |_| {});
5692
5693 cx.write_to_clipboard(ClipboardItem::new_string(
5694 " d(\n e\n );\n".into(),
5695 ));
5696
5697 let mut cx = EditorTestContext::new(cx).await;
5698 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5699
5700 cx.set_state(indoc! {"
5701 fn a() {
5702 b();
5703 if c() {
5704 ˇ
5705 }
5706 }
5707 "});
5708
5709 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5710 cx.assert_editor_state(indoc! {"
5711 fn a() {
5712 b();
5713 if c() {
5714 d(
5715 e
5716 );
5717 ˇ
5718 }
5719 }
5720 "});
5721
5722 cx.set_state(indoc! {"
5723 fn a() {
5724 b();
5725 ˇ
5726 }
5727 "});
5728
5729 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5730 cx.assert_editor_state(indoc! {"
5731 fn a() {
5732 b();
5733 d(
5734 e
5735 );
5736 ˇ
5737 }
5738 "});
5739}
5740
5741#[gpui::test]
5742fn test_select_all(cx: &mut TestAppContext) {
5743 init_test(cx, |_| {});
5744
5745 let editor = cx.add_window(|window, cx| {
5746 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
5747 build_editor(buffer, window, cx)
5748 });
5749 _ = editor.update(cx, |editor, window, cx| {
5750 editor.select_all(&SelectAll, window, cx);
5751 assert_eq!(
5752 editor.selections.display_ranges(cx),
5753 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
5754 );
5755 });
5756}
5757
5758#[gpui::test]
5759fn test_select_line(cx: &mut TestAppContext) {
5760 init_test(cx, |_| {});
5761
5762 let editor = cx.add_window(|window, cx| {
5763 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
5764 build_editor(buffer, window, cx)
5765 });
5766 _ = editor.update(cx, |editor, window, cx| {
5767 editor.change_selections(None, window, cx, |s| {
5768 s.select_display_ranges([
5769 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5770 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5771 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5772 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
5773 ])
5774 });
5775 editor.select_line(&SelectLine, window, cx);
5776 assert_eq!(
5777 editor.selections.display_ranges(cx),
5778 vec![
5779 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
5780 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
5781 ]
5782 );
5783 });
5784
5785 _ = editor.update(cx, |editor, window, cx| {
5786 editor.select_line(&SelectLine, window, cx);
5787 assert_eq!(
5788 editor.selections.display_ranges(cx),
5789 vec![
5790 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
5791 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
5792 ]
5793 );
5794 });
5795
5796 _ = editor.update(cx, |editor, window, cx| {
5797 editor.select_line(&SelectLine, window, cx);
5798 assert_eq!(
5799 editor.selections.display_ranges(cx),
5800 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
5801 );
5802 });
5803}
5804
5805#[gpui::test]
5806async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
5807 init_test(cx, |_| {});
5808 let mut cx = EditorTestContext::new(cx).await;
5809
5810 #[track_caller]
5811 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
5812 cx.set_state(initial_state);
5813 cx.update_editor(|e, window, cx| {
5814 e.split_selection_into_lines(&SplitSelectionIntoLines, window, cx)
5815 });
5816 cx.assert_editor_state(expected_state);
5817 }
5818
5819 // Selection starts and ends at the middle of lines, left-to-right
5820 test(
5821 &mut cx,
5822 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
5823 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5824 );
5825 // Same thing, right-to-left
5826 test(
5827 &mut cx,
5828 "aa\nb«b\ncc\ndd\neˇ»e\nff",
5829 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5830 );
5831
5832 // Whole buffer, left-to-right, last line *doesn't* end with newline
5833 test(
5834 &mut cx,
5835 "«ˇaa\nbb\ncc\ndd\nee\nff»",
5836 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5837 );
5838 // Same thing, right-to-left
5839 test(
5840 &mut cx,
5841 "«aa\nbb\ncc\ndd\nee\nffˇ»",
5842 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5843 );
5844
5845 // Whole buffer, left-to-right, last line ends with newline
5846 test(
5847 &mut cx,
5848 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
5849 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5850 );
5851 // Same thing, right-to-left
5852 test(
5853 &mut cx,
5854 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
5855 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5856 );
5857
5858 // Starts at the end of a line, ends at the start of another
5859 test(
5860 &mut cx,
5861 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
5862 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
5863 );
5864}
5865
5866#[gpui::test]
5867async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
5868 init_test(cx, |_| {});
5869
5870 let editor = cx.add_window(|window, cx| {
5871 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
5872 build_editor(buffer, window, cx)
5873 });
5874
5875 // setup
5876 _ = editor.update(cx, |editor, window, cx| {
5877 editor.fold_creases(
5878 vec![
5879 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5880 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5881 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5882 ],
5883 true,
5884 window,
5885 cx,
5886 );
5887 assert_eq!(
5888 editor.display_text(cx),
5889 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5890 );
5891 });
5892
5893 _ = editor.update(cx, |editor, window, cx| {
5894 editor.change_selections(None, window, cx, |s| {
5895 s.select_display_ranges([
5896 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5897 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5898 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5899 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
5900 ])
5901 });
5902 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5903 assert_eq!(
5904 editor.display_text(cx),
5905 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5906 );
5907 });
5908 EditorTestContext::for_editor(editor, cx)
5909 .await
5910 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
5911
5912 _ = editor.update(cx, |editor, window, cx| {
5913 editor.change_selections(None, window, cx, |s| {
5914 s.select_display_ranges([
5915 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
5916 ])
5917 });
5918 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5919 assert_eq!(
5920 editor.display_text(cx),
5921 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
5922 );
5923 assert_eq!(
5924 editor.selections.display_ranges(cx),
5925 [
5926 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
5927 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
5928 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
5929 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
5930 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
5931 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
5932 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
5933 ]
5934 );
5935 });
5936 EditorTestContext::for_editor(editor, cx)
5937 .await
5938 .assert_editor_state(
5939 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
5940 );
5941}
5942
5943#[gpui::test]
5944async fn test_add_selection_above_below(cx: &mut TestAppContext) {
5945 init_test(cx, |_| {});
5946
5947 let mut cx = EditorTestContext::new(cx).await;
5948
5949 cx.set_state(indoc!(
5950 r#"abc
5951 defˇghi
5952
5953 jk
5954 nlmo
5955 "#
5956 ));
5957
5958 cx.update_editor(|editor, window, cx| {
5959 editor.add_selection_above(&Default::default(), window, cx);
5960 });
5961
5962 cx.assert_editor_state(indoc!(
5963 r#"abcˇ
5964 defˇghi
5965
5966 jk
5967 nlmo
5968 "#
5969 ));
5970
5971 cx.update_editor(|editor, window, cx| {
5972 editor.add_selection_above(&Default::default(), window, cx);
5973 });
5974
5975 cx.assert_editor_state(indoc!(
5976 r#"abcˇ
5977 defˇghi
5978
5979 jk
5980 nlmo
5981 "#
5982 ));
5983
5984 cx.update_editor(|editor, window, cx| {
5985 editor.add_selection_below(&Default::default(), window, cx);
5986 });
5987
5988 cx.assert_editor_state(indoc!(
5989 r#"abc
5990 defˇghi
5991
5992 jk
5993 nlmo
5994 "#
5995 ));
5996
5997 cx.update_editor(|editor, window, cx| {
5998 editor.undo_selection(&Default::default(), window, cx);
5999 });
6000
6001 cx.assert_editor_state(indoc!(
6002 r#"abcˇ
6003 defˇghi
6004
6005 jk
6006 nlmo
6007 "#
6008 ));
6009
6010 cx.update_editor(|editor, window, cx| {
6011 editor.redo_selection(&Default::default(), window, cx);
6012 });
6013
6014 cx.assert_editor_state(indoc!(
6015 r#"abc
6016 defˇghi
6017
6018 jk
6019 nlmo
6020 "#
6021 ));
6022
6023 cx.update_editor(|editor, window, cx| {
6024 editor.add_selection_below(&Default::default(), window, cx);
6025 });
6026
6027 cx.assert_editor_state(indoc!(
6028 r#"abc
6029 defˇghi
6030
6031 jk
6032 nlmˇo
6033 "#
6034 ));
6035
6036 cx.update_editor(|editor, window, cx| {
6037 editor.add_selection_below(&Default::default(), window, cx);
6038 });
6039
6040 cx.assert_editor_state(indoc!(
6041 r#"abc
6042 defˇghi
6043
6044 jk
6045 nlmˇo
6046 "#
6047 ));
6048
6049 // change selections
6050 cx.set_state(indoc!(
6051 r#"abc
6052 def«ˇg»hi
6053
6054 jk
6055 nlmo
6056 "#
6057 ));
6058
6059 cx.update_editor(|editor, window, cx| {
6060 editor.add_selection_below(&Default::default(), window, cx);
6061 });
6062
6063 cx.assert_editor_state(indoc!(
6064 r#"abc
6065 def«ˇg»hi
6066
6067 jk
6068 nlm«ˇo»
6069 "#
6070 ));
6071
6072 cx.update_editor(|editor, window, cx| {
6073 editor.add_selection_below(&Default::default(), window, cx);
6074 });
6075
6076 cx.assert_editor_state(indoc!(
6077 r#"abc
6078 def«ˇg»hi
6079
6080 jk
6081 nlm«ˇo»
6082 "#
6083 ));
6084
6085 cx.update_editor(|editor, window, cx| {
6086 editor.add_selection_above(&Default::default(), window, cx);
6087 });
6088
6089 cx.assert_editor_state(indoc!(
6090 r#"abc
6091 def«ˇg»hi
6092
6093 jk
6094 nlmo
6095 "#
6096 ));
6097
6098 cx.update_editor(|editor, window, cx| {
6099 editor.add_selection_above(&Default::default(), window, cx);
6100 });
6101
6102 cx.assert_editor_state(indoc!(
6103 r#"abc
6104 def«ˇg»hi
6105
6106 jk
6107 nlmo
6108 "#
6109 ));
6110
6111 // Change selections again
6112 cx.set_state(indoc!(
6113 r#"a«bc
6114 defgˇ»hi
6115
6116 jk
6117 nlmo
6118 "#
6119 ));
6120
6121 cx.update_editor(|editor, window, cx| {
6122 editor.add_selection_below(&Default::default(), window, cx);
6123 });
6124
6125 cx.assert_editor_state(indoc!(
6126 r#"a«bcˇ»
6127 d«efgˇ»hi
6128
6129 j«kˇ»
6130 nlmo
6131 "#
6132 ));
6133
6134 cx.update_editor(|editor, window, cx| {
6135 editor.add_selection_below(&Default::default(), window, cx);
6136 });
6137 cx.assert_editor_state(indoc!(
6138 r#"a«bcˇ»
6139 d«efgˇ»hi
6140
6141 j«kˇ»
6142 n«lmoˇ»
6143 "#
6144 ));
6145 cx.update_editor(|editor, window, cx| {
6146 editor.add_selection_above(&Default::default(), window, cx);
6147 });
6148
6149 cx.assert_editor_state(indoc!(
6150 r#"a«bcˇ»
6151 d«efgˇ»hi
6152
6153 j«kˇ»
6154 nlmo
6155 "#
6156 ));
6157
6158 // Change selections again
6159 cx.set_state(indoc!(
6160 r#"abc
6161 d«ˇefghi
6162
6163 jk
6164 nlm»o
6165 "#
6166 ));
6167
6168 cx.update_editor(|editor, window, cx| {
6169 editor.add_selection_above(&Default::default(), window, cx);
6170 });
6171
6172 cx.assert_editor_state(indoc!(
6173 r#"a«ˇbc»
6174 d«ˇef»ghi
6175
6176 j«ˇk»
6177 n«ˇlm»o
6178 "#
6179 ));
6180
6181 cx.update_editor(|editor, window, cx| {
6182 editor.add_selection_below(&Default::default(), window, cx);
6183 });
6184
6185 cx.assert_editor_state(indoc!(
6186 r#"abc
6187 d«ˇef»ghi
6188
6189 j«ˇk»
6190 n«ˇlm»o
6191 "#
6192 ));
6193}
6194
6195#[gpui::test]
6196async fn test_select_next(cx: &mut TestAppContext) {
6197 init_test(cx, |_| {});
6198
6199 let mut cx = EditorTestContext::new(cx).await;
6200 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6201
6202 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6203 .unwrap();
6204 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6205
6206 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6207 .unwrap();
6208 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
6209
6210 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6211 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6212
6213 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6214 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
6215
6216 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6217 .unwrap();
6218 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6219
6220 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6221 .unwrap();
6222 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6223
6224 // Test selection direction should be preserved
6225 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
6226
6227 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6228 .unwrap();
6229 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
6230}
6231
6232#[gpui::test]
6233async fn test_select_all_matches(cx: &mut TestAppContext) {
6234 init_test(cx, |_| {});
6235
6236 let mut cx = EditorTestContext::new(cx).await;
6237
6238 // Test caret-only selections
6239 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6240 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6241 .unwrap();
6242 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6243
6244 // Test left-to-right selections
6245 cx.set_state("abc\n«abcˇ»\nabc");
6246 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6247 .unwrap();
6248 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
6249
6250 // Test right-to-left selections
6251 cx.set_state("abc\n«ˇabc»\nabc");
6252 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6253 .unwrap();
6254 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
6255
6256 // Test selecting whitespace with caret selection
6257 cx.set_state("abc\nˇ abc\nabc");
6258 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6259 .unwrap();
6260 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
6261
6262 // Test selecting whitespace with left-to-right selection
6263 cx.set_state("abc\n«ˇ »abc\nabc");
6264 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6265 .unwrap();
6266 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
6267
6268 // Test no matches with right-to-left selection
6269 cx.set_state("abc\n« ˇ»abc\nabc");
6270 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6271 .unwrap();
6272 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
6273}
6274
6275#[gpui::test]
6276async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
6277 init_test(cx, |_| {});
6278
6279 let mut cx = EditorTestContext::new(cx).await;
6280
6281 let large_body_1 = "\nd".repeat(200);
6282 let large_body_2 = "\ne".repeat(200);
6283
6284 cx.set_state(&format!(
6285 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
6286 ));
6287 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
6288 let scroll_position = editor.scroll_position(cx);
6289 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
6290 scroll_position
6291 });
6292
6293 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6294 .unwrap();
6295 cx.assert_editor_state(&format!(
6296 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
6297 ));
6298 let scroll_position_after_selection =
6299 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
6300 assert_eq!(
6301 initial_scroll_position, scroll_position_after_selection,
6302 "Scroll position should not change after selecting all matches"
6303 );
6304}
6305
6306#[gpui::test]
6307async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
6308 init_test(cx, |_| {});
6309
6310 let mut cx = EditorLspTestContext::new_rust(
6311 lsp::ServerCapabilities {
6312 document_formatting_provider: Some(lsp::OneOf::Left(true)),
6313 ..Default::default()
6314 },
6315 cx,
6316 )
6317 .await;
6318
6319 cx.set_state(indoc! {"
6320 line 1
6321 line 2
6322 linˇe 3
6323 line 4
6324 line 5
6325 "});
6326
6327 // Make an edit
6328 cx.update_editor(|editor, window, cx| {
6329 editor.handle_input("X", window, cx);
6330 });
6331
6332 // Move cursor to a different position
6333 cx.update_editor(|editor, window, cx| {
6334 editor.change_selections(None, window, cx, |s| {
6335 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
6336 });
6337 });
6338
6339 cx.assert_editor_state(indoc! {"
6340 line 1
6341 line 2
6342 linXe 3
6343 line 4
6344 liˇne 5
6345 "});
6346
6347 cx.lsp
6348 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
6349 Ok(Some(vec![lsp::TextEdit::new(
6350 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
6351 "PREFIX ".to_string(),
6352 )]))
6353 });
6354
6355 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
6356 .unwrap()
6357 .await
6358 .unwrap();
6359
6360 cx.assert_editor_state(indoc! {"
6361 PREFIX line 1
6362 line 2
6363 linXe 3
6364 line 4
6365 liˇne 5
6366 "});
6367
6368 // Undo formatting
6369 cx.update_editor(|editor, window, cx| {
6370 editor.undo(&Default::default(), window, cx);
6371 });
6372
6373 // Verify cursor moved back to position after edit
6374 cx.assert_editor_state(indoc! {"
6375 line 1
6376 line 2
6377 linXˇe 3
6378 line 4
6379 line 5
6380 "});
6381}
6382
6383#[gpui::test]
6384async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
6385 init_test(cx, |_| {});
6386
6387 let mut cx = EditorTestContext::new(cx).await;
6388 cx.set_state(
6389 r#"let foo = 2;
6390lˇet foo = 2;
6391let fooˇ = 2;
6392let foo = 2;
6393let foo = ˇ2;"#,
6394 );
6395
6396 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6397 .unwrap();
6398 cx.assert_editor_state(
6399 r#"let foo = 2;
6400«letˇ» foo = 2;
6401let «fooˇ» = 2;
6402let foo = 2;
6403let foo = «2ˇ»;"#,
6404 );
6405
6406 // noop for multiple selections with different contents
6407 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6408 .unwrap();
6409 cx.assert_editor_state(
6410 r#"let foo = 2;
6411«letˇ» foo = 2;
6412let «fooˇ» = 2;
6413let foo = 2;
6414let foo = «2ˇ»;"#,
6415 );
6416
6417 // Test last selection direction should be preserved
6418 cx.set_state(
6419 r#"let foo = 2;
6420let foo = 2;
6421let «fooˇ» = 2;
6422let «ˇfoo» = 2;
6423let foo = 2;"#,
6424 );
6425
6426 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6427 .unwrap();
6428 cx.assert_editor_state(
6429 r#"let foo = 2;
6430let foo = 2;
6431let «fooˇ» = 2;
6432let «ˇfoo» = 2;
6433let «ˇfoo» = 2;"#,
6434 );
6435}
6436
6437#[gpui::test]
6438async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
6439 init_test(cx, |_| {});
6440
6441 let mut cx =
6442 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
6443
6444 cx.assert_editor_state(indoc! {"
6445 ˇbbb
6446 ccc
6447
6448 bbb
6449 ccc
6450 "});
6451 cx.dispatch_action(SelectPrevious::default());
6452 cx.assert_editor_state(indoc! {"
6453 «bbbˇ»
6454 ccc
6455
6456 bbb
6457 ccc
6458 "});
6459 cx.dispatch_action(SelectPrevious::default());
6460 cx.assert_editor_state(indoc! {"
6461 «bbbˇ»
6462 ccc
6463
6464 «bbbˇ»
6465 ccc
6466 "});
6467}
6468
6469#[gpui::test]
6470async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
6471 init_test(cx, |_| {});
6472
6473 let mut cx = EditorTestContext::new(cx).await;
6474 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6475
6476 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6477 .unwrap();
6478 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6479
6480 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6481 .unwrap();
6482 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
6483
6484 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6485 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6486
6487 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6488 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
6489
6490 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6491 .unwrap();
6492 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
6493
6494 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6495 .unwrap();
6496 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6497}
6498
6499#[gpui::test]
6500async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
6501 init_test(cx, |_| {});
6502
6503 let mut cx = EditorTestContext::new(cx).await;
6504 cx.set_state("aˇ");
6505
6506 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6507 .unwrap();
6508 cx.assert_editor_state("«aˇ»");
6509 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6510 .unwrap();
6511 cx.assert_editor_state("«aˇ»");
6512}
6513
6514#[gpui::test]
6515async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
6516 init_test(cx, |_| {});
6517
6518 let mut cx = EditorTestContext::new(cx).await;
6519 cx.set_state(
6520 r#"let foo = 2;
6521lˇet foo = 2;
6522let fooˇ = 2;
6523let foo = 2;
6524let foo = ˇ2;"#,
6525 );
6526
6527 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6528 .unwrap();
6529 cx.assert_editor_state(
6530 r#"let foo = 2;
6531«letˇ» foo = 2;
6532let «fooˇ» = 2;
6533let foo = 2;
6534let foo = «2ˇ»;"#,
6535 );
6536
6537 // noop for multiple selections with different contents
6538 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6539 .unwrap();
6540 cx.assert_editor_state(
6541 r#"let foo = 2;
6542«letˇ» foo = 2;
6543let «fooˇ» = 2;
6544let foo = 2;
6545let foo = «2ˇ»;"#,
6546 );
6547}
6548
6549#[gpui::test]
6550async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
6551 init_test(cx, |_| {});
6552
6553 let mut cx = EditorTestContext::new(cx).await;
6554 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
6555
6556 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6557 .unwrap();
6558 // selection direction is preserved
6559 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
6560
6561 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6562 .unwrap();
6563 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
6564
6565 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6566 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
6567
6568 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6569 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
6570
6571 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6572 .unwrap();
6573 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
6574
6575 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6576 .unwrap();
6577 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
6578}
6579
6580#[gpui::test]
6581async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
6582 init_test(cx, |_| {});
6583
6584 let language = Arc::new(Language::new(
6585 LanguageConfig::default(),
6586 Some(tree_sitter_rust::LANGUAGE.into()),
6587 ));
6588
6589 let text = r#"
6590 use mod1::mod2::{mod3, mod4};
6591
6592 fn fn_1(param1: bool, param2: &str) {
6593 let var1 = "text";
6594 }
6595 "#
6596 .unindent();
6597
6598 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6599 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6600 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6601
6602 editor
6603 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6604 .await;
6605
6606 editor.update_in(cx, |editor, window, cx| {
6607 editor.change_selections(None, window, cx, |s| {
6608 s.select_display_ranges([
6609 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
6610 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
6611 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
6612 ]);
6613 });
6614 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6615 });
6616 editor.update(cx, |editor, cx| {
6617 assert_text_with_selections(
6618 editor,
6619 indoc! {r#"
6620 use mod1::mod2::{mod3, «mod4ˇ»};
6621
6622 fn fn_1«ˇ(param1: bool, param2: &str)» {
6623 let var1 = "«ˇtext»";
6624 }
6625 "#},
6626 cx,
6627 );
6628 });
6629
6630 editor.update_in(cx, |editor, window, cx| {
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 assert_eq!(
6651 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
6652 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
6653 );
6654
6655 // Trying to expand the selected syntax node one more time has no effect.
6656 editor.update_in(cx, |editor, window, cx| {
6657 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6658 });
6659 assert_eq!(
6660 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
6661 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
6662 );
6663
6664 editor.update_in(cx, |editor, window, cx| {
6665 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6666 });
6667 editor.update(cx, |editor, cx| {
6668 assert_text_with_selections(
6669 editor,
6670 indoc! {r#"
6671 use mod1::mod2::«{mod3, mod4}ˇ»;
6672
6673 «ˇfn fn_1(param1: bool, param2: &str) {
6674 let var1 = "text";
6675 }»
6676 "#},
6677 cx,
6678 );
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, mo«ˇ»d4};
6706
6707 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
6708 let var1 = "te«ˇ»xt";
6709 }
6710 "#},
6711 cx,
6712 );
6713 });
6714
6715 // Trying to shrink the selected syntax node one more time has no effect.
6716 editor.update_in(cx, |editor, window, cx| {
6717 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6718 });
6719 editor.update_in(cx, |editor, _, cx| {
6720 assert_text_with_selections(
6721 editor,
6722 indoc! {r#"
6723 use mod1::mod2::{mod3, mo«ˇ»d4};
6724
6725 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
6726 let var1 = "te«ˇ»xt";
6727 }
6728 "#},
6729 cx,
6730 );
6731 });
6732
6733 // Ensure that we keep expanding the selection if the larger selection starts or ends within
6734 // a fold.
6735 editor.update_in(cx, |editor, window, cx| {
6736 editor.fold_creases(
6737 vec![
6738 Crease::simple(
6739 Point::new(0, 21)..Point::new(0, 24),
6740 FoldPlaceholder::test(),
6741 ),
6742 Crease::simple(
6743 Point::new(3, 20)..Point::new(3, 22),
6744 FoldPlaceholder::test(),
6745 ),
6746 ],
6747 true,
6748 window,
6749 cx,
6750 );
6751 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6752 });
6753 editor.update(cx, |editor, cx| {
6754 assert_text_with_selections(
6755 editor,
6756 indoc! {r#"
6757 use mod1::mod2::«{mod3, mod4}ˇ»;
6758
6759 fn fn_1«ˇ(param1: bool, param2: &str)» {
6760 let var1 = "«ˇtext»";
6761 }
6762 "#},
6763 cx,
6764 );
6765 });
6766}
6767
6768#[gpui::test]
6769async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
6770 init_test(cx, |_| {});
6771
6772 let language = Arc::new(Language::new(
6773 LanguageConfig::default(),
6774 Some(tree_sitter_rust::LANGUAGE.into()),
6775 ));
6776
6777 let text = "let a = 2;";
6778
6779 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6780 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6781 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6782
6783 editor
6784 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6785 .await;
6786
6787 // Test case 1: Cursor at end of word
6788 editor.update_in(cx, |editor, window, cx| {
6789 editor.change_selections(None, window, cx, |s| {
6790 s.select_display_ranges([
6791 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
6792 ]);
6793 });
6794 });
6795 editor.update(cx, |editor, cx| {
6796 assert_text_with_selections(editor, "let aˇ = 2;", cx);
6797 });
6798 editor.update_in(cx, |editor, window, cx| {
6799 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6800 });
6801 editor.update(cx, |editor, cx| {
6802 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
6803 });
6804 editor.update_in(cx, |editor, window, cx| {
6805 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6806 });
6807 editor.update(cx, |editor, cx| {
6808 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
6809 });
6810
6811 // Test case 2: Cursor at end of statement
6812 editor.update_in(cx, |editor, window, cx| {
6813 editor.change_selections(None, window, cx, |s| {
6814 s.select_display_ranges([
6815 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
6816 ]);
6817 });
6818 });
6819 editor.update(cx, |editor, cx| {
6820 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
6821 });
6822 editor.update_in(cx, |editor, window, cx| {
6823 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6824 });
6825 editor.update(cx, |editor, cx| {
6826 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
6827 });
6828}
6829
6830#[gpui::test]
6831async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
6832 init_test(cx, |_| {});
6833
6834 let language = Arc::new(Language::new(
6835 LanguageConfig::default(),
6836 Some(tree_sitter_rust::LANGUAGE.into()),
6837 ));
6838
6839 let text = r#"
6840 use mod1::mod2::{mod3, mod4};
6841
6842 fn fn_1(param1: bool, param2: &str) {
6843 let var1 = "hello world";
6844 }
6845 "#
6846 .unindent();
6847
6848 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6849 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6850 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6851
6852 editor
6853 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6854 .await;
6855
6856 // Test 1: Cursor on a letter of a string word
6857 editor.update_in(cx, |editor, window, cx| {
6858 editor.change_selections(None, window, cx, |s| {
6859 s.select_display_ranges([
6860 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
6861 ]);
6862 });
6863 });
6864 editor.update_in(cx, |editor, window, cx| {
6865 assert_text_with_selections(
6866 editor,
6867 indoc! {r#"
6868 use mod1::mod2::{mod3, mod4};
6869
6870 fn fn_1(param1: bool, param2: &str) {
6871 let var1 = "hˇello world";
6872 }
6873 "#},
6874 cx,
6875 );
6876 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6877 assert_text_with_selections(
6878 editor,
6879 indoc! {r#"
6880 use mod1::mod2::{mod3, mod4};
6881
6882 fn fn_1(param1: bool, param2: &str) {
6883 let var1 = "«ˇhello» world";
6884 }
6885 "#},
6886 cx,
6887 );
6888 });
6889
6890 // Test 2: Partial selection within a word
6891 editor.update_in(cx, |editor, window, cx| {
6892 editor.change_selections(None, window, cx, |s| {
6893 s.select_display_ranges([
6894 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
6895 ]);
6896 });
6897 });
6898 editor.update_in(cx, |editor, window, cx| {
6899 assert_text_with_selections(
6900 editor,
6901 indoc! {r#"
6902 use mod1::mod2::{mod3, mod4};
6903
6904 fn fn_1(param1: bool, param2: &str) {
6905 let var1 = "h«elˇ»lo world";
6906 }
6907 "#},
6908 cx,
6909 );
6910 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6911 assert_text_with_selections(
6912 editor,
6913 indoc! {r#"
6914 use mod1::mod2::{mod3, mod4};
6915
6916 fn fn_1(param1: bool, param2: &str) {
6917 let var1 = "«ˇhello» world";
6918 }
6919 "#},
6920 cx,
6921 );
6922 });
6923
6924 // Test 3: Complete word already selected
6925 editor.update_in(cx, |editor, window, cx| {
6926 editor.change_selections(None, window, cx, |s| {
6927 s.select_display_ranges([
6928 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
6929 ]);
6930 });
6931 });
6932 editor.update_in(cx, |editor, window, cx| {
6933 assert_text_with_selections(
6934 editor,
6935 indoc! {r#"
6936 use mod1::mod2::{mod3, mod4};
6937
6938 fn fn_1(param1: bool, param2: &str) {
6939 let var1 = "«helloˇ» world";
6940 }
6941 "#},
6942 cx,
6943 );
6944 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6945 assert_text_with_selections(
6946 editor,
6947 indoc! {r#"
6948 use mod1::mod2::{mod3, mod4};
6949
6950 fn fn_1(param1: bool, param2: &str) {
6951 let var1 = "«hello worldˇ»";
6952 }
6953 "#},
6954 cx,
6955 );
6956 });
6957
6958 // Test 4: Selection spanning across words
6959 editor.update_in(cx, |editor, window, cx| {
6960 editor.change_selections(None, window, cx, |s| {
6961 s.select_display_ranges([
6962 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
6963 ]);
6964 });
6965 });
6966 editor.update_in(cx, |editor, window, cx| {
6967 assert_text_with_selections(
6968 editor,
6969 indoc! {r#"
6970 use mod1::mod2::{mod3, mod4};
6971
6972 fn fn_1(param1: bool, param2: &str) {
6973 let var1 = "hel«lo woˇ»rld";
6974 }
6975 "#},
6976 cx,
6977 );
6978 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6979 assert_text_with_selections(
6980 editor,
6981 indoc! {r#"
6982 use mod1::mod2::{mod3, mod4};
6983
6984 fn fn_1(param1: bool, param2: &str) {
6985 let var1 = "«ˇhello world»";
6986 }
6987 "#},
6988 cx,
6989 );
6990 });
6991
6992 // Test 5: Expansion beyond string
6993 editor.update_in(cx, |editor, window, cx| {
6994 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
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
7010#[gpui::test]
7011async fn test_fold_function_bodies(cx: &mut TestAppContext) {
7012 init_test(cx, |_| {});
7013
7014 let base_text = r#"
7015 impl A {
7016 // this is an uncommitted comment
7017
7018 fn b() {
7019 c();
7020 }
7021
7022 // this is another uncommitted comment
7023
7024 fn d() {
7025 // e
7026 // f
7027 }
7028 }
7029
7030 fn g() {
7031 // h
7032 }
7033 "#
7034 .unindent();
7035
7036 let text = r#"
7037 ˇimpl A {
7038
7039 fn b() {
7040 c();
7041 }
7042
7043 fn d() {
7044 // e
7045 // f
7046 }
7047 }
7048
7049 fn g() {
7050 // h
7051 }
7052 "#
7053 .unindent();
7054
7055 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
7056 cx.set_state(&text);
7057 cx.set_head_text(&base_text);
7058 cx.update_editor(|editor, window, cx| {
7059 editor.expand_all_diff_hunks(&Default::default(), window, cx);
7060 });
7061
7062 cx.assert_state_with_diff(
7063 "
7064 ˇimpl A {
7065 - // this is an uncommitted comment
7066
7067 fn b() {
7068 c();
7069 }
7070
7071 - // this is another uncommitted comment
7072 -
7073 fn d() {
7074 // e
7075 // f
7076 }
7077 }
7078
7079 fn g() {
7080 // h
7081 }
7082 "
7083 .unindent(),
7084 );
7085
7086 let expected_display_text = "
7087 impl A {
7088 // this is an uncommitted comment
7089
7090 fn b() {
7091 ⋯
7092 }
7093
7094 // this is another uncommitted comment
7095
7096 fn d() {
7097 ⋯
7098 }
7099 }
7100
7101 fn g() {
7102 ⋯
7103 }
7104 "
7105 .unindent();
7106
7107 cx.update_editor(|editor, window, cx| {
7108 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
7109 assert_eq!(editor.display_text(cx), expected_display_text);
7110 });
7111}
7112
7113#[gpui::test]
7114async fn test_autoindent(cx: &mut TestAppContext) {
7115 init_test(cx, |_| {});
7116
7117 let language = Arc::new(
7118 Language::new(
7119 LanguageConfig {
7120 brackets: BracketPairConfig {
7121 pairs: vec![
7122 BracketPair {
7123 start: "{".to_string(),
7124 end: "}".to_string(),
7125 close: false,
7126 surround: false,
7127 newline: true,
7128 },
7129 BracketPair {
7130 start: "(".to_string(),
7131 end: ")".to_string(),
7132 close: false,
7133 surround: false,
7134 newline: true,
7135 },
7136 ],
7137 ..Default::default()
7138 },
7139 ..Default::default()
7140 },
7141 Some(tree_sitter_rust::LANGUAGE.into()),
7142 )
7143 .with_indents_query(
7144 r#"
7145 (_ "(" ")" @end) @indent
7146 (_ "{" "}" @end) @indent
7147 "#,
7148 )
7149 .unwrap(),
7150 );
7151
7152 let text = "fn a() {}";
7153
7154 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7155 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7156 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7157 editor
7158 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7159 .await;
7160
7161 editor.update_in(cx, |editor, window, cx| {
7162 editor.change_selections(None, window, cx, |s| s.select_ranges([5..5, 8..8, 9..9]));
7163 editor.newline(&Newline, window, cx);
7164 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
7165 assert_eq!(
7166 editor.selections.ranges(cx),
7167 &[
7168 Point::new(1, 4)..Point::new(1, 4),
7169 Point::new(3, 4)..Point::new(3, 4),
7170 Point::new(5, 0)..Point::new(5, 0)
7171 ]
7172 );
7173 });
7174}
7175
7176#[gpui::test]
7177async fn test_autoindent_selections(cx: &mut TestAppContext) {
7178 init_test(cx, |_| {});
7179
7180 {
7181 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
7182 cx.set_state(indoc! {"
7183 impl A {
7184
7185 fn b() {}
7186
7187 «fn c() {
7188
7189 }ˇ»
7190 }
7191 "});
7192
7193 cx.update_editor(|editor, window, cx| {
7194 editor.autoindent(&Default::default(), window, cx);
7195 });
7196
7197 cx.assert_editor_state(indoc! {"
7198 impl A {
7199
7200 fn b() {}
7201
7202 «fn c() {
7203
7204 }ˇ»
7205 }
7206 "});
7207 }
7208
7209 {
7210 let mut cx = EditorTestContext::new_multibuffer(
7211 cx,
7212 [indoc! { "
7213 impl A {
7214 «
7215 // a
7216 fn b(){}
7217 »
7218 «
7219 }
7220 fn c(){}
7221 »
7222 "}],
7223 );
7224
7225 let buffer = cx.update_editor(|editor, _, cx| {
7226 let buffer = editor.buffer().update(cx, |buffer, _| {
7227 buffer.all_buffers().iter().next().unwrap().clone()
7228 });
7229 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7230 buffer
7231 });
7232
7233 cx.run_until_parked();
7234 cx.update_editor(|editor, window, cx| {
7235 editor.select_all(&Default::default(), window, cx);
7236 editor.autoindent(&Default::default(), window, cx)
7237 });
7238 cx.run_until_parked();
7239
7240 cx.update(|_, cx| {
7241 assert_eq!(
7242 buffer.read(cx).text(),
7243 indoc! { "
7244 impl A {
7245
7246 // a
7247 fn b(){}
7248
7249
7250 }
7251 fn c(){}
7252
7253 " }
7254 )
7255 });
7256 }
7257}
7258
7259#[gpui::test]
7260async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
7261 init_test(cx, |_| {});
7262
7263 let mut cx = EditorTestContext::new(cx).await;
7264
7265 let language = Arc::new(Language::new(
7266 LanguageConfig {
7267 brackets: BracketPairConfig {
7268 pairs: vec![
7269 BracketPair {
7270 start: "{".to_string(),
7271 end: "}".to_string(),
7272 close: true,
7273 surround: true,
7274 newline: true,
7275 },
7276 BracketPair {
7277 start: "(".to_string(),
7278 end: ")".to_string(),
7279 close: true,
7280 surround: true,
7281 newline: true,
7282 },
7283 BracketPair {
7284 start: "/*".to_string(),
7285 end: " */".to_string(),
7286 close: true,
7287 surround: true,
7288 newline: true,
7289 },
7290 BracketPair {
7291 start: "[".to_string(),
7292 end: "]".to_string(),
7293 close: false,
7294 surround: false,
7295 newline: true,
7296 },
7297 BracketPair {
7298 start: "\"".to_string(),
7299 end: "\"".to_string(),
7300 close: true,
7301 surround: true,
7302 newline: false,
7303 },
7304 BracketPair {
7305 start: "<".to_string(),
7306 end: ">".to_string(),
7307 close: false,
7308 surround: true,
7309 newline: true,
7310 },
7311 ],
7312 ..Default::default()
7313 },
7314 autoclose_before: "})]".to_string(),
7315 ..Default::default()
7316 },
7317 Some(tree_sitter_rust::LANGUAGE.into()),
7318 ));
7319
7320 cx.language_registry().add(language.clone());
7321 cx.update_buffer(|buffer, cx| {
7322 buffer.set_language(Some(language), cx);
7323 });
7324
7325 cx.set_state(
7326 &r#"
7327 🏀ˇ
7328 εˇ
7329 ❤️ˇ
7330 "#
7331 .unindent(),
7332 );
7333
7334 // autoclose multiple nested brackets at multiple cursors
7335 cx.update_editor(|editor, window, cx| {
7336 editor.handle_input("{", window, cx);
7337 editor.handle_input("{", window, cx);
7338 editor.handle_input("{", window, cx);
7339 });
7340 cx.assert_editor_state(
7341 &"
7342 🏀{{{ˇ}}}
7343 ε{{{ˇ}}}
7344 ❤️{{{ˇ}}}
7345 "
7346 .unindent(),
7347 );
7348
7349 // insert a different closing bracket
7350 cx.update_editor(|editor, window, cx| {
7351 editor.handle_input(")", window, cx);
7352 });
7353 cx.assert_editor_state(
7354 &"
7355 🏀{{{)ˇ}}}
7356 ε{{{)ˇ}}}
7357 ❤️{{{)ˇ}}}
7358 "
7359 .unindent(),
7360 );
7361
7362 // skip over the auto-closed brackets when typing a closing bracket
7363 cx.update_editor(|editor, window, cx| {
7364 editor.move_right(&MoveRight, window, cx);
7365 editor.handle_input("}", window, cx);
7366 editor.handle_input("}", window, cx);
7367 editor.handle_input("}", window, cx);
7368 });
7369 cx.assert_editor_state(
7370 &"
7371 🏀{{{)}}}}ˇ
7372 ε{{{)}}}}ˇ
7373 ❤️{{{)}}}}ˇ
7374 "
7375 .unindent(),
7376 );
7377
7378 // autoclose multi-character pairs
7379 cx.set_state(
7380 &"
7381 ˇ
7382 ˇ
7383 "
7384 .unindent(),
7385 );
7386 cx.update_editor(|editor, window, cx| {
7387 editor.handle_input("/", window, cx);
7388 editor.handle_input("*", window, cx);
7389 });
7390 cx.assert_editor_state(
7391 &"
7392 /*ˇ */
7393 /*ˇ */
7394 "
7395 .unindent(),
7396 );
7397
7398 // one cursor autocloses a multi-character pair, one cursor
7399 // does not autoclose.
7400 cx.set_state(
7401 &"
7402 /ˇ
7403 ˇ
7404 "
7405 .unindent(),
7406 );
7407 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
7408 cx.assert_editor_state(
7409 &"
7410 /*ˇ */
7411 *ˇ
7412 "
7413 .unindent(),
7414 );
7415
7416 // Don't autoclose if the next character isn't whitespace and isn't
7417 // listed in the language's "autoclose_before" section.
7418 cx.set_state("ˇa b");
7419 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7420 cx.assert_editor_state("{ˇa b");
7421
7422 // Don't autoclose if `close` is false for the bracket pair
7423 cx.set_state("ˇ");
7424 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
7425 cx.assert_editor_state("[ˇ");
7426
7427 // Surround with brackets if text is selected
7428 cx.set_state("«aˇ» b");
7429 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7430 cx.assert_editor_state("{«aˇ»} b");
7431
7432 // Autoclose when not immediately after a word character
7433 cx.set_state("a ˇ");
7434 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7435 cx.assert_editor_state("a \"ˇ\"");
7436
7437 // Autoclose pair where the start and end characters are the same
7438 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7439 cx.assert_editor_state("a \"\"ˇ");
7440
7441 // Don't autoclose when immediately after a word character
7442 cx.set_state("aˇ");
7443 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7444 cx.assert_editor_state("a\"ˇ");
7445
7446 // Do autoclose when after a non-word character
7447 cx.set_state("{ˇ");
7448 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7449 cx.assert_editor_state("{\"ˇ\"");
7450
7451 // Non identical pairs autoclose regardless of preceding character
7452 cx.set_state("aˇ");
7453 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7454 cx.assert_editor_state("a{ˇ}");
7455
7456 // Don't autoclose pair if autoclose is disabled
7457 cx.set_state("ˇ");
7458 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
7459 cx.assert_editor_state("<ˇ");
7460
7461 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
7462 cx.set_state("«aˇ» b");
7463 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
7464 cx.assert_editor_state("<«aˇ»> b");
7465}
7466
7467#[gpui::test]
7468async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
7469 init_test(cx, |settings| {
7470 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
7471 });
7472
7473 let mut cx = EditorTestContext::new(cx).await;
7474
7475 let language = Arc::new(Language::new(
7476 LanguageConfig {
7477 brackets: BracketPairConfig {
7478 pairs: vec![
7479 BracketPair {
7480 start: "{".to_string(),
7481 end: "}".to_string(),
7482 close: true,
7483 surround: true,
7484 newline: true,
7485 },
7486 BracketPair {
7487 start: "(".to_string(),
7488 end: ")".to_string(),
7489 close: true,
7490 surround: true,
7491 newline: true,
7492 },
7493 BracketPair {
7494 start: "[".to_string(),
7495 end: "]".to_string(),
7496 close: false,
7497 surround: false,
7498 newline: true,
7499 },
7500 ],
7501 ..Default::default()
7502 },
7503 autoclose_before: "})]".to_string(),
7504 ..Default::default()
7505 },
7506 Some(tree_sitter_rust::LANGUAGE.into()),
7507 ));
7508
7509 cx.language_registry().add(language.clone());
7510 cx.update_buffer(|buffer, cx| {
7511 buffer.set_language(Some(language), cx);
7512 });
7513
7514 cx.set_state(
7515 &"
7516 ˇ
7517 ˇ
7518 ˇ
7519 "
7520 .unindent(),
7521 );
7522
7523 // ensure only matching closing brackets are skipped over
7524 cx.update_editor(|editor, window, cx| {
7525 editor.handle_input("}", window, cx);
7526 editor.move_left(&MoveLeft, window, cx);
7527 editor.handle_input(")", window, cx);
7528 editor.move_left(&MoveLeft, window, cx);
7529 });
7530 cx.assert_editor_state(
7531 &"
7532 ˇ)}
7533 ˇ)}
7534 ˇ)}
7535 "
7536 .unindent(),
7537 );
7538
7539 // skip-over closing brackets at multiple cursors
7540 cx.update_editor(|editor, window, cx| {
7541 editor.handle_input(")", window, cx);
7542 editor.handle_input("}", window, cx);
7543 });
7544 cx.assert_editor_state(
7545 &"
7546 )}ˇ
7547 )}ˇ
7548 )}ˇ
7549 "
7550 .unindent(),
7551 );
7552
7553 // ignore non-close brackets
7554 cx.update_editor(|editor, window, cx| {
7555 editor.handle_input("]", window, cx);
7556 editor.move_left(&MoveLeft, window, cx);
7557 editor.handle_input("]", window, cx);
7558 });
7559 cx.assert_editor_state(
7560 &"
7561 )}]ˇ]
7562 )}]ˇ]
7563 )}]ˇ]
7564 "
7565 .unindent(),
7566 );
7567}
7568
7569#[gpui::test]
7570async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
7571 init_test(cx, |_| {});
7572
7573 let mut cx = EditorTestContext::new(cx).await;
7574
7575 let html_language = Arc::new(
7576 Language::new(
7577 LanguageConfig {
7578 name: "HTML".into(),
7579 brackets: BracketPairConfig {
7580 pairs: vec![
7581 BracketPair {
7582 start: "<".into(),
7583 end: ">".into(),
7584 close: true,
7585 ..Default::default()
7586 },
7587 BracketPair {
7588 start: "{".into(),
7589 end: "}".into(),
7590 close: true,
7591 ..Default::default()
7592 },
7593 BracketPair {
7594 start: "(".into(),
7595 end: ")".into(),
7596 close: true,
7597 ..Default::default()
7598 },
7599 ],
7600 ..Default::default()
7601 },
7602 autoclose_before: "})]>".into(),
7603 ..Default::default()
7604 },
7605 Some(tree_sitter_html::LANGUAGE.into()),
7606 )
7607 .with_injection_query(
7608 r#"
7609 (script_element
7610 (raw_text) @injection.content
7611 (#set! injection.language "javascript"))
7612 "#,
7613 )
7614 .unwrap(),
7615 );
7616
7617 let javascript_language = Arc::new(Language::new(
7618 LanguageConfig {
7619 name: "JavaScript".into(),
7620 brackets: BracketPairConfig {
7621 pairs: vec![
7622 BracketPair {
7623 start: "/*".into(),
7624 end: " */".into(),
7625 close: true,
7626 ..Default::default()
7627 },
7628 BracketPair {
7629 start: "{".into(),
7630 end: "}".into(),
7631 close: true,
7632 ..Default::default()
7633 },
7634 BracketPair {
7635 start: "(".into(),
7636 end: ")".into(),
7637 close: true,
7638 ..Default::default()
7639 },
7640 ],
7641 ..Default::default()
7642 },
7643 autoclose_before: "})]>".into(),
7644 ..Default::default()
7645 },
7646 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
7647 ));
7648
7649 cx.language_registry().add(html_language.clone());
7650 cx.language_registry().add(javascript_language.clone());
7651
7652 cx.update_buffer(|buffer, cx| {
7653 buffer.set_language(Some(html_language), cx);
7654 });
7655
7656 cx.set_state(
7657 &r#"
7658 <body>ˇ
7659 <script>
7660 var x = 1;ˇ
7661 </script>
7662 </body>ˇ
7663 "#
7664 .unindent(),
7665 );
7666
7667 // Precondition: different languages are active at different locations.
7668 cx.update_editor(|editor, window, cx| {
7669 let snapshot = editor.snapshot(window, cx);
7670 let cursors = editor.selections.ranges::<usize>(cx);
7671 let languages = cursors
7672 .iter()
7673 .map(|c| snapshot.language_at(c.start).unwrap().name())
7674 .collect::<Vec<_>>();
7675 assert_eq!(
7676 languages,
7677 &["HTML".into(), "JavaScript".into(), "HTML".into()]
7678 );
7679 });
7680
7681 // Angle brackets autoclose in HTML, but not JavaScript.
7682 cx.update_editor(|editor, window, cx| {
7683 editor.handle_input("<", window, cx);
7684 editor.handle_input("a", window, cx);
7685 });
7686 cx.assert_editor_state(
7687 &r#"
7688 <body><aˇ>
7689 <script>
7690 var x = 1;<aˇ
7691 </script>
7692 </body><aˇ>
7693 "#
7694 .unindent(),
7695 );
7696
7697 // Curly braces and parens autoclose in both HTML and JavaScript.
7698 cx.update_editor(|editor, window, cx| {
7699 editor.handle_input(" b=", window, cx);
7700 editor.handle_input("{", window, cx);
7701 editor.handle_input("c", window, cx);
7702 editor.handle_input("(", window, cx);
7703 });
7704 cx.assert_editor_state(
7705 &r#"
7706 <body><a b={c(ˇ)}>
7707 <script>
7708 var x = 1;<a b={c(ˇ)}
7709 </script>
7710 </body><a b={c(ˇ)}>
7711 "#
7712 .unindent(),
7713 );
7714
7715 // Brackets that were already autoclosed are skipped.
7716 cx.update_editor(|editor, window, cx| {
7717 editor.handle_input(")", window, cx);
7718 editor.handle_input("d", window, cx);
7719 editor.handle_input("}", window, cx);
7720 });
7721 cx.assert_editor_state(
7722 &r#"
7723 <body><a b={c()d}ˇ>
7724 <script>
7725 var x = 1;<a b={c()d}ˇ
7726 </script>
7727 </body><a b={c()d}ˇ>
7728 "#
7729 .unindent(),
7730 );
7731 cx.update_editor(|editor, window, cx| {
7732 editor.handle_input(">", window, cx);
7733 });
7734 cx.assert_editor_state(
7735 &r#"
7736 <body><a b={c()d}>ˇ
7737 <script>
7738 var x = 1;<a b={c()d}>ˇ
7739 </script>
7740 </body><a b={c()d}>ˇ
7741 "#
7742 .unindent(),
7743 );
7744
7745 // Reset
7746 cx.set_state(
7747 &r#"
7748 <body>ˇ
7749 <script>
7750 var x = 1;ˇ
7751 </script>
7752 </body>ˇ
7753 "#
7754 .unindent(),
7755 );
7756
7757 cx.update_editor(|editor, window, cx| {
7758 editor.handle_input("<", window, cx);
7759 });
7760 cx.assert_editor_state(
7761 &r#"
7762 <body><ˇ>
7763 <script>
7764 var x = 1;<ˇ
7765 </script>
7766 </body><ˇ>
7767 "#
7768 .unindent(),
7769 );
7770
7771 // When backspacing, the closing angle brackets are removed.
7772 cx.update_editor(|editor, window, cx| {
7773 editor.backspace(&Backspace, window, cx);
7774 });
7775 cx.assert_editor_state(
7776 &r#"
7777 <body>ˇ
7778 <script>
7779 var x = 1;ˇ
7780 </script>
7781 </body>ˇ
7782 "#
7783 .unindent(),
7784 );
7785
7786 // Block comments autoclose in JavaScript, but not HTML.
7787 cx.update_editor(|editor, window, cx| {
7788 editor.handle_input("/", window, cx);
7789 editor.handle_input("*", window, cx);
7790 });
7791 cx.assert_editor_state(
7792 &r#"
7793 <body>/*ˇ
7794 <script>
7795 var x = 1;/*ˇ */
7796 </script>
7797 </body>/*ˇ
7798 "#
7799 .unindent(),
7800 );
7801}
7802
7803#[gpui::test]
7804async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
7805 init_test(cx, |_| {});
7806
7807 let mut cx = EditorTestContext::new(cx).await;
7808
7809 let rust_language = Arc::new(
7810 Language::new(
7811 LanguageConfig {
7812 name: "Rust".into(),
7813 brackets: serde_json::from_value(json!([
7814 { "start": "{", "end": "}", "close": true, "newline": true },
7815 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
7816 ]))
7817 .unwrap(),
7818 autoclose_before: "})]>".into(),
7819 ..Default::default()
7820 },
7821 Some(tree_sitter_rust::LANGUAGE.into()),
7822 )
7823 .with_override_query("(string_literal) @string")
7824 .unwrap(),
7825 );
7826
7827 cx.language_registry().add(rust_language.clone());
7828 cx.update_buffer(|buffer, cx| {
7829 buffer.set_language(Some(rust_language), cx);
7830 });
7831
7832 cx.set_state(
7833 &r#"
7834 let x = ˇ
7835 "#
7836 .unindent(),
7837 );
7838
7839 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
7840 cx.update_editor(|editor, window, cx| {
7841 editor.handle_input("\"", window, cx);
7842 });
7843 cx.assert_editor_state(
7844 &r#"
7845 let x = "ˇ"
7846 "#
7847 .unindent(),
7848 );
7849
7850 // Inserting another quotation mark. The cursor moves across the existing
7851 // automatically-inserted quotation mark.
7852 cx.update_editor(|editor, window, cx| {
7853 editor.handle_input("\"", window, cx);
7854 });
7855 cx.assert_editor_state(
7856 &r#"
7857 let x = ""ˇ
7858 "#
7859 .unindent(),
7860 );
7861
7862 // Reset
7863 cx.set_state(
7864 &r#"
7865 let x = ˇ
7866 "#
7867 .unindent(),
7868 );
7869
7870 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
7871 cx.update_editor(|editor, window, cx| {
7872 editor.handle_input("\"", window, cx);
7873 editor.handle_input(" ", window, cx);
7874 editor.move_left(&Default::default(), window, cx);
7875 editor.handle_input("\\", window, cx);
7876 editor.handle_input("\"", window, cx);
7877 });
7878 cx.assert_editor_state(
7879 &r#"
7880 let x = "\"ˇ "
7881 "#
7882 .unindent(),
7883 );
7884
7885 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
7886 // mark. Nothing is inserted.
7887 cx.update_editor(|editor, window, cx| {
7888 editor.move_right(&Default::default(), window, cx);
7889 editor.handle_input("\"", window, cx);
7890 });
7891 cx.assert_editor_state(
7892 &r#"
7893 let x = "\" "ˇ
7894 "#
7895 .unindent(),
7896 );
7897}
7898
7899#[gpui::test]
7900async fn test_surround_with_pair(cx: &mut TestAppContext) {
7901 init_test(cx, |_| {});
7902
7903 let language = Arc::new(Language::new(
7904 LanguageConfig {
7905 brackets: BracketPairConfig {
7906 pairs: vec![
7907 BracketPair {
7908 start: "{".to_string(),
7909 end: "}".to_string(),
7910 close: true,
7911 surround: true,
7912 newline: true,
7913 },
7914 BracketPair {
7915 start: "/* ".to_string(),
7916 end: "*/".to_string(),
7917 close: true,
7918 surround: true,
7919 ..Default::default()
7920 },
7921 ],
7922 ..Default::default()
7923 },
7924 ..Default::default()
7925 },
7926 Some(tree_sitter_rust::LANGUAGE.into()),
7927 ));
7928
7929 let text = r#"
7930 a
7931 b
7932 c
7933 "#
7934 .unindent();
7935
7936 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7937 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7938 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7939 editor
7940 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7941 .await;
7942
7943 editor.update_in(cx, |editor, window, cx| {
7944 editor.change_selections(None, window, cx, |s| {
7945 s.select_display_ranges([
7946 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7947 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7948 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
7949 ])
7950 });
7951
7952 editor.handle_input("{", window, cx);
7953 editor.handle_input("{", window, cx);
7954 editor.handle_input("{", window, cx);
7955 assert_eq!(
7956 editor.text(cx),
7957 "
7958 {{{a}}}
7959 {{{b}}}
7960 {{{c}}}
7961 "
7962 .unindent()
7963 );
7964 assert_eq!(
7965 editor.selections.display_ranges(cx),
7966 [
7967 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
7968 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
7969 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
7970 ]
7971 );
7972
7973 editor.undo(&Undo, window, cx);
7974 editor.undo(&Undo, window, cx);
7975 editor.undo(&Undo, window, cx);
7976 assert_eq!(
7977 editor.text(cx),
7978 "
7979 a
7980 b
7981 c
7982 "
7983 .unindent()
7984 );
7985 assert_eq!(
7986 editor.selections.display_ranges(cx),
7987 [
7988 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7989 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7990 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
7991 ]
7992 );
7993
7994 // Ensure inserting the first character of a multi-byte bracket pair
7995 // doesn't surround the selections with the bracket.
7996 editor.handle_input("/", window, cx);
7997 assert_eq!(
7998 editor.text(cx),
7999 "
8000 /
8001 /
8002 /
8003 "
8004 .unindent()
8005 );
8006 assert_eq!(
8007 editor.selections.display_ranges(cx),
8008 [
8009 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
8010 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
8011 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
8012 ]
8013 );
8014
8015 editor.undo(&Undo, window, cx);
8016 assert_eq!(
8017 editor.text(cx),
8018 "
8019 a
8020 b
8021 c
8022 "
8023 .unindent()
8024 );
8025 assert_eq!(
8026 editor.selections.display_ranges(cx),
8027 [
8028 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8029 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8030 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
8031 ]
8032 );
8033
8034 // Ensure inserting the last character of a multi-byte bracket pair
8035 // doesn't surround the selections with the bracket.
8036 editor.handle_input("*", window, cx);
8037 assert_eq!(
8038 editor.text(cx),
8039 "
8040 *
8041 *
8042 *
8043 "
8044 .unindent()
8045 );
8046 assert_eq!(
8047 editor.selections.display_ranges(cx),
8048 [
8049 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
8050 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
8051 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
8052 ]
8053 );
8054 });
8055}
8056
8057#[gpui::test]
8058async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
8059 init_test(cx, |_| {});
8060
8061 let language = Arc::new(Language::new(
8062 LanguageConfig {
8063 brackets: BracketPairConfig {
8064 pairs: vec![BracketPair {
8065 start: "{".to_string(),
8066 end: "}".to_string(),
8067 close: true,
8068 surround: true,
8069 newline: true,
8070 }],
8071 ..Default::default()
8072 },
8073 autoclose_before: "}".to_string(),
8074 ..Default::default()
8075 },
8076 Some(tree_sitter_rust::LANGUAGE.into()),
8077 ));
8078
8079 let text = r#"
8080 a
8081 b
8082 c
8083 "#
8084 .unindent();
8085
8086 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8087 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8088 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8089 editor
8090 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8091 .await;
8092
8093 editor.update_in(cx, |editor, window, cx| {
8094 editor.change_selections(None, window, cx, |s| {
8095 s.select_ranges([
8096 Point::new(0, 1)..Point::new(0, 1),
8097 Point::new(1, 1)..Point::new(1, 1),
8098 Point::new(2, 1)..Point::new(2, 1),
8099 ])
8100 });
8101
8102 editor.handle_input("{", window, cx);
8103 editor.handle_input("{", window, cx);
8104 editor.handle_input("_", window, cx);
8105 assert_eq!(
8106 editor.text(cx),
8107 "
8108 a{{_}}
8109 b{{_}}
8110 c{{_}}
8111 "
8112 .unindent()
8113 );
8114 assert_eq!(
8115 editor.selections.ranges::<Point>(cx),
8116 [
8117 Point::new(0, 4)..Point::new(0, 4),
8118 Point::new(1, 4)..Point::new(1, 4),
8119 Point::new(2, 4)..Point::new(2, 4)
8120 ]
8121 );
8122
8123 editor.backspace(&Default::default(), window, cx);
8124 editor.backspace(&Default::default(), window, cx);
8125 assert_eq!(
8126 editor.text(cx),
8127 "
8128 a{}
8129 b{}
8130 c{}
8131 "
8132 .unindent()
8133 );
8134 assert_eq!(
8135 editor.selections.ranges::<Point>(cx),
8136 [
8137 Point::new(0, 2)..Point::new(0, 2),
8138 Point::new(1, 2)..Point::new(1, 2),
8139 Point::new(2, 2)..Point::new(2, 2)
8140 ]
8141 );
8142
8143 editor.delete_to_previous_word_start(&Default::default(), window, cx);
8144 assert_eq!(
8145 editor.text(cx),
8146 "
8147 a
8148 b
8149 c
8150 "
8151 .unindent()
8152 );
8153 assert_eq!(
8154 editor.selections.ranges::<Point>(cx),
8155 [
8156 Point::new(0, 1)..Point::new(0, 1),
8157 Point::new(1, 1)..Point::new(1, 1),
8158 Point::new(2, 1)..Point::new(2, 1)
8159 ]
8160 );
8161 });
8162}
8163
8164#[gpui::test]
8165async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
8166 init_test(cx, |settings| {
8167 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
8168 });
8169
8170 let mut cx = EditorTestContext::new(cx).await;
8171
8172 let language = Arc::new(Language::new(
8173 LanguageConfig {
8174 brackets: BracketPairConfig {
8175 pairs: vec![
8176 BracketPair {
8177 start: "{".to_string(),
8178 end: "}".to_string(),
8179 close: true,
8180 surround: true,
8181 newline: true,
8182 },
8183 BracketPair {
8184 start: "(".to_string(),
8185 end: ")".to_string(),
8186 close: true,
8187 surround: true,
8188 newline: true,
8189 },
8190 BracketPair {
8191 start: "[".to_string(),
8192 end: "]".to_string(),
8193 close: false,
8194 surround: true,
8195 newline: true,
8196 },
8197 ],
8198 ..Default::default()
8199 },
8200 autoclose_before: "})]".to_string(),
8201 ..Default::default()
8202 },
8203 Some(tree_sitter_rust::LANGUAGE.into()),
8204 ));
8205
8206 cx.language_registry().add(language.clone());
8207 cx.update_buffer(|buffer, cx| {
8208 buffer.set_language(Some(language), cx);
8209 });
8210
8211 cx.set_state(
8212 &"
8213 {(ˇ)}
8214 [[ˇ]]
8215 {(ˇ)}
8216 "
8217 .unindent(),
8218 );
8219
8220 cx.update_editor(|editor, window, cx| {
8221 editor.backspace(&Default::default(), window, cx);
8222 editor.backspace(&Default::default(), window, cx);
8223 });
8224
8225 cx.assert_editor_state(
8226 &"
8227 ˇ
8228 ˇ]]
8229 ˇ
8230 "
8231 .unindent(),
8232 );
8233
8234 cx.update_editor(|editor, window, cx| {
8235 editor.handle_input("{", window, cx);
8236 editor.handle_input("{", window, cx);
8237 editor.move_right(&MoveRight, window, cx);
8238 editor.move_right(&MoveRight, window, cx);
8239 editor.move_left(&MoveLeft, window, cx);
8240 editor.move_left(&MoveLeft, window, cx);
8241 editor.backspace(&Default::default(), window, cx);
8242 });
8243
8244 cx.assert_editor_state(
8245 &"
8246 {ˇ}
8247 {ˇ}]]
8248 {ˇ}
8249 "
8250 .unindent(),
8251 );
8252
8253 cx.update_editor(|editor, window, cx| {
8254 editor.backspace(&Default::default(), window, cx);
8255 });
8256
8257 cx.assert_editor_state(
8258 &"
8259 ˇ
8260 ˇ]]
8261 ˇ
8262 "
8263 .unindent(),
8264 );
8265}
8266
8267#[gpui::test]
8268async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
8269 init_test(cx, |_| {});
8270
8271 let language = Arc::new(Language::new(
8272 LanguageConfig::default(),
8273 Some(tree_sitter_rust::LANGUAGE.into()),
8274 ));
8275
8276 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
8277 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8278 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8279 editor
8280 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8281 .await;
8282
8283 editor.update_in(cx, |editor, window, cx| {
8284 editor.set_auto_replace_emoji_shortcode(true);
8285
8286 editor.handle_input("Hello ", window, cx);
8287 editor.handle_input(":wave", window, cx);
8288 assert_eq!(editor.text(cx), "Hello :wave".unindent());
8289
8290 editor.handle_input(":", window, cx);
8291 assert_eq!(editor.text(cx), "Hello 👋".unindent());
8292
8293 editor.handle_input(" :smile", window, cx);
8294 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
8295
8296 editor.handle_input(":", window, cx);
8297 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
8298
8299 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
8300 editor.handle_input(":wave", window, cx);
8301 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
8302
8303 editor.handle_input(":", window, cx);
8304 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
8305
8306 editor.handle_input(":1", window, cx);
8307 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
8308
8309 editor.handle_input(":", window, cx);
8310 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
8311
8312 // Ensure shortcode does not get replaced when it is part of a word
8313 editor.handle_input(" Test:wave", window, cx);
8314 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
8315
8316 editor.handle_input(":", window, cx);
8317 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
8318
8319 editor.set_auto_replace_emoji_shortcode(false);
8320
8321 // Ensure shortcode does not get replaced when auto replace is off
8322 editor.handle_input(" :wave", window, cx);
8323 assert_eq!(
8324 editor.text(cx),
8325 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
8326 );
8327
8328 editor.handle_input(":", window, cx);
8329 assert_eq!(
8330 editor.text(cx),
8331 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
8332 );
8333 });
8334}
8335
8336#[gpui::test]
8337async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
8338 init_test(cx, |_| {});
8339
8340 let (text, insertion_ranges) = marked_text_ranges(
8341 indoc! {"
8342 ˇ
8343 "},
8344 false,
8345 );
8346
8347 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
8348 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8349
8350 _ = editor.update_in(cx, |editor, window, cx| {
8351 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
8352
8353 editor
8354 .insert_snippet(&insertion_ranges, snippet, window, cx)
8355 .unwrap();
8356
8357 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
8358 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
8359 assert_eq!(editor.text(cx), expected_text);
8360 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
8361 }
8362
8363 assert(
8364 editor,
8365 cx,
8366 indoc! {"
8367 type «» =•
8368 "},
8369 );
8370
8371 assert!(editor.context_menu_visible(), "There should be a matches");
8372 });
8373}
8374
8375#[gpui::test]
8376async fn test_snippets(cx: &mut TestAppContext) {
8377 init_test(cx, |_| {});
8378
8379 let (text, insertion_ranges) = marked_text_ranges(
8380 indoc! {"
8381 a.ˇ b
8382 a.ˇ b
8383 a.ˇ b
8384 "},
8385 false,
8386 );
8387
8388 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
8389 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8390
8391 editor.update_in(cx, |editor, window, cx| {
8392 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
8393
8394 editor
8395 .insert_snippet(&insertion_ranges, snippet, window, cx)
8396 .unwrap();
8397
8398 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
8399 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
8400 assert_eq!(editor.text(cx), expected_text);
8401 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
8402 }
8403
8404 assert(
8405 editor,
8406 cx,
8407 indoc! {"
8408 a.f(«one», two, «three») b
8409 a.f(«one», two, «three») b
8410 a.f(«one», two, «three») b
8411 "},
8412 );
8413
8414 // Can't move earlier than the first tab stop
8415 assert!(!editor.move_to_prev_snippet_tabstop(window, cx));
8416 assert(
8417 editor,
8418 cx,
8419 indoc! {"
8420 a.f(«one», two, «three») b
8421 a.f(«one», two, «three») b
8422 a.f(«one», two, «three») b
8423 "},
8424 );
8425
8426 assert!(editor.move_to_next_snippet_tabstop(window, cx));
8427 assert(
8428 editor,
8429 cx,
8430 indoc! {"
8431 a.f(one, «two», three) b
8432 a.f(one, «two», three) b
8433 a.f(one, «two», three) b
8434 "},
8435 );
8436
8437 editor.move_to_prev_snippet_tabstop(window, cx);
8438 assert(
8439 editor,
8440 cx,
8441 indoc! {"
8442 a.f(«one», two, «three») b
8443 a.f(«one», two, «three») b
8444 a.f(«one», two, «three») b
8445 "},
8446 );
8447
8448 assert!(editor.move_to_next_snippet_tabstop(window, cx));
8449 assert(
8450 editor,
8451 cx,
8452 indoc! {"
8453 a.f(one, «two», three) b
8454 a.f(one, «two», three) b
8455 a.f(one, «two», three) b
8456 "},
8457 );
8458 assert!(editor.move_to_next_snippet_tabstop(window, cx));
8459 assert(
8460 editor,
8461 cx,
8462 indoc! {"
8463 a.f(one, two, three)ˇ b
8464 a.f(one, two, three)ˇ b
8465 a.f(one, two, three)ˇ b
8466 "},
8467 );
8468
8469 // As soon as the last tab stop is reached, snippet state is gone
8470 editor.move_to_prev_snippet_tabstop(window, cx);
8471 assert(
8472 editor,
8473 cx,
8474 indoc! {"
8475 a.f(one, two, three)ˇ b
8476 a.f(one, two, three)ˇ b
8477 a.f(one, two, three)ˇ b
8478 "},
8479 );
8480 });
8481}
8482
8483#[gpui::test]
8484async fn test_document_format_during_save(cx: &mut TestAppContext) {
8485 init_test(cx, |_| {});
8486
8487 let fs = FakeFs::new(cx.executor());
8488 fs.insert_file(path!("/file.rs"), Default::default()).await;
8489
8490 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
8491
8492 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8493 language_registry.add(rust_lang());
8494 let mut fake_servers = language_registry.register_fake_lsp(
8495 "Rust",
8496 FakeLspAdapter {
8497 capabilities: lsp::ServerCapabilities {
8498 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8499 ..Default::default()
8500 },
8501 ..Default::default()
8502 },
8503 );
8504
8505 let buffer = project
8506 .update(cx, |project, cx| {
8507 project.open_local_buffer(path!("/file.rs"), cx)
8508 })
8509 .await
8510 .unwrap();
8511
8512 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8513 let (editor, cx) = cx.add_window_view(|window, cx| {
8514 build_editor_with_project(project.clone(), buffer, window, cx)
8515 });
8516 editor.update_in(cx, |editor, window, cx| {
8517 editor.set_text("one\ntwo\nthree\n", window, cx)
8518 });
8519 assert!(cx.read(|cx| editor.is_dirty(cx)));
8520
8521 cx.executor().start_waiting();
8522 let fake_server = fake_servers.next().await.unwrap();
8523
8524 {
8525 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8526 move |params, _| async move {
8527 assert_eq!(
8528 params.text_document.uri,
8529 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8530 );
8531 assert_eq!(params.options.tab_size, 4);
8532 Ok(Some(vec![lsp::TextEdit::new(
8533 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8534 ", ".to_string(),
8535 )]))
8536 },
8537 );
8538 let save = editor
8539 .update_in(cx, |editor, window, cx| {
8540 editor.save(true, project.clone(), window, cx)
8541 })
8542 .unwrap();
8543 cx.executor().start_waiting();
8544 save.await;
8545
8546 assert_eq!(
8547 editor.update(cx, |editor, cx| editor.text(cx)),
8548 "one, two\nthree\n"
8549 );
8550 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8551 }
8552
8553 {
8554 editor.update_in(cx, |editor, window, cx| {
8555 editor.set_text("one\ntwo\nthree\n", window, cx)
8556 });
8557 assert!(cx.read(|cx| editor.is_dirty(cx)));
8558
8559 // Ensure we can still save even if formatting hangs.
8560 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8561 move |params, _| async move {
8562 assert_eq!(
8563 params.text_document.uri,
8564 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8565 );
8566 futures::future::pending::<()>().await;
8567 unreachable!()
8568 },
8569 );
8570 let save = editor
8571 .update_in(cx, |editor, window, cx| {
8572 editor.save(true, project.clone(), window, cx)
8573 })
8574 .unwrap();
8575 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8576 cx.executor().start_waiting();
8577 save.await;
8578 assert_eq!(
8579 editor.update(cx, |editor, cx| editor.text(cx)),
8580 "one\ntwo\nthree\n"
8581 );
8582 }
8583
8584 // For non-dirty buffer, no formatting request should be sent
8585 {
8586 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8587
8588 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
8589 panic!("Should not be invoked on non-dirty buffer");
8590 });
8591 let save = editor
8592 .update_in(cx, |editor, window, cx| {
8593 editor.save(true, project.clone(), window, cx)
8594 })
8595 .unwrap();
8596 cx.executor().start_waiting();
8597 save.await;
8598 }
8599
8600 // Set rust language override and assert overridden tabsize is sent to language server
8601 update_test_language_settings(cx, |settings| {
8602 settings.languages.insert(
8603 "Rust".into(),
8604 LanguageSettingsContent {
8605 tab_size: NonZeroU32::new(8),
8606 ..Default::default()
8607 },
8608 );
8609 });
8610
8611 {
8612 editor.update_in(cx, |editor, window, cx| {
8613 editor.set_text("somehting_new\n", window, cx)
8614 });
8615 assert!(cx.read(|cx| editor.is_dirty(cx)));
8616 let _formatting_request_signal = fake_server
8617 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
8618 assert_eq!(
8619 params.text_document.uri,
8620 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8621 );
8622 assert_eq!(params.options.tab_size, 8);
8623 Ok(Some(vec![]))
8624 });
8625 let save = editor
8626 .update_in(cx, |editor, window, cx| {
8627 editor.save(true, project.clone(), window, cx)
8628 })
8629 .unwrap();
8630 cx.executor().start_waiting();
8631 save.await;
8632 }
8633}
8634
8635#[gpui::test]
8636async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
8637 init_test(cx, |_| {});
8638
8639 let cols = 4;
8640 let rows = 10;
8641 let sample_text_1 = sample_text(rows, cols, 'a');
8642 assert_eq!(
8643 sample_text_1,
8644 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
8645 );
8646 let sample_text_2 = sample_text(rows, cols, 'l');
8647 assert_eq!(
8648 sample_text_2,
8649 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
8650 );
8651 let sample_text_3 = sample_text(rows, cols, 'v');
8652 assert_eq!(
8653 sample_text_3,
8654 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
8655 );
8656
8657 let fs = FakeFs::new(cx.executor());
8658 fs.insert_tree(
8659 path!("/a"),
8660 json!({
8661 "main.rs": sample_text_1,
8662 "other.rs": sample_text_2,
8663 "lib.rs": sample_text_3,
8664 }),
8665 )
8666 .await;
8667
8668 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
8669 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
8670 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
8671
8672 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8673 language_registry.add(rust_lang());
8674 let mut fake_servers = language_registry.register_fake_lsp(
8675 "Rust",
8676 FakeLspAdapter {
8677 capabilities: lsp::ServerCapabilities {
8678 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8679 ..Default::default()
8680 },
8681 ..Default::default()
8682 },
8683 );
8684
8685 let worktree = project.update(cx, |project, cx| {
8686 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
8687 assert_eq!(worktrees.len(), 1);
8688 worktrees.pop().unwrap()
8689 });
8690 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
8691
8692 let buffer_1 = project
8693 .update(cx, |project, cx| {
8694 project.open_buffer((worktree_id, "main.rs"), cx)
8695 })
8696 .await
8697 .unwrap();
8698 let buffer_2 = project
8699 .update(cx, |project, cx| {
8700 project.open_buffer((worktree_id, "other.rs"), cx)
8701 })
8702 .await
8703 .unwrap();
8704 let buffer_3 = project
8705 .update(cx, |project, cx| {
8706 project.open_buffer((worktree_id, "lib.rs"), cx)
8707 })
8708 .await
8709 .unwrap();
8710
8711 let multi_buffer = cx.new(|cx| {
8712 let mut multi_buffer = MultiBuffer::new(ReadWrite);
8713 multi_buffer.push_excerpts(
8714 buffer_1.clone(),
8715 [
8716 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8717 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8718 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8719 ],
8720 cx,
8721 );
8722 multi_buffer.push_excerpts(
8723 buffer_2.clone(),
8724 [
8725 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8726 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8727 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8728 ],
8729 cx,
8730 );
8731 multi_buffer.push_excerpts(
8732 buffer_3.clone(),
8733 [
8734 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8735 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8736 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8737 ],
8738 cx,
8739 );
8740 multi_buffer
8741 });
8742 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
8743 Editor::new(
8744 EditorMode::full(),
8745 multi_buffer,
8746 Some(project.clone()),
8747 window,
8748 cx,
8749 )
8750 });
8751
8752 multi_buffer_editor.update_in(cx, |editor, window, cx| {
8753 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
8754 s.select_ranges(Some(1..2))
8755 });
8756 editor.insert("|one|two|three|", window, cx);
8757 });
8758 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
8759 multi_buffer_editor.update_in(cx, |editor, window, cx| {
8760 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
8761 s.select_ranges(Some(60..70))
8762 });
8763 editor.insert("|four|five|six|", window, cx);
8764 });
8765 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
8766
8767 // First two buffers should be edited, but not the third one.
8768 assert_eq!(
8769 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
8770 "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}",
8771 );
8772 buffer_1.update(cx, |buffer, _| {
8773 assert!(buffer.is_dirty());
8774 assert_eq!(
8775 buffer.text(),
8776 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
8777 )
8778 });
8779 buffer_2.update(cx, |buffer, _| {
8780 assert!(buffer.is_dirty());
8781 assert_eq!(
8782 buffer.text(),
8783 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
8784 )
8785 });
8786 buffer_3.update(cx, |buffer, _| {
8787 assert!(!buffer.is_dirty());
8788 assert_eq!(buffer.text(), sample_text_3,)
8789 });
8790 cx.executor().run_until_parked();
8791
8792 cx.executor().start_waiting();
8793 let save = multi_buffer_editor
8794 .update_in(cx, |editor, window, cx| {
8795 editor.save(true, project.clone(), window, cx)
8796 })
8797 .unwrap();
8798
8799 let fake_server = fake_servers.next().await.unwrap();
8800 fake_server
8801 .server
8802 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
8803 Ok(Some(vec![lsp::TextEdit::new(
8804 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8805 format!("[{} formatted]", params.text_document.uri),
8806 )]))
8807 })
8808 .detach();
8809 save.await;
8810
8811 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
8812 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
8813 assert_eq!(
8814 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
8815 uri!(
8816 "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}"
8817 ),
8818 );
8819 buffer_1.update(cx, |buffer, _| {
8820 assert!(!buffer.is_dirty());
8821 assert_eq!(
8822 buffer.text(),
8823 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
8824 )
8825 });
8826 buffer_2.update(cx, |buffer, _| {
8827 assert!(!buffer.is_dirty());
8828 assert_eq!(
8829 buffer.text(),
8830 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
8831 )
8832 });
8833 buffer_3.update(cx, |buffer, _| {
8834 assert!(!buffer.is_dirty());
8835 assert_eq!(buffer.text(), sample_text_3,)
8836 });
8837}
8838
8839#[gpui::test]
8840async fn test_range_format_during_save(cx: &mut TestAppContext) {
8841 init_test(cx, |_| {});
8842
8843 let fs = FakeFs::new(cx.executor());
8844 fs.insert_file(path!("/file.rs"), Default::default()).await;
8845
8846 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8847
8848 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8849 language_registry.add(rust_lang());
8850 let mut fake_servers = language_registry.register_fake_lsp(
8851 "Rust",
8852 FakeLspAdapter {
8853 capabilities: lsp::ServerCapabilities {
8854 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
8855 ..Default::default()
8856 },
8857 ..Default::default()
8858 },
8859 );
8860
8861 let buffer = project
8862 .update(cx, |project, cx| {
8863 project.open_local_buffer(path!("/file.rs"), cx)
8864 })
8865 .await
8866 .unwrap();
8867
8868 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8869 let (editor, cx) = cx.add_window_view(|window, cx| {
8870 build_editor_with_project(project.clone(), buffer, window, cx)
8871 });
8872 editor.update_in(cx, |editor, window, cx| {
8873 editor.set_text("one\ntwo\nthree\n", window, cx)
8874 });
8875 assert!(cx.read(|cx| editor.is_dirty(cx)));
8876
8877 cx.executor().start_waiting();
8878 let fake_server = fake_servers.next().await.unwrap();
8879
8880 let save = editor
8881 .update_in(cx, |editor, window, cx| {
8882 editor.save(true, project.clone(), window, cx)
8883 })
8884 .unwrap();
8885 fake_server
8886 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
8887 assert_eq!(
8888 params.text_document.uri,
8889 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8890 );
8891 assert_eq!(params.options.tab_size, 4);
8892 Ok(Some(vec![lsp::TextEdit::new(
8893 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8894 ", ".to_string(),
8895 )]))
8896 })
8897 .next()
8898 .await;
8899 cx.executor().start_waiting();
8900 save.await;
8901 assert_eq!(
8902 editor.update(cx, |editor, cx| editor.text(cx)),
8903 "one, two\nthree\n"
8904 );
8905 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8906
8907 editor.update_in(cx, |editor, window, cx| {
8908 editor.set_text("one\ntwo\nthree\n", window, cx)
8909 });
8910 assert!(cx.read(|cx| editor.is_dirty(cx)));
8911
8912 // Ensure we can still save even if formatting hangs.
8913 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
8914 move |params, _| async move {
8915 assert_eq!(
8916 params.text_document.uri,
8917 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8918 );
8919 futures::future::pending::<()>().await;
8920 unreachable!()
8921 },
8922 );
8923 let save = editor
8924 .update_in(cx, |editor, window, cx| {
8925 editor.save(true, project.clone(), window, cx)
8926 })
8927 .unwrap();
8928 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8929 cx.executor().start_waiting();
8930 save.await;
8931 assert_eq!(
8932 editor.update(cx, |editor, cx| editor.text(cx)),
8933 "one\ntwo\nthree\n"
8934 );
8935 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8936
8937 // For non-dirty buffer, no formatting request should be sent
8938 let save = editor
8939 .update_in(cx, |editor, window, cx| {
8940 editor.save(true, project.clone(), window, cx)
8941 })
8942 .unwrap();
8943 let _pending_format_request = fake_server
8944 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
8945 panic!("Should not be invoked on non-dirty buffer");
8946 })
8947 .next();
8948 cx.executor().start_waiting();
8949 save.await;
8950
8951 // Set Rust language override and assert overridden tabsize is sent to language server
8952 update_test_language_settings(cx, |settings| {
8953 settings.languages.insert(
8954 "Rust".into(),
8955 LanguageSettingsContent {
8956 tab_size: NonZeroU32::new(8),
8957 ..Default::default()
8958 },
8959 );
8960 });
8961
8962 editor.update_in(cx, |editor, window, cx| {
8963 editor.set_text("somehting_new\n", window, cx)
8964 });
8965 assert!(cx.read(|cx| editor.is_dirty(cx)));
8966 let save = editor
8967 .update_in(cx, |editor, window, cx| {
8968 editor.save(true, project.clone(), window, cx)
8969 })
8970 .unwrap();
8971 fake_server
8972 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
8973 assert_eq!(
8974 params.text_document.uri,
8975 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8976 );
8977 assert_eq!(params.options.tab_size, 8);
8978 Ok(Some(vec![]))
8979 })
8980 .next()
8981 .await;
8982 cx.executor().start_waiting();
8983 save.await;
8984}
8985
8986#[gpui::test]
8987async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
8988 init_test(cx, |settings| {
8989 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
8990 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
8991 ))
8992 });
8993
8994 let fs = FakeFs::new(cx.executor());
8995 fs.insert_file(path!("/file.rs"), Default::default()).await;
8996
8997 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8998
8999 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9000 language_registry.add(Arc::new(Language::new(
9001 LanguageConfig {
9002 name: "Rust".into(),
9003 matcher: LanguageMatcher {
9004 path_suffixes: vec!["rs".to_string()],
9005 ..Default::default()
9006 },
9007 ..LanguageConfig::default()
9008 },
9009 Some(tree_sitter_rust::LANGUAGE.into()),
9010 )));
9011 update_test_language_settings(cx, |settings| {
9012 // Enable Prettier formatting for the same buffer, and ensure
9013 // LSP is called instead of Prettier.
9014 settings.defaults.prettier = Some(PrettierSettings {
9015 allowed: true,
9016 ..PrettierSettings::default()
9017 });
9018 });
9019 let mut fake_servers = language_registry.register_fake_lsp(
9020 "Rust",
9021 FakeLspAdapter {
9022 capabilities: lsp::ServerCapabilities {
9023 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9024 ..Default::default()
9025 },
9026 ..Default::default()
9027 },
9028 );
9029
9030 let buffer = project
9031 .update(cx, |project, cx| {
9032 project.open_local_buffer(path!("/file.rs"), cx)
9033 })
9034 .await
9035 .unwrap();
9036
9037 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9038 let (editor, cx) = cx.add_window_view(|window, cx| {
9039 build_editor_with_project(project.clone(), buffer, window, cx)
9040 });
9041 editor.update_in(cx, |editor, window, cx| {
9042 editor.set_text("one\ntwo\nthree\n", window, cx)
9043 });
9044
9045 cx.executor().start_waiting();
9046 let fake_server = fake_servers.next().await.unwrap();
9047
9048 let format = editor
9049 .update_in(cx, |editor, window, cx| {
9050 editor.perform_format(
9051 project.clone(),
9052 FormatTrigger::Manual,
9053 FormatTarget::Buffers,
9054 window,
9055 cx,
9056 )
9057 })
9058 .unwrap();
9059 fake_server
9060 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
9061 assert_eq!(
9062 params.text_document.uri,
9063 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9064 );
9065 assert_eq!(params.options.tab_size, 4);
9066 Ok(Some(vec![lsp::TextEdit::new(
9067 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9068 ", ".to_string(),
9069 )]))
9070 })
9071 .next()
9072 .await;
9073 cx.executor().start_waiting();
9074 format.await;
9075 assert_eq!(
9076 editor.update(cx, |editor, cx| editor.text(cx)),
9077 "one, two\nthree\n"
9078 );
9079
9080 editor.update_in(cx, |editor, window, cx| {
9081 editor.set_text("one\ntwo\nthree\n", window, cx)
9082 });
9083 // Ensure we don't lock if formatting hangs.
9084 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9085 move |params, _| async move {
9086 assert_eq!(
9087 params.text_document.uri,
9088 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9089 );
9090 futures::future::pending::<()>().await;
9091 unreachable!()
9092 },
9093 );
9094 let format = editor
9095 .update_in(cx, |editor, window, cx| {
9096 editor.perform_format(
9097 project,
9098 FormatTrigger::Manual,
9099 FormatTarget::Buffers,
9100 window,
9101 cx,
9102 )
9103 })
9104 .unwrap();
9105 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9106 cx.executor().start_waiting();
9107 format.await;
9108 assert_eq!(
9109 editor.update(cx, |editor, cx| editor.text(cx)),
9110 "one\ntwo\nthree\n"
9111 );
9112}
9113
9114#[gpui::test]
9115async fn test_multiple_formatters(cx: &mut TestAppContext) {
9116 init_test(cx, |settings| {
9117 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
9118 settings.defaults.formatter =
9119 Some(language_settings::SelectedFormatter::List(FormatterList(
9120 vec![
9121 Formatter::LanguageServer { name: None },
9122 Formatter::CodeActions(
9123 [
9124 ("code-action-1".into(), true),
9125 ("code-action-2".into(), true),
9126 ]
9127 .into_iter()
9128 .collect(),
9129 ),
9130 ]
9131 .into(),
9132 )))
9133 });
9134
9135 let fs = FakeFs::new(cx.executor());
9136 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
9137 .await;
9138
9139 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9140 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9141 language_registry.add(rust_lang());
9142
9143 let mut fake_servers = language_registry.register_fake_lsp(
9144 "Rust",
9145 FakeLspAdapter {
9146 capabilities: lsp::ServerCapabilities {
9147 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9148 execute_command_provider: Some(lsp::ExecuteCommandOptions {
9149 commands: vec!["the-command-for-code-action-1".into()],
9150 ..Default::default()
9151 }),
9152 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
9153 ..Default::default()
9154 },
9155 ..Default::default()
9156 },
9157 );
9158
9159 let buffer = project
9160 .update(cx, |project, cx| {
9161 project.open_local_buffer(path!("/file.rs"), cx)
9162 })
9163 .await
9164 .unwrap();
9165
9166 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9167 let (editor, cx) = cx.add_window_view(|window, cx| {
9168 build_editor_with_project(project.clone(), buffer, window, cx)
9169 });
9170
9171 cx.executor().start_waiting();
9172
9173 let fake_server = fake_servers.next().await.unwrap();
9174 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9175 move |_params, _| async move {
9176 Ok(Some(vec![lsp::TextEdit::new(
9177 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9178 "applied-formatting\n".to_string(),
9179 )]))
9180 },
9181 );
9182 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
9183 move |params, _| async move {
9184 assert_eq!(
9185 params.context.only,
9186 Some(vec!["code-action-1".into(), "code-action-2".into()])
9187 );
9188 let uri = lsp::Url::from_file_path(path!("/file.rs")).unwrap();
9189 Ok(Some(vec![
9190 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
9191 kind: Some("code-action-1".into()),
9192 edit: Some(lsp::WorkspaceEdit::new(
9193 [(
9194 uri.clone(),
9195 vec![lsp::TextEdit::new(
9196 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9197 "applied-code-action-1-edit\n".to_string(),
9198 )],
9199 )]
9200 .into_iter()
9201 .collect(),
9202 )),
9203 command: Some(lsp::Command {
9204 command: "the-command-for-code-action-1".into(),
9205 ..Default::default()
9206 }),
9207 ..Default::default()
9208 }),
9209 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
9210 kind: Some("code-action-2".into()),
9211 edit: Some(lsp::WorkspaceEdit::new(
9212 [(
9213 uri.clone(),
9214 vec![lsp::TextEdit::new(
9215 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9216 "applied-code-action-2-edit\n".to_string(),
9217 )],
9218 )]
9219 .into_iter()
9220 .collect(),
9221 )),
9222 ..Default::default()
9223 }),
9224 ]))
9225 },
9226 );
9227
9228 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
9229 move |params, _| async move { Ok(params) }
9230 });
9231
9232 let command_lock = Arc::new(futures::lock::Mutex::new(()));
9233 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
9234 let fake = fake_server.clone();
9235 let lock = command_lock.clone();
9236 move |params, _| {
9237 assert_eq!(params.command, "the-command-for-code-action-1");
9238 let fake = fake.clone();
9239 let lock = lock.clone();
9240 async move {
9241 lock.lock().await;
9242 fake.server
9243 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
9244 label: None,
9245 edit: lsp::WorkspaceEdit {
9246 changes: Some(
9247 [(
9248 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
9249 vec![lsp::TextEdit {
9250 range: lsp::Range::new(
9251 lsp::Position::new(0, 0),
9252 lsp::Position::new(0, 0),
9253 ),
9254 new_text: "applied-code-action-1-command\n".into(),
9255 }],
9256 )]
9257 .into_iter()
9258 .collect(),
9259 ),
9260 ..Default::default()
9261 },
9262 })
9263 .await
9264 .into_response()
9265 .unwrap();
9266 Ok(Some(json!(null)))
9267 }
9268 }
9269 });
9270
9271 cx.executor().start_waiting();
9272 editor
9273 .update_in(cx, |editor, window, cx| {
9274 editor.perform_format(
9275 project.clone(),
9276 FormatTrigger::Manual,
9277 FormatTarget::Buffers,
9278 window,
9279 cx,
9280 )
9281 })
9282 .unwrap()
9283 .await;
9284 editor.update(cx, |editor, cx| {
9285 assert_eq!(
9286 editor.text(cx),
9287 r#"
9288 applied-code-action-2-edit
9289 applied-code-action-1-command
9290 applied-code-action-1-edit
9291 applied-formatting
9292 one
9293 two
9294 three
9295 "#
9296 .unindent()
9297 );
9298 });
9299
9300 editor.update_in(cx, |editor, window, cx| {
9301 editor.undo(&Default::default(), window, cx);
9302 assert_eq!(editor.text(cx), "one \ntwo \nthree");
9303 });
9304
9305 // Perform a manual edit while waiting for an LSP command
9306 // that's being run as part of a formatting code action.
9307 let lock_guard = command_lock.lock().await;
9308 let format = editor
9309 .update_in(cx, |editor, window, cx| {
9310 editor.perform_format(
9311 project.clone(),
9312 FormatTrigger::Manual,
9313 FormatTarget::Buffers,
9314 window,
9315 cx,
9316 )
9317 })
9318 .unwrap();
9319 cx.run_until_parked();
9320 editor.update(cx, |editor, cx| {
9321 assert_eq!(
9322 editor.text(cx),
9323 r#"
9324 applied-code-action-1-edit
9325 applied-formatting
9326 one
9327 two
9328 three
9329 "#
9330 .unindent()
9331 );
9332
9333 editor.buffer.update(cx, |buffer, cx| {
9334 let ix = buffer.len(cx);
9335 buffer.edit([(ix..ix, "edited\n")], None, cx);
9336 });
9337 });
9338
9339 // Allow the LSP command to proceed. Because the buffer was edited,
9340 // the second code action will not be run.
9341 drop(lock_guard);
9342 format.await;
9343 editor.update_in(cx, |editor, window, cx| {
9344 assert_eq!(
9345 editor.text(cx),
9346 r#"
9347 applied-code-action-1-command
9348 applied-code-action-1-edit
9349 applied-formatting
9350 one
9351 two
9352 three
9353 edited
9354 "#
9355 .unindent()
9356 );
9357
9358 // The manual edit is undone first, because it is the last thing the user did
9359 // (even though the command completed afterwards).
9360 editor.undo(&Default::default(), 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 "#
9371 .unindent()
9372 );
9373
9374 // All the formatting (including the command, which completed after the manual edit)
9375 // is undone together.
9376 editor.undo(&Default::default(), window, cx);
9377 assert_eq!(editor.text(cx), "one \ntwo \nthree");
9378 });
9379}
9380
9381#[gpui::test]
9382async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
9383 init_test(cx, |settings| {
9384 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
9385 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
9386 ))
9387 });
9388
9389 let fs = FakeFs::new(cx.executor());
9390 fs.insert_file(path!("/file.ts"), Default::default()).await;
9391
9392 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9393
9394 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9395 language_registry.add(Arc::new(Language::new(
9396 LanguageConfig {
9397 name: "TypeScript".into(),
9398 matcher: LanguageMatcher {
9399 path_suffixes: vec!["ts".to_string()],
9400 ..Default::default()
9401 },
9402 ..LanguageConfig::default()
9403 },
9404 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
9405 )));
9406 update_test_language_settings(cx, |settings| {
9407 settings.defaults.prettier = Some(PrettierSettings {
9408 allowed: true,
9409 ..PrettierSettings::default()
9410 });
9411 });
9412 let mut fake_servers = language_registry.register_fake_lsp(
9413 "TypeScript",
9414 FakeLspAdapter {
9415 capabilities: lsp::ServerCapabilities {
9416 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
9417 ..Default::default()
9418 },
9419 ..Default::default()
9420 },
9421 );
9422
9423 let buffer = project
9424 .update(cx, |project, cx| {
9425 project.open_local_buffer(path!("/file.ts"), cx)
9426 })
9427 .await
9428 .unwrap();
9429
9430 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9431 let (editor, cx) = cx.add_window_view(|window, cx| {
9432 build_editor_with_project(project.clone(), buffer, window, cx)
9433 });
9434 editor.update_in(cx, |editor, window, cx| {
9435 editor.set_text(
9436 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
9437 window,
9438 cx,
9439 )
9440 });
9441
9442 cx.executor().start_waiting();
9443 let fake_server = fake_servers.next().await.unwrap();
9444
9445 let format = editor
9446 .update_in(cx, |editor, window, cx| {
9447 editor.perform_code_action_kind(
9448 project.clone(),
9449 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
9450 window,
9451 cx,
9452 )
9453 })
9454 .unwrap();
9455 fake_server
9456 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
9457 assert_eq!(
9458 params.text_document.uri,
9459 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
9460 );
9461 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
9462 lsp::CodeAction {
9463 title: "Organize Imports".to_string(),
9464 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
9465 edit: Some(lsp::WorkspaceEdit {
9466 changes: Some(
9467 [(
9468 params.text_document.uri.clone(),
9469 vec![lsp::TextEdit::new(
9470 lsp::Range::new(
9471 lsp::Position::new(1, 0),
9472 lsp::Position::new(2, 0),
9473 ),
9474 "".to_string(),
9475 )],
9476 )]
9477 .into_iter()
9478 .collect(),
9479 ),
9480 ..Default::default()
9481 }),
9482 ..Default::default()
9483 },
9484 )]))
9485 })
9486 .next()
9487 .await;
9488 cx.executor().start_waiting();
9489 format.await;
9490 assert_eq!(
9491 editor.update(cx, |editor, cx| editor.text(cx)),
9492 "import { a } from 'module';\n\nconst x = a;\n"
9493 );
9494
9495 editor.update_in(cx, |editor, window, cx| {
9496 editor.set_text(
9497 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
9498 window,
9499 cx,
9500 )
9501 });
9502 // Ensure we don't lock if code action hangs.
9503 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
9504 move |params, _| async move {
9505 assert_eq!(
9506 params.text_document.uri,
9507 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
9508 );
9509 futures::future::pending::<()>().await;
9510 unreachable!()
9511 },
9512 );
9513 let format = editor
9514 .update_in(cx, |editor, window, cx| {
9515 editor.perform_code_action_kind(
9516 project,
9517 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
9518 window,
9519 cx,
9520 )
9521 })
9522 .unwrap();
9523 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
9524 cx.executor().start_waiting();
9525 format.await;
9526 assert_eq!(
9527 editor.update(cx, |editor, cx| editor.text(cx)),
9528 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
9529 );
9530}
9531
9532#[gpui::test]
9533async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
9534 init_test(cx, |_| {});
9535
9536 let mut cx = EditorLspTestContext::new_rust(
9537 lsp::ServerCapabilities {
9538 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9539 ..Default::default()
9540 },
9541 cx,
9542 )
9543 .await;
9544
9545 cx.set_state(indoc! {"
9546 one.twoˇ
9547 "});
9548
9549 // The format request takes a long time. When it completes, it inserts
9550 // a newline and an indent before the `.`
9551 cx.lsp
9552 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
9553 let executor = cx.background_executor().clone();
9554 async move {
9555 executor.timer(Duration::from_millis(100)).await;
9556 Ok(Some(vec![lsp::TextEdit {
9557 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
9558 new_text: "\n ".into(),
9559 }]))
9560 }
9561 });
9562
9563 // Submit a format request.
9564 let format_1 = cx
9565 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
9566 .unwrap();
9567 cx.executor().run_until_parked();
9568
9569 // Submit a second format request.
9570 let format_2 = cx
9571 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
9572 .unwrap();
9573 cx.executor().run_until_parked();
9574
9575 // Wait for both format requests to complete
9576 cx.executor().advance_clock(Duration::from_millis(200));
9577 cx.executor().start_waiting();
9578 format_1.await.unwrap();
9579 cx.executor().start_waiting();
9580 format_2.await.unwrap();
9581
9582 // The formatting edits only happens once.
9583 cx.assert_editor_state(indoc! {"
9584 one
9585 .twoˇ
9586 "});
9587}
9588
9589#[gpui::test]
9590async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
9591 init_test(cx, |settings| {
9592 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
9593 });
9594
9595 let mut cx = EditorLspTestContext::new_rust(
9596 lsp::ServerCapabilities {
9597 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9598 ..Default::default()
9599 },
9600 cx,
9601 )
9602 .await;
9603
9604 // Set up a buffer white some trailing whitespace and no trailing newline.
9605 cx.set_state(
9606 &[
9607 "one ", //
9608 "twoˇ", //
9609 "three ", //
9610 "four", //
9611 ]
9612 .join("\n"),
9613 );
9614
9615 // Submit a format request.
9616 let format = cx
9617 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
9618 .unwrap();
9619
9620 // Record which buffer changes have been sent to the language server
9621 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
9622 cx.lsp
9623 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
9624 let buffer_changes = buffer_changes.clone();
9625 move |params, _| {
9626 buffer_changes.lock().extend(
9627 params
9628 .content_changes
9629 .into_iter()
9630 .map(|e| (e.range.unwrap(), e.text)),
9631 );
9632 }
9633 });
9634
9635 // Handle formatting requests to the language server.
9636 cx.lsp
9637 .set_request_handler::<lsp::request::Formatting, _, _>({
9638 let buffer_changes = buffer_changes.clone();
9639 move |_, _| {
9640 // When formatting is requested, trailing whitespace has already been stripped,
9641 // and the trailing newline has already been added.
9642 assert_eq!(
9643 &buffer_changes.lock()[1..],
9644 &[
9645 (
9646 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
9647 "".into()
9648 ),
9649 (
9650 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
9651 "".into()
9652 ),
9653 (
9654 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
9655 "\n".into()
9656 ),
9657 ]
9658 );
9659
9660 // Insert blank lines between each line of the buffer.
9661 async move {
9662 Ok(Some(vec![
9663 lsp::TextEdit {
9664 range: lsp::Range::new(
9665 lsp::Position::new(1, 0),
9666 lsp::Position::new(1, 0),
9667 ),
9668 new_text: "\n".into(),
9669 },
9670 lsp::TextEdit {
9671 range: lsp::Range::new(
9672 lsp::Position::new(2, 0),
9673 lsp::Position::new(2, 0),
9674 ),
9675 new_text: "\n".into(),
9676 },
9677 ]))
9678 }
9679 }
9680 });
9681
9682 // After formatting the buffer, the trailing whitespace is stripped,
9683 // a newline is appended, and the edits provided by the language server
9684 // have been applied.
9685 format.await.unwrap();
9686 cx.assert_editor_state(
9687 &[
9688 "one", //
9689 "", //
9690 "twoˇ", //
9691 "", //
9692 "three", //
9693 "four", //
9694 "", //
9695 ]
9696 .join("\n"),
9697 );
9698
9699 // Undoing the formatting undoes the trailing whitespace removal, the
9700 // trailing newline, and the LSP edits.
9701 cx.update_buffer(|buffer, cx| buffer.undo(cx));
9702 cx.assert_editor_state(
9703 &[
9704 "one ", //
9705 "twoˇ", //
9706 "three ", //
9707 "four", //
9708 ]
9709 .join("\n"),
9710 );
9711}
9712
9713#[gpui::test]
9714async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
9715 cx: &mut TestAppContext,
9716) {
9717 init_test(cx, |_| {});
9718
9719 cx.update(|cx| {
9720 cx.update_global::<SettingsStore, _>(|settings, cx| {
9721 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9722 settings.auto_signature_help = Some(true);
9723 });
9724 });
9725 });
9726
9727 let mut cx = EditorLspTestContext::new_rust(
9728 lsp::ServerCapabilities {
9729 signature_help_provider: Some(lsp::SignatureHelpOptions {
9730 ..Default::default()
9731 }),
9732 ..Default::default()
9733 },
9734 cx,
9735 )
9736 .await;
9737
9738 let language = Language::new(
9739 LanguageConfig {
9740 name: "Rust".into(),
9741 brackets: BracketPairConfig {
9742 pairs: vec![
9743 BracketPair {
9744 start: "{".to_string(),
9745 end: "}".to_string(),
9746 close: true,
9747 surround: true,
9748 newline: true,
9749 },
9750 BracketPair {
9751 start: "(".to_string(),
9752 end: ")".to_string(),
9753 close: true,
9754 surround: true,
9755 newline: true,
9756 },
9757 BracketPair {
9758 start: "/*".to_string(),
9759 end: " */".to_string(),
9760 close: true,
9761 surround: true,
9762 newline: true,
9763 },
9764 BracketPair {
9765 start: "[".to_string(),
9766 end: "]".to_string(),
9767 close: false,
9768 surround: false,
9769 newline: true,
9770 },
9771 BracketPair {
9772 start: "\"".to_string(),
9773 end: "\"".to_string(),
9774 close: true,
9775 surround: true,
9776 newline: false,
9777 },
9778 BracketPair {
9779 start: "<".to_string(),
9780 end: ">".to_string(),
9781 close: false,
9782 surround: true,
9783 newline: true,
9784 },
9785 ],
9786 ..Default::default()
9787 },
9788 autoclose_before: "})]".to_string(),
9789 ..Default::default()
9790 },
9791 Some(tree_sitter_rust::LANGUAGE.into()),
9792 );
9793 let language = Arc::new(language);
9794
9795 cx.language_registry().add(language.clone());
9796 cx.update_buffer(|buffer, cx| {
9797 buffer.set_language(Some(language), cx);
9798 });
9799
9800 cx.set_state(
9801 &r#"
9802 fn main() {
9803 sampleˇ
9804 }
9805 "#
9806 .unindent(),
9807 );
9808
9809 cx.update_editor(|editor, window, cx| {
9810 editor.handle_input("(", window, cx);
9811 });
9812 cx.assert_editor_state(
9813 &"
9814 fn main() {
9815 sample(ˇ)
9816 }
9817 "
9818 .unindent(),
9819 );
9820
9821 let mocked_response = lsp::SignatureHelp {
9822 signatures: vec![lsp::SignatureInformation {
9823 label: "fn sample(param1: u8, param2: u8)".to_string(),
9824 documentation: None,
9825 parameters: Some(vec![
9826 lsp::ParameterInformation {
9827 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9828 documentation: None,
9829 },
9830 lsp::ParameterInformation {
9831 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9832 documentation: None,
9833 },
9834 ]),
9835 active_parameter: None,
9836 }],
9837 active_signature: Some(0),
9838 active_parameter: Some(0),
9839 };
9840 handle_signature_help_request(&mut cx, mocked_response).await;
9841
9842 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9843 .await;
9844
9845 cx.editor(|editor, _, _| {
9846 let signature_help_state = editor.signature_help_state.popover().cloned();
9847 assert_eq!(
9848 signature_help_state.unwrap().label,
9849 "param1: u8, param2: u8"
9850 );
9851 });
9852}
9853
9854#[gpui::test]
9855async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
9856 init_test(cx, |_| {});
9857
9858 cx.update(|cx| {
9859 cx.update_global::<SettingsStore, _>(|settings, cx| {
9860 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9861 settings.auto_signature_help = Some(false);
9862 settings.show_signature_help_after_edits = Some(false);
9863 });
9864 });
9865 });
9866
9867 let mut cx = EditorLspTestContext::new_rust(
9868 lsp::ServerCapabilities {
9869 signature_help_provider: Some(lsp::SignatureHelpOptions {
9870 ..Default::default()
9871 }),
9872 ..Default::default()
9873 },
9874 cx,
9875 )
9876 .await;
9877
9878 let language = Language::new(
9879 LanguageConfig {
9880 name: "Rust".into(),
9881 brackets: BracketPairConfig {
9882 pairs: vec![
9883 BracketPair {
9884 start: "{".to_string(),
9885 end: "}".to_string(),
9886 close: true,
9887 surround: true,
9888 newline: true,
9889 },
9890 BracketPair {
9891 start: "(".to_string(),
9892 end: ")".to_string(),
9893 close: true,
9894 surround: true,
9895 newline: true,
9896 },
9897 BracketPair {
9898 start: "/*".to_string(),
9899 end: " */".to_string(),
9900 close: true,
9901 surround: true,
9902 newline: true,
9903 },
9904 BracketPair {
9905 start: "[".to_string(),
9906 end: "]".to_string(),
9907 close: false,
9908 surround: false,
9909 newline: true,
9910 },
9911 BracketPair {
9912 start: "\"".to_string(),
9913 end: "\"".to_string(),
9914 close: true,
9915 surround: true,
9916 newline: false,
9917 },
9918 BracketPair {
9919 start: "<".to_string(),
9920 end: ">".to_string(),
9921 close: false,
9922 surround: true,
9923 newline: true,
9924 },
9925 ],
9926 ..Default::default()
9927 },
9928 autoclose_before: "})]".to_string(),
9929 ..Default::default()
9930 },
9931 Some(tree_sitter_rust::LANGUAGE.into()),
9932 );
9933 let language = Arc::new(language);
9934
9935 cx.language_registry().add(language.clone());
9936 cx.update_buffer(|buffer, cx| {
9937 buffer.set_language(Some(language), cx);
9938 });
9939
9940 // Ensure that signature_help is not called when no signature help is enabled.
9941 cx.set_state(
9942 &r#"
9943 fn main() {
9944 sampleˇ
9945 }
9946 "#
9947 .unindent(),
9948 );
9949 cx.update_editor(|editor, window, cx| {
9950 editor.handle_input("(", window, cx);
9951 });
9952 cx.assert_editor_state(
9953 &"
9954 fn main() {
9955 sample(ˇ)
9956 }
9957 "
9958 .unindent(),
9959 );
9960 cx.editor(|editor, _, _| {
9961 assert!(editor.signature_help_state.task().is_none());
9962 });
9963
9964 let mocked_response = lsp::SignatureHelp {
9965 signatures: vec![lsp::SignatureInformation {
9966 label: "fn sample(param1: u8, param2: u8)".to_string(),
9967 documentation: None,
9968 parameters: Some(vec![
9969 lsp::ParameterInformation {
9970 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9971 documentation: None,
9972 },
9973 lsp::ParameterInformation {
9974 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9975 documentation: None,
9976 },
9977 ]),
9978 active_parameter: None,
9979 }],
9980 active_signature: Some(0),
9981 active_parameter: Some(0),
9982 };
9983
9984 // Ensure that signature_help is called when enabled afte edits
9985 cx.update(|_, cx| {
9986 cx.update_global::<SettingsStore, _>(|settings, cx| {
9987 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9988 settings.auto_signature_help = Some(false);
9989 settings.show_signature_help_after_edits = Some(true);
9990 });
9991 });
9992 });
9993 cx.set_state(
9994 &r#"
9995 fn main() {
9996 sampleˇ
9997 }
9998 "#
9999 .unindent(),
10000 );
10001 cx.update_editor(|editor, window, cx| {
10002 editor.handle_input("(", window, cx);
10003 });
10004 cx.assert_editor_state(
10005 &"
10006 fn main() {
10007 sample(ˇ)
10008 }
10009 "
10010 .unindent(),
10011 );
10012 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10013 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10014 .await;
10015 cx.update_editor(|editor, _, _| {
10016 let signature_help_state = editor.signature_help_state.popover().cloned();
10017 assert!(signature_help_state.is_some());
10018 assert_eq!(
10019 signature_help_state.unwrap().label,
10020 "param1: u8, param2: u8"
10021 );
10022 editor.signature_help_state = SignatureHelpState::default();
10023 });
10024
10025 // Ensure that signature_help is called when auto signature help override is enabled
10026 cx.update(|_, cx| {
10027 cx.update_global::<SettingsStore, _>(|settings, cx| {
10028 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10029 settings.auto_signature_help = Some(true);
10030 settings.show_signature_help_after_edits = Some(false);
10031 });
10032 });
10033 });
10034 cx.set_state(
10035 &r#"
10036 fn main() {
10037 sampleˇ
10038 }
10039 "#
10040 .unindent(),
10041 );
10042 cx.update_editor(|editor, window, cx| {
10043 editor.handle_input("(", window, cx);
10044 });
10045 cx.assert_editor_state(
10046 &"
10047 fn main() {
10048 sample(ˇ)
10049 }
10050 "
10051 .unindent(),
10052 );
10053 handle_signature_help_request(&mut cx, mocked_response).await;
10054 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10055 .await;
10056 cx.editor(|editor, _, _| {
10057 let signature_help_state = editor.signature_help_state.popover().cloned();
10058 assert!(signature_help_state.is_some());
10059 assert_eq!(
10060 signature_help_state.unwrap().label,
10061 "param1: u8, param2: u8"
10062 );
10063 });
10064}
10065
10066#[gpui::test]
10067async fn test_signature_help(cx: &mut TestAppContext) {
10068 init_test(cx, |_| {});
10069 cx.update(|cx| {
10070 cx.update_global::<SettingsStore, _>(|settings, cx| {
10071 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10072 settings.auto_signature_help = Some(true);
10073 });
10074 });
10075 });
10076
10077 let mut cx = EditorLspTestContext::new_rust(
10078 lsp::ServerCapabilities {
10079 signature_help_provider: Some(lsp::SignatureHelpOptions {
10080 ..Default::default()
10081 }),
10082 ..Default::default()
10083 },
10084 cx,
10085 )
10086 .await;
10087
10088 // A test that directly calls `show_signature_help`
10089 cx.update_editor(|editor, window, cx| {
10090 editor.show_signature_help(&ShowSignatureHelp, window, cx);
10091 });
10092
10093 let mocked_response = lsp::SignatureHelp {
10094 signatures: vec![lsp::SignatureInformation {
10095 label: "fn sample(param1: u8, param2: u8)".to_string(),
10096 documentation: None,
10097 parameters: Some(vec![
10098 lsp::ParameterInformation {
10099 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10100 documentation: None,
10101 },
10102 lsp::ParameterInformation {
10103 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10104 documentation: None,
10105 },
10106 ]),
10107 active_parameter: None,
10108 }],
10109 active_signature: Some(0),
10110 active_parameter: Some(0),
10111 };
10112 handle_signature_help_request(&mut cx, mocked_response).await;
10113
10114 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10115 .await;
10116
10117 cx.editor(|editor, _, _| {
10118 let signature_help_state = editor.signature_help_state.popover().cloned();
10119 assert!(signature_help_state.is_some());
10120 assert_eq!(
10121 signature_help_state.unwrap().label,
10122 "param1: u8, param2: u8"
10123 );
10124 });
10125
10126 // When exiting outside from inside the brackets, `signature_help` is closed.
10127 cx.set_state(indoc! {"
10128 fn main() {
10129 sample(ˇ);
10130 }
10131
10132 fn sample(param1: u8, param2: u8) {}
10133 "});
10134
10135 cx.update_editor(|editor, window, cx| {
10136 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
10137 });
10138
10139 let mocked_response = lsp::SignatureHelp {
10140 signatures: Vec::new(),
10141 active_signature: None,
10142 active_parameter: None,
10143 };
10144 handle_signature_help_request(&mut cx, mocked_response).await;
10145
10146 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
10147 .await;
10148
10149 cx.editor(|editor, _, _| {
10150 assert!(!editor.signature_help_state.is_shown());
10151 });
10152
10153 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
10154 cx.set_state(indoc! {"
10155 fn main() {
10156 sample(ˇ);
10157 }
10158
10159 fn sample(param1: u8, param2: u8) {}
10160 "});
10161
10162 let mocked_response = lsp::SignatureHelp {
10163 signatures: vec![lsp::SignatureInformation {
10164 label: "fn sample(param1: u8, param2: u8)".to_string(),
10165 documentation: None,
10166 parameters: Some(vec![
10167 lsp::ParameterInformation {
10168 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10169 documentation: None,
10170 },
10171 lsp::ParameterInformation {
10172 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10173 documentation: None,
10174 },
10175 ]),
10176 active_parameter: None,
10177 }],
10178 active_signature: Some(0),
10179 active_parameter: Some(0),
10180 };
10181 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10182 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10183 .await;
10184 cx.editor(|editor, _, _| {
10185 assert!(editor.signature_help_state.is_shown());
10186 });
10187
10188 // Restore the popover with more parameter input
10189 cx.set_state(indoc! {"
10190 fn main() {
10191 sample(param1, param2ˇ);
10192 }
10193
10194 fn sample(param1: u8, param2: u8) {}
10195 "});
10196
10197 let mocked_response = lsp::SignatureHelp {
10198 signatures: vec![lsp::SignatureInformation {
10199 label: "fn sample(param1: u8, param2: u8)".to_string(),
10200 documentation: None,
10201 parameters: Some(vec![
10202 lsp::ParameterInformation {
10203 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10204 documentation: None,
10205 },
10206 lsp::ParameterInformation {
10207 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10208 documentation: None,
10209 },
10210 ]),
10211 active_parameter: None,
10212 }],
10213 active_signature: Some(0),
10214 active_parameter: Some(1),
10215 };
10216 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10217 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10218 .await;
10219
10220 // When selecting a range, the popover is gone.
10221 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
10222 cx.update_editor(|editor, window, cx| {
10223 editor.change_selections(None, window, cx, |s| {
10224 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
10225 })
10226 });
10227 cx.assert_editor_state(indoc! {"
10228 fn main() {
10229 sample(param1, «ˇparam2»);
10230 }
10231
10232 fn sample(param1: u8, param2: u8) {}
10233 "});
10234 cx.editor(|editor, _, _| {
10235 assert!(!editor.signature_help_state.is_shown());
10236 });
10237
10238 // When unselecting again, the popover is back if within the brackets.
10239 cx.update_editor(|editor, window, cx| {
10240 editor.change_selections(None, window, cx, |s| {
10241 s.select_ranges(Some(Point::new(1, 19)..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 handle_signature_help_request(&mut cx, mocked_response).await;
10252 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10253 .await;
10254 cx.editor(|editor, _, _| {
10255 assert!(editor.signature_help_state.is_shown());
10256 });
10257
10258 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
10259 cx.update_editor(|editor, window, cx| {
10260 editor.change_selections(None, window, cx, |s| {
10261 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
10262 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
10263 })
10264 });
10265 cx.assert_editor_state(indoc! {"
10266 fn main() {
10267 sample(param1, ˇparam2);
10268 }
10269
10270 fn sample(param1: u8, param2: u8) {}
10271 "});
10272
10273 let mocked_response = lsp::SignatureHelp {
10274 signatures: vec![lsp::SignatureInformation {
10275 label: "fn sample(param1: u8, param2: u8)".to_string(),
10276 documentation: None,
10277 parameters: Some(vec![
10278 lsp::ParameterInformation {
10279 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10280 documentation: None,
10281 },
10282 lsp::ParameterInformation {
10283 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10284 documentation: None,
10285 },
10286 ]),
10287 active_parameter: None,
10288 }],
10289 active_signature: Some(0),
10290 active_parameter: Some(1),
10291 };
10292 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10293 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10294 .await;
10295 cx.update_editor(|editor, _, cx| {
10296 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
10297 });
10298 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
10299 .await;
10300 cx.update_editor(|editor, window, cx| {
10301 editor.change_selections(None, window, cx, |s| {
10302 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
10303 })
10304 });
10305 cx.assert_editor_state(indoc! {"
10306 fn main() {
10307 sample(param1, «ˇparam2»);
10308 }
10309
10310 fn sample(param1: u8, param2: u8) {}
10311 "});
10312 cx.update_editor(|editor, window, cx| {
10313 editor.change_selections(None, window, cx, |s| {
10314 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
10315 })
10316 });
10317 cx.assert_editor_state(indoc! {"
10318 fn main() {
10319 sample(param1, ˇparam2);
10320 }
10321
10322 fn sample(param1: u8, param2: u8) {}
10323 "});
10324 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
10325 .await;
10326}
10327
10328#[gpui::test]
10329async fn test_completion_mode(cx: &mut TestAppContext) {
10330 init_test(cx, |_| {});
10331 let mut cx = EditorLspTestContext::new_rust(
10332 lsp::ServerCapabilities {
10333 completion_provider: Some(lsp::CompletionOptions {
10334 resolve_provider: Some(true),
10335 ..Default::default()
10336 }),
10337 ..Default::default()
10338 },
10339 cx,
10340 )
10341 .await;
10342
10343 struct Run {
10344 run_description: &'static str,
10345 initial_state: String,
10346 buffer_marked_text: String,
10347 completion_text: &'static str,
10348 expected_with_insert_mode: String,
10349 expected_with_replace_mode: String,
10350 expected_with_replace_subsequence_mode: String,
10351 expected_with_replace_suffix_mode: String,
10352 }
10353
10354 let runs = [
10355 Run {
10356 run_description: "Start of word matches completion text",
10357 initial_state: "before ediˇ after".into(),
10358 buffer_marked_text: "before <edi|> after".into(),
10359 completion_text: "editor",
10360 expected_with_insert_mode: "before editorˇ after".into(),
10361 expected_with_replace_mode: "before editorˇ after".into(),
10362 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10363 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10364 },
10365 Run {
10366 run_description: "Accept same text at the middle of the word",
10367 initial_state: "before ediˇtor after".into(),
10368 buffer_marked_text: "before <edi|tor> after".into(),
10369 completion_text: "editor",
10370 expected_with_insert_mode: "before editorˇtor after".into(),
10371 expected_with_replace_mode: "before editorˇ after".into(),
10372 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10373 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10374 },
10375 Run {
10376 run_description: "End of word matches completion text -- cursor at end",
10377 initial_state: "before torˇ after".into(),
10378 buffer_marked_text: "before <tor|> after".into(),
10379 completion_text: "editor",
10380 expected_with_insert_mode: "before editorˇ after".into(),
10381 expected_with_replace_mode: "before editorˇ after".into(),
10382 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10383 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10384 },
10385 Run {
10386 run_description: "End of word matches completion text -- cursor at start",
10387 initial_state: "before ˇtor after".into(),
10388 buffer_marked_text: "before <|tor> after".into(),
10389 completion_text: "editor",
10390 expected_with_insert_mode: "before editorˇtor after".into(),
10391 expected_with_replace_mode: "before editorˇ after".into(),
10392 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10393 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10394 },
10395 Run {
10396 run_description: "Prepend text containing whitespace",
10397 initial_state: "pˇfield: bool".into(),
10398 buffer_marked_text: "<p|field>: bool".into(),
10399 completion_text: "pub ",
10400 expected_with_insert_mode: "pub ˇfield: bool".into(),
10401 expected_with_replace_mode: "pub ˇ: bool".into(),
10402 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
10403 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
10404 },
10405 Run {
10406 run_description: "Add element to start of list",
10407 initial_state: "[element_ˇelement_2]".into(),
10408 buffer_marked_text: "[<element_|element_2>]".into(),
10409 completion_text: "element_1",
10410 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
10411 expected_with_replace_mode: "[element_1ˇ]".into(),
10412 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
10413 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
10414 },
10415 Run {
10416 run_description: "Add element to start of list -- first and second elements are equal",
10417 initial_state: "[elˇelement]".into(),
10418 buffer_marked_text: "[<el|element>]".into(),
10419 completion_text: "element",
10420 expected_with_insert_mode: "[elementˇelement]".into(),
10421 expected_with_replace_mode: "[elementˇ]".into(),
10422 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
10423 expected_with_replace_suffix_mode: "[elementˇ]".into(),
10424 },
10425 Run {
10426 run_description: "Ends with matching suffix",
10427 initial_state: "SubˇError".into(),
10428 buffer_marked_text: "<Sub|Error>".into(),
10429 completion_text: "SubscriptionError",
10430 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
10431 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10432 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10433 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
10434 },
10435 Run {
10436 run_description: "Suffix is a subsequence -- contiguous",
10437 initial_state: "SubˇErr".into(),
10438 buffer_marked_text: "<Sub|Err>".into(),
10439 completion_text: "SubscriptionError",
10440 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
10441 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10442 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10443 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
10444 },
10445 Run {
10446 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
10447 initial_state: "Suˇscrirr".into(),
10448 buffer_marked_text: "<Su|scrirr>".into(),
10449 completion_text: "SubscriptionError",
10450 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
10451 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10452 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10453 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
10454 },
10455 Run {
10456 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
10457 initial_state: "foo(indˇix)".into(),
10458 buffer_marked_text: "foo(<ind|ix>)".into(),
10459 completion_text: "node_index",
10460 expected_with_insert_mode: "foo(node_indexˇix)".into(),
10461 expected_with_replace_mode: "foo(node_indexˇ)".into(),
10462 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
10463 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
10464 },
10465 ];
10466
10467 for run in runs {
10468 let run_variations = [
10469 (LspInsertMode::Insert, run.expected_with_insert_mode),
10470 (LspInsertMode::Replace, run.expected_with_replace_mode),
10471 (
10472 LspInsertMode::ReplaceSubsequence,
10473 run.expected_with_replace_subsequence_mode,
10474 ),
10475 (
10476 LspInsertMode::ReplaceSuffix,
10477 run.expected_with_replace_suffix_mode,
10478 ),
10479 ];
10480
10481 for (lsp_insert_mode, expected_text) in run_variations {
10482 eprintln!(
10483 "run = {:?}, mode = {lsp_insert_mode:.?}",
10484 run.run_description,
10485 );
10486
10487 update_test_language_settings(&mut cx, |settings| {
10488 settings.defaults.completions = Some(CompletionSettings {
10489 lsp_insert_mode,
10490 words: WordsCompletionMode::Disabled,
10491 lsp: true,
10492 lsp_fetch_timeout_ms: 0,
10493 });
10494 });
10495
10496 cx.set_state(&run.initial_state);
10497 cx.update_editor(|editor, window, cx| {
10498 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10499 });
10500
10501 let counter = Arc::new(AtomicUsize::new(0));
10502 handle_completion_request_with_insert_and_replace(
10503 &mut cx,
10504 &run.buffer_marked_text,
10505 vec![run.completion_text],
10506 counter.clone(),
10507 )
10508 .await;
10509 cx.condition(|editor, _| editor.context_menu_visible())
10510 .await;
10511 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
10512
10513 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10514 editor
10515 .confirm_completion(&ConfirmCompletion::default(), window, cx)
10516 .unwrap()
10517 });
10518 cx.assert_editor_state(&expected_text);
10519 handle_resolve_completion_request(&mut cx, None).await;
10520 apply_additional_edits.await.unwrap();
10521 }
10522 }
10523}
10524
10525#[gpui::test]
10526async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
10527 init_test(cx, |_| {});
10528 let mut cx = EditorLspTestContext::new_rust(
10529 lsp::ServerCapabilities {
10530 completion_provider: Some(lsp::CompletionOptions {
10531 resolve_provider: Some(true),
10532 ..Default::default()
10533 }),
10534 ..Default::default()
10535 },
10536 cx,
10537 )
10538 .await;
10539
10540 let initial_state = "SubˇError";
10541 let buffer_marked_text = "<Sub|Error>";
10542 let completion_text = "SubscriptionError";
10543 let expected_with_insert_mode = "SubscriptionErrorˇError";
10544 let expected_with_replace_mode = "SubscriptionErrorˇ";
10545
10546 update_test_language_settings(&mut cx, |settings| {
10547 settings.defaults.completions = Some(CompletionSettings {
10548 words: WordsCompletionMode::Disabled,
10549 // set the opposite here to ensure that the action is overriding the default behavior
10550 lsp_insert_mode: LspInsertMode::Insert,
10551 lsp: true,
10552 lsp_fetch_timeout_ms: 0,
10553 });
10554 });
10555
10556 cx.set_state(initial_state);
10557 cx.update_editor(|editor, window, cx| {
10558 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10559 });
10560
10561 let counter = Arc::new(AtomicUsize::new(0));
10562 handle_completion_request_with_insert_and_replace(
10563 &mut cx,
10564 &buffer_marked_text,
10565 vec![completion_text],
10566 counter.clone(),
10567 )
10568 .await;
10569 cx.condition(|editor, _| editor.context_menu_visible())
10570 .await;
10571 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
10572
10573 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10574 editor
10575 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10576 .unwrap()
10577 });
10578 cx.assert_editor_state(&expected_with_replace_mode);
10579 handle_resolve_completion_request(&mut cx, None).await;
10580 apply_additional_edits.await.unwrap();
10581
10582 update_test_language_settings(&mut cx, |settings| {
10583 settings.defaults.completions = Some(CompletionSettings {
10584 words: WordsCompletionMode::Disabled,
10585 // set the opposite here to ensure that the action is overriding the default behavior
10586 lsp_insert_mode: LspInsertMode::Replace,
10587 lsp: true,
10588 lsp_fetch_timeout_ms: 0,
10589 });
10590 });
10591
10592 cx.set_state(initial_state);
10593 cx.update_editor(|editor, window, cx| {
10594 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10595 });
10596 handle_completion_request_with_insert_and_replace(
10597 &mut cx,
10598 &buffer_marked_text,
10599 vec![completion_text],
10600 counter.clone(),
10601 )
10602 .await;
10603 cx.condition(|editor, _| editor.context_menu_visible())
10604 .await;
10605 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
10606
10607 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10608 editor
10609 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
10610 .unwrap()
10611 });
10612 cx.assert_editor_state(&expected_with_insert_mode);
10613 handle_resolve_completion_request(&mut cx, None).await;
10614 apply_additional_edits.await.unwrap();
10615}
10616
10617#[gpui::test]
10618async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
10619 init_test(cx, |_| {});
10620 let mut cx = EditorLspTestContext::new_rust(
10621 lsp::ServerCapabilities {
10622 completion_provider: Some(lsp::CompletionOptions {
10623 resolve_provider: Some(true),
10624 ..Default::default()
10625 }),
10626 ..Default::default()
10627 },
10628 cx,
10629 )
10630 .await;
10631
10632 // scenario: surrounding text matches completion text
10633 let completion_text = "to_offset";
10634 let initial_state = indoc! {"
10635 1. buf.to_offˇsuffix
10636 2. buf.to_offˇsuf
10637 3. buf.to_offˇfix
10638 4. buf.to_offˇ
10639 5. into_offˇensive
10640 6. ˇsuffix
10641 7. let ˇ //
10642 8. aaˇzz
10643 9. buf.to_off«zzzzzˇ»suffix
10644 10. buf.«ˇzzzzz»suffix
10645 11. to_off«ˇzzzzz»
10646
10647 buf.to_offˇsuffix // newest cursor
10648 "};
10649 let completion_marked_buffer = indoc! {"
10650 1. buf.to_offsuffix
10651 2. buf.to_offsuf
10652 3. buf.to_offfix
10653 4. buf.to_off
10654 5. into_offensive
10655 6. suffix
10656 7. let //
10657 8. aazz
10658 9. buf.to_offzzzzzsuffix
10659 10. buf.zzzzzsuffix
10660 11. to_offzzzzz
10661
10662 buf.<to_off|suffix> // newest cursor
10663 "};
10664 let expected = indoc! {"
10665 1. buf.to_offsetˇ
10666 2. buf.to_offsetˇsuf
10667 3. buf.to_offsetˇfix
10668 4. buf.to_offsetˇ
10669 5. into_offsetˇensive
10670 6. to_offsetˇsuffix
10671 7. let to_offsetˇ //
10672 8. aato_offsetˇzz
10673 9. buf.to_offsetˇ
10674 10. buf.to_offsetˇsuffix
10675 11. to_offsetˇ
10676
10677 buf.to_offsetˇ // newest cursor
10678 "};
10679 cx.set_state(initial_state);
10680 cx.update_editor(|editor, window, cx| {
10681 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10682 });
10683 handle_completion_request_with_insert_and_replace(
10684 &mut cx,
10685 completion_marked_buffer,
10686 vec![completion_text],
10687 Arc::new(AtomicUsize::new(0)),
10688 )
10689 .await;
10690 cx.condition(|editor, _| editor.context_menu_visible())
10691 .await;
10692 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10693 editor
10694 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10695 .unwrap()
10696 });
10697 cx.assert_editor_state(expected);
10698 handle_resolve_completion_request(&mut cx, None).await;
10699 apply_additional_edits.await.unwrap();
10700
10701 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
10702 let completion_text = "foo_and_bar";
10703 let initial_state = indoc! {"
10704 1. ooanbˇ
10705 2. zooanbˇ
10706 3. ooanbˇz
10707 4. zooanbˇz
10708 5. ooanˇ
10709 6. oanbˇ
10710
10711 ooanbˇ
10712 "};
10713 let completion_marked_buffer = indoc! {"
10714 1. ooanb
10715 2. zooanb
10716 3. ooanbz
10717 4. zooanbz
10718 5. ooan
10719 6. oanb
10720
10721 <ooanb|>
10722 "};
10723 let expected = indoc! {"
10724 1. foo_and_barˇ
10725 2. zfoo_and_barˇ
10726 3. foo_and_barˇz
10727 4. zfoo_and_barˇz
10728 5. ooanfoo_and_barˇ
10729 6. oanbfoo_and_barˇ
10730
10731 foo_and_barˇ
10732 "};
10733 cx.set_state(initial_state);
10734 cx.update_editor(|editor, window, cx| {
10735 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10736 });
10737 handle_completion_request_with_insert_and_replace(
10738 &mut cx,
10739 completion_marked_buffer,
10740 vec![completion_text],
10741 Arc::new(AtomicUsize::new(0)),
10742 )
10743 .await;
10744 cx.condition(|editor, _| editor.context_menu_visible())
10745 .await;
10746 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10747 editor
10748 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10749 .unwrap()
10750 });
10751 cx.assert_editor_state(expected);
10752 handle_resolve_completion_request(&mut cx, None).await;
10753 apply_additional_edits.await.unwrap();
10754
10755 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
10756 // (expects the same as if it was inserted at the end)
10757 let completion_text = "foo_and_bar";
10758 let initial_state = indoc! {"
10759 1. ooˇanb
10760 2. zooˇanb
10761 3. ooˇanbz
10762 4. zooˇanbz
10763
10764 ooˇanb
10765 "};
10766 let completion_marked_buffer = indoc! {"
10767 1. ooanb
10768 2. zooanb
10769 3. ooanbz
10770 4. zooanbz
10771
10772 <oo|anb>
10773 "};
10774 let expected = indoc! {"
10775 1. foo_and_barˇ
10776 2. zfoo_and_barˇ
10777 3. foo_and_barˇz
10778 4. zfoo_and_barˇz
10779
10780 foo_and_barˇ
10781 "};
10782 cx.set_state(initial_state);
10783 cx.update_editor(|editor, window, cx| {
10784 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10785 });
10786 handle_completion_request_with_insert_and_replace(
10787 &mut cx,
10788 completion_marked_buffer,
10789 vec![completion_text],
10790 Arc::new(AtomicUsize::new(0)),
10791 )
10792 .await;
10793 cx.condition(|editor, _| editor.context_menu_visible())
10794 .await;
10795 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10796 editor
10797 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10798 .unwrap()
10799 });
10800 cx.assert_editor_state(expected);
10801 handle_resolve_completion_request(&mut cx, None).await;
10802 apply_additional_edits.await.unwrap();
10803}
10804
10805// This used to crash
10806#[gpui::test]
10807async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
10808 init_test(cx, |_| {});
10809
10810 let buffer_text = indoc! {"
10811 fn main() {
10812 10.satu;
10813
10814 //
10815 // separate cursors so they open in different excerpts (manually reproducible)
10816 //
10817
10818 10.satu20;
10819 }
10820 "};
10821 let multibuffer_text_with_selections = indoc! {"
10822 fn main() {
10823 10.satuˇ;
10824
10825 //
10826
10827 //
10828
10829 10.satuˇ20;
10830 }
10831 "};
10832 let expected_multibuffer = indoc! {"
10833 fn main() {
10834 10.saturating_sub()ˇ;
10835
10836 //
10837
10838 //
10839
10840 10.saturating_sub()ˇ;
10841 }
10842 "};
10843
10844 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
10845 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
10846
10847 let fs = FakeFs::new(cx.executor());
10848 fs.insert_tree(
10849 path!("/a"),
10850 json!({
10851 "main.rs": buffer_text,
10852 }),
10853 )
10854 .await;
10855
10856 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
10857 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10858 language_registry.add(rust_lang());
10859 let mut fake_servers = language_registry.register_fake_lsp(
10860 "Rust",
10861 FakeLspAdapter {
10862 capabilities: lsp::ServerCapabilities {
10863 completion_provider: Some(lsp::CompletionOptions {
10864 resolve_provider: None,
10865 ..lsp::CompletionOptions::default()
10866 }),
10867 ..lsp::ServerCapabilities::default()
10868 },
10869 ..FakeLspAdapter::default()
10870 },
10871 );
10872 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
10873 let cx = &mut VisualTestContext::from_window(*workspace, cx);
10874 let buffer = project
10875 .update(cx, |project, cx| {
10876 project.open_local_buffer(path!("/a/main.rs"), cx)
10877 })
10878 .await
10879 .unwrap();
10880
10881 let multi_buffer = cx.new(|cx| {
10882 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
10883 multi_buffer.push_excerpts(
10884 buffer.clone(),
10885 [ExcerptRange::new(0..first_excerpt_end)],
10886 cx,
10887 );
10888 multi_buffer.push_excerpts(
10889 buffer.clone(),
10890 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
10891 cx,
10892 );
10893 multi_buffer
10894 });
10895
10896 let editor = workspace
10897 .update(cx, |_, window, cx| {
10898 cx.new(|cx| {
10899 Editor::new(
10900 EditorMode::Full {
10901 scale_ui_elements_with_buffer_font_size: false,
10902 show_active_line_background: false,
10903 sized_by_content: false,
10904 },
10905 multi_buffer.clone(),
10906 Some(project.clone()),
10907 window,
10908 cx,
10909 )
10910 })
10911 })
10912 .unwrap();
10913
10914 let pane = workspace
10915 .update(cx, |workspace, _, _| workspace.active_pane().clone())
10916 .unwrap();
10917 pane.update_in(cx, |pane, window, cx| {
10918 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
10919 });
10920
10921 let fake_server = fake_servers.next().await.unwrap();
10922
10923 editor.update_in(cx, |editor, window, cx| {
10924 editor.change_selections(None, window, cx, |s| {
10925 s.select_ranges([
10926 Point::new(1, 11)..Point::new(1, 11),
10927 Point::new(7, 11)..Point::new(7, 11),
10928 ])
10929 });
10930
10931 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
10932 });
10933
10934 editor.update_in(cx, |editor, window, cx| {
10935 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10936 });
10937
10938 fake_server
10939 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
10940 let completion_item = lsp::CompletionItem {
10941 label: "saturating_sub()".into(),
10942 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
10943 lsp::InsertReplaceEdit {
10944 new_text: "saturating_sub()".to_owned(),
10945 insert: lsp::Range::new(
10946 lsp::Position::new(7, 7),
10947 lsp::Position::new(7, 11),
10948 ),
10949 replace: lsp::Range::new(
10950 lsp::Position::new(7, 7),
10951 lsp::Position::new(7, 13),
10952 ),
10953 },
10954 )),
10955 ..lsp::CompletionItem::default()
10956 };
10957
10958 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
10959 })
10960 .next()
10961 .await
10962 .unwrap();
10963
10964 cx.condition(&editor, |editor, _| editor.context_menu_visible())
10965 .await;
10966
10967 editor
10968 .update_in(cx, |editor, window, cx| {
10969 editor
10970 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10971 .unwrap()
10972 })
10973 .await
10974 .unwrap();
10975
10976 editor.update(cx, |editor, cx| {
10977 assert_text_with_selections(editor, expected_multibuffer, cx);
10978 })
10979}
10980
10981#[gpui::test]
10982async fn test_completion(cx: &mut TestAppContext) {
10983 init_test(cx, |_| {});
10984
10985 let mut cx = EditorLspTestContext::new_rust(
10986 lsp::ServerCapabilities {
10987 completion_provider: Some(lsp::CompletionOptions {
10988 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
10989 resolve_provider: Some(true),
10990 ..Default::default()
10991 }),
10992 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
10993 ..Default::default()
10994 },
10995 cx,
10996 )
10997 .await;
10998 let counter = Arc::new(AtomicUsize::new(0));
10999
11000 cx.set_state(indoc! {"
11001 oneˇ
11002 two
11003 three
11004 "});
11005 cx.simulate_keystroke(".");
11006 handle_completion_request(
11007 &mut cx,
11008 indoc! {"
11009 one.|<>
11010 two
11011 three
11012 "},
11013 vec!["first_completion", "second_completion"],
11014 counter.clone(),
11015 )
11016 .await;
11017 cx.condition(|editor, _| editor.context_menu_visible())
11018 .await;
11019 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11020
11021 let _handler = handle_signature_help_request(
11022 &mut cx,
11023 lsp::SignatureHelp {
11024 signatures: vec![lsp::SignatureInformation {
11025 label: "test signature".to_string(),
11026 documentation: None,
11027 parameters: Some(vec![lsp::ParameterInformation {
11028 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
11029 documentation: None,
11030 }]),
11031 active_parameter: None,
11032 }],
11033 active_signature: None,
11034 active_parameter: None,
11035 },
11036 );
11037 cx.update_editor(|editor, window, cx| {
11038 assert!(
11039 !editor.signature_help_state.is_shown(),
11040 "No signature help was called for"
11041 );
11042 editor.show_signature_help(&ShowSignatureHelp, window, cx);
11043 });
11044 cx.run_until_parked();
11045 cx.update_editor(|editor, _, _| {
11046 assert!(
11047 !editor.signature_help_state.is_shown(),
11048 "No signature help should be shown when completions menu is open"
11049 );
11050 });
11051
11052 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11053 editor.context_menu_next(&Default::default(), window, cx);
11054 editor
11055 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11056 .unwrap()
11057 });
11058 cx.assert_editor_state(indoc! {"
11059 one.second_completionˇ
11060 two
11061 three
11062 "});
11063
11064 handle_resolve_completion_request(
11065 &mut cx,
11066 Some(vec![
11067 (
11068 //This overlaps with the primary completion edit which is
11069 //misbehavior from the LSP spec, test that we filter it out
11070 indoc! {"
11071 one.second_ˇcompletion
11072 two
11073 threeˇ
11074 "},
11075 "overlapping additional edit",
11076 ),
11077 (
11078 indoc! {"
11079 one.second_completion
11080 two
11081 threeˇ
11082 "},
11083 "\nadditional edit",
11084 ),
11085 ]),
11086 )
11087 .await;
11088 apply_additional_edits.await.unwrap();
11089 cx.assert_editor_state(indoc! {"
11090 one.second_completionˇ
11091 two
11092 three
11093 additional edit
11094 "});
11095
11096 cx.set_state(indoc! {"
11097 one.second_completion
11098 twoˇ
11099 threeˇ
11100 additional edit
11101 "});
11102 cx.simulate_keystroke(" ");
11103 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11104 cx.simulate_keystroke("s");
11105 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11106
11107 cx.assert_editor_state(indoc! {"
11108 one.second_completion
11109 two sˇ
11110 three sˇ
11111 additional edit
11112 "});
11113 handle_completion_request(
11114 &mut cx,
11115 indoc! {"
11116 one.second_completion
11117 two s
11118 three <s|>
11119 additional edit
11120 "},
11121 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
11122 counter.clone(),
11123 )
11124 .await;
11125 cx.condition(|editor, _| editor.context_menu_visible())
11126 .await;
11127 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
11128
11129 cx.simulate_keystroke("i");
11130
11131 handle_completion_request(
11132 &mut cx,
11133 indoc! {"
11134 one.second_completion
11135 two si
11136 three <si|>
11137 additional edit
11138 "},
11139 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
11140 counter.clone(),
11141 )
11142 .await;
11143 cx.condition(|editor, _| editor.context_menu_visible())
11144 .await;
11145 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
11146
11147 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11148 editor
11149 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11150 .unwrap()
11151 });
11152 cx.assert_editor_state(indoc! {"
11153 one.second_completion
11154 two sixth_completionˇ
11155 three sixth_completionˇ
11156 additional edit
11157 "});
11158
11159 apply_additional_edits.await.unwrap();
11160
11161 update_test_language_settings(&mut cx, |settings| {
11162 settings.defaults.show_completions_on_input = Some(false);
11163 });
11164 cx.set_state("editorˇ");
11165 cx.simulate_keystroke(".");
11166 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11167 cx.simulate_keystrokes("c l o");
11168 cx.assert_editor_state("editor.cloˇ");
11169 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11170 cx.update_editor(|editor, window, cx| {
11171 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11172 });
11173 handle_completion_request(
11174 &mut cx,
11175 "editor.<clo|>",
11176 vec!["close", "clobber"],
11177 counter.clone(),
11178 )
11179 .await;
11180 cx.condition(|editor, _| editor.context_menu_visible())
11181 .await;
11182 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
11183
11184 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11185 editor
11186 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11187 .unwrap()
11188 });
11189 cx.assert_editor_state("editor.closeˇ");
11190 handle_resolve_completion_request(&mut cx, None).await;
11191 apply_additional_edits.await.unwrap();
11192}
11193
11194#[gpui::test]
11195async fn test_word_completion(cx: &mut TestAppContext) {
11196 let lsp_fetch_timeout_ms = 10;
11197 init_test(cx, |language_settings| {
11198 language_settings.defaults.completions = Some(CompletionSettings {
11199 words: WordsCompletionMode::Fallback,
11200 lsp: true,
11201 lsp_fetch_timeout_ms: 10,
11202 lsp_insert_mode: LspInsertMode::Insert,
11203 });
11204 });
11205
11206 let mut cx = EditorLspTestContext::new_rust(
11207 lsp::ServerCapabilities {
11208 completion_provider: Some(lsp::CompletionOptions {
11209 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11210 ..lsp::CompletionOptions::default()
11211 }),
11212 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11213 ..lsp::ServerCapabilities::default()
11214 },
11215 cx,
11216 )
11217 .await;
11218
11219 let throttle_completions = Arc::new(AtomicBool::new(false));
11220
11221 let lsp_throttle_completions = throttle_completions.clone();
11222 let _completion_requests_handler =
11223 cx.lsp
11224 .server
11225 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
11226 let lsp_throttle_completions = lsp_throttle_completions.clone();
11227 let cx = cx.clone();
11228 async move {
11229 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
11230 cx.background_executor()
11231 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
11232 .await;
11233 }
11234 Ok(Some(lsp::CompletionResponse::Array(vec![
11235 lsp::CompletionItem {
11236 label: "first".into(),
11237 ..lsp::CompletionItem::default()
11238 },
11239 lsp::CompletionItem {
11240 label: "last".into(),
11241 ..lsp::CompletionItem::default()
11242 },
11243 ])))
11244 }
11245 });
11246
11247 cx.set_state(indoc! {"
11248 oneˇ
11249 two
11250 three
11251 "});
11252 cx.simulate_keystroke(".");
11253 cx.executor().run_until_parked();
11254 cx.condition(|editor, _| editor.context_menu_visible())
11255 .await;
11256 cx.update_editor(|editor, window, cx| {
11257 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11258 {
11259 assert_eq!(
11260 completion_menu_entries(&menu),
11261 &["first", "last"],
11262 "When LSP server is fast to reply, no fallback word completions are used"
11263 );
11264 } else {
11265 panic!("expected completion menu to be open");
11266 }
11267 editor.cancel(&Cancel, window, cx);
11268 });
11269 cx.executor().run_until_parked();
11270 cx.condition(|editor, _| !editor.context_menu_visible())
11271 .await;
11272
11273 throttle_completions.store(true, atomic::Ordering::Release);
11274 cx.simulate_keystroke(".");
11275 cx.executor()
11276 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
11277 cx.executor().run_until_parked();
11278 cx.condition(|editor, _| editor.context_menu_visible())
11279 .await;
11280 cx.update_editor(|editor, _, _| {
11281 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11282 {
11283 assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
11284 "When LSP server is slow, document words can be shown instead, if configured accordingly");
11285 } else {
11286 panic!("expected completion menu to be open");
11287 }
11288 });
11289}
11290
11291#[gpui::test]
11292async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
11293 init_test(cx, |language_settings| {
11294 language_settings.defaults.completions = Some(CompletionSettings {
11295 words: WordsCompletionMode::Enabled,
11296 lsp: true,
11297 lsp_fetch_timeout_ms: 0,
11298 lsp_insert_mode: LspInsertMode::Insert,
11299 });
11300 });
11301
11302 let mut cx = EditorLspTestContext::new_rust(
11303 lsp::ServerCapabilities {
11304 completion_provider: Some(lsp::CompletionOptions {
11305 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11306 ..lsp::CompletionOptions::default()
11307 }),
11308 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11309 ..lsp::ServerCapabilities::default()
11310 },
11311 cx,
11312 )
11313 .await;
11314
11315 let _completion_requests_handler =
11316 cx.lsp
11317 .server
11318 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
11319 Ok(Some(lsp::CompletionResponse::Array(vec![
11320 lsp::CompletionItem {
11321 label: "first".into(),
11322 ..lsp::CompletionItem::default()
11323 },
11324 lsp::CompletionItem {
11325 label: "last".into(),
11326 ..lsp::CompletionItem::default()
11327 },
11328 ])))
11329 });
11330
11331 cx.set_state(indoc! {"ˇ
11332 first
11333 last
11334 second
11335 "});
11336 cx.simulate_keystroke(".");
11337 cx.executor().run_until_parked();
11338 cx.condition(|editor, _| editor.context_menu_visible())
11339 .await;
11340 cx.update_editor(|editor, _, _| {
11341 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11342 {
11343 assert_eq!(
11344 completion_menu_entries(&menu),
11345 &["first", "last", "second"],
11346 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
11347 );
11348 } else {
11349 panic!("expected completion menu to be open");
11350 }
11351 });
11352}
11353
11354#[gpui::test]
11355async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
11356 init_test(cx, |language_settings| {
11357 language_settings.defaults.completions = Some(CompletionSettings {
11358 words: WordsCompletionMode::Disabled,
11359 lsp: true,
11360 lsp_fetch_timeout_ms: 0,
11361 lsp_insert_mode: LspInsertMode::Insert,
11362 });
11363 });
11364
11365 let mut cx = EditorLspTestContext::new_rust(
11366 lsp::ServerCapabilities {
11367 completion_provider: Some(lsp::CompletionOptions {
11368 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11369 ..lsp::CompletionOptions::default()
11370 }),
11371 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11372 ..lsp::ServerCapabilities::default()
11373 },
11374 cx,
11375 )
11376 .await;
11377
11378 let _completion_requests_handler =
11379 cx.lsp
11380 .server
11381 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
11382 panic!("LSP completions should not be queried when dealing with word completions")
11383 });
11384
11385 cx.set_state(indoc! {"ˇ
11386 first
11387 last
11388 second
11389 "});
11390 cx.update_editor(|editor, window, cx| {
11391 editor.show_word_completions(&ShowWordCompletions, window, cx);
11392 });
11393 cx.executor().run_until_parked();
11394 cx.condition(|editor, _| editor.context_menu_visible())
11395 .await;
11396 cx.update_editor(|editor, _, _| {
11397 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11398 {
11399 assert_eq!(
11400 completion_menu_entries(&menu),
11401 &["first", "last", "second"],
11402 "`ShowWordCompletions` action should show word completions"
11403 );
11404 } else {
11405 panic!("expected completion menu to be open");
11406 }
11407 });
11408
11409 cx.simulate_keystroke("l");
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 &["last"],
11419 "After showing word completions, further editing should filter them and not query the LSP"
11420 );
11421 } else {
11422 panic!("expected completion menu to be open");
11423 }
11424 });
11425}
11426
11427#[gpui::test]
11428async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
11429 init_test(cx, |language_settings| {
11430 language_settings.defaults.completions = Some(CompletionSettings {
11431 words: WordsCompletionMode::Fallback,
11432 lsp: false,
11433 lsp_fetch_timeout_ms: 0,
11434 lsp_insert_mode: LspInsertMode::Insert,
11435 });
11436 });
11437
11438 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
11439
11440 cx.set_state(indoc! {"ˇ
11441 0_usize
11442 let
11443 33
11444 4.5f32
11445 "});
11446 cx.update_editor(|editor, window, cx| {
11447 editor.show_completions(&ShowCompletions::default(), window, cx);
11448 });
11449 cx.executor().run_until_parked();
11450 cx.condition(|editor, _| editor.context_menu_visible())
11451 .await;
11452 cx.update_editor(|editor, window, cx| {
11453 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11454 {
11455 assert_eq!(
11456 completion_menu_entries(&menu),
11457 &["let"],
11458 "With no digits in the completion query, no digits should be in the word completions"
11459 );
11460 } else {
11461 panic!("expected completion menu to be open");
11462 }
11463 editor.cancel(&Cancel, window, cx);
11464 });
11465
11466 cx.set_state(indoc! {"3ˇ
11467 0_usize
11468 let
11469 3
11470 33.35f32
11471 "});
11472 cx.update_editor(|editor, window, cx| {
11473 editor.show_completions(&ShowCompletions::default(), window, cx);
11474 });
11475 cx.executor().run_until_parked();
11476 cx.condition(|editor, _| editor.context_menu_visible())
11477 .await;
11478 cx.update_editor(|editor, _, _| {
11479 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11480 {
11481 assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
11482 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
11483 } else {
11484 panic!("expected completion menu to be open");
11485 }
11486 });
11487}
11488
11489fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
11490 let position = || lsp::Position {
11491 line: params.text_document_position.position.line,
11492 character: params.text_document_position.position.character,
11493 };
11494 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11495 range: lsp::Range {
11496 start: position(),
11497 end: position(),
11498 },
11499 new_text: text.to_string(),
11500 }))
11501}
11502
11503#[gpui::test]
11504async fn test_multiline_completion(cx: &mut TestAppContext) {
11505 init_test(cx, |_| {});
11506
11507 let fs = FakeFs::new(cx.executor());
11508 fs.insert_tree(
11509 path!("/a"),
11510 json!({
11511 "main.ts": "a",
11512 }),
11513 )
11514 .await;
11515
11516 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11517 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11518 let typescript_language = Arc::new(Language::new(
11519 LanguageConfig {
11520 name: "TypeScript".into(),
11521 matcher: LanguageMatcher {
11522 path_suffixes: vec!["ts".to_string()],
11523 ..LanguageMatcher::default()
11524 },
11525 line_comments: vec!["// ".into()],
11526 ..LanguageConfig::default()
11527 },
11528 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
11529 ));
11530 language_registry.add(typescript_language.clone());
11531 let mut fake_servers = language_registry.register_fake_lsp(
11532 "TypeScript",
11533 FakeLspAdapter {
11534 capabilities: lsp::ServerCapabilities {
11535 completion_provider: Some(lsp::CompletionOptions {
11536 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11537 ..lsp::CompletionOptions::default()
11538 }),
11539 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11540 ..lsp::ServerCapabilities::default()
11541 },
11542 // Emulate vtsls label generation
11543 label_for_completion: Some(Box::new(|item, _| {
11544 let text = if let Some(description) = item
11545 .label_details
11546 .as_ref()
11547 .and_then(|label_details| label_details.description.as_ref())
11548 {
11549 format!("{} {}", item.label, description)
11550 } else if let Some(detail) = &item.detail {
11551 format!("{} {}", item.label, detail)
11552 } else {
11553 item.label.clone()
11554 };
11555 let len = text.len();
11556 Some(language::CodeLabel {
11557 text,
11558 runs: Vec::new(),
11559 filter_range: 0..len,
11560 })
11561 })),
11562 ..FakeLspAdapter::default()
11563 },
11564 );
11565 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11566 let cx = &mut VisualTestContext::from_window(*workspace, cx);
11567 let worktree_id = workspace
11568 .update(cx, |workspace, _window, cx| {
11569 workspace.project().update(cx, |project, cx| {
11570 project.worktrees(cx).next().unwrap().read(cx).id()
11571 })
11572 })
11573 .unwrap();
11574 let _buffer = project
11575 .update(cx, |project, cx| {
11576 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
11577 })
11578 .await
11579 .unwrap();
11580 let editor = workspace
11581 .update(cx, |workspace, window, cx| {
11582 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
11583 })
11584 .unwrap()
11585 .await
11586 .unwrap()
11587 .downcast::<Editor>()
11588 .unwrap();
11589 let fake_server = fake_servers.next().await.unwrap();
11590
11591 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
11592 let multiline_label_2 = "a\nb\nc\n";
11593 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
11594 let multiline_description = "d\ne\nf\n";
11595 let multiline_detail_2 = "g\nh\ni\n";
11596
11597 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
11598 move |params, _| async move {
11599 Ok(Some(lsp::CompletionResponse::Array(vec![
11600 lsp::CompletionItem {
11601 label: multiline_label.to_string(),
11602 text_edit: gen_text_edit(¶ms, "new_text_1"),
11603 ..lsp::CompletionItem::default()
11604 },
11605 lsp::CompletionItem {
11606 label: "single line label 1".to_string(),
11607 detail: Some(multiline_detail.to_string()),
11608 text_edit: gen_text_edit(¶ms, "new_text_2"),
11609 ..lsp::CompletionItem::default()
11610 },
11611 lsp::CompletionItem {
11612 label: "single line label 2".to_string(),
11613 label_details: Some(lsp::CompletionItemLabelDetails {
11614 description: Some(multiline_description.to_string()),
11615 detail: None,
11616 }),
11617 text_edit: gen_text_edit(¶ms, "new_text_2"),
11618 ..lsp::CompletionItem::default()
11619 },
11620 lsp::CompletionItem {
11621 label: multiline_label_2.to_string(),
11622 detail: Some(multiline_detail_2.to_string()),
11623 text_edit: gen_text_edit(¶ms, "new_text_3"),
11624 ..lsp::CompletionItem::default()
11625 },
11626 lsp::CompletionItem {
11627 label: "Label with many spaces and \t but without newlines".to_string(),
11628 detail: Some(
11629 "Details with many spaces and \t but without newlines".to_string(),
11630 ),
11631 text_edit: gen_text_edit(¶ms, "new_text_4"),
11632 ..lsp::CompletionItem::default()
11633 },
11634 ])))
11635 },
11636 );
11637
11638 editor.update_in(cx, |editor, window, cx| {
11639 cx.focus_self(window);
11640 editor.move_to_end(&MoveToEnd, window, cx);
11641 editor.handle_input(".", window, cx);
11642 });
11643 cx.run_until_parked();
11644 completion_handle.next().await.unwrap();
11645
11646 editor.update(cx, |editor, _| {
11647 assert!(editor.context_menu_visible());
11648 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11649 {
11650 let completion_labels = menu
11651 .completions
11652 .borrow()
11653 .iter()
11654 .map(|c| c.label.text.clone())
11655 .collect::<Vec<_>>();
11656 assert_eq!(
11657 completion_labels,
11658 &[
11659 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
11660 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
11661 "single line label 2 d e f ",
11662 "a b c g h i ",
11663 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
11664 ],
11665 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
11666 );
11667
11668 for completion in menu
11669 .completions
11670 .borrow()
11671 .iter() {
11672 assert_eq!(
11673 completion.label.filter_range,
11674 0..completion.label.text.len(),
11675 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
11676 );
11677 }
11678 } else {
11679 panic!("expected completion menu to be open");
11680 }
11681 });
11682}
11683
11684#[gpui::test]
11685async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
11686 init_test(cx, |_| {});
11687 let mut cx = EditorLspTestContext::new_rust(
11688 lsp::ServerCapabilities {
11689 completion_provider: Some(lsp::CompletionOptions {
11690 trigger_characters: Some(vec![".".to_string()]),
11691 ..Default::default()
11692 }),
11693 ..Default::default()
11694 },
11695 cx,
11696 )
11697 .await;
11698 cx.lsp
11699 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
11700 Ok(Some(lsp::CompletionResponse::Array(vec![
11701 lsp::CompletionItem {
11702 label: "first".into(),
11703 ..Default::default()
11704 },
11705 lsp::CompletionItem {
11706 label: "last".into(),
11707 ..Default::default()
11708 },
11709 ])))
11710 });
11711 cx.set_state("variableˇ");
11712 cx.simulate_keystroke(".");
11713 cx.executor().run_until_parked();
11714
11715 cx.update_editor(|editor, _, _| {
11716 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11717 {
11718 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
11719 } else {
11720 panic!("expected completion menu to be open");
11721 }
11722 });
11723
11724 cx.update_editor(|editor, window, cx| {
11725 editor.move_page_down(&MovePageDown::default(), window, cx);
11726 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11727 {
11728 assert!(
11729 menu.selected_item == 1,
11730 "expected PageDown to select the last item from the context menu"
11731 );
11732 } else {
11733 panic!("expected completion menu to stay open after PageDown");
11734 }
11735 });
11736
11737 cx.update_editor(|editor, window, cx| {
11738 editor.move_page_up(&MovePageUp::default(), window, cx);
11739 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11740 {
11741 assert!(
11742 menu.selected_item == 0,
11743 "expected PageUp to select the first item from the context menu"
11744 );
11745 } else {
11746 panic!("expected completion menu to stay open after PageUp");
11747 }
11748 });
11749}
11750
11751#[gpui::test]
11752async fn test_as_is_completions(cx: &mut TestAppContext) {
11753 init_test(cx, |_| {});
11754 let mut cx = EditorLspTestContext::new_rust(
11755 lsp::ServerCapabilities {
11756 completion_provider: Some(lsp::CompletionOptions {
11757 ..Default::default()
11758 }),
11759 ..Default::default()
11760 },
11761 cx,
11762 )
11763 .await;
11764 cx.lsp
11765 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
11766 Ok(Some(lsp::CompletionResponse::Array(vec![
11767 lsp::CompletionItem {
11768 label: "unsafe".into(),
11769 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11770 range: lsp::Range {
11771 start: lsp::Position {
11772 line: 1,
11773 character: 2,
11774 },
11775 end: lsp::Position {
11776 line: 1,
11777 character: 3,
11778 },
11779 },
11780 new_text: "unsafe".to_string(),
11781 })),
11782 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
11783 ..Default::default()
11784 },
11785 ])))
11786 });
11787 cx.set_state("fn a() {}\n nˇ");
11788 cx.executor().run_until_parked();
11789 cx.update_editor(|editor, window, cx| {
11790 editor.show_completions(
11791 &ShowCompletions {
11792 trigger: Some("\n".into()),
11793 },
11794 window,
11795 cx,
11796 );
11797 });
11798 cx.executor().run_until_parked();
11799
11800 cx.update_editor(|editor, window, cx| {
11801 editor.confirm_completion(&Default::default(), window, cx)
11802 });
11803 cx.executor().run_until_parked();
11804 cx.assert_editor_state("fn a() {}\n unsafeˇ");
11805}
11806
11807#[gpui::test]
11808async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
11809 init_test(cx, |_| {});
11810
11811 let mut cx = EditorLspTestContext::new_rust(
11812 lsp::ServerCapabilities {
11813 completion_provider: Some(lsp::CompletionOptions {
11814 trigger_characters: Some(vec![".".to_string()]),
11815 resolve_provider: Some(true),
11816 ..Default::default()
11817 }),
11818 ..Default::default()
11819 },
11820 cx,
11821 )
11822 .await;
11823
11824 cx.set_state("fn main() { let a = 2ˇ; }");
11825 cx.simulate_keystroke(".");
11826 let completion_item = lsp::CompletionItem {
11827 label: "Some".into(),
11828 kind: Some(lsp::CompletionItemKind::SNIPPET),
11829 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
11830 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
11831 kind: lsp::MarkupKind::Markdown,
11832 value: "```rust\nSome(2)\n```".to_string(),
11833 })),
11834 deprecated: Some(false),
11835 sort_text: Some("Some".to_string()),
11836 filter_text: Some("Some".to_string()),
11837 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
11838 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11839 range: lsp::Range {
11840 start: lsp::Position {
11841 line: 0,
11842 character: 22,
11843 },
11844 end: lsp::Position {
11845 line: 0,
11846 character: 22,
11847 },
11848 },
11849 new_text: "Some(2)".to_string(),
11850 })),
11851 additional_text_edits: Some(vec![lsp::TextEdit {
11852 range: lsp::Range {
11853 start: lsp::Position {
11854 line: 0,
11855 character: 20,
11856 },
11857 end: lsp::Position {
11858 line: 0,
11859 character: 22,
11860 },
11861 },
11862 new_text: "".to_string(),
11863 }]),
11864 ..Default::default()
11865 };
11866
11867 let closure_completion_item = completion_item.clone();
11868 let counter = Arc::new(AtomicUsize::new(0));
11869 let counter_clone = counter.clone();
11870 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
11871 let task_completion_item = closure_completion_item.clone();
11872 counter_clone.fetch_add(1, atomic::Ordering::Release);
11873 async move {
11874 Ok(Some(lsp::CompletionResponse::Array(vec![
11875 task_completion_item,
11876 ])))
11877 }
11878 });
11879
11880 cx.condition(|editor, _| editor.context_menu_visible())
11881 .await;
11882 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
11883 assert!(request.next().await.is_some());
11884 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11885
11886 cx.simulate_keystrokes("S o m");
11887 cx.condition(|editor, _| editor.context_menu_visible())
11888 .await;
11889 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
11890 assert!(request.next().await.is_some());
11891 assert!(request.next().await.is_some());
11892 assert!(request.next().await.is_some());
11893 request.close();
11894 assert!(request.next().await.is_none());
11895 assert_eq!(
11896 counter.load(atomic::Ordering::Acquire),
11897 4,
11898 "With the completions menu open, only one LSP request should happen per input"
11899 );
11900}
11901
11902#[gpui::test]
11903async fn test_toggle_comment(cx: &mut TestAppContext) {
11904 init_test(cx, |_| {});
11905 let mut cx = EditorTestContext::new(cx).await;
11906 let language = Arc::new(Language::new(
11907 LanguageConfig {
11908 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
11909 ..Default::default()
11910 },
11911 Some(tree_sitter_rust::LANGUAGE.into()),
11912 ));
11913 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
11914
11915 // If multiple selections intersect a line, the line is only toggled once.
11916 cx.set_state(indoc! {"
11917 fn a() {
11918 «//b();
11919 ˇ»// «c();
11920 //ˇ» d();
11921 }
11922 "});
11923
11924 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11925
11926 cx.assert_editor_state(indoc! {"
11927 fn a() {
11928 «b();
11929 c();
11930 ˇ» d();
11931 }
11932 "});
11933
11934 // The comment prefix is inserted at the same column for every line in a
11935 // selection.
11936 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11937
11938 cx.assert_editor_state(indoc! {"
11939 fn a() {
11940 // «b();
11941 // c();
11942 ˇ»// d();
11943 }
11944 "});
11945
11946 // If a selection ends at the beginning of a line, that line is not toggled.
11947 cx.set_selections_state(indoc! {"
11948 fn a() {
11949 // b();
11950 «// c();
11951 ˇ» // d();
11952 }
11953 "});
11954
11955 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11956
11957 cx.assert_editor_state(indoc! {"
11958 fn a() {
11959 // b();
11960 «c();
11961 ˇ» // d();
11962 }
11963 "});
11964
11965 // If a selection span a single line and is empty, the line is toggled.
11966 cx.set_state(indoc! {"
11967 fn a() {
11968 a();
11969 b();
11970 ˇ
11971 }
11972 "});
11973
11974 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11975
11976 cx.assert_editor_state(indoc! {"
11977 fn a() {
11978 a();
11979 b();
11980 //•ˇ
11981 }
11982 "});
11983
11984 // If a selection span multiple lines, empty lines are not toggled.
11985 cx.set_state(indoc! {"
11986 fn a() {
11987 «a();
11988
11989 c();ˇ»
11990 }
11991 "});
11992
11993 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11994
11995 cx.assert_editor_state(indoc! {"
11996 fn a() {
11997 // «a();
11998
11999 // c();ˇ»
12000 }
12001 "});
12002
12003 // If a selection includes multiple comment prefixes, all lines are uncommented.
12004 cx.set_state(indoc! {"
12005 fn a() {
12006 «// a();
12007 /// b();
12008 //! c();ˇ»
12009 }
12010 "});
12011
12012 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12013
12014 cx.assert_editor_state(indoc! {"
12015 fn a() {
12016 «a();
12017 b();
12018 c();ˇ»
12019 }
12020 "});
12021}
12022
12023#[gpui::test]
12024async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
12025 init_test(cx, |_| {});
12026 let mut cx = EditorTestContext::new(cx).await;
12027 let language = Arc::new(Language::new(
12028 LanguageConfig {
12029 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
12030 ..Default::default()
12031 },
12032 Some(tree_sitter_rust::LANGUAGE.into()),
12033 ));
12034 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
12035
12036 let toggle_comments = &ToggleComments {
12037 advance_downwards: false,
12038 ignore_indent: true,
12039 };
12040
12041 // If multiple selections intersect a line, the line is only toggled once.
12042 cx.set_state(indoc! {"
12043 fn a() {
12044 // «b();
12045 // c();
12046 // ˇ» d();
12047 }
12048 "});
12049
12050 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12051
12052 cx.assert_editor_state(indoc! {"
12053 fn a() {
12054 «b();
12055 c();
12056 ˇ» d();
12057 }
12058 "});
12059
12060 // The comment prefix is inserted at the beginning of each line
12061 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12062
12063 cx.assert_editor_state(indoc! {"
12064 fn a() {
12065 // «b();
12066 // c();
12067 // ˇ» d();
12068 }
12069 "});
12070
12071 // If a selection ends at the beginning of a line, that line is not toggled.
12072 cx.set_selections_state(indoc! {"
12073 fn a() {
12074 // b();
12075 // «c();
12076 ˇ»// d();
12077 }
12078 "});
12079
12080 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12081
12082 cx.assert_editor_state(indoc! {"
12083 fn a() {
12084 // b();
12085 «c();
12086 ˇ»// d();
12087 }
12088 "});
12089
12090 // If a selection span a single line and is empty, the line is toggled.
12091 cx.set_state(indoc! {"
12092 fn a() {
12093 a();
12094 b();
12095 ˇ
12096 }
12097 "});
12098
12099 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12100
12101 cx.assert_editor_state(indoc! {"
12102 fn a() {
12103 a();
12104 b();
12105 //ˇ
12106 }
12107 "});
12108
12109 // If a selection span multiple lines, empty lines are not toggled.
12110 cx.set_state(indoc! {"
12111 fn a() {
12112 «a();
12113
12114 c();ˇ»
12115 }
12116 "});
12117
12118 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12119
12120 cx.assert_editor_state(indoc! {"
12121 fn a() {
12122 // «a();
12123
12124 // c();ˇ»
12125 }
12126 "});
12127
12128 // If a selection includes multiple comment prefixes, all lines are uncommented.
12129 cx.set_state(indoc! {"
12130 fn a() {
12131 // «a();
12132 /// b();
12133 //! c();ˇ»
12134 }
12135 "});
12136
12137 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12138
12139 cx.assert_editor_state(indoc! {"
12140 fn a() {
12141 «a();
12142 b();
12143 c();ˇ»
12144 }
12145 "});
12146}
12147
12148#[gpui::test]
12149async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
12150 init_test(cx, |_| {});
12151
12152 let language = Arc::new(Language::new(
12153 LanguageConfig {
12154 line_comments: vec!["// ".into()],
12155 ..Default::default()
12156 },
12157 Some(tree_sitter_rust::LANGUAGE.into()),
12158 ));
12159
12160 let mut cx = EditorTestContext::new(cx).await;
12161
12162 cx.language_registry().add(language.clone());
12163 cx.update_buffer(|buffer, cx| {
12164 buffer.set_language(Some(language), cx);
12165 });
12166
12167 let toggle_comments = &ToggleComments {
12168 advance_downwards: true,
12169 ignore_indent: false,
12170 };
12171
12172 // Single cursor on one line -> advance
12173 // Cursor moves horizontally 3 characters as well on non-blank line
12174 cx.set_state(indoc!(
12175 "fn a() {
12176 ˇdog();
12177 cat();
12178 }"
12179 ));
12180 cx.update_editor(|editor, window, cx| {
12181 editor.toggle_comments(toggle_comments, window, cx);
12182 });
12183 cx.assert_editor_state(indoc!(
12184 "fn a() {
12185 // dog();
12186 catˇ();
12187 }"
12188 ));
12189
12190 // Single selection on one line -> don't advance
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 // Multiple cursors on one line -> advance
12208 cx.set_state(indoc!(
12209 "fn a() {
12210 ˇdˇog();
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, with selection -> don't 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 // ˇdˇog«()ˇ»;
12237 cat();
12238 }"
12239 ));
12240
12241 // Single cursor on one line -> advance
12242 // Cursor moves to column 0 on blank line
12243 cx.set_state(indoc!(
12244 "fn a() {
12245 ˇdog();
12246
12247 cat();
12248 }"
12249 ));
12250 cx.update_editor(|editor, window, cx| {
12251 editor.toggle_comments(toggle_comments, window, cx);
12252 });
12253 cx.assert_editor_state(indoc!(
12254 "fn a() {
12255 // dog();
12256 ˇ
12257 cat();
12258 }"
12259 ));
12260
12261 // Single cursor on one line -> advance
12262 // Cursor starts and ends at column 0
12263 cx.set_state(indoc!(
12264 "fn a() {
12265 ˇ dog();
12266 cat();
12267 }"
12268 ));
12269 cx.update_editor(|editor, window, cx| {
12270 editor.toggle_comments(toggle_comments, window, cx);
12271 });
12272 cx.assert_editor_state(indoc!(
12273 "fn a() {
12274 // dog();
12275 ˇ cat();
12276 }"
12277 ));
12278}
12279
12280#[gpui::test]
12281async fn test_toggle_block_comment(cx: &mut TestAppContext) {
12282 init_test(cx, |_| {});
12283
12284 let mut cx = EditorTestContext::new(cx).await;
12285
12286 let html_language = Arc::new(
12287 Language::new(
12288 LanguageConfig {
12289 name: "HTML".into(),
12290 block_comment: Some(("<!-- ".into(), " -->".into())),
12291 ..Default::default()
12292 },
12293 Some(tree_sitter_html::LANGUAGE.into()),
12294 )
12295 .with_injection_query(
12296 r#"
12297 (script_element
12298 (raw_text) @injection.content
12299 (#set! injection.language "javascript"))
12300 "#,
12301 )
12302 .unwrap(),
12303 );
12304
12305 let javascript_language = Arc::new(Language::new(
12306 LanguageConfig {
12307 name: "JavaScript".into(),
12308 line_comments: vec!["// ".into()],
12309 ..Default::default()
12310 },
12311 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
12312 ));
12313
12314 cx.language_registry().add(html_language.clone());
12315 cx.language_registry().add(javascript_language.clone());
12316 cx.update_buffer(|buffer, cx| {
12317 buffer.set_language(Some(html_language), cx);
12318 });
12319
12320 // Toggle comments for empty selections
12321 cx.set_state(
12322 &r#"
12323 <p>A</p>ˇ
12324 <p>B</p>ˇ
12325 <p>C</p>ˇ
12326 "#
12327 .unindent(),
12328 );
12329 cx.update_editor(|editor, window, cx| {
12330 editor.toggle_comments(&ToggleComments::default(), window, cx)
12331 });
12332 cx.assert_editor_state(
12333 &r#"
12334 <!-- <p>A</p>ˇ -->
12335 <!-- <p>B</p>ˇ -->
12336 <!-- <p>C</p>ˇ -->
12337 "#
12338 .unindent(),
12339 );
12340 cx.update_editor(|editor, window, cx| {
12341 editor.toggle_comments(&ToggleComments::default(), window, cx)
12342 });
12343 cx.assert_editor_state(
12344 &r#"
12345 <p>A</p>ˇ
12346 <p>B</p>ˇ
12347 <p>C</p>ˇ
12348 "#
12349 .unindent(),
12350 );
12351
12352 // Toggle comments for mixture of empty and non-empty selections, where
12353 // multiple selections occupy a given line.
12354 cx.set_state(
12355 &r#"
12356 <p>A«</p>
12357 <p>ˇ»B</p>ˇ
12358 <p>C«</p>
12359 <p>ˇ»D</p>ˇ
12360 "#
12361 .unindent(),
12362 );
12363
12364 cx.update_editor(|editor, window, cx| {
12365 editor.toggle_comments(&ToggleComments::default(), window, cx)
12366 });
12367 cx.assert_editor_state(
12368 &r#"
12369 <!-- <p>A«</p>
12370 <p>ˇ»B</p>ˇ -->
12371 <!-- <p>C«</p>
12372 <p>ˇ»D</p>ˇ -->
12373 "#
12374 .unindent(),
12375 );
12376 cx.update_editor(|editor, window, cx| {
12377 editor.toggle_comments(&ToggleComments::default(), window, cx)
12378 });
12379 cx.assert_editor_state(
12380 &r#"
12381 <p>A«</p>
12382 <p>ˇ»B</p>ˇ
12383 <p>C«</p>
12384 <p>ˇ»D</p>ˇ
12385 "#
12386 .unindent(),
12387 );
12388
12389 // Toggle comments when different languages are active for different
12390 // selections.
12391 cx.set_state(
12392 &r#"
12393 ˇ<script>
12394 ˇvar x = new Y();
12395 ˇ</script>
12396 "#
12397 .unindent(),
12398 );
12399 cx.executor().run_until_parked();
12400 cx.update_editor(|editor, window, cx| {
12401 editor.toggle_comments(&ToggleComments::default(), window, cx)
12402 });
12403 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
12404 // Uncommenting and commenting from this position brings in even more wrong artifacts.
12405 cx.assert_editor_state(
12406 &r#"
12407 <!-- ˇ<script> -->
12408 // ˇvar x = new Y();
12409 <!-- ˇ</script> -->
12410 "#
12411 .unindent(),
12412 );
12413}
12414
12415#[gpui::test]
12416fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
12417 init_test(cx, |_| {});
12418
12419 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
12420 let multibuffer = cx.new(|cx| {
12421 let mut multibuffer = MultiBuffer::new(ReadWrite);
12422 multibuffer.push_excerpts(
12423 buffer.clone(),
12424 [
12425 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
12426 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
12427 ],
12428 cx,
12429 );
12430 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
12431 multibuffer
12432 });
12433
12434 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
12435 editor.update_in(cx, |editor, window, cx| {
12436 assert_eq!(editor.text(cx), "aaaa\nbbbb");
12437 editor.change_selections(None, window, cx, |s| {
12438 s.select_ranges([
12439 Point::new(0, 0)..Point::new(0, 0),
12440 Point::new(1, 0)..Point::new(1, 0),
12441 ])
12442 });
12443
12444 editor.handle_input("X", window, cx);
12445 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
12446 assert_eq!(
12447 editor.selections.ranges(cx),
12448 [
12449 Point::new(0, 1)..Point::new(0, 1),
12450 Point::new(1, 1)..Point::new(1, 1),
12451 ]
12452 );
12453
12454 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
12455 editor.change_selections(None, window, cx, |s| {
12456 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
12457 });
12458 editor.backspace(&Default::default(), window, cx);
12459 assert_eq!(editor.text(cx), "Xa\nbbb");
12460 assert_eq!(
12461 editor.selections.ranges(cx),
12462 [Point::new(1, 0)..Point::new(1, 0)]
12463 );
12464
12465 editor.change_selections(None, window, cx, |s| {
12466 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
12467 });
12468 editor.backspace(&Default::default(), window, cx);
12469 assert_eq!(editor.text(cx), "X\nbb");
12470 assert_eq!(
12471 editor.selections.ranges(cx),
12472 [Point::new(0, 1)..Point::new(0, 1)]
12473 );
12474 });
12475}
12476
12477#[gpui::test]
12478fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
12479 init_test(cx, |_| {});
12480
12481 let markers = vec![('[', ']').into(), ('(', ')').into()];
12482 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
12483 indoc! {"
12484 [aaaa
12485 (bbbb]
12486 cccc)",
12487 },
12488 markers.clone(),
12489 );
12490 let excerpt_ranges = markers.into_iter().map(|marker| {
12491 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
12492 ExcerptRange::new(context.clone())
12493 });
12494 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
12495 let multibuffer = cx.new(|cx| {
12496 let mut multibuffer = MultiBuffer::new(ReadWrite);
12497 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
12498 multibuffer
12499 });
12500
12501 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
12502 editor.update_in(cx, |editor, window, cx| {
12503 let (expected_text, selection_ranges) = marked_text_ranges(
12504 indoc! {"
12505 aaaa
12506 bˇbbb
12507 bˇbbˇb
12508 cccc"
12509 },
12510 true,
12511 );
12512 assert_eq!(editor.text(cx), expected_text);
12513 editor.change_selections(None, window, cx, |s| s.select_ranges(selection_ranges));
12514
12515 editor.handle_input("X", window, cx);
12516
12517 let (expected_text, expected_selections) = marked_text_ranges(
12518 indoc! {"
12519 aaaa
12520 bXˇbbXb
12521 bXˇbbXˇb
12522 cccc"
12523 },
12524 false,
12525 );
12526 assert_eq!(editor.text(cx), expected_text);
12527 assert_eq!(editor.selections.ranges(cx), expected_selections);
12528
12529 editor.newline(&Newline, window, cx);
12530 let (expected_text, expected_selections) = marked_text_ranges(
12531 indoc! {"
12532 aaaa
12533 bX
12534 ˇbbX
12535 b
12536 bX
12537 ˇbbX
12538 ˇ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}
12547
12548#[gpui::test]
12549fn test_refresh_selections(cx: &mut TestAppContext) {
12550 init_test(cx, |_| {});
12551
12552 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
12553 let mut excerpt1_id = None;
12554 let multibuffer = cx.new(|cx| {
12555 let mut multibuffer = MultiBuffer::new(ReadWrite);
12556 excerpt1_id = multibuffer
12557 .push_excerpts(
12558 buffer.clone(),
12559 [
12560 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
12561 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
12562 ],
12563 cx,
12564 )
12565 .into_iter()
12566 .next();
12567 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
12568 multibuffer
12569 });
12570
12571 let editor = cx.add_window(|window, cx| {
12572 let mut editor = build_editor(multibuffer.clone(), window, cx);
12573 let snapshot = editor.snapshot(window, cx);
12574 editor.change_selections(None, window, cx, |s| {
12575 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
12576 });
12577 editor.begin_selection(
12578 Point::new(2, 1).to_display_point(&snapshot),
12579 true,
12580 1,
12581 window,
12582 cx,
12583 );
12584 assert_eq!(
12585 editor.selections.ranges(cx),
12586 [
12587 Point::new(1, 3)..Point::new(1, 3),
12588 Point::new(2, 1)..Point::new(2, 1),
12589 ]
12590 );
12591 editor
12592 });
12593
12594 // Refreshing selections is a no-op when excerpts haven't changed.
12595 _ = editor.update(cx, |editor, window, cx| {
12596 editor.change_selections(None, window, cx, |s| s.refresh());
12597 assert_eq!(
12598 editor.selections.ranges(cx),
12599 [
12600 Point::new(1, 3)..Point::new(1, 3),
12601 Point::new(2, 1)..Point::new(2, 1),
12602 ]
12603 );
12604 });
12605
12606 multibuffer.update(cx, |multibuffer, cx| {
12607 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
12608 });
12609 _ = editor.update(cx, |editor, window, cx| {
12610 // Removing an excerpt causes the first selection to become degenerate.
12611 assert_eq!(
12612 editor.selections.ranges(cx),
12613 [
12614 Point::new(0, 0)..Point::new(0, 0),
12615 Point::new(0, 1)..Point::new(0, 1)
12616 ]
12617 );
12618
12619 // Refreshing selections will relocate the first selection to the original buffer
12620 // location.
12621 editor.change_selections(None, window, cx, |s| s.refresh());
12622 assert_eq!(
12623 editor.selections.ranges(cx),
12624 [
12625 Point::new(0, 1)..Point::new(0, 1),
12626 Point::new(0, 3)..Point::new(0, 3)
12627 ]
12628 );
12629 assert!(editor.selections.pending_anchor().is_some());
12630 });
12631}
12632
12633#[gpui::test]
12634fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
12635 init_test(cx, |_| {});
12636
12637 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
12638 let mut excerpt1_id = None;
12639 let multibuffer = cx.new(|cx| {
12640 let mut multibuffer = MultiBuffer::new(ReadWrite);
12641 excerpt1_id = multibuffer
12642 .push_excerpts(
12643 buffer.clone(),
12644 [
12645 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
12646 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
12647 ],
12648 cx,
12649 )
12650 .into_iter()
12651 .next();
12652 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
12653 multibuffer
12654 });
12655
12656 let editor = cx.add_window(|window, cx| {
12657 let mut editor = build_editor(multibuffer.clone(), window, cx);
12658 let snapshot = editor.snapshot(window, cx);
12659 editor.begin_selection(
12660 Point::new(1, 3).to_display_point(&snapshot),
12661 false,
12662 1,
12663 window,
12664 cx,
12665 );
12666 assert_eq!(
12667 editor.selections.ranges(cx),
12668 [Point::new(1, 3)..Point::new(1, 3)]
12669 );
12670 editor
12671 });
12672
12673 multibuffer.update(cx, |multibuffer, cx| {
12674 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
12675 });
12676 _ = editor.update(cx, |editor, window, cx| {
12677 assert_eq!(
12678 editor.selections.ranges(cx),
12679 [Point::new(0, 0)..Point::new(0, 0)]
12680 );
12681
12682 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
12683 editor.change_selections(None, window, cx, |s| s.refresh());
12684 assert_eq!(
12685 editor.selections.ranges(cx),
12686 [Point::new(0, 3)..Point::new(0, 3)]
12687 );
12688 assert!(editor.selections.pending_anchor().is_some());
12689 });
12690}
12691
12692#[gpui::test]
12693async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
12694 init_test(cx, |_| {});
12695
12696 let language = Arc::new(
12697 Language::new(
12698 LanguageConfig {
12699 brackets: BracketPairConfig {
12700 pairs: vec![
12701 BracketPair {
12702 start: "{".to_string(),
12703 end: "}".to_string(),
12704 close: true,
12705 surround: true,
12706 newline: true,
12707 },
12708 BracketPair {
12709 start: "/* ".to_string(),
12710 end: " */".to_string(),
12711 close: true,
12712 surround: true,
12713 newline: true,
12714 },
12715 ],
12716 ..Default::default()
12717 },
12718 ..Default::default()
12719 },
12720 Some(tree_sitter_rust::LANGUAGE.into()),
12721 )
12722 .with_indents_query("")
12723 .unwrap(),
12724 );
12725
12726 let text = concat!(
12727 "{ }\n", //
12728 " x\n", //
12729 " /* */\n", //
12730 "x\n", //
12731 "{{} }\n", //
12732 );
12733
12734 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
12735 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12736 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
12737 editor
12738 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
12739 .await;
12740
12741 editor.update_in(cx, |editor, window, cx| {
12742 editor.change_selections(None, window, cx, |s| {
12743 s.select_display_ranges([
12744 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
12745 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
12746 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
12747 ])
12748 });
12749 editor.newline(&Newline, window, cx);
12750
12751 assert_eq!(
12752 editor.buffer().read(cx).read(cx).text(),
12753 concat!(
12754 "{ \n", // Suppress rustfmt
12755 "\n", //
12756 "}\n", //
12757 " x\n", //
12758 " /* \n", //
12759 " \n", //
12760 " */\n", //
12761 "x\n", //
12762 "{{} \n", //
12763 "}\n", //
12764 )
12765 );
12766 });
12767}
12768
12769#[gpui::test]
12770fn test_highlighted_ranges(cx: &mut TestAppContext) {
12771 init_test(cx, |_| {});
12772
12773 let editor = cx.add_window(|window, cx| {
12774 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
12775 build_editor(buffer.clone(), window, cx)
12776 });
12777
12778 _ = editor.update(cx, |editor, window, cx| {
12779 struct Type1;
12780 struct Type2;
12781
12782 let buffer = editor.buffer.read(cx).snapshot(cx);
12783
12784 let anchor_range =
12785 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
12786
12787 editor.highlight_background::<Type1>(
12788 &[
12789 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
12790 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
12791 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
12792 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
12793 ],
12794 |_| Hsla::red(),
12795 cx,
12796 );
12797 editor.highlight_background::<Type2>(
12798 &[
12799 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
12800 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
12801 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
12802 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
12803 ],
12804 |_| Hsla::green(),
12805 cx,
12806 );
12807
12808 let snapshot = editor.snapshot(window, cx);
12809 let mut highlighted_ranges = editor.background_highlights_in_range(
12810 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
12811 &snapshot,
12812 cx.theme().colors(),
12813 );
12814 // Enforce a consistent ordering based on color without relying on the ordering of the
12815 // highlight's `TypeId` which is non-executor.
12816 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
12817 assert_eq!(
12818 highlighted_ranges,
12819 &[
12820 (
12821 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
12822 Hsla::red(),
12823 ),
12824 (
12825 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
12826 Hsla::red(),
12827 ),
12828 (
12829 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
12830 Hsla::green(),
12831 ),
12832 (
12833 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
12834 Hsla::green(),
12835 ),
12836 ]
12837 );
12838 assert_eq!(
12839 editor.background_highlights_in_range(
12840 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
12841 &snapshot,
12842 cx.theme().colors(),
12843 ),
12844 &[(
12845 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
12846 Hsla::red(),
12847 )]
12848 );
12849 });
12850}
12851
12852#[gpui::test]
12853async fn test_following(cx: &mut TestAppContext) {
12854 init_test(cx, |_| {});
12855
12856 let fs = FakeFs::new(cx.executor());
12857 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
12858
12859 let buffer = project.update(cx, |project, cx| {
12860 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
12861 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
12862 });
12863 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
12864 let follower = cx.update(|cx| {
12865 cx.open_window(
12866 WindowOptions {
12867 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
12868 gpui::Point::new(px(0.), px(0.)),
12869 gpui::Point::new(px(10.), px(80.)),
12870 ))),
12871 ..Default::default()
12872 },
12873 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
12874 )
12875 .unwrap()
12876 });
12877
12878 let is_still_following = Rc::new(RefCell::new(true));
12879 let follower_edit_event_count = Rc::new(RefCell::new(0));
12880 let pending_update = Rc::new(RefCell::new(None));
12881 let leader_entity = leader.root(cx).unwrap();
12882 let follower_entity = follower.root(cx).unwrap();
12883 _ = follower.update(cx, {
12884 let update = pending_update.clone();
12885 let is_still_following = is_still_following.clone();
12886 let follower_edit_event_count = follower_edit_event_count.clone();
12887 |_, window, cx| {
12888 cx.subscribe_in(
12889 &leader_entity,
12890 window,
12891 move |_, leader, event, window, cx| {
12892 leader.read(cx).add_event_to_update_proto(
12893 event,
12894 &mut update.borrow_mut(),
12895 window,
12896 cx,
12897 );
12898 },
12899 )
12900 .detach();
12901
12902 cx.subscribe_in(
12903 &follower_entity,
12904 window,
12905 move |_, _, event: &EditorEvent, _window, _cx| {
12906 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
12907 *is_still_following.borrow_mut() = false;
12908 }
12909
12910 if let EditorEvent::BufferEdited = event {
12911 *follower_edit_event_count.borrow_mut() += 1;
12912 }
12913 },
12914 )
12915 .detach();
12916 }
12917 });
12918
12919 // Update the selections only
12920 _ = leader.update(cx, |leader, window, cx| {
12921 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
12922 });
12923 follower
12924 .update(cx, |follower, window, cx| {
12925 follower.apply_update_proto(
12926 &project,
12927 pending_update.borrow_mut().take().unwrap(),
12928 window,
12929 cx,
12930 )
12931 })
12932 .unwrap()
12933 .await
12934 .unwrap();
12935 _ = follower.update(cx, |follower, _, cx| {
12936 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
12937 });
12938 assert!(*is_still_following.borrow());
12939 assert_eq!(*follower_edit_event_count.borrow(), 0);
12940
12941 // Update the scroll position only
12942 _ = leader.update(cx, |leader, window, cx| {
12943 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
12944 });
12945 follower
12946 .update(cx, |follower, window, cx| {
12947 follower.apply_update_proto(
12948 &project,
12949 pending_update.borrow_mut().take().unwrap(),
12950 window,
12951 cx,
12952 )
12953 })
12954 .unwrap()
12955 .await
12956 .unwrap();
12957 assert_eq!(
12958 follower
12959 .update(cx, |follower, _, cx| follower.scroll_position(cx))
12960 .unwrap(),
12961 gpui::Point::new(1.5, 3.5)
12962 );
12963 assert!(*is_still_following.borrow());
12964 assert_eq!(*follower_edit_event_count.borrow(), 0);
12965
12966 // Update the selections and scroll position. The follower's scroll position is updated
12967 // via autoscroll, not via the leader's exact scroll position.
12968 _ = leader.update(cx, |leader, window, cx| {
12969 leader.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
12970 leader.request_autoscroll(Autoscroll::newest(), cx);
12971 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
12972 });
12973 follower
12974 .update(cx, |follower, window, cx| {
12975 follower.apply_update_proto(
12976 &project,
12977 pending_update.borrow_mut().take().unwrap(),
12978 window,
12979 cx,
12980 )
12981 })
12982 .unwrap()
12983 .await
12984 .unwrap();
12985 _ = follower.update(cx, |follower, _, cx| {
12986 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
12987 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
12988 });
12989 assert!(*is_still_following.borrow());
12990
12991 // Creating a pending selection that precedes another selection
12992 _ = leader.update(cx, |leader, window, cx| {
12993 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
12994 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
12995 });
12996 follower
12997 .update(cx, |follower, window, cx| {
12998 follower.apply_update_proto(
12999 &project,
13000 pending_update.borrow_mut().take().unwrap(),
13001 window,
13002 cx,
13003 )
13004 })
13005 .unwrap()
13006 .await
13007 .unwrap();
13008 _ = follower.update(cx, |follower, _, cx| {
13009 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
13010 });
13011 assert!(*is_still_following.borrow());
13012
13013 // Extend the pending selection so that it surrounds another selection
13014 _ = leader.update(cx, |leader, window, cx| {
13015 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
13016 });
13017 follower
13018 .update(cx, |follower, window, cx| {
13019 follower.apply_update_proto(
13020 &project,
13021 pending_update.borrow_mut().take().unwrap(),
13022 window,
13023 cx,
13024 )
13025 })
13026 .unwrap()
13027 .await
13028 .unwrap();
13029 _ = follower.update(cx, |follower, _, cx| {
13030 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
13031 });
13032
13033 // Scrolling locally breaks the follow
13034 _ = follower.update(cx, |follower, window, cx| {
13035 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
13036 follower.set_scroll_anchor(
13037 ScrollAnchor {
13038 anchor: top_anchor,
13039 offset: gpui::Point::new(0.0, 0.5),
13040 },
13041 window,
13042 cx,
13043 );
13044 });
13045 assert!(!(*is_still_following.borrow()));
13046}
13047
13048#[gpui::test]
13049async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
13050 init_test(cx, |_| {});
13051
13052 let fs = FakeFs::new(cx.executor());
13053 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
13054 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13055 let pane = workspace
13056 .update(cx, |workspace, _, _| workspace.active_pane().clone())
13057 .unwrap();
13058
13059 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
13060
13061 let leader = pane.update_in(cx, |_, window, cx| {
13062 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
13063 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
13064 });
13065
13066 // Start following the editor when it has no excerpts.
13067 let mut state_message =
13068 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
13069 let workspace_entity = workspace.root(cx).unwrap();
13070 let follower_1 = cx
13071 .update_window(*workspace.deref(), |_, window, cx| {
13072 Editor::from_state_proto(
13073 workspace_entity,
13074 ViewId {
13075 creator: CollaboratorId::PeerId(PeerId::default()),
13076 id: 0,
13077 },
13078 &mut state_message,
13079 window,
13080 cx,
13081 )
13082 })
13083 .unwrap()
13084 .unwrap()
13085 .await
13086 .unwrap();
13087
13088 let update_message = Rc::new(RefCell::new(None));
13089 follower_1.update_in(cx, {
13090 let update = update_message.clone();
13091 |_, window, cx| {
13092 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
13093 leader.read(cx).add_event_to_update_proto(
13094 event,
13095 &mut update.borrow_mut(),
13096 window,
13097 cx,
13098 );
13099 })
13100 .detach();
13101 }
13102 });
13103
13104 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
13105 (
13106 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
13107 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
13108 )
13109 });
13110
13111 // Insert some excerpts.
13112 leader.update(cx, |leader, cx| {
13113 leader.buffer.update(cx, |multibuffer, cx| {
13114 multibuffer.set_excerpts_for_path(
13115 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
13116 buffer_1.clone(),
13117 vec![
13118 Point::row_range(0..3),
13119 Point::row_range(1..6),
13120 Point::row_range(12..15),
13121 ],
13122 0,
13123 cx,
13124 );
13125 multibuffer.set_excerpts_for_path(
13126 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
13127 buffer_2.clone(),
13128 vec![Point::row_range(0..6), Point::row_range(8..12)],
13129 0,
13130 cx,
13131 );
13132 });
13133 });
13134
13135 // Apply the update of adding the excerpts.
13136 follower_1
13137 .update_in(cx, |follower, window, cx| {
13138 follower.apply_update_proto(
13139 &project,
13140 update_message.borrow().clone().unwrap(),
13141 window,
13142 cx,
13143 )
13144 })
13145 .await
13146 .unwrap();
13147 assert_eq!(
13148 follower_1.update(cx, |editor, cx| editor.text(cx)),
13149 leader.update(cx, |editor, cx| editor.text(cx))
13150 );
13151 update_message.borrow_mut().take();
13152
13153 // Start following separately after it already has excerpts.
13154 let mut state_message =
13155 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
13156 let workspace_entity = workspace.root(cx).unwrap();
13157 let follower_2 = cx
13158 .update_window(*workspace.deref(), |_, window, cx| {
13159 Editor::from_state_proto(
13160 workspace_entity,
13161 ViewId {
13162 creator: CollaboratorId::PeerId(PeerId::default()),
13163 id: 0,
13164 },
13165 &mut state_message,
13166 window,
13167 cx,
13168 )
13169 })
13170 .unwrap()
13171 .unwrap()
13172 .await
13173 .unwrap();
13174 assert_eq!(
13175 follower_2.update(cx, |editor, cx| editor.text(cx)),
13176 leader.update(cx, |editor, cx| editor.text(cx))
13177 );
13178
13179 // Remove some excerpts.
13180 leader.update(cx, |leader, cx| {
13181 leader.buffer.update(cx, |multibuffer, cx| {
13182 let excerpt_ids = multibuffer.excerpt_ids();
13183 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
13184 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
13185 });
13186 });
13187
13188 // Apply the update of removing the excerpts.
13189 follower_1
13190 .update_in(cx, |follower, window, cx| {
13191 follower.apply_update_proto(
13192 &project,
13193 update_message.borrow().clone().unwrap(),
13194 window,
13195 cx,
13196 )
13197 })
13198 .await
13199 .unwrap();
13200 follower_2
13201 .update_in(cx, |follower, window, cx| {
13202 follower.apply_update_proto(
13203 &project,
13204 update_message.borrow().clone().unwrap(),
13205 window,
13206 cx,
13207 )
13208 })
13209 .await
13210 .unwrap();
13211 update_message.borrow_mut().take();
13212 assert_eq!(
13213 follower_1.update(cx, |editor, cx| editor.text(cx)),
13214 leader.update(cx, |editor, cx| editor.text(cx))
13215 );
13216}
13217
13218#[gpui::test]
13219async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
13220 init_test(cx, |_| {});
13221
13222 let mut cx = EditorTestContext::new(cx).await;
13223 let lsp_store =
13224 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
13225
13226 cx.set_state(indoc! {"
13227 ˇfn func(abc def: i32) -> u32 {
13228 }
13229 "});
13230
13231 cx.update(|_, cx| {
13232 lsp_store.update(cx, |lsp_store, cx| {
13233 lsp_store
13234 .update_diagnostics(
13235 LanguageServerId(0),
13236 lsp::PublishDiagnosticsParams {
13237 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
13238 version: None,
13239 diagnostics: vec![
13240 lsp::Diagnostic {
13241 range: lsp::Range::new(
13242 lsp::Position::new(0, 11),
13243 lsp::Position::new(0, 12),
13244 ),
13245 severity: Some(lsp::DiagnosticSeverity::ERROR),
13246 ..Default::default()
13247 },
13248 lsp::Diagnostic {
13249 range: lsp::Range::new(
13250 lsp::Position::new(0, 12),
13251 lsp::Position::new(0, 15),
13252 ),
13253 severity: Some(lsp::DiagnosticSeverity::ERROR),
13254 ..Default::default()
13255 },
13256 lsp::Diagnostic {
13257 range: lsp::Range::new(
13258 lsp::Position::new(0, 25),
13259 lsp::Position::new(0, 28),
13260 ),
13261 severity: Some(lsp::DiagnosticSeverity::ERROR),
13262 ..Default::default()
13263 },
13264 ],
13265 },
13266 &[],
13267 cx,
13268 )
13269 .unwrap()
13270 });
13271 });
13272
13273 executor.run_until_parked();
13274
13275 cx.update_editor(|editor, window, cx| {
13276 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13277 });
13278
13279 cx.assert_editor_state(indoc! {"
13280 fn func(abc def: i32) -> ˇu32 {
13281 }
13282 "});
13283
13284 cx.update_editor(|editor, window, cx| {
13285 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13286 });
13287
13288 cx.assert_editor_state(indoc! {"
13289 fn func(abc ˇdef: i32) -> u32 {
13290 }
13291 "});
13292
13293 cx.update_editor(|editor, window, cx| {
13294 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13295 });
13296
13297 cx.assert_editor_state(indoc! {"
13298 fn func(abcˇ def: i32) -> u32 {
13299 }
13300 "});
13301
13302 cx.update_editor(|editor, window, cx| {
13303 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13304 });
13305
13306 cx.assert_editor_state(indoc! {"
13307 fn func(abc def: i32) -> ˇu32 {
13308 }
13309 "});
13310}
13311
13312#[gpui::test]
13313async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
13314 init_test(cx, |_| {});
13315
13316 let mut cx = EditorTestContext::new(cx).await;
13317
13318 let diff_base = r#"
13319 use some::mod;
13320
13321 const A: u32 = 42;
13322
13323 fn main() {
13324 println!("hello");
13325
13326 println!("world");
13327 }
13328 "#
13329 .unindent();
13330
13331 // Edits are modified, removed, modified, added
13332 cx.set_state(
13333 &r#"
13334 use some::modified;
13335
13336 ˇ
13337 fn main() {
13338 println!("hello there");
13339
13340 println!("around the");
13341 println!("world");
13342 }
13343 "#
13344 .unindent(),
13345 );
13346
13347 cx.set_head_text(&diff_base);
13348 executor.run_until_parked();
13349
13350 cx.update_editor(|editor, window, cx| {
13351 //Wrap around the bottom of the buffer
13352 for _ in 0..3 {
13353 editor.go_to_next_hunk(&GoToHunk, window, cx);
13354 }
13355 });
13356
13357 cx.assert_editor_state(
13358 &r#"
13359 ˇuse some::modified;
13360
13361
13362 fn main() {
13363 println!("hello there");
13364
13365 println!("around the");
13366 println!("world");
13367 }
13368 "#
13369 .unindent(),
13370 );
13371
13372 cx.update_editor(|editor, window, cx| {
13373 //Wrap around the top of the buffer
13374 for _ in 0..2 {
13375 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13376 }
13377 });
13378
13379 cx.assert_editor_state(
13380 &r#"
13381 use some::modified;
13382
13383
13384 fn main() {
13385 ˇ println!("hello there");
13386
13387 println!("around the");
13388 println!("world");
13389 }
13390 "#
13391 .unindent(),
13392 );
13393
13394 cx.update_editor(|editor, window, cx| {
13395 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13396 });
13397
13398 cx.assert_editor_state(
13399 &r#"
13400 use some::modified;
13401
13402 ˇ
13403 fn main() {
13404 println!("hello there");
13405
13406 println!("around the");
13407 println!("world");
13408 }
13409 "#
13410 .unindent(),
13411 );
13412
13413 cx.update_editor(|editor, window, cx| {
13414 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13415 });
13416
13417 cx.assert_editor_state(
13418 &r#"
13419 ˇuse some::modified;
13420
13421
13422 fn main() {
13423 println!("hello there");
13424
13425 println!("around the");
13426 println!("world");
13427 }
13428 "#
13429 .unindent(),
13430 );
13431
13432 cx.update_editor(|editor, window, cx| {
13433 for _ in 0..2 {
13434 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13435 }
13436 });
13437
13438 cx.assert_editor_state(
13439 &r#"
13440 use some::modified;
13441
13442
13443 fn main() {
13444 ˇ println!("hello there");
13445
13446 println!("around the");
13447 println!("world");
13448 }
13449 "#
13450 .unindent(),
13451 );
13452
13453 cx.update_editor(|editor, window, cx| {
13454 editor.fold(&Fold, window, cx);
13455 });
13456
13457 cx.update_editor(|editor, window, cx| {
13458 editor.go_to_next_hunk(&GoToHunk, window, cx);
13459 });
13460
13461 cx.assert_editor_state(
13462 &r#"
13463 ˇuse some::modified;
13464
13465
13466 fn main() {
13467 println!("hello there");
13468
13469 println!("around the");
13470 println!("world");
13471 }
13472 "#
13473 .unindent(),
13474 );
13475}
13476
13477#[test]
13478fn test_split_words() {
13479 fn split(text: &str) -> Vec<&str> {
13480 split_words(text).collect()
13481 }
13482
13483 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
13484 assert_eq!(split("hello_world"), &["hello_", "world"]);
13485 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
13486 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
13487 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
13488 assert_eq!(split("helloworld"), &["helloworld"]);
13489
13490 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
13491}
13492
13493#[gpui::test]
13494async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
13495 init_test(cx, |_| {});
13496
13497 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
13498 let mut assert = |before, after| {
13499 let _state_context = cx.set_state(before);
13500 cx.run_until_parked();
13501 cx.update_editor(|editor, window, cx| {
13502 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
13503 });
13504 cx.run_until_parked();
13505 cx.assert_editor_state(after);
13506 };
13507
13508 // Outside bracket jumps to outside of matching bracket
13509 assert("console.logˇ(var);", "console.log(var)ˇ;");
13510 assert("console.log(var)ˇ;", "console.logˇ(var);");
13511
13512 // Inside bracket jumps to inside of matching bracket
13513 assert("console.log(ˇvar);", "console.log(varˇ);");
13514 assert("console.log(varˇ);", "console.log(ˇvar);");
13515
13516 // When outside a bracket and inside, favor jumping to the inside bracket
13517 assert(
13518 "console.log('foo', [1, 2, 3]ˇ);",
13519 "console.log(ˇ'foo', [1, 2, 3]);",
13520 );
13521 assert(
13522 "console.log(ˇ'foo', [1, 2, 3]);",
13523 "console.log('foo', [1, 2, 3]ˇ);",
13524 );
13525
13526 // Bias forward if two options are equally likely
13527 assert(
13528 "let result = curried_fun()ˇ();",
13529 "let result = curried_fun()()ˇ;",
13530 );
13531
13532 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
13533 assert(
13534 indoc! {"
13535 function test() {
13536 console.log('test')ˇ
13537 }"},
13538 indoc! {"
13539 function test() {
13540 console.logˇ('test')
13541 }"},
13542 );
13543}
13544
13545#[gpui::test]
13546async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
13547 init_test(cx, |_| {});
13548
13549 let fs = FakeFs::new(cx.executor());
13550 fs.insert_tree(
13551 path!("/a"),
13552 json!({
13553 "main.rs": "fn main() { let a = 5; }",
13554 "other.rs": "// Test file",
13555 }),
13556 )
13557 .await;
13558 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13559
13560 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13561 language_registry.add(Arc::new(Language::new(
13562 LanguageConfig {
13563 name: "Rust".into(),
13564 matcher: LanguageMatcher {
13565 path_suffixes: vec!["rs".to_string()],
13566 ..Default::default()
13567 },
13568 brackets: BracketPairConfig {
13569 pairs: vec![BracketPair {
13570 start: "{".to_string(),
13571 end: "}".to_string(),
13572 close: true,
13573 surround: true,
13574 newline: true,
13575 }],
13576 disabled_scopes_by_bracket_ix: Vec::new(),
13577 },
13578 ..Default::default()
13579 },
13580 Some(tree_sitter_rust::LANGUAGE.into()),
13581 )));
13582 let mut fake_servers = language_registry.register_fake_lsp(
13583 "Rust",
13584 FakeLspAdapter {
13585 capabilities: lsp::ServerCapabilities {
13586 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
13587 first_trigger_character: "{".to_string(),
13588 more_trigger_character: None,
13589 }),
13590 ..Default::default()
13591 },
13592 ..Default::default()
13593 },
13594 );
13595
13596 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13597
13598 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13599
13600 let worktree_id = workspace
13601 .update(cx, |workspace, _, cx| {
13602 workspace.project().update(cx, |project, cx| {
13603 project.worktrees(cx).next().unwrap().read(cx).id()
13604 })
13605 })
13606 .unwrap();
13607
13608 let buffer = project
13609 .update(cx, |project, cx| {
13610 project.open_local_buffer(path!("/a/main.rs"), cx)
13611 })
13612 .await
13613 .unwrap();
13614 let editor_handle = workspace
13615 .update(cx, |workspace, window, cx| {
13616 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
13617 })
13618 .unwrap()
13619 .await
13620 .unwrap()
13621 .downcast::<Editor>()
13622 .unwrap();
13623
13624 cx.executor().start_waiting();
13625 let fake_server = fake_servers.next().await.unwrap();
13626
13627 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
13628 |params, _| async move {
13629 assert_eq!(
13630 params.text_document_position.text_document.uri,
13631 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
13632 );
13633 assert_eq!(
13634 params.text_document_position.position,
13635 lsp::Position::new(0, 21),
13636 );
13637
13638 Ok(Some(vec![lsp::TextEdit {
13639 new_text: "]".to_string(),
13640 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13641 }]))
13642 },
13643 );
13644
13645 editor_handle.update_in(cx, |editor, window, cx| {
13646 window.focus(&editor.focus_handle(cx));
13647 editor.change_selections(None, window, cx, |s| {
13648 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
13649 });
13650 editor.handle_input("{", window, cx);
13651 });
13652
13653 cx.executor().run_until_parked();
13654
13655 buffer.update(cx, |buffer, _| {
13656 assert_eq!(
13657 buffer.text(),
13658 "fn main() { let a = {5}; }",
13659 "No extra braces from on type formatting should appear in the buffer"
13660 )
13661 });
13662}
13663
13664#[gpui::test]
13665async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
13666 init_test(cx, |_| {});
13667
13668 let fs = FakeFs::new(cx.executor());
13669 fs.insert_tree(
13670 path!("/a"),
13671 json!({
13672 "main.rs": "fn main() { let a = 5; }",
13673 "other.rs": "// Test file",
13674 }),
13675 )
13676 .await;
13677
13678 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13679
13680 let server_restarts = Arc::new(AtomicUsize::new(0));
13681 let closure_restarts = Arc::clone(&server_restarts);
13682 let language_server_name = "test language server";
13683 let language_name: LanguageName = "Rust".into();
13684
13685 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13686 language_registry.add(Arc::new(Language::new(
13687 LanguageConfig {
13688 name: language_name.clone(),
13689 matcher: LanguageMatcher {
13690 path_suffixes: vec!["rs".to_string()],
13691 ..Default::default()
13692 },
13693 ..Default::default()
13694 },
13695 Some(tree_sitter_rust::LANGUAGE.into()),
13696 )));
13697 let mut fake_servers = language_registry.register_fake_lsp(
13698 "Rust",
13699 FakeLspAdapter {
13700 name: language_server_name,
13701 initialization_options: Some(json!({
13702 "testOptionValue": true
13703 })),
13704 initializer: Some(Box::new(move |fake_server| {
13705 let task_restarts = Arc::clone(&closure_restarts);
13706 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
13707 task_restarts.fetch_add(1, atomic::Ordering::Release);
13708 futures::future::ready(Ok(()))
13709 });
13710 })),
13711 ..Default::default()
13712 },
13713 );
13714
13715 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13716 let _buffer = project
13717 .update(cx, |project, cx| {
13718 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
13719 })
13720 .await
13721 .unwrap();
13722 let _fake_server = fake_servers.next().await.unwrap();
13723 update_test_language_settings(cx, |language_settings| {
13724 language_settings.languages.insert(
13725 language_name.clone(),
13726 LanguageSettingsContent {
13727 tab_size: NonZeroU32::new(8),
13728 ..Default::default()
13729 },
13730 );
13731 });
13732 cx.executor().run_until_parked();
13733 assert_eq!(
13734 server_restarts.load(atomic::Ordering::Acquire),
13735 0,
13736 "Should not restart LSP server on an unrelated change"
13737 );
13738
13739 update_test_project_settings(cx, |project_settings| {
13740 project_settings.lsp.insert(
13741 "Some other server name".into(),
13742 LspSettings {
13743 binary: None,
13744 settings: None,
13745 initialization_options: Some(json!({
13746 "some other init value": false
13747 })),
13748 enable_lsp_tasks: false,
13749 },
13750 );
13751 });
13752 cx.executor().run_until_parked();
13753 assert_eq!(
13754 server_restarts.load(atomic::Ordering::Acquire),
13755 0,
13756 "Should not restart LSP server on an unrelated LSP settings change"
13757 );
13758
13759 update_test_project_settings(cx, |project_settings| {
13760 project_settings.lsp.insert(
13761 language_server_name.into(),
13762 LspSettings {
13763 binary: None,
13764 settings: None,
13765 initialization_options: Some(json!({
13766 "anotherInitValue": false
13767 })),
13768 enable_lsp_tasks: false,
13769 },
13770 );
13771 });
13772 cx.executor().run_until_parked();
13773 assert_eq!(
13774 server_restarts.load(atomic::Ordering::Acquire),
13775 1,
13776 "Should restart LSP server on a related LSP settings change"
13777 );
13778
13779 update_test_project_settings(cx, |project_settings| {
13780 project_settings.lsp.insert(
13781 language_server_name.into(),
13782 LspSettings {
13783 binary: None,
13784 settings: None,
13785 initialization_options: Some(json!({
13786 "anotherInitValue": false
13787 })),
13788 enable_lsp_tasks: false,
13789 },
13790 );
13791 });
13792 cx.executor().run_until_parked();
13793 assert_eq!(
13794 server_restarts.load(atomic::Ordering::Acquire),
13795 1,
13796 "Should not restart LSP server on a related LSP settings change that is the same"
13797 );
13798
13799 update_test_project_settings(cx, |project_settings| {
13800 project_settings.lsp.insert(
13801 language_server_name.into(),
13802 LspSettings {
13803 binary: None,
13804 settings: None,
13805 initialization_options: None,
13806 enable_lsp_tasks: false,
13807 },
13808 );
13809 });
13810 cx.executor().run_until_parked();
13811 assert_eq!(
13812 server_restarts.load(atomic::Ordering::Acquire),
13813 2,
13814 "Should restart LSP server on another related LSP settings change"
13815 );
13816}
13817
13818#[gpui::test]
13819async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
13820 init_test(cx, |_| {});
13821
13822 let mut cx = EditorLspTestContext::new_rust(
13823 lsp::ServerCapabilities {
13824 completion_provider: Some(lsp::CompletionOptions {
13825 trigger_characters: Some(vec![".".to_string()]),
13826 resolve_provider: Some(true),
13827 ..Default::default()
13828 }),
13829 ..Default::default()
13830 },
13831 cx,
13832 )
13833 .await;
13834
13835 cx.set_state("fn main() { let a = 2ˇ; }");
13836 cx.simulate_keystroke(".");
13837 let completion_item = lsp::CompletionItem {
13838 label: "some".into(),
13839 kind: Some(lsp::CompletionItemKind::SNIPPET),
13840 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
13841 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
13842 kind: lsp::MarkupKind::Markdown,
13843 value: "```rust\nSome(2)\n```".to_string(),
13844 })),
13845 deprecated: Some(false),
13846 sort_text: Some("fffffff2".to_string()),
13847 filter_text: Some("some".to_string()),
13848 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
13849 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13850 range: lsp::Range {
13851 start: lsp::Position {
13852 line: 0,
13853 character: 22,
13854 },
13855 end: lsp::Position {
13856 line: 0,
13857 character: 22,
13858 },
13859 },
13860 new_text: "Some(2)".to_string(),
13861 })),
13862 additional_text_edits: Some(vec![lsp::TextEdit {
13863 range: lsp::Range {
13864 start: lsp::Position {
13865 line: 0,
13866 character: 20,
13867 },
13868 end: lsp::Position {
13869 line: 0,
13870 character: 22,
13871 },
13872 },
13873 new_text: "".to_string(),
13874 }]),
13875 ..Default::default()
13876 };
13877
13878 let closure_completion_item = completion_item.clone();
13879 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13880 let task_completion_item = closure_completion_item.clone();
13881 async move {
13882 Ok(Some(lsp::CompletionResponse::Array(vec![
13883 task_completion_item,
13884 ])))
13885 }
13886 });
13887
13888 request.next().await;
13889
13890 cx.condition(|editor, _| editor.context_menu_visible())
13891 .await;
13892 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13893 editor
13894 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13895 .unwrap()
13896 });
13897 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
13898
13899 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
13900 let task_completion_item = completion_item.clone();
13901 async move { Ok(task_completion_item) }
13902 })
13903 .next()
13904 .await
13905 .unwrap();
13906 apply_additional_edits.await.unwrap();
13907 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
13908}
13909
13910#[gpui::test]
13911async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
13912 init_test(cx, |_| {});
13913
13914 let mut cx = EditorLspTestContext::new_rust(
13915 lsp::ServerCapabilities {
13916 completion_provider: Some(lsp::CompletionOptions {
13917 trigger_characters: Some(vec![".".to_string()]),
13918 resolve_provider: Some(true),
13919 ..Default::default()
13920 }),
13921 ..Default::default()
13922 },
13923 cx,
13924 )
13925 .await;
13926
13927 cx.set_state("fn main() { let a = 2ˇ; }");
13928 cx.simulate_keystroke(".");
13929
13930 let item1 = lsp::CompletionItem {
13931 label: "method id()".to_string(),
13932 filter_text: Some("id".to_string()),
13933 detail: None,
13934 documentation: None,
13935 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13936 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13937 new_text: ".id".to_string(),
13938 })),
13939 ..lsp::CompletionItem::default()
13940 };
13941
13942 let item2 = lsp::CompletionItem {
13943 label: "other".to_string(),
13944 filter_text: Some("other".to_string()),
13945 detail: None,
13946 documentation: None,
13947 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13948 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13949 new_text: ".other".to_string(),
13950 })),
13951 ..lsp::CompletionItem::default()
13952 };
13953
13954 let item1 = item1.clone();
13955 cx.set_request_handler::<lsp::request::Completion, _, _>({
13956 let item1 = item1.clone();
13957 move |_, _, _| {
13958 let item1 = item1.clone();
13959 let item2 = item2.clone();
13960 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
13961 }
13962 })
13963 .next()
13964 .await;
13965
13966 cx.condition(|editor, _| editor.context_menu_visible())
13967 .await;
13968 cx.update_editor(|editor, _, _| {
13969 let context_menu = editor.context_menu.borrow_mut();
13970 let context_menu = context_menu
13971 .as_ref()
13972 .expect("Should have the context menu deployed");
13973 match context_menu {
13974 CodeContextMenu::Completions(completions_menu) => {
13975 let completions = completions_menu.completions.borrow_mut();
13976 assert_eq!(
13977 completions
13978 .iter()
13979 .map(|completion| &completion.label.text)
13980 .collect::<Vec<_>>(),
13981 vec!["method id()", "other"]
13982 )
13983 }
13984 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
13985 }
13986 });
13987
13988 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
13989 let item1 = item1.clone();
13990 move |_, item_to_resolve, _| {
13991 let item1 = item1.clone();
13992 async move {
13993 if item1 == item_to_resolve {
13994 Ok(lsp::CompletionItem {
13995 label: "method id()".to_string(),
13996 filter_text: Some("id".to_string()),
13997 detail: Some("Now resolved!".to_string()),
13998 documentation: Some(lsp::Documentation::String("Docs".to_string())),
13999 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14000 range: lsp::Range::new(
14001 lsp::Position::new(0, 22),
14002 lsp::Position::new(0, 22),
14003 ),
14004 new_text: ".id".to_string(),
14005 })),
14006 ..lsp::CompletionItem::default()
14007 })
14008 } else {
14009 Ok(item_to_resolve)
14010 }
14011 }
14012 }
14013 })
14014 .next()
14015 .await
14016 .unwrap();
14017 cx.run_until_parked();
14018
14019 cx.update_editor(|editor, window, cx| {
14020 editor.context_menu_next(&Default::default(), window, cx);
14021 });
14022
14023 cx.update_editor(|editor, _, _| {
14024 let context_menu = editor.context_menu.borrow_mut();
14025 let context_menu = context_menu
14026 .as_ref()
14027 .expect("Should have the context menu deployed");
14028 match context_menu {
14029 CodeContextMenu::Completions(completions_menu) => {
14030 let completions = completions_menu.completions.borrow_mut();
14031 assert_eq!(
14032 completions
14033 .iter()
14034 .map(|completion| &completion.label.text)
14035 .collect::<Vec<_>>(),
14036 vec!["method id() Now resolved!", "other"],
14037 "Should update first completion label, but not second as the filter text did not match."
14038 );
14039 }
14040 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
14041 }
14042 });
14043}
14044
14045#[gpui::test]
14046async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
14047 init_test(cx, |_| {});
14048 let mut cx = EditorLspTestContext::new_rust(
14049 lsp::ServerCapabilities {
14050 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
14051 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
14052 completion_provider: Some(lsp::CompletionOptions {
14053 resolve_provider: Some(true),
14054 ..Default::default()
14055 }),
14056 ..Default::default()
14057 },
14058 cx,
14059 )
14060 .await;
14061 cx.set_state(indoc! {"
14062 struct TestStruct {
14063 field: i32
14064 }
14065
14066 fn mainˇ() {
14067 let unused_var = 42;
14068 let test_struct = TestStruct { field: 42 };
14069 }
14070 "});
14071 let symbol_range = cx.lsp_range(indoc! {"
14072 struct TestStruct {
14073 field: i32
14074 }
14075
14076 «fn main»() {
14077 let unused_var = 42;
14078 let test_struct = TestStruct { field: 42 };
14079 }
14080 "});
14081 let mut hover_requests =
14082 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
14083 Ok(Some(lsp::Hover {
14084 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
14085 kind: lsp::MarkupKind::Markdown,
14086 value: "Function documentation".to_string(),
14087 }),
14088 range: Some(symbol_range),
14089 }))
14090 });
14091
14092 // Case 1: Test that code action menu hide hover popover
14093 cx.dispatch_action(Hover);
14094 hover_requests.next().await;
14095 cx.condition(|editor, _| editor.hover_state.visible()).await;
14096 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
14097 move |_, _, _| async move {
14098 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
14099 lsp::CodeAction {
14100 title: "Remove unused variable".to_string(),
14101 kind: Some(CodeActionKind::QUICKFIX),
14102 edit: Some(lsp::WorkspaceEdit {
14103 changes: Some(
14104 [(
14105 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
14106 vec![lsp::TextEdit {
14107 range: lsp::Range::new(
14108 lsp::Position::new(5, 4),
14109 lsp::Position::new(5, 27),
14110 ),
14111 new_text: "".to_string(),
14112 }],
14113 )]
14114 .into_iter()
14115 .collect(),
14116 ),
14117 ..Default::default()
14118 }),
14119 ..Default::default()
14120 },
14121 )]))
14122 },
14123 );
14124 cx.update_editor(|editor, window, cx| {
14125 editor.toggle_code_actions(
14126 &ToggleCodeActions {
14127 deployed_from_indicator: None,
14128 quick_launch: false,
14129 },
14130 window,
14131 cx,
14132 );
14133 });
14134 code_action_requests.next().await;
14135 cx.run_until_parked();
14136 cx.condition(|editor, _| editor.context_menu_visible())
14137 .await;
14138 cx.update_editor(|editor, _, _| {
14139 assert!(
14140 !editor.hover_state.visible(),
14141 "Hover popover should be hidden when code action menu is shown"
14142 );
14143 // Hide code actions
14144 editor.context_menu.take();
14145 });
14146
14147 // Case 2: Test that code completions hide hover popover
14148 cx.dispatch_action(Hover);
14149 hover_requests.next().await;
14150 cx.condition(|editor, _| editor.hover_state.visible()).await;
14151 let counter = Arc::new(AtomicUsize::new(0));
14152 let mut completion_requests =
14153 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14154 let counter = counter.clone();
14155 async move {
14156 counter.fetch_add(1, atomic::Ordering::Release);
14157 Ok(Some(lsp::CompletionResponse::Array(vec![
14158 lsp::CompletionItem {
14159 label: "main".into(),
14160 kind: Some(lsp::CompletionItemKind::FUNCTION),
14161 detail: Some("() -> ()".to_string()),
14162 ..Default::default()
14163 },
14164 lsp::CompletionItem {
14165 label: "TestStruct".into(),
14166 kind: Some(lsp::CompletionItemKind::STRUCT),
14167 detail: Some("struct TestStruct".to_string()),
14168 ..Default::default()
14169 },
14170 ])))
14171 }
14172 });
14173 cx.update_editor(|editor, window, cx| {
14174 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14175 });
14176 completion_requests.next().await;
14177 cx.condition(|editor, _| editor.context_menu_visible())
14178 .await;
14179 cx.update_editor(|editor, _, _| {
14180 assert!(
14181 !editor.hover_state.visible(),
14182 "Hover popover should be hidden when completion menu is shown"
14183 );
14184 });
14185}
14186
14187#[gpui::test]
14188async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
14189 init_test(cx, |_| {});
14190
14191 let mut cx = EditorLspTestContext::new_rust(
14192 lsp::ServerCapabilities {
14193 completion_provider: Some(lsp::CompletionOptions {
14194 trigger_characters: Some(vec![".".to_string()]),
14195 resolve_provider: Some(true),
14196 ..Default::default()
14197 }),
14198 ..Default::default()
14199 },
14200 cx,
14201 )
14202 .await;
14203
14204 cx.set_state("fn main() { let a = 2ˇ; }");
14205 cx.simulate_keystroke(".");
14206
14207 let unresolved_item_1 = lsp::CompletionItem {
14208 label: "id".to_string(),
14209 filter_text: Some("id".to_string()),
14210 detail: None,
14211 documentation: None,
14212 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14213 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14214 new_text: ".id".to_string(),
14215 })),
14216 ..lsp::CompletionItem::default()
14217 };
14218 let resolved_item_1 = lsp::CompletionItem {
14219 additional_text_edits: Some(vec![lsp::TextEdit {
14220 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
14221 new_text: "!!".to_string(),
14222 }]),
14223 ..unresolved_item_1.clone()
14224 };
14225 let unresolved_item_2 = lsp::CompletionItem {
14226 label: "other".to_string(),
14227 filter_text: Some("other".to_string()),
14228 detail: None,
14229 documentation: None,
14230 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14231 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14232 new_text: ".other".to_string(),
14233 })),
14234 ..lsp::CompletionItem::default()
14235 };
14236 let resolved_item_2 = lsp::CompletionItem {
14237 additional_text_edits: Some(vec![lsp::TextEdit {
14238 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
14239 new_text: "??".to_string(),
14240 }]),
14241 ..unresolved_item_2.clone()
14242 };
14243
14244 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
14245 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
14246 cx.lsp
14247 .server
14248 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
14249 let unresolved_item_1 = unresolved_item_1.clone();
14250 let resolved_item_1 = resolved_item_1.clone();
14251 let unresolved_item_2 = unresolved_item_2.clone();
14252 let resolved_item_2 = resolved_item_2.clone();
14253 let resolve_requests_1 = resolve_requests_1.clone();
14254 let resolve_requests_2 = resolve_requests_2.clone();
14255 move |unresolved_request, _| {
14256 let unresolved_item_1 = unresolved_item_1.clone();
14257 let resolved_item_1 = resolved_item_1.clone();
14258 let unresolved_item_2 = unresolved_item_2.clone();
14259 let resolved_item_2 = resolved_item_2.clone();
14260 let resolve_requests_1 = resolve_requests_1.clone();
14261 let resolve_requests_2 = resolve_requests_2.clone();
14262 async move {
14263 if unresolved_request == unresolved_item_1 {
14264 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
14265 Ok(resolved_item_1.clone())
14266 } else if unresolved_request == unresolved_item_2 {
14267 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
14268 Ok(resolved_item_2.clone())
14269 } else {
14270 panic!("Unexpected completion item {unresolved_request:?}")
14271 }
14272 }
14273 }
14274 })
14275 .detach();
14276
14277 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14278 let unresolved_item_1 = unresolved_item_1.clone();
14279 let unresolved_item_2 = unresolved_item_2.clone();
14280 async move {
14281 Ok(Some(lsp::CompletionResponse::Array(vec![
14282 unresolved_item_1,
14283 unresolved_item_2,
14284 ])))
14285 }
14286 })
14287 .next()
14288 .await;
14289
14290 cx.condition(|editor, _| editor.context_menu_visible())
14291 .await;
14292 cx.update_editor(|editor, _, _| {
14293 let context_menu = editor.context_menu.borrow_mut();
14294 let context_menu = context_menu
14295 .as_ref()
14296 .expect("Should have the context menu deployed");
14297 match context_menu {
14298 CodeContextMenu::Completions(completions_menu) => {
14299 let completions = completions_menu.completions.borrow_mut();
14300 assert_eq!(
14301 completions
14302 .iter()
14303 .map(|completion| &completion.label.text)
14304 .collect::<Vec<_>>(),
14305 vec!["id", "other"]
14306 )
14307 }
14308 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
14309 }
14310 });
14311 cx.run_until_parked();
14312
14313 cx.update_editor(|editor, window, cx| {
14314 editor.context_menu_next(&ContextMenuNext, window, cx);
14315 });
14316 cx.run_until_parked();
14317 cx.update_editor(|editor, window, cx| {
14318 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
14319 });
14320 cx.run_until_parked();
14321 cx.update_editor(|editor, window, cx| {
14322 editor.context_menu_next(&ContextMenuNext, window, cx);
14323 });
14324 cx.run_until_parked();
14325 cx.update_editor(|editor, window, cx| {
14326 editor
14327 .compose_completion(&ComposeCompletion::default(), window, cx)
14328 .expect("No task returned")
14329 })
14330 .await
14331 .expect("Completion failed");
14332 cx.run_until_parked();
14333
14334 cx.update_editor(|editor, _, cx| {
14335 assert_eq!(
14336 resolve_requests_1.load(atomic::Ordering::Acquire),
14337 1,
14338 "Should always resolve once despite multiple selections"
14339 );
14340 assert_eq!(
14341 resolve_requests_2.load(atomic::Ordering::Acquire),
14342 1,
14343 "Should always resolve once after multiple selections and applying the completion"
14344 );
14345 assert_eq!(
14346 editor.text(cx),
14347 "fn main() { let a = ??.other; }",
14348 "Should use resolved data when applying the completion"
14349 );
14350 });
14351}
14352
14353#[gpui::test]
14354async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
14355 init_test(cx, |_| {});
14356
14357 let item_0 = lsp::CompletionItem {
14358 label: "abs".into(),
14359 insert_text: Some("abs".into()),
14360 data: Some(json!({ "very": "special"})),
14361 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
14362 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
14363 lsp::InsertReplaceEdit {
14364 new_text: "abs".to_string(),
14365 insert: lsp::Range::default(),
14366 replace: lsp::Range::default(),
14367 },
14368 )),
14369 ..lsp::CompletionItem::default()
14370 };
14371 let items = iter::once(item_0.clone())
14372 .chain((11..51).map(|i| lsp::CompletionItem {
14373 label: format!("item_{}", i),
14374 insert_text: Some(format!("item_{}", i)),
14375 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
14376 ..lsp::CompletionItem::default()
14377 }))
14378 .collect::<Vec<_>>();
14379
14380 let default_commit_characters = vec!["?".to_string()];
14381 let default_data = json!({ "default": "data"});
14382 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
14383 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
14384 let default_edit_range = lsp::Range {
14385 start: lsp::Position {
14386 line: 0,
14387 character: 5,
14388 },
14389 end: lsp::Position {
14390 line: 0,
14391 character: 5,
14392 },
14393 };
14394
14395 let mut cx = EditorLspTestContext::new_rust(
14396 lsp::ServerCapabilities {
14397 completion_provider: Some(lsp::CompletionOptions {
14398 trigger_characters: Some(vec![".".to_string()]),
14399 resolve_provider: Some(true),
14400 ..Default::default()
14401 }),
14402 ..Default::default()
14403 },
14404 cx,
14405 )
14406 .await;
14407
14408 cx.set_state("fn main() { let a = 2ˇ; }");
14409 cx.simulate_keystroke(".");
14410
14411 let completion_data = default_data.clone();
14412 let completion_characters = default_commit_characters.clone();
14413 let completion_items = items.clone();
14414 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14415 let default_data = completion_data.clone();
14416 let default_commit_characters = completion_characters.clone();
14417 let items = completion_items.clone();
14418 async move {
14419 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
14420 items,
14421 item_defaults: Some(lsp::CompletionListItemDefaults {
14422 data: Some(default_data.clone()),
14423 commit_characters: Some(default_commit_characters.clone()),
14424 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
14425 default_edit_range,
14426 )),
14427 insert_text_format: Some(default_insert_text_format),
14428 insert_text_mode: Some(default_insert_text_mode),
14429 }),
14430 ..lsp::CompletionList::default()
14431 })))
14432 }
14433 })
14434 .next()
14435 .await;
14436
14437 let resolved_items = Arc::new(Mutex::new(Vec::new()));
14438 cx.lsp
14439 .server
14440 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
14441 let closure_resolved_items = resolved_items.clone();
14442 move |item_to_resolve, _| {
14443 let closure_resolved_items = closure_resolved_items.clone();
14444 async move {
14445 closure_resolved_items.lock().push(item_to_resolve.clone());
14446 Ok(item_to_resolve)
14447 }
14448 }
14449 })
14450 .detach();
14451
14452 cx.condition(|editor, _| editor.context_menu_visible())
14453 .await;
14454 cx.run_until_parked();
14455 cx.update_editor(|editor, _, _| {
14456 let menu = editor.context_menu.borrow_mut();
14457 match menu.as_ref().expect("should have the completions menu") {
14458 CodeContextMenu::Completions(completions_menu) => {
14459 assert_eq!(
14460 completions_menu
14461 .entries
14462 .borrow()
14463 .iter()
14464 .map(|mat| mat.string.clone())
14465 .collect::<Vec<String>>(),
14466 items
14467 .iter()
14468 .map(|completion| completion.label.clone())
14469 .collect::<Vec<String>>()
14470 );
14471 }
14472 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
14473 }
14474 });
14475 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
14476 // with 4 from the end.
14477 assert_eq!(
14478 *resolved_items.lock(),
14479 [&items[0..16], &items[items.len() - 4..items.len()]]
14480 .concat()
14481 .iter()
14482 .cloned()
14483 .map(|mut item| {
14484 if item.data.is_none() {
14485 item.data = Some(default_data.clone());
14486 }
14487 item
14488 })
14489 .collect::<Vec<lsp::CompletionItem>>(),
14490 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
14491 );
14492 resolved_items.lock().clear();
14493
14494 cx.update_editor(|editor, window, cx| {
14495 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
14496 });
14497 cx.run_until_parked();
14498 // Completions that have already been resolved are skipped.
14499 assert_eq!(
14500 *resolved_items.lock(),
14501 items[items.len() - 16..items.len() - 4]
14502 .iter()
14503 .cloned()
14504 .map(|mut item| {
14505 if item.data.is_none() {
14506 item.data = Some(default_data.clone());
14507 }
14508 item
14509 })
14510 .collect::<Vec<lsp::CompletionItem>>()
14511 );
14512 resolved_items.lock().clear();
14513}
14514
14515#[gpui::test]
14516async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
14517 init_test(cx, |_| {});
14518
14519 let mut cx = EditorLspTestContext::new(
14520 Language::new(
14521 LanguageConfig {
14522 matcher: LanguageMatcher {
14523 path_suffixes: vec!["jsx".into()],
14524 ..Default::default()
14525 },
14526 overrides: [(
14527 "element".into(),
14528 LanguageConfigOverride {
14529 completion_query_characters: Override::Set(['-'].into_iter().collect()),
14530 ..Default::default()
14531 },
14532 )]
14533 .into_iter()
14534 .collect(),
14535 ..Default::default()
14536 },
14537 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
14538 )
14539 .with_override_query("(jsx_self_closing_element) @element")
14540 .unwrap(),
14541 lsp::ServerCapabilities {
14542 completion_provider: Some(lsp::CompletionOptions {
14543 trigger_characters: Some(vec![":".to_string()]),
14544 ..Default::default()
14545 }),
14546 ..Default::default()
14547 },
14548 cx,
14549 )
14550 .await;
14551
14552 cx.lsp
14553 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14554 Ok(Some(lsp::CompletionResponse::Array(vec![
14555 lsp::CompletionItem {
14556 label: "bg-blue".into(),
14557 ..Default::default()
14558 },
14559 lsp::CompletionItem {
14560 label: "bg-red".into(),
14561 ..Default::default()
14562 },
14563 lsp::CompletionItem {
14564 label: "bg-yellow".into(),
14565 ..Default::default()
14566 },
14567 ])))
14568 });
14569
14570 cx.set_state(r#"<p class="bgˇ" />"#);
14571
14572 // Trigger completion when typing a dash, because the dash is an extra
14573 // word character in the 'element' scope, which contains the cursor.
14574 cx.simulate_keystroke("-");
14575 cx.executor().run_until_parked();
14576 cx.update_editor(|editor, _, _| {
14577 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14578 {
14579 assert_eq!(
14580 completion_menu_entries(&menu),
14581 &["bg-red", "bg-blue", "bg-yellow"]
14582 );
14583 } else {
14584 panic!("expected completion menu to be open");
14585 }
14586 });
14587
14588 cx.simulate_keystroke("l");
14589 cx.executor().run_until_parked();
14590 cx.update_editor(|editor, _, _| {
14591 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14592 {
14593 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
14594 } else {
14595 panic!("expected completion menu to be open");
14596 }
14597 });
14598
14599 // When filtering completions, consider the character after the '-' to
14600 // be the start of a subword.
14601 cx.set_state(r#"<p class="yelˇ" />"#);
14602 cx.simulate_keystroke("l");
14603 cx.executor().run_until_parked();
14604 cx.update_editor(|editor, _, _| {
14605 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14606 {
14607 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
14608 } else {
14609 panic!("expected completion menu to be open");
14610 }
14611 });
14612}
14613
14614fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
14615 let entries = menu.entries.borrow();
14616 entries.iter().map(|mat| mat.string.clone()).collect()
14617}
14618
14619#[gpui::test]
14620async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
14621 init_test(cx, |settings| {
14622 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
14623 FormatterList(vec![Formatter::Prettier].into()),
14624 ))
14625 });
14626
14627 let fs = FakeFs::new(cx.executor());
14628 fs.insert_file(path!("/file.ts"), Default::default()).await;
14629
14630 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
14631 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14632
14633 language_registry.add(Arc::new(Language::new(
14634 LanguageConfig {
14635 name: "TypeScript".into(),
14636 matcher: LanguageMatcher {
14637 path_suffixes: vec!["ts".to_string()],
14638 ..Default::default()
14639 },
14640 ..Default::default()
14641 },
14642 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
14643 )));
14644 update_test_language_settings(cx, |settings| {
14645 settings.defaults.prettier = Some(PrettierSettings {
14646 allowed: true,
14647 ..PrettierSettings::default()
14648 });
14649 });
14650
14651 let test_plugin = "test_plugin";
14652 let _ = language_registry.register_fake_lsp(
14653 "TypeScript",
14654 FakeLspAdapter {
14655 prettier_plugins: vec![test_plugin],
14656 ..Default::default()
14657 },
14658 );
14659
14660 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
14661 let buffer = project
14662 .update(cx, |project, cx| {
14663 project.open_local_buffer(path!("/file.ts"), cx)
14664 })
14665 .await
14666 .unwrap();
14667
14668 let buffer_text = "one\ntwo\nthree\n";
14669 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
14670 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
14671 editor.update_in(cx, |editor, window, cx| {
14672 editor.set_text(buffer_text, window, cx)
14673 });
14674
14675 editor
14676 .update_in(cx, |editor, window, cx| {
14677 editor.perform_format(
14678 project.clone(),
14679 FormatTrigger::Manual,
14680 FormatTarget::Buffers,
14681 window,
14682 cx,
14683 )
14684 })
14685 .unwrap()
14686 .await;
14687 assert_eq!(
14688 editor.update(cx, |editor, cx| editor.text(cx)),
14689 buffer_text.to_string() + prettier_format_suffix,
14690 "Test prettier formatting was not applied to the original buffer text",
14691 );
14692
14693 update_test_language_settings(cx, |settings| {
14694 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
14695 });
14696 let format = editor.update_in(cx, |editor, window, cx| {
14697 editor.perform_format(
14698 project.clone(),
14699 FormatTrigger::Manual,
14700 FormatTarget::Buffers,
14701 window,
14702 cx,
14703 )
14704 });
14705 format.await.unwrap();
14706 assert_eq!(
14707 editor.update(cx, |editor, cx| editor.text(cx)),
14708 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
14709 "Autoformatting (via test prettier) was not applied to the original buffer text",
14710 );
14711}
14712
14713#[gpui::test]
14714async fn test_addition_reverts(cx: &mut TestAppContext) {
14715 init_test(cx, |_| {});
14716 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14717 let base_text = indoc! {r#"
14718 struct Row;
14719 struct Row1;
14720 struct Row2;
14721
14722 struct Row4;
14723 struct Row5;
14724 struct Row6;
14725
14726 struct Row8;
14727 struct Row9;
14728 struct Row10;"#};
14729
14730 // When addition hunks are not adjacent to carets, no hunk revert is performed
14731 assert_hunk_revert(
14732 indoc! {r#"struct Row;
14733 struct Row1;
14734 struct Row1.1;
14735 struct Row1.2;
14736 struct Row2;ˇ
14737
14738 struct Row4;
14739 struct Row5;
14740 struct Row6;
14741
14742 struct Row8;
14743 ˇstruct Row9;
14744 struct Row9.1;
14745 struct Row9.2;
14746 struct Row9.3;
14747 struct Row10;"#},
14748 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
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 base_text,
14766 &mut cx,
14767 );
14768 // Same for selections
14769 assert_hunk_revert(
14770 indoc! {r#"struct Row;
14771 struct Row1;
14772 struct Row2;
14773 struct Row2.1;
14774 struct Row2.2;
14775 «ˇ
14776 struct Row4;
14777 struct» Row5;
14778 «struct Row6;
14779 ˇ»
14780 struct Row9.1;
14781 struct Row9.2;
14782 struct Row9.3;
14783 struct Row8;
14784 struct Row9;
14785 struct Row10;"#},
14786 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
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 base_text,
14804 &mut cx,
14805 );
14806
14807 // When carets and selections intersect the addition hunks, those are reverted.
14808 // Adjacent carets got merged.
14809 assert_hunk_revert(
14810 indoc! {r#"struct Row;
14811 ˇ// something on the top
14812 struct Row1;
14813 struct Row2;
14814 struct Roˇw3.1;
14815 struct Row2.2;
14816 struct Row2.3;ˇ
14817
14818 struct Row4;
14819 struct ˇRow5.1;
14820 struct Row5.2;
14821 struct «Rowˇ»5.3;
14822 struct Row5;
14823 struct Row6;
14824 ˇ
14825 struct Row9.1;
14826 struct «Rowˇ»9.2;
14827 struct «ˇRow»9.3;
14828 struct Row8;
14829 struct Row9;
14830 «ˇ// something on bottom»
14831 struct Row10;"#},
14832 vec![
14833 DiffHunkStatusKind::Added,
14834 DiffHunkStatusKind::Added,
14835 DiffHunkStatusKind::Added,
14836 DiffHunkStatusKind::Added,
14837 DiffHunkStatusKind::Added,
14838 ],
14839 indoc! {r#"struct Row;
14840 ˇstruct Row1;
14841 struct Row2;
14842 ˇ
14843 struct Row4;
14844 ˇstruct Row5;
14845 struct Row6;
14846 ˇ
14847 ˇstruct Row8;
14848 struct Row9;
14849 ˇstruct Row10;"#},
14850 base_text,
14851 &mut cx,
14852 );
14853}
14854
14855#[gpui::test]
14856async fn test_modification_reverts(cx: &mut TestAppContext) {
14857 init_test(cx, |_| {});
14858 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14859 let base_text = indoc! {r#"
14860 struct Row;
14861 struct Row1;
14862 struct Row2;
14863
14864 struct Row4;
14865 struct Row5;
14866 struct Row6;
14867
14868 struct Row8;
14869 struct Row9;
14870 struct Row10;"#};
14871
14872 // Modification hunks behave the same as the addition ones.
14873 assert_hunk_revert(
14874 indoc! {r#"struct Row;
14875 struct Row1;
14876 struct Row33;
14877 ˇ
14878 struct Row4;
14879 struct Row5;
14880 struct Row6;
14881 ˇ
14882 struct Row99;
14883 struct Row9;
14884 struct Row10;"#},
14885 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
14886 indoc! {r#"struct Row;
14887 struct Row1;
14888 struct Row33;
14889 ˇ
14890 struct Row4;
14891 struct Row5;
14892 struct Row6;
14893 ˇ
14894 struct Row99;
14895 struct Row9;
14896 struct Row10;"#},
14897 base_text,
14898 &mut cx,
14899 );
14900 assert_hunk_revert(
14901 indoc! {r#"struct Row;
14902 struct Row1;
14903 struct Row33;
14904 «ˇ
14905 struct Row4;
14906 struct» Row5;
14907 «struct Row6;
14908 ˇ»
14909 struct Row99;
14910 struct Row9;
14911 struct Row10;"#},
14912 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
14913 indoc! {r#"struct Row;
14914 struct Row1;
14915 struct Row33;
14916 «ˇ
14917 struct Row4;
14918 struct» Row5;
14919 «struct Row6;
14920 ˇ»
14921 struct Row99;
14922 struct Row9;
14923 struct Row10;"#},
14924 base_text,
14925 &mut cx,
14926 );
14927
14928 assert_hunk_revert(
14929 indoc! {r#"ˇstruct Row1.1;
14930 struct Row1;
14931 «ˇstr»uct Row22;
14932
14933 struct ˇRow44;
14934 struct Row5;
14935 struct «Rˇ»ow66;ˇ
14936
14937 «struˇ»ct Row88;
14938 struct Row9;
14939 struct Row1011;ˇ"#},
14940 vec![
14941 DiffHunkStatusKind::Modified,
14942 DiffHunkStatusKind::Modified,
14943 DiffHunkStatusKind::Modified,
14944 DiffHunkStatusKind::Modified,
14945 DiffHunkStatusKind::Modified,
14946 DiffHunkStatusKind::Modified,
14947 ],
14948 indoc! {r#"struct Row;
14949 ˇstruct Row1;
14950 struct Row2;
14951 ˇ
14952 struct Row4;
14953 ˇstruct Row5;
14954 struct Row6;
14955 ˇ
14956 struct Row8;
14957 ˇstruct Row9;
14958 struct Row10;ˇ"#},
14959 base_text,
14960 &mut cx,
14961 );
14962}
14963
14964#[gpui::test]
14965async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
14966 init_test(cx, |_| {});
14967 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14968 let base_text = indoc! {r#"
14969 one
14970
14971 two
14972 three
14973 "#};
14974
14975 cx.set_head_text(base_text);
14976 cx.set_state("\nˇ\n");
14977 cx.executor().run_until_parked();
14978 cx.update_editor(|editor, _window, cx| {
14979 editor.expand_selected_diff_hunks(cx);
14980 });
14981 cx.executor().run_until_parked();
14982 cx.update_editor(|editor, window, cx| {
14983 editor.backspace(&Default::default(), window, cx);
14984 });
14985 cx.run_until_parked();
14986 cx.assert_state_with_diff(
14987 indoc! {r#"
14988
14989 - two
14990 - threeˇ
14991 +
14992 "#}
14993 .to_string(),
14994 );
14995}
14996
14997#[gpui::test]
14998async fn test_deletion_reverts(cx: &mut TestAppContext) {
14999 init_test(cx, |_| {});
15000 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15001 let base_text = indoc! {r#"struct Row;
15002struct Row1;
15003struct Row2;
15004
15005struct Row4;
15006struct Row5;
15007struct Row6;
15008
15009struct Row8;
15010struct Row9;
15011struct Row10;"#};
15012
15013 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
15014 assert_hunk_revert(
15015 indoc! {r#"struct Row;
15016 struct Row2;
15017
15018 ˇstruct Row4;
15019 struct Row5;
15020 struct Row6;
15021 ˇ
15022 struct Row8;
15023 struct Row10;"#},
15024 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
15025 indoc! {r#"struct Row;
15026 struct Row2;
15027
15028 ˇstruct Row4;
15029 struct Row5;
15030 struct Row6;
15031 ˇ
15032 struct Row8;
15033 struct Row10;"#},
15034 base_text,
15035 &mut cx,
15036 );
15037 assert_hunk_revert(
15038 indoc! {r#"struct Row;
15039 struct Row2;
15040
15041 «ˇstruct Row4;
15042 struct» Row5;
15043 «struct Row6;
15044 ˇ»
15045 struct Row8;
15046 struct Row10;"#},
15047 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
15048 indoc! {r#"struct Row;
15049 struct Row2;
15050
15051 «ˇstruct Row4;
15052 struct» Row5;
15053 «struct Row6;
15054 ˇ»
15055 struct Row8;
15056 struct Row10;"#},
15057 base_text,
15058 &mut cx,
15059 );
15060
15061 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
15062 assert_hunk_revert(
15063 indoc! {r#"struct Row;
15064 ˇstruct Row2;
15065
15066 struct Row4;
15067 struct Row5;
15068 struct Row6;
15069
15070 struct Row8;ˇ
15071 struct Row10;"#},
15072 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
15073 indoc! {r#"struct Row;
15074 struct Row1;
15075 ˇstruct Row2;
15076
15077 struct Row4;
15078 struct Row5;
15079 struct Row6;
15080
15081 struct Row8;ˇ
15082 struct Row9;
15083 struct Row10;"#},
15084 base_text,
15085 &mut cx,
15086 );
15087 assert_hunk_revert(
15088 indoc! {r#"struct Row;
15089 struct Row2«ˇ;
15090 struct Row4;
15091 struct» Row5;
15092 «struct Row6;
15093
15094 struct Row8;ˇ»
15095 struct Row10;"#},
15096 vec![
15097 DiffHunkStatusKind::Deleted,
15098 DiffHunkStatusKind::Deleted,
15099 DiffHunkStatusKind::Deleted,
15100 ],
15101 indoc! {r#"struct Row;
15102 struct Row1;
15103 struct Row2«ˇ;
15104
15105 struct Row4;
15106 struct» Row5;
15107 «struct Row6;
15108
15109 struct Row8;ˇ»
15110 struct Row9;
15111 struct Row10;"#},
15112 base_text,
15113 &mut cx,
15114 );
15115}
15116
15117#[gpui::test]
15118async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
15119 init_test(cx, |_| {});
15120
15121 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
15122 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
15123 let base_text_3 =
15124 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
15125
15126 let text_1 = edit_first_char_of_every_line(base_text_1);
15127 let text_2 = edit_first_char_of_every_line(base_text_2);
15128 let text_3 = edit_first_char_of_every_line(base_text_3);
15129
15130 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
15131 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
15132 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
15133
15134 let multibuffer = cx.new(|cx| {
15135 let mut multibuffer = MultiBuffer::new(ReadWrite);
15136 multibuffer.push_excerpts(
15137 buffer_1.clone(),
15138 [
15139 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15140 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15141 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15142 ],
15143 cx,
15144 );
15145 multibuffer.push_excerpts(
15146 buffer_2.clone(),
15147 [
15148 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15149 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15150 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15151 ],
15152 cx,
15153 );
15154 multibuffer.push_excerpts(
15155 buffer_3.clone(),
15156 [
15157 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15158 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15159 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15160 ],
15161 cx,
15162 );
15163 multibuffer
15164 });
15165
15166 let fs = FakeFs::new(cx.executor());
15167 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
15168 let (editor, cx) = cx
15169 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
15170 editor.update_in(cx, |editor, _window, cx| {
15171 for (buffer, diff_base) in [
15172 (buffer_1.clone(), base_text_1),
15173 (buffer_2.clone(), base_text_2),
15174 (buffer_3.clone(), base_text_3),
15175 ] {
15176 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
15177 editor
15178 .buffer
15179 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
15180 }
15181 });
15182 cx.executor().run_until_parked();
15183
15184 editor.update_in(cx, |editor, window, cx| {
15185 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}");
15186 editor.select_all(&SelectAll, window, cx);
15187 editor.git_restore(&Default::default(), window, cx);
15188 });
15189 cx.executor().run_until_parked();
15190
15191 // When all ranges are selected, all buffer hunks are reverted.
15192 editor.update(cx, |editor, cx| {
15193 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");
15194 });
15195 buffer_1.update(cx, |buffer, _| {
15196 assert_eq!(buffer.text(), base_text_1);
15197 });
15198 buffer_2.update(cx, |buffer, _| {
15199 assert_eq!(buffer.text(), base_text_2);
15200 });
15201 buffer_3.update(cx, |buffer, _| {
15202 assert_eq!(buffer.text(), base_text_3);
15203 });
15204
15205 editor.update_in(cx, |editor, window, cx| {
15206 editor.undo(&Default::default(), window, cx);
15207 });
15208
15209 editor.update_in(cx, |editor, window, cx| {
15210 editor.change_selections(None, window, cx, |s| {
15211 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
15212 });
15213 editor.git_restore(&Default::default(), window, cx);
15214 });
15215
15216 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
15217 // but not affect buffer_2 and its related excerpts.
15218 editor.update(cx, |editor, cx| {
15219 assert_eq!(
15220 editor.text(cx),
15221 "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}"
15222 );
15223 });
15224 buffer_1.update(cx, |buffer, _| {
15225 assert_eq!(buffer.text(), base_text_1);
15226 });
15227 buffer_2.update(cx, |buffer, _| {
15228 assert_eq!(
15229 buffer.text(),
15230 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
15231 );
15232 });
15233 buffer_3.update(cx, |buffer, _| {
15234 assert_eq!(
15235 buffer.text(),
15236 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
15237 );
15238 });
15239
15240 fn edit_first_char_of_every_line(text: &str) -> String {
15241 text.split('\n')
15242 .map(|line| format!("X{}", &line[1..]))
15243 .collect::<Vec<_>>()
15244 .join("\n")
15245 }
15246}
15247
15248#[gpui::test]
15249async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) {
15250 init_test(cx, |_| {});
15251
15252 let cols = 4;
15253 let rows = 10;
15254 let sample_text_1 = sample_text(rows, cols, 'a');
15255 assert_eq!(
15256 sample_text_1,
15257 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
15258 );
15259 let sample_text_2 = sample_text(rows, cols, 'l');
15260 assert_eq!(
15261 sample_text_2,
15262 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
15263 );
15264 let sample_text_3 = sample_text(rows, cols, 'v');
15265 assert_eq!(
15266 sample_text_3,
15267 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
15268 );
15269
15270 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
15271 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
15272 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
15273
15274 let multi_buffer = cx.new(|cx| {
15275 let mut multibuffer = MultiBuffer::new(ReadWrite);
15276 multibuffer.push_excerpts(
15277 buffer_1.clone(),
15278 [
15279 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15280 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15281 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15282 ],
15283 cx,
15284 );
15285 multibuffer.push_excerpts(
15286 buffer_2.clone(),
15287 [
15288 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15289 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15290 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15291 ],
15292 cx,
15293 );
15294 multibuffer.push_excerpts(
15295 buffer_3.clone(),
15296 [
15297 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15298 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15299 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15300 ],
15301 cx,
15302 );
15303 multibuffer
15304 });
15305
15306 let fs = FakeFs::new(cx.executor());
15307 fs.insert_tree(
15308 "/a",
15309 json!({
15310 "main.rs": sample_text_1,
15311 "other.rs": sample_text_2,
15312 "lib.rs": sample_text_3,
15313 }),
15314 )
15315 .await;
15316 let project = Project::test(fs, ["/a".as_ref()], cx).await;
15317 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15318 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
15319 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
15320 Editor::new(
15321 EditorMode::full(),
15322 multi_buffer,
15323 Some(project.clone()),
15324 window,
15325 cx,
15326 )
15327 });
15328 let multibuffer_item_id = workspace
15329 .update(cx, |workspace, window, cx| {
15330 assert!(
15331 workspace.active_item(cx).is_none(),
15332 "active item should be None before the first item is added"
15333 );
15334 workspace.add_item_to_active_pane(
15335 Box::new(multi_buffer_editor.clone()),
15336 None,
15337 true,
15338 window,
15339 cx,
15340 );
15341 let active_item = workspace
15342 .active_item(cx)
15343 .expect("should have an active item after adding the multi buffer");
15344 assert!(
15345 !active_item.is_singleton(cx),
15346 "A multi buffer was expected to active after adding"
15347 );
15348 active_item.item_id()
15349 })
15350 .unwrap();
15351 cx.executor().run_until_parked();
15352
15353 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15354 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
15355 s.select_ranges(Some(1..2))
15356 });
15357 editor.open_excerpts(&OpenExcerpts, window, cx);
15358 });
15359 cx.executor().run_until_parked();
15360 let first_item_id = workspace
15361 .update(cx, |workspace, window, cx| {
15362 let active_item = workspace
15363 .active_item(cx)
15364 .expect("should have an active item after navigating into the 1st buffer");
15365 let first_item_id = active_item.item_id();
15366 assert_ne!(
15367 first_item_id, multibuffer_item_id,
15368 "Should navigate into the 1st buffer and activate it"
15369 );
15370 assert!(
15371 active_item.is_singleton(cx),
15372 "New active item should be a singleton buffer"
15373 );
15374 assert_eq!(
15375 active_item
15376 .act_as::<Editor>(cx)
15377 .expect("should have navigated into an editor for the 1st buffer")
15378 .read(cx)
15379 .text(cx),
15380 sample_text_1
15381 );
15382
15383 workspace
15384 .go_back(workspace.active_pane().downgrade(), window, cx)
15385 .detach_and_log_err(cx);
15386
15387 first_item_id
15388 })
15389 .unwrap();
15390 cx.executor().run_until_parked();
15391 workspace
15392 .update(cx, |workspace, _, cx| {
15393 let active_item = workspace
15394 .active_item(cx)
15395 .expect("should have an active item after navigating back");
15396 assert_eq!(
15397 active_item.item_id(),
15398 multibuffer_item_id,
15399 "Should navigate back to the multi buffer"
15400 );
15401 assert!(!active_item.is_singleton(cx));
15402 })
15403 .unwrap();
15404
15405 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15406 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
15407 s.select_ranges(Some(39..40))
15408 });
15409 editor.open_excerpts(&OpenExcerpts, window, cx);
15410 });
15411 cx.executor().run_until_parked();
15412 let second_item_id = workspace
15413 .update(cx, |workspace, window, cx| {
15414 let active_item = workspace
15415 .active_item(cx)
15416 .expect("should have an active item after navigating into the 2nd buffer");
15417 let second_item_id = active_item.item_id();
15418 assert_ne!(
15419 second_item_id, multibuffer_item_id,
15420 "Should navigate away from the multibuffer"
15421 );
15422 assert_ne!(
15423 second_item_id, first_item_id,
15424 "Should navigate into the 2nd buffer and activate it"
15425 );
15426 assert!(
15427 active_item.is_singleton(cx),
15428 "New active item should be a singleton buffer"
15429 );
15430 assert_eq!(
15431 active_item
15432 .act_as::<Editor>(cx)
15433 .expect("should have navigated into an editor")
15434 .read(cx)
15435 .text(cx),
15436 sample_text_2
15437 );
15438
15439 workspace
15440 .go_back(workspace.active_pane().downgrade(), window, cx)
15441 .detach_and_log_err(cx);
15442
15443 second_item_id
15444 })
15445 .unwrap();
15446 cx.executor().run_until_parked();
15447 workspace
15448 .update(cx, |workspace, _, cx| {
15449 let active_item = workspace
15450 .active_item(cx)
15451 .expect("should have an active item after navigating back from the 2nd buffer");
15452 assert_eq!(
15453 active_item.item_id(),
15454 multibuffer_item_id,
15455 "Should navigate back from the 2nd buffer to the multi buffer"
15456 );
15457 assert!(!active_item.is_singleton(cx));
15458 })
15459 .unwrap();
15460
15461 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15462 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
15463 s.select_ranges(Some(70..70))
15464 });
15465 editor.open_excerpts(&OpenExcerpts, window, cx);
15466 });
15467 cx.executor().run_until_parked();
15468 workspace
15469 .update(cx, |workspace, window, cx| {
15470 let active_item = workspace
15471 .active_item(cx)
15472 .expect("should have an active item after navigating into the 3rd buffer");
15473 let third_item_id = active_item.item_id();
15474 assert_ne!(
15475 third_item_id, multibuffer_item_id,
15476 "Should navigate into the 3rd buffer and activate it"
15477 );
15478 assert_ne!(third_item_id, first_item_id);
15479 assert_ne!(third_item_id, second_item_id);
15480 assert!(
15481 active_item.is_singleton(cx),
15482 "New active item should be a singleton buffer"
15483 );
15484 assert_eq!(
15485 active_item
15486 .act_as::<Editor>(cx)
15487 .expect("should have navigated into an editor")
15488 .read(cx)
15489 .text(cx),
15490 sample_text_3
15491 );
15492
15493 workspace
15494 .go_back(workspace.active_pane().downgrade(), window, cx)
15495 .detach_and_log_err(cx);
15496 })
15497 .unwrap();
15498 cx.executor().run_until_parked();
15499 workspace
15500 .update(cx, |workspace, _, cx| {
15501 let active_item = workspace
15502 .active_item(cx)
15503 .expect("should have an active item after navigating back from the 3rd buffer");
15504 assert_eq!(
15505 active_item.item_id(),
15506 multibuffer_item_id,
15507 "Should navigate back from the 3rd buffer to the multi buffer"
15508 );
15509 assert!(!active_item.is_singleton(cx));
15510 })
15511 .unwrap();
15512}
15513
15514#[gpui::test]
15515async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
15516 init_test(cx, |_| {});
15517
15518 let mut cx = EditorTestContext::new(cx).await;
15519
15520 let diff_base = r#"
15521 use some::mod;
15522
15523 const A: u32 = 42;
15524
15525 fn main() {
15526 println!("hello");
15527
15528 println!("world");
15529 }
15530 "#
15531 .unindent();
15532
15533 cx.set_state(
15534 &r#"
15535 use some::modified;
15536
15537 ˇ
15538 fn main() {
15539 println!("hello there");
15540
15541 println!("around the");
15542 println!("world");
15543 }
15544 "#
15545 .unindent(),
15546 );
15547
15548 cx.set_head_text(&diff_base);
15549 executor.run_until_parked();
15550
15551 cx.update_editor(|editor, window, cx| {
15552 editor.go_to_next_hunk(&GoToHunk, window, cx);
15553 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15554 });
15555 executor.run_until_parked();
15556 cx.assert_state_with_diff(
15557 r#"
15558 use some::modified;
15559
15560
15561 fn main() {
15562 - println!("hello");
15563 + ˇ println!("hello there");
15564
15565 println!("around the");
15566 println!("world");
15567 }
15568 "#
15569 .unindent(),
15570 );
15571
15572 cx.update_editor(|editor, window, cx| {
15573 for _ in 0..2 {
15574 editor.go_to_next_hunk(&GoToHunk, window, cx);
15575 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15576 }
15577 });
15578 executor.run_until_parked();
15579 cx.assert_state_with_diff(
15580 r#"
15581 - use some::mod;
15582 + ˇuse some::modified;
15583
15584
15585 fn main() {
15586 - println!("hello");
15587 + println!("hello there");
15588
15589 + println!("around the");
15590 println!("world");
15591 }
15592 "#
15593 .unindent(),
15594 );
15595
15596 cx.update_editor(|editor, window, cx| {
15597 editor.go_to_next_hunk(&GoToHunk, window, cx);
15598 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15599 });
15600 executor.run_until_parked();
15601 cx.assert_state_with_diff(
15602 r#"
15603 - use some::mod;
15604 + use some::modified;
15605
15606 - const A: u32 = 42;
15607 ˇ
15608 fn main() {
15609 - println!("hello");
15610 + println!("hello there");
15611
15612 + println!("around the");
15613 println!("world");
15614 }
15615 "#
15616 .unindent(),
15617 );
15618
15619 cx.update_editor(|editor, window, cx| {
15620 editor.cancel(&Cancel, window, cx);
15621 });
15622
15623 cx.assert_state_with_diff(
15624 r#"
15625 use some::modified;
15626
15627 ˇ
15628 fn main() {
15629 println!("hello there");
15630
15631 println!("around the");
15632 println!("world");
15633 }
15634 "#
15635 .unindent(),
15636 );
15637}
15638
15639#[gpui::test]
15640async fn test_diff_base_change_with_expanded_diff_hunks(
15641 executor: BackgroundExecutor,
15642 cx: &mut TestAppContext,
15643) {
15644 init_test(cx, |_| {});
15645
15646 let mut cx = EditorTestContext::new(cx).await;
15647
15648 let diff_base = r#"
15649 use some::mod1;
15650 use some::mod2;
15651
15652 const A: u32 = 42;
15653 const B: u32 = 42;
15654 const C: u32 = 42;
15655
15656 fn main() {
15657 println!("hello");
15658
15659 println!("world");
15660 }
15661 "#
15662 .unindent();
15663
15664 cx.set_state(
15665 &r#"
15666 use some::mod2;
15667
15668 const A: u32 = 42;
15669 const C: u32 = 42;
15670
15671 fn main(ˇ) {
15672 //println!("hello");
15673
15674 println!("world");
15675 //
15676 //
15677 }
15678 "#
15679 .unindent(),
15680 );
15681
15682 cx.set_head_text(&diff_base);
15683 executor.run_until_parked();
15684
15685 cx.update_editor(|editor, window, cx| {
15686 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15687 });
15688 executor.run_until_parked();
15689 cx.assert_state_with_diff(
15690 r#"
15691 - use some::mod1;
15692 use some::mod2;
15693
15694 const A: u32 = 42;
15695 - const B: u32 = 42;
15696 const C: u32 = 42;
15697
15698 fn main(ˇ) {
15699 - println!("hello");
15700 + //println!("hello");
15701
15702 println!("world");
15703 + //
15704 + //
15705 }
15706 "#
15707 .unindent(),
15708 );
15709
15710 cx.set_head_text("new diff base!");
15711 executor.run_until_parked();
15712 cx.assert_state_with_diff(
15713 r#"
15714 - new diff base!
15715 + use some::mod2;
15716 +
15717 + const A: u32 = 42;
15718 + const C: u32 = 42;
15719 +
15720 + fn main(ˇ) {
15721 + //println!("hello");
15722 +
15723 + println!("world");
15724 + //
15725 + //
15726 + }
15727 "#
15728 .unindent(),
15729 );
15730}
15731
15732#[gpui::test]
15733async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
15734 init_test(cx, |_| {});
15735
15736 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
15737 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
15738 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
15739 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
15740 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
15741 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
15742
15743 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
15744 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
15745 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
15746
15747 let multi_buffer = cx.new(|cx| {
15748 let mut multibuffer = MultiBuffer::new(ReadWrite);
15749 multibuffer.push_excerpts(
15750 buffer_1.clone(),
15751 [
15752 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15753 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15754 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
15755 ],
15756 cx,
15757 );
15758 multibuffer.push_excerpts(
15759 buffer_2.clone(),
15760 [
15761 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15762 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15763 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
15764 ],
15765 cx,
15766 );
15767 multibuffer.push_excerpts(
15768 buffer_3.clone(),
15769 [
15770 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15771 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15772 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
15773 ],
15774 cx,
15775 );
15776 multibuffer
15777 });
15778
15779 let editor =
15780 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
15781 editor
15782 .update(cx, |editor, _window, cx| {
15783 for (buffer, diff_base) in [
15784 (buffer_1.clone(), file_1_old),
15785 (buffer_2.clone(), file_2_old),
15786 (buffer_3.clone(), file_3_old),
15787 ] {
15788 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
15789 editor
15790 .buffer
15791 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
15792 }
15793 })
15794 .unwrap();
15795
15796 let mut cx = EditorTestContext::for_editor(editor, cx).await;
15797 cx.run_until_parked();
15798
15799 cx.assert_editor_state(
15800 &"
15801 ˇaaa
15802 ccc
15803 ddd
15804
15805 ggg
15806 hhh
15807
15808
15809 lll
15810 mmm
15811 NNN
15812
15813 qqq
15814 rrr
15815
15816 uuu
15817 111
15818 222
15819 333
15820
15821 666
15822 777
15823
15824 000
15825 !!!"
15826 .unindent(),
15827 );
15828
15829 cx.update_editor(|editor, window, cx| {
15830 editor.select_all(&SelectAll, window, cx);
15831 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15832 });
15833 cx.executor().run_until_parked();
15834
15835 cx.assert_state_with_diff(
15836 "
15837 «aaa
15838 - bbb
15839 ccc
15840 ddd
15841
15842 ggg
15843 hhh
15844
15845
15846 lll
15847 mmm
15848 - nnn
15849 + NNN
15850
15851 qqq
15852 rrr
15853
15854 uuu
15855 111
15856 222
15857 333
15858
15859 + 666
15860 777
15861
15862 000
15863 !!!ˇ»"
15864 .unindent(),
15865 );
15866}
15867
15868#[gpui::test]
15869async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
15870 init_test(cx, |_| {});
15871
15872 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
15873 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
15874
15875 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
15876 let multi_buffer = cx.new(|cx| {
15877 let mut multibuffer = MultiBuffer::new(ReadWrite);
15878 multibuffer.push_excerpts(
15879 buffer.clone(),
15880 [
15881 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
15882 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
15883 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
15884 ],
15885 cx,
15886 );
15887 multibuffer
15888 });
15889
15890 let editor =
15891 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
15892 editor
15893 .update(cx, |editor, _window, cx| {
15894 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
15895 editor
15896 .buffer
15897 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
15898 })
15899 .unwrap();
15900
15901 let mut cx = EditorTestContext::for_editor(editor, cx).await;
15902 cx.run_until_parked();
15903
15904 cx.update_editor(|editor, window, cx| {
15905 editor.expand_all_diff_hunks(&Default::default(), window, cx)
15906 });
15907 cx.executor().run_until_parked();
15908
15909 // When the start of a hunk coincides with the start of its excerpt,
15910 // the hunk is expanded. When the start of a a hunk is earlier than
15911 // the start of its excerpt, the hunk is not expanded.
15912 cx.assert_state_with_diff(
15913 "
15914 ˇaaa
15915 - bbb
15916 + BBB
15917
15918 - ddd
15919 - eee
15920 + DDD
15921 + EEE
15922 fff
15923
15924 iii
15925 "
15926 .unindent(),
15927 );
15928}
15929
15930#[gpui::test]
15931async fn test_edits_around_expanded_insertion_hunks(
15932 executor: BackgroundExecutor,
15933 cx: &mut TestAppContext,
15934) {
15935 init_test(cx, |_| {});
15936
15937 let mut cx = EditorTestContext::new(cx).await;
15938
15939 let diff_base = r#"
15940 use some::mod1;
15941 use some::mod2;
15942
15943 const A: u32 = 42;
15944
15945 fn main() {
15946 println!("hello");
15947
15948 println!("world");
15949 }
15950 "#
15951 .unindent();
15952 executor.run_until_parked();
15953 cx.set_state(
15954 &r#"
15955 use some::mod1;
15956 use some::mod2;
15957
15958 const A: u32 = 42;
15959 const B: u32 = 42;
15960 const C: u32 = 42;
15961 ˇ
15962
15963 fn main() {
15964 println!("hello");
15965
15966 println!("world");
15967 }
15968 "#
15969 .unindent(),
15970 );
15971
15972 cx.set_head_text(&diff_base);
15973 executor.run_until_parked();
15974
15975 cx.update_editor(|editor, window, cx| {
15976 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15977 });
15978 executor.run_until_parked();
15979
15980 cx.assert_state_with_diff(
15981 r#"
15982 use some::mod1;
15983 use some::mod2;
15984
15985 const A: u32 = 42;
15986 + const B: u32 = 42;
15987 + const C: u32 = 42;
15988 + ˇ
15989
15990 fn main() {
15991 println!("hello");
15992
15993 println!("world");
15994 }
15995 "#
15996 .unindent(),
15997 );
15998
15999 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
16000 executor.run_until_parked();
16001
16002 cx.assert_state_with_diff(
16003 r#"
16004 use some::mod1;
16005 use some::mod2;
16006
16007 const A: u32 = 42;
16008 + const B: u32 = 42;
16009 + const C: u32 = 42;
16010 + const D: u32 = 42;
16011 + ˇ
16012
16013 fn main() {
16014 println!("hello");
16015
16016 println!("world");
16017 }
16018 "#
16019 .unindent(),
16020 );
16021
16022 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
16023 executor.run_until_parked();
16024
16025 cx.assert_state_with_diff(
16026 r#"
16027 use some::mod1;
16028 use some::mod2;
16029
16030 const A: u32 = 42;
16031 + const B: u32 = 42;
16032 + const C: u32 = 42;
16033 + const D: u32 = 42;
16034 + const E: u32 = 42;
16035 + ˇ
16036
16037 fn main() {
16038 println!("hello");
16039
16040 println!("world");
16041 }
16042 "#
16043 .unindent(),
16044 );
16045
16046 cx.update_editor(|editor, window, cx| {
16047 editor.delete_line(&DeleteLine, window, cx);
16048 });
16049 executor.run_until_parked();
16050
16051 cx.assert_state_with_diff(
16052 r#"
16053 use some::mod1;
16054 use some::mod2;
16055
16056 const A: u32 = 42;
16057 + const B: u32 = 42;
16058 + const C: u32 = 42;
16059 + const D: u32 = 42;
16060 + const E: u32 = 42;
16061 ˇ
16062 fn main() {
16063 println!("hello");
16064
16065 println!("world");
16066 }
16067 "#
16068 .unindent(),
16069 );
16070
16071 cx.update_editor(|editor, window, cx| {
16072 editor.move_up(&MoveUp, window, cx);
16073 editor.delete_line(&DeleteLine, window, cx);
16074 editor.move_up(&MoveUp, window, cx);
16075 editor.delete_line(&DeleteLine, window, cx);
16076 editor.move_up(&MoveUp, window, cx);
16077 editor.delete_line(&DeleteLine, window, cx);
16078 });
16079 executor.run_until_parked();
16080 cx.assert_state_with_diff(
16081 r#"
16082 use some::mod1;
16083 use some::mod2;
16084
16085 const A: u32 = 42;
16086 + const B: u32 = 42;
16087 ˇ
16088 fn main() {
16089 println!("hello");
16090
16091 println!("world");
16092 }
16093 "#
16094 .unindent(),
16095 );
16096
16097 cx.update_editor(|editor, window, cx| {
16098 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
16099 editor.delete_line(&DeleteLine, window, cx);
16100 });
16101 executor.run_until_parked();
16102 cx.assert_state_with_diff(
16103 r#"
16104 ˇ
16105 fn main() {
16106 println!("hello");
16107
16108 println!("world");
16109 }
16110 "#
16111 .unindent(),
16112 );
16113}
16114
16115#[gpui::test]
16116async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
16117 init_test(cx, |_| {});
16118
16119 let mut cx = EditorTestContext::new(cx).await;
16120 cx.set_head_text(indoc! { "
16121 one
16122 two
16123 three
16124 four
16125 five
16126 "
16127 });
16128 cx.set_state(indoc! { "
16129 one
16130 ˇthree
16131 five
16132 "});
16133 cx.run_until_parked();
16134 cx.update_editor(|editor, window, cx| {
16135 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
16136 });
16137 cx.assert_state_with_diff(
16138 indoc! { "
16139 one
16140 - two
16141 ˇthree
16142 - four
16143 five
16144 "}
16145 .to_string(),
16146 );
16147 cx.update_editor(|editor, window, cx| {
16148 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
16149 });
16150
16151 cx.assert_state_with_diff(
16152 indoc! { "
16153 one
16154 ˇthree
16155 five
16156 "}
16157 .to_string(),
16158 );
16159
16160 cx.set_state(indoc! { "
16161 one
16162 ˇTWO
16163 three
16164 four
16165 five
16166 "});
16167 cx.run_until_parked();
16168 cx.update_editor(|editor, window, cx| {
16169 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
16170 });
16171
16172 cx.assert_state_with_diff(
16173 indoc! { "
16174 one
16175 - two
16176 + ˇTWO
16177 three
16178 four
16179 five
16180 "}
16181 .to_string(),
16182 );
16183 cx.update_editor(|editor, window, cx| {
16184 editor.move_up(&Default::default(), window, cx);
16185 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
16186 });
16187 cx.assert_state_with_diff(
16188 indoc! { "
16189 one
16190 ˇTWO
16191 three
16192 four
16193 five
16194 "}
16195 .to_string(),
16196 );
16197}
16198
16199#[gpui::test]
16200async fn test_edits_around_expanded_deletion_hunks(
16201 executor: BackgroundExecutor,
16202 cx: &mut TestAppContext,
16203) {
16204 init_test(cx, |_| {});
16205
16206 let mut cx = EditorTestContext::new(cx).await;
16207
16208 let diff_base = r#"
16209 use some::mod1;
16210 use some::mod2;
16211
16212 const A: u32 = 42;
16213 const B: u32 = 42;
16214 const C: u32 = 42;
16215
16216
16217 fn main() {
16218 println!("hello");
16219
16220 println!("world");
16221 }
16222 "#
16223 .unindent();
16224 executor.run_until_parked();
16225 cx.set_state(
16226 &r#"
16227 use some::mod1;
16228 use some::mod2;
16229
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 );
16242
16243 cx.set_head_text(&diff_base);
16244 executor.run_until_parked();
16245
16246 cx.update_editor(|editor, window, cx| {
16247 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16248 });
16249 executor.run_until_parked();
16250
16251 cx.assert_state_with_diff(
16252 r#"
16253 use some::mod1;
16254 use some::mod2;
16255
16256 - const A: u32 = 42;
16257 ˇconst B: u32 = 42;
16258 const C: u32 = 42;
16259
16260
16261 fn main() {
16262 println!("hello");
16263
16264 println!("world");
16265 }
16266 "#
16267 .unindent(),
16268 );
16269
16270 cx.update_editor(|editor, window, cx| {
16271 editor.delete_line(&DeleteLine, window, cx);
16272 });
16273 executor.run_until_parked();
16274 cx.assert_state_with_diff(
16275 r#"
16276 use some::mod1;
16277 use some::mod2;
16278
16279 - const A: u32 = 42;
16280 - const B: u32 = 42;
16281 ˇconst C: u32 = 42;
16282
16283
16284 fn main() {
16285 println!("hello");
16286
16287 println!("world");
16288 }
16289 "#
16290 .unindent(),
16291 );
16292
16293 cx.update_editor(|editor, window, cx| {
16294 editor.delete_line(&DeleteLine, window, cx);
16295 });
16296 executor.run_until_parked();
16297 cx.assert_state_with_diff(
16298 r#"
16299 use some::mod1;
16300 use some::mod2;
16301
16302 - const A: u32 = 42;
16303 - const B: u32 = 42;
16304 - const C: u32 = 42;
16305 ˇ
16306
16307 fn main() {
16308 println!("hello");
16309
16310 println!("world");
16311 }
16312 "#
16313 .unindent(),
16314 );
16315
16316 cx.update_editor(|editor, window, cx| {
16317 editor.handle_input("replacement", window, cx);
16318 });
16319 executor.run_until_parked();
16320 cx.assert_state_with_diff(
16321 r#"
16322 use some::mod1;
16323 use some::mod2;
16324
16325 - const A: u32 = 42;
16326 - const B: u32 = 42;
16327 - const C: u32 = 42;
16328 -
16329 + replacementˇ
16330
16331 fn main() {
16332 println!("hello");
16333
16334 println!("world");
16335 }
16336 "#
16337 .unindent(),
16338 );
16339}
16340
16341#[gpui::test]
16342async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16343 init_test(cx, |_| {});
16344
16345 let mut cx = EditorTestContext::new(cx).await;
16346
16347 let base_text = r#"
16348 one
16349 two
16350 three
16351 four
16352 five
16353 "#
16354 .unindent();
16355 executor.run_until_parked();
16356 cx.set_state(
16357 &r#"
16358 one
16359 two
16360 fˇour
16361 five
16362 "#
16363 .unindent(),
16364 );
16365
16366 cx.set_head_text(&base_text);
16367 executor.run_until_parked();
16368
16369 cx.update_editor(|editor, window, cx| {
16370 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16371 });
16372 executor.run_until_parked();
16373
16374 cx.assert_state_with_diff(
16375 r#"
16376 one
16377 two
16378 - three
16379 fˇour
16380 five
16381 "#
16382 .unindent(),
16383 );
16384
16385 cx.update_editor(|editor, window, cx| {
16386 editor.backspace(&Backspace, window, cx);
16387 editor.backspace(&Backspace, window, cx);
16388 });
16389 executor.run_until_parked();
16390 cx.assert_state_with_diff(
16391 r#"
16392 one
16393 two
16394 - threeˇ
16395 - four
16396 + our
16397 five
16398 "#
16399 .unindent(),
16400 );
16401}
16402
16403#[gpui::test]
16404async fn test_edit_after_expanded_modification_hunk(
16405 executor: BackgroundExecutor,
16406 cx: &mut TestAppContext,
16407) {
16408 init_test(cx, |_| {});
16409
16410 let mut cx = EditorTestContext::new(cx).await;
16411
16412 let diff_base = r#"
16413 use some::mod1;
16414 use some::mod2;
16415
16416 const A: u32 = 42;
16417 const B: u32 = 42;
16418 const C: u32 = 42;
16419 const D: u32 = 42;
16420
16421
16422 fn main() {
16423 println!("hello");
16424
16425 println!("world");
16426 }"#
16427 .unindent();
16428
16429 cx.set_state(
16430 &r#"
16431 use some::mod1;
16432 use some::mod2;
16433
16434 const A: u32 = 42;
16435 const B: u32 = 42;
16436 const C: u32 = 43ˇ
16437 const D: u32 = 42;
16438
16439
16440 fn main() {
16441 println!("hello");
16442
16443 println!("world");
16444 }"#
16445 .unindent(),
16446 );
16447
16448 cx.set_head_text(&diff_base);
16449 executor.run_until_parked();
16450 cx.update_editor(|editor, window, cx| {
16451 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16452 });
16453 executor.run_until_parked();
16454
16455 cx.assert_state_with_diff(
16456 r#"
16457 use some::mod1;
16458 use some::mod2;
16459
16460 const A: u32 = 42;
16461 const B: u32 = 42;
16462 - const C: u32 = 42;
16463 + const C: u32 = 43ˇ
16464 const D: u32 = 42;
16465
16466
16467 fn main() {
16468 println!("hello");
16469
16470 println!("world");
16471 }"#
16472 .unindent(),
16473 );
16474
16475 cx.update_editor(|editor, window, cx| {
16476 editor.handle_input("\nnew_line\n", window, cx);
16477 });
16478 executor.run_until_parked();
16479
16480 cx.assert_state_with_diff(
16481 r#"
16482 use some::mod1;
16483 use some::mod2;
16484
16485 const A: u32 = 42;
16486 const B: u32 = 42;
16487 - const C: u32 = 42;
16488 + const C: u32 = 43
16489 + new_line
16490 + ˇ
16491 const D: u32 = 42;
16492
16493
16494 fn main() {
16495 println!("hello");
16496
16497 println!("world");
16498 }"#
16499 .unindent(),
16500 );
16501}
16502
16503#[gpui::test]
16504async fn test_stage_and_unstage_added_file_hunk(
16505 executor: BackgroundExecutor,
16506 cx: &mut TestAppContext,
16507) {
16508 init_test(cx, |_| {});
16509
16510 let mut cx = EditorTestContext::new(cx).await;
16511 cx.update_editor(|editor, _, cx| {
16512 editor.set_expand_all_diff_hunks(cx);
16513 });
16514
16515 let working_copy = r#"
16516 ˇfn main() {
16517 println!("hello, world!");
16518 }
16519 "#
16520 .unindent();
16521
16522 cx.set_state(&working_copy);
16523 executor.run_until_parked();
16524
16525 cx.assert_state_with_diff(
16526 r#"
16527 + ˇfn main() {
16528 + println!("hello, world!");
16529 + }
16530 "#
16531 .unindent(),
16532 );
16533 cx.assert_index_text(None);
16534
16535 cx.update_editor(|editor, window, cx| {
16536 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16537 });
16538 executor.run_until_parked();
16539 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
16540 cx.assert_state_with_diff(
16541 r#"
16542 + ˇfn main() {
16543 + println!("hello, world!");
16544 + }
16545 "#
16546 .unindent(),
16547 );
16548
16549 cx.update_editor(|editor, window, cx| {
16550 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16551 });
16552 executor.run_until_parked();
16553 cx.assert_index_text(None);
16554}
16555
16556async fn setup_indent_guides_editor(
16557 text: &str,
16558 cx: &mut TestAppContext,
16559) -> (BufferId, EditorTestContext) {
16560 init_test(cx, |_| {});
16561
16562 let mut cx = EditorTestContext::new(cx).await;
16563
16564 let buffer_id = cx.update_editor(|editor, window, cx| {
16565 editor.set_text(text, window, cx);
16566 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
16567
16568 buffer_ids[0]
16569 });
16570
16571 (buffer_id, cx)
16572}
16573
16574fn assert_indent_guides(
16575 range: Range<u32>,
16576 expected: Vec<IndentGuide>,
16577 active_indices: Option<Vec<usize>>,
16578 cx: &mut EditorTestContext,
16579) {
16580 let indent_guides = cx.update_editor(|editor, window, cx| {
16581 let snapshot = editor.snapshot(window, cx).display_snapshot;
16582 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
16583 editor,
16584 MultiBufferRow(range.start)..MultiBufferRow(range.end),
16585 true,
16586 &snapshot,
16587 cx,
16588 );
16589
16590 indent_guides.sort_by(|a, b| {
16591 a.depth.cmp(&b.depth).then(
16592 a.start_row
16593 .cmp(&b.start_row)
16594 .then(a.end_row.cmp(&b.end_row)),
16595 )
16596 });
16597 indent_guides
16598 });
16599
16600 if let Some(expected) = active_indices {
16601 let active_indices = cx.update_editor(|editor, window, cx| {
16602 let snapshot = editor.snapshot(window, cx).display_snapshot;
16603 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
16604 });
16605
16606 assert_eq!(
16607 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
16608 expected,
16609 "Active indent guide indices do not match"
16610 );
16611 }
16612
16613 assert_eq!(indent_guides, expected, "Indent guides do not match");
16614}
16615
16616fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
16617 IndentGuide {
16618 buffer_id,
16619 start_row: MultiBufferRow(start_row),
16620 end_row: MultiBufferRow(end_row),
16621 depth,
16622 tab_size: 4,
16623 settings: IndentGuideSettings {
16624 enabled: true,
16625 line_width: 1,
16626 active_line_width: 1,
16627 ..Default::default()
16628 },
16629 }
16630}
16631
16632#[gpui::test]
16633async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
16634 let (buffer_id, mut cx) = setup_indent_guides_editor(
16635 &"
16636 fn main() {
16637 let a = 1;
16638 }"
16639 .unindent(),
16640 cx,
16641 )
16642 .await;
16643
16644 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
16645}
16646
16647#[gpui::test]
16648async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
16649 let (buffer_id, mut cx) = setup_indent_guides_editor(
16650 &"
16651 fn main() {
16652 let a = 1;
16653 let b = 2;
16654 }"
16655 .unindent(),
16656 cx,
16657 )
16658 .await;
16659
16660 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
16661}
16662
16663#[gpui::test]
16664async fn test_indent_guide_nested(cx: &mut TestAppContext) {
16665 let (buffer_id, mut cx) = setup_indent_guides_editor(
16666 &"
16667 fn main() {
16668 let a = 1;
16669 if a == 3 {
16670 let b = 2;
16671 } else {
16672 let c = 3;
16673 }
16674 }"
16675 .unindent(),
16676 cx,
16677 )
16678 .await;
16679
16680 assert_indent_guides(
16681 0..8,
16682 vec![
16683 indent_guide(buffer_id, 1, 6, 0),
16684 indent_guide(buffer_id, 3, 3, 1),
16685 indent_guide(buffer_id, 5, 5, 1),
16686 ],
16687 None,
16688 &mut cx,
16689 );
16690}
16691
16692#[gpui::test]
16693async fn test_indent_guide_tab(cx: &mut TestAppContext) {
16694 let (buffer_id, mut cx) = setup_indent_guides_editor(
16695 &"
16696 fn main() {
16697 let a = 1;
16698 let b = 2;
16699 let c = 3;
16700 }"
16701 .unindent(),
16702 cx,
16703 )
16704 .await;
16705
16706 assert_indent_guides(
16707 0..5,
16708 vec![
16709 indent_guide(buffer_id, 1, 3, 0),
16710 indent_guide(buffer_id, 2, 2, 1),
16711 ],
16712 None,
16713 &mut cx,
16714 );
16715}
16716
16717#[gpui::test]
16718async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
16719 let (buffer_id, mut cx) = setup_indent_guides_editor(
16720 &"
16721 fn main() {
16722 let a = 1;
16723
16724 let c = 3;
16725 }"
16726 .unindent(),
16727 cx,
16728 )
16729 .await;
16730
16731 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
16732}
16733
16734#[gpui::test]
16735async fn test_indent_guide_complex(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 if a == 3 {
16744 let b = 2;
16745 } else {
16746 let c = 3;
16747 }
16748 }"
16749 .unindent(),
16750 cx,
16751 )
16752 .await;
16753
16754 assert_indent_guides(
16755 0..11,
16756 vec![
16757 indent_guide(buffer_id, 1, 9, 0),
16758 indent_guide(buffer_id, 6, 6, 1),
16759 indent_guide(buffer_id, 8, 8, 1),
16760 ],
16761 None,
16762 &mut cx,
16763 );
16764}
16765
16766#[gpui::test]
16767async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
16768 let (buffer_id, mut cx) = setup_indent_guides_editor(
16769 &"
16770 fn main() {
16771 let a = 1;
16772
16773 let c = 3;
16774
16775 if a == 3 {
16776 let b = 2;
16777 } else {
16778 let c = 3;
16779 }
16780 }"
16781 .unindent(),
16782 cx,
16783 )
16784 .await;
16785
16786 assert_indent_guides(
16787 1..11,
16788 vec![
16789 indent_guide(buffer_id, 1, 9, 0),
16790 indent_guide(buffer_id, 6, 6, 1),
16791 indent_guide(buffer_id, 8, 8, 1),
16792 ],
16793 None,
16794 &mut cx,
16795 );
16796}
16797
16798#[gpui::test]
16799async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
16800 let (buffer_id, mut cx) = setup_indent_guides_editor(
16801 &"
16802 fn main() {
16803 let a = 1;
16804
16805 let c = 3;
16806
16807 if a == 3 {
16808 let b = 2;
16809 } else {
16810 let c = 3;
16811 }
16812 }"
16813 .unindent(),
16814 cx,
16815 )
16816 .await;
16817
16818 assert_indent_guides(
16819 1..10,
16820 vec![
16821 indent_guide(buffer_id, 1, 9, 0),
16822 indent_guide(buffer_id, 6, 6, 1),
16823 indent_guide(buffer_id, 8, 8, 1),
16824 ],
16825 None,
16826 &mut cx,
16827 );
16828}
16829
16830#[gpui::test]
16831async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
16832 let (buffer_id, mut cx) = setup_indent_guides_editor(
16833 &"
16834 block1
16835 block2
16836 block3
16837 block4
16838 block2
16839 block1
16840 block1"
16841 .unindent(),
16842 cx,
16843 )
16844 .await;
16845
16846 assert_indent_guides(
16847 1..10,
16848 vec![
16849 indent_guide(buffer_id, 1, 4, 0),
16850 indent_guide(buffer_id, 2, 3, 1),
16851 indent_guide(buffer_id, 3, 3, 2),
16852 ],
16853 None,
16854 &mut cx,
16855 );
16856}
16857
16858#[gpui::test]
16859async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
16860 let (buffer_id, mut cx) = setup_indent_guides_editor(
16861 &"
16862 block1
16863 block2
16864 block3
16865
16866 block1
16867 block1"
16868 .unindent(),
16869 cx,
16870 )
16871 .await;
16872
16873 assert_indent_guides(
16874 0..6,
16875 vec![
16876 indent_guide(buffer_id, 1, 2, 0),
16877 indent_guide(buffer_id, 2, 2, 1),
16878 ],
16879 None,
16880 &mut cx,
16881 );
16882}
16883
16884#[gpui::test]
16885async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
16886 let (buffer_id, mut cx) = setup_indent_guides_editor(
16887 &"
16888 block1
16889
16890
16891
16892 block2
16893 "
16894 .unindent(),
16895 cx,
16896 )
16897 .await;
16898
16899 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
16900}
16901
16902#[gpui::test]
16903async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
16904 let (buffer_id, mut cx) = setup_indent_guides_editor(
16905 &"
16906 def a:
16907 \tb = 3
16908 \tif True:
16909 \t\tc = 4
16910 \t\td = 5
16911 \tprint(b)
16912 "
16913 .unindent(),
16914 cx,
16915 )
16916 .await;
16917
16918 assert_indent_guides(
16919 0..6,
16920 vec![
16921 indent_guide(buffer_id, 1, 5, 0),
16922 indent_guide(buffer_id, 3, 4, 1),
16923 ],
16924 None,
16925 &mut cx,
16926 );
16927}
16928
16929#[gpui::test]
16930async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
16931 let (buffer_id, mut cx) = setup_indent_guides_editor(
16932 &"
16933 fn main() {
16934 let a = 1;
16935 }"
16936 .unindent(),
16937 cx,
16938 )
16939 .await;
16940
16941 cx.update_editor(|editor, window, cx| {
16942 editor.change_selections(None, window, cx, |s| {
16943 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
16944 });
16945 });
16946
16947 assert_indent_guides(
16948 0..3,
16949 vec![indent_guide(buffer_id, 1, 1, 0)],
16950 Some(vec![0]),
16951 &mut cx,
16952 );
16953}
16954
16955#[gpui::test]
16956async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
16957 let (buffer_id, mut cx) = setup_indent_guides_editor(
16958 &"
16959 fn main() {
16960 if 1 == 2 {
16961 let a = 1;
16962 }
16963 }"
16964 .unindent(),
16965 cx,
16966 )
16967 .await;
16968
16969 cx.update_editor(|editor, window, cx| {
16970 editor.change_selections(None, window, cx, |s| {
16971 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
16972 });
16973 });
16974
16975 assert_indent_guides(
16976 0..4,
16977 vec![
16978 indent_guide(buffer_id, 1, 3, 0),
16979 indent_guide(buffer_id, 2, 2, 1),
16980 ],
16981 Some(vec![1]),
16982 &mut cx,
16983 );
16984
16985 cx.update_editor(|editor, window, cx| {
16986 editor.change_selections(None, window, cx, |s| {
16987 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
16988 });
16989 });
16990
16991 assert_indent_guides(
16992 0..4,
16993 vec![
16994 indent_guide(buffer_id, 1, 3, 0),
16995 indent_guide(buffer_id, 2, 2, 1),
16996 ],
16997 Some(vec![1]),
16998 &mut cx,
16999 );
17000
17001 cx.update_editor(|editor, window, cx| {
17002 editor.change_selections(None, window, cx, |s| {
17003 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
17004 });
17005 });
17006
17007 assert_indent_guides(
17008 0..4,
17009 vec![
17010 indent_guide(buffer_id, 1, 3, 0),
17011 indent_guide(buffer_id, 2, 2, 1),
17012 ],
17013 Some(vec![0]),
17014 &mut cx,
17015 );
17016}
17017
17018#[gpui::test]
17019async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
17020 let (buffer_id, mut cx) = setup_indent_guides_editor(
17021 &"
17022 fn main() {
17023 let a = 1;
17024
17025 let b = 2;
17026 }"
17027 .unindent(),
17028 cx,
17029 )
17030 .await;
17031
17032 cx.update_editor(|editor, window, cx| {
17033 editor.change_selections(None, window, cx, |s| {
17034 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
17035 });
17036 });
17037
17038 assert_indent_guides(
17039 0..5,
17040 vec![indent_guide(buffer_id, 1, 3, 0)],
17041 Some(vec![0]),
17042 &mut cx,
17043 );
17044}
17045
17046#[gpui::test]
17047async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
17048 let (buffer_id, mut cx) = setup_indent_guides_editor(
17049 &"
17050 def m:
17051 a = 1
17052 pass"
17053 .unindent(),
17054 cx,
17055 )
17056 .await;
17057
17058 cx.update_editor(|editor, window, cx| {
17059 editor.change_selections(None, window, cx, |s| {
17060 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
17061 });
17062 });
17063
17064 assert_indent_guides(
17065 0..3,
17066 vec![indent_guide(buffer_id, 1, 2, 0)],
17067 Some(vec![0]),
17068 &mut cx,
17069 );
17070}
17071
17072#[gpui::test]
17073async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
17074 init_test(cx, |_| {});
17075 let mut cx = EditorTestContext::new(cx).await;
17076 let text = indoc! {
17077 "
17078 impl A {
17079 fn b() {
17080 0;
17081 3;
17082 5;
17083 6;
17084 7;
17085 }
17086 }
17087 "
17088 };
17089 let base_text = indoc! {
17090 "
17091 impl A {
17092 fn b() {
17093 0;
17094 1;
17095 2;
17096 3;
17097 4;
17098 }
17099 fn c() {
17100 5;
17101 6;
17102 7;
17103 }
17104 }
17105 "
17106 };
17107
17108 cx.update_editor(|editor, window, cx| {
17109 editor.set_text(text, window, cx);
17110
17111 editor.buffer().update(cx, |multibuffer, cx| {
17112 let buffer = multibuffer.as_singleton().unwrap();
17113 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
17114
17115 multibuffer.set_all_diff_hunks_expanded(cx);
17116 multibuffer.add_diff(diff, cx);
17117
17118 buffer.read(cx).remote_id()
17119 })
17120 });
17121 cx.run_until_parked();
17122
17123 cx.assert_state_with_diff(
17124 indoc! { "
17125 impl A {
17126 fn b() {
17127 0;
17128 - 1;
17129 - 2;
17130 3;
17131 - 4;
17132 - }
17133 - fn c() {
17134 5;
17135 6;
17136 7;
17137 }
17138 }
17139 ˇ"
17140 }
17141 .to_string(),
17142 );
17143
17144 let mut actual_guides = cx.update_editor(|editor, window, cx| {
17145 editor
17146 .snapshot(window, cx)
17147 .buffer_snapshot
17148 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
17149 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
17150 .collect::<Vec<_>>()
17151 });
17152 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
17153 assert_eq!(
17154 actual_guides,
17155 vec![
17156 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
17157 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
17158 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
17159 ]
17160 );
17161}
17162
17163#[gpui::test]
17164async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17165 init_test(cx, |_| {});
17166 let mut cx = EditorTestContext::new(cx).await;
17167
17168 let diff_base = r#"
17169 a
17170 b
17171 c
17172 "#
17173 .unindent();
17174
17175 cx.set_state(
17176 &r#"
17177 ˇA
17178 b
17179 C
17180 "#
17181 .unindent(),
17182 );
17183 cx.set_head_text(&diff_base);
17184 cx.update_editor(|editor, window, cx| {
17185 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17186 });
17187 executor.run_until_parked();
17188
17189 let both_hunks_expanded = r#"
17190 - a
17191 + ˇA
17192 b
17193 - c
17194 + C
17195 "#
17196 .unindent();
17197
17198 cx.assert_state_with_diff(both_hunks_expanded.clone());
17199
17200 let hunk_ranges = cx.update_editor(|editor, window, cx| {
17201 let snapshot = editor.snapshot(window, cx);
17202 let hunks = editor
17203 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
17204 .collect::<Vec<_>>();
17205 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
17206 let buffer_id = hunks[0].buffer_id;
17207 hunks
17208 .into_iter()
17209 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
17210 .collect::<Vec<_>>()
17211 });
17212 assert_eq!(hunk_ranges.len(), 2);
17213
17214 cx.update_editor(|editor, _, cx| {
17215 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
17216 });
17217 executor.run_until_parked();
17218
17219 let second_hunk_expanded = r#"
17220 ˇA
17221 b
17222 - c
17223 + C
17224 "#
17225 .unindent();
17226
17227 cx.assert_state_with_diff(second_hunk_expanded);
17228
17229 cx.update_editor(|editor, _, cx| {
17230 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
17231 });
17232 executor.run_until_parked();
17233
17234 cx.assert_state_with_diff(both_hunks_expanded.clone());
17235
17236 cx.update_editor(|editor, _, cx| {
17237 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
17238 });
17239 executor.run_until_parked();
17240
17241 let first_hunk_expanded = r#"
17242 - a
17243 + ˇA
17244 b
17245 C
17246 "#
17247 .unindent();
17248
17249 cx.assert_state_with_diff(first_hunk_expanded);
17250
17251 cx.update_editor(|editor, _, cx| {
17252 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
17253 });
17254 executor.run_until_parked();
17255
17256 cx.assert_state_with_diff(both_hunks_expanded);
17257
17258 cx.set_state(
17259 &r#"
17260 ˇA
17261 b
17262 "#
17263 .unindent(),
17264 );
17265 cx.run_until_parked();
17266
17267 // TODO this cursor position seems bad
17268 cx.assert_state_with_diff(
17269 r#"
17270 - ˇa
17271 + A
17272 b
17273 "#
17274 .unindent(),
17275 );
17276
17277 cx.update_editor(|editor, window, cx| {
17278 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17279 });
17280
17281 cx.assert_state_with_diff(
17282 r#"
17283 - ˇa
17284 + A
17285 b
17286 - c
17287 "#
17288 .unindent(),
17289 );
17290
17291 let hunk_ranges = cx.update_editor(|editor, window, cx| {
17292 let snapshot = editor.snapshot(window, cx);
17293 let hunks = editor
17294 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
17295 .collect::<Vec<_>>();
17296 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
17297 let buffer_id = hunks[0].buffer_id;
17298 hunks
17299 .into_iter()
17300 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
17301 .collect::<Vec<_>>()
17302 });
17303 assert_eq!(hunk_ranges.len(), 2);
17304
17305 cx.update_editor(|editor, _, cx| {
17306 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
17307 });
17308 executor.run_until_parked();
17309
17310 cx.assert_state_with_diff(
17311 r#"
17312 - ˇa
17313 + A
17314 b
17315 "#
17316 .unindent(),
17317 );
17318}
17319
17320#[gpui::test]
17321async fn test_toggle_deletion_hunk_at_start_of_file(
17322 executor: BackgroundExecutor,
17323 cx: &mut TestAppContext,
17324) {
17325 init_test(cx, |_| {});
17326 let mut cx = EditorTestContext::new(cx).await;
17327
17328 let diff_base = r#"
17329 a
17330 b
17331 c
17332 "#
17333 .unindent();
17334
17335 cx.set_state(
17336 &r#"
17337 ˇb
17338 c
17339 "#
17340 .unindent(),
17341 );
17342 cx.set_head_text(&diff_base);
17343 cx.update_editor(|editor, window, cx| {
17344 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17345 });
17346 executor.run_until_parked();
17347
17348 let hunk_expanded = r#"
17349 - a
17350 ˇb
17351 c
17352 "#
17353 .unindent();
17354
17355 cx.assert_state_with_diff(hunk_expanded.clone());
17356
17357 let hunk_ranges = cx.update_editor(|editor, window, cx| {
17358 let snapshot = editor.snapshot(window, cx);
17359 let hunks = editor
17360 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
17361 .collect::<Vec<_>>();
17362 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
17363 let buffer_id = hunks[0].buffer_id;
17364 hunks
17365 .into_iter()
17366 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
17367 .collect::<Vec<_>>()
17368 });
17369 assert_eq!(hunk_ranges.len(), 1);
17370
17371 cx.update_editor(|editor, _, cx| {
17372 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
17373 });
17374 executor.run_until_parked();
17375
17376 let hunk_collapsed = r#"
17377 ˇb
17378 c
17379 "#
17380 .unindent();
17381
17382 cx.assert_state_with_diff(hunk_collapsed);
17383
17384 cx.update_editor(|editor, _, cx| {
17385 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
17386 });
17387 executor.run_until_parked();
17388
17389 cx.assert_state_with_diff(hunk_expanded.clone());
17390}
17391
17392#[gpui::test]
17393async fn test_display_diff_hunks(cx: &mut TestAppContext) {
17394 init_test(cx, |_| {});
17395
17396 let fs = FakeFs::new(cx.executor());
17397 fs.insert_tree(
17398 path!("/test"),
17399 json!({
17400 ".git": {},
17401 "file-1": "ONE\n",
17402 "file-2": "TWO\n",
17403 "file-3": "THREE\n",
17404 }),
17405 )
17406 .await;
17407
17408 fs.set_head_for_repo(
17409 path!("/test/.git").as_ref(),
17410 &[
17411 ("file-1".into(), "one\n".into()),
17412 ("file-2".into(), "two\n".into()),
17413 ("file-3".into(), "three\n".into()),
17414 ],
17415 );
17416
17417 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
17418 let mut buffers = vec![];
17419 for i in 1..=3 {
17420 let buffer = project
17421 .update(cx, |project, cx| {
17422 let path = format!(path!("/test/file-{}"), i);
17423 project.open_local_buffer(path, cx)
17424 })
17425 .await
17426 .unwrap();
17427 buffers.push(buffer);
17428 }
17429
17430 let multibuffer = cx.new(|cx| {
17431 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
17432 multibuffer.set_all_diff_hunks_expanded(cx);
17433 for buffer in &buffers {
17434 let snapshot = buffer.read(cx).snapshot();
17435 multibuffer.set_excerpts_for_path(
17436 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
17437 buffer.clone(),
17438 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
17439 DEFAULT_MULTIBUFFER_CONTEXT,
17440 cx,
17441 );
17442 }
17443 multibuffer
17444 });
17445
17446 let editor = cx.add_window(|window, cx| {
17447 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
17448 });
17449 cx.run_until_parked();
17450
17451 let snapshot = editor
17452 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
17453 .unwrap();
17454 let hunks = snapshot
17455 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
17456 .map(|hunk| match hunk {
17457 DisplayDiffHunk::Unfolded {
17458 display_row_range, ..
17459 } => display_row_range,
17460 DisplayDiffHunk::Folded { .. } => unreachable!(),
17461 })
17462 .collect::<Vec<_>>();
17463 assert_eq!(
17464 hunks,
17465 [
17466 DisplayRow(2)..DisplayRow(4),
17467 DisplayRow(7)..DisplayRow(9),
17468 DisplayRow(12)..DisplayRow(14),
17469 ]
17470 );
17471}
17472
17473#[gpui::test]
17474async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
17475 init_test(cx, |_| {});
17476
17477 let mut cx = EditorTestContext::new(cx).await;
17478 cx.set_head_text(indoc! { "
17479 one
17480 two
17481 three
17482 four
17483 five
17484 "
17485 });
17486 cx.set_index_text(indoc! { "
17487 one
17488 two
17489 three
17490 four
17491 five
17492 "
17493 });
17494 cx.set_state(indoc! {"
17495 one
17496 TWO
17497 ˇTHREE
17498 FOUR
17499 five
17500 "});
17501 cx.run_until_parked();
17502 cx.update_editor(|editor, window, cx| {
17503 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
17504 });
17505 cx.run_until_parked();
17506 cx.assert_index_text(Some(indoc! {"
17507 one
17508 TWO
17509 THREE
17510 FOUR
17511 five
17512 "}));
17513 cx.set_state(indoc! { "
17514 one
17515 TWO
17516 ˇTHREE-HUNDRED
17517 FOUR
17518 five
17519 "});
17520 cx.run_until_parked();
17521 cx.update_editor(|editor, window, cx| {
17522 let snapshot = editor.snapshot(window, cx);
17523 let hunks = editor
17524 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
17525 .collect::<Vec<_>>();
17526 assert_eq!(hunks.len(), 1);
17527 assert_eq!(
17528 hunks[0].status(),
17529 DiffHunkStatus {
17530 kind: DiffHunkStatusKind::Modified,
17531 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
17532 }
17533 );
17534
17535 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
17536 });
17537 cx.run_until_parked();
17538 cx.assert_index_text(Some(indoc! {"
17539 one
17540 TWO
17541 THREE-HUNDRED
17542 FOUR
17543 five
17544 "}));
17545}
17546
17547#[gpui::test]
17548fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
17549 init_test(cx, |_| {});
17550
17551 let editor = cx.add_window(|window, cx| {
17552 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
17553 build_editor(buffer, window, cx)
17554 });
17555
17556 let render_args = Arc::new(Mutex::new(None));
17557 let snapshot = editor
17558 .update(cx, |editor, window, cx| {
17559 let snapshot = editor.buffer().read(cx).snapshot(cx);
17560 let range =
17561 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
17562
17563 struct RenderArgs {
17564 row: MultiBufferRow,
17565 folded: bool,
17566 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
17567 }
17568
17569 let crease = Crease::inline(
17570 range,
17571 FoldPlaceholder::test(),
17572 {
17573 let toggle_callback = render_args.clone();
17574 move |row, folded, callback, _window, _cx| {
17575 *toggle_callback.lock() = Some(RenderArgs {
17576 row,
17577 folded,
17578 callback,
17579 });
17580 div()
17581 }
17582 },
17583 |_row, _folded, _window, _cx| div(),
17584 );
17585
17586 editor.insert_creases(Some(crease), cx);
17587 let snapshot = editor.snapshot(window, cx);
17588 let _div = snapshot.render_crease_toggle(
17589 MultiBufferRow(1),
17590 false,
17591 cx.entity().clone(),
17592 window,
17593 cx,
17594 );
17595 snapshot
17596 })
17597 .unwrap();
17598
17599 let render_args = render_args.lock().take().unwrap();
17600 assert_eq!(render_args.row, MultiBufferRow(1));
17601 assert!(!render_args.folded);
17602 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
17603
17604 cx.update_window(*editor, |_, window, cx| {
17605 (render_args.callback)(true, window, cx)
17606 })
17607 .unwrap();
17608 let snapshot = editor
17609 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
17610 .unwrap();
17611 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
17612
17613 cx.update_window(*editor, |_, window, cx| {
17614 (render_args.callback)(false, window, cx)
17615 })
17616 .unwrap();
17617 let snapshot = editor
17618 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
17619 .unwrap();
17620 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
17621}
17622
17623#[gpui::test]
17624async fn test_input_text(cx: &mut TestAppContext) {
17625 init_test(cx, |_| {});
17626 let mut cx = EditorTestContext::new(cx).await;
17627
17628 cx.set_state(
17629 &r#"ˇone
17630 two
17631
17632 three
17633 fourˇ
17634 five
17635
17636 siˇx"#
17637 .unindent(),
17638 );
17639
17640 cx.dispatch_action(HandleInput(String::new()));
17641 cx.assert_editor_state(
17642 &r#"ˇone
17643 two
17644
17645 three
17646 fourˇ
17647 five
17648
17649 siˇx"#
17650 .unindent(),
17651 );
17652
17653 cx.dispatch_action(HandleInput("AAAA".to_string()));
17654 cx.assert_editor_state(
17655 &r#"AAAAˇone
17656 two
17657
17658 three
17659 fourAAAAˇ
17660 five
17661
17662 siAAAAˇx"#
17663 .unindent(),
17664 );
17665}
17666
17667#[gpui::test]
17668async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
17669 init_test(cx, |_| {});
17670
17671 let mut cx = EditorTestContext::new(cx).await;
17672 cx.set_state(
17673 r#"let foo = 1;
17674let foo = 2;
17675let foo = 3;
17676let fooˇ = 4;
17677let foo = 5;
17678let foo = 6;
17679let foo = 7;
17680let foo = 8;
17681let foo = 9;
17682let foo = 10;
17683let foo = 11;
17684let foo = 12;
17685let foo = 13;
17686let foo = 14;
17687let foo = 15;"#,
17688 );
17689
17690 cx.update_editor(|e, window, cx| {
17691 assert_eq!(
17692 e.next_scroll_position,
17693 NextScrollCursorCenterTopBottom::Center,
17694 "Default next scroll direction is center",
17695 );
17696
17697 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17698 assert_eq!(
17699 e.next_scroll_position,
17700 NextScrollCursorCenterTopBottom::Top,
17701 "After center, next scroll direction should be top",
17702 );
17703
17704 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17705 assert_eq!(
17706 e.next_scroll_position,
17707 NextScrollCursorCenterTopBottom::Bottom,
17708 "After top, next scroll direction should be bottom",
17709 );
17710
17711 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17712 assert_eq!(
17713 e.next_scroll_position,
17714 NextScrollCursorCenterTopBottom::Center,
17715 "After bottom, scrolling should start over",
17716 );
17717
17718 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17719 assert_eq!(
17720 e.next_scroll_position,
17721 NextScrollCursorCenterTopBottom::Top,
17722 "Scrolling continues if retriggered fast enough"
17723 );
17724 });
17725
17726 cx.executor()
17727 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
17728 cx.executor().run_until_parked();
17729 cx.update_editor(|e, _, _| {
17730 assert_eq!(
17731 e.next_scroll_position,
17732 NextScrollCursorCenterTopBottom::Center,
17733 "If scrolling is not triggered fast enough, it should reset"
17734 );
17735 });
17736}
17737
17738#[gpui::test]
17739async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
17740 init_test(cx, |_| {});
17741 let mut cx = EditorLspTestContext::new_rust(
17742 lsp::ServerCapabilities {
17743 definition_provider: Some(lsp::OneOf::Left(true)),
17744 references_provider: Some(lsp::OneOf::Left(true)),
17745 ..lsp::ServerCapabilities::default()
17746 },
17747 cx,
17748 )
17749 .await;
17750
17751 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
17752 let go_to_definition = cx
17753 .lsp
17754 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
17755 move |params, _| async move {
17756 if empty_go_to_definition {
17757 Ok(None)
17758 } else {
17759 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
17760 uri: params.text_document_position_params.text_document.uri,
17761 range: lsp::Range::new(
17762 lsp::Position::new(4, 3),
17763 lsp::Position::new(4, 6),
17764 ),
17765 })))
17766 }
17767 },
17768 );
17769 let references = cx
17770 .lsp
17771 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
17772 Ok(Some(vec![lsp::Location {
17773 uri: params.text_document_position.text_document.uri,
17774 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
17775 }]))
17776 });
17777 (go_to_definition, references)
17778 };
17779
17780 cx.set_state(
17781 &r#"fn one() {
17782 let mut a = ˇtwo();
17783 }
17784
17785 fn two() {}"#
17786 .unindent(),
17787 );
17788 set_up_lsp_handlers(false, &mut cx);
17789 let navigated = cx
17790 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
17791 .await
17792 .expect("Failed to navigate to definition");
17793 assert_eq!(
17794 navigated,
17795 Navigated::Yes,
17796 "Should have navigated to definition from the GetDefinition response"
17797 );
17798 cx.assert_editor_state(
17799 &r#"fn one() {
17800 let mut a = two();
17801 }
17802
17803 fn «twoˇ»() {}"#
17804 .unindent(),
17805 );
17806
17807 let editors = cx.update_workspace(|workspace, _, cx| {
17808 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
17809 });
17810 cx.update_editor(|_, _, test_editor_cx| {
17811 assert_eq!(
17812 editors.len(),
17813 1,
17814 "Initially, only one, test, editor should be open in the workspace"
17815 );
17816 assert_eq!(
17817 test_editor_cx.entity(),
17818 editors.last().expect("Asserted len is 1").clone()
17819 );
17820 });
17821
17822 set_up_lsp_handlers(true, &mut cx);
17823 let navigated = cx
17824 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
17825 .await
17826 .expect("Failed to navigate to lookup references");
17827 assert_eq!(
17828 navigated,
17829 Navigated::Yes,
17830 "Should have navigated to references as a fallback after empty GoToDefinition response"
17831 );
17832 // We should not change the selections in the existing file,
17833 // if opening another milti buffer with the references
17834 cx.assert_editor_state(
17835 &r#"fn one() {
17836 let mut a = two();
17837 }
17838
17839 fn «twoˇ»() {}"#
17840 .unindent(),
17841 );
17842 let editors = cx.update_workspace(|workspace, _, cx| {
17843 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
17844 });
17845 cx.update_editor(|_, _, test_editor_cx| {
17846 assert_eq!(
17847 editors.len(),
17848 2,
17849 "After falling back to references search, we open a new editor with the results"
17850 );
17851 let references_fallback_text = editors
17852 .into_iter()
17853 .find(|new_editor| *new_editor != test_editor_cx.entity())
17854 .expect("Should have one non-test editor now")
17855 .read(test_editor_cx)
17856 .text(test_editor_cx);
17857 assert_eq!(
17858 references_fallback_text, "fn one() {\n let mut a = two();\n}",
17859 "Should use the range from the references response and not the GoToDefinition one"
17860 );
17861 });
17862}
17863
17864#[gpui::test]
17865async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
17866 init_test(cx, |_| {});
17867 cx.update(|cx| {
17868 let mut editor_settings = EditorSettings::get_global(cx).clone();
17869 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
17870 EditorSettings::override_global(editor_settings, cx);
17871 });
17872 let mut cx = EditorLspTestContext::new_rust(
17873 lsp::ServerCapabilities {
17874 definition_provider: Some(lsp::OneOf::Left(true)),
17875 references_provider: Some(lsp::OneOf::Left(true)),
17876 ..lsp::ServerCapabilities::default()
17877 },
17878 cx,
17879 )
17880 .await;
17881 let original_state = r#"fn one() {
17882 let mut a = ˇtwo();
17883 }
17884
17885 fn two() {}"#
17886 .unindent();
17887 cx.set_state(&original_state);
17888
17889 let mut go_to_definition = cx
17890 .lsp
17891 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
17892 move |_, _| async move { Ok(None) },
17893 );
17894 let _references = cx
17895 .lsp
17896 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
17897 panic!("Should not call for references with no go to definition fallback")
17898 });
17899
17900 let navigated = cx
17901 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
17902 .await
17903 .expect("Failed to navigate to lookup references");
17904 go_to_definition
17905 .next()
17906 .await
17907 .expect("Should have called the go_to_definition handler");
17908
17909 assert_eq!(
17910 navigated,
17911 Navigated::No,
17912 "Should have navigated to references as a fallback after empty GoToDefinition response"
17913 );
17914 cx.assert_editor_state(&original_state);
17915 let editors = cx.update_workspace(|workspace, _, cx| {
17916 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
17917 });
17918 cx.update_editor(|_, _, _| {
17919 assert_eq!(
17920 editors.len(),
17921 1,
17922 "After unsuccessful fallback, no other editor should have been opened"
17923 );
17924 });
17925}
17926
17927#[gpui::test]
17928async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
17929 init_test(cx, |_| {});
17930
17931 let language = Arc::new(Language::new(
17932 LanguageConfig::default(),
17933 Some(tree_sitter_rust::LANGUAGE.into()),
17934 ));
17935
17936 let text = r#"
17937 #[cfg(test)]
17938 mod tests() {
17939 #[test]
17940 fn runnable_1() {
17941 let a = 1;
17942 }
17943
17944 #[test]
17945 fn runnable_2() {
17946 let a = 1;
17947 let b = 2;
17948 }
17949 }
17950 "#
17951 .unindent();
17952
17953 let fs = FakeFs::new(cx.executor());
17954 fs.insert_file("/file.rs", Default::default()).await;
17955
17956 let project = Project::test(fs, ["/a".as_ref()], cx).await;
17957 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17958 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17959 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
17960 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
17961
17962 let editor = cx.new_window_entity(|window, cx| {
17963 Editor::new(
17964 EditorMode::full(),
17965 multi_buffer,
17966 Some(project.clone()),
17967 window,
17968 cx,
17969 )
17970 });
17971
17972 editor.update_in(cx, |editor, window, cx| {
17973 let snapshot = editor.buffer().read(cx).snapshot(cx);
17974 editor.tasks.insert(
17975 (buffer.read(cx).remote_id(), 3),
17976 RunnableTasks {
17977 templates: vec![],
17978 offset: snapshot.anchor_before(43),
17979 column: 0,
17980 extra_variables: HashMap::default(),
17981 context_range: BufferOffset(43)..BufferOffset(85),
17982 },
17983 );
17984 editor.tasks.insert(
17985 (buffer.read(cx).remote_id(), 8),
17986 RunnableTasks {
17987 templates: vec![],
17988 offset: snapshot.anchor_before(86),
17989 column: 0,
17990 extra_variables: HashMap::default(),
17991 context_range: BufferOffset(86)..BufferOffset(191),
17992 },
17993 );
17994
17995 // Test finding task when cursor is inside function body
17996 editor.change_selections(None, window, cx, |s| {
17997 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
17998 });
17999 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
18000 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
18001
18002 // Test finding task when cursor is on function name
18003 editor.change_selections(None, window, cx, |s| {
18004 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
18005 });
18006 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
18007 assert_eq!(row, 8, "Should find task when cursor is on function name");
18008 });
18009}
18010
18011#[gpui::test]
18012async fn test_folding_buffers(cx: &mut TestAppContext) {
18013 init_test(cx, |_| {});
18014
18015 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
18016 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
18017 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
18018
18019 let fs = FakeFs::new(cx.executor());
18020 fs.insert_tree(
18021 path!("/a"),
18022 json!({
18023 "first.rs": sample_text_1,
18024 "second.rs": sample_text_2,
18025 "third.rs": sample_text_3,
18026 }),
18027 )
18028 .await;
18029 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18030 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18031 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18032 let worktree = project.update(cx, |project, cx| {
18033 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
18034 assert_eq!(worktrees.len(), 1);
18035 worktrees.pop().unwrap()
18036 });
18037 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
18038
18039 let buffer_1 = project
18040 .update(cx, |project, cx| {
18041 project.open_buffer((worktree_id, "first.rs"), cx)
18042 })
18043 .await
18044 .unwrap();
18045 let buffer_2 = project
18046 .update(cx, |project, cx| {
18047 project.open_buffer((worktree_id, "second.rs"), cx)
18048 })
18049 .await
18050 .unwrap();
18051 let buffer_3 = project
18052 .update(cx, |project, cx| {
18053 project.open_buffer((worktree_id, "third.rs"), cx)
18054 })
18055 .await
18056 .unwrap();
18057
18058 let multi_buffer = cx.new(|cx| {
18059 let mut multi_buffer = MultiBuffer::new(ReadWrite);
18060 multi_buffer.push_excerpts(
18061 buffer_1.clone(),
18062 [
18063 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18064 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18065 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18066 ],
18067 cx,
18068 );
18069 multi_buffer.push_excerpts(
18070 buffer_2.clone(),
18071 [
18072 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18073 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18074 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18075 ],
18076 cx,
18077 );
18078 multi_buffer.push_excerpts(
18079 buffer_3.clone(),
18080 [
18081 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18082 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18083 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18084 ],
18085 cx,
18086 );
18087 multi_buffer
18088 });
18089 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18090 Editor::new(
18091 EditorMode::full(),
18092 multi_buffer.clone(),
18093 Some(project.clone()),
18094 window,
18095 cx,
18096 )
18097 });
18098
18099 assert_eq!(
18100 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18101 "\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",
18102 );
18103
18104 multi_buffer_editor.update(cx, |editor, cx| {
18105 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
18106 });
18107 assert_eq!(
18108 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18109 "\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",
18110 "After folding the first buffer, its text should not be displayed"
18111 );
18112
18113 multi_buffer_editor.update(cx, |editor, cx| {
18114 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
18115 });
18116 assert_eq!(
18117 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18118 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
18119 "After folding the second buffer, its text should not be displayed"
18120 );
18121
18122 multi_buffer_editor.update(cx, |editor, cx| {
18123 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
18124 });
18125 assert_eq!(
18126 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18127 "\n\n\n\n\n",
18128 "After folding the third buffer, its text should not be displayed"
18129 );
18130
18131 // Emulate selection inside the fold logic, that should work
18132 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18133 editor
18134 .snapshot(window, cx)
18135 .next_line_boundary(Point::new(0, 4));
18136 });
18137
18138 multi_buffer_editor.update(cx, |editor, cx| {
18139 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
18140 });
18141 assert_eq!(
18142 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18143 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
18144 "After unfolding the second buffer, its text should be displayed"
18145 );
18146
18147 // Typing inside of buffer 1 causes that buffer to be unfolded.
18148 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18149 assert_eq!(
18150 multi_buffer
18151 .read(cx)
18152 .snapshot(cx)
18153 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
18154 .collect::<String>(),
18155 "bbbb"
18156 );
18157 editor.change_selections(None, window, cx, |selections| {
18158 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
18159 });
18160 editor.handle_input("B", window, cx);
18161 });
18162
18163 assert_eq!(
18164 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18165 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
18166 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
18167 );
18168
18169 multi_buffer_editor.update(cx, |editor, cx| {
18170 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
18171 });
18172 assert_eq!(
18173 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18174 "\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",
18175 "After unfolding the all buffers, all original text should be displayed"
18176 );
18177}
18178
18179#[gpui::test]
18180async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
18181 init_test(cx, |_| {});
18182
18183 let sample_text_1 = "1111\n2222\n3333".to_string();
18184 let sample_text_2 = "4444\n5555\n6666".to_string();
18185 let sample_text_3 = "7777\n8888\n9999".to_string();
18186
18187 let fs = FakeFs::new(cx.executor());
18188 fs.insert_tree(
18189 path!("/a"),
18190 json!({
18191 "first.rs": sample_text_1,
18192 "second.rs": sample_text_2,
18193 "third.rs": sample_text_3,
18194 }),
18195 )
18196 .await;
18197 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18198 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18199 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18200 let worktree = project.update(cx, |project, cx| {
18201 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
18202 assert_eq!(worktrees.len(), 1);
18203 worktrees.pop().unwrap()
18204 });
18205 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
18206
18207 let buffer_1 = project
18208 .update(cx, |project, cx| {
18209 project.open_buffer((worktree_id, "first.rs"), cx)
18210 })
18211 .await
18212 .unwrap();
18213 let buffer_2 = project
18214 .update(cx, |project, cx| {
18215 project.open_buffer((worktree_id, "second.rs"), cx)
18216 })
18217 .await
18218 .unwrap();
18219 let buffer_3 = project
18220 .update(cx, |project, cx| {
18221 project.open_buffer((worktree_id, "third.rs"), cx)
18222 })
18223 .await
18224 .unwrap();
18225
18226 let multi_buffer = cx.new(|cx| {
18227 let mut multi_buffer = MultiBuffer::new(ReadWrite);
18228 multi_buffer.push_excerpts(
18229 buffer_1.clone(),
18230 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
18231 cx,
18232 );
18233 multi_buffer.push_excerpts(
18234 buffer_2.clone(),
18235 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
18236 cx,
18237 );
18238 multi_buffer.push_excerpts(
18239 buffer_3.clone(),
18240 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
18241 cx,
18242 );
18243 multi_buffer
18244 });
18245
18246 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18247 Editor::new(
18248 EditorMode::full(),
18249 multi_buffer,
18250 Some(project.clone()),
18251 window,
18252 cx,
18253 )
18254 });
18255
18256 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
18257 assert_eq!(
18258 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18259 full_text,
18260 );
18261
18262 multi_buffer_editor.update(cx, |editor, cx| {
18263 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
18264 });
18265 assert_eq!(
18266 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18267 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
18268 "After folding the first buffer, its text should not be displayed"
18269 );
18270
18271 multi_buffer_editor.update(cx, |editor, cx| {
18272 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
18273 });
18274
18275 assert_eq!(
18276 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18277 "\n\n\n\n\n\n7777\n8888\n9999",
18278 "After folding the second buffer, its text should not be displayed"
18279 );
18280
18281 multi_buffer_editor.update(cx, |editor, cx| {
18282 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
18283 });
18284 assert_eq!(
18285 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18286 "\n\n\n\n\n",
18287 "After folding the third buffer, its text should not be displayed"
18288 );
18289
18290 multi_buffer_editor.update(cx, |editor, cx| {
18291 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
18292 });
18293 assert_eq!(
18294 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18295 "\n\n\n\n4444\n5555\n6666\n\n",
18296 "After unfolding the second buffer, its text should be displayed"
18297 );
18298
18299 multi_buffer_editor.update(cx, |editor, cx| {
18300 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
18301 });
18302 assert_eq!(
18303 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18304 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
18305 "After unfolding the first buffer, its text should be displayed"
18306 );
18307
18308 multi_buffer_editor.update(cx, |editor, cx| {
18309 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
18310 });
18311 assert_eq!(
18312 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18313 full_text,
18314 "After unfolding all buffers, all original text should be displayed"
18315 );
18316}
18317
18318#[gpui::test]
18319async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
18320 init_test(cx, |_| {});
18321
18322 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
18323
18324 let fs = FakeFs::new(cx.executor());
18325 fs.insert_tree(
18326 path!("/a"),
18327 json!({
18328 "main.rs": sample_text,
18329 }),
18330 )
18331 .await;
18332 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18333 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18334 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18335 let worktree = project.update(cx, |project, cx| {
18336 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
18337 assert_eq!(worktrees.len(), 1);
18338 worktrees.pop().unwrap()
18339 });
18340 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
18341
18342 let buffer_1 = project
18343 .update(cx, |project, cx| {
18344 project.open_buffer((worktree_id, "main.rs"), cx)
18345 })
18346 .await
18347 .unwrap();
18348
18349 let multi_buffer = cx.new(|cx| {
18350 let mut multi_buffer = MultiBuffer::new(ReadWrite);
18351 multi_buffer.push_excerpts(
18352 buffer_1.clone(),
18353 [ExcerptRange::new(
18354 Point::new(0, 0)
18355 ..Point::new(
18356 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
18357 0,
18358 ),
18359 )],
18360 cx,
18361 );
18362 multi_buffer
18363 });
18364 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18365 Editor::new(
18366 EditorMode::full(),
18367 multi_buffer,
18368 Some(project.clone()),
18369 window,
18370 cx,
18371 )
18372 });
18373
18374 let selection_range = Point::new(1, 0)..Point::new(2, 0);
18375 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18376 enum TestHighlight {}
18377 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
18378 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
18379 editor.highlight_text::<TestHighlight>(
18380 vec![highlight_range.clone()],
18381 HighlightStyle::color(Hsla::green()),
18382 cx,
18383 );
18384 editor.change_selections(None, window, cx, |s| s.select_ranges(Some(highlight_range)));
18385 });
18386
18387 let full_text = format!("\n\n{sample_text}");
18388 assert_eq!(
18389 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18390 full_text,
18391 );
18392}
18393
18394#[gpui::test]
18395async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
18396 init_test(cx, |_| {});
18397 cx.update(|cx| {
18398 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
18399 "keymaps/default-linux.json",
18400 cx,
18401 )
18402 .unwrap();
18403 cx.bind_keys(default_key_bindings);
18404 });
18405
18406 let (editor, cx) = cx.add_window_view(|window, cx| {
18407 let multi_buffer = MultiBuffer::build_multi(
18408 [
18409 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
18410 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
18411 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
18412 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
18413 ],
18414 cx,
18415 );
18416 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
18417
18418 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
18419 // fold all but the second buffer, so that we test navigating between two
18420 // adjacent folded buffers, as well as folded buffers at the start and
18421 // end the multibuffer
18422 editor.fold_buffer(buffer_ids[0], cx);
18423 editor.fold_buffer(buffer_ids[2], cx);
18424 editor.fold_buffer(buffer_ids[3], cx);
18425
18426 editor
18427 });
18428 cx.simulate_resize(size(px(1000.), px(1000.)));
18429
18430 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
18431 cx.assert_excerpts_with_selections(indoc! {"
18432 [EXCERPT]
18433 ˇ[FOLDED]
18434 [EXCERPT]
18435 a1
18436 b1
18437 [EXCERPT]
18438 [FOLDED]
18439 [EXCERPT]
18440 [FOLDED]
18441 "
18442 });
18443 cx.simulate_keystroke("down");
18444 cx.assert_excerpts_with_selections(indoc! {"
18445 [EXCERPT]
18446 [FOLDED]
18447 [EXCERPT]
18448 ˇa1
18449 b1
18450 [EXCERPT]
18451 [FOLDED]
18452 [EXCERPT]
18453 [FOLDED]
18454 "
18455 });
18456 cx.simulate_keystroke("down");
18457 cx.assert_excerpts_with_selections(indoc! {"
18458 [EXCERPT]
18459 [FOLDED]
18460 [EXCERPT]
18461 a1
18462 ˇb1
18463 [EXCERPT]
18464 [FOLDED]
18465 [EXCERPT]
18466 [FOLDED]
18467 "
18468 });
18469 cx.simulate_keystroke("down");
18470 cx.assert_excerpts_with_selections(indoc! {"
18471 [EXCERPT]
18472 [FOLDED]
18473 [EXCERPT]
18474 a1
18475 b1
18476 ˇ[EXCERPT]
18477 [FOLDED]
18478 [EXCERPT]
18479 [FOLDED]
18480 "
18481 });
18482 cx.simulate_keystroke("down");
18483 cx.assert_excerpts_with_selections(indoc! {"
18484 [EXCERPT]
18485 [FOLDED]
18486 [EXCERPT]
18487 a1
18488 b1
18489 [EXCERPT]
18490 ˇ[FOLDED]
18491 [EXCERPT]
18492 [FOLDED]
18493 "
18494 });
18495 for _ in 0..5 {
18496 cx.simulate_keystroke("down");
18497 cx.assert_excerpts_with_selections(indoc! {"
18498 [EXCERPT]
18499 [FOLDED]
18500 [EXCERPT]
18501 a1
18502 b1
18503 [EXCERPT]
18504 [FOLDED]
18505 [EXCERPT]
18506 ˇ[FOLDED]
18507 "
18508 });
18509 }
18510
18511 cx.simulate_keystroke("up");
18512 cx.assert_excerpts_with_selections(indoc! {"
18513 [EXCERPT]
18514 [FOLDED]
18515 [EXCERPT]
18516 a1
18517 b1
18518 [EXCERPT]
18519 ˇ[FOLDED]
18520 [EXCERPT]
18521 [FOLDED]
18522 "
18523 });
18524 cx.simulate_keystroke("up");
18525 cx.assert_excerpts_with_selections(indoc! {"
18526 [EXCERPT]
18527 [FOLDED]
18528 [EXCERPT]
18529 a1
18530 b1
18531 ˇ[EXCERPT]
18532 [FOLDED]
18533 [EXCERPT]
18534 [FOLDED]
18535 "
18536 });
18537 cx.simulate_keystroke("up");
18538 cx.assert_excerpts_with_selections(indoc! {"
18539 [EXCERPT]
18540 [FOLDED]
18541 [EXCERPT]
18542 a1
18543 ˇb1
18544 [EXCERPT]
18545 [FOLDED]
18546 [EXCERPT]
18547 [FOLDED]
18548 "
18549 });
18550 cx.simulate_keystroke("up");
18551 cx.assert_excerpts_with_selections(indoc! {"
18552 [EXCERPT]
18553 [FOLDED]
18554 [EXCERPT]
18555 ˇa1
18556 b1
18557 [EXCERPT]
18558 [FOLDED]
18559 [EXCERPT]
18560 [FOLDED]
18561 "
18562 });
18563 for _ in 0..5 {
18564 cx.simulate_keystroke("up");
18565 cx.assert_excerpts_with_selections(indoc! {"
18566 [EXCERPT]
18567 ˇ[FOLDED]
18568 [EXCERPT]
18569 a1
18570 b1
18571 [EXCERPT]
18572 [FOLDED]
18573 [EXCERPT]
18574 [FOLDED]
18575 "
18576 });
18577 }
18578}
18579
18580#[gpui::test]
18581async fn test_inline_completion_text(cx: &mut TestAppContext) {
18582 init_test(cx, |_| {});
18583
18584 // Simple insertion
18585 assert_highlighted_edits(
18586 "Hello, world!",
18587 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
18588 true,
18589 cx,
18590 |highlighted_edits, cx| {
18591 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
18592 assert_eq!(highlighted_edits.highlights.len(), 1);
18593 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
18594 assert_eq!(
18595 highlighted_edits.highlights[0].1.background_color,
18596 Some(cx.theme().status().created_background)
18597 );
18598 },
18599 )
18600 .await;
18601
18602 // Replacement
18603 assert_highlighted_edits(
18604 "This is a test.",
18605 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
18606 false,
18607 cx,
18608 |highlighted_edits, cx| {
18609 assert_eq!(highlighted_edits.text, "That is a test.");
18610 assert_eq!(highlighted_edits.highlights.len(), 1);
18611 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
18612 assert_eq!(
18613 highlighted_edits.highlights[0].1.background_color,
18614 Some(cx.theme().status().created_background)
18615 );
18616 },
18617 )
18618 .await;
18619
18620 // Multiple edits
18621 assert_highlighted_edits(
18622 "Hello, world!",
18623 vec![
18624 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
18625 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
18626 ],
18627 false,
18628 cx,
18629 |highlighted_edits, cx| {
18630 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
18631 assert_eq!(highlighted_edits.highlights.len(), 2);
18632 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
18633 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
18634 assert_eq!(
18635 highlighted_edits.highlights[0].1.background_color,
18636 Some(cx.theme().status().created_background)
18637 );
18638 assert_eq!(
18639 highlighted_edits.highlights[1].1.background_color,
18640 Some(cx.theme().status().created_background)
18641 );
18642 },
18643 )
18644 .await;
18645
18646 // Multiple lines with edits
18647 assert_highlighted_edits(
18648 "First line\nSecond line\nThird line\nFourth line",
18649 vec![
18650 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
18651 (
18652 Point::new(2, 0)..Point::new(2, 10),
18653 "New third line".to_string(),
18654 ),
18655 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
18656 ],
18657 false,
18658 cx,
18659 |highlighted_edits, cx| {
18660 assert_eq!(
18661 highlighted_edits.text,
18662 "Second modified\nNew third line\nFourth updated line"
18663 );
18664 assert_eq!(highlighted_edits.highlights.len(), 3);
18665 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
18666 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
18667 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
18668 for highlight in &highlighted_edits.highlights {
18669 assert_eq!(
18670 highlight.1.background_color,
18671 Some(cx.theme().status().created_background)
18672 );
18673 }
18674 },
18675 )
18676 .await;
18677}
18678
18679#[gpui::test]
18680async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
18681 init_test(cx, |_| {});
18682
18683 // Deletion
18684 assert_highlighted_edits(
18685 "Hello, world!",
18686 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
18687 true,
18688 cx,
18689 |highlighted_edits, cx| {
18690 assert_eq!(highlighted_edits.text, "Hello, world!");
18691 assert_eq!(highlighted_edits.highlights.len(), 1);
18692 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
18693 assert_eq!(
18694 highlighted_edits.highlights[0].1.background_color,
18695 Some(cx.theme().status().deleted_background)
18696 );
18697 },
18698 )
18699 .await;
18700
18701 // Insertion
18702 assert_highlighted_edits(
18703 "Hello, world!",
18704 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
18705 true,
18706 cx,
18707 |highlighted_edits, cx| {
18708 assert_eq!(highlighted_edits.highlights.len(), 1);
18709 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
18710 assert_eq!(
18711 highlighted_edits.highlights[0].1.background_color,
18712 Some(cx.theme().status().created_background)
18713 );
18714 },
18715 )
18716 .await;
18717}
18718
18719async fn assert_highlighted_edits(
18720 text: &str,
18721 edits: Vec<(Range<Point>, String)>,
18722 include_deletions: bool,
18723 cx: &mut TestAppContext,
18724 assertion_fn: impl Fn(HighlightedText, &App),
18725) {
18726 let window = cx.add_window(|window, cx| {
18727 let buffer = MultiBuffer::build_simple(text, cx);
18728 Editor::new(EditorMode::full(), buffer, None, window, cx)
18729 });
18730 let cx = &mut VisualTestContext::from_window(*window, cx);
18731
18732 let (buffer, snapshot) = window
18733 .update(cx, |editor, _window, cx| {
18734 (
18735 editor.buffer().clone(),
18736 editor.buffer().read(cx).snapshot(cx),
18737 )
18738 })
18739 .unwrap();
18740
18741 let edits = edits
18742 .into_iter()
18743 .map(|(range, edit)| {
18744 (
18745 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
18746 edit,
18747 )
18748 })
18749 .collect::<Vec<_>>();
18750
18751 let text_anchor_edits = edits
18752 .clone()
18753 .into_iter()
18754 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
18755 .collect::<Vec<_>>();
18756
18757 let edit_preview = window
18758 .update(cx, |_, _window, cx| {
18759 buffer
18760 .read(cx)
18761 .as_singleton()
18762 .unwrap()
18763 .read(cx)
18764 .preview_edits(text_anchor_edits.into(), cx)
18765 })
18766 .unwrap()
18767 .await;
18768
18769 cx.update(|_window, cx| {
18770 let highlighted_edits = inline_completion_edit_text(
18771 &snapshot.as_singleton().unwrap().2,
18772 &edits,
18773 &edit_preview,
18774 include_deletions,
18775 cx,
18776 );
18777 assertion_fn(highlighted_edits, cx)
18778 });
18779}
18780
18781#[track_caller]
18782fn assert_breakpoint(
18783 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
18784 path: &Arc<Path>,
18785 expected: Vec<(u32, Breakpoint)>,
18786) {
18787 if expected.len() == 0usize {
18788 assert!(!breakpoints.contains_key(path), "{}", path.display());
18789 } else {
18790 let mut breakpoint = breakpoints
18791 .get(path)
18792 .unwrap()
18793 .into_iter()
18794 .map(|breakpoint| {
18795 (
18796 breakpoint.row,
18797 Breakpoint {
18798 message: breakpoint.message.clone(),
18799 state: breakpoint.state,
18800 condition: breakpoint.condition.clone(),
18801 hit_condition: breakpoint.hit_condition.clone(),
18802 },
18803 )
18804 })
18805 .collect::<Vec<_>>();
18806
18807 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
18808
18809 assert_eq!(expected, breakpoint);
18810 }
18811}
18812
18813fn add_log_breakpoint_at_cursor(
18814 editor: &mut Editor,
18815 log_message: &str,
18816 window: &mut Window,
18817 cx: &mut Context<Editor>,
18818) {
18819 let (anchor, bp) = editor
18820 .breakpoints_at_cursors(window, cx)
18821 .first()
18822 .and_then(|(anchor, bp)| {
18823 if let Some(bp) = bp {
18824 Some((*anchor, bp.clone()))
18825 } else {
18826 None
18827 }
18828 })
18829 .unwrap_or_else(|| {
18830 let cursor_position: Point = editor.selections.newest(cx).head();
18831
18832 let breakpoint_position = editor
18833 .snapshot(window, cx)
18834 .display_snapshot
18835 .buffer_snapshot
18836 .anchor_before(Point::new(cursor_position.row, 0));
18837
18838 (breakpoint_position, Breakpoint::new_log(&log_message))
18839 });
18840
18841 editor.edit_breakpoint_at_anchor(
18842 anchor,
18843 bp,
18844 BreakpointEditAction::EditLogMessage(log_message.into()),
18845 cx,
18846 );
18847}
18848
18849#[gpui::test]
18850async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
18851 init_test(cx, |_| {});
18852
18853 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
18854 let fs = FakeFs::new(cx.executor());
18855 fs.insert_tree(
18856 path!("/a"),
18857 json!({
18858 "main.rs": sample_text,
18859 }),
18860 )
18861 .await;
18862 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18863 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18864 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18865
18866 let fs = FakeFs::new(cx.executor());
18867 fs.insert_tree(
18868 path!("/a"),
18869 json!({
18870 "main.rs": sample_text,
18871 }),
18872 )
18873 .await;
18874 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18875 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18876 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18877 let worktree_id = workspace
18878 .update(cx, |workspace, _window, cx| {
18879 workspace.project().update(cx, |project, cx| {
18880 project.worktrees(cx).next().unwrap().read(cx).id()
18881 })
18882 })
18883 .unwrap();
18884
18885 let buffer = project
18886 .update(cx, |project, cx| {
18887 project.open_buffer((worktree_id, "main.rs"), cx)
18888 })
18889 .await
18890 .unwrap();
18891
18892 let (editor, cx) = cx.add_window_view(|window, cx| {
18893 Editor::new(
18894 EditorMode::full(),
18895 MultiBuffer::build_from_buffer(buffer, cx),
18896 Some(project.clone()),
18897 window,
18898 cx,
18899 )
18900 });
18901
18902 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
18903 let abs_path = project.read_with(cx, |project, cx| {
18904 project
18905 .absolute_path(&project_path, cx)
18906 .map(|path_buf| Arc::from(path_buf.to_owned()))
18907 .unwrap()
18908 });
18909
18910 // assert we can add breakpoint on the first line
18911 editor.update_in(cx, |editor, window, cx| {
18912 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18913 editor.move_to_end(&MoveToEnd, window, cx);
18914 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18915 });
18916
18917 let breakpoints = editor.update(cx, |editor, cx| {
18918 editor
18919 .breakpoint_store()
18920 .as_ref()
18921 .unwrap()
18922 .read(cx)
18923 .all_source_breakpoints(cx)
18924 .clone()
18925 });
18926
18927 assert_eq!(1, breakpoints.len());
18928 assert_breakpoint(
18929 &breakpoints,
18930 &abs_path,
18931 vec![
18932 (0, Breakpoint::new_standard()),
18933 (3, Breakpoint::new_standard()),
18934 ],
18935 );
18936
18937 editor.update_in(cx, |editor, window, cx| {
18938 editor.move_to_beginning(&MoveToBeginning, window, cx);
18939 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18940 });
18941
18942 let breakpoints = editor.update(cx, |editor, cx| {
18943 editor
18944 .breakpoint_store()
18945 .as_ref()
18946 .unwrap()
18947 .read(cx)
18948 .all_source_breakpoints(cx)
18949 .clone()
18950 });
18951
18952 assert_eq!(1, breakpoints.len());
18953 assert_breakpoint(
18954 &breakpoints,
18955 &abs_path,
18956 vec![(3, Breakpoint::new_standard())],
18957 );
18958
18959 editor.update_in(cx, |editor, window, cx| {
18960 editor.move_to_end(&MoveToEnd, window, cx);
18961 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18962 });
18963
18964 let breakpoints = editor.update(cx, |editor, cx| {
18965 editor
18966 .breakpoint_store()
18967 .as_ref()
18968 .unwrap()
18969 .read(cx)
18970 .all_source_breakpoints(cx)
18971 .clone()
18972 });
18973
18974 assert_eq!(0, breakpoints.len());
18975 assert_breakpoint(&breakpoints, &abs_path, vec![]);
18976}
18977
18978#[gpui::test]
18979async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
18980 init_test(cx, |_| {});
18981
18982 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
18983
18984 let fs = FakeFs::new(cx.executor());
18985 fs.insert_tree(
18986 path!("/a"),
18987 json!({
18988 "main.rs": sample_text,
18989 }),
18990 )
18991 .await;
18992 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18993 let (workspace, cx) =
18994 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
18995
18996 let worktree_id = workspace.update(cx, |workspace, cx| {
18997 workspace.project().update(cx, |project, cx| {
18998 project.worktrees(cx).next().unwrap().read(cx).id()
18999 })
19000 });
19001
19002 let buffer = project
19003 .update(cx, |project, cx| {
19004 project.open_buffer((worktree_id, "main.rs"), cx)
19005 })
19006 .await
19007 .unwrap();
19008
19009 let (editor, cx) = cx.add_window_view(|window, cx| {
19010 Editor::new(
19011 EditorMode::full(),
19012 MultiBuffer::build_from_buffer(buffer, cx),
19013 Some(project.clone()),
19014 window,
19015 cx,
19016 )
19017 });
19018
19019 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
19020 let abs_path = project.read_with(cx, |project, cx| {
19021 project
19022 .absolute_path(&project_path, cx)
19023 .map(|path_buf| Arc::from(path_buf.to_owned()))
19024 .unwrap()
19025 });
19026
19027 editor.update_in(cx, |editor, window, cx| {
19028 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
19029 });
19030
19031 let breakpoints = editor.update(cx, |editor, cx| {
19032 editor
19033 .breakpoint_store()
19034 .as_ref()
19035 .unwrap()
19036 .read(cx)
19037 .all_source_breakpoints(cx)
19038 .clone()
19039 });
19040
19041 assert_breakpoint(
19042 &breakpoints,
19043 &abs_path,
19044 vec![(0, Breakpoint::new_log("hello world"))],
19045 );
19046
19047 // Removing a log message from a log breakpoint should remove it
19048 editor.update_in(cx, |editor, window, cx| {
19049 add_log_breakpoint_at_cursor(editor, "", window, cx);
19050 });
19051
19052 let breakpoints = editor.update(cx, |editor, cx| {
19053 editor
19054 .breakpoint_store()
19055 .as_ref()
19056 .unwrap()
19057 .read(cx)
19058 .all_source_breakpoints(cx)
19059 .clone()
19060 });
19061
19062 assert_breakpoint(&breakpoints, &abs_path, vec![]);
19063
19064 editor.update_in(cx, |editor, window, cx| {
19065 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19066 editor.move_to_end(&MoveToEnd, window, cx);
19067 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19068 // Not adding a log message to a standard breakpoint shouldn't remove it
19069 add_log_breakpoint_at_cursor(editor, "", window, cx);
19070 });
19071
19072 let breakpoints = editor.update(cx, |editor, cx| {
19073 editor
19074 .breakpoint_store()
19075 .as_ref()
19076 .unwrap()
19077 .read(cx)
19078 .all_source_breakpoints(cx)
19079 .clone()
19080 });
19081
19082 assert_breakpoint(
19083 &breakpoints,
19084 &abs_path,
19085 vec![
19086 (0, Breakpoint::new_standard()),
19087 (3, Breakpoint::new_standard()),
19088 ],
19089 );
19090
19091 editor.update_in(cx, |editor, window, cx| {
19092 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
19093 });
19094
19095 let breakpoints = editor.update(cx, |editor, cx| {
19096 editor
19097 .breakpoint_store()
19098 .as_ref()
19099 .unwrap()
19100 .read(cx)
19101 .all_source_breakpoints(cx)
19102 .clone()
19103 });
19104
19105 assert_breakpoint(
19106 &breakpoints,
19107 &abs_path,
19108 vec![
19109 (0, Breakpoint::new_standard()),
19110 (3, Breakpoint::new_log("hello world")),
19111 ],
19112 );
19113
19114 editor.update_in(cx, |editor, window, cx| {
19115 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
19116 });
19117
19118 let breakpoints = editor.update(cx, |editor, cx| {
19119 editor
19120 .breakpoint_store()
19121 .as_ref()
19122 .unwrap()
19123 .read(cx)
19124 .all_source_breakpoints(cx)
19125 .clone()
19126 });
19127
19128 assert_breakpoint(
19129 &breakpoints,
19130 &abs_path,
19131 vec![
19132 (0, Breakpoint::new_standard()),
19133 (3, Breakpoint::new_log("hello Earth!!")),
19134 ],
19135 );
19136}
19137
19138/// This also tests that Editor::breakpoint_at_cursor_head is working properly
19139/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
19140/// or when breakpoints were placed out of order. This tests for a regression too
19141#[gpui::test]
19142async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
19143 init_test(cx, |_| {});
19144
19145 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
19146 let fs = FakeFs::new(cx.executor());
19147 fs.insert_tree(
19148 path!("/a"),
19149 json!({
19150 "main.rs": sample_text,
19151 }),
19152 )
19153 .await;
19154 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19155 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19156 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19157
19158 let fs = FakeFs::new(cx.executor());
19159 fs.insert_tree(
19160 path!("/a"),
19161 json!({
19162 "main.rs": sample_text,
19163 }),
19164 )
19165 .await;
19166 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19167 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19168 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19169 let worktree_id = workspace
19170 .update(cx, |workspace, _window, cx| {
19171 workspace.project().update(cx, |project, cx| {
19172 project.worktrees(cx).next().unwrap().read(cx).id()
19173 })
19174 })
19175 .unwrap();
19176
19177 let buffer = project
19178 .update(cx, |project, cx| {
19179 project.open_buffer((worktree_id, "main.rs"), cx)
19180 })
19181 .await
19182 .unwrap();
19183
19184 let (editor, cx) = cx.add_window_view(|window, cx| {
19185 Editor::new(
19186 EditorMode::full(),
19187 MultiBuffer::build_from_buffer(buffer, cx),
19188 Some(project.clone()),
19189 window,
19190 cx,
19191 )
19192 });
19193
19194 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
19195 let abs_path = project.read_with(cx, |project, cx| {
19196 project
19197 .absolute_path(&project_path, cx)
19198 .map(|path_buf| Arc::from(path_buf.to_owned()))
19199 .unwrap()
19200 });
19201
19202 // assert we can add breakpoint on the first line
19203 editor.update_in(cx, |editor, window, cx| {
19204 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19205 editor.move_to_end(&MoveToEnd, window, cx);
19206 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19207 editor.move_up(&MoveUp, window, cx);
19208 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19209 });
19210
19211 let breakpoints = editor.update(cx, |editor, cx| {
19212 editor
19213 .breakpoint_store()
19214 .as_ref()
19215 .unwrap()
19216 .read(cx)
19217 .all_source_breakpoints(cx)
19218 .clone()
19219 });
19220
19221 assert_eq!(1, breakpoints.len());
19222 assert_breakpoint(
19223 &breakpoints,
19224 &abs_path,
19225 vec![
19226 (0, Breakpoint::new_standard()),
19227 (2, Breakpoint::new_standard()),
19228 (3, Breakpoint::new_standard()),
19229 ],
19230 );
19231
19232 editor.update_in(cx, |editor, window, cx| {
19233 editor.move_to_beginning(&MoveToBeginning, window, cx);
19234 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
19235 editor.move_to_end(&MoveToEnd, window, cx);
19236 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
19237 // Disabling a breakpoint that doesn't exist should do nothing
19238 editor.move_up(&MoveUp, window, cx);
19239 editor.move_up(&MoveUp, window, cx);
19240 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
19241 });
19242
19243 let breakpoints = editor.update(cx, |editor, cx| {
19244 editor
19245 .breakpoint_store()
19246 .as_ref()
19247 .unwrap()
19248 .read(cx)
19249 .all_source_breakpoints(cx)
19250 .clone()
19251 });
19252
19253 let disable_breakpoint = {
19254 let mut bp = Breakpoint::new_standard();
19255 bp.state = BreakpointState::Disabled;
19256 bp
19257 };
19258
19259 assert_eq!(1, breakpoints.len());
19260 assert_breakpoint(
19261 &breakpoints,
19262 &abs_path,
19263 vec![
19264 (0, disable_breakpoint.clone()),
19265 (2, Breakpoint::new_standard()),
19266 (3, disable_breakpoint.clone()),
19267 ],
19268 );
19269
19270 editor.update_in(cx, |editor, window, cx| {
19271 editor.move_to_beginning(&MoveToBeginning, window, cx);
19272 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
19273 editor.move_to_end(&MoveToEnd, window, cx);
19274 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
19275 editor.move_up(&MoveUp, window, cx);
19276 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
19277 });
19278
19279 let breakpoints = editor.update(cx, |editor, cx| {
19280 editor
19281 .breakpoint_store()
19282 .as_ref()
19283 .unwrap()
19284 .read(cx)
19285 .all_source_breakpoints(cx)
19286 .clone()
19287 });
19288
19289 assert_eq!(1, breakpoints.len());
19290 assert_breakpoint(
19291 &breakpoints,
19292 &abs_path,
19293 vec![
19294 (0, Breakpoint::new_standard()),
19295 (2, disable_breakpoint),
19296 (3, Breakpoint::new_standard()),
19297 ],
19298 );
19299}
19300
19301#[gpui::test]
19302async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
19303 init_test(cx, |_| {});
19304 let capabilities = lsp::ServerCapabilities {
19305 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
19306 prepare_provider: Some(true),
19307 work_done_progress_options: Default::default(),
19308 })),
19309 ..Default::default()
19310 };
19311 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
19312
19313 cx.set_state(indoc! {"
19314 struct Fˇoo {}
19315 "});
19316
19317 cx.update_editor(|editor, _, cx| {
19318 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
19319 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
19320 editor.highlight_background::<DocumentHighlightRead>(
19321 &[highlight_range],
19322 |c| c.editor_document_highlight_read_background,
19323 cx,
19324 );
19325 });
19326
19327 let mut prepare_rename_handler = cx
19328 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
19329 move |_, _, _| async move {
19330 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
19331 start: lsp::Position {
19332 line: 0,
19333 character: 7,
19334 },
19335 end: lsp::Position {
19336 line: 0,
19337 character: 10,
19338 },
19339 })))
19340 },
19341 );
19342 let prepare_rename_task = cx
19343 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
19344 .expect("Prepare rename was not started");
19345 prepare_rename_handler.next().await.unwrap();
19346 prepare_rename_task.await.expect("Prepare rename failed");
19347
19348 let mut rename_handler =
19349 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
19350 let edit = lsp::TextEdit {
19351 range: lsp::Range {
19352 start: lsp::Position {
19353 line: 0,
19354 character: 7,
19355 },
19356 end: lsp::Position {
19357 line: 0,
19358 character: 10,
19359 },
19360 },
19361 new_text: "FooRenamed".to_string(),
19362 };
19363 Ok(Some(lsp::WorkspaceEdit::new(
19364 // Specify the same edit twice
19365 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
19366 )))
19367 });
19368 let rename_task = cx
19369 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
19370 .expect("Confirm rename was not started");
19371 rename_handler.next().await.unwrap();
19372 rename_task.await.expect("Confirm rename failed");
19373 cx.run_until_parked();
19374
19375 // Despite two edits, only one is actually applied as those are identical
19376 cx.assert_editor_state(indoc! {"
19377 struct FooRenamedˇ {}
19378 "});
19379}
19380
19381#[gpui::test]
19382async fn test_rename_without_prepare(cx: &mut TestAppContext) {
19383 init_test(cx, |_| {});
19384 // These capabilities indicate that the server does not support prepare rename.
19385 let capabilities = lsp::ServerCapabilities {
19386 rename_provider: Some(lsp::OneOf::Left(true)),
19387 ..Default::default()
19388 };
19389 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
19390
19391 cx.set_state(indoc! {"
19392 struct Fˇoo {}
19393 "});
19394
19395 cx.update_editor(|editor, _window, cx| {
19396 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
19397 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
19398 editor.highlight_background::<DocumentHighlightRead>(
19399 &[highlight_range],
19400 |c| c.editor_document_highlight_read_background,
19401 cx,
19402 );
19403 });
19404
19405 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
19406 .expect("Prepare rename was not started")
19407 .await
19408 .expect("Prepare rename failed");
19409
19410 let mut rename_handler =
19411 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
19412 let edit = lsp::TextEdit {
19413 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 new_text: "FooRenamed".to_string(),
19424 };
19425 Ok(Some(lsp::WorkspaceEdit::new(
19426 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
19427 )))
19428 });
19429 let rename_task = cx
19430 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
19431 .expect("Confirm rename was not started");
19432 rename_handler.next().await.unwrap();
19433 rename_task.await.expect("Confirm rename failed");
19434 cx.run_until_parked();
19435
19436 // Correct range is renamed, as `surrounding_word` is used to find it.
19437 cx.assert_editor_state(indoc! {"
19438 struct FooRenamedˇ {}
19439 "});
19440}
19441
19442#[gpui::test]
19443async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
19444 init_test(cx, |_| {});
19445 let mut cx = EditorTestContext::new(cx).await;
19446
19447 let language = Arc::new(
19448 Language::new(
19449 LanguageConfig::default(),
19450 Some(tree_sitter_html::LANGUAGE.into()),
19451 )
19452 .with_brackets_query(
19453 r#"
19454 ("<" @open "/>" @close)
19455 ("</" @open ">" @close)
19456 ("<" @open ">" @close)
19457 ("\"" @open "\"" @close)
19458 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
19459 "#,
19460 )
19461 .unwrap(),
19462 );
19463 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
19464
19465 cx.set_state(indoc! {"
19466 <span>ˇ</span>
19467 "});
19468 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
19469 cx.assert_editor_state(indoc! {"
19470 <span>
19471 ˇ
19472 </span>
19473 "});
19474
19475 cx.set_state(indoc! {"
19476 <span><span></span>ˇ</span>
19477 "});
19478 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
19479 cx.assert_editor_state(indoc! {"
19480 <span><span></span>
19481 ˇ</span>
19482 "});
19483
19484 cx.set_state(indoc! {"
19485 <span>ˇ
19486 </span>
19487 "});
19488 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
19489 cx.assert_editor_state(indoc! {"
19490 <span>
19491 ˇ
19492 </span>
19493 "});
19494}
19495
19496#[gpui::test(iterations = 10)]
19497async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
19498 init_test(cx, |_| {});
19499
19500 let fs = FakeFs::new(cx.executor());
19501 fs.insert_tree(
19502 path!("/dir"),
19503 json!({
19504 "a.ts": "a",
19505 }),
19506 )
19507 .await;
19508
19509 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
19510 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19511 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19512
19513 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
19514 language_registry.add(Arc::new(Language::new(
19515 LanguageConfig {
19516 name: "TypeScript".into(),
19517 matcher: LanguageMatcher {
19518 path_suffixes: vec!["ts".to_string()],
19519 ..Default::default()
19520 },
19521 ..Default::default()
19522 },
19523 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
19524 )));
19525 let mut fake_language_servers = language_registry.register_fake_lsp(
19526 "TypeScript",
19527 FakeLspAdapter {
19528 capabilities: lsp::ServerCapabilities {
19529 code_lens_provider: Some(lsp::CodeLensOptions {
19530 resolve_provider: Some(true),
19531 }),
19532 execute_command_provider: Some(lsp::ExecuteCommandOptions {
19533 commands: vec!["_the/command".to_string()],
19534 ..lsp::ExecuteCommandOptions::default()
19535 }),
19536 ..lsp::ServerCapabilities::default()
19537 },
19538 ..FakeLspAdapter::default()
19539 },
19540 );
19541
19542 let (buffer, _handle) = project
19543 .update(cx, |p, cx| {
19544 p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
19545 })
19546 .await
19547 .unwrap();
19548 cx.executor().run_until_parked();
19549
19550 let fake_server = fake_language_servers.next().await.unwrap();
19551
19552 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
19553 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
19554 drop(buffer_snapshot);
19555 let actions = cx
19556 .update_window(*workspace, |_, window, cx| {
19557 project.code_actions(&buffer, anchor..anchor, window, cx)
19558 })
19559 .unwrap();
19560
19561 fake_server
19562 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
19563 Ok(Some(vec![
19564 lsp::CodeLens {
19565 range: lsp::Range::default(),
19566 command: Some(lsp::Command {
19567 title: "Code lens command".to_owned(),
19568 command: "_the/command".to_owned(),
19569 arguments: None,
19570 }),
19571 data: None,
19572 },
19573 lsp::CodeLens {
19574 range: lsp::Range::default(),
19575 command: Some(lsp::Command {
19576 title: "Command not in capabilities".to_owned(),
19577 command: "not in capabilities".to_owned(),
19578 arguments: None,
19579 }),
19580 data: None,
19581 },
19582 lsp::CodeLens {
19583 range: lsp::Range {
19584 start: lsp::Position {
19585 line: 1,
19586 character: 1,
19587 },
19588 end: lsp::Position {
19589 line: 1,
19590 character: 1,
19591 },
19592 },
19593 command: Some(lsp::Command {
19594 title: "Command not in range".to_owned(),
19595 command: "_the/command".to_owned(),
19596 arguments: None,
19597 }),
19598 data: None,
19599 },
19600 ]))
19601 })
19602 .next()
19603 .await;
19604
19605 let actions = actions.await.unwrap();
19606 assert_eq!(
19607 actions.len(),
19608 1,
19609 "Should have only one valid action for the 0..0 range"
19610 );
19611 let action = actions[0].clone();
19612 let apply = project.update(cx, |project, cx| {
19613 project.apply_code_action(buffer.clone(), action, true, cx)
19614 });
19615
19616 // Resolving the code action does not populate its edits. In absence of
19617 // edits, we must execute the given command.
19618 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
19619 |mut lens, _| async move {
19620 let lens_command = lens.command.as_mut().expect("should have a command");
19621 assert_eq!(lens_command.title, "Code lens command");
19622 lens_command.arguments = Some(vec![json!("the-argument")]);
19623 Ok(lens)
19624 },
19625 );
19626
19627 // While executing the command, the language server sends the editor
19628 // a `workspaceEdit` request.
19629 fake_server
19630 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
19631 let fake = fake_server.clone();
19632 move |params, _| {
19633 assert_eq!(params.command, "_the/command");
19634 let fake = fake.clone();
19635 async move {
19636 fake.server
19637 .request::<lsp::request::ApplyWorkspaceEdit>(
19638 lsp::ApplyWorkspaceEditParams {
19639 label: None,
19640 edit: lsp::WorkspaceEdit {
19641 changes: Some(
19642 [(
19643 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
19644 vec![lsp::TextEdit {
19645 range: lsp::Range::new(
19646 lsp::Position::new(0, 0),
19647 lsp::Position::new(0, 0),
19648 ),
19649 new_text: "X".into(),
19650 }],
19651 )]
19652 .into_iter()
19653 .collect(),
19654 ),
19655 ..Default::default()
19656 },
19657 },
19658 )
19659 .await
19660 .into_response()
19661 .unwrap();
19662 Ok(Some(json!(null)))
19663 }
19664 }
19665 })
19666 .next()
19667 .await;
19668
19669 // Applying the code lens command returns a project transaction containing the edits
19670 // sent by the language server in its `workspaceEdit` request.
19671 let transaction = apply.await.unwrap();
19672 assert!(transaction.0.contains_key(&buffer));
19673 buffer.update(cx, |buffer, cx| {
19674 assert_eq!(buffer.text(), "Xa");
19675 buffer.undo(cx);
19676 assert_eq!(buffer.text(), "a");
19677 });
19678}
19679
19680#[gpui::test]
19681async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
19682 init_test(cx, |_| {});
19683
19684 let fs = FakeFs::new(cx.executor());
19685 let main_text = r#"fn main() {
19686println!("1");
19687println!("2");
19688println!("3");
19689println!("4");
19690println!("5");
19691}"#;
19692 let lib_text = "mod foo {}";
19693 fs.insert_tree(
19694 path!("/a"),
19695 json!({
19696 "lib.rs": lib_text,
19697 "main.rs": main_text,
19698 }),
19699 )
19700 .await;
19701
19702 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19703 let (workspace, cx) =
19704 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
19705 let worktree_id = workspace.update(cx, |workspace, cx| {
19706 workspace.project().update(cx, |project, cx| {
19707 project.worktrees(cx).next().unwrap().read(cx).id()
19708 })
19709 });
19710
19711 let expected_ranges = vec![
19712 Point::new(0, 0)..Point::new(0, 0),
19713 Point::new(1, 0)..Point::new(1, 1),
19714 Point::new(2, 0)..Point::new(2, 2),
19715 Point::new(3, 0)..Point::new(3, 3),
19716 ];
19717
19718 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
19719 let editor_1 = workspace
19720 .update_in(cx, |workspace, window, cx| {
19721 workspace.open_path(
19722 (worktree_id, "main.rs"),
19723 Some(pane_1.downgrade()),
19724 true,
19725 window,
19726 cx,
19727 )
19728 })
19729 .unwrap()
19730 .await
19731 .downcast::<Editor>()
19732 .unwrap();
19733 pane_1.update(cx, |pane, cx| {
19734 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19735 open_editor.update(cx, |editor, cx| {
19736 assert_eq!(
19737 editor.display_text(cx),
19738 main_text,
19739 "Original main.rs text on initial open",
19740 );
19741 assert_eq!(
19742 editor
19743 .selections
19744 .all::<Point>(cx)
19745 .into_iter()
19746 .map(|s| s.range())
19747 .collect::<Vec<_>>(),
19748 vec![Point::zero()..Point::zero()],
19749 "Default selections on initial open",
19750 );
19751 })
19752 });
19753 editor_1.update_in(cx, |editor, window, cx| {
19754 editor.change_selections(None, window, cx, |s| {
19755 s.select_ranges(expected_ranges.clone());
19756 });
19757 });
19758
19759 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
19760 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
19761 });
19762 let editor_2 = workspace
19763 .update_in(cx, |workspace, window, cx| {
19764 workspace.open_path(
19765 (worktree_id, "main.rs"),
19766 Some(pane_2.downgrade()),
19767 true,
19768 window,
19769 cx,
19770 )
19771 })
19772 .unwrap()
19773 .await
19774 .downcast::<Editor>()
19775 .unwrap();
19776 pane_2.update(cx, |pane, cx| {
19777 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19778 open_editor.update(cx, |editor, cx| {
19779 assert_eq!(
19780 editor.display_text(cx),
19781 main_text,
19782 "Original main.rs text on initial open in another panel",
19783 );
19784 assert_eq!(
19785 editor
19786 .selections
19787 .all::<Point>(cx)
19788 .into_iter()
19789 .map(|s| s.range())
19790 .collect::<Vec<_>>(),
19791 vec![Point::zero()..Point::zero()],
19792 "Default selections on initial open in another panel",
19793 );
19794 })
19795 });
19796
19797 editor_2.update_in(cx, |editor, window, cx| {
19798 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
19799 });
19800
19801 let _other_editor_1 = workspace
19802 .update_in(cx, |workspace, window, cx| {
19803 workspace.open_path(
19804 (worktree_id, "lib.rs"),
19805 Some(pane_1.downgrade()),
19806 true,
19807 window,
19808 cx,
19809 )
19810 })
19811 .unwrap()
19812 .await
19813 .downcast::<Editor>()
19814 .unwrap();
19815 pane_1
19816 .update_in(cx, |pane, window, cx| {
19817 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
19818 .unwrap()
19819 })
19820 .await
19821 .unwrap();
19822 drop(editor_1);
19823 pane_1.update(cx, |pane, cx| {
19824 pane.active_item()
19825 .unwrap()
19826 .downcast::<Editor>()
19827 .unwrap()
19828 .update(cx, |editor, cx| {
19829 assert_eq!(
19830 editor.display_text(cx),
19831 lib_text,
19832 "Other file should be open and active",
19833 );
19834 });
19835 assert_eq!(pane.items().count(), 1, "No other editors should be open");
19836 });
19837
19838 let _other_editor_2 = workspace
19839 .update_in(cx, |workspace, window, cx| {
19840 workspace.open_path(
19841 (worktree_id, "lib.rs"),
19842 Some(pane_2.downgrade()),
19843 true,
19844 window,
19845 cx,
19846 )
19847 })
19848 .unwrap()
19849 .await
19850 .downcast::<Editor>()
19851 .unwrap();
19852 pane_2
19853 .update_in(cx, |pane, window, cx| {
19854 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
19855 .unwrap()
19856 })
19857 .await
19858 .unwrap();
19859 drop(editor_2);
19860 pane_2.update(cx, |pane, cx| {
19861 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19862 open_editor.update(cx, |editor, cx| {
19863 assert_eq!(
19864 editor.display_text(cx),
19865 lib_text,
19866 "Other file should be open and active in another panel too",
19867 );
19868 });
19869 assert_eq!(
19870 pane.items().count(),
19871 1,
19872 "No other editors should be open in another pane",
19873 );
19874 });
19875
19876 let _editor_1_reopened = workspace
19877 .update_in(cx, |workspace, window, cx| {
19878 workspace.open_path(
19879 (worktree_id, "main.rs"),
19880 Some(pane_1.downgrade()),
19881 true,
19882 window,
19883 cx,
19884 )
19885 })
19886 .unwrap()
19887 .await
19888 .downcast::<Editor>()
19889 .unwrap();
19890 let _editor_2_reopened = workspace
19891 .update_in(cx, |workspace, window, cx| {
19892 workspace.open_path(
19893 (worktree_id, "main.rs"),
19894 Some(pane_2.downgrade()),
19895 true,
19896 window,
19897 cx,
19898 )
19899 })
19900 .unwrap()
19901 .await
19902 .downcast::<Editor>()
19903 .unwrap();
19904 pane_1.update(cx, |pane, cx| {
19905 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19906 open_editor.update(cx, |editor, cx| {
19907 assert_eq!(
19908 editor.display_text(cx),
19909 main_text,
19910 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
19911 );
19912 assert_eq!(
19913 editor
19914 .selections
19915 .all::<Point>(cx)
19916 .into_iter()
19917 .map(|s| s.range())
19918 .collect::<Vec<_>>(),
19919 expected_ranges,
19920 "Previous editor in the 1st panel had selections and should get them restored on reopen",
19921 );
19922 })
19923 });
19924 pane_2.update(cx, |pane, cx| {
19925 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19926 open_editor.update(cx, |editor, cx| {
19927 assert_eq!(
19928 editor.display_text(cx),
19929 r#"fn main() {
19930⋯rintln!("1");
19931⋯intln!("2");
19932⋯ntln!("3");
19933println!("4");
19934println!("5");
19935}"#,
19936 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
19937 );
19938 assert_eq!(
19939 editor
19940 .selections
19941 .all::<Point>(cx)
19942 .into_iter()
19943 .map(|s| s.range())
19944 .collect::<Vec<_>>(),
19945 vec![Point::zero()..Point::zero()],
19946 "Previous editor in the 2nd pane had no selections changed hence should restore none",
19947 );
19948 })
19949 });
19950}
19951
19952#[gpui::test]
19953async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
19954 init_test(cx, |_| {});
19955
19956 let fs = FakeFs::new(cx.executor());
19957 let main_text = r#"fn main() {
19958println!("1");
19959println!("2");
19960println!("3");
19961println!("4");
19962println!("5");
19963}"#;
19964 let lib_text = "mod foo {}";
19965 fs.insert_tree(
19966 path!("/a"),
19967 json!({
19968 "lib.rs": lib_text,
19969 "main.rs": main_text,
19970 }),
19971 )
19972 .await;
19973
19974 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19975 let (workspace, cx) =
19976 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
19977 let worktree_id = workspace.update(cx, |workspace, cx| {
19978 workspace.project().update(cx, |project, cx| {
19979 project.worktrees(cx).next().unwrap().read(cx).id()
19980 })
19981 });
19982
19983 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
19984 let editor = workspace
19985 .update_in(cx, |workspace, window, cx| {
19986 workspace.open_path(
19987 (worktree_id, "main.rs"),
19988 Some(pane.downgrade()),
19989 true,
19990 window,
19991 cx,
19992 )
19993 })
19994 .unwrap()
19995 .await
19996 .downcast::<Editor>()
19997 .unwrap();
19998 pane.update(cx, |pane, cx| {
19999 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20000 open_editor.update(cx, |editor, cx| {
20001 assert_eq!(
20002 editor.display_text(cx),
20003 main_text,
20004 "Original main.rs text on initial open",
20005 );
20006 })
20007 });
20008 editor.update_in(cx, |editor, window, cx| {
20009 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
20010 });
20011
20012 cx.update_global(|store: &mut SettingsStore, cx| {
20013 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
20014 s.restore_on_file_reopen = Some(false);
20015 });
20016 });
20017 editor.update_in(cx, |editor, window, cx| {
20018 editor.fold_ranges(
20019 vec![
20020 Point::new(1, 0)..Point::new(1, 1),
20021 Point::new(2, 0)..Point::new(2, 2),
20022 Point::new(3, 0)..Point::new(3, 3),
20023 ],
20024 false,
20025 window,
20026 cx,
20027 );
20028 });
20029 pane.update_in(cx, |pane, window, cx| {
20030 pane.close_all_items(&CloseAllItems::default(), window, cx)
20031 .unwrap()
20032 })
20033 .await
20034 .unwrap();
20035 pane.update(cx, |pane, _| {
20036 assert!(pane.active_item().is_none());
20037 });
20038 cx.update_global(|store: &mut SettingsStore, cx| {
20039 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
20040 s.restore_on_file_reopen = Some(true);
20041 });
20042 });
20043
20044 let _editor_reopened = workspace
20045 .update_in(cx, |workspace, window, cx| {
20046 workspace.open_path(
20047 (worktree_id, "main.rs"),
20048 Some(pane.downgrade()),
20049 true,
20050 window,
20051 cx,
20052 )
20053 })
20054 .unwrap()
20055 .await
20056 .downcast::<Editor>()
20057 .unwrap();
20058 pane.update(cx, |pane, cx| {
20059 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20060 open_editor.update(cx, |editor, cx| {
20061 assert_eq!(
20062 editor.display_text(cx),
20063 main_text,
20064 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
20065 );
20066 })
20067 });
20068}
20069
20070#[gpui::test]
20071async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
20072 struct EmptyModalView {
20073 focus_handle: gpui::FocusHandle,
20074 }
20075 impl EventEmitter<DismissEvent> for EmptyModalView {}
20076 impl Render for EmptyModalView {
20077 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
20078 div()
20079 }
20080 }
20081 impl Focusable for EmptyModalView {
20082 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
20083 self.focus_handle.clone()
20084 }
20085 }
20086 impl workspace::ModalView for EmptyModalView {}
20087 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
20088 EmptyModalView {
20089 focus_handle: cx.focus_handle(),
20090 }
20091 }
20092
20093 init_test(cx, |_| {});
20094
20095 let fs = FakeFs::new(cx.executor());
20096 let project = Project::test(fs, [], cx).await;
20097 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20098 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
20099 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20100 let editor = cx.new_window_entity(|window, cx| {
20101 Editor::new(
20102 EditorMode::full(),
20103 buffer,
20104 Some(project.clone()),
20105 window,
20106 cx,
20107 )
20108 });
20109 workspace
20110 .update(cx, |workspace, window, cx| {
20111 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
20112 })
20113 .unwrap();
20114 editor.update_in(cx, |editor, window, cx| {
20115 editor.open_context_menu(&OpenContextMenu, window, cx);
20116 assert!(editor.mouse_context_menu.is_some());
20117 });
20118 workspace
20119 .update(cx, |workspace, window, cx| {
20120 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
20121 })
20122 .unwrap();
20123 cx.read(|cx| {
20124 assert!(editor.read(cx).mouse_context_menu.is_none());
20125 });
20126}
20127
20128#[gpui::test]
20129async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
20130 init_test(cx, |_| {});
20131
20132 let fs = FakeFs::new(cx.executor());
20133 fs.insert_file(path!("/file.html"), Default::default())
20134 .await;
20135
20136 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
20137
20138 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
20139 let html_language = Arc::new(Language::new(
20140 LanguageConfig {
20141 name: "HTML".into(),
20142 matcher: LanguageMatcher {
20143 path_suffixes: vec!["html".to_string()],
20144 ..LanguageMatcher::default()
20145 },
20146 brackets: BracketPairConfig {
20147 pairs: vec![BracketPair {
20148 start: "<".into(),
20149 end: ">".into(),
20150 close: true,
20151 ..Default::default()
20152 }],
20153 ..Default::default()
20154 },
20155 ..Default::default()
20156 },
20157 Some(tree_sitter_html::LANGUAGE.into()),
20158 ));
20159 language_registry.add(html_language);
20160 let mut fake_servers = language_registry.register_fake_lsp(
20161 "HTML",
20162 FakeLspAdapter {
20163 capabilities: lsp::ServerCapabilities {
20164 completion_provider: Some(lsp::CompletionOptions {
20165 resolve_provider: Some(true),
20166 ..Default::default()
20167 }),
20168 ..Default::default()
20169 },
20170 ..Default::default()
20171 },
20172 );
20173
20174 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20175 let cx = &mut VisualTestContext::from_window(*workspace, cx);
20176
20177 let worktree_id = workspace
20178 .update(cx, |workspace, _window, cx| {
20179 workspace.project().update(cx, |project, cx| {
20180 project.worktrees(cx).next().unwrap().read(cx).id()
20181 })
20182 })
20183 .unwrap();
20184 project
20185 .update(cx, |project, cx| {
20186 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
20187 })
20188 .await
20189 .unwrap();
20190 let editor = workspace
20191 .update(cx, |workspace, window, cx| {
20192 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
20193 })
20194 .unwrap()
20195 .await
20196 .unwrap()
20197 .downcast::<Editor>()
20198 .unwrap();
20199
20200 let fake_server = fake_servers.next().await.unwrap();
20201 editor.update_in(cx, |editor, window, cx| {
20202 editor.set_text("<ad></ad>", window, cx);
20203 editor.change_selections(None, window, cx, |selections| {
20204 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
20205 });
20206 let Some((buffer, _)) = editor
20207 .buffer
20208 .read(cx)
20209 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
20210 else {
20211 panic!("Failed to get buffer for selection position");
20212 };
20213 let buffer = buffer.read(cx);
20214 let buffer_id = buffer.remote_id();
20215 let opening_range =
20216 buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3));
20217 let closing_range =
20218 buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
20219 let mut linked_ranges = HashMap::default();
20220 linked_ranges.insert(
20221 buffer_id,
20222 vec![(opening_range.clone(), vec![closing_range.clone()])],
20223 );
20224 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
20225 });
20226 let mut completion_handle =
20227 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
20228 Ok(Some(lsp::CompletionResponse::Array(vec![
20229 lsp::CompletionItem {
20230 label: "head".to_string(),
20231 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
20232 lsp::InsertReplaceEdit {
20233 new_text: "head".to_string(),
20234 insert: lsp::Range::new(
20235 lsp::Position::new(0, 1),
20236 lsp::Position::new(0, 3),
20237 ),
20238 replace: lsp::Range::new(
20239 lsp::Position::new(0, 1),
20240 lsp::Position::new(0, 3),
20241 ),
20242 },
20243 )),
20244 ..Default::default()
20245 },
20246 ])))
20247 });
20248 editor.update_in(cx, |editor, window, cx| {
20249 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
20250 });
20251 cx.run_until_parked();
20252 completion_handle.next().await.unwrap();
20253 editor.update(cx, |editor, _| {
20254 assert!(
20255 editor.context_menu_visible(),
20256 "Completion menu should be visible"
20257 );
20258 });
20259 editor.update_in(cx, |editor, window, cx| {
20260 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
20261 });
20262 cx.executor().run_until_parked();
20263 editor.update(cx, |editor, cx| {
20264 assert_eq!(editor.text(cx), "<head></head>");
20265 });
20266}
20267
20268#[gpui::test]
20269async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
20270 init_test(cx, |_| {});
20271
20272 let fs = FakeFs::new(cx.executor());
20273 fs.insert_tree(
20274 path!("/root"),
20275 json!({
20276 "a": {
20277 "main.rs": "fn main() {}",
20278 },
20279 "foo": {
20280 "bar": {
20281 "external_file.rs": "pub mod external {}",
20282 }
20283 }
20284 }),
20285 )
20286 .await;
20287
20288 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
20289 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
20290 language_registry.add(rust_lang());
20291 let _fake_servers = language_registry.register_fake_lsp(
20292 "Rust",
20293 FakeLspAdapter {
20294 ..FakeLspAdapter::default()
20295 },
20296 );
20297 let (workspace, cx) =
20298 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20299 let worktree_id = workspace.update(cx, |workspace, cx| {
20300 workspace.project().update(cx, |project, cx| {
20301 project.worktrees(cx).next().unwrap().read(cx).id()
20302 })
20303 });
20304
20305 let assert_language_servers_count =
20306 |expected: usize, context: &str, cx: &mut VisualTestContext| {
20307 project.update(cx, |project, cx| {
20308 let current = project
20309 .lsp_store()
20310 .read(cx)
20311 .as_local()
20312 .unwrap()
20313 .language_servers
20314 .len();
20315 assert_eq!(expected, current, "{context}");
20316 });
20317 };
20318
20319 assert_language_servers_count(
20320 0,
20321 "No servers should be running before any file is open",
20322 cx,
20323 );
20324 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
20325 let main_editor = workspace
20326 .update_in(cx, |workspace, window, cx| {
20327 workspace.open_path(
20328 (worktree_id, "main.rs"),
20329 Some(pane.downgrade()),
20330 true,
20331 window,
20332 cx,
20333 )
20334 })
20335 .unwrap()
20336 .await
20337 .downcast::<Editor>()
20338 .unwrap();
20339 pane.update(cx, |pane, cx| {
20340 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20341 open_editor.update(cx, |editor, cx| {
20342 assert_eq!(
20343 editor.display_text(cx),
20344 "fn main() {}",
20345 "Original main.rs text on initial open",
20346 );
20347 });
20348 assert_eq!(open_editor, main_editor);
20349 });
20350 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
20351
20352 let external_editor = workspace
20353 .update_in(cx, |workspace, window, cx| {
20354 workspace.open_abs_path(
20355 PathBuf::from("/root/foo/bar/external_file.rs"),
20356 OpenOptions::default(),
20357 window,
20358 cx,
20359 )
20360 })
20361 .await
20362 .expect("opening external file")
20363 .downcast::<Editor>()
20364 .expect("downcasted external file's open element to editor");
20365 pane.update(cx, |pane, cx| {
20366 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20367 open_editor.update(cx, |editor, cx| {
20368 assert_eq!(
20369 editor.display_text(cx),
20370 "pub mod external {}",
20371 "External file is open now",
20372 );
20373 });
20374 assert_eq!(open_editor, external_editor);
20375 });
20376 assert_language_servers_count(
20377 1,
20378 "Second, external, *.rs file should join the existing server",
20379 cx,
20380 );
20381
20382 pane.update_in(cx, |pane, window, cx| {
20383 pane.close_active_item(&CloseActiveItem::default(), window, cx)
20384 })
20385 .unwrap()
20386 .await
20387 .unwrap();
20388 pane.update_in(cx, |pane, window, cx| {
20389 pane.navigate_backward(window, cx);
20390 });
20391 cx.run_until_parked();
20392 pane.update(cx, |pane, cx| {
20393 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20394 open_editor.update(cx, |editor, cx| {
20395 assert_eq!(
20396 editor.display_text(cx),
20397 "pub mod external {}",
20398 "External file is open now",
20399 );
20400 });
20401 });
20402 assert_language_servers_count(
20403 1,
20404 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
20405 cx,
20406 );
20407
20408 cx.update(|_, cx| {
20409 workspace::reload(&workspace::Reload::default(), cx);
20410 });
20411 assert_language_servers_count(
20412 1,
20413 "After reloading the worktree with local and external files opened, only one project should be started",
20414 cx,
20415 );
20416}
20417
20418#[gpui::test]
20419async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
20420 init_test(cx, |_| {});
20421
20422 let mut cx = EditorTestContext::new(cx).await;
20423 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
20424 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
20425
20426 // test cursor move to start of each line on tab
20427 // for `if`, `elif`, `else`, `while`, `with` and `for`
20428 cx.set_state(indoc! {"
20429 def main():
20430 ˇ for item in items:
20431 ˇ while item.active:
20432 ˇ if item.value > 10:
20433 ˇ continue
20434 ˇ elif item.value < 0:
20435 ˇ break
20436 ˇ else:
20437 ˇ with item.context() as ctx:
20438 ˇ yield count
20439 ˇ else:
20440 ˇ log('while else')
20441 ˇ else:
20442 ˇ log('for else')
20443 "});
20444 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
20445 cx.assert_editor_state(indoc! {"
20446 def main():
20447 ˇfor item in items:
20448 ˇwhile item.active:
20449 ˇif item.value > 10:
20450 ˇcontinue
20451 ˇelif item.value < 0:
20452 ˇbreak
20453 ˇelse:
20454 ˇwith item.context() as ctx:
20455 ˇyield count
20456 ˇelse:
20457 ˇlog('while else')
20458 ˇelse:
20459 ˇlog('for else')
20460 "});
20461 // test relative indent is preserved when tab
20462 // for `if`, `elif`, `else`, `while`, `with` and `for`
20463 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
20464 cx.assert_editor_state(indoc! {"
20465 def main():
20466 ˇfor item in items:
20467 ˇwhile item.active:
20468 ˇif item.value > 10:
20469 ˇcontinue
20470 ˇelif item.value < 0:
20471 ˇbreak
20472 ˇelse:
20473 ˇwith item.context() as ctx:
20474 ˇyield count
20475 ˇelse:
20476 ˇlog('while else')
20477 ˇelse:
20478 ˇlog('for else')
20479 "});
20480
20481 // test cursor move to start of each line on tab
20482 // for `try`, `except`, `else`, `finally`, `match` and `def`
20483 cx.set_state(indoc! {"
20484 def main():
20485 ˇ try:
20486 ˇ fetch()
20487 ˇ except ValueError:
20488 ˇ handle_error()
20489 ˇ else:
20490 ˇ match value:
20491 ˇ case _:
20492 ˇ finally:
20493 ˇ def status():
20494 ˇ return 0
20495 "});
20496 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
20497 cx.assert_editor_state(indoc! {"
20498 def main():
20499 ˇtry:
20500 ˇfetch()
20501 ˇexcept ValueError:
20502 ˇhandle_error()
20503 ˇelse:
20504 ˇmatch value:
20505 ˇcase _:
20506 ˇfinally:
20507 ˇdef status():
20508 ˇreturn 0
20509 "});
20510 // test relative indent is preserved when tab
20511 // for `try`, `except`, `else`, `finally`, `match` and `def`
20512 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
20513 cx.assert_editor_state(indoc! {"
20514 def main():
20515 ˇtry:
20516 ˇfetch()
20517 ˇexcept ValueError:
20518 ˇhandle_error()
20519 ˇelse:
20520 ˇmatch value:
20521 ˇcase _:
20522 ˇfinally:
20523 ˇdef status():
20524 ˇreturn 0
20525 "});
20526}
20527
20528#[gpui::test]
20529async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
20530 init_test(cx, |_| {});
20531
20532 let mut cx = EditorTestContext::new(cx).await;
20533 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
20534 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
20535
20536 // test `else` auto outdents when typed inside `if` block
20537 cx.set_state(indoc! {"
20538 def main():
20539 if i == 2:
20540 return
20541 ˇ
20542 "});
20543 cx.update_editor(|editor, window, cx| {
20544 editor.handle_input("else:", window, cx);
20545 });
20546 cx.assert_editor_state(indoc! {"
20547 def main():
20548 if i == 2:
20549 return
20550 else:ˇ
20551 "});
20552
20553 // test `except` auto outdents when typed inside `try` block
20554 cx.set_state(indoc! {"
20555 def main():
20556 try:
20557 i = 2
20558 ˇ
20559 "});
20560 cx.update_editor(|editor, window, cx| {
20561 editor.handle_input("except:", window, cx);
20562 });
20563 cx.assert_editor_state(indoc! {"
20564 def main():
20565 try:
20566 i = 2
20567 except:ˇ
20568 "});
20569
20570 // test `else` auto outdents when typed inside `except` block
20571 cx.set_state(indoc! {"
20572 def main():
20573 try:
20574 i = 2
20575 except:
20576 j = 2
20577 ˇ
20578 "});
20579 cx.update_editor(|editor, window, cx| {
20580 editor.handle_input("else:", window, cx);
20581 });
20582 cx.assert_editor_state(indoc! {"
20583 def main():
20584 try:
20585 i = 2
20586 except:
20587 j = 2
20588 else:ˇ
20589 "});
20590
20591 // test `finally` auto outdents when typed inside `else` block
20592 cx.set_state(indoc! {"
20593 def main():
20594 try:
20595 i = 2
20596 except:
20597 j = 2
20598 else:
20599 k = 2
20600 ˇ
20601 "});
20602 cx.update_editor(|editor, window, cx| {
20603 editor.handle_input("finally:", window, cx);
20604 });
20605 cx.assert_editor_state(indoc! {"
20606 def main():
20607 try:
20608 i = 2
20609 except:
20610 j = 2
20611 else:
20612 k = 2
20613 finally:ˇ
20614 "});
20615
20616 // TODO: test `except` auto outdents when typed inside `try` block right after for block
20617 // cx.set_state(indoc! {"
20618 // def main():
20619 // try:
20620 // for i in range(n):
20621 // pass
20622 // ˇ
20623 // "});
20624 // cx.update_editor(|editor, window, cx| {
20625 // editor.handle_input("except:", window, cx);
20626 // });
20627 // cx.assert_editor_state(indoc! {"
20628 // def main():
20629 // try:
20630 // for i in range(n):
20631 // pass
20632 // except:ˇ
20633 // "});
20634
20635 // TODO: test `else` auto outdents when typed inside `except` block right after for block
20636 // cx.set_state(indoc! {"
20637 // def main():
20638 // try:
20639 // i = 2
20640 // except:
20641 // for i in range(n):
20642 // pass
20643 // ˇ
20644 // "});
20645 // cx.update_editor(|editor, window, cx| {
20646 // editor.handle_input("else:", window, cx);
20647 // });
20648 // cx.assert_editor_state(indoc! {"
20649 // def main():
20650 // try:
20651 // i = 2
20652 // except:
20653 // for i in range(n):
20654 // pass
20655 // else:ˇ
20656 // "});
20657
20658 // TODO: test `finally` auto outdents when typed inside `else` block right after for block
20659 // cx.set_state(indoc! {"
20660 // def main():
20661 // try:
20662 // i = 2
20663 // except:
20664 // j = 2
20665 // else:
20666 // for i in range(n):
20667 // pass
20668 // ˇ
20669 // "});
20670 // cx.update_editor(|editor, window, cx| {
20671 // editor.handle_input("finally:", window, cx);
20672 // });
20673 // cx.assert_editor_state(indoc! {"
20674 // def main():
20675 // try:
20676 // i = 2
20677 // except:
20678 // j = 2
20679 // else:
20680 // for i in range(n):
20681 // pass
20682 // finally:ˇ
20683 // "});
20684
20685 // test `else` stays at correct indent when typed after `for` block
20686 cx.set_state(indoc! {"
20687 def main():
20688 for i in range(10):
20689 if i == 3:
20690 break
20691 ˇ
20692 "});
20693 cx.update_editor(|editor, window, cx| {
20694 editor.handle_input("else:", window, cx);
20695 });
20696 cx.assert_editor_state(indoc! {"
20697 def main():
20698 for i in range(10):
20699 if i == 3:
20700 break
20701 else:ˇ
20702 "});
20703}
20704
20705#[gpui::test]
20706async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
20707 init_test(cx, |_| {});
20708 update_test_language_settings(cx, |settings| {
20709 settings.defaults.extend_comment_on_newline = Some(false);
20710 });
20711 let mut cx = EditorTestContext::new(cx).await;
20712 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
20713 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
20714
20715 // test correct indent after newline on comment
20716 cx.set_state(indoc! {"
20717 # COMMENT:ˇ
20718 "});
20719 cx.update_editor(|editor, window, cx| {
20720 editor.newline(&Newline, window, cx);
20721 });
20722 cx.assert_editor_state(indoc! {"
20723 # COMMENT:
20724 ˇ
20725 "});
20726
20727 // test correct indent after newline in curly brackets
20728 cx.set_state(indoc! {"
20729 {ˇ}
20730 "});
20731 cx.update_editor(|editor, window, cx| {
20732 editor.newline(&Newline, window, cx);
20733 });
20734 cx.run_until_parked();
20735 cx.assert_editor_state(indoc! {"
20736 {
20737 ˇ
20738 }
20739 "});
20740}
20741
20742fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
20743 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
20744 point..point
20745}
20746
20747fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
20748 let (text, ranges) = marked_text_ranges(marked_text, true);
20749 assert_eq!(editor.text(cx), text);
20750 assert_eq!(
20751 editor.selections.ranges(cx),
20752 ranges,
20753 "Assert selections are {}",
20754 marked_text
20755 );
20756}
20757
20758pub fn handle_signature_help_request(
20759 cx: &mut EditorLspTestContext,
20760 mocked_response: lsp::SignatureHelp,
20761) -> impl Future<Output = ()> + use<> {
20762 let mut request =
20763 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
20764 let mocked_response = mocked_response.clone();
20765 async move { Ok(Some(mocked_response)) }
20766 });
20767
20768 async move {
20769 request.next().await;
20770 }
20771}
20772
20773/// Handle completion request passing a marked string specifying where the completion
20774/// should be triggered from using '|' character, what range should be replaced, and what completions
20775/// should be returned using '<' and '>' to delimit the range.
20776///
20777/// Also see `handle_completion_request_with_insert_and_replace`.
20778#[track_caller]
20779pub fn handle_completion_request(
20780 cx: &mut EditorLspTestContext,
20781 marked_string: &str,
20782 completions: Vec<&'static str>,
20783 counter: Arc<AtomicUsize>,
20784) -> impl Future<Output = ()> {
20785 let complete_from_marker: TextRangeMarker = '|'.into();
20786 let replace_range_marker: TextRangeMarker = ('<', '>').into();
20787 let (_, mut marked_ranges) = marked_text_ranges_by(
20788 marked_string,
20789 vec![complete_from_marker.clone(), replace_range_marker.clone()],
20790 );
20791
20792 let complete_from_position =
20793 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
20794 let replace_range =
20795 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
20796
20797 let mut request =
20798 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
20799 let completions = completions.clone();
20800 counter.fetch_add(1, atomic::Ordering::Release);
20801 async move {
20802 assert_eq!(params.text_document_position.text_document.uri, url.clone());
20803 assert_eq!(
20804 params.text_document_position.position,
20805 complete_from_position
20806 );
20807 Ok(Some(lsp::CompletionResponse::Array(
20808 completions
20809 .iter()
20810 .map(|completion_text| lsp::CompletionItem {
20811 label: completion_text.to_string(),
20812 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
20813 range: replace_range,
20814 new_text: completion_text.to_string(),
20815 })),
20816 ..Default::default()
20817 })
20818 .collect(),
20819 )))
20820 }
20821 });
20822
20823 async move {
20824 request.next().await;
20825 }
20826}
20827
20828/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
20829/// given instead, which also contains an `insert` range.
20830///
20831/// This function uses the cursor position to mimic what Rust-Analyzer provides as the `insert` range,
20832/// that is, `replace_range.start..cursor_pos`.
20833pub fn handle_completion_request_with_insert_and_replace(
20834 cx: &mut EditorLspTestContext,
20835 marked_string: &str,
20836 completions: Vec<&'static str>,
20837 counter: Arc<AtomicUsize>,
20838) -> impl Future<Output = ()> {
20839 let complete_from_marker: TextRangeMarker = '|'.into();
20840 let replace_range_marker: TextRangeMarker = ('<', '>').into();
20841 let (_, mut marked_ranges) = marked_text_ranges_by(
20842 marked_string,
20843 vec![complete_from_marker.clone(), replace_range_marker.clone()],
20844 );
20845
20846 let complete_from_position =
20847 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
20848 let replace_range =
20849 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
20850
20851 let mut request =
20852 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
20853 let completions = completions.clone();
20854 counter.fetch_add(1, atomic::Ordering::Release);
20855 async move {
20856 assert_eq!(params.text_document_position.text_document.uri, url.clone());
20857 assert_eq!(
20858 params.text_document_position.position, complete_from_position,
20859 "marker `|` position doesn't match",
20860 );
20861 Ok(Some(lsp::CompletionResponse::Array(
20862 completions
20863 .iter()
20864 .map(|completion_text| lsp::CompletionItem {
20865 label: completion_text.to_string(),
20866 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
20867 lsp::InsertReplaceEdit {
20868 insert: lsp::Range {
20869 start: replace_range.start,
20870 end: complete_from_position,
20871 },
20872 replace: replace_range,
20873 new_text: completion_text.to_string(),
20874 },
20875 )),
20876 ..Default::default()
20877 })
20878 .collect(),
20879 )))
20880 }
20881 });
20882
20883 async move {
20884 request.next().await;
20885 }
20886}
20887
20888fn handle_resolve_completion_request(
20889 cx: &mut EditorLspTestContext,
20890 edits: Option<Vec<(&'static str, &'static str)>>,
20891) -> impl Future<Output = ()> {
20892 let edits = edits.map(|edits| {
20893 edits
20894 .iter()
20895 .map(|(marked_string, new_text)| {
20896 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
20897 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
20898 lsp::TextEdit::new(replace_range, new_text.to_string())
20899 })
20900 .collect::<Vec<_>>()
20901 });
20902
20903 let mut request =
20904 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
20905 let edits = edits.clone();
20906 async move {
20907 Ok(lsp::CompletionItem {
20908 additional_text_edits: edits,
20909 ..Default::default()
20910 })
20911 }
20912 });
20913
20914 async move {
20915 request.next().await;
20916 }
20917}
20918
20919pub(crate) fn update_test_language_settings(
20920 cx: &mut TestAppContext,
20921 f: impl Fn(&mut AllLanguageSettingsContent),
20922) {
20923 cx.update(|cx| {
20924 SettingsStore::update_global(cx, |store, cx| {
20925 store.update_user_settings::<AllLanguageSettings>(cx, f);
20926 });
20927 });
20928}
20929
20930pub(crate) fn update_test_project_settings(
20931 cx: &mut TestAppContext,
20932 f: impl Fn(&mut ProjectSettings),
20933) {
20934 cx.update(|cx| {
20935 SettingsStore::update_global(cx, |store, cx| {
20936 store.update_user_settings::<ProjectSettings>(cx, f);
20937 });
20938 });
20939}
20940
20941pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
20942 cx.update(|cx| {
20943 assets::Assets.load_test_fonts(cx);
20944 let store = SettingsStore::test(cx);
20945 cx.set_global(store);
20946 theme::init(theme::LoadThemes::JustBase, cx);
20947 release_channel::init(SemanticVersion::default(), cx);
20948 client::init_settings(cx);
20949 language::init(cx);
20950 Project::init_settings(cx);
20951 workspace::init_settings(cx);
20952 crate::init(cx);
20953 });
20954
20955 update_test_language_settings(cx, f);
20956}
20957
20958#[track_caller]
20959fn assert_hunk_revert(
20960 not_reverted_text_with_selections: &str,
20961 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
20962 expected_reverted_text_with_selections: &str,
20963 base_text: &str,
20964 cx: &mut EditorLspTestContext,
20965) {
20966 cx.set_state(not_reverted_text_with_selections);
20967 cx.set_head_text(base_text);
20968 cx.executor().run_until_parked();
20969
20970 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
20971 let snapshot = editor.snapshot(window, cx);
20972 let reverted_hunk_statuses = snapshot
20973 .buffer_snapshot
20974 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
20975 .map(|hunk| hunk.status().kind)
20976 .collect::<Vec<_>>();
20977
20978 editor.git_restore(&Default::default(), window, cx);
20979 reverted_hunk_statuses
20980 });
20981 cx.executor().run_until_parked();
20982 cx.assert_editor_state(expected_reverted_text_with_selections);
20983 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
20984}