1use super::*;
2use crate::{
3 JoinLines,
4 code_context_menus::CodeContextMenu,
5 inline_completion_tests::FakeInlineCompletionProvider,
6 linked_editing_ranges::LinkedEditingRanges,
7 scroll::scroll_amount::ScrollAmount,
8 test::{
9 assert_text_with_selections, build_editor,
10 editor_lsp_test_context::{EditorLspTestContext, git_commit_lang},
11 editor_test_context::EditorTestContext,
12 select_ranges,
13 },
14};
15use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
16use futures::StreamExt;
17use gpui::{
18 BackgroundExecutor, DismissEvent, Rgba, SemanticVersion, TestAppContext, UpdateGlobal,
19 VisualTestContext, WindowBounds, WindowOptions, div,
20};
21use indoc::indoc;
22use language::{
23 BracketPairConfig,
24 Capability::ReadWrite,
25 DiagnosticSourceKind, FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher,
26 LanguageName, Override, Point,
27 language_settings::{
28 AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings,
29 LanguageSettingsContent, LspInsertMode, PrettierSettings,
30 },
31 tree_sitter_python,
32};
33use language_settings::{Formatter, FormatterList, IndentGuideSettings};
34use lsp::CompletionParams;
35use multi_buffer::{IndentGuide, PathKey};
36use parking_lot::Mutex;
37use pretty_assertions::{assert_eq, assert_ne};
38use project::{
39 FakeFs,
40 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
41 project_settings::{LspSettings, ProjectSettings},
42};
43use serde_json::{self, json};
44use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
45use std::{
46 iter,
47 sync::atomic::{self, AtomicUsize},
48};
49use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
50use text::ToPoint as _;
51use unindent::Unindent;
52use util::{
53 assert_set_eq, path,
54 test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
55 uri,
56};
57use workspace::{
58 CloseActiveItem, CloseAllItems, CloseInactiveItems, MoveItemToPaneInDirection, NavigationEntry,
59 OpenOptions, ViewId,
60 item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
61};
62
63#[gpui::test]
64fn test_edit_events(cx: &mut TestAppContext) {
65 init_test(cx, |_| {});
66
67 let buffer = cx.new(|cx| {
68 let mut buffer = language::Buffer::local("123456", cx);
69 buffer.set_group_interval(Duration::from_secs(1));
70 buffer
71 });
72
73 let events = Rc::new(RefCell::new(Vec::new()));
74 let editor1 = cx.add_window({
75 let events = events.clone();
76 |window, cx| {
77 let entity = cx.entity().clone();
78 cx.subscribe_in(
79 &entity,
80 window,
81 move |_, _, event: &EditorEvent, _, _| match event {
82 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
83 EditorEvent::BufferEdited => {
84 events.borrow_mut().push(("editor1", "buffer edited"))
85 }
86 _ => {}
87 },
88 )
89 .detach();
90 Editor::for_buffer(buffer.clone(), None, window, cx)
91 }
92 });
93
94 let editor2 = cx.add_window({
95 let events = events.clone();
96 |window, cx| {
97 cx.subscribe_in(
98 &cx.entity().clone(),
99 window,
100 move |_, _, event: &EditorEvent, _, _| match event {
101 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
102 EditorEvent::BufferEdited => {
103 events.borrow_mut().push(("editor2", "buffer edited"))
104 }
105 _ => {}
106 },
107 )
108 .detach();
109 Editor::for_buffer(buffer.clone(), None, window, cx)
110 }
111 });
112
113 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
114
115 // Mutating editor 1 will emit an `Edited` event only for that editor.
116 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
117 assert_eq!(
118 mem::take(&mut *events.borrow_mut()),
119 [
120 ("editor1", "edited"),
121 ("editor1", "buffer edited"),
122 ("editor2", "buffer edited"),
123 ]
124 );
125
126 // Mutating editor 2 will emit an `Edited` event only for that editor.
127 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
128 assert_eq!(
129 mem::take(&mut *events.borrow_mut()),
130 [
131 ("editor2", "edited"),
132 ("editor1", "buffer edited"),
133 ("editor2", "buffer edited"),
134 ]
135 );
136
137 // Undoing on editor 1 will emit an `Edited` event only for that editor.
138 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
139 assert_eq!(
140 mem::take(&mut *events.borrow_mut()),
141 [
142 ("editor1", "edited"),
143 ("editor1", "buffer edited"),
144 ("editor2", "buffer edited"),
145 ]
146 );
147
148 // Redoing on editor 1 will emit an `Edited` event only for that editor.
149 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
150 assert_eq!(
151 mem::take(&mut *events.borrow_mut()),
152 [
153 ("editor1", "edited"),
154 ("editor1", "buffer edited"),
155 ("editor2", "buffer edited"),
156 ]
157 );
158
159 // Undoing on editor 2 will emit an `Edited` event only for that editor.
160 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
161 assert_eq!(
162 mem::take(&mut *events.borrow_mut()),
163 [
164 ("editor2", "edited"),
165 ("editor1", "buffer edited"),
166 ("editor2", "buffer edited"),
167 ]
168 );
169
170 // Redoing on editor 2 will emit an `Edited` event only for that editor.
171 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
172 assert_eq!(
173 mem::take(&mut *events.borrow_mut()),
174 [
175 ("editor2", "edited"),
176 ("editor1", "buffer edited"),
177 ("editor2", "buffer edited"),
178 ]
179 );
180
181 // No event is emitted when the mutation is a no-op.
182 _ = editor2.update(cx, |editor, window, cx| {
183 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
184
185 editor.backspace(&Backspace, window, cx);
186 });
187 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
188}
189
190#[gpui::test]
191fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
192 init_test(cx, |_| {});
193
194 let mut now = Instant::now();
195 let group_interval = Duration::from_millis(1);
196 let buffer = cx.new(|cx| {
197 let mut buf = language::Buffer::local("123456", cx);
198 buf.set_group_interval(group_interval);
199 buf
200 });
201 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
202 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
203
204 _ = editor.update(cx, |editor, window, cx| {
205 editor.start_transaction_at(now, window, cx);
206 editor.change_selections(None, window, cx, |s| s.select_ranges([2..4]));
207
208 editor.insert("cd", window, cx);
209 editor.end_transaction_at(now, cx);
210 assert_eq!(editor.text(cx), "12cd56");
211 assert_eq!(editor.selections.ranges(cx), vec![4..4]);
212
213 editor.start_transaction_at(now, window, cx);
214 editor.change_selections(None, window, cx, |s| s.select_ranges([4..5]));
215 editor.insert("e", window, cx);
216 editor.end_transaction_at(now, cx);
217 assert_eq!(editor.text(cx), "12cde6");
218 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
219
220 now += group_interval + Duration::from_millis(1);
221 editor.change_selections(None, window, cx, |s| s.select_ranges([2..2]));
222
223 // Simulate an edit in another editor
224 buffer.update(cx, |buffer, cx| {
225 buffer.start_transaction_at(now, cx);
226 buffer.edit([(0..1, "a")], None, cx);
227 buffer.edit([(1..1, "b")], None, cx);
228 buffer.end_transaction_at(now, cx);
229 });
230
231 assert_eq!(editor.text(cx), "ab2cde6");
232 assert_eq!(editor.selections.ranges(cx), vec![3..3]);
233
234 // Last transaction happened past the group interval in a different editor.
235 // Undo it individually and don't restore selections.
236 editor.undo(&Undo, window, cx);
237 assert_eq!(editor.text(cx), "12cde6");
238 assert_eq!(editor.selections.ranges(cx), vec![2..2]);
239
240 // First two transactions happened within the group interval in this editor.
241 // Undo them together and restore selections.
242 editor.undo(&Undo, window, cx);
243 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
244 assert_eq!(editor.text(cx), "123456");
245 assert_eq!(editor.selections.ranges(cx), vec![0..0]);
246
247 // Redo the first two transactions together.
248 editor.redo(&Redo, window, cx);
249 assert_eq!(editor.text(cx), "12cde6");
250 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
251
252 // Redo the last transaction on its own.
253 editor.redo(&Redo, window, cx);
254 assert_eq!(editor.text(cx), "ab2cde6");
255 assert_eq!(editor.selections.ranges(cx), vec![6..6]);
256
257 // Test empty transactions.
258 editor.start_transaction_at(now, window, cx);
259 editor.end_transaction_at(now, cx);
260 editor.undo(&Undo, window, cx);
261 assert_eq!(editor.text(cx), "12cde6");
262 });
263}
264
265#[gpui::test]
266fn test_ime_composition(cx: &mut TestAppContext) {
267 init_test(cx, |_| {});
268
269 let buffer = cx.new(|cx| {
270 let mut buffer = language::Buffer::local("abcde", cx);
271 // Ensure automatic grouping doesn't occur.
272 buffer.set_group_interval(Duration::ZERO);
273 buffer
274 });
275
276 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
277 cx.add_window(|window, cx| {
278 let mut editor = build_editor(buffer.clone(), window, cx);
279
280 // Start a new IME composition.
281 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
282 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
283 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
284 assert_eq!(editor.text(cx), "äbcde");
285 assert_eq!(
286 editor.marked_text_ranges(cx),
287 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
288 );
289
290 // Finalize IME composition.
291 editor.replace_text_in_range(None, "ā", window, cx);
292 assert_eq!(editor.text(cx), "ābcde");
293 assert_eq!(editor.marked_text_ranges(cx), None);
294
295 // IME composition edits are grouped and are undone/redone at once.
296 editor.undo(&Default::default(), window, cx);
297 assert_eq!(editor.text(cx), "abcde");
298 assert_eq!(editor.marked_text_ranges(cx), None);
299 editor.redo(&Default::default(), window, cx);
300 assert_eq!(editor.text(cx), "ābcde");
301 assert_eq!(editor.marked_text_ranges(cx), None);
302
303 // Start a new IME composition.
304 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
305 assert_eq!(
306 editor.marked_text_ranges(cx),
307 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
308 );
309
310 // Undoing during an IME composition cancels it.
311 editor.undo(&Default::default(), window, cx);
312 assert_eq!(editor.text(cx), "ābcde");
313 assert_eq!(editor.marked_text_ranges(cx), None);
314
315 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
316 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
317 assert_eq!(editor.text(cx), "ābcdè");
318 assert_eq!(
319 editor.marked_text_ranges(cx),
320 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
321 );
322
323 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
324 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
325 assert_eq!(editor.text(cx), "ābcdę");
326 assert_eq!(editor.marked_text_ranges(cx), None);
327
328 // Start a new IME composition with multiple cursors.
329 editor.change_selections(None, window, cx, |s| {
330 s.select_ranges([
331 OffsetUtf16(1)..OffsetUtf16(1),
332 OffsetUtf16(3)..OffsetUtf16(3),
333 OffsetUtf16(5)..OffsetUtf16(5),
334 ])
335 });
336 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
337 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
338 assert_eq!(
339 editor.marked_text_ranges(cx),
340 Some(vec![
341 OffsetUtf16(0)..OffsetUtf16(3),
342 OffsetUtf16(4)..OffsetUtf16(7),
343 OffsetUtf16(8)..OffsetUtf16(11)
344 ])
345 );
346
347 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
348 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
349 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
350 assert_eq!(
351 editor.marked_text_ranges(cx),
352 Some(vec![
353 OffsetUtf16(1)..OffsetUtf16(2),
354 OffsetUtf16(5)..OffsetUtf16(6),
355 OffsetUtf16(9)..OffsetUtf16(10)
356 ])
357 );
358
359 // Finalize IME composition with multiple cursors.
360 editor.replace_text_in_range(Some(9..10), "2", window, cx);
361 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
362 assert_eq!(editor.marked_text_ranges(cx), None);
363
364 editor
365 });
366}
367
368#[gpui::test]
369fn test_selection_with_mouse(cx: &mut TestAppContext) {
370 init_test(cx, |_| {});
371
372 let editor = cx.add_window(|window, cx| {
373 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
374 build_editor(buffer, window, cx)
375 });
376
377 _ = editor.update(cx, |editor, window, cx| {
378 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
379 });
380 assert_eq!(
381 editor
382 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
383 .unwrap(),
384 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
385 );
386
387 _ = editor.update(cx, |editor, window, cx| {
388 editor.update_selection(
389 DisplayPoint::new(DisplayRow(3), 3),
390 0,
391 gpui::Point::<f32>::default(),
392 window,
393 cx,
394 );
395 });
396
397 assert_eq!(
398 editor
399 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
400 .unwrap(),
401 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
402 );
403
404 _ = editor.update(cx, |editor, window, cx| {
405 editor.update_selection(
406 DisplayPoint::new(DisplayRow(1), 1),
407 0,
408 gpui::Point::<f32>::default(),
409 window,
410 cx,
411 );
412 });
413
414 assert_eq!(
415 editor
416 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
417 .unwrap(),
418 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
419 );
420
421 _ = editor.update(cx, |editor, window, cx| {
422 editor.end_selection(window, cx);
423 editor.update_selection(
424 DisplayPoint::new(DisplayRow(3), 3),
425 0,
426 gpui::Point::<f32>::default(),
427 window,
428 cx,
429 );
430 });
431
432 assert_eq!(
433 editor
434 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
435 .unwrap(),
436 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
437 );
438
439 _ = editor.update(cx, |editor, window, cx| {
440 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
441 editor.update_selection(
442 DisplayPoint::new(DisplayRow(0), 0),
443 0,
444 gpui::Point::<f32>::default(),
445 window,
446 cx,
447 );
448 });
449
450 assert_eq!(
451 editor
452 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
453 .unwrap(),
454 [
455 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
456 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
457 ]
458 );
459
460 _ = editor.update(cx, |editor, window, cx| {
461 editor.end_selection(window, cx);
462 });
463
464 assert_eq!(
465 editor
466 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
467 .unwrap(),
468 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
469 );
470}
471
472#[gpui::test]
473fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
474 init_test(cx, |_| {});
475
476 let editor = cx.add_window(|window, cx| {
477 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
478 build_editor(buffer, window, cx)
479 });
480
481 _ = editor.update(cx, |editor, window, cx| {
482 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
483 });
484
485 _ = editor.update(cx, |editor, window, cx| {
486 editor.end_selection(window, cx);
487 });
488
489 _ = editor.update(cx, |editor, window, cx| {
490 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
491 });
492
493 _ = editor.update(cx, |editor, window, cx| {
494 editor.end_selection(window, cx);
495 });
496
497 assert_eq!(
498 editor
499 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
500 .unwrap(),
501 [
502 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
503 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
504 ]
505 );
506
507 _ = editor.update(cx, |editor, window, cx| {
508 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
509 });
510
511 _ = editor.update(cx, |editor, window, cx| {
512 editor.end_selection(window, cx);
513 });
514
515 assert_eq!(
516 editor
517 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
518 .unwrap(),
519 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
520 );
521}
522
523#[gpui::test]
524fn test_canceling_pending_selection(cx: &mut TestAppContext) {
525 init_test(cx, |_| {});
526
527 let editor = cx.add_window(|window, cx| {
528 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
529 build_editor(buffer, window, cx)
530 });
531
532 _ = editor.update(cx, |editor, window, cx| {
533 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
534 assert_eq!(
535 editor.selections.display_ranges(cx),
536 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
537 );
538 });
539
540 _ = editor.update(cx, |editor, window, cx| {
541 editor.update_selection(
542 DisplayPoint::new(DisplayRow(3), 3),
543 0,
544 gpui::Point::<f32>::default(),
545 window,
546 cx,
547 );
548 assert_eq!(
549 editor.selections.display_ranges(cx),
550 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
551 );
552 });
553
554 _ = editor.update(cx, |editor, window, cx| {
555 editor.cancel(&Cancel, window, cx);
556 editor.update_selection(
557 DisplayPoint::new(DisplayRow(1), 1),
558 0,
559 gpui::Point::<f32>::default(),
560 window,
561 cx,
562 );
563 assert_eq!(
564 editor.selections.display_ranges(cx),
565 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
566 );
567 });
568}
569
570#[gpui::test]
571fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
572 init_test(cx, |_| {});
573
574 let editor = cx.add_window(|window, cx| {
575 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
576 build_editor(buffer, window, cx)
577 });
578
579 _ = editor.update(cx, |editor, window, cx| {
580 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
581 assert_eq!(
582 editor.selections.display_ranges(cx),
583 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
584 );
585
586 editor.move_down(&Default::default(), window, cx);
587 assert_eq!(
588 editor.selections.display_ranges(cx),
589 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
590 );
591
592 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
593 assert_eq!(
594 editor.selections.display_ranges(cx),
595 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
596 );
597
598 editor.move_up(&Default::default(), window, cx);
599 assert_eq!(
600 editor.selections.display_ranges(cx),
601 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
602 );
603 });
604}
605
606#[gpui::test]
607fn test_clone(cx: &mut TestAppContext) {
608 init_test(cx, |_| {});
609
610 let (text, selection_ranges) = marked_text_ranges(
611 indoc! {"
612 one
613 two
614 threeˇ
615 four
616 fiveˇ
617 "},
618 true,
619 );
620
621 let editor = cx.add_window(|window, cx| {
622 let buffer = MultiBuffer::build_simple(&text, cx);
623 build_editor(buffer, window, cx)
624 });
625
626 _ = editor.update(cx, |editor, window, cx| {
627 editor.change_selections(None, window, cx, |s| {
628 s.select_ranges(selection_ranges.clone())
629 });
630 editor.fold_creases(
631 vec![
632 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
633 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
634 ],
635 true,
636 window,
637 cx,
638 );
639 });
640
641 let cloned_editor = editor
642 .update(cx, |editor, _, cx| {
643 cx.open_window(Default::default(), |window, cx| {
644 cx.new(|cx| editor.clone(window, cx))
645 })
646 })
647 .unwrap()
648 .unwrap();
649
650 let snapshot = editor
651 .update(cx, |e, window, cx| e.snapshot(window, cx))
652 .unwrap();
653 let cloned_snapshot = cloned_editor
654 .update(cx, |e, window, cx| e.snapshot(window, cx))
655 .unwrap();
656
657 assert_eq!(
658 cloned_editor
659 .update(cx, |e, _, cx| e.display_text(cx))
660 .unwrap(),
661 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
662 );
663 assert_eq!(
664 cloned_snapshot
665 .folds_in_range(0..text.len())
666 .collect::<Vec<_>>(),
667 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
668 );
669 assert_set_eq!(
670 cloned_editor
671 .update(cx, |editor, _, cx| editor.selections.ranges::<Point>(cx))
672 .unwrap(),
673 editor
674 .update(cx, |editor, _, cx| editor.selections.ranges(cx))
675 .unwrap()
676 );
677 assert_set_eq!(
678 cloned_editor
679 .update(cx, |e, _window, cx| e.selections.display_ranges(cx))
680 .unwrap(),
681 editor
682 .update(cx, |e, _, cx| e.selections.display_ranges(cx))
683 .unwrap()
684 );
685}
686
687#[gpui::test]
688async fn test_navigation_history(cx: &mut TestAppContext) {
689 init_test(cx, |_| {});
690
691 use workspace::item::Item;
692
693 let fs = FakeFs::new(cx.executor());
694 let project = Project::test(fs, [], cx).await;
695 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
696 let pane = workspace
697 .update(cx, |workspace, _, _| workspace.active_pane().clone())
698 .unwrap();
699
700 _ = workspace.update(cx, |_v, window, cx| {
701 cx.new(|cx| {
702 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
703 let mut editor = build_editor(buffer.clone(), window, cx);
704 let handle = cx.entity();
705 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
706
707 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
708 editor.nav_history.as_mut().unwrap().pop_backward(cx)
709 }
710
711 // Move the cursor a small distance.
712 // Nothing is added to the navigation history.
713 editor.change_selections(None, window, cx, |s| {
714 s.select_display_ranges([
715 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
716 ])
717 });
718 editor.change_selections(None, window, cx, |s| {
719 s.select_display_ranges([
720 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
721 ])
722 });
723 assert!(pop_history(&mut editor, cx).is_none());
724
725 // Move the cursor a large distance.
726 // The history can jump back to the previous position.
727 editor.change_selections(None, window, cx, |s| {
728 s.select_display_ranges([
729 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
730 ])
731 });
732 let nav_entry = pop_history(&mut editor, cx).unwrap();
733 editor.navigate(nav_entry.data.unwrap(), window, cx);
734 assert_eq!(nav_entry.item.id(), cx.entity_id());
735 assert_eq!(
736 editor.selections.display_ranges(cx),
737 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
738 );
739 assert!(pop_history(&mut editor, cx).is_none());
740
741 // Move the cursor a small distance via the mouse.
742 // Nothing is added to the navigation history.
743 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
744 editor.end_selection(window, cx);
745 assert_eq!(
746 editor.selections.display_ranges(cx),
747 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
748 );
749 assert!(pop_history(&mut editor, cx).is_none());
750
751 // Move the cursor a large distance via the mouse.
752 // The history can jump back to the previous position.
753 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
754 editor.end_selection(window, cx);
755 assert_eq!(
756 editor.selections.display_ranges(cx),
757 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
758 );
759 let nav_entry = pop_history(&mut editor, cx).unwrap();
760 editor.navigate(nav_entry.data.unwrap(), window, cx);
761 assert_eq!(nav_entry.item.id(), cx.entity_id());
762 assert_eq!(
763 editor.selections.display_ranges(cx),
764 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
765 );
766 assert!(pop_history(&mut editor, cx).is_none());
767
768 // Set scroll position to check later
769 editor.set_scroll_position(gpui::Point::<f32>::new(5.5, 5.5), window, cx);
770 let original_scroll_position = editor.scroll_manager.anchor();
771
772 // Jump to the end of the document and adjust scroll
773 editor.move_to_end(&MoveToEnd, window, cx);
774 editor.set_scroll_position(gpui::Point::<f32>::new(-2.5, -0.5), window, cx);
775 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
776
777 let nav_entry = pop_history(&mut editor, cx).unwrap();
778 editor.navigate(nav_entry.data.unwrap(), window, cx);
779 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
780
781 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
782 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
783 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
784 let invalid_point = Point::new(9999, 0);
785 editor.navigate(
786 Box::new(NavigationData {
787 cursor_anchor: invalid_anchor,
788 cursor_position: invalid_point,
789 scroll_anchor: ScrollAnchor {
790 anchor: invalid_anchor,
791 offset: Default::default(),
792 },
793 scroll_top_row: invalid_point.row,
794 }),
795 window,
796 cx,
797 );
798 assert_eq!(
799 editor.selections.display_ranges(cx),
800 &[editor.max_point(cx)..editor.max_point(cx)]
801 );
802 assert_eq!(
803 editor.scroll_position(cx),
804 gpui::Point::new(0., editor.max_point(cx).row().as_f32())
805 );
806
807 editor
808 })
809 });
810}
811
812#[gpui::test]
813fn test_cancel(cx: &mut TestAppContext) {
814 init_test(cx, |_| {});
815
816 let editor = cx.add_window(|window, cx| {
817 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
818 build_editor(buffer, window, cx)
819 });
820
821 _ = editor.update(cx, |editor, window, cx| {
822 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
823 editor.update_selection(
824 DisplayPoint::new(DisplayRow(1), 1),
825 0,
826 gpui::Point::<f32>::default(),
827 window,
828 cx,
829 );
830 editor.end_selection(window, cx);
831
832 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
833 editor.update_selection(
834 DisplayPoint::new(DisplayRow(0), 3),
835 0,
836 gpui::Point::<f32>::default(),
837 window,
838 cx,
839 );
840 editor.end_selection(window, cx);
841 assert_eq!(
842 editor.selections.display_ranges(cx),
843 [
844 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
845 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
846 ]
847 );
848 });
849
850 _ = editor.update(cx, |editor, window, cx| {
851 editor.cancel(&Cancel, window, cx);
852 assert_eq!(
853 editor.selections.display_ranges(cx),
854 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
855 );
856 });
857
858 _ = editor.update(cx, |editor, window, cx| {
859 editor.cancel(&Cancel, window, cx);
860 assert_eq!(
861 editor.selections.display_ranges(cx),
862 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
863 );
864 });
865}
866
867#[gpui::test]
868fn test_fold_action(cx: &mut TestAppContext) {
869 init_test(cx, |_| {});
870
871 let editor = cx.add_window(|window, cx| {
872 let buffer = MultiBuffer::build_simple(
873 &"
874 impl Foo {
875 // Hello!
876
877 fn a() {
878 1
879 }
880
881 fn b() {
882 2
883 }
884
885 fn c() {
886 3
887 }
888 }
889 "
890 .unindent(),
891 cx,
892 );
893 build_editor(buffer.clone(), window, cx)
894 });
895
896 _ = editor.update(cx, |editor, window, cx| {
897 editor.change_selections(None, window, cx, |s| {
898 s.select_display_ranges([
899 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
900 ]);
901 });
902 editor.fold(&Fold, window, cx);
903 assert_eq!(
904 editor.display_text(cx),
905 "
906 impl Foo {
907 // Hello!
908
909 fn a() {
910 1
911 }
912
913 fn b() {⋯
914 }
915
916 fn c() {⋯
917 }
918 }
919 "
920 .unindent(),
921 );
922
923 editor.fold(&Fold, window, cx);
924 assert_eq!(
925 editor.display_text(cx),
926 "
927 impl Foo {⋯
928 }
929 "
930 .unindent(),
931 );
932
933 editor.unfold_lines(&UnfoldLines, window, cx);
934 assert_eq!(
935 editor.display_text(cx),
936 "
937 impl Foo {
938 // Hello!
939
940 fn a() {
941 1
942 }
943
944 fn b() {⋯
945 }
946
947 fn c() {⋯
948 }
949 }
950 "
951 .unindent(),
952 );
953
954 editor.unfold_lines(&UnfoldLines, window, cx);
955 assert_eq!(
956 editor.display_text(cx),
957 editor.buffer.read(cx).read(cx).text()
958 );
959 });
960}
961
962#[gpui::test]
963fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
964 init_test(cx, |_| {});
965
966 let editor = cx.add_window(|window, cx| {
967 let buffer = MultiBuffer::build_simple(
968 &"
969 class Foo:
970 # Hello!
971
972 def a():
973 print(1)
974
975 def b():
976 print(2)
977
978 def c():
979 print(3)
980 "
981 .unindent(),
982 cx,
983 );
984 build_editor(buffer.clone(), window, cx)
985 });
986
987 _ = editor.update(cx, |editor, window, cx| {
988 editor.change_selections(None, window, cx, |s| {
989 s.select_display_ranges([
990 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
991 ]);
992 });
993 editor.fold(&Fold, window, cx);
994 assert_eq!(
995 editor.display_text(cx),
996 "
997 class Foo:
998 # Hello!
999
1000 def a():
1001 print(1)
1002
1003 def b():⋯
1004
1005 def c():⋯
1006 "
1007 .unindent(),
1008 );
1009
1010 editor.fold(&Fold, window, cx);
1011 assert_eq!(
1012 editor.display_text(cx),
1013 "
1014 class Foo:⋯
1015 "
1016 .unindent(),
1017 );
1018
1019 editor.unfold_lines(&UnfoldLines, window, cx);
1020 assert_eq!(
1021 editor.display_text(cx),
1022 "
1023 class Foo:
1024 # Hello!
1025
1026 def a():
1027 print(1)
1028
1029 def b():⋯
1030
1031 def c():⋯
1032 "
1033 .unindent(),
1034 );
1035
1036 editor.unfold_lines(&UnfoldLines, window, cx);
1037 assert_eq!(
1038 editor.display_text(cx),
1039 editor.buffer.read(cx).read(cx).text()
1040 );
1041 });
1042}
1043
1044#[gpui::test]
1045fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1046 init_test(cx, |_| {});
1047
1048 let editor = cx.add_window(|window, cx| {
1049 let buffer = MultiBuffer::build_simple(
1050 &"
1051 class Foo:
1052 # Hello!
1053
1054 def a():
1055 print(1)
1056
1057 def b():
1058 print(2)
1059
1060
1061 def c():
1062 print(3)
1063
1064
1065 "
1066 .unindent(),
1067 cx,
1068 );
1069 build_editor(buffer.clone(), window, cx)
1070 });
1071
1072 _ = editor.update(cx, |editor, window, cx| {
1073 editor.change_selections(None, window, cx, |s| {
1074 s.select_display_ranges([
1075 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1076 ]);
1077 });
1078 editor.fold(&Fold, window, cx);
1079 assert_eq!(
1080 editor.display_text(cx),
1081 "
1082 class Foo:
1083 # Hello!
1084
1085 def a():
1086 print(1)
1087
1088 def b():⋯
1089
1090
1091 def c():⋯
1092
1093
1094 "
1095 .unindent(),
1096 );
1097
1098 editor.fold(&Fold, window, cx);
1099 assert_eq!(
1100 editor.display_text(cx),
1101 "
1102 class Foo:⋯
1103
1104
1105 "
1106 .unindent(),
1107 );
1108
1109 editor.unfold_lines(&UnfoldLines, window, cx);
1110 assert_eq!(
1111 editor.display_text(cx),
1112 "
1113 class Foo:
1114 # Hello!
1115
1116 def a():
1117 print(1)
1118
1119 def b():⋯
1120
1121
1122 def c():⋯
1123
1124
1125 "
1126 .unindent(),
1127 );
1128
1129 editor.unfold_lines(&UnfoldLines, window, cx);
1130 assert_eq!(
1131 editor.display_text(cx),
1132 editor.buffer.read(cx).read(cx).text()
1133 );
1134 });
1135}
1136
1137#[gpui::test]
1138fn test_fold_at_level(cx: &mut TestAppContext) {
1139 init_test(cx, |_| {});
1140
1141 let editor = cx.add_window(|window, cx| {
1142 let buffer = MultiBuffer::build_simple(
1143 &"
1144 class Foo:
1145 # Hello!
1146
1147 def a():
1148 print(1)
1149
1150 def b():
1151 print(2)
1152
1153
1154 class Bar:
1155 # World!
1156
1157 def a():
1158 print(1)
1159
1160 def b():
1161 print(2)
1162
1163
1164 "
1165 .unindent(),
1166 cx,
1167 );
1168 build_editor(buffer.clone(), window, cx)
1169 });
1170
1171 _ = editor.update(cx, |editor, window, cx| {
1172 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1173 assert_eq!(
1174 editor.display_text(cx),
1175 "
1176 class Foo:
1177 # Hello!
1178
1179 def a():⋯
1180
1181 def b():⋯
1182
1183
1184 class Bar:
1185 # World!
1186
1187 def a():⋯
1188
1189 def b():⋯
1190
1191
1192 "
1193 .unindent(),
1194 );
1195
1196 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1197 assert_eq!(
1198 editor.display_text(cx),
1199 "
1200 class Foo:⋯
1201
1202
1203 class Bar:⋯
1204
1205
1206 "
1207 .unindent(),
1208 );
1209
1210 editor.unfold_all(&UnfoldAll, window, cx);
1211 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1212 assert_eq!(
1213 editor.display_text(cx),
1214 "
1215 class Foo:
1216 # Hello!
1217
1218 def a():
1219 print(1)
1220
1221 def b():
1222 print(2)
1223
1224
1225 class Bar:
1226 # World!
1227
1228 def a():
1229 print(1)
1230
1231 def b():
1232 print(2)
1233
1234
1235 "
1236 .unindent(),
1237 );
1238
1239 assert_eq!(
1240 editor.display_text(cx),
1241 editor.buffer.read(cx).read(cx).text()
1242 );
1243 });
1244}
1245
1246#[gpui::test]
1247fn test_move_cursor(cx: &mut TestAppContext) {
1248 init_test(cx, |_| {});
1249
1250 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1251 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1252
1253 buffer.update(cx, |buffer, cx| {
1254 buffer.edit(
1255 vec![
1256 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1257 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1258 ],
1259 None,
1260 cx,
1261 );
1262 });
1263 _ = editor.update(cx, |editor, window, cx| {
1264 assert_eq!(
1265 editor.selections.display_ranges(cx),
1266 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1267 );
1268
1269 editor.move_down(&MoveDown, window, cx);
1270 assert_eq!(
1271 editor.selections.display_ranges(cx),
1272 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1273 );
1274
1275 editor.move_right(&MoveRight, window, cx);
1276 assert_eq!(
1277 editor.selections.display_ranges(cx),
1278 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1279 );
1280
1281 editor.move_left(&MoveLeft, window, cx);
1282 assert_eq!(
1283 editor.selections.display_ranges(cx),
1284 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1285 );
1286
1287 editor.move_up(&MoveUp, window, cx);
1288 assert_eq!(
1289 editor.selections.display_ranges(cx),
1290 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1291 );
1292
1293 editor.move_to_end(&MoveToEnd, window, cx);
1294 assert_eq!(
1295 editor.selections.display_ranges(cx),
1296 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1297 );
1298
1299 editor.move_to_beginning(&MoveToBeginning, window, cx);
1300 assert_eq!(
1301 editor.selections.display_ranges(cx),
1302 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1303 );
1304
1305 editor.change_selections(None, window, cx, |s| {
1306 s.select_display_ranges([
1307 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1308 ]);
1309 });
1310 editor.select_to_beginning(&SelectToBeginning, window, cx);
1311 assert_eq!(
1312 editor.selections.display_ranges(cx),
1313 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1314 );
1315
1316 editor.select_to_end(&SelectToEnd, window, cx);
1317 assert_eq!(
1318 editor.selections.display_ranges(cx),
1319 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1320 );
1321 });
1322}
1323
1324#[gpui::test]
1325fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1326 init_test(cx, |_| {});
1327
1328 let editor = cx.add_window(|window, cx| {
1329 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1330 build_editor(buffer.clone(), window, cx)
1331 });
1332
1333 assert_eq!('🟥'.len_utf8(), 4);
1334 assert_eq!('α'.len_utf8(), 2);
1335
1336 _ = editor.update(cx, |editor, window, cx| {
1337 editor.fold_creases(
1338 vec![
1339 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1340 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1341 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1342 ],
1343 true,
1344 window,
1345 cx,
1346 );
1347 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1348
1349 editor.move_right(&MoveRight, window, cx);
1350 assert_eq!(
1351 editor.selections.display_ranges(cx),
1352 &[empty_range(0, "🟥".len())]
1353 );
1354 editor.move_right(&MoveRight, window, cx);
1355 assert_eq!(
1356 editor.selections.display_ranges(cx),
1357 &[empty_range(0, "🟥🟧".len())]
1358 );
1359 editor.move_right(&MoveRight, window, cx);
1360 assert_eq!(
1361 editor.selections.display_ranges(cx),
1362 &[empty_range(0, "🟥🟧⋯".len())]
1363 );
1364
1365 editor.move_down(&MoveDown, window, cx);
1366 assert_eq!(
1367 editor.selections.display_ranges(cx),
1368 &[empty_range(1, "ab⋯e".len())]
1369 );
1370 editor.move_left(&MoveLeft, window, cx);
1371 assert_eq!(
1372 editor.selections.display_ranges(cx),
1373 &[empty_range(1, "ab⋯".len())]
1374 );
1375 editor.move_left(&MoveLeft, window, cx);
1376 assert_eq!(
1377 editor.selections.display_ranges(cx),
1378 &[empty_range(1, "ab".len())]
1379 );
1380 editor.move_left(&MoveLeft, window, cx);
1381 assert_eq!(
1382 editor.selections.display_ranges(cx),
1383 &[empty_range(1, "a".len())]
1384 );
1385
1386 editor.move_down(&MoveDown, window, cx);
1387 assert_eq!(
1388 editor.selections.display_ranges(cx),
1389 &[empty_range(2, "α".len())]
1390 );
1391 editor.move_right(&MoveRight, window, cx);
1392 assert_eq!(
1393 editor.selections.display_ranges(cx),
1394 &[empty_range(2, "αβ".len())]
1395 );
1396 editor.move_right(&MoveRight, window, cx);
1397 assert_eq!(
1398 editor.selections.display_ranges(cx),
1399 &[empty_range(2, "αβ⋯".len())]
1400 );
1401 editor.move_right(&MoveRight, window, cx);
1402 assert_eq!(
1403 editor.selections.display_ranges(cx),
1404 &[empty_range(2, "αβ⋯ε".len())]
1405 );
1406
1407 editor.move_up(&MoveUp, window, cx);
1408 assert_eq!(
1409 editor.selections.display_ranges(cx),
1410 &[empty_range(1, "ab⋯e".len())]
1411 );
1412 editor.move_down(&MoveDown, window, cx);
1413 assert_eq!(
1414 editor.selections.display_ranges(cx),
1415 &[empty_range(2, "αβ⋯ε".len())]
1416 );
1417 editor.move_up(&MoveUp, window, cx);
1418 assert_eq!(
1419 editor.selections.display_ranges(cx),
1420 &[empty_range(1, "ab⋯e".len())]
1421 );
1422
1423 editor.move_up(&MoveUp, window, cx);
1424 assert_eq!(
1425 editor.selections.display_ranges(cx),
1426 &[empty_range(0, "🟥🟧".len())]
1427 );
1428 editor.move_left(&MoveLeft, window, cx);
1429 assert_eq!(
1430 editor.selections.display_ranges(cx),
1431 &[empty_range(0, "🟥".len())]
1432 );
1433 editor.move_left(&MoveLeft, window, cx);
1434 assert_eq!(
1435 editor.selections.display_ranges(cx),
1436 &[empty_range(0, "".len())]
1437 );
1438 });
1439}
1440
1441#[gpui::test]
1442fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1443 init_test(cx, |_| {});
1444
1445 let editor = cx.add_window(|window, cx| {
1446 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1447 build_editor(buffer.clone(), window, cx)
1448 });
1449 _ = editor.update(cx, |editor, window, cx| {
1450 editor.change_selections(None, window, cx, |s| {
1451 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1452 });
1453
1454 // moving above start of document should move selection to start of document,
1455 // but the next move down should still be at the original goal_x
1456 editor.move_up(&MoveUp, window, cx);
1457 assert_eq!(
1458 editor.selections.display_ranges(cx),
1459 &[empty_range(0, "".len())]
1460 );
1461
1462 editor.move_down(&MoveDown, window, cx);
1463 assert_eq!(
1464 editor.selections.display_ranges(cx),
1465 &[empty_range(1, "abcd".len())]
1466 );
1467
1468 editor.move_down(&MoveDown, window, cx);
1469 assert_eq!(
1470 editor.selections.display_ranges(cx),
1471 &[empty_range(2, "αβγ".len())]
1472 );
1473
1474 editor.move_down(&MoveDown, window, cx);
1475 assert_eq!(
1476 editor.selections.display_ranges(cx),
1477 &[empty_range(3, "abcd".len())]
1478 );
1479
1480 editor.move_down(&MoveDown, window, cx);
1481 assert_eq!(
1482 editor.selections.display_ranges(cx),
1483 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1484 );
1485
1486 // moving past end of document should not change goal_x
1487 editor.move_down(&MoveDown, window, cx);
1488 assert_eq!(
1489 editor.selections.display_ranges(cx),
1490 &[empty_range(5, "".len())]
1491 );
1492
1493 editor.move_down(&MoveDown, window, cx);
1494 assert_eq!(
1495 editor.selections.display_ranges(cx),
1496 &[empty_range(5, "".len())]
1497 );
1498
1499 editor.move_up(&MoveUp, window, cx);
1500 assert_eq!(
1501 editor.selections.display_ranges(cx),
1502 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1503 );
1504
1505 editor.move_up(&MoveUp, window, cx);
1506 assert_eq!(
1507 editor.selections.display_ranges(cx),
1508 &[empty_range(3, "abcd".len())]
1509 );
1510
1511 editor.move_up(&MoveUp, window, cx);
1512 assert_eq!(
1513 editor.selections.display_ranges(cx),
1514 &[empty_range(2, "αβγ".len())]
1515 );
1516 });
1517}
1518
1519#[gpui::test]
1520fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1521 init_test(cx, |_| {});
1522 let move_to_beg = MoveToBeginningOfLine {
1523 stop_at_soft_wraps: true,
1524 stop_at_indent: true,
1525 };
1526
1527 let delete_to_beg = DeleteToBeginningOfLine {
1528 stop_at_indent: false,
1529 };
1530
1531 let move_to_end = MoveToEndOfLine {
1532 stop_at_soft_wraps: true,
1533 };
1534
1535 let editor = cx.add_window(|window, cx| {
1536 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1537 build_editor(buffer, window, cx)
1538 });
1539 _ = editor.update(cx, |editor, window, cx| {
1540 editor.change_selections(None, window, cx, |s| {
1541 s.select_display_ranges([
1542 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1543 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1544 ]);
1545 });
1546 });
1547
1548 _ = editor.update(cx, |editor, window, cx| {
1549 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1550 assert_eq!(
1551 editor.selections.display_ranges(cx),
1552 &[
1553 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1554 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1555 ]
1556 );
1557 });
1558
1559 _ = editor.update(cx, |editor, window, cx| {
1560 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1561 assert_eq!(
1562 editor.selections.display_ranges(cx),
1563 &[
1564 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1565 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1566 ]
1567 );
1568 });
1569
1570 _ = editor.update(cx, |editor, window, cx| {
1571 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1572 assert_eq!(
1573 editor.selections.display_ranges(cx),
1574 &[
1575 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1576 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1577 ]
1578 );
1579 });
1580
1581 _ = editor.update(cx, |editor, window, cx| {
1582 editor.move_to_end_of_line(&move_to_end, window, cx);
1583 assert_eq!(
1584 editor.selections.display_ranges(cx),
1585 &[
1586 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1587 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1588 ]
1589 );
1590 });
1591
1592 // Moving to the end of line again is a no-op.
1593 _ = editor.update(cx, |editor, window, cx| {
1594 editor.move_to_end_of_line(&move_to_end, window, cx);
1595 assert_eq!(
1596 editor.selections.display_ranges(cx),
1597 &[
1598 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1599 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1600 ]
1601 );
1602 });
1603
1604 _ = editor.update(cx, |editor, window, cx| {
1605 editor.move_left(&MoveLeft, window, cx);
1606 editor.select_to_beginning_of_line(
1607 &SelectToBeginningOfLine {
1608 stop_at_soft_wraps: true,
1609 stop_at_indent: true,
1610 },
1611 window,
1612 cx,
1613 );
1614 assert_eq!(
1615 editor.selections.display_ranges(cx),
1616 &[
1617 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1618 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1619 ]
1620 );
1621 });
1622
1623 _ = editor.update(cx, |editor, window, cx| {
1624 editor.select_to_beginning_of_line(
1625 &SelectToBeginningOfLine {
1626 stop_at_soft_wraps: true,
1627 stop_at_indent: true,
1628 },
1629 window,
1630 cx,
1631 );
1632 assert_eq!(
1633 editor.selections.display_ranges(cx),
1634 &[
1635 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1636 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1637 ]
1638 );
1639 });
1640
1641 _ = editor.update(cx, |editor, window, cx| {
1642 editor.select_to_beginning_of_line(
1643 &SelectToBeginningOfLine {
1644 stop_at_soft_wraps: true,
1645 stop_at_indent: true,
1646 },
1647 window,
1648 cx,
1649 );
1650 assert_eq!(
1651 editor.selections.display_ranges(cx),
1652 &[
1653 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1654 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1655 ]
1656 );
1657 });
1658
1659 _ = editor.update(cx, |editor, window, cx| {
1660 editor.select_to_end_of_line(
1661 &SelectToEndOfLine {
1662 stop_at_soft_wraps: true,
1663 },
1664 window,
1665 cx,
1666 );
1667 assert_eq!(
1668 editor.selections.display_ranges(cx),
1669 &[
1670 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1671 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1672 ]
1673 );
1674 });
1675
1676 _ = editor.update(cx, |editor, window, cx| {
1677 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1678 assert_eq!(editor.display_text(cx), "ab\n de");
1679 assert_eq!(
1680 editor.selections.display_ranges(cx),
1681 &[
1682 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1683 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1684 ]
1685 );
1686 });
1687
1688 _ = editor.update(cx, |editor, window, cx| {
1689 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1690 assert_eq!(editor.display_text(cx), "\n");
1691 assert_eq!(
1692 editor.selections.display_ranges(cx),
1693 &[
1694 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1695 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1696 ]
1697 );
1698 });
1699}
1700
1701#[gpui::test]
1702fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1703 init_test(cx, |_| {});
1704 let move_to_beg = MoveToBeginningOfLine {
1705 stop_at_soft_wraps: false,
1706 stop_at_indent: false,
1707 };
1708
1709 let move_to_end = MoveToEndOfLine {
1710 stop_at_soft_wraps: false,
1711 };
1712
1713 let editor = cx.add_window(|window, cx| {
1714 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1715 build_editor(buffer, window, cx)
1716 });
1717
1718 _ = editor.update(cx, |editor, window, cx| {
1719 editor.set_wrap_width(Some(140.0.into()), cx);
1720
1721 // We expect the following lines after wrapping
1722 // ```
1723 // thequickbrownfox
1724 // jumpedoverthelazydo
1725 // gs
1726 // ```
1727 // The final `gs` was soft-wrapped onto a new line.
1728 assert_eq!(
1729 "thequickbrownfox\njumpedoverthelaz\nydogs",
1730 editor.display_text(cx),
1731 );
1732
1733 // First, let's assert behavior on the first line, that was not soft-wrapped.
1734 // Start the cursor at the `k` on the first line
1735 editor.change_selections(None, window, cx, |s| {
1736 s.select_display_ranges([
1737 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1738 ]);
1739 });
1740
1741 // Moving to the beginning of the line should put us at the beginning of the line.
1742 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1743 assert_eq!(
1744 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1745 editor.selections.display_ranges(cx)
1746 );
1747
1748 // Moving to the end of the line should put us at the end of the line.
1749 editor.move_to_end_of_line(&move_to_end, window, cx);
1750 assert_eq!(
1751 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1752 editor.selections.display_ranges(cx)
1753 );
1754
1755 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1756 // Start the cursor at the last line (`y` that was wrapped to a new line)
1757 editor.change_selections(None, window, cx, |s| {
1758 s.select_display_ranges([
1759 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1760 ]);
1761 });
1762
1763 // Moving to the beginning of the line should put us at the start of the second line of
1764 // display text, i.e., the `j`.
1765 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1766 assert_eq!(
1767 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1768 editor.selections.display_ranges(cx)
1769 );
1770
1771 // Moving to the beginning of the line again should be a no-op.
1772 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1773 assert_eq!(
1774 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1775 editor.selections.display_ranges(cx)
1776 );
1777
1778 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1779 // next display line.
1780 editor.move_to_end_of_line(&move_to_end, window, cx);
1781 assert_eq!(
1782 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1783 editor.selections.display_ranges(cx)
1784 );
1785
1786 // Moving to the end of the line again should be a no-op.
1787 editor.move_to_end_of_line(&move_to_end, window, cx);
1788 assert_eq!(
1789 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1790 editor.selections.display_ranges(cx)
1791 );
1792 });
1793}
1794
1795#[gpui::test]
1796fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1797 init_test(cx, |_| {});
1798
1799 let move_to_beg = MoveToBeginningOfLine {
1800 stop_at_soft_wraps: true,
1801 stop_at_indent: true,
1802 };
1803
1804 let select_to_beg = SelectToBeginningOfLine {
1805 stop_at_soft_wraps: true,
1806 stop_at_indent: true,
1807 };
1808
1809 let delete_to_beg = DeleteToBeginningOfLine {
1810 stop_at_indent: true,
1811 };
1812
1813 let move_to_end = MoveToEndOfLine {
1814 stop_at_soft_wraps: false,
1815 };
1816
1817 let editor = cx.add_window(|window, cx| {
1818 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1819 build_editor(buffer, window, cx)
1820 });
1821
1822 _ = editor.update(cx, |editor, window, cx| {
1823 editor.change_selections(None, window, cx, |s| {
1824 s.select_display_ranges([
1825 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1826 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1827 ]);
1828 });
1829
1830 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1831 // and the second cursor at the first non-whitespace character in the line.
1832 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1833 assert_eq!(
1834 editor.selections.display_ranges(cx),
1835 &[
1836 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1837 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1838 ]
1839 );
1840
1841 // Moving to the beginning of the line again should be a no-op for the first cursor,
1842 // and should move the second cursor to the beginning of the line.
1843 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1844 assert_eq!(
1845 editor.selections.display_ranges(cx),
1846 &[
1847 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1848 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1849 ]
1850 );
1851
1852 // Moving to the beginning of the line again should still be a no-op for the first cursor,
1853 // and should move the second cursor back to the first non-whitespace character in the line.
1854 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1855 assert_eq!(
1856 editor.selections.display_ranges(cx),
1857 &[
1858 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1859 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1860 ]
1861 );
1862
1863 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
1864 // and to the first non-whitespace character in the line for the second cursor.
1865 editor.move_to_end_of_line(&move_to_end, window, cx);
1866 editor.move_left(&MoveLeft, window, cx);
1867 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1868 assert_eq!(
1869 editor.selections.display_ranges(cx),
1870 &[
1871 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1872 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1873 ]
1874 );
1875
1876 // Selecting to the beginning of the line again should be a no-op for the first cursor,
1877 // and should select to the beginning of the line for the second cursor.
1878 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1879 assert_eq!(
1880 editor.selections.display_ranges(cx),
1881 &[
1882 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1883 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1884 ]
1885 );
1886
1887 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
1888 // and should delete to the first non-whitespace character in the line for the second cursor.
1889 editor.move_to_end_of_line(&move_to_end, window, cx);
1890 editor.move_left(&MoveLeft, window, cx);
1891 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1892 assert_eq!(editor.text(cx), "c\n f");
1893 });
1894}
1895
1896#[gpui::test]
1897fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
1898 init_test(cx, |_| {});
1899
1900 let editor = cx.add_window(|window, cx| {
1901 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
1902 build_editor(buffer, window, cx)
1903 });
1904 _ = editor.update(cx, |editor, window, cx| {
1905 editor.change_selections(None, window, cx, |s| {
1906 s.select_display_ranges([
1907 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
1908 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
1909 ])
1910 });
1911 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1912 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1913
1914 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1915 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
1916
1917 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1918 assert_selection_ranges("use ˇstd::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1919
1920 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1921 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1922
1923 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1924 assert_selection_ranges("ˇuse std::str::{foo, ˇbar}\n\n {baz.qux()}", editor, cx);
1925
1926 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1927 assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1928
1929 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1930 assert_selection_ranges("use stdˇ::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
1931
1932 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1933 assert_selection_ranges("use std::ˇstr::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1934
1935 editor.move_right(&MoveRight, window, cx);
1936 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1937 assert_selection_ranges(
1938 "use std::«ˇs»tr::{foo, bar}\n«ˇ\n» {baz.qux()}",
1939 editor,
1940 cx,
1941 );
1942
1943 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1944 assert_selection_ranges(
1945 "use std«ˇ::s»tr::{foo, bar«ˇ}\n\n» {baz.qux()}",
1946 editor,
1947 cx,
1948 );
1949
1950 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
1951 assert_selection_ranges(
1952 "use std::«ˇs»tr::{foo, bar}«ˇ\n\n» {baz.qux()}",
1953 editor,
1954 cx,
1955 );
1956 });
1957}
1958
1959#[gpui::test]
1960fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
1961 init_test(cx, |_| {});
1962
1963 let editor = cx.add_window(|window, cx| {
1964 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
1965 build_editor(buffer, window, cx)
1966 });
1967
1968 _ = editor.update(cx, |editor, window, cx| {
1969 editor.set_wrap_width(Some(140.0.into()), cx);
1970 assert_eq!(
1971 editor.display_text(cx),
1972 "use one::{\n two::three::\n four::five\n};"
1973 );
1974
1975 editor.change_selections(None, window, cx, |s| {
1976 s.select_display_ranges([
1977 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
1978 ]);
1979 });
1980
1981 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1982 assert_eq!(
1983 editor.selections.display_ranges(cx),
1984 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
1985 );
1986
1987 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1988 assert_eq!(
1989 editor.selections.display_ranges(cx),
1990 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
1991 );
1992
1993 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1994 assert_eq!(
1995 editor.selections.display_ranges(cx),
1996 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
1997 );
1998
1999 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2000 assert_eq!(
2001 editor.selections.display_ranges(cx),
2002 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2003 );
2004
2005 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2006 assert_eq!(
2007 editor.selections.display_ranges(cx),
2008 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2009 );
2010
2011 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2012 assert_eq!(
2013 editor.selections.display_ranges(cx),
2014 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2015 );
2016 });
2017}
2018
2019#[gpui::test]
2020async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2021 init_test(cx, |_| {});
2022 let mut cx = EditorTestContext::new(cx).await;
2023
2024 let line_height = cx.editor(|editor, window, _| {
2025 editor
2026 .style()
2027 .unwrap()
2028 .text
2029 .line_height_in_pixels(window.rem_size())
2030 });
2031 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2032
2033 cx.set_state(
2034 &r#"ˇone
2035 two
2036
2037 three
2038 fourˇ
2039 five
2040
2041 six"#
2042 .unindent(),
2043 );
2044
2045 cx.update_editor(|editor, window, cx| {
2046 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2047 });
2048 cx.assert_editor_state(
2049 &r#"one
2050 two
2051 ˇ
2052 three
2053 four
2054 five
2055 ˇ
2056 six"#
2057 .unindent(),
2058 );
2059
2060 cx.update_editor(|editor, window, cx| {
2061 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2062 });
2063 cx.assert_editor_state(
2064 &r#"one
2065 two
2066
2067 three
2068 four
2069 five
2070 ˇ
2071 sixˇ"#
2072 .unindent(),
2073 );
2074
2075 cx.update_editor(|editor, window, cx| {
2076 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2077 });
2078 cx.assert_editor_state(
2079 &r#"one
2080 two
2081
2082 three
2083 four
2084 five
2085
2086 sixˇ"#
2087 .unindent(),
2088 );
2089
2090 cx.update_editor(|editor, window, cx| {
2091 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2092 });
2093 cx.assert_editor_state(
2094 &r#"one
2095 two
2096
2097 three
2098 four
2099 five
2100 ˇ
2101 six"#
2102 .unindent(),
2103 );
2104
2105 cx.update_editor(|editor, window, cx| {
2106 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2107 });
2108 cx.assert_editor_state(
2109 &r#"one
2110 two
2111 ˇ
2112 three
2113 four
2114 five
2115
2116 six"#
2117 .unindent(),
2118 );
2119
2120 cx.update_editor(|editor, window, cx| {
2121 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2122 });
2123 cx.assert_editor_state(
2124 &r#"ˇone
2125 two
2126
2127 three
2128 four
2129 five
2130
2131 six"#
2132 .unindent(),
2133 );
2134}
2135
2136#[gpui::test]
2137async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2138 init_test(cx, |_| {});
2139 let mut cx = EditorTestContext::new(cx).await;
2140 let line_height = cx.editor(|editor, window, _| {
2141 editor
2142 .style()
2143 .unwrap()
2144 .text
2145 .line_height_in_pixels(window.rem_size())
2146 });
2147 let window = cx.window;
2148 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2149
2150 cx.set_state(
2151 r#"ˇone
2152 two
2153 three
2154 four
2155 five
2156 six
2157 seven
2158 eight
2159 nine
2160 ten
2161 "#,
2162 );
2163
2164 cx.update_editor(|editor, window, cx| {
2165 assert_eq!(
2166 editor.snapshot(window, cx).scroll_position(),
2167 gpui::Point::new(0., 0.)
2168 );
2169 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2170 assert_eq!(
2171 editor.snapshot(window, cx).scroll_position(),
2172 gpui::Point::new(0., 3.)
2173 );
2174 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2175 assert_eq!(
2176 editor.snapshot(window, cx).scroll_position(),
2177 gpui::Point::new(0., 6.)
2178 );
2179 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2180 assert_eq!(
2181 editor.snapshot(window, cx).scroll_position(),
2182 gpui::Point::new(0., 3.)
2183 );
2184
2185 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2186 assert_eq!(
2187 editor.snapshot(window, cx).scroll_position(),
2188 gpui::Point::new(0., 1.)
2189 );
2190 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2191 assert_eq!(
2192 editor.snapshot(window, cx).scroll_position(),
2193 gpui::Point::new(0., 3.)
2194 );
2195 });
2196}
2197
2198#[gpui::test]
2199async fn test_autoscroll(cx: &mut TestAppContext) {
2200 init_test(cx, |_| {});
2201 let mut cx = EditorTestContext::new(cx).await;
2202
2203 let line_height = cx.update_editor(|editor, window, cx| {
2204 editor.set_vertical_scroll_margin(2, cx);
2205 editor
2206 .style()
2207 .unwrap()
2208 .text
2209 .line_height_in_pixels(window.rem_size())
2210 });
2211 let window = cx.window;
2212 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2213
2214 cx.set_state(
2215 r#"ˇone
2216 two
2217 three
2218 four
2219 five
2220 six
2221 seven
2222 eight
2223 nine
2224 ten
2225 "#,
2226 );
2227 cx.update_editor(|editor, window, cx| {
2228 assert_eq!(
2229 editor.snapshot(window, cx).scroll_position(),
2230 gpui::Point::new(0., 0.0)
2231 );
2232 });
2233
2234 // Add a cursor below the visible area. Since both cursors cannot fit
2235 // on screen, the editor autoscrolls to reveal the newest cursor, and
2236 // allows the vertical scroll margin below that cursor.
2237 cx.update_editor(|editor, window, cx| {
2238 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2239 selections.select_ranges([
2240 Point::new(0, 0)..Point::new(0, 0),
2241 Point::new(6, 0)..Point::new(6, 0),
2242 ]);
2243 })
2244 });
2245 cx.update_editor(|editor, window, cx| {
2246 assert_eq!(
2247 editor.snapshot(window, cx).scroll_position(),
2248 gpui::Point::new(0., 3.0)
2249 );
2250 });
2251
2252 // Move down. The editor cursor scrolls down to track the newest cursor.
2253 cx.update_editor(|editor, window, cx| {
2254 editor.move_down(&Default::default(), window, cx);
2255 });
2256 cx.update_editor(|editor, window, cx| {
2257 assert_eq!(
2258 editor.snapshot(window, cx).scroll_position(),
2259 gpui::Point::new(0., 4.0)
2260 );
2261 });
2262
2263 // Add a cursor above the visible area. Since both cursors fit on screen,
2264 // the editor scrolls to show both.
2265 cx.update_editor(|editor, window, cx| {
2266 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2267 selections.select_ranges([
2268 Point::new(1, 0)..Point::new(1, 0),
2269 Point::new(6, 0)..Point::new(6, 0),
2270 ]);
2271 })
2272 });
2273 cx.update_editor(|editor, window, cx| {
2274 assert_eq!(
2275 editor.snapshot(window, cx).scroll_position(),
2276 gpui::Point::new(0., 1.0)
2277 );
2278 });
2279}
2280
2281#[gpui::test]
2282async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2283 init_test(cx, |_| {});
2284 let mut cx = EditorTestContext::new(cx).await;
2285
2286 let line_height = cx.editor(|editor, window, _cx| {
2287 editor
2288 .style()
2289 .unwrap()
2290 .text
2291 .line_height_in_pixels(window.rem_size())
2292 });
2293 let window = cx.window;
2294 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2295 cx.set_state(
2296 &r#"
2297 ˇone
2298 two
2299 threeˇ
2300 four
2301 five
2302 six
2303 seven
2304 eight
2305 nine
2306 ten
2307 "#
2308 .unindent(),
2309 );
2310
2311 cx.update_editor(|editor, window, cx| {
2312 editor.move_page_down(&MovePageDown::default(), window, cx)
2313 });
2314 cx.assert_editor_state(
2315 &r#"
2316 one
2317 two
2318 three
2319 ˇfour
2320 five
2321 sixˇ
2322 seven
2323 eight
2324 nine
2325 ten
2326 "#
2327 .unindent(),
2328 );
2329
2330 cx.update_editor(|editor, window, cx| {
2331 editor.move_page_down(&MovePageDown::default(), window, cx)
2332 });
2333 cx.assert_editor_state(
2334 &r#"
2335 one
2336 two
2337 three
2338 four
2339 five
2340 six
2341 ˇseven
2342 eight
2343 nineˇ
2344 ten
2345 "#
2346 .unindent(),
2347 );
2348
2349 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2350 cx.assert_editor_state(
2351 &r#"
2352 one
2353 two
2354 three
2355 ˇfour
2356 five
2357 sixˇ
2358 seven
2359 eight
2360 nine
2361 ten
2362 "#
2363 .unindent(),
2364 );
2365
2366 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2367 cx.assert_editor_state(
2368 &r#"
2369 ˇone
2370 two
2371 threeˇ
2372 four
2373 five
2374 six
2375 seven
2376 eight
2377 nine
2378 ten
2379 "#
2380 .unindent(),
2381 );
2382
2383 // Test select collapsing
2384 cx.update_editor(|editor, window, cx| {
2385 editor.move_page_down(&MovePageDown::default(), window, cx);
2386 editor.move_page_down(&MovePageDown::default(), window, cx);
2387 editor.move_page_down(&MovePageDown::default(), window, cx);
2388 });
2389 cx.assert_editor_state(
2390 &r#"
2391 one
2392 two
2393 three
2394 four
2395 five
2396 six
2397 seven
2398 eight
2399 nine
2400 ˇten
2401 ˇ"#
2402 .unindent(),
2403 );
2404}
2405
2406#[gpui::test]
2407async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2408 init_test(cx, |_| {});
2409 let mut cx = EditorTestContext::new(cx).await;
2410 cx.set_state("one «two threeˇ» four");
2411 cx.update_editor(|editor, window, cx| {
2412 editor.delete_to_beginning_of_line(
2413 &DeleteToBeginningOfLine {
2414 stop_at_indent: false,
2415 },
2416 window,
2417 cx,
2418 );
2419 assert_eq!(editor.text(cx), " four");
2420 });
2421}
2422
2423#[gpui::test]
2424fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2425 init_test(cx, |_| {});
2426
2427 let editor = cx.add_window(|window, cx| {
2428 let buffer = MultiBuffer::build_simple("one two three four", cx);
2429 build_editor(buffer.clone(), window, cx)
2430 });
2431
2432 _ = editor.update(cx, |editor, window, cx| {
2433 editor.change_selections(None, window, cx, |s| {
2434 s.select_display_ranges([
2435 // an empty selection - the preceding word fragment is deleted
2436 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2437 // characters selected - they are deleted
2438 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 12),
2439 ])
2440 });
2441 editor.delete_to_previous_word_start(
2442 &DeleteToPreviousWordStart {
2443 ignore_newlines: false,
2444 },
2445 window,
2446 cx,
2447 );
2448 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e two te four");
2449 });
2450
2451 _ = editor.update(cx, |editor, window, cx| {
2452 editor.change_selections(None, window, cx, |s| {
2453 s.select_display_ranges([
2454 // an empty selection - the following word fragment is deleted
2455 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
2456 // characters selected - they are deleted
2457 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 10),
2458 ])
2459 });
2460 editor.delete_to_next_word_end(
2461 &DeleteToNextWordEnd {
2462 ignore_newlines: false,
2463 },
2464 window,
2465 cx,
2466 );
2467 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e t te our");
2468 });
2469}
2470
2471#[gpui::test]
2472fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2473 init_test(cx, |_| {});
2474
2475 let editor = cx.add_window(|window, cx| {
2476 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2477 build_editor(buffer.clone(), window, cx)
2478 });
2479 let del_to_prev_word_start = DeleteToPreviousWordStart {
2480 ignore_newlines: false,
2481 };
2482 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2483 ignore_newlines: true,
2484 };
2485
2486 _ = editor.update(cx, |editor, window, cx| {
2487 editor.change_selections(None, window, cx, |s| {
2488 s.select_display_ranges([
2489 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2490 ])
2491 });
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\n");
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\nthree");
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\n");
2498 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2499 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
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(), "one\n");
2502 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2503 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2504 });
2505}
2506
2507#[gpui::test]
2508fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
2509 init_test(cx, |_| {});
2510
2511 let editor = cx.add_window(|window, cx| {
2512 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
2513 build_editor(buffer.clone(), window, cx)
2514 });
2515 let del_to_next_word_end = DeleteToNextWordEnd {
2516 ignore_newlines: false,
2517 };
2518 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
2519 ignore_newlines: true,
2520 };
2521
2522 _ = editor.update(cx, |editor, window, cx| {
2523 editor.change_selections(None, window, cx, |s| {
2524 s.select_display_ranges([
2525 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2526 ])
2527 });
2528 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2529 assert_eq!(
2530 editor.buffer.read(cx).read(cx).text(),
2531 "one\n two\nthree\n four"
2532 );
2533 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2534 assert_eq!(
2535 editor.buffer.read(cx).read(cx).text(),
2536 "\n two\nthree\n four"
2537 );
2538 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2539 assert_eq!(
2540 editor.buffer.read(cx).read(cx).text(),
2541 "two\nthree\n four"
2542 );
2543 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2544 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\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(), "\n four");
2547 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2548 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2549 });
2550}
2551
2552#[gpui::test]
2553fn test_newline(cx: &mut TestAppContext) {
2554 init_test(cx, |_| {});
2555
2556 let editor = cx.add_window(|window, cx| {
2557 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
2558 build_editor(buffer.clone(), window, cx)
2559 });
2560
2561 _ = editor.update(cx, |editor, window, cx| {
2562 editor.change_selections(None, window, cx, |s| {
2563 s.select_display_ranges([
2564 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2565 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2566 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
2567 ])
2568 });
2569
2570 editor.newline(&Newline, window, cx);
2571 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
2572 });
2573}
2574
2575#[gpui::test]
2576fn test_newline_with_old_selections(cx: &mut TestAppContext) {
2577 init_test(cx, |_| {});
2578
2579 let editor = cx.add_window(|window, cx| {
2580 let buffer = MultiBuffer::build_simple(
2581 "
2582 a
2583 b(
2584 X
2585 )
2586 c(
2587 X
2588 )
2589 "
2590 .unindent()
2591 .as_str(),
2592 cx,
2593 );
2594 let mut editor = build_editor(buffer.clone(), window, cx);
2595 editor.change_selections(None, window, cx, |s| {
2596 s.select_ranges([
2597 Point::new(2, 4)..Point::new(2, 5),
2598 Point::new(5, 4)..Point::new(5, 5),
2599 ])
2600 });
2601 editor
2602 });
2603
2604 _ = editor.update(cx, |editor, window, cx| {
2605 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2606 editor.buffer.update(cx, |buffer, cx| {
2607 buffer.edit(
2608 [
2609 (Point::new(1, 2)..Point::new(3, 0), ""),
2610 (Point::new(4, 2)..Point::new(6, 0), ""),
2611 ],
2612 None,
2613 cx,
2614 );
2615 assert_eq!(
2616 buffer.read(cx).text(),
2617 "
2618 a
2619 b()
2620 c()
2621 "
2622 .unindent()
2623 );
2624 });
2625 assert_eq!(
2626 editor.selections.ranges(cx),
2627 &[
2628 Point::new(1, 2)..Point::new(1, 2),
2629 Point::new(2, 2)..Point::new(2, 2),
2630 ],
2631 );
2632
2633 editor.newline(&Newline, window, cx);
2634 assert_eq!(
2635 editor.text(cx),
2636 "
2637 a
2638 b(
2639 )
2640 c(
2641 )
2642 "
2643 .unindent()
2644 );
2645
2646 // The selections are moved after the inserted newlines
2647 assert_eq!(
2648 editor.selections.ranges(cx),
2649 &[
2650 Point::new(2, 0)..Point::new(2, 0),
2651 Point::new(4, 0)..Point::new(4, 0),
2652 ],
2653 );
2654 });
2655}
2656
2657#[gpui::test]
2658async fn test_newline_above(cx: &mut TestAppContext) {
2659 init_test(cx, |settings| {
2660 settings.defaults.tab_size = NonZeroU32::new(4)
2661 });
2662
2663 let language = Arc::new(
2664 Language::new(
2665 LanguageConfig::default(),
2666 Some(tree_sitter_rust::LANGUAGE.into()),
2667 )
2668 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2669 .unwrap(),
2670 );
2671
2672 let mut cx = EditorTestContext::new(cx).await;
2673 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2674 cx.set_state(indoc! {"
2675 const a: ˇA = (
2676 (ˇ
2677 «const_functionˇ»(ˇ),
2678 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2679 )ˇ
2680 ˇ);ˇ
2681 "});
2682
2683 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
2684 cx.assert_editor_state(indoc! {"
2685 ˇ
2686 const a: A = (
2687 ˇ
2688 (
2689 ˇ
2690 ˇ
2691 const_function(),
2692 ˇ
2693 ˇ
2694 ˇ
2695 ˇ
2696 something_else,
2697 ˇ
2698 )
2699 ˇ
2700 ˇ
2701 );
2702 "});
2703}
2704
2705#[gpui::test]
2706async fn test_newline_below(cx: &mut TestAppContext) {
2707 init_test(cx, |settings| {
2708 settings.defaults.tab_size = NonZeroU32::new(4)
2709 });
2710
2711 let language = Arc::new(
2712 Language::new(
2713 LanguageConfig::default(),
2714 Some(tree_sitter_rust::LANGUAGE.into()),
2715 )
2716 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2717 .unwrap(),
2718 );
2719
2720 let mut cx = EditorTestContext::new(cx).await;
2721 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2722 cx.set_state(indoc! {"
2723 const a: ˇA = (
2724 (ˇ
2725 «const_functionˇ»(ˇ),
2726 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2727 )ˇ
2728 ˇ);ˇ
2729 "});
2730
2731 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
2732 cx.assert_editor_state(indoc! {"
2733 const a: A = (
2734 ˇ
2735 (
2736 ˇ
2737 const_function(),
2738 ˇ
2739 ˇ
2740 something_else,
2741 ˇ
2742 ˇ
2743 ˇ
2744 ˇ
2745 )
2746 ˇ
2747 );
2748 ˇ
2749 ˇ
2750 "});
2751}
2752
2753#[gpui::test]
2754async fn test_newline_comments(cx: &mut TestAppContext) {
2755 init_test(cx, |settings| {
2756 settings.defaults.tab_size = NonZeroU32::new(4)
2757 });
2758
2759 let language = Arc::new(Language::new(
2760 LanguageConfig {
2761 line_comments: vec!["// ".into()],
2762 ..LanguageConfig::default()
2763 },
2764 None,
2765 ));
2766 {
2767 let mut cx = EditorTestContext::new(cx).await;
2768 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2769 cx.set_state(indoc! {"
2770 // Fooˇ
2771 "});
2772
2773 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2774 cx.assert_editor_state(indoc! {"
2775 // Foo
2776 // ˇ
2777 "});
2778 // Ensure that we add comment prefix when existing line contains space
2779 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2780 cx.assert_editor_state(
2781 indoc! {"
2782 // Foo
2783 //s
2784 // ˇ
2785 "}
2786 .replace("s", " ") // s is used as space placeholder to prevent format on save
2787 .as_str(),
2788 );
2789 // Ensure that we add comment prefix when existing line does not contain space
2790 cx.set_state(indoc! {"
2791 // Foo
2792 //ˇ
2793 "});
2794 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2795 cx.assert_editor_state(indoc! {"
2796 // Foo
2797 //
2798 // ˇ
2799 "});
2800 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
2801 cx.set_state(indoc! {"
2802 ˇ// Foo
2803 "});
2804 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2805 cx.assert_editor_state(indoc! {"
2806
2807 ˇ// Foo
2808 "});
2809 }
2810 // Ensure that comment continuations can be disabled.
2811 update_test_language_settings(cx, |settings| {
2812 settings.defaults.extend_comment_on_newline = Some(false);
2813 });
2814 let mut cx = EditorTestContext::new(cx).await;
2815 cx.set_state(indoc! {"
2816 // Fooˇ
2817 "});
2818 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2819 cx.assert_editor_state(indoc! {"
2820 // Foo
2821 ˇ
2822 "});
2823}
2824
2825#[gpui::test]
2826async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
2827 init_test(cx, |settings| {
2828 settings.defaults.tab_size = NonZeroU32::new(4)
2829 });
2830
2831 let language = Arc::new(Language::new(
2832 LanguageConfig {
2833 line_comments: vec!["// ".into(), "/// ".into()],
2834 ..LanguageConfig::default()
2835 },
2836 None,
2837 ));
2838 {
2839 let mut cx = EditorTestContext::new(cx).await;
2840 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2841 cx.set_state(indoc! {"
2842 //ˇ
2843 "});
2844 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2845 cx.assert_editor_state(indoc! {"
2846 //
2847 // ˇ
2848 "});
2849
2850 cx.set_state(indoc! {"
2851 ///ˇ
2852 "});
2853 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2854 cx.assert_editor_state(indoc! {"
2855 ///
2856 /// ˇ
2857 "});
2858 }
2859}
2860
2861#[gpui::test]
2862async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
2863 init_test(cx, |settings| {
2864 settings.defaults.tab_size = NonZeroU32::new(4)
2865 });
2866
2867 let language = Arc::new(
2868 Language::new(
2869 LanguageConfig {
2870 documentation: Some(language::DocumentationConfig {
2871 start: "/**".into(),
2872 end: "*/".into(),
2873 prefix: "* ".into(),
2874 tab_size: NonZeroU32::new(1).unwrap(),
2875 }),
2876
2877 ..LanguageConfig::default()
2878 },
2879 Some(tree_sitter_rust::LANGUAGE.into()),
2880 )
2881 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
2882 .unwrap(),
2883 );
2884
2885 {
2886 let mut cx = EditorTestContext::new(cx).await;
2887 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2888 cx.set_state(indoc! {"
2889 /**ˇ
2890 "});
2891
2892 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2893 cx.assert_editor_state(indoc! {"
2894 /**
2895 * ˇ
2896 "});
2897 // Ensure that if cursor is before the comment start,
2898 // we do not actually insert a comment prefix.
2899 cx.set_state(indoc! {"
2900 ˇ/**
2901 "});
2902 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2903 cx.assert_editor_state(indoc! {"
2904
2905 ˇ/**
2906 "});
2907 // Ensure that if cursor is between it doesn't add comment prefix.
2908 cx.set_state(indoc! {"
2909 /*ˇ*
2910 "});
2911 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2912 cx.assert_editor_state(indoc! {"
2913 /*
2914 ˇ*
2915 "});
2916 // Ensure that if suffix exists on same line after cursor it adds new line.
2917 cx.set_state(indoc! {"
2918 /**ˇ*/
2919 "});
2920 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2921 cx.assert_editor_state(indoc! {"
2922 /**
2923 * ˇ
2924 */
2925 "});
2926 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2927 cx.set_state(indoc! {"
2928 /**ˇ */
2929 "});
2930 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2931 cx.assert_editor_state(indoc! {"
2932 /**
2933 * ˇ
2934 */
2935 "});
2936 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2937 cx.set_state(indoc! {"
2938 /** ˇ*/
2939 "});
2940 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2941 cx.assert_editor_state(
2942 indoc! {"
2943 /**s
2944 * ˇ
2945 */
2946 "}
2947 .replace("s", " ") // s is used as space placeholder to prevent format on save
2948 .as_str(),
2949 );
2950 // Ensure that delimiter space is preserved when newline on already
2951 // spaced delimiter.
2952 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2953 cx.assert_editor_state(
2954 indoc! {"
2955 /**s
2956 *s
2957 * ˇ
2958 */
2959 "}
2960 .replace("s", " ") // s is used as space placeholder to prevent format on save
2961 .as_str(),
2962 );
2963 // Ensure that delimiter space is preserved when space is not
2964 // on existing delimiter.
2965 cx.set_state(indoc! {"
2966 /**
2967 *ˇ
2968 */
2969 "});
2970 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2971 cx.assert_editor_state(indoc! {"
2972 /**
2973 *
2974 * ˇ
2975 */
2976 "});
2977 // Ensure that if suffix exists on same line after cursor it
2978 // doesn't add extra new line if prefix is not on same line.
2979 cx.set_state(indoc! {"
2980 /**
2981 ˇ*/
2982 "});
2983 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2984 cx.assert_editor_state(indoc! {"
2985 /**
2986
2987 ˇ*/
2988 "});
2989 // Ensure that it detects suffix after existing prefix.
2990 cx.set_state(indoc! {"
2991 /**ˇ/
2992 "});
2993 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2994 cx.assert_editor_state(indoc! {"
2995 /**
2996 ˇ/
2997 "});
2998 // Ensure that if suffix exists on same line before
2999 // cursor it does not add comment prefix.
3000 cx.set_state(indoc! {"
3001 /** */ˇ
3002 "});
3003 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3004 cx.assert_editor_state(indoc! {"
3005 /** */
3006 ˇ
3007 "});
3008 // Ensure that if suffix exists on same line before
3009 // cursor it does not add comment prefix.
3010 cx.set_state(indoc! {"
3011 /**
3012 *
3013 */ˇ
3014 "});
3015 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3016 cx.assert_editor_state(indoc! {"
3017 /**
3018 *
3019 */
3020 ˇ
3021 "});
3022
3023 // Ensure that inline comment followed by code
3024 // doesn't add comment prefix on newline
3025 cx.set_state(indoc! {"
3026 /** */ textˇ
3027 "});
3028 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3029 cx.assert_editor_state(indoc! {"
3030 /** */ text
3031 ˇ
3032 "});
3033
3034 // Ensure that text after comment end tag
3035 // doesn't add comment prefix on newline
3036 cx.set_state(indoc! {"
3037 /**
3038 *
3039 */ˇtext
3040 "});
3041 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3042 cx.assert_editor_state(indoc! {"
3043 /**
3044 *
3045 */
3046 ˇtext
3047 "});
3048
3049 // Ensure if not comment block it doesn't
3050 // add comment prefix on newline
3051 cx.set_state(indoc! {"
3052 * textˇ
3053 "});
3054 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3055 cx.assert_editor_state(indoc! {"
3056 * text
3057 ˇ
3058 "});
3059 }
3060 // Ensure that comment continuations can be disabled.
3061 update_test_language_settings(cx, |settings| {
3062 settings.defaults.extend_comment_on_newline = Some(false);
3063 });
3064 let mut cx = EditorTestContext::new(cx).await;
3065 cx.set_state(indoc! {"
3066 /**ˇ
3067 "});
3068 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3069 cx.assert_editor_state(indoc! {"
3070 /**
3071 ˇ
3072 "});
3073}
3074
3075#[gpui::test]
3076fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3077 init_test(cx, |_| {});
3078
3079 let editor = cx.add_window(|window, cx| {
3080 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3081 let mut editor = build_editor(buffer.clone(), window, cx);
3082 editor.change_selections(None, window, cx, |s| {
3083 s.select_ranges([3..4, 11..12, 19..20])
3084 });
3085 editor
3086 });
3087
3088 _ = editor.update(cx, |editor, window, cx| {
3089 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3090 editor.buffer.update(cx, |buffer, cx| {
3091 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
3092 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3093 });
3094 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
3095
3096 editor.insert("Z", window, cx);
3097 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3098
3099 // The selections are moved after the inserted characters
3100 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
3101 });
3102}
3103
3104#[gpui::test]
3105async fn test_tab(cx: &mut TestAppContext) {
3106 init_test(cx, |settings| {
3107 settings.defaults.tab_size = NonZeroU32::new(3)
3108 });
3109
3110 let mut cx = EditorTestContext::new(cx).await;
3111 cx.set_state(indoc! {"
3112 ˇabˇc
3113 ˇ🏀ˇ🏀ˇefg
3114 dˇ
3115 "});
3116 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3117 cx.assert_editor_state(indoc! {"
3118 ˇab ˇc
3119 ˇ🏀 ˇ🏀 ˇefg
3120 d ˇ
3121 "});
3122
3123 cx.set_state(indoc! {"
3124 a
3125 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3126 "});
3127 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3128 cx.assert_editor_state(indoc! {"
3129 a
3130 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3131 "});
3132}
3133
3134#[gpui::test]
3135async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3136 init_test(cx, |_| {});
3137
3138 let mut cx = EditorTestContext::new(cx).await;
3139 let language = Arc::new(
3140 Language::new(
3141 LanguageConfig::default(),
3142 Some(tree_sitter_rust::LANGUAGE.into()),
3143 )
3144 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3145 .unwrap(),
3146 );
3147 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3148
3149 // test when all cursors are not at suggested indent
3150 // then simply move to their suggested indent location
3151 cx.set_state(indoc! {"
3152 const a: B = (
3153 c(
3154 ˇ
3155 ˇ )
3156 );
3157 "});
3158 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3159 cx.assert_editor_state(indoc! {"
3160 const a: B = (
3161 c(
3162 ˇ
3163 ˇ)
3164 );
3165 "});
3166
3167 // test cursor already at suggested indent not moving when
3168 // other cursors are yet to reach their suggested indents
3169 cx.set_state(indoc! {"
3170 ˇ
3171 const a: B = (
3172 c(
3173 d(
3174 ˇ
3175 )
3176 ˇ
3177 ˇ )
3178 );
3179 "});
3180 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3181 cx.assert_editor_state(indoc! {"
3182 ˇ
3183 const a: B = (
3184 c(
3185 d(
3186 ˇ
3187 )
3188 ˇ
3189 ˇ)
3190 );
3191 "});
3192 // test when all cursors are at suggested indent then tab is inserted
3193 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3194 cx.assert_editor_state(indoc! {"
3195 ˇ
3196 const a: B = (
3197 c(
3198 d(
3199 ˇ
3200 )
3201 ˇ
3202 ˇ)
3203 );
3204 "});
3205
3206 // test when current indent is less than suggested indent,
3207 // we adjust line to match suggested indent and move cursor to it
3208 //
3209 // when no other cursor is at word boundary, all of them should move
3210 cx.set_state(indoc! {"
3211 const a: B = (
3212 c(
3213 d(
3214 ˇ
3215 ˇ )
3216 ˇ )
3217 );
3218 "});
3219 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3220 cx.assert_editor_state(indoc! {"
3221 const a: B = (
3222 c(
3223 d(
3224 ˇ
3225 ˇ)
3226 ˇ)
3227 );
3228 "});
3229
3230 // test when current indent is less than suggested indent,
3231 // we adjust line to match suggested indent and move cursor to it
3232 //
3233 // when some other cursor is at word boundary, it should not move
3234 cx.set_state(indoc! {"
3235 const a: B = (
3236 c(
3237 d(
3238 ˇ
3239 ˇ )
3240 ˇ)
3241 );
3242 "});
3243 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3244 cx.assert_editor_state(indoc! {"
3245 const a: B = (
3246 c(
3247 d(
3248 ˇ
3249 ˇ)
3250 ˇ)
3251 );
3252 "});
3253
3254 // test when current indent is more than suggested indent,
3255 // we just move cursor to current indent instead of suggested indent
3256 //
3257 // when no other cursor is at word boundary, all of them should move
3258 cx.set_state(indoc! {"
3259 const a: B = (
3260 c(
3261 d(
3262 ˇ
3263 ˇ )
3264 ˇ )
3265 );
3266 "});
3267 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3268 cx.assert_editor_state(indoc! {"
3269 const a: B = (
3270 c(
3271 d(
3272 ˇ
3273 ˇ)
3274 ˇ)
3275 );
3276 "});
3277 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3278 cx.assert_editor_state(indoc! {"
3279 const a: B = (
3280 c(
3281 d(
3282 ˇ
3283 ˇ)
3284 ˇ)
3285 );
3286 "});
3287
3288 // test when current indent is more than suggested indent,
3289 // we just move cursor to current indent instead of suggested indent
3290 //
3291 // when some other cursor is at word boundary, it doesn't move
3292 cx.set_state(indoc! {"
3293 const a: B = (
3294 c(
3295 d(
3296 ˇ
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 d(
3306 ˇ
3307 ˇ)
3308 ˇ)
3309 );
3310 "});
3311
3312 // handle auto-indent when there are multiple cursors on the same line
3313 cx.set_state(indoc! {"
3314 const a: B = (
3315 c(
3316 ˇ ˇ
3317 ˇ )
3318 );
3319 "});
3320 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3321 cx.assert_editor_state(indoc! {"
3322 const a: B = (
3323 c(
3324 ˇ
3325 ˇ)
3326 );
3327 "});
3328}
3329
3330#[gpui::test]
3331async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3332 init_test(cx, |settings| {
3333 settings.defaults.tab_size = NonZeroU32::new(3)
3334 });
3335
3336 let mut cx = EditorTestContext::new(cx).await;
3337 cx.set_state(indoc! {"
3338 ˇ
3339 \t ˇ
3340 \t ˇ
3341 \t ˇ
3342 \t \t\t \t \t\t \t\t \t \t ˇ
3343 "});
3344
3345 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3346 cx.assert_editor_state(indoc! {"
3347 ˇ
3348 \t ˇ
3349 \t ˇ
3350 \t ˇ
3351 \t \t\t \t \t\t \t\t \t \t ˇ
3352 "});
3353}
3354
3355#[gpui::test]
3356async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3357 init_test(cx, |settings| {
3358 settings.defaults.tab_size = NonZeroU32::new(4)
3359 });
3360
3361 let language = Arc::new(
3362 Language::new(
3363 LanguageConfig::default(),
3364 Some(tree_sitter_rust::LANGUAGE.into()),
3365 )
3366 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3367 .unwrap(),
3368 );
3369
3370 let mut cx = EditorTestContext::new(cx).await;
3371 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3372 cx.set_state(indoc! {"
3373 fn a() {
3374 if b {
3375 \t ˇc
3376 }
3377 }
3378 "});
3379
3380 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3381 cx.assert_editor_state(indoc! {"
3382 fn a() {
3383 if b {
3384 ˇc
3385 }
3386 }
3387 "});
3388}
3389
3390#[gpui::test]
3391async fn test_indent_outdent(cx: &mut TestAppContext) {
3392 init_test(cx, |settings| {
3393 settings.defaults.tab_size = NonZeroU32::new(4);
3394 });
3395
3396 let mut cx = EditorTestContext::new(cx).await;
3397
3398 cx.set_state(indoc! {"
3399 «oneˇ» «twoˇ»
3400 three
3401 four
3402 "});
3403 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3404 cx.assert_editor_state(indoc! {"
3405 «oneˇ» «twoˇ»
3406 three
3407 four
3408 "});
3409
3410 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3411 cx.assert_editor_state(indoc! {"
3412 «oneˇ» «twoˇ»
3413 three
3414 four
3415 "});
3416
3417 // select across line ending
3418 cx.set_state(indoc! {"
3419 one two
3420 t«hree
3421 ˇ» four
3422 "});
3423 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3424 cx.assert_editor_state(indoc! {"
3425 one two
3426 t«hree
3427 ˇ» four
3428 "});
3429
3430 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3431 cx.assert_editor_state(indoc! {"
3432 one two
3433 t«hree
3434 ˇ» four
3435 "});
3436
3437 // Ensure that indenting/outdenting works when the cursor is at column 0.
3438 cx.set_state(indoc! {"
3439 one two
3440 ˇthree
3441 four
3442 "});
3443 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3444 cx.assert_editor_state(indoc! {"
3445 one two
3446 ˇthree
3447 four
3448 "});
3449
3450 cx.set_state(indoc! {"
3451 one two
3452 ˇ three
3453 four
3454 "});
3455 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3456 cx.assert_editor_state(indoc! {"
3457 one two
3458 ˇthree
3459 four
3460 "});
3461}
3462
3463#[gpui::test]
3464async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
3465 init_test(cx, |settings| {
3466 settings.defaults.hard_tabs = Some(true);
3467 });
3468
3469 let mut cx = EditorTestContext::new(cx).await;
3470
3471 // select two ranges on one line
3472 cx.set_state(indoc! {"
3473 «oneˇ» «twoˇ»
3474 three
3475 four
3476 "});
3477 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3478 cx.assert_editor_state(indoc! {"
3479 \t«oneˇ» «twoˇ»
3480 three
3481 four
3482 "});
3483 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3484 cx.assert_editor_state(indoc! {"
3485 \t\t«oneˇ» «twoˇ»
3486 three
3487 four
3488 "});
3489 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3490 cx.assert_editor_state(indoc! {"
3491 \t«oneˇ» «twoˇ»
3492 three
3493 four
3494 "});
3495 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3496 cx.assert_editor_state(indoc! {"
3497 «oneˇ» «twoˇ»
3498 three
3499 four
3500 "});
3501
3502 // select across a line ending
3503 cx.set_state(indoc! {"
3504 one two
3505 t«hree
3506 ˇ»four
3507 "});
3508 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3509 cx.assert_editor_state(indoc! {"
3510 one two
3511 \tt«hree
3512 ˇ»four
3513 "});
3514 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3515 cx.assert_editor_state(indoc! {"
3516 one two
3517 \t\tt«hree
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 \tt«hree
3524 ˇ»four
3525 "});
3526 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3527 cx.assert_editor_state(indoc! {"
3528 one two
3529 t«hree
3530 ˇ»four
3531 "});
3532
3533 // Ensure that indenting/outdenting works when the cursor is at column 0.
3534 cx.set_state(indoc! {"
3535 one two
3536 ˇthree
3537 four
3538 "});
3539 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3540 cx.assert_editor_state(indoc! {"
3541 one two
3542 ˇthree
3543 four
3544 "});
3545 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3546 cx.assert_editor_state(indoc! {"
3547 one two
3548 \tˇthree
3549 four
3550 "});
3551 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3552 cx.assert_editor_state(indoc! {"
3553 one two
3554 ˇthree
3555 four
3556 "});
3557}
3558
3559#[gpui::test]
3560fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
3561 init_test(cx, |settings| {
3562 settings.languages.extend([
3563 (
3564 "TOML".into(),
3565 LanguageSettingsContent {
3566 tab_size: NonZeroU32::new(2),
3567 ..Default::default()
3568 },
3569 ),
3570 (
3571 "Rust".into(),
3572 LanguageSettingsContent {
3573 tab_size: NonZeroU32::new(4),
3574 ..Default::default()
3575 },
3576 ),
3577 ]);
3578 });
3579
3580 let toml_language = Arc::new(Language::new(
3581 LanguageConfig {
3582 name: "TOML".into(),
3583 ..Default::default()
3584 },
3585 None,
3586 ));
3587 let rust_language = Arc::new(Language::new(
3588 LanguageConfig {
3589 name: "Rust".into(),
3590 ..Default::default()
3591 },
3592 None,
3593 ));
3594
3595 let toml_buffer =
3596 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
3597 let rust_buffer =
3598 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
3599 let multibuffer = cx.new(|cx| {
3600 let mut multibuffer = MultiBuffer::new(ReadWrite);
3601 multibuffer.push_excerpts(
3602 toml_buffer.clone(),
3603 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
3604 cx,
3605 );
3606 multibuffer.push_excerpts(
3607 rust_buffer.clone(),
3608 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
3609 cx,
3610 );
3611 multibuffer
3612 });
3613
3614 cx.add_window(|window, cx| {
3615 let mut editor = build_editor(multibuffer, window, cx);
3616
3617 assert_eq!(
3618 editor.text(cx),
3619 indoc! {"
3620 a = 1
3621 b = 2
3622
3623 const c: usize = 3;
3624 "}
3625 );
3626
3627 select_ranges(
3628 &mut editor,
3629 indoc! {"
3630 «aˇ» = 1
3631 b = 2
3632
3633 «const c:ˇ» usize = 3;
3634 "},
3635 window,
3636 cx,
3637 );
3638
3639 editor.tab(&Tab, window, cx);
3640 assert_text_with_selections(
3641 &mut editor,
3642 indoc! {"
3643 «aˇ» = 1
3644 b = 2
3645
3646 «const c:ˇ» usize = 3;
3647 "},
3648 cx,
3649 );
3650 editor.backtab(&Backtab, window, cx);
3651 assert_text_with_selections(
3652 &mut editor,
3653 indoc! {"
3654 «aˇ» = 1
3655 b = 2
3656
3657 «const c:ˇ» usize = 3;
3658 "},
3659 cx,
3660 );
3661
3662 editor
3663 });
3664}
3665
3666#[gpui::test]
3667async fn test_backspace(cx: &mut TestAppContext) {
3668 init_test(cx, |_| {});
3669
3670 let mut cx = EditorTestContext::new(cx).await;
3671
3672 // Basic backspace
3673 cx.set_state(indoc! {"
3674 onˇe two three
3675 fou«rˇ» five six
3676 seven «ˇeight nine
3677 »ten
3678 "});
3679 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3680 cx.assert_editor_state(indoc! {"
3681 oˇe two three
3682 fouˇ five six
3683 seven ˇten
3684 "});
3685
3686 // Test backspace inside and around indents
3687 cx.set_state(indoc! {"
3688 zero
3689 ˇone
3690 ˇtwo
3691 ˇ ˇ ˇ three
3692 ˇ ˇ four
3693 "});
3694 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3695 cx.assert_editor_state(indoc! {"
3696 zero
3697 ˇone
3698 ˇtwo
3699 ˇ threeˇ four
3700 "});
3701}
3702
3703#[gpui::test]
3704async fn test_delete(cx: &mut TestAppContext) {
3705 init_test(cx, |_| {});
3706
3707 let mut cx = EditorTestContext::new(cx).await;
3708 cx.set_state(indoc! {"
3709 onˇe two three
3710 fou«rˇ» five six
3711 seven «ˇeight nine
3712 »ten
3713 "});
3714 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
3715 cx.assert_editor_state(indoc! {"
3716 onˇ two three
3717 fouˇ five six
3718 seven ˇten
3719 "});
3720}
3721
3722#[gpui::test]
3723fn test_delete_line(cx: &mut TestAppContext) {
3724 init_test(cx, |_| {});
3725
3726 let editor = cx.add_window(|window, cx| {
3727 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3728 build_editor(buffer, window, cx)
3729 });
3730 _ = editor.update(cx, |editor, window, cx| {
3731 editor.change_selections(None, window, cx, |s| {
3732 s.select_display_ranges([
3733 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
3734 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3735 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3736 ])
3737 });
3738 editor.delete_line(&DeleteLine, window, cx);
3739 assert_eq!(editor.display_text(cx), "ghi");
3740 assert_eq!(
3741 editor.selections.display_ranges(cx),
3742 vec![
3743 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
3744 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
3745 ]
3746 );
3747 });
3748
3749 let editor = cx.add_window(|window, cx| {
3750 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3751 build_editor(buffer, window, cx)
3752 });
3753 _ = editor.update(cx, |editor, window, cx| {
3754 editor.change_selections(None, window, cx, |s| {
3755 s.select_display_ranges([
3756 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
3757 ])
3758 });
3759 editor.delete_line(&DeleteLine, window, cx);
3760 assert_eq!(editor.display_text(cx), "ghi\n");
3761 assert_eq!(
3762 editor.selections.display_ranges(cx),
3763 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
3764 );
3765 });
3766}
3767
3768#[gpui::test]
3769fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
3770 init_test(cx, |_| {});
3771
3772 cx.add_window(|window, cx| {
3773 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3774 let mut editor = build_editor(buffer.clone(), window, cx);
3775 let buffer = buffer.read(cx).as_singleton().unwrap();
3776
3777 assert_eq!(
3778 editor.selections.ranges::<Point>(cx),
3779 &[Point::new(0, 0)..Point::new(0, 0)]
3780 );
3781
3782 // When on single line, replace newline at end by space
3783 editor.join_lines(&JoinLines, 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, 3)..Point::new(0, 3)]
3788 );
3789
3790 // When multiple lines are selected, remove newlines that are spanned by the selection
3791 editor.change_selections(None, window, cx, |s| {
3792 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
3793 });
3794 editor.join_lines(&JoinLines, window, cx);
3795 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
3796 assert_eq!(
3797 editor.selections.ranges::<Point>(cx),
3798 &[Point::new(0, 11)..Point::new(0, 11)]
3799 );
3800
3801 // Undo should be transactional
3802 editor.undo(&Undo, window, cx);
3803 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3804 assert_eq!(
3805 editor.selections.ranges::<Point>(cx),
3806 &[Point::new(0, 5)..Point::new(2, 2)]
3807 );
3808
3809 // When joining an empty line don't insert a space
3810 editor.change_selections(None, window, cx, |s| {
3811 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
3812 });
3813 editor.join_lines(&JoinLines, window, cx);
3814 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
3815 assert_eq!(
3816 editor.selections.ranges::<Point>(cx),
3817 [Point::new(2, 3)..Point::new(2, 3)]
3818 );
3819
3820 // We can remove trailing newlines
3821 editor.join_lines(&JoinLines, window, cx);
3822 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3823 assert_eq!(
3824 editor.selections.ranges::<Point>(cx),
3825 [Point::new(2, 3)..Point::new(2, 3)]
3826 );
3827
3828 // We don't blow up on the last line
3829 editor.join_lines(&JoinLines, window, cx);
3830 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3831 assert_eq!(
3832 editor.selections.ranges::<Point>(cx),
3833 [Point::new(2, 3)..Point::new(2, 3)]
3834 );
3835
3836 // reset to test indentation
3837 editor.buffer.update(cx, |buffer, cx| {
3838 buffer.edit(
3839 [
3840 (Point::new(1, 0)..Point::new(1, 2), " "),
3841 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
3842 ],
3843 None,
3844 cx,
3845 )
3846 });
3847
3848 // We remove any leading spaces
3849 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
3850 editor.change_selections(None, window, cx, |s| {
3851 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
3852 });
3853 editor.join_lines(&JoinLines, window, cx);
3854 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
3855
3856 // We don't insert a space for a line containing only spaces
3857 editor.join_lines(&JoinLines, window, cx);
3858 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
3859
3860 // We ignore any leading tabs
3861 editor.join_lines(&JoinLines, window, cx);
3862 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
3863
3864 editor
3865 });
3866}
3867
3868#[gpui::test]
3869fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
3870 init_test(cx, |_| {});
3871
3872 cx.add_window(|window, cx| {
3873 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3874 let mut editor = build_editor(buffer.clone(), window, cx);
3875 let buffer = buffer.read(cx).as_singleton().unwrap();
3876
3877 editor.change_selections(None, window, cx, |s| {
3878 s.select_ranges([
3879 Point::new(0, 2)..Point::new(1, 1),
3880 Point::new(1, 2)..Point::new(1, 2),
3881 Point::new(3, 1)..Point::new(3, 2),
3882 ])
3883 });
3884
3885 editor.join_lines(&JoinLines, window, cx);
3886 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
3887
3888 assert_eq!(
3889 editor.selections.ranges::<Point>(cx),
3890 [
3891 Point::new(0, 7)..Point::new(0, 7),
3892 Point::new(1, 3)..Point::new(1, 3)
3893 ]
3894 );
3895 editor
3896 });
3897}
3898
3899#[gpui::test]
3900async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
3901 init_test(cx, |_| {});
3902
3903 let mut cx = EditorTestContext::new(cx).await;
3904
3905 let diff_base = r#"
3906 Line 0
3907 Line 1
3908 Line 2
3909 Line 3
3910 "#
3911 .unindent();
3912
3913 cx.set_state(
3914 &r#"
3915 ˇLine 0
3916 Line 1
3917 Line 2
3918 Line 3
3919 "#
3920 .unindent(),
3921 );
3922
3923 cx.set_head_text(&diff_base);
3924 executor.run_until_parked();
3925
3926 // Join lines
3927 cx.update_editor(|editor, window, cx| {
3928 editor.join_lines(&JoinLines, window, cx);
3929 });
3930 executor.run_until_parked();
3931
3932 cx.assert_editor_state(
3933 &r#"
3934 Line 0ˇ Line 1
3935 Line 2
3936 Line 3
3937 "#
3938 .unindent(),
3939 );
3940 // Join again
3941 cx.update_editor(|editor, window, cx| {
3942 editor.join_lines(&JoinLines, window, cx);
3943 });
3944 executor.run_until_parked();
3945
3946 cx.assert_editor_state(
3947 &r#"
3948 Line 0 Line 1ˇ Line 2
3949 Line 3
3950 "#
3951 .unindent(),
3952 );
3953}
3954
3955#[gpui::test]
3956async fn test_custom_newlines_cause_no_false_positive_diffs(
3957 executor: BackgroundExecutor,
3958 cx: &mut TestAppContext,
3959) {
3960 init_test(cx, |_| {});
3961 let mut cx = EditorTestContext::new(cx).await;
3962 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
3963 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
3964 executor.run_until_parked();
3965
3966 cx.update_editor(|editor, window, cx| {
3967 let snapshot = editor.snapshot(window, cx);
3968 assert_eq!(
3969 snapshot
3970 .buffer_snapshot
3971 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
3972 .collect::<Vec<_>>(),
3973 Vec::new(),
3974 "Should not have any diffs for files with custom newlines"
3975 );
3976 });
3977}
3978
3979#[gpui::test]
3980async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppContext) {
3981 init_test(cx, |_| {});
3982
3983 let mut cx = EditorTestContext::new(cx).await;
3984
3985 // Test sort_lines_case_insensitive()
3986 cx.set_state(indoc! {"
3987 «z
3988 y
3989 x
3990 Z
3991 Y
3992 Xˇ»
3993 "});
3994 cx.update_editor(|e, window, cx| {
3995 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
3996 });
3997 cx.assert_editor_state(indoc! {"
3998 «x
3999 X
4000 y
4001 Y
4002 z
4003 Zˇ»
4004 "});
4005
4006 // Test reverse_lines()
4007 cx.set_state(indoc! {"
4008 «5
4009 4
4010 3
4011 2
4012 1ˇ»
4013 "});
4014 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4015 cx.assert_editor_state(indoc! {"
4016 «1
4017 2
4018 3
4019 4
4020 5ˇ»
4021 "});
4022
4023 // Skip testing shuffle_line()
4024
4025 // From here on out, test more complex cases of manipulate_immutable_lines() with a single driver method: sort_lines_case_sensitive()
4026 // Since all methods calling manipulate_immutable_lines() are doing the exact same general thing (reordering lines)
4027
4028 // Don't manipulate when cursor is on single line, but expand the selection
4029 cx.set_state(indoc! {"
4030 ddˇdd
4031 ccc
4032 bb
4033 a
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 «ddddˇ»
4040 ccc
4041 bb
4042 a
4043 "});
4044
4045 // Basic manipulate case
4046 // Start selection moves to column 0
4047 // End of selection shrinks to fit shorter line
4048 cx.set_state(indoc! {"
4049 dd«d
4050 ccc
4051 bb
4052 aaaaaˇ»
4053 "});
4054 cx.update_editor(|e, window, cx| {
4055 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4056 });
4057 cx.assert_editor_state(indoc! {"
4058 «aaaaa
4059 bb
4060 ccc
4061 dddˇ»
4062 "});
4063
4064 // Manipulate case with newlines
4065 cx.set_state(indoc! {"
4066 dd«d
4067 ccc
4068
4069 bb
4070 aaaaa
4071
4072 ˇ»
4073 "});
4074 cx.update_editor(|e, window, cx| {
4075 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4076 });
4077 cx.assert_editor_state(indoc! {"
4078 «
4079
4080 aaaaa
4081 bb
4082 ccc
4083 dddˇ»
4084
4085 "});
4086
4087 // Adding new line
4088 cx.set_state(indoc! {"
4089 aa«a
4090 bbˇ»b
4091 "});
4092 cx.update_editor(|e, window, cx| {
4093 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added_line"))
4094 });
4095 cx.assert_editor_state(indoc! {"
4096 «aaa
4097 bbb
4098 added_lineˇ»
4099 "});
4100
4101 // Removing line
4102 cx.set_state(indoc! {"
4103 aa«a
4104 bbbˇ»
4105 "});
4106 cx.update_editor(|e, window, cx| {
4107 e.manipulate_immutable_lines(window, cx, |lines| {
4108 lines.pop();
4109 })
4110 });
4111 cx.assert_editor_state(indoc! {"
4112 «aaaˇ»
4113 "});
4114
4115 // Removing all lines
4116 cx.set_state(indoc! {"
4117 aa«a
4118 bbbˇ»
4119 "});
4120 cx.update_editor(|e, window, cx| {
4121 e.manipulate_immutable_lines(window, cx, |lines| {
4122 lines.drain(..);
4123 })
4124 });
4125 cx.assert_editor_state(indoc! {"
4126 ˇ
4127 "});
4128}
4129
4130#[gpui::test]
4131async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4132 init_test(cx, |_| {});
4133
4134 let mut cx = EditorTestContext::new(cx).await;
4135
4136 // Consider continuous selection as single selection
4137 cx.set_state(indoc! {"
4138 Aaa«aa
4139 cˇ»c«c
4140 bb
4141 aaaˇ»aa
4142 "});
4143 cx.update_editor(|e, window, cx| {
4144 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4145 });
4146 cx.assert_editor_state(indoc! {"
4147 «Aaaaa
4148 ccc
4149 bb
4150 aaaaaˇ»
4151 "});
4152
4153 cx.set_state(indoc! {"
4154 Aaa«aa
4155 cˇ»c«c
4156 bb
4157 aaaˇ»aa
4158 "});
4159 cx.update_editor(|e, window, cx| {
4160 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4161 });
4162 cx.assert_editor_state(indoc! {"
4163 «Aaaaa
4164 ccc
4165 bbˇ»
4166 "});
4167
4168 // Consider non continuous selection as distinct dedup operations
4169 cx.set_state(indoc! {"
4170 «aaaaa
4171 bb
4172 aaaaa
4173 aaaaaˇ»
4174
4175 aaa«aaˇ»
4176 "});
4177 cx.update_editor(|e, window, cx| {
4178 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4179 });
4180 cx.assert_editor_state(indoc! {"
4181 «aaaaa
4182 bbˇ»
4183
4184 «aaaaaˇ»
4185 "});
4186}
4187
4188#[gpui::test]
4189async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4190 init_test(cx, |_| {});
4191
4192 let mut cx = EditorTestContext::new(cx).await;
4193
4194 cx.set_state(indoc! {"
4195 «Aaa
4196 aAa
4197 Aaaˇ»
4198 "});
4199 cx.update_editor(|e, window, cx| {
4200 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4201 });
4202 cx.assert_editor_state(indoc! {"
4203 «Aaa
4204 aAaˇ»
4205 "});
4206
4207 cx.set_state(indoc! {"
4208 «Aaa
4209 aAa
4210 aaAˇ»
4211 "});
4212 cx.update_editor(|e, window, cx| {
4213 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4214 });
4215 cx.assert_editor_state(indoc! {"
4216 «Aaaˇ»
4217 "});
4218}
4219
4220#[gpui::test]
4221async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
4222 init_test(cx, |_| {});
4223
4224 let mut cx = EditorTestContext::new(cx).await;
4225
4226 // Manipulate with multiple selections on a single line
4227 cx.set_state(indoc! {"
4228 dd«dd
4229 cˇ»c«c
4230 bb
4231 aaaˇ»aa
4232 "});
4233 cx.update_editor(|e, window, cx| {
4234 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4235 });
4236 cx.assert_editor_state(indoc! {"
4237 «aaaaa
4238 bb
4239 ccc
4240 ddddˇ»
4241 "});
4242
4243 // Manipulate with multiple disjoin selections
4244 cx.set_state(indoc! {"
4245 5«
4246 4
4247 3
4248 2
4249 1ˇ»
4250
4251 dd«dd
4252 ccc
4253 bb
4254 aaaˇ»aa
4255 "});
4256 cx.update_editor(|e, window, cx| {
4257 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4258 });
4259 cx.assert_editor_state(indoc! {"
4260 «1
4261 2
4262 3
4263 4
4264 5ˇ»
4265
4266 «aaaaa
4267 bb
4268 ccc
4269 ddddˇ»
4270 "});
4271
4272 // Adding lines on each selection
4273 cx.set_state(indoc! {"
4274 2«
4275 1ˇ»
4276
4277 bb«bb
4278 aaaˇ»aa
4279 "});
4280 cx.update_editor(|e, window, cx| {
4281 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added line"))
4282 });
4283 cx.assert_editor_state(indoc! {"
4284 «2
4285 1
4286 added lineˇ»
4287
4288 «bbbb
4289 aaaaa
4290 added lineˇ»
4291 "});
4292
4293 // Removing lines on each selection
4294 cx.set_state(indoc! {"
4295 2«
4296 1ˇ»
4297
4298 bb«bb
4299 aaaˇ»aa
4300 "});
4301 cx.update_editor(|e, window, cx| {
4302 e.manipulate_immutable_lines(window, cx, |lines| {
4303 lines.pop();
4304 })
4305 });
4306 cx.assert_editor_state(indoc! {"
4307 «2ˇ»
4308
4309 «bbbbˇ»
4310 "});
4311}
4312
4313#[gpui::test]
4314async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) {
4315 init_test(cx, |settings| {
4316 settings.defaults.tab_size = NonZeroU32::new(3)
4317 });
4318
4319 let mut cx = EditorTestContext::new(cx).await;
4320
4321 // MULTI SELECTION
4322 // Ln.1 "«" tests empty lines
4323 // Ln.9 tests just leading whitespace
4324 cx.set_state(indoc! {"
4325 «
4326 abc // No indentationˇ»
4327 «\tabc // 1 tabˇ»
4328 \t\tabc « ˇ» // 2 tabs
4329 \t ab«c // Tab followed by space
4330 \tabc // Space followed by tab (3 spaces should be the result)
4331 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4332 abˇ»ˇc ˇ ˇ // Already space indented«
4333 \t
4334 \tabc\tdef // Only the leading tab is manipulatedˇ»
4335 "});
4336 cx.update_editor(|e, window, cx| {
4337 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4338 });
4339 cx.assert_editor_state(indoc! {"
4340 «
4341 abc // No indentation
4342 abc // 1 tab
4343 abc // 2 tabs
4344 abc // Tab followed by space
4345 abc // Space followed by tab (3 spaces should be the result)
4346 abc // Mixed indentation (tab conversion depends on the column)
4347 abc // Already space indented
4348
4349 abc\tdef // Only the leading tab is manipulatedˇ»
4350 "});
4351
4352 // Test on just a few lines, the others should remain unchanged
4353 // Only lines (3, 5, 10, 11) should change
4354 cx.set_state(indoc! {"
4355
4356 abc // No indentation
4357 \tabcˇ // 1 tab
4358 \t\tabc // 2 tabs
4359 \t abcˇ // Tab followed by space
4360 \tabc // Space followed by tab (3 spaces should be the result)
4361 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4362 abc // Already space indented
4363 «\t
4364 \tabc\tdef // Only the leading tab is manipulatedˇ»
4365 "});
4366 cx.update_editor(|e, window, cx| {
4367 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4368 });
4369 cx.assert_editor_state(indoc! {"
4370
4371 abc // No indentation
4372 « abc // 1 tabˇ»
4373 \t\tabc // 2 tabs
4374 « abc // Tab followed by spaceˇ»
4375 \tabc // Space followed by tab (3 spaces should be the result)
4376 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4377 abc // Already space indented
4378 «
4379 abc\tdef // Only the leading tab is manipulatedˇ»
4380 "});
4381
4382 // SINGLE SELECTION
4383 // Ln.1 "«" tests empty lines
4384 // Ln.9 tests just leading whitespace
4385 cx.set_state(indoc! {"
4386 «
4387 abc // No indentation
4388 \tabc // 1 tab
4389 \t\tabc // 2 tabs
4390 \t abc // Tab followed by space
4391 \tabc // Space followed by tab (3 spaces should be the result)
4392 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4393 abc // Already space indented
4394 \t
4395 \tabc\tdef // Only the leading tab is manipulatedˇ»
4396 "});
4397 cx.update_editor(|e, window, cx| {
4398 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4399 });
4400 cx.assert_editor_state(indoc! {"
4401 «
4402 abc // No indentation
4403 abc // 1 tab
4404 abc // 2 tabs
4405 abc // Tab followed by space
4406 abc // Space followed by tab (3 spaces should be the result)
4407 abc // Mixed indentation (tab conversion depends on the column)
4408 abc // Already space indented
4409
4410 abc\tdef // Only the leading tab is manipulatedˇ»
4411 "});
4412}
4413
4414#[gpui::test]
4415async fn test_convert_indentation_to_tabs(cx: &mut TestAppContext) {
4416 init_test(cx, |settings| {
4417 settings.defaults.tab_size = NonZeroU32::new(3)
4418 });
4419
4420 let mut cx = EditorTestContext::new(cx).await;
4421
4422 // MULTI SELECTION
4423 // Ln.1 "«" tests empty lines
4424 // Ln.11 tests just leading whitespace
4425 cx.set_state(indoc! {"
4426 «
4427 abˇ»ˇc // No indentation
4428 abc ˇ ˇ // 1 space (< 3 so dont convert)
4429 abc « // 2 spaces (< 3 so dont convert)
4430 abc // 3 spaces (convert)
4431 abc ˇ» // 5 spaces (1 tab + 2 spaces)
4432 «\tˇ»\t«\tˇ»abc // Already tab indented
4433 «\t abc // Tab followed by space
4434 \tabc // Space followed by tab (should be consumed due to tab)
4435 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4436 \tˇ» «\t
4437 abcˇ» \t ˇˇˇ // Only the leading spaces should be converted
4438 "});
4439 cx.update_editor(|e, window, cx| {
4440 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
4441 });
4442 cx.assert_editor_state(indoc! {"
4443 «
4444 abc // No indentation
4445 abc // 1 space (< 3 so dont convert)
4446 abc // 2 spaces (< 3 so dont convert)
4447 \tabc // 3 spaces (convert)
4448 \t abc // 5 spaces (1 tab + 2 spaces)
4449 \t\t\tabc // Already tab indented
4450 \t abc // Tab followed by space
4451 \tabc // Space followed by tab (should be consumed due to tab)
4452 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4453 \t\t\t
4454 \tabc \t // Only the leading spaces should be convertedˇ»
4455 "});
4456
4457 // Test on just a few lines, the other should remain unchanged
4458 // Only lines (4, 8, 11, 12) should change
4459 cx.set_state(indoc! {"
4460
4461 abc // No indentation
4462 abc // 1 space (< 3 so dont convert)
4463 abc // 2 spaces (< 3 so dont convert)
4464 « abc // 3 spaces (convert)ˇ»
4465 abc // 5 spaces (1 tab + 2 spaces)
4466 \t\t\tabc // Already tab indented
4467 \t abc // Tab followed by space
4468 \tabc ˇ // Space followed by tab (should be consumed due to tab)
4469 \t\t \tabc // Mixed indentation
4470 \t \t \t \tabc // Mixed indentation
4471 \t \tˇ
4472 « abc \t // Only the leading spaces should be convertedˇ»
4473 "});
4474 cx.update_editor(|e, window, cx| {
4475 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
4476 });
4477 cx.assert_editor_state(indoc! {"
4478
4479 abc // No indentation
4480 abc // 1 space (< 3 so dont convert)
4481 abc // 2 spaces (< 3 so dont convert)
4482 «\tabc // 3 spaces (convert)ˇ»
4483 abc // 5 spaces (1 tab + 2 spaces)
4484 \t\t\tabc // Already tab indented
4485 \t abc // Tab followed by space
4486 «\tabc // Space followed by tab (should be consumed due to tab)ˇ»
4487 \t\t \tabc // Mixed indentation
4488 \t \t \t \tabc // Mixed indentation
4489 «\t\t\t
4490 \tabc \t // Only the leading spaces should be convertedˇ»
4491 "});
4492
4493 // SINGLE SELECTION
4494 // Ln.1 "«" tests empty lines
4495 // Ln.11 tests just leading whitespace
4496 cx.set_state(indoc! {"
4497 «
4498 abc // No indentation
4499 abc // 1 space (< 3 so dont convert)
4500 abc // 2 spaces (< 3 so dont convert)
4501 abc // 3 spaces (convert)
4502 abc // 5 spaces (1 tab + 2 spaces)
4503 \t\t\tabc // Already tab indented
4504 \t abc // Tab followed by space
4505 \tabc // Space followed by tab (should be consumed due to tab)
4506 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4507 \t \t
4508 abc \t // Only the leading spaces should be convertedˇ»
4509 "});
4510 cx.update_editor(|e, window, cx| {
4511 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
4512 });
4513 cx.assert_editor_state(indoc! {"
4514 «
4515 abc // No indentation
4516 abc // 1 space (< 3 so dont convert)
4517 abc // 2 spaces (< 3 so dont convert)
4518 \tabc // 3 spaces (convert)
4519 \t abc // 5 spaces (1 tab + 2 spaces)
4520 \t\t\tabc // Already tab indented
4521 \t abc // Tab followed by space
4522 \tabc // Space followed by tab (should be consumed due to tab)
4523 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4524 \t\t\t
4525 \tabc \t // Only the leading spaces should be convertedˇ»
4526 "});
4527}
4528
4529#[gpui::test]
4530async fn test_toggle_case(cx: &mut TestAppContext) {
4531 init_test(cx, |_| {});
4532
4533 let mut cx = EditorTestContext::new(cx).await;
4534
4535 // If all lower case -> upper case
4536 cx.set_state(indoc! {"
4537 «hello worldˇ»
4538 "});
4539 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4540 cx.assert_editor_state(indoc! {"
4541 «HELLO WORLDˇ»
4542 "});
4543
4544 // If all upper case -> lower case
4545 cx.set_state(indoc! {"
4546 «HELLO WORLDˇ»
4547 "});
4548 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4549 cx.assert_editor_state(indoc! {"
4550 «hello worldˇ»
4551 "});
4552
4553 // If any upper case characters are identified -> lower case
4554 // This matches JetBrains IDEs
4555 cx.set_state(indoc! {"
4556 «hEllo worldˇ»
4557 "});
4558 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4559 cx.assert_editor_state(indoc! {"
4560 «hello worldˇ»
4561 "});
4562}
4563
4564#[gpui::test]
4565async fn test_manipulate_text(cx: &mut TestAppContext) {
4566 init_test(cx, |_| {});
4567
4568 let mut cx = EditorTestContext::new(cx).await;
4569
4570 // Test convert_to_upper_case()
4571 cx.set_state(indoc! {"
4572 «hello worldˇ»
4573 "});
4574 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4575 cx.assert_editor_state(indoc! {"
4576 «HELLO WORLDˇ»
4577 "});
4578
4579 // Test convert_to_lower_case()
4580 cx.set_state(indoc! {"
4581 «HELLO WORLDˇ»
4582 "});
4583 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
4584 cx.assert_editor_state(indoc! {"
4585 «hello worldˇ»
4586 "});
4587
4588 // Test multiple line, single selection case
4589 cx.set_state(indoc! {"
4590 «The quick brown
4591 fox jumps over
4592 the lazy dogˇ»
4593 "});
4594 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
4595 cx.assert_editor_state(indoc! {"
4596 «The Quick Brown
4597 Fox Jumps Over
4598 The Lazy Dogˇ»
4599 "});
4600
4601 // Test multiple line, single selection case
4602 cx.set_state(indoc! {"
4603 «The quick brown
4604 fox jumps over
4605 the lazy dogˇ»
4606 "});
4607 cx.update_editor(|e, window, cx| {
4608 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
4609 });
4610 cx.assert_editor_state(indoc! {"
4611 «TheQuickBrown
4612 FoxJumpsOver
4613 TheLazyDogˇ»
4614 "});
4615
4616 // From here on out, test more complex cases of manipulate_text()
4617
4618 // Test no selection case - should affect words cursors are in
4619 // Cursor at beginning, middle, and end of word
4620 cx.set_state(indoc! {"
4621 ˇhello big beauˇtiful worldˇ
4622 "});
4623 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4624 cx.assert_editor_state(indoc! {"
4625 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
4626 "});
4627
4628 // Test multiple selections on a single line and across multiple lines
4629 cx.set_state(indoc! {"
4630 «Theˇ» quick «brown
4631 foxˇ» jumps «overˇ»
4632 the «lazyˇ» dog
4633 "});
4634 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4635 cx.assert_editor_state(indoc! {"
4636 «THEˇ» quick «BROWN
4637 FOXˇ» jumps «OVERˇ»
4638 the «LAZYˇ» dog
4639 "});
4640
4641 // Test case where text length grows
4642 cx.set_state(indoc! {"
4643 «tschüߡ»
4644 "});
4645 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4646 cx.assert_editor_state(indoc! {"
4647 «TSCHÜSSˇ»
4648 "});
4649
4650 // Test to make sure we don't crash when text shrinks
4651 cx.set_state(indoc! {"
4652 aaa_bbbˇ
4653 "});
4654 cx.update_editor(|e, window, cx| {
4655 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4656 });
4657 cx.assert_editor_state(indoc! {"
4658 «aaaBbbˇ»
4659 "});
4660
4661 // Test to make sure we all aware of the fact that each word can grow and shrink
4662 // Final selections should be aware of this fact
4663 cx.set_state(indoc! {"
4664 aaa_bˇbb bbˇb_ccc ˇccc_ddd
4665 "});
4666 cx.update_editor(|e, window, cx| {
4667 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4668 });
4669 cx.assert_editor_state(indoc! {"
4670 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
4671 "});
4672
4673 cx.set_state(indoc! {"
4674 «hElLo, WoRld!ˇ»
4675 "});
4676 cx.update_editor(|e, window, cx| {
4677 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
4678 });
4679 cx.assert_editor_state(indoc! {"
4680 «HeLlO, wOrLD!ˇ»
4681 "});
4682}
4683
4684#[gpui::test]
4685fn test_duplicate_line(cx: &mut TestAppContext) {
4686 init_test(cx, |_| {});
4687
4688 let editor = cx.add_window(|window, cx| {
4689 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4690 build_editor(buffer, window, cx)
4691 });
4692 _ = editor.update(cx, |editor, window, cx| {
4693 editor.change_selections(None, window, cx, |s| {
4694 s.select_display_ranges([
4695 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4696 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4697 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4698 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4699 ])
4700 });
4701 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4702 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4703 assert_eq!(
4704 editor.selections.display_ranges(cx),
4705 vec![
4706 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4707 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
4708 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4709 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4710 ]
4711 );
4712 });
4713
4714 let editor = cx.add_window(|window, cx| {
4715 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4716 build_editor(buffer, window, cx)
4717 });
4718 _ = editor.update(cx, |editor, window, cx| {
4719 editor.change_selections(None, window, cx, |s| {
4720 s.select_display_ranges([
4721 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4722 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4723 ])
4724 });
4725 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4726 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4727 assert_eq!(
4728 editor.selections.display_ranges(cx),
4729 vec![
4730 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
4731 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
4732 ]
4733 );
4734 });
4735
4736 // With `move_upwards` the selections stay in place, except for
4737 // the lines inserted above them
4738 let editor = cx.add_window(|window, cx| {
4739 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4740 build_editor(buffer, window, cx)
4741 });
4742 _ = editor.update(cx, |editor, window, cx| {
4743 editor.change_selections(None, window, cx, |s| {
4744 s.select_display_ranges([
4745 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4746 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4747 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4748 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4749 ])
4750 });
4751 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4752 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4753 assert_eq!(
4754 editor.selections.display_ranges(cx),
4755 vec![
4756 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4757 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4758 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
4759 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4760 ]
4761 );
4762 });
4763
4764 let editor = cx.add_window(|window, cx| {
4765 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4766 build_editor(buffer, window, cx)
4767 });
4768 _ = editor.update(cx, |editor, window, cx| {
4769 editor.change_selections(None, window, cx, |s| {
4770 s.select_display_ranges([
4771 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4772 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4773 ])
4774 });
4775 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4776 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4777 assert_eq!(
4778 editor.selections.display_ranges(cx),
4779 vec![
4780 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4781 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4782 ]
4783 );
4784 });
4785
4786 let editor = cx.add_window(|window, cx| {
4787 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4788 build_editor(buffer, window, cx)
4789 });
4790 _ = editor.update(cx, |editor, window, cx| {
4791 editor.change_selections(None, window, cx, |s| {
4792 s.select_display_ranges([
4793 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4794 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4795 ])
4796 });
4797 editor.duplicate_selection(&DuplicateSelection, window, cx);
4798 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
4799 assert_eq!(
4800 editor.selections.display_ranges(cx),
4801 vec![
4802 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4803 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
4804 ]
4805 );
4806 });
4807}
4808
4809#[gpui::test]
4810fn test_move_line_up_down(cx: &mut TestAppContext) {
4811 init_test(cx, |_| {});
4812
4813 let editor = cx.add_window(|window, cx| {
4814 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4815 build_editor(buffer, window, cx)
4816 });
4817 _ = editor.update(cx, |editor, window, cx| {
4818 editor.fold_creases(
4819 vec![
4820 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
4821 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
4822 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
4823 ],
4824 true,
4825 window,
4826 cx,
4827 );
4828 editor.change_selections(None, window, cx, |s| {
4829 s.select_display_ranges([
4830 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4831 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4832 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4833 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
4834 ])
4835 });
4836 assert_eq!(
4837 editor.display_text(cx),
4838 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
4839 );
4840
4841 editor.move_line_up(&MoveLineUp, window, cx);
4842 assert_eq!(
4843 editor.display_text(cx),
4844 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
4845 );
4846 assert_eq!(
4847 editor.selections.display_ranges(cx),
4848 vec![
4849 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4850 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4851 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4852 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4853 ]
4854 );
4855 });
4856
4857 _ = editor.update(cx, |editor, window, cx| {
4858 editor.move_line_down(&MoveLineDown, window, cx);
4859 assert_eq!(
4860 editor.display_text(cx),
4861 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
4862 );
4863 assert_eq!(
4864 editor.selections.display_ranges(cx),
4865 vec![
4866 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4867 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4868 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4869 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4870 ]
4871 );
4872 });
4873
4874 _ = editor.update(cx, |editor, window, cx| {
4875 editor.move_line_down(&MoveLineDown, window, cx);
4876 assert_eq!(
4877 editor.display_text(cx),
4878 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
4879 );
4880 assert_eq!(
4881 editor.selections.display_ranges(cx),
4882 vec![
4883 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4884 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4885 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4886 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4887 ]
4888 );
4889 });
4890
4891 _ = editor.update(cx, |editor, window, cx| {
4892 editor.move_line_up(&MoveLineUp, window, cx);
4893 assert_eq!(
4894 editor.display_text(cx),
4895 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
4896 );
4897 assert_eq!(
4898 editor.selections.display_ranges(cx),
4899 vec![
4900 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4901 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4902 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4903 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4904 ]
4905 );
4906 });
4907}
4908
4909#[gpui::test]
4910fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
4911 init_test(cx, |_| {});
4912
4913 let editor = cx.add_window(|window, cx| {
4914 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4915 build_editor(buffer, window, cx)
4916 });
4917 _ = editor.update(cx, |editor, window, cx| {
4918 let snapshot = editor.buffer.read(cx).snapshot(cx);
4919 editor.insert_blocks(
4920 [BlockProperties {
4921 style: BlockStyle::Fixed,
4922 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
4923 height: Some(1),
4924 render: Arc::new(|_| div().into_any()),
4925 priority: 0,
4926 render_in_minimap: true,
4927 }],
4928 Some(Autoscroll::fit()),
4929 cx,
4930 );
4931 editor.change_selections(None, window, cx, |s| {
4932 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
4933 });
4934 editor.move_line_down(&MoveLineDown, window, cx);
4935 });
4936}
4937
4938#[gpui::test]
4939async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
4940 init_test(cx, |_| {});
4941
4942 let mut cx = EditorTestContext::new(cx).await;
4943 cx.set_state(
4944 &"
4945 ˇzero
4946 one
4947 two
4948 three
4949 four
4950 five
4951 "
4952 .unindent(),
4953 );
4954
4955 // Create a four-line block that replaces three lines of text.
4956 cx.update_editor(|editor, window, cx| {
4957 let snapshot = editor.snapshot(window, cx);
4958 let snapshot = &snapshot.buffer_snapshot;
4959 let placement = BlockPlacement::Replace(
4960 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
4961 );
4962 editor.insert_blocks(
4963 [BlockProperties {
4964 placement,
4965 height: Some(4),
4966 style: BlockStyle::Sticky,
4967 render: Arc::new(|_| gpui::div().into_any_element()),
4968 priority: 0,
4969 render_in_minimap: true,
4970 }],
4971 None,
4972 cx,
4973 );
4974 });
4975
4976 // Move down so that the cursor touches the block.
4977 cx.update_editor(|editor, window, cx| {
4978 editor.move_down(&Default::default(), window, cx);
4979 });
4980 cx.assert_editor_state(
4981 &"
4982 zero
4983 «one
4984 two
4985 threeˇ»
4986 four
4987 five
4988 "
4989 .unindent(),
4990 );
4991
4992 // Move down past the block.
4993 cx.update_editor(|editor, window, cx| {
4994 editor.move_down(&Default::default(), window, cx);
4995 });
4996 cx.assert_editor_state(
4997 &"
4998 zero
4999 one
5000 two
5001 three
5002 ˇfour
5003 five
5004 "
5005 .unindent(),
5006 );
5007}
5008
5009#[gpui::test]
5010fn test_transpose(cx: &mut TestAppContext) {
5011 init_test(cx, |_| {});
5012
5013 _ = cx.add_window(|window, cx| {
5014 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
5015 editor.set_style(EditorStyle::default(), window, cx);
5016 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
5017 editor.transpose(&Default::default(), window, cx);
5018 assert_eq!(editor.text(cx), "bac");
5019 assert_eq!(editor.selections.ranges(cx), [2..2]);
5020
5021 editor.transpose(&Default::default(), window, cx);
5022 assert_eq!(editor.text(cx), "bca");
5023 assert_eq!(editor.selections.ranges(cx), [3..3]);
5024
5025 editor.transpose(&Default::default(), window, cx);
5026 assert_eq!(editor.text(cx), "bac");
5027 assert_eq!(editor.selections.ranges(cx), [3..3]);
5028
5029 editor
5030 });
5031
5032 _ = cx.add_window(|window, cx| {
5033 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5034 editor.set_style(EditorStyle::default(), window, cx);
5035 editor.change_selections(None, window, cx, |s| s.select_ranges([3..3]));
5036 editor.transpose(&Default::default(), window, cx);
5037 assert_eq!(editor.text(cx), "acb\nde");
5038 assert_eq!(editor.selections.ranges(cx), [3..3]);
5039
5040 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
5041 editor.transpose(&Default::default(), window, cx);
5042 assert_eq!(editor.text(cx), "acbd\ne");
5043 assert_eq!(editor.selections.ranges(cx), [5..5]);
5044
5045 editor.transpose(&Default::default(), window, cx);
5046 assert_eq!(editor.text(cx), "acbde\n");
5047 assert_eq!(editor.selections.ranges(cx), [6..6]);
5048
5049 editor.transpose(&Default::default(), window, cx);
5050 assert_eq!(editor.text(cx), "acbd\ne");
5051 assert_eq!(editor.selections.ranges(cx), [6..6]);
5052
5053 editor
5054 });
5055
5056 _ = cx.add_window(|window, cx| {
5057 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5058 editor.set_style(EditorStyle::default(), window, cx);
5059 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1, 2..2, 4..4]));
5060 editor.transpose(&Default::default(), window, cx);
5061 assert_eq!(editor.text(cx), "bacd\ne");
5062 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
5063
5064 editor.transpose(&Default::default(), window, cx);
5065 assert_eq!(editor.text(cx), "bcade\n");
5066 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
5067
5068 editor.transpose(&Default::default(), window, cx);
5069 assert_eq!(editor.text(cx), "bcda\ne");
5070 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5071
5072 editor.transpose(&Default::default(), window, cx);
5073 assert_eq!(editor.text(cx), "bcade\n");
5074 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5075
5076 editor.transpose(&Default::default(), window, cx);
5077 assert_eq!(editor.text(cx), "bcaed\n");
5078 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
5079
5080 editor
5081 });
5082
5083 _ = cx.add_window(|window, cx| {
5084 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
5085 editor.set_style(EditorStyle::default(), window, cx);
5086 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
5087 editor.transpose(&Default::default(), window, cx);
5088 assert_eq!(editor.text(cx), "🏀🍐✋");
5089 assert_eq!(editor.selections.ranges(cx), [8..8]);
5090
5091 editor.transpose(&Default::default(), window, cx);
5092 assert_eq!(editor.text(cx), "🏀✋🍐");
5093 assert_eq!(editor.selections.ranges(cx), [11..11]);
5094
5095 editor.transpose(&Default::default(), window, cx);
5096 assert_eq!(editor.text(cx), "🏀🍐✋");
5097 assert_eq!(editor.selections.ranges(cx), [11..11]);
5098
5099 editor
5100 });
5101}
5102
5103#[gpui::test]
5104async fn test_rewrap(cx: &mut TestAppContext) {
5105 init_test(cx, |settings| {
5106 settings.languages.extend([
5107 (
5108 "Markdown".into(),
5109 LanguageSettingsContent {
5110 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5111 ..Default::default()
5112 },
5113 ),
5114 (
5115 "Plain Text".into(),
5116 LanguageSettingsContent {
5117 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5118 ..Default::default()
5119 },
5120 ),
5121 ])
5122 });
5123
5124 let mut cx = EditorTestContext::new(cx).await;
5125
5126 let language_with_c_comments = Arc::new(Language::new(
5127 LanguageConfig {
5128 line_comments: vec!["// ".into()],
5129 ..LanguageConfig::default()
5130 },
5131 None,
5132 ));
5133 let language_with_pound_comments = Arc::new(Language::new(
5134 LanguageConfig {
5135 line_comments: vec!["# ".into()],
5136 ..LanguageConfig::default()
5137 },
5138 None,
5139 ));
5140 let markdown_language = Arc::new(Language::new(
5141 LanguageConfig {
5142 name: "Markdown".into(),
5143 ..LanguageConfig::default()
5144 },
5145 None,
5146 ));
5147 let language_with_doc_comments = Arc::new(Language::new(
5148 LanguageConfig {
5149 line_comments: vec!["// ".into(), "/// ".into()],
5150 ..LanguageConfig::default()
5151 },
5152 Some(tree_sitter_rust::LANGUAGE.into()),
5153 ));
5154
5155 let plaintext_language = Arc::new(Language::new(
5156 LanguageConfig {
5157 name: "Plain Text".into(),
5158 ..LanguageConfig::default()
5159 },
5160 None,
5161 ));
5162
5163 assert_rewrap(
5164 indoc! {"
5165 // ˇ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.
5166 "},
5167 indoc! {"
5168 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
5169 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
5170 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
5171 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
5172 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
5173 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
5174 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
5175 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
5176 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
5177 // porttitor id. Aliquam id accumsan eros.
5178 "},
5179 language_with_c_comments.clone(),
5180 &mut cx,
5181 );
5182
5183 // Test that rewrapping works inside of a selection
5184 assert_rewrap(
5185 indoc! {"
5186 «// 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.ˇ»
5187 "},
5188 indoc! {"
5189 «// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
5190 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
5191 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
5192 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
5193 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
5194 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
5195 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
5196 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
5197 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
5198 // porttitor id. Aliquam id accumsan eros.ˇ»
5199 "},
5200 language_with_c_comments.clone(),
5201 &mut cx,
5202 );
5203
5204 // Test that cursors that expand to the same region are collapsed.
5205 assert_rewrap(
5206 indoc! {"
5207 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
5208 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
5209 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
5210 // ˇ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.
5211 "},
5212 indoc! {"
5213 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
5214 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
5215 // auctor, eu lacinia sapien scelerisque. ˇVivamus sit amet neque et quam
5216 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
5217 // Pellentesque odio lectus, iaculis ac volutpat et, ˇblandit quis urna. Sed
5218 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
5219 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
5220 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
5221 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
5222 // porttitor id. Aliquam id accumsan eros.
5223 "},
5224 language_with_c_comments.clone(),
5225 &mut cx,
5226 );
5227
5228 // Test that non-contiguous selections are treated separately.
5229 assert_rewrap(
5230 indoc! {"
5231 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
5232 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
5233 //
5234 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
5235 // ˇ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.
5236 "},
5237 indoc! {"
5238 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
5239 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
5240 // auctor, eu lacinia sapien scelerisque.
5241 //
5242 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas
5243 // tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
5244 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec
5245 // molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque
5246 // nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas
5247 // porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id
5248 // vulputate turpis porttitor id. Aliquam id accumsan eros.
5249 "},
5250 language_with_c_comments.clone(),
5251 &mut cx,
5252 );
5253
5254 // Test that different comment prefixes are supported.
5255 assert_rewrap(
5256 indoc! {"
5257 # ˇ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.
5258 "},
5259 indoc! {"
5260 # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
5261 # purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
5262 # eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
5263 # hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
5264 # lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit
5265 # amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet
5266 # in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur
5267 # adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis.
5268 # Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id
5269 # accumsan eros.
5270 "},
5271 language_with_pound_comments.clone(),
5272 &mut cx,
5273 );
5274
5275 // Test that rewrapping is ignored outside of comments in most languages.
5276 assert_rewrap(
5277 indoc! {"
5278 /// Adds two numbers.
5279 /// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
5280 fn add(a: u32, b: u32) -> u32 {
5281 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ˇ
5282 }
5283 "},
5284 indoc! {"
5285 /// Adds two numbers. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
5286 /// Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
5287 fn add(a: u32, b: u32) -> u32 {
5288 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ˇ
5289 }
5290 "},
5291 language_with_doc_comments.clone(),
5292 &mut cx,
5293 );
5294
5295 // Test that rewrapping works in Markdown and Plain Text languages.
5296 assert_rewrap(
5297 indoc! {"
5298 # Hello
5299
5300 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.
5301 "},
5302 indoc! {"
5303 # Hello
5304
5305 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
5306 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
5307 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
5308 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
5309 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
5310 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
5311 Integer sit amet scelerisque nisi.
5312 "},
5313 markdown_language,
5314 &mut cx,
5315 );
5316
5317 assert_rewrap(
5318 indoc! {"
5319 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.
5320 "},
5321 indoc! {"
5322 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
5323 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
5324 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
5325 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
5326 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
5327 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
5328 Integer sit amet scelerisque nisi.
5329 "},
5330 plaintext_language.clone(),
5331 &mut cx,
5332 );
5333
5334 // Test rewrapping unaligned comments in a selection.
5335 assert_rewrap(
5336 indoc! {"
5337 fn foo() {
5338 if true {
5339 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
5340 // Praesent semper egestas tellus id dignissim.ˇ»
5341 do_something();
5342 } else {
5343 //
5344 }
5345 }
5346 "},
5347 indoc! {"
5348 fn foo() {
5349 if true {
5350 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
5351 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
5352 // egestas tellus id dignissim.ˇ»
5353 do_something();
5354 } else {
5355 //
5356 }
5357 }
5358 "},
5359 language_with_doc_comments.clone(),
5360 &mut cx,
5361 );
5362
5363 assert_rewrap(
5364 indoc! {"
5365 fn foo() {
5366 if true {
5367 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
5368 // Praesent semper egestas tellus id dignissim.»
5369 do_something();
5370 } else {
5371 //
5372 }
5373
5374 }
5375 "},
5376 indoc! {"
5377 fn foo() {
5378 if true {
5379 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
5380 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
5381 // egestas tellus id dignissim.»
5382 do_something();
5383 } else {
5384 //
5385 }
5386
5387 }
5388 "},
5389 language_with_doc_comments.clone(),
5390 &mut cx,
5391 );
5392
5393 assert_rewrap(
5394 indoc! {"
5395 «ˇone one one one one one one one one one one one one one one one one one one one one one one one one
5396
5397 two»
5398
5399 three
5400
5401 «ˇ\t
5402
5403 four four four four four four four four four four four four four four four four four four four four»
5404
5405 «ˇfive five five five five five five five five five five five five five five five five five five five
5406 \t»
5407 six six six six six six six six six six six six six six six six six six six six six six six six six
5408 "},
5409 indoc! {"
5410 «ˇone one one one one one one one one one one one one one one one one one one one
5411 one one one one one
5412
5413 two»
5414
5415 three
5416
5417 «ˇ\t
5418
5419 four four four four four four four four four four four four four four four four
5420 four four four four»
5421
5422 «ˇfive five five five five five five five five five five five five five five five
5423 five five five five
5424 \t»
5425 six six six six six six six six six six six six six six six six six six six six six six six six six
5426 "},
5427 plaintext_language.clone(),
5428 &mut cx,
5429 );
5430
5431 assert_rewrap(
5432 indoc! {"
5433 //ˇ long long long long long long long long long long long long long long long long long long long long long long long long long long long long
5434 //ˇ
5435 //ˇ long long long long long long long long long long long long long long long long long long long long long long long long long long long long
5436 //ˇ short short short
5437 int main(void) {
5438 return 17;
5439 }
5440 "},
5441 indoc! {"
5442 //ˇ long long long long long long long long long long long long long long long
5443 // long long long long long long long long long long long long long
5444 //ˇ
5445 //ˇ long long long long long long long long long long long long long long long
5446 //ˇ long long long long long long long long long long long long long short short
5447 // short
5448 int main(void) {
5449 return 17;
5450 }
5451 "},
5452 language_with_c_comments,
5453 &mut cx,
5454 );
5455
5456 #[track_caller]
5457 fn assert_rewrap(
5458 unwrapped_text: &str,
5459 wrapped_text: &str,
5460 language: Arc<Language>,
5461 cx: &mut EditorTestContext,
5462 ) {
5463 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
5464 cx.set_state(unwrapped_text);
5465 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
5466 cx.assert_editor_state(wrapped_text);
5467 }
5468}
5469
5470#[gpui::test]
5471async fn test_hard_wrap(cx: &mut TestAppContext) {
5472 init_test(cx, |_| {});
5473 let mut cx = EditorTestContext::new(cx).await;
5474
5475 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
5476 cx.update_editor(|editor, _, cx| {
5477 editor.set_hard_wrap(Some(14), cx);
5478 });
5479
5480 cx.set_state(indoc!(
5481 "
5482 one two three ˇ
5483 "
5484 ));
5485 cx.simulate_input("four");
5486 cx.run_until_parked();
5487
5488 cx.assert_editor_state(indoc!(
5489 "
5490 one two three
5491 fourˇ
5492 "
5493 ));
5494
5495 cx.update_editor(|editor, window, cx| {
5496 editor.newline(&Default::default(), window, cx);
5497 });
5498 cx.run_until_parked();
5499 cx.assert_editor_state(indoc!(
5500 "
5501 one two three
5502 four
5503 ˇ
5504 "
5505 ));
5506
5507 cx.simulate_input("five");
5508 cx.run_until_parked();
5509 cx.assert_editor_state(indoc!(
5510 "
5511 one two three
5512 four
5513 fiveˇ
5514 "
5515 ));
5516
5517 cx.update_editor(|editor, window, cx| {
5518 editor.newline(&Default::default(), window, cx);
5519 });
5520 cx.run_until_parked();
5521 cx.simulate_input("# ");
5522 cx.run_until_parked();
5523 cx.assert_editor_state(indoc!(
5524 "
5525 one two three
5526 four
5527 five
5528 # ˇ
5529 "
5530 ));
5531
5532 cx.update_editor(|editor, window, cx| {
5533 editor.newline(&Default::default(), window, cx);
5534 });
5535 cx.run_until_parked();
5536 cx.assert_editor_state(indoc!(
5537 "
5538 one two three
5539 four
5540 five
5541 #\x20
5542 #ˇ
5543 "
5544 ));
5545
5546 cx.simulate_input(" 6");
5547 cx.run_until_parked();
5548 cx.assert_editor_state(indoc!(
5549 "
5550 one two three
5551 four
5552 five
5553 #
5554 # 6ˇ
5555 "
5556 ));
5557}
5558
5559#[gpui::test]
5560async fn test_clipboard(cx: &mut TestAppContext) {
5561 init_test(cx, |_| {});
5562
5563 let mut cx = EditorTestContext::new(cx).await;
5564
5565 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
5566 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5567 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
5568
5569 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
5570 cx.set_state("two ˇfour ˇsix ˇ");
5571 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5572 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
5573
5574 // Paste again but with only two cursors. Since the number of cursors doesn't
5575 // match the number of slices in the clipboard, the entire clipboard text
5576 // is pasted at each cursor.
5577 cx.set_state("ˇtwo one✅ four three six five ˇ");
5578 cx.update_editor(|e, window, cx| {
5579 e.handle_input("( ", window, cx);
5580 e.paste(&Paste, window, cx);
5581 e.handle_input(") ", window, cx);
5582 });
5583 cx.assert_editor_state(
5584 &([
5585 "( one✅ ",
5586 "three ",
5587 "five ) ˇtwo one✅ four three six five ( one✅ ",
5588 "three ",
5589 "five ) ˇ",
5590 ]
5591 .join("\n")),
5592 );
5593
5594 // Cut with three selections, one of which is full-line.
5595 cx.set_state(indoc! {"
5596 1«2ˇ»3
5597 4ˇ567
5598 «8ˇ»9"});
5599 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5600 cx.assert_editor_state(indoc! {"
5601 1ˇ3
5602 ˇ9"});
5603
5604 // Paste with three selections, noticing how the copied selection that was full-line
5605 // gets inserted before the second cursor.
5606 cx.set_state(indoc! {"
5607 1ˇ3
5608 9ˇ
5609 «oˇ»ne"});
5610 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5611 cx.assert_editor_state(indoc! {"
5612 12ˇ3
5613 4567
5614 9ˇ
5615 8ˇne"});
5616
5617 // Copy with a single cursor only, which writes the whole line into the clipboard.
5618 cx.set_state(indoc! {"
5619 The quick brown
5620 fox juˇmps over
5621 the lazy dog"});
5622 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5623 assert_eq!(
5624 cx.read_from_clipboard()
5625 .and_then(|item| item.text().as_deref().map(str::to_string)),
5626 Some("fox jumps over\n".to_string())
5627 );
5628
5629 // Paste with three selections, noticing how the copied full-line selection is inserted
5630 // before the empty selections but replaces the selection that is non-empty.
5631 cx.set_state(indoc! {"
5632 Tˇhe quick brown
5633 «foˇ»x jumps over
5634 tˇhe lazy dog"});
5635 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5636 cx.assert_editor_state(indoc! {"
5637 fox jumps over
5638 Tˇhe quick brown
5639 fox jumps over
5640 ˇx jumps over
5641 fox jumps over
5642 tˇhe lazy dog"});
5643}
5644
5645#[gpui::test]
5646async fn test_copy_trim(cx: &mut TestAppContext) {
5647 init_test(cx, |_| {});
5648
5649 let mut cx = EditorTestContext::new(cx).await;
5650 cx.set_state(
5651 r#" «for selection in selections.iter() {
5652 let mut start = selection.start;
5653 let mut end = selection.end;
5654 let is_entire_line = selection.is_empty();
5655 if is_entire_line {
5656 start = Point::new(start.row, 0);ˇ»
5657 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5658 }
5659 "#,
5660 );
5661 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5662 assert_eq!(
5663 cx.read_from_clipboard()
5664 .and_then(|item| item.text().as_deref().map(str::to_string)),
5665 Some(
5666 "for selection in selections.iter() {
5667 let mut start = selection.start;
5668 let mut end = selection.end;
5669 let is_entire_line = selection.is_empty();
5670 if is_entire_line {
5671 start = Point::new(start.row, 0);"
5672 .to_string()
5673 ),
5674 "Regular copying preserves all indentation selected",
5675 );
5676 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5677 assert_eq!(
5678 cx.read_from_clipboard()
5679 .and_then(|item| item.text().as_deref().map(str::to_string)),
5680 Some(
5681 "for selection in selections.iter() {
5682let mut start = selection.start;
5683let mut end = selection.end;
5684let is_entire_line = selection.is_empty();
5685if is_entire_line {
5686 start = Point::new(start.row, 0);"
5687 .to_string()
5688 ),
5689 "Copying with stripping should strip all leading whitespaces"
5690 );
5691
5692 cx.set_state(
5693 r#" « for selection in selections.iter() {
5694 let mut start = selection.start;
5695 let mut end = selection.end;
5696 let is_entire_line = selection.is_empty();
5697 if is_entire_line {
5698 start = Point::new(start.row, 0);ˇ»
5699 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5700 }
5701 "#,
5702 );
5703 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5704 assert_eq!(
5705 cx.read_from_clipboard()
5706 .and_then(|item| item.text().as_deref().map(str::to_string)),
5707 Some(
5708 " for selection in selections.iter() {
5709 let mut start = selection.start;
5710 let mut end = selection.end;
5711 let is_entire_line = selection.is_empty();
5712 if is_entire_line {
5713 start = Point::new(start.row, 0);"
5714 .to_string()
5715 ),
5716 "Regular copying preserves all indentation selected",
5717 );
5718 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5719 assert_eq!(
5720 cx.read_from_clipboard()
5721 .and_then(|item| item.text().as_deref().map(str::to_string)),
5722 Some(
5723 "for selection in selections.iter() {
5724let mut start = selection.start;
5725let mut end = selection.end;
5726let is_entire_line = selection.is_empty();
5727if is_entire_line {
5728 start = Point::new(start.row, 0);"
5729 .to_string()
5730 ),
5731 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
5732 );
5733
5734 cx.set_state(
5735 r#" «ˇ for selection in selections.iter() {
5736 let mut start = selection.start;
5737 let mut end = selection.end;
5738 let is_entire_line = selection.is_empty();
5739 if is_entire_line {
5740 start = Point::new(start.row, 0);»
5741 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5742 }
5743 "#,
5744 );
5745 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5746 assert_eq!(
5747 cx.read_from_clipboard()
5748 .and_then(|item| item.text().as_deref().map(str::to_string)),
5749 Some(
5750 " for selection in selections.iter() {
5751 let mut start = selection.start;
5752 let mut end = selection.end;
5753 let is_entire_line = selection.is_empty();
5754 if is_entire_line {
5755 start = Point::new(start.row, 0);"
5756 .to_string()
5757 ),
5758 "Regular copying for reverse selection works the same",
5759 );
5760 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5761 assert_eq!(
5762 cx.read_from_clipboard()
5763 .and_then(|item| item.text().as_deref().map(str::to_string)),
5764 Some(
5765 "for selection in selections.iter() {
5766let mut start = selection.start;
5767let mut end = selection.end;
5768let is_entire_line = selection.is_empty();
5769if is_entire_line {
5770 start = Point::new(start.row, 0);"
5771 .to_string()
5772 ),
5773 "Copying with stripping for reverse selection works the same"
5774 );
5775
5776 cx.set_state(
5777 r#" for selection «in selections.iter() {
5778 let mut start = selection.start;
5779 let mut end = selection.end;
5780 let is_entire_line = selection.is_empty();
5781 if is_entire_line {
5782 start = Point::new(start.row, 0);ˇ»
5783 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5784 }
5785 "#,
5786 );
5787 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5788 assert_eq!(
5789 cx.read_from_clipboard()
5790 .and_then(|item| item.text().as_deref().map(str::to_string)),
5791 Some(
5792 "in selections.iter() {
5793 let mut start = selection.start;
5794 let mut end = selection.end;
5795 let is_entire_line = selection.is_empty();
5796 if is_entire_line {
5797 start = Point::new(start.row, 0);"
5798 .to_string()
5799 ),
5800 "When selecting past the indent, the copying works as usual",
5801 );
5802 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5803 assert_eq!(
5804 cx.read_from_clipboard()
5805 .and_then(|item| item.text().as_deref().map(str::to_string)),
5806 Some(
5807 "in selections.iter() {
5808 let mut start = selection.start;
5809 let mut end = selection.end;
5810 let is_entire_line = selection.is_empty();
5811 if is_entire_line {
5812 start = Point::new(start.row, 0);"
5813 .to_string()
5814 ),
5815 "When selecting past the indent, nothing is trimmed"
5816 );
5817
5818 cx.set_state(
5819 r#" «for selection in selections.iter() {
5820 let mut start = selection.start;
5821
5822 let mut end = selection.end;
5823 let is_entire_line = selection.is_empty();
5824 if is_entire_line {
5825 start = Point::new(start.row, 0);
5826ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
5827 }
5828 "#,
5829 );
5830 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5831 assert_eq!(
5832 cx.read_from_clipboard()
5833 .and_then(|item| item.text().as_deref().map(str::to_string)),
5834 Some(
5835 "for selection in selections.iter() {
5836let mut start = selection.start;
5837
5838let mut end = selection.end;
5839let is_entire_line = selection.is_empty();
5840if is_entire_line {
5841 start = Point::new(start.row, 0);
5842"
5843 .to_string()
5844 ),
5845 "Copying with stripping should ignore empty lines"
5846 );
5847}
5848
5849#[gpui::test]
5850async fn test_paste_multiline(cx: &mut TestAppContext) {
5851 init_test(cx, |_| {});
5852
5853 let mut cx = EditorTestContext::new(cx).await;
5854 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5855
5856 // Cut an indented block, without the leading whitespace.
5857 cx.set_state(indoc! {"
5858 const a: B = (
5859 c(),
5860 «d(
5861 e,
5862 f
5863 )ˇ»
5864 );
5865 "});
5866 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5867 cx.assert_editor_state(indoc! {"
5868 const a: B = (
5869 c(),
5870 ˇ
5871 );
5872 "});
5873
5874 // Paste it at the same position.
5875 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5876 cx.assert_editor_state(indoc! {"
5877 const a: B = (
5878 c(),
5879 d(
5880 e,
5881 f
5882 )ˇ
5883 );
5884 "});
5885
5886 // Paste it at a line with a lower indent level.
5887 cx.set_state(indoc! {"
5888 ˇ
5889 const a: B = (
5890 c(),
5891 );
5892 "});
5893 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5894 cx.assert_editor_state(indoc! {"
5895 d(
5896 e,
5897 f
5898 )ˇ
5899 const a: B = (
5900 c(),
5901 );
5902 "});
5903
5904 // Cut an indented block, with the leading whitespace.
5905 cx.set_state(indoc! {"
5906 const a: B = (
5907 c(),
5908 « d(
5909 e,
5910 f
5911 )
5912 ˇ»);
5913 "});
5914 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5915 cx.assert_editor_state(indoc! {"
5916 const a: B = (
5917 c(),
5918 ˇ);
5919 "});
5920
5921 // Paste it at the same position.
5922 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5923 cx.assert_editor_state(indoc! {"
5924 const a: B = (
5925 c(),
5926 d(
5927 e,
5928 f
5929 )
5930 ˇ);
5931 "});
5932
5933 // Paste it at a line with a higher indent level.
5934 cx.set_state(indoc! {"
5935 const a: B = (
5936 c(),
5937 d(
5938 e,
5939 fˇ
5940 )
5941 );
5942 "});
5943 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5944 cx.assert_editor_state(indoc! {"
5945 const a: B = (
5946 c(),
5947 d(
5948 e,
5949 f d(
5950 e,
5951 f
5952 )
5953 ˇ
5954 )
5955 );
5956 "});
5957
5958 // Copy an indented block, starting mid-line
5959 cx.set_state(indoc! {"
5960 const a: B = (
5961 c(),
5962 somethin«g(
5963 e,
5964 f
5965 )ˇ»
5966 );
5967 "});
5968 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5969
5970 // Paste it on a line with a lower indent level
5971 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
5972 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5973 cx.assert_editor_state(indoc! {"
5974 const a: B = (
5975 c(),
5976 something(
5977 e,
5978 f
5979 )
5980 );
5981 g(
5982 e,
5983 f
5984 )ˇ"});
5985}
5986
5987#[gpui::test]
5988async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
5989 init_test(cx, |_| {});
5990
5991 cx.write_to_clipboard(ClipboardItem::new_string(
5992 " d(\n e\n );\n".into(),
5993 ));
5994
5995 let mut cx = EditorTestContext::new(cx).await;
5996 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5997
5998 cx.set_state(indoc! {"
5999 fn a() {
6000 b();
6001 if c() {
6002 ˇ
6003 }
6004 }
6005 "});
6006
6007 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6008 cx.assert_editor_state(indoc! {"
6009 fn a() {
6010 b();
6011 if c() {
6012 d(
6013 e
6014 );
6015 ˇ
6016 }
6017 }
6018 "});
6019
6020 cx.set_state(indoc! {"
6021 fn a() {
6022 b();
6023 ˇ
6024 }
6025 "});
6026
6027 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6028 cx.assert_editor_state(indoc! {"
6029 fn a() {
6030 b();
6031 d(
6032 e
6033 );
6034 ˇ
6035 }
6036 "});
6037}
6038
6039#[gpui::test]
6040fn test_select_all(cx: &mut TestAppContext) {
6041 init_test(cx, |_| {});
6042
6043 let editor = cx.add_window(|window, cx| {
6044 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
6045 build_editor(buffer, window, cx)
6046 });
6047 _ = editor.update(cx, |editor, window, cx| {
6048 editor.select_all(&SelectAll, window, cx);
6049 assert_eq!(
6050 editor.selections.display_ranges(cx),
6051 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
6052 );
6053 });
6054}
6055
6056#[gpui::test]
6057fn test_select_line(cx: &mut TestAppContext) {
6058 init_test(cx, |_| {});
6059
6060 let editor = cx.add_window(|window, cx| {
6061 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
6062 build_editor(buffer, window, cx)
6063 });
6064 _ = editor.update(cx, |editor, window, cx| {
6065 editor.change_selections(None, window, cx, |s| {
6066 s.select_display_ranges([
6067 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6068 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
6069 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
6070 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
6071 ])
6072 });
6073 editor.select_line(&SelectLine, window, cx);
6074 assert_eq!(
6075 editor.selections.display_ranges(cx),
6076 vec![
6077 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
6078 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
6079 ]
6080 );
6081 });
6082
6083 _ = editor.update(cx, |editor, window, cx| {
6084 editor.select_line(&SelectLine, window, cx);
6085 assert_eq!(
6086 editor.selections.display_ranges(cx),
6087 vec![
6088 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
6089 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
6090 ]
6091 );
6092 });
6093
6094 _ = editor.update(cx, |editor, window, cx| {
6095 editor.select_line(&SelectLine, window, cx);
6096 assert_eq!(
6097 editor.selections.display_ranges(cx),
6098 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
6099 );
6100 });
6101}
6102
6103#[gpui::test]
6104async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
6105 init_test(cx, |_| {});
6106 let mut cx = EditorTestContext::new(cx).await;
6107
6108 #[track_caller]
6109 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
6110 cx.set_state(initial_state);
6111 cx.update_editor(|e, window, cx| {
6112 e.split_selection_into_lines(&SplitSelectionIntoLines, window, cx)
6113 });
6114 cx.assert_editor_state(expected_state);
6115 }
6116
6117 // Selection starts and ends at the middle of lines, left-to-right
6118 test(
6119 &mut cx,
6120 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
6121 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
6122 );
6123 // Same thing, right-to-left
6124 test(
6125 &mut cx,
6126 "aa\nb«b\ncc\ndd\neˇ»e\nff",
6127 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
6128 );
6129
6130 // Whole buffer, left-to-right, last line *doesn't* end with newline
6131 test(
6132 &mut cx,
6133 "«ˇaa\nbb\ncc\ndd\nee\nff»",
6134 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
6135 );
6136 // Same thing, right-to-left
6137 test(
6138 &mut cx,
6139 "«aa\nbb\ncc\ndd\nee\nffˇ»",
6140 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
6141 );
6142
6143 // Whole buffer, left-to-right, last line ends with newline
6144 test(
6145 &mut cx,
6146 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
6147 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
6148 );
6149 // Same thing, right-to-left
6150 test(
6151 &mut cx,
6152 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
6153 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
6154 );
6155
6156 // Starts at the end of a line, ends at the start of another
6157 test(
6158 &mut cx,
6159 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
6160 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
6161 );
6162}
6163
6164#[gpui::test]
6165async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
6166 init_test(cx, |_| {});
6167
6168 let editor = cx.add_window(|window, cx| {
6169 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
6170 build_editor(buffer, window, cx)
6171 });
6172
6173 // setup
6174 _ = editor.update(cx, |editor, window, cx| {
6175 editor.fold_creases(
6176 vec![
6177 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
6178 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
6179 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
6180 ],
6181 true,
6182 window,
6183 cx,
6184 );
6185 assert_eq!(
6186 editor.display_text(cx),
6187 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
6188 );
6189 });
6190
6191 _ = editor.update(cx, |editor, window, cx| {
6192 editor.change_selections(None, window, cx, |s| {
6193 s.select_display_ranges([
6194 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6195 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
6196 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
6197 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
6198 ])
6199 });
6200 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
6201 assert_eq!(
6202 editor.display_text(cx),
6203 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
6204 );
6205 });
6206 EditorTestContext::for_editor(editor, cx)
6207 .await
6208 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
6209
6210 _ = editor.update(cx, |editor, window, cx| {
6211 editor.change_selections(None, window, cx, |s| {
6212 s.select_display_ranges([
6213 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
6214 ])
6215 });
6216 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
6217 assert_eq!(
6218 editor.display_text(cx),
6219 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
6220 );
6221 assert_eq!(
6222 editor.selections.display_ranges(cx),
6223 [
6224 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
6225 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
6226 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
6227 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
6228 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
6229 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
6230 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
6231 ]
6232 );
6233 });
6234 EditorTestContext::for_editor(editor, cx)
6235 .await
6236 .assert_editor_state(
6237 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
6238 );
6239}
6240
6241#[gpui::test]
6242async fn test_add_selection_above_below(cx: &mut TestAppContext) {
6243 init_test(cx, |_| {});
6244
6245 let mut cx = EditorTestContext::new(cx).await;
6246
6247 cx.set_state(indoc!(
6248 r#"abc
6249 defˇghi
6250
6251 jk
6252 nlmo
6253 "#
6254 ));
6255
6256 cx.update_editor(|editor, window, cx| {
6257 editor.add_selection_above(&Default::default(), window, cx);
6258 });
6259
6260 cx.assert_editor_state(indoc!(
6261 r#"abcˇ
6262 defˇghi
6263
6264 jk
6265 nlmo
6266 "#
6267 ));
6268
6269 cx.update_editor(|editor, window, cx| {
6270 editor.add_selection_above(&Default::default(), window, cx);
6271 });
6272
6273 cx.assert_editor_state(indoc!(
6274 r#"abcˇ
6275 defˇghi
6276
6277 jk
6278 nlmo
6279 "#
6280 ));
6281
6282 cx.update_editor(|editor, window, cx| {
6283 editor.add_selection_below(&Default::default(), window, cx);
6284 });
6285
6286 cx.assert_editor_state(indoc!(
6287 r#"abc
6288 defˇghi
6289
6290 jk
6291 nlmo
6292 "#
6293 ));
6294
6295 cx.update_editor(|editor, window, cx| {
6296 editor.undo_selection(&Default::default(), window, cx);
6297 });
6298
6299 cx.assert_editor_state(indoc!(
6300 r#"abcˇ
6301 defˇghi
6302
6303 jk
6304 nlmo
6305 "#
6306 ));
6307
6308 cx.update_editor(|editor, window, cx| {
6309 editor.redo_selection(&Default::default(), window, cx);
6310 });
6311
6312 cx.assert_editor_state(indoc!(
6313 r#"abc
6314 defˇghi
6315
6316 jk
6317 nlmo
6318 "#
6319 ));
6320
6321 cx.update_editor(|editor, window, cx| {
6322 editor.add_selection_below(&Default::default(), window, cx);
6323 });
6324
6325 cx.assert_editor_state(indoc!(
6326 r#"abc
6327 defˇghi
6328 ˇ
6329 jk
6330 nlmo
6331 "#
6332 ));
6333
6334 cx.update_editor(|editor, window, cx| {
6335 editor.add_selection_below(&Default::default(), window, cx);
6336 });
6337
6338 cx.assert_editor_state(indoc!(
6339 r#"abc
6340 defˇghi
6341 ˇ
6342 jkˇ
6343 nlmo
6344 "#
6345 ));
6346
6347 cx.update_editor(|editor, window, cx| {
6348 editor.add_selection_below(&Default::default(), window, cx);
6349 });
6350
6351 cx.assert_editor_state(indoc!(
6352 r#"abc
6353 defˇghi
6354 ˇ
6355 jkˇ
6356 nlmˇo
6357 "#
6358 ));
6359
6360 cx.update_editor(|editor, window, cx| {
6361 editor.add_selection_below(&Default::default(), window, cx);
6362 });
6363
6364 cx.assert_editor_state(indoc!(
6365 r#"abc
6366 defˇghi
6367 ˇ
6368 jkˇ
6369 nlmˇo
6370 ˇ"#
6371 ));
6372
6373 // change selections
6374 cx.set_state(indoc!(
6375 r#"abc
6376 def«ˇg»hi
6377
6378 jk
6379 nlmo
6380 "#
6381 ));
6382
6383 cx.update_editor(|editor, window, cx| {
6384 editor.add_selection_below(&Default::default(), window, cx);
6385 });
6386
6387 cx.assert_editor_state(indoc!(
6388 r#"abc
6389 def«ˇg»hi
6390
6391 jk
6392 nlm«ˇo»
6393 "#
6394 ));
6395
6396 cx.update_editor(|editor, window, cx| {
6397 editor.add_selection_below(&Default::default(), window, cx);
6398 });
6399
6400 cx.assert_editor_state(indoc!(
6401 r#"abc
6402 def«ˇg»hi
6403
6404 jk
6405 nlm«ˇo»
6406 "#
6407 ));
6408
6409 cx.update_editor(|editor, window, cx| {
6410 editor.add_selection_above(&Default::default(), window, cx);
6411 });
6412
6413 cx.assert_editor_state(indoc!(
6414 r#"abc
6415 def«ˇg»hi
6416
6417 jk
6418 nlmo
6419 "#
6420 ));
6421
6422 cx.update_editor(|editor, window, cx| {
6423 editor.add_selection_above(&Default::default(), window, cx);
6424 });
6425
6426 cx.assert_editor_state(indoc!(
6427 r#"abc
6428 def«ˇg»hi
6429
6430 jk
6431 nlmo
6432 "#
6433 ));
6434
6435 // Change selections again
6436 cx.set_state(indoc!(
6437 r#"a«bc
6438 defgˇ»hi
6439
6440 jk
6441 nlmo
6442 "#
6443 ));
6444
6445 cx.update_editor(|editor, window, cx| {
6446 editor.add_selection_below(&Default::default(), window, cx);
6447 });
6448
6449 cx.assert_editor_state(indoc!(
6450 r#"a«bcˇ»
6451 d«efgˇ»hi
6452
6453 j«kˇ»
6454 nlmo
6455 "#
6456 ));
6457
6458 cx.update_editor(|editor, window, cx| {
6459 editor.add_selection_below(&Default::default(), window, cx);
6460 });
6461 cx.assert_editor_state(indoc!(
6462 r#"a«bcˇ»
6463 d«efgˇ»hi
6464
6465 j«kˇ»
6466 n«lmoˇ»
6467 "#
6468 ));
6469 cx.update_editor(|editor, window, cx| {
6470 editor.add_selection_above(&Default::default(), window, cx);
6471 });
6472
6473 cx.assert_editor_state(indoc!(
6474 r#"a«bcˇ»
6475 d«efgˇ»hi
6476
6477 j«kˇ»
6478 nlmo
6479 "#
6480 ));
6481
6482 // Change selections again
6483 cx.set_state(indoc!(
6484 r#"abc
6485 d«ˇefghi
6486
6487 jk
6488 nlm»o
6489 "#
6490 ));
6491
6492 cx.update_editor(|editor, window, cx| {
6493 editor.add_selection_above(&Default::default(), window, cx);
6494 });
6495
6496 cx.assert_editor_state(indoc!(
6497 r#"a«ˇbc»
6498 d«ˇef»ghi
6499
6500 j«ˇk»
6501 n«ˇlm»o
6502 "#
6503 ));
6504
6505 cx.update_editor(|editor, window, cx| {
6506 editor.add_selection_below(&Default::default(), window, cx);
6507 });
6508
6509 cx.assert_editor_state(indoc!(
6510 r#"abc
6511 d«ˇef»ghi
6512
6513 j«ˇk»
6514 n«ˇlm»o
6515 "#
6516 ));
6517}
6518
6519#[gpui::test]
6520async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
6521 init_test(cx, |_| {});
6522 let mut cx = EditorTestContext::new(cx).await;
6523
6524 cx.set_state(indoc!(
6525 r#"line onˇe
6526 liˇne two
6527 line three
6528 line four"#
6529 ));
6530
6531 cx.update_editor(|editor, window, cx| {
6532 editor.add_selection_below(&Default::default(), window, cx);
6533 });
6534
6535 // test multiple cursors expand in the same direction
6536 cx.assert_editor_state(indoc!(
6537 r#"line onˇe
6538 liˇne twˇo
6539 liˇne three
6540 line four"#
6541 ));
6542
6543 cx.update_editor(|editor, window, cx| {
6544 editor.add_selection_below(&Default::default(), window, cx);
6545 });
6546
6547 cx.update_editor(|editor, window, cx| {
6548 editor.add_selection_below(&Default::default(), window, cx);
6549 });
6550
6551 // test multiple cursors expand below overflow
6552 cx.assert_editor_state(indoc!(
6553 r#"line onˇe
6554 liˇne twˇo
6555 liˇne thˇree
6556 liˇne foˇur"#
6557 ));
6558
6559 cx.update_editor(|editor, window, cx| {
6560 editor.add_selection_above(&Default::default(), window, cx);
6561 });
6562
6563 // test multiple cursors retrieves back correctly
6564 cx.assert_editor_state(indoc!(
6565 r#"line onˇe
6566 liˇne twˇo
6567 liˇne thˇree
6568 line four"#
6569 ));
6570
6571 cx.update_editor(|editor, window, cx| {
6572 editor.add_selection_above(&Default::default(), window, cx);
6573 });
6574
6575 cx.update_editor(|editor, window, cx| {
6576 editor.add_selection_above(&Default::default(), window, cx);
6577 });
6578
6579 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
6580 cx.assert_editor_state(indoc!(
6581 r#"liˇne onˇe
6582 liˇne two
6583 line three
6584 line four"#
6585 ));
6586
6587 cx.update_editor(|editor, window, cx| {
6588 editor.undo_selection(&Default::default(), window, cx);
6589 });
6590
6591 // test undo
6592 cx.assert_editor_state(indoc!(
6593 r#"line onˇe
6594 liˇne twˇo
6595 line three
6596 line four"#
6597 ));
6598
6599 cx.update_editor(|editor, window, cx| {
6600 editor.redo_selection(&Default::default(), window, cx);
6601 });
6602
6603 // test redo
6604 cx.assert_editor_state(indoc!(
6605 r#"liˇne onˇe
6606 liˇne two
6607 line three
6608 line four"#
6609 ));
6610
6611 cx.set_state(indoc!(
6612 r#"abcd
6613 ef«ghˇ»
6614 ijkl
6615 «mˇ»nop"#
6616 ));
6617
6618 cx.update_editor(|editor, window, cx| {
6619 editor.add_selection_above(&Default::default(), window, cx);
6620 });
6621
6622 // test multiple selections expand in the same direction
6623 cx.assert_editor_state(indoc!(
6624 r#"ab«cdˇ»
6625 ef«ghˇ»
6626 «iˇ»jkl
6627 «mˇ»nop"#
6628 ));
6629
6630 cx.update_editor(|editor, window, cx| {
6631 editor.add_selection_above(&Default::default(), window, cx);
6632 });
6633
6634 // test multiple selection upward overflow
6635 cx.assert_editor_state(indoc!(
6636 r#"ab«cdˇ»
6637 «eˇ»f«ghˇ»
6638 «iˇ»jkl
6639 «mˇ»nop"#
6640 ));
6641
6642 cx.update_editor(|editor, window, cx| {
6643 editor.add_selection_below(&Default::default(), window, cx);
6644 });
6645
6646 // test multiple selection retrieves back correctly
6647 cx.assert_editor_state(indoc!(
6648 r#"abcd
6649 ef«ghˇ»
6650 «iˇ»jkl
6651 «mˇ»nop"#
6652 ));
6653
6654 cx.update_editor(|editor, window, cx| {
6655 editor.add_selection_below(&Default::default(), window, cx);
6656 });
6657
6658 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
6659 cx.assert_editor_state(indoc!(
6660 r#"abcd
6661 ef«ghˇ»
6662 ij«klˇ»
6663 «mˇ»nop"#
6664 ));
6665
6666 cx.update_editor(|editor, window, cx| {
6667 editor.undo_selection(&Default::default(), window, cx);
6668 });
6669
6670 // test undo
6671 cx.assert_editor_state(indoc!(
6672 r#"abcd
6673 ef«ghˇ»
6674 «iˇ»jkl
6675 «mˇ»nop"#
6676 ));
6677
6678 cx.update_editor(|editor, window, cx| {
6679 editor.redo_selection(&Default::default(), window, cx);
6680 });
6681
6682 // test redo
6683 cx.assert_editor_state(indoc!(
6684 r#"abcd
6685 ef«ghˇ»
6686 ij«klˇ»
6687 «mˇ»nop"#
6688 ));
6689}
6690
6691#[gpui::test]
6692async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
6693 init_test(cx, |_| {});
6694 let mut cx = EditorTestContext::new(cx).await;
6695
6696 cx.set_state(indoc!(
6697 r#"line onˇe
6698 liˇne two
6699 line three
6700 line four"#
6701 ));
6702
6703 cx.update_editor(|editor, window, cx| {
6704 editor.add_selection_below(&Default::default(), window, cx);
6705 editor.add_selection_below(&Default::default(), window, cx);
6706 editor.add_selection_below(&Default::default(), window, cx);
6707 });
6708
6709 // initial state with two multi cursor groups
6710 cx.assert_editor_state(indoc!(
6711 r#"line onˇe
6712 liˇne twˇo
6713 liˇne thˇree
6714 liˇne foˇur"#
6715 ));
6716
6717 // add single cursor in middle - simulate opt click
6718 cx.update_editor(|editor, window, cx| {
6719 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
6720 editor.begin_selection(new_cursor_point, true, 1, window, cx);
6721 editor.end_selection(window, cx);
6722 });
6723
6724 cx.assert_editor_state(indoc!(
6725 r#"line onˇe
6726 liˇne twˇo
6727 liˇneˇ thˇree
6728 liˇne foˇur"#
6729 ));
6730
6731 cx.update_editor(|editor, window, cx| {
6732 editor.add_selection_above(&Default::default(), window, cx);
6733 });
6734
6735 // test new added selection expands above and existing selection shrinks
6736 cx.assert_editor_state(indoc!(
6737 r#"line onˇe
6738 liˇneˇ twˇo
6739 liˇneˇ thˇree
6740 line four"#
6741 ));
6742
6743 cx.update_editor(|editor, window, cx| {
6744 editor.add_selection_above(&Default::default(), window, cx);
6745 });
6746
6747 // test new added selection expands above and existing selection shrinks
6748 cx.assert_editor_state(indoc!(
6749 r#"lineˇ onˇe
6750 liˇneˇ twˇo
6751 lineˇ three
6752 line four"#
6753 ));
6754
6755 // intial state with two selection groups
6756 cx.set_state(indoc!(
6757 r#"abcd
6758 ef«ghˇ»
6759 ijkl
6760 «mˇ»nop"#
6761 ));
6762
6763 cx.update_editor(|editor, window, cx| {
6764 editor.add_selection_above(&Default::default(), window, cx);
6765 editor.add_selection_above(&Default::default(), window, cx);
6766 });
6767
6768 cx.assert_editor_state(indoc!(
6769 r#"ab«cdˇ»
6770 «eˇ»f«ghˇ»
6771 «iˇ»jkl
6772 «mˇ»nop"#
6773 ));
6774
6775 // add single selection in middle - simulate opt drag
6776 cx.update_editor(|editor, window, cx| {
6777 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
6778 editor.begin_selection(new_cursor_point, true, 1, window, cx);
6779 editor.update_selection(
6780 DisplayPoint::new(DisplayRow(2), 4),
6781 0,
6782 gpui::Point::<f32>::default(),
6783 window,
6784 cx,
6785 );
6786 editor.end_selection(window, cx);
6787 });
6788
6789 cx.assert_editor_state(indoc!(
6790 r#"ab«cdˇ»
6791 «eˇ»f«ghˇ»
6792 «iˇ»jk«lˇ»
6793 «mˇ»nop"#
6794 ));
6795
6796 cx.update_editor(|editor, window, cx| {
6797 editor.add_selection_below(&Default::default(), window, cx);
6798 });
6799
6800 // test new added selection expands below, others shrinks from above
6801 cx.assert_editor_state(indoc!(
6802 r#"abcd
6803 ef«ghˇ»
6804 «iˇ»jk«lˇ»
6805 «mˇ»no«pˇ»"#
6806 ));
6807}
6808
6809#[gpui::test]
6810async fn test_select_next(cx: &mut TestAppContext) {
6811 init_test(cx, |_| {});
6812
6813 let mut cx = EditorTestContext::new(cx).await;
6814 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6815
6816 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6817 .unwrap();
6818 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6819
6820 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6821 .unwrap();
6822 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
6823
6824 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6825 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6826
6827 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6828 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
6829
6830 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6831 .unwrap();
6832 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6833
6834 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6835 .unwrap();
6836 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6837
6838 // Test selection direction should be preserved
6839 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
6840
6841 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6842 .unwrap();
6843 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
6844}
6845
6846#[gpui::test]
6847async fn test_select_all_matches(cx: &mut TestAppContext) {
6848 init_test(cx, |_| {});
6849
6850 let mut cx = EditorTestContext::new(cx).await;
6851
6852 // Test caret-only selections
6853 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6854 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6855 .unwrap();
6856 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6857
6858 // Test left-to-right selections
6859 cx.set_state("abc\n«abcˇ»\nabc");
6860 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6861 .unwrap();
6862 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
6863
6864 // Test right-to-left selections
6865 cx.set_state("abc\n«ˇabc»\nabc");
6866 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6867 .unwrap();
6868 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
6869
6870 // Test selecting whitespace with caret selection
6871 cx.set_state("abc\nˇ abc\nabc");
6872 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6873 .unwrap();
6874 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
6875
6876 // Test selecting whitespace with left-to-right selection
6877 cx.set_state("abc\n«ˇ »abc\nabc");
6878 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6879 .unwrap();
6880 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
6881
6882 // Test no matches with right-to-left selection
6883 cx.set_state("abc\n« ˇ»abc\nabc");
6884 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6885 .unwrap();
6886 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
6887
6888 // Test with a single word and clip_at_line_ends=true (#29823)
6889 cx.set_state("aˇbc");
6890 cx.update_editor(|e, window, cx| {
6891 e.set_clip_at_line_ends(true, cx);
6892 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
6893 e.set_clip_at_line_ends(false, cx);
6894 });
6895 cx.assert_editor_state("«abcˇ»");
6896}
6897
6898#[gpui::test]
6899async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
6900 init_test(cx, |_| {});
6901
6902 let mut cx = EditorTestContext::new(cx).await;
6903
6904 let large_body_1 = "\nd".repeat(200);
6905 let large_body_2 = "\ne".repeat(200);
6906
6907 cx.set_state(&format!(
6908 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
6909 ));
6910 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
6911 let scroll_position = editor.scroll_position(cx);
6912 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
6913 scroll_position
6914 });
6915
6916 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6917 .unwrap();
6918 cx.assert_editor_state(&format!(
6919 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
6920 ));
6921 let scroll_position_after_selection =
6922 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
6923 assert_eq!(
6924 initial_scroll_position, scroll_position_after_selection,
6925 "Scroll position should not change after selecting all matches"
6926 );
6927}
6928
6929#[gpui::test]
6930async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
6931 init_test(cx, |_| {});
6932
6933 let mut cx = EditorLspTestContext::new_rust(
6934 lsp::ServerCapabilities {
6935 document_formatting_provider: Some(lsp::OneOf::Left(true)),
6936 ..Default::default()
6937 },
6938 cx,
6939 )
6940 .await;
6941
6942 cx.set_state(indoc! {"
6943 line 1
6944 line 2
6945 linˇe 3
6946 line 4
6947 line 5
6948 "});
6949
6950 // Make an edit
6951 cx.update_editor(|editor, window, cx| {
6952 editor.handle_input("X", window, cx);
6953 });
6954
6955 // Move cursor to a different position
6956 cx.update_editor(|editor, window, cx| {
6957 editor.change_selections(None, window, cx, |s| {
6958 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
6959 });
6960 });
6961
6962 cx.assert_editor_state(indoc! {"
6963 line 1
6964 line 2
6965 linXe 3
6966 line 4
6967 liˇne 5
6968 "});
6969
6970 cx.lsp
6971 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
6972 Ok(Some(vec![lsp::TextEdit::new(
6973 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
6974 "PREFIX ".to_string(),
6975 )]))
6976 });
6977
6978 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
6979 .unwrap()
6980 .await
6981 .unwrap();
6982
6983 cx.assert_editor_state(indoc! {"
6984 PREFIX line 1
6985 line 2
6986 linXe 3
6987 line 4
6988 liˇne 5
6989 "});
6990
6991 // Undo formatting
6992 cx.update_editor(|editor, window, cx| {
6993 editor.undo(&Default::default(), window, cx);
6994 });
6995
6996 // Verify cursor moved back to position after edit
6997 cx.assert_editor_state(indoc! {"
6998 line 1
6999 line 2
7000 linXˇe 3
7001 line 4
7002 line 5
7003 "});
7004}
7005
7006#[gpui::test]
7007async fn test_undo_inline_completion_scrolls_to_edit_pos(cx: &mut TestAppContext) {
7008 init_test(cx, |_| {});
7009
7010 let mut cx = EditorTestContext::new(cx).await;
7011
7012 let provider = cx.new(|_| FakeInlineCompletionProvider::default());
7013 cx.update_editor(|editor, window, cx| {
7014 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
7015 });
7016
7017 cx.set_state(indoc! {"
7018 line 1
7019 line 2
7020 linˇe 3
7021 line 4
7022 line 5
7023 line 6
7024 line 7
7025 line 8
7026 line 9
7027 line 10
7028 "});
7029
7030 let snapshot = cx.buffer_snapshot();
7031 let edit_position = snapshot.anchor_after(Point::new(2, 4));
7032
7033 cx.update(|_, cx| {
7034 provider.update(cx, |provider, _| {
7035 provider.set_inline_completion(Some(inline_completion::InlineCompletion {
7036 id: None,
7037 edits: vec![(edit_position..edit_position, "X".into())],
7038 edit_preview: None,
7039 }))
7040 })
7041 });
7042
7043 cx.update_editor(|editor, window, cx| editor.update_visible_inline_completion(window, cx));
7044 cx.update_editor(|editor, window, cx| {
7045 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
7046 });
7047
7048 cx.assert_editor_state(indoc! {"
7049 line 1
7050 line 2
7051 lineXˇ 3
7052 line 4
7053 line 5
7054 line 6
7055 line 7
7056 line 8
7057 line 9
7058 line 10
7059 "});
7060
7061 cx.update_editor(|editor, window, cx| {
7062 editor.change_selections(None, window, cx, |s| {
7063 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
7064 });
7065 });
7066
7067 cx.assert_editor_state(indoc! {"
7068 line 1
7069 line 2
7070 lineX 3
7071 line 4
7072 line 5
7073 line 6
7074 line 7
7075 line 8
7076 line 9
7077 liˇne 10
7078 "});
7079
7080 cx.update_editor(|editor, window, cx| {
7081 editor.undo(&Default::default(), window, cx);
7082 });
7083
7084 cx.assert_editor_state(indoc! {"
7085 line 1
7086 line 2
7087 lineˇ 3
7088 line 4
7089 line 5
7090 line 6
7091 line 7
7092 line 8
7093 line 9
7094 line 10
7095 "});
7096}
7097
7098#[gpui::test]
7099async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
7100 init_test(cx, |_| {});
7101
7102 let mut cx = EditorTestContext::new(cx).await;
7103 cx.set_state(
7104 r#"let foo = 2;
7105lˇet foo = 2;
7106let fooˇ = 2;
7107let foo = 2;
7108let foo = ˇ2;"#,
7109 );
7110
7111 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7112 .unwrap();
7113 cx.assert_editor_state(
7114 r#"let foo = 2;
7115«letˇ» foo = 2;
7116let «fooˇ» = 2;
7117let foo = 2;
7118let foo = «2ˇ»;"#,
7119 );
7120
7121 // noop for multiple selections with different contents
7122 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7123 .unwrap();
7124 cx.assert_editor_state(
7125 r#"let foo = 2;
7126«letˇ» foo = 2;
7127let «fooˇ» = 2;
7128let foo = 2;
7129let foo = «2ˇ»;"#,
7130 );
7131
7132 // Test last selection direction should be preserved
7133 cx.set_state(
7134 r#"let foo = 2;
7135let foo = 2;
7136let «fooˇ» = 2;
7137let «ˇfoo» = 2;
7138let foo = 2;"#,
7139 );
7140
7141 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7142 .unwrap();
7143 cx.assert_editor_state(
7144 r#"let foo = 2;
7145let foo = 2;
7146let «fooˇ» = 2;
7147let «ˇfoo» = 2;
7148let «ˇfoo» = 2;"#,
7149 );
7150}
7151
7152#[gpui::test]
7153async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
7154 init_test(cx, |_| {});
7155
7156 let mut cx =
7157 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
7158
7159 cx.assert_editor_state(indoc! {"
7160 ˇbbb
7161 ccc
7162
7163 bbb
7164 ccc
7165 "});
7166 cx.dispatch_action(SelectPrevious::default());
7167 cx.assert_editor_state(indoc! {"
7168 «bbbˇ»
7169 ccc
7170
7171 bbb
7172 ccc
7173 "});
7174 cx.dispatch_action(SelectPrevious::default());
7175 cx.assert_editor_state(indoc! {"
7176 «bbbˇ»
7177 ccc
7178
7179 «bbbˇ»
7180 ccc
7181 "});
7182}
7183
7184#[gpui::test]
7185async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
7186 init_test(cx, |_| {});
7187
7188 let mut cx = EditorTestContext::new(cx).await;
7189 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
7190
7191 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7192 .unwrap();
7193 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7194
7195 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7196 .unwrap();
7197 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
7198
7199 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7200 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7201
7202 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7203 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
7204
7205 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7206 .unwrap();
7207 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
7208
7209 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7210 .unwrap();
7211 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7212}
7213
7214#[gpui::test]
7215async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
7216 init_test(cx, |_| {});
7217
7218 let mut cx = EditorTestContext::new(cx).await;
7219 cx.set_state("aˇ");
7220
7221 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7222 .unwrap();
7223 cx.assert_editor_state("«aˇ»");
7224 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7225 .unwrap();
7226 cx.assert_editor_state("«aˇ»");
7227}
7228
7229#[gpui::test]
7230async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
7231 init_test(cx, |_| {});
7232
7233 let mut cx = EditorTestContext::new(cx).await;
7234 cx.set_state(
7235 r#"let foo = 2;
7236lˇet foo = 2;
7237let fooˇ = 2;
7238let foo = 2;
7239let foo = ˇ2;"#,
7240 );
7241
7242 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7243 .unwrap();
7244 cx.assert_editor_state(
7245 r#"let foo = 2;
7246«letˇ» foo = 2;
7247let «fooˇ» = 2;
7248let foo = 2;
7249let foo = «2ˇ»;"#,
7250 );
7251
7252 // noop for multiple selections with different contents
7253 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7254 .unwrap();
7255 cx.assert_editor_state(
7256 r#"let foo = 2;
7257«letˇ» foo = 2;
7258let «fooˇ» = 2;
7259let foo = 2;
7260let foo = «2ˇ»;"#,
7261 );
7262}
7263
7264#[gpui::test]
7265async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
7266 init_test(cx, |_| {});
7267
7268 let mut cx = EditorTestContext::new(cx).await;
7269 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
7270
7271 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7272 .unwrap();
7273 // selection direction is preserved
7274 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
7275
7276 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7277 .unwrap();
7278 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
7279
7280 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7281 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
7282
7283 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7284 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
7285
7286 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7287 .unwrap();
7288 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
7289
7290 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7291 .unwrap();
7292 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
7293}
7294
7295#[gpui::test]
7296async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
7297 init_test(cx, |_| {});
7298
7299 let language = Arc::new(Language::new(
7300 LanguageConfig::default(),
7301 Some(tree_sitter_rust::LANGUAGE.into()),
7302 ));
7303
7304 let text = r#"
7305 use mod1::mod2::{mod3, mod4};
7306
7307 fn fn_1(param1: bool, param2: &str) {
7308 let var1 = "text";
7309 }
7310 "#
7311 .unindent();
7312
7313 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7314 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7315 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7316
7317 editor
7318 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7319 .await;
7320
7321 editor.update_in(cx, |editor, window, cx| {
7322 editor.change_selections(None, window, cx, |s| {
7323 s.select_display_ranges([
7324 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
7325 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
7326 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
7327 ]);
7328 });
7329 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7330 });
7331 editor.update(cx, |editor, cx| {
7332 assert_text_with_selections(
7333 editor,
7334 indoc! {r#"
7335 use mod1::mod2::{mod3, «mod4ˇ»};
7336
7337 fn fn_1«ˇ(param1: bool, param2: &str)» {
7338 let var1 = "«ˇtext»";
7339 }
7340 "#},
7341 cx,
7342 );
7343 });
7344
7345 editor.update_in(cx, |editor, window, cx| {
7346 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7347 });
7348 editor.update(cx, |editor, cx| {
7349 assert_text_with_selections(
7350 editor,
7351 indoc! {r#"
7352 use mod1::mod2::«{mod3, mod4}ˇ»;
7353
7354 «ˇfn fn_1(param1: bool, param2: &str) {
7355 let var1 = "text";
7356 }»
7357 "#},
7358 cx,
7359 );
7360 });
7361
7362 editor.update_in(cx, |editor, window, cx| {
7363 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7364 });
7365 assert_eq!(
7366 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
7367 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
7368 );
7369
7370 // Trying to expand the selected syntax node one more time has no effect.
7371 editor.update_in(cx, |editor, window, cx| {
7372 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7373 });
7374 assert_eq!(
7375 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
7376 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
7377 );
7378
7379 editor.update_in(cx, |editor, window, cx| {
7380 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7381 });
7382 editor.update(cx, |editor, cx| {
7383 assert_text_with_selections(
7384 editor,
7385 indoc! {r#"
7386 use mod1::mod2::«{mod3, mod4}ˇ»;
7387
7388 «ˇfn fn_1(param1: bool, param2: &str) {
7389 let var1 = "text";
7390 }»
7391 "#},
7392 cx,
7393 );
7394 });
7395
7396 editor.update_in(cx, |editor, window, cx| {
7397 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7398 });
7399 editor.update(cx, |editor, cx| {
7400 assert_text_with_selections(
7401 editor,
7402 indoc! {r#"
7403 use mod1::mod2::{mod3, «mod4ˇ»};
7404
7405 fn fn_1«ˇ(param1: bool, param2: &str)» {
7406 let var1 = "«ˇtext»";
7407 }
7408 "#},
7409 cx,
7410 );
7411 });
7412
7413 editor.update_in(cx, |editor, window, cx| {
7414 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7415 });
7416 editor.update(cx, |editor, cx| {
7417 assert_text_with_selections(
7418 editor,
7419 indoc! {r#"
7420 use mod1::mod2::{mod3, mo«ˇ»d4};
7421
7422 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
7423 let var1 = "te«ˇ»xt";
7424 }
7425 "#},
7426 cx,
7427 );
7428 });
7429
7430 // Trying to shrink the selected syntax node one more time has no effect.
7431 editor.update_in(cx, |editor, window, cx| {
7432 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7433 });
7434 editor.update_in(cx, |editor, _, cx| {
7435 assert_text_with_selections(
7436 editor,
7437 indoc! {r#"
7438 use mod1::mod2::{mod3, mo«ˇ»d4};
7439
7440 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
7441 let var1 = "te«ˇ»xt";
7442 }
7443 "#},
7444 cx,
7445 );
7446 });
7447
7448 // Ensure that we keep expanding the selection if the larger selection starts or ends within
7449 // a fold.
7450 editor.update_in(cx, |editor, window, cx| {
7451 editor.fold_creases(
7452 vec![
7453 Crease::simple(
7454 Point::new(0, 21)..Point::new(0, 24),
7455 FoldPlaceholder::test(),
7456 ),
7457 Crease::simple(
7458 Point::new(3, 20)..Point::new(3, 22),
7459 FoldPlaceholder::test(),
7460 ),
7461 ],
7462 true,
7463 window,
7464 cx,
7465 );
7466 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7467 });
7468 editor.update(cx, |editor, cx| {
7469 assert_text_with_selections(
7470 editor,
7471 indoc! {r#"
7472 use mod1::mod2::«{mod3, mod4}ˇ»;
7473
7474 fn fn_1«ˇ(param1: bool, param2: &str)» {
7475 let var1 = "«ˇtext»";
7476 }
7477 "#},
7478 cx,
7479 );
7480 });
7481}
7482
7483#[gpui::test]
7484async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
7485 init_test(cx, |_| {});
7486
7487 let language = Arc::new(Language::new(
7488 LanguageConfig::default(),
7489 Some(tree_sitter_rust::LANGUAGE.into()),
7490 ));
7491
7492 let text = "let a = 2;";
7493
7494 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7495 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7496 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7497
7498 editor
7499 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7500 .await;
7501
7502 // Test case 1: Cursor at end of word
7503 editor.update_in(cx, |editor, window, cx| {
7504 editor.change_selections(None, window, cx, |s| {
7505 s.select_display_ranges([
7506 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
7507 ]);
7508 });
7509 });
7510 editor.update(cx, |editor, cx| {
7511 assert_text_with_selections(editor, "let aˇ = 2;", cx);
7512 });
7513 editor.update_in(cx, |editor, window, cx| {
7514 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7515 });
7516 editor.update(cx, |editor, cx| {
7517 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
7518 });
7519 editor.update_in(cx, |editor, window, cx| {
7520 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7521 });
7522 editor.update(cx, |editor, cx| {
7523 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
7524 });
7525
7526 // Test case 2: Cursor at end of statement
7527 editor.update_in(cx, |editor, window, cx| {
7528 editor.change_selections(None, window, cx, |s| {
7529 s.select_display_ranges([
7530 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
7531 ]);
7532 });
7533 });
7534 editor.update(cx, |editor, cx| {
7535 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
7536 });
7537 editor.update_in(cx, |editor, window, cx| {
7538 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7539 });
7540 editor.update(cx, |editor, cx| {
7541 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
7542 });
7543}
7544
7545#[gpui::test]
7546async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
7547 init_test(cx, |_| {});
7548
7549 let language = Arc::new(Language::new(
7550 LanguageConfig::default(),
7551 Some(tree_sitter_rust::LANGUAGE.into()),
7552 ));
7553
7554 let text = r#"
7555 use mod1::mod2::{mod3, mod4};
7556
7557 fn fn_1(param1: bool, param2: &str) {
7558 let var1 = "hello world";
7559 }
7560 "#
7561 .unindent();
7562
7563 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7564 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7565 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7566
7567 editor
7568 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7569 .await;
7570
7571 // Test 1: Cursor on a letter of a string word
7572 editor.update_in(cx, |editor, window, cx| {
7573 editor.change_selections(None, window, cx, |s| {
7574 s.select_display_ranges([
7575 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
7576 ]);
7577 });
7578 });
7579 editor.update_in(cx, |editor, window, cx| {
7580 assert_text_with_selections(
7581 editor,
7582 indoc! {r#"
7583 use mod1::mod2::{mod3, mod4};
7584
7585 fn fn_1(param1: bool, param2: &str) {
7586 let var1 = "hˇello world";
7587 }
7588 "#},
7589 cx,
7590 );
7591 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7592 assert_text_with_selections(
7593 editor,
7594 indoc! {r#"
7595 use mod1::mod2::{mod3, mod4};
7596
7597 fn fn_1(param1: bool, param2: &str) {
7598 let var1 = "«ˇhello» world";
7599 }
7600 "#},
7601 cx,
7602 );
7603 });
7604
7605 // Test 2: Partial selection within a word
7606 editor.update_in(cx, |editor, window, cx| {
7607 editor.change_selections(None, window, cx, |s| {
7608 s.select_display_ranges([
7609 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
7610 ]);
7611 });
7612 });
7613 editor.update_in(cx, |editor, window, cx| {
7614 assert_text_with_selections(
7615 editor,
7616 indoc! {r#"
7617 use mod1::mod2::{mod3, mod4};
7618
7619 fn fn_1(param1: bool, param2: &str) {
7620 let var1 = "h«elˇ»lo world";
7621 }
7622 "#},
7623 cx,
7624 );
7625 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7626 assert_text_with_selections(
7627 editor,
7628 indoc! {r#"
7629 use mod1::mod2::{mod3, mod4};
7630
7631 fn fn_1(param1: bool, param2: &str) {
7632 let var1 = "«ˇhello» world";
7633 }
7634 "#},
7635 cx,
7636 );
7637 });
7638
7639 // Test 3: Complete word already selected
7640 editor.update_in(cx, |editor, window, cx| {
7641 editor.change_selections(None, window, cx, |s| {
7642 s.select_display_ranges([
7643 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
7644 ]);
7645 });
7646 });
7647 editor.update_in(cx, |editor, window, cx| {
7648 assert_text_with_selections(
7649 editor,
7650 indoc! {r#"
7651 use mod1::mod2::{mod3, mod4};
7652
7653 fn fn_1(param1: bool, param2: &str) {
7654 let var1 = "«helloˇ» world";
7655 }
7656 "#},
7657 cx,
7658 );
7659 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7660 assert_text_with_selections(
7661 editor,
7662 indoc! {r#"
7663 use mod1::mod2::{mod3, mod4};
7664
7665 fn fn_1(param1: bool, param2: &str) {
7666 let var1 = "«hello worldˇ»";
7667 }
7668 "#},
7669 cx,
7670 );
7671 });
7672
7673 // Test 4: Selection spanning across words
7674 editor.update_in(cx, |editor, window, cx| {
7675 editor.change_selections(None, window, cx, |s| {
7676 s.select_display_ranges([
7677 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
7678 ]);
7679 });
7680 });
7681 editor.update_in(cx, |editor, window, cx| {
7682 assert_text_with_selections(
7683 editor,
7684 indoc! {r#"
7685 use mod1::mod2::{mod3, mod4};
7686
7687 fn fn_1(param1: bool, param2: &str) {
7688 let var1 = "hel«lo woˇ»rld";
7689 }
7690 "#},
7691 cx,
7692 );
7693 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7694 assert_text_with_selections(
7695 editor,
7696 indoc! {r#"
7697 use mod1::mod2::{mod3, mod4};
7698
7699 fn fn_1(param1: bool, param2: &str) {
7700 let var1 = "«ˇhello world»";
7701 }
7702 "#},
7703 cx,
7704 );
7705 });
7706
7707 // Test 5: Expansion beyond string
7708 editor.update_in(cx, |editor, window, cx| {
7709 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7710 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7711 assert_text_with_selections(
7712 editor,
7713 indoc! {r#"
7714 use mod1::mod2::{mod3, mod4};
7715
7716 fn fn_1(param1: bool, param2: &str) {
7717 «ˇlet var1 = "hello world";»
7718 }
7719 "#},
7720 cx,
7721 );
7722 });
7723}
7724
7725#[gpui::test]
7726async fn test_fold_function_bodies(cx: &mut TestAppContext) {
7727 init_test(cx, |_| {});
7728
7729 let base_text = r#"
7730 impl A {
7731 // this is an uncommitted comment
7732
7733 fn b() {
7734 c();
7735 }
7736
7737 // this is another uncommitted comment
7738
7739 fn d() {
7740 // e
7741 // f
7742 }
7743 }
7744
7745 fn g() {
7746 // h
7747 }
7748 "#
7749 .unindent();
7750
7751 let text = r#"
7752 ˇimpl A {
7753
7754 fn b() {
7755 c();
7756 }
7757
7758 fn d() {
7759 // e
7760 // f
7761 }
7762 }
7763
7764 fn g() {
7765 // h
7766 }
7767 "#
7768 .unindent();
7769
7770 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
7771 cx.set_state(&text);
7772 cx.set_head_text(&base_text);
7773 cx.update_editor(|editor, window, cx| {
7774 editor.expand_all_diff_hunks(&Default::default(), window, cx);
7775 });
7776
7777 cx.assert_state_with_diff(
7778 "
7779 ˇimpl A {
7780 - // this is an uncommitted comment
7781
7782 fn b() {
7783 c();
7784 }
7785
7786 - // this is another uncommitted comment
7787 -
7788 fn d() {
7789 // e
7790 // f
7791 }
7792 }
7793
7794 fn g() {
7795 // h
7796 }
7797 "
7798 .unindent(),
7799 );
7800
7801 let expected_display_text = "
7802 impl A {
7803 // this is an uncommitted comment
7804
7805 fn b() {
7806 ⋯
7807 }
7808
7809 // this is another uncommitted comment
7810
7811 fn d() {
7812 ⋯
7813 }
7814 }
7815
7816 fn g() {
7817 ⋯
7818 }
7819 "
7820 .unindent();
7821
7822 cx.update_editor(|editor, window, cx| {
7823 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
7824 assert_eq!(editor.display_text(cx), expected_display_text);
7825 });
7826}
7827
7828#[gpui::test]
7829async fn test_autoindent(cx: &mut TestAppContext) {
7830 init_test(cx, |_| {});
7831
7832 let language = Arc::new(
7833 Language::new(
7834 LanguageConfig {
7835 brackets: BracketPairConfig {
7836 pairs: vec![
7837 BracketPair {
7838 start: "{".to_string(),
7839 end: "}".to_string(),
7840 close: false,
7841 surround: false,
7842 newline: true,
7843 },
7844 BracketPair {
7845 start: "(".to_string(),
7846 end: ")".to_string(),
7847 close: false,
7848 surround: false,
7849 newline: true,
7850 },
7851 ],
7852 ..Default::default()
7853 },
7854 ..Default::default()
7855 },
7856 Some(tree_sitter_rust::LANGUAGE.into()),
7857 )
7858 .with_indents_query(
7859 r#"
7860 (_ "(" ")" @end) @indent
7861 (_ "{" "}" @end) @indent
7862 "#,
7863 )
7864 .unwrap(),
7865 );
7866
7867 let text = "fn a() {}";
7868
7869 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7870 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7871 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7872 editor
7873 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7874 .await;
7875
7876 editor.update_in(cx, |editor, window, cx| {
7877 editor.change_selections(None, window, cx, |s| s.select_ranges([5..5, 8..8, 9..9]));
7878 editor.newline(&Newline, window, cx);
7879 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
7880 assert_eq!(
7881 editor.selections.ranges(cx),
7882 &[
7883 Point::new(1, 4)..Point::new(1, 4),
7884 Point::new(3, 4)..Point::new(3, 4),
7885 Point::new(5, 0)..Point::new(5, 0)
7886 ]
7887 );
7888 });
7889}
7890
7891#[gpui::test]
7892async fn test_autoindent_selections(cx: &mut TestAppContext) {
7893 init_test(cx, |_| {});
7894
7895 {
7896 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
7897 cx.set_state(indoc! {"
7898 impl A {
7899
7900 fn b() {}
7901
7902 «fn c() {
7903
7904 }ˇ»
7905 }
7906 "});
7907
7908 cx.update_editor(|editor, window, cx| {
7909 editor.autoindent(&Default::default(), window, cx);
7910 });
7911
7912 cx.assert_editor_state(indoc! {"
7913 impl A {
7914
7915 fn b() {}
7916
7917 «fn c() {
7918
7919 }ˇ»
7920 }
7921 "});
7922 }
7923
7924 {
7925 let mut cx = EditorTestContext::new_multibuffer(
7926 cx,
7927 [indoc! { "
7928 impl A {
7929 «
7930 // a
7931 fn b(){}
7932 »
7933 «
7934 }
7935 fn c(){}
7936 »
7937 "}],
7938 );
7939
7940 let buffer = cx.update_editor(|editor, _, cx| {
7941 let buffer = editor.buffer().update(cx, |buffer, _| {
7942 buffer.all_buffers().iter().next().unwrap().clone()
7943 });
7944 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7945 buffer
7946 });
7947
7948 cx.run_until_parked();
7949 cx.update_editor(|editor, window, cx| {
7950 editor.select_all(&Default::default(), window, cx);
7951 editor.autoindent(&Default::default(), window, cx)
7952 });
7953 cx.run_until_parked();
7954
7955 cx.update(|_, cx| {
7956 assert_eq!(
7957 buffer.read(cx).text(),
7958 indoc! { "
7959 impl A {
7960
7961 // a
7962 fn b(){}
7963
7964
7965 }
7966 fn c(){}
7967
7968 " }
7969 )
7970 });
7971 }
7972}
7973
7974#[gpui::test]
7975async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
7976 init_test(cx, |_| {});
7977
7978 let mut cx = EditorTestContext::new(cx).await;
7979
7980 let language = Arc::new(Language::new(
7981 LanguageConfig {
7982 brackets: BracketPairConfig {
7983 pairs: vec![
7984 BracketPair {
7985 start: "{".to_string(),
7986 end: "}".to_string(),
7987 close: true,
7988 surround: true,
7989 newline: true,
7990 },
7991 BracketPair {
7992 start: "(".to_string(),
7993 end: ")".to_string(),
7994 close: true,
7995 surround: true,
7996 newline: true,
7997 },
7998 BracketPair {
7999 start: "/*".to_string(),
8000 end: " */".to_string(),
8001 close: true,
8002 surround: true,
8003 newline: true,
8004 },
8005 BracketPair {
8006 start: "[".to_string(),
8007 end: "]".to_string(),
8008 close: false,
8009 surround: false,
8010 newline: true,
8011 },
8012 BracketPair {
8013 start: "\"".to_string(),
8014 end: "\"".to_string(),
8015 close: true,
8016 surround: true,
8017 newline: false,
8018 },
8019 BracketPair {
8020 start: "<".to_string(),
8021 end: ">".to_string(),
8022 close: false,
8023 surround: true,
8024 newline: true,
8025 },
8026 ],
8027 ..Default::default()
8028 },
8029 autoclose_before: "})]".to_string(),
8030 ..Default::default()
8031 },
8032 Some(tree_sitter_rust::LANGUAGE.into()),
8033 ));
8034
8035 cx.language_registry().add(language.clone());
8036 cx.update_buffer(|buffer, cx| {
8037 buffer.set_language(Some(language), cx);
8038 });
8039
8040 cx.set_state(
8041 &r#"
8042 🏀ˇ
8043 εˇ
8044 ❤️ˇ
8045 "#
8046 .unindent(),
8047 );
8048
8049 // autoclose multiple nested brackets at multiple cursors
8050 cx.update_editor(|editor, window, cx| {
8051 editor.handle_input("{", window, cx);
8052 editor.handle_input("{", window, cx);
8053 editor.handle_input("{", window, cx);
8054 });
8055 cx.assert_editor_state(
8056 &"
8057 🏀{{{ˇ}}}
8058 ε{{{ˇ}}}
8059 ❤️{{{ˇ}}}
8060 "
8061 .unindent(),
8062 );
8063
8064 // insert a different closing bracket
8065 cx.update_editor(|editor, window, cx| {
8066 editor.handle_input(")", window, cx);
8067 });
8068 cx.assert_editor_state(
8069 &"
8070 🏀{{{)ˇ}}}
8071 ε{{{)ˇ}}}
8072 ❤️{{{)ˇ}}}
8073 "
8074 .unindent(),
8075 );
8076
8077 // skip over the auto-closed brackets when typing a closing bracket
8078 cx.update_editor(|editor, window, cx| {
8079 editor.move_right(&MoveRight, window, cx);
8080 editor.handle_input("}", window, cx);
8081 editor.handle_input("}", window, cx);
8082 editor.handle_input("}", window, cx);
8083 });
8084 cx.assert_editor_state(
8085 &"
8086 🏀{{{)}}}}ˇ
8087 ε{{{)}}}}ˇ
8088 ❤️{{{)}}}}ˇ
8089 "
8090 .unindent(),
8091 );
8092
8093 // autoclose multi-character pairs
8094 cx.set_state(
8095 &"
8096 ˇ
8097 ˇ
8098 "
8099 .unindent(),
8100 );
8101 cx.update_editor(|editor, window, cx| {
8102 editor.handle_input("/", window, cx);
8103 editor.handle_input("*", window, cx);
8104 });
8105 cx.assert_editor_state(
8106 &"
8107 /*ˇ */
8108 /*ˇ */
8109 "
8110 .unindent(),
8111 );
8112
8113 // one cursor autocloses a multi-character pair, one cursor
8114 // does not autoclose.
8115 cx.set_state(
8116 &"
8117 /ˇ
8118 ˇ
8119 "
8120 .unindent(),
8121 );
8122 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
8123 cx.assert_editor_state(
8124 &"
8125 /*ˇ */
8126 *ˇ
8127 "
8128 .unindent(),
8129 );
8130
8131 // Don't autoclose if the next character isn't whitespace and isn't
8132 // listed in the language's "autoclose_before" section.
8133 cx.set_state("ˇa b");
8134 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8135 cx.assert_editor_state("{ˇa b");
8136
8137 // Don't autoclose if `close` is false for the bracket pair
8138 cx.set_state("ˇ");
8139 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
8140 cx.assert_editor_state("[ˇ");
8141
8142 // Surround with brackets if text is selected
8143 cx.set_state("«aˇ» b");
8144 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8145 cx.assert_editor_state("{«aˇ»} b");
8146
8147 // Autoclose when not immediately after a word character
8148 cx.set_state("a ˇ");
8149 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8150 cx.assert_editor_state("a \"ˇ\"");
8151
8152 // Autoclose pair where the start and end characters are the same
8153 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8154 cx.assert_editor_state("a \"\"ˇ");
8155
8156 // Don't autoclose when immediately after a word character
8157 cx.set_state("aˇ");
8158 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8159 cx.assert_editor_state("a\"ˇ");
8160
8161 // Do autoclose when after a non-word character
8162 cx.set_state("{ˇ");
8163 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8164 cx.assert_editor_state("{\"ˇ\"");
8165
8166 // Non identical pairs autoclose regardless of preceding character
8167 cx.set_state("aˇ");
8168 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8169 cx.assert_editor_state("a{ˇ}");
8170
8171 // Don't autoclose pair if autoclose is disabled
8172 cx.set_state("ˇ");
8173 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
8174 cx.assert_editor_state("<ˇ");
8175
8176 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
8177 cx.set_state("«aˇ» b");
8178 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
8179 cx.assert_editor_state("<«aˇ»> b");
8180}
8181
8182#[gpui::test]
8183async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
8184 init_test(cx, |settings| {
8185 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
8186 });
8187
8188 let mut cx = EditorTestContext::new(cx).await;
8189
8190 let language = Arc::new(Language::new(
8191 LanguageConfig {
8192 brackets: BracketPairConfig {
8193 pairs: vec![
8194 BracketPair {
8195 start: "{".to_string(),
8196 end: "}".to_string(),
8197 close: true,
8198 surround: true,
8199 newline: true,
8200 },
8201 BracketPair {
8202 start: "(".to_string(),
8203 end: ")".to_string(),
8204 close: true,
8205 surround: true,
8206 newline: true,
8207 },
8208 BracketPair {
8209 start: "[".to_string(),
8210 end: "]".to_string(),
8211 close: false,
8212 surround: false,
8213 newline: true,
8214 },
8215 ],
8216 ..Default::default()
8217 },
8218 autoclose_before: "})]".to_string(),
8219 ..Default::default()
8220 },
8221 Some(tree_sitter_rust::LANGUAGE.into()),
8222 ));
8223
8224 cx.language_registry().add(language.clone());
8225 cx.update_buffer(|buffer, cx| {
8226 buffer.set_language(Some(language), cx);
8227 });
8228
8229 cx.set_state(
8230 &"
8231 ˇ
8232 ˇ
8233 ˇ
8234 "
8235 .unindent(),
8236 );
8237
8238 // ensure only matching closing brackets are skipped over
8239 cx.update_editor(|editor, window, cx| {
8240 editor.handle_input("}", window, cx);
8241 editor.move_left(&MoveLeft, window, cx);
8242 editor.handle_input(")", window, cx);
8243 editor.move_left(&MoveLeft, window, cx);
8244 });
8245 cx.assert_editor_state(
8246 &"
8247 ˇ)}
8248 ˇ)}
8249 ˇ)}
8250 "
8251 .unindent(),
8252 );
8253
8254 // skip-over closing brackets at multiple cursors
8255 cx.update_editor(|editor, window, cx| {
8256 editor.handle_input(")", window, cx);
8257 editor.handle_input("}", window, cx);
8258 });
8259 cx.assert_editor_state(
8260 &"
8261 )}ˇ
8262 )}ˇ
8263 )}ˇ
8264 "
8265 .unindent(),
8266 );
8267
8268 // ignore non-close brackets
8269 cx.update_editor(|editor, window, cx| {
8270 editor.handle_input("]", window, cx);
8271 editor.move_left(&MoveLeft, window, cx);
8272 editor.handle_input("]", window, cx);
8273 });
8274 cx.assert_editor_state(
8275 &"
8276 )}]ˇ]
8277 )}]ˇ]
8278 )}]ˇ]
8279 "
8280 .unindent(),
8281 );
8282}
8283
8284#[gpui::test]
8285async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
8286 init_test(cx, |_| {});
8287
8288 let mut cx = EditorTestContext::new(cx).await;
8289
8290 let html_language = Arc::new(
8291 Language::new(
8292 LanguageConfig {
8293 name: "HTML".into(),
8294 brackets: BracketPairConfig {
8295 pairs: vec![
8296 BracketPair {
8297 start: "<".into(),
8298 end: ">".into(),
8299 close: true,
8300 ..Default::default()
8301 },
8302 BracketPair {
8303 start: "{".into(),
8304 end: "}".into(),
8305 close: true,
8306 ..Default::default()
8307 },
8308 BracketPair {
8309 start: "(".into(),
8310 end: ")".into(),
8311 close: true,
8312 ..Default::default()
8313 },
8314 ],
8315 ..Default::default()
8316 },
8317 autoclose_before: "})]>".into(),
8318 ..Default::default()
8319 },
8320 Some(tree_sitter_html::LANGUAGE.into()),
8321 )
8322 .with_injection_query(
8323 r#"
8324 (script_element
8325 (raw_text) @injection.content
8326 (#set! injection.language "javascript"))
8327 "#,
8328 )
8329 .unwrap(),
8330 );
8331
8332 let javascript_language = Arc::new(Language::new(
8333 LanguageConfig {
8334 name: "JavaScript".into(),
8335 brackets: BracketPairConfig {
8336 pairs: vec![
8337 BracketPair {
8338 start: "/*".into(),
8339 end: " */".into(),
8340 close: true,
8341 ..Default::default()
8342 },
8343 BracketPair {
8344 start: "{".into(),
8345 end: "}".into(),
8346 close: true,
8347 ..Default::default()
8348 },
8349 BracketPair {
8350 start: "(".into(),
8351 end: ")".into(),
8352 close: true,
8353 ..Default::default()
8354 },
8355 ],
8356 ..Default::default()
8357 },
8358 autoclose_before: "})]>".into(),
8359 ..Default::default()
8360 },
8361 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
8362 ));
8363
8364 cx.language_registry().add(html_language.clone());
8365 cx.language_registry().add(javascript_language.clone());
8366
8367 cx.update_buffer(|buffer, cx| {
8368 buffer.set_language(Some(html_language), cx);
8369 });
8370
8371 cx.set_state(
8372 &r#"
8373 <body>ˇ
8374 <script>
8375 var x = 1;ˇ
8376 </script>
8377 </body>ˇ
8378 "#
8379 .unindent(),
8380 );
8381
8382 // Precondition: different languages are active at different locations.
8383 cx.update_editor(|editor, window, cx| {
8384 let snapshot = editor.snapshot(window, cx);
8385 let cursors = editor.selections.ranges::<usize>(cx);
8386 let languages = cursors
8387 .iter()
8388 .map(|c| snapshot.language_at(c.start).unwrap().name())
8389 .collect::<Vec<_>>();
8390 assert_eq!(
8391 languages,
8392 &["HTML".into(), "JavaScript".into(), "HTML".into()]
8393 );
8394 });
8395
8396 // Angle brackets autoclose in HTML, but not JavaScript.
8397 cx.update_editor(|editor, window, cx| {
8398 editor.handle_input("<", window, cx);
8399 editor.handle_input("a", window, cx);
8400 });
8401 cx.assert_editor_state(
8402 &r#"
8403 <body><aˇ>
8404 <script>
8405 var x = 1;<aˇ
8406 </script>
8407 </body><aˇ>
8408 "#
8409 .unindent(),
8410 );
8411
8412 // Curly braces and parens autoclose in both HTML and JavaScript.
8413 cx.update_editor(|editor, window, cx| {
8414 editor.handle_input(" b=", window, cx);
8415 editor.handle_input("{", window, cx);
8416 editor.handle_input("c", window, cx);
8417 editor.handle_input("(", window, cx);
8418 });
8419 cx.assert_editor_state(
8420 &r#"
8421 <body><a b={c(ˇ)}>
8422 <script>
8423 var x = 1;<a b={c(ˇ)}
8424 </script>
8425 </body><a b={c(ˇ)}>
8426 "#
8427 .unindent(),
8428 );
8429
8430 // Brackets that were already autoclosed are skipped.
8431 cx.update_editor(|editor, window, cx| {
8432 editor.handle_input(")", window, cx);
8433 editor.handle_input("d", window, cx);
8434 editor.handle_input("}", window, cx);
8435 });
8436 cx.assert_editor_state(
8437 &r#"
8438 <body><a b={c()d}ˇ>
8439 <script>
8440 var x = 1;<a b={c()d}ˇ
8441 </script>
8442 </body><a b={c()d}ˇ>
8443 "#
8444 .unindent(),
8445 );
8446 cx.update_editor(|editor, window, cx| {
8447 editor.handle_input(">", window, cx);
8448 });
8449 cx.assert_editor_state(
8450 &r#"
8451 <body><a b={c()d}>ˇ
8452 <script>
8453 var x = 1;<a b={c()d}>ˇ
8454 </script>
8455 </body><a b={c()d}>ˇ
8456 "#
8457 .unindent(),
8458 );
8459
8460 // Reset
8461 cx.set_state(
8462 &r#"
8463 <body>ˇ
8464 <script>
8465 var x = 1;ˇ
8466 </script>
8467 </body>ˇ
8468 "#
8469 .unindent(),
8470 );
8471
8472 cx.update_editor(|editor, window, cx| {
8473 editor.handle_input("<", window, cx);
8474 });
8475 cx.assert_editor_state(
8476 &r#"
8477 <body><ˇ>
8478 <script>
8479 var x = 1;<ˇ
8480 </script>
8481 </body><ˇ>
8482 "#
8483 .unindent(),
8484 );
8485
8486 // When backspacing, the closing angle brackets are removed.
8487 cx.update_editor(|editor, window, cx| {
8488 editor.backspace(&Backspace, window, cx);
8489 });
8490 cx.assert_editor_state(
8491 &r#"
8492 <body>ˇ
8493 <script>
8494 var x = 1;ˇ
8495 </script>
8496 </body>ˇ
8497 "#
8498 .unindent(),
8499 );
8500
8501 // Block comments autoclose in JavaScript, but not HTML.
8502 cx.update_editor(|editor, window, cx| {
8503 editor.handle_input("/", window, cx);
8504 editor.handle_input("*", window, cx);
8505 });
8506 cx.assert_editor_state(
8507 &r#"
8508 <body>/*ˇ
8509 <script>
8510 var x = 1;/*ˇ */
8511 </script>
8512 </body>/*ˇ
8513 "#
8514 .unindent(),
8515 );
8516}
8517
8518#[gpui::test]
8519async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
8520 init_test(cx, |_| {});
8521
8522 let mut cx = EditorTestContext::new(cx).await;
8523
8524 let rust_language = Arc::new(
8525 Language::new(
8526 LanguageConfig {
8527 name: "Rust".into(),
8528 brackets: serde_json::from_value(json!([
8529 { "start": "{", "end": "}", "close": true, "newline": true },
8530 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
8531 ]))
8532 .unwrap(),
8533 autoclose_before: "})]>".into(),
8534 ..Default::default()
8535 },
8536 Some(tree_sitter_rust::LANGUAGE.into()),
8537 )
8538 .with_override_query("(string_literal) @string")
8539 .unwrap(),
8540 );
8541
8542 cx.language_registry().add(rust_language.clone());
8543 cx.update_buffer(|buffer, cx| {
8544 buffer.set_language(Some(rust_language), cx);
8545 });
8546
8547 cx.set_state(
8548 &r#"
8549 let x = ˇ
8550 "#
8551 .unindent(),
8552 );
8553
8554 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
8555 cx.update_editor(|editor, window, cx| {
8556 editor.handle_input("\"", window, cx);
8557 });
8558 cx.assert_editor_state(
8559 &r#"
8560 let x = "ˇ"
8561 "#
8562 .unindent(),
8563 );
8564
8565 // Inserting another quotation mark. The cursor moves across the existing
8566 // automatically-inserted quotation mark.
8567 cx.update_editor(|editor, window, cx| {
8568 editor.handle_input("\"", window, cx);
8569 });
8570 cx.assert_editor_state(
8571 &r#"
8572 let x = ""ˇ
8573 "#
8574 .unindent(),
8575 );
8576
8577 // Reset
8578 cx.set_state(
8579 &r#"
8580 let x = ˇ
8581 "#
8582 .unindent(),
8583 );
8584
8585 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
8586 cx.update_editor(|editor, window, cx| {
8587 editor.handle_input("\"", window, cx);
8588 editor.handle_input(" ", window, cx);
8589 editor.move_left(&Default::default(), window, cx);
8590 editor.handle_input("\\", window, cx);
8591 editor.handle_input("\"", window, cx);
8592 });
8593 cx.assert_editor_state(
8594 &r#"
8595 let x = "\"ˇ "
8596 "#
8597 .unindent(),
8598 );
8599
8600 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
8601 // mark. Nothing is inserted.
8602 cx.update_editor(|editor, window, cx| {
8603 editor.move_right(&Default::default(), window, cx);
8604 editor.handle_input("\"", window, cx);
8605 });
8606 cx.assert_editor_state(
8607 &r#"
8608 let x = "\" "ˇ
8609 "#
8610 .unindent(),
8611 );
8612}
8613
8614#[gpui::test]
8615async fn test_surround_with_pair(cx: &mut TestAppContext) {
8616 init_test(cx, |_| {});
8617
8618 let language = Arc::new(Language::new(
8619 LanguageConfig {
8620 brackets: BracketPairConfig {
8621 pairs: vec![
8622 BracketPair {
8623 start: "{".to_string(),
8624 end: "}".to_string(),
8625 close: true,
8626 surround: true,
8627 newline: true,
8628 },
8629 BracketPair {
8630 start: "/* ".to_string(),
8631 end: "*/".to_string(),
8632 close: true,
8633 surround: true,
8634 ..Default::default()
8635 },
8636 ],
8637 ..Default::default()
8638 },
8639 ..Default::default()
8640 },
8641 Some(tree_sitter_rust::LANGUAGE.into()),
8642 ));
8643
8644 let text = r#"
8645 a
8646 b
8647 c
8648 "#
8649 .unindent();
8650
8651 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8652 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8653 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8654 editor
8655 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8656 .await;
8657
8658 editor.update_in(cx, |editor, window, cx| {
8659 editor.change_selections(None, window, cx, |s| {
8660 s.select_display_ranges([
8661 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8662 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8663 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
8664 ])
8665 });
8666
8667 editor.handle_input("{", window, cx);
8668 editor.handle_input("{", window, cx);
8669 editor.handle_input("{", window, cx);
8670 assert_eq!(
8671 editor.text(cx),
8672 "
8673 {{{a}}}
8674 {{{b}}}
8675 {{{c}}}
8676 "
8677 .unindent()
8678 );
8679 assert_eq!(
8680 editor.selections.display_ranges(cx),
8681 [
8682 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
8683 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
8684 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
8685 ]
8686 );
8687
8688 editor.undo(&Undo, window, cx);
8689 editor.undo(&Undo, window, cx);
8690 editor.undo(&Undo, window, cx);
8691 assert_eq!(
8692 editor.text(cx),
8693 "
8694 a
8695 b
8696 c
8697 "
8698 .unindent()
8699 );
8700 assert_eq!(
8701 editor.selections.display_ranges(cx),
8702 [
8703 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8704 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8705 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
8706 ]
8707 );
8708
8709 // Ensure inserting the first character of a multi-byte bracket pair
8710 // doesn't surround the selections with the bracket.
8711 editor.handle_input("/", window, cx);
8712 assert_eq!(
8713 editor.text(cx),
8714 "
8715 /
8716 /
8717 /
8718 "
8719 .unindent()
8720 );
8721 assert_eq!(
8722 editor.selections.display_ranges(cx),
8723 [
8724 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
8725 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
8726 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
8727 ]
8728 );
8729
8730 editor.undo(&Undo, window, cx);
8731 assert_eq!(
8732 editor.text(cx),
8733 "
8734 a
8735 b
8736 c
8737 "
8738 .unindent()
8739 );
8740 assert_eq!(
8741 editor.selections.display_ranges(cx),
8742 [
8743 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8744 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8745 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
8746 ]
8747 );
8748
8749 // Ensure inserting the last character of a multi-byte bracket pair
8750 // doesn't surround the selections with the bracket.
8751 editor.handle_input("*", window, cx);
8752 assert_eq!(
8753 editor.text(cx),
8754 "
8755 *
8756 *
8757 *
8758 "
8759 .unindent()
8760 );
8761 assert_eq!(
8762 editor.selections.display_ranges(cx),
8763 [
8764 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
8765 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
8766 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
8767 ]
8768 );
8769 });
8770}
8771
8772#[gpui::test]
8773async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
8774 init_test(cx, |_| {});
8775
8776 let language = Arc::new(Language::new(
8777 LanguageConfig {
8778 brackets: BracketPairConfig {
8779 pairs: vec![BracketPair {
8780 start: "{".to_string(),
8781 end: "}".to_string(),
8782 close: true,
8783 surround: true,
8784 newline: true,
8785 }],
8786 ..Default::default()
8787 },
8788 autoclose_before: "}".to_string(),
8789 ..Default::default()
8790 },
8791 Some(tree_sitter_rust::LANGUAGE.into()),
8792 ));
8793
8794 let text = r#"
8795 a
8796 b
8797 c
8798 "#
8799 .unindent();
8800
8801 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8802 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8803 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8804 editor
8805 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8806 .await;
8807
8808 editor.update_in(cx, |editor, window, cx| {
8809 editor.change_selections(None, window, cx, |s| {
8810 s.select_ranges([
8811 Point::new(0, 1)..Point::new(0, 1),
8812 Point::new(1, 1)..Point::new(1, 1),
8813 Point::new(2, 1)..Point::new(2, 1),
8814 ])
8815 });
8816
8817 editor.handle_input("{", window, cx);
8818 editor.handle_input("{", window, cx);
8819 editor.handle_input("_", window, cx);
8820 assert_eq!(
8821 editor.text(cx),
8822 "
8823 a{{_}}
8824 b{{_}}
8825 c{{_}}
8826 "
8827 .unindent()
8828 );
8829 assert_eq!(
8830 editor.selections.ranges::<Point>(cx),
8831 [
8832 Point::new(0, 4)..Point::new(0, 4),
8833 Point::new(1, 4)..Point::new(1, 4),
8834 Point::new(2, 4)..Point::new(2, 4)
8835 ]
8836 );
8837
8838 editor.backspace(&Default::default(), window, cx);
8839 editor.backspace(&Default::default(), window, cx);
8840 assert_eq!(
8841 editor.text(cx),
8842 "
8843 a{}
8844 b{}
8845 c{}
8846 "
8847 .unindent()
8848 );
8849 assert_eq!(
8850 editor.selections.ranges::<Point>(cx),
8851 [
8852 Point::new(0, 2)..Point::new(0, 2),
8853 Point::new(1, 2)..Point::new(1, 2),
8854 Point::new(2, 2)..Point::new(2, 2)
8855 ]
8856 );
8857
8858 editor.delete_to_previous_word_start(&Default::default(), window, cx);
8859 assert_eq!(
8860 editor.text(cx),
8861 "
8862 a
8863 b
8864 c
8865 "
8866 .unindent()
8867 );
8868 assert_eq!(
8869 editor.selections.ranges::<Point>(cx),
8870 [
8871 Point::new(0, 1)..Point::new(0, 1),
8872 Point::new(1, 1)..Point::new(1, 1),
8873 Point::new(2, 1)..Point::new(2, 1)
8874 ]
8875 );
8876 });
8877}
8878
8879#[gpui::test]
8880async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
8881 init_test(cx, |settings| {
8882 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
8883 });
8884
8885 let mut cx = EditorTestContext::new(cx).await;
8886
8887 let language = Arc::new(Language::new(
8888 LanguageConfig {
8889 brackets: BracketPairConfig {
8890 pairs: vec![
8891 BracketPair {
8892 start: "{".to_string(),
8893 end: "}".to_string(),
8894 close: true,
8895 surround: true,
8896 newline: true,
8897 },
8898 BracketPair {
8899 start: "(".to_string(),
8900 end: ")".to_string(),
8901 close: true,
8902 surround: true,
8903 newline: true,
8904 },
8905 BracketPair {
8906 start: "[".to_string(),
8907 end: "]".to_string(),
8908 close: false,
8909 surround: true,
8910 newline: true,
8911 },
8912 ],
8913 ..Default::default()
8914 },
8915 autoclose_before: "})]".to_string(),
8916 ..Default::default()
8917 },
8918 Some(tree_sitter_rust::LANGUAGE.into()),
8919 ));
8920
8921 cx.language_registry().add(language.clone());
8922 cx.update_buffer(|buffer, cx| {
8923 buffer.set_language(Some(language), cx);
8924 });
8925
8926 cx.set_state(
8927 &"
8928 {(ˇ)}
8929 [[ˇ]]
8930 {(ˇ)}
8931 "
8932 .unindent(),
8933 );
8934
8935 cx.update_editor(|editor, window, cx| {
8936 editor.backspace(&Default::default(), window, cx);
8937 editor.backspace(&Default::default(), window, cx);
8938 });
8939
8940 cx.assert_editor_state(
8941 &"
8942 ˇ
8943 ˇ]]
8944 ˇ
8945 "
8946 .unindent(),
8947 );
8948
8949 cx.update_editor(|editor, window, cx| {
8950 editor.handle_input("{", window, cx);
8951 editor.handle_input("{", window, cx);
8952 editor.move_right(&MoveRight, window, cx);
8953 editor.move_right(&MoveRight, window, cx);
8954 editor.move_left(&MoveLeft, window, cx);
8955 editor.move_left(&MoveLeft, window, cx);
8956 editor.backspace(&Default::default(), window, cx);
8957 });
8958
8959 cx.assert_editor_state(
8960 &"
8961 {ˇ}
8962 {ˇ}]]
8963 {ˇ}
8964 "
8965 .unindent(),
8966 );
8967
8968 cx.update_editor(|editor, window, cx| {
8969 editor.backspace(&Default::default(), window, cx);
8970 });
8971
8972 cx.assert_editor_state(
8973 &"
8974 ˇ
8975 ˇ]]
8976 ˇ
8977 "
8978 .unindent(),
8979 );
8980}
8981
8982#[gpui::test]
8983async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
8984 init_test(cx, |_| {});
8985
8986 let language = Arc::new(Language::new(
8987 LanguageConfig::default(),
8988 Some(tree_sitter_rust::LANGUAGE.into()),
8989 ));
8990
8991 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
8992 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8993 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8994 editor
8995 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8996 .await;
8997
8998 editor.update_in(cx, |editor, window, cx| {
8999 editor.set_auto_replace_emoji_shortcode(true);
9000
9001 editor.handle_input("Hello ", window, cx);
9002 editor.handle_input(":wave", window, cx);
9003 assert_eq!(editor.text(cx), "Hello :wave".unindent());
9004
9005 editor.handle_input(":", window, cx);
9006 assert_eq!(editor.text(cx), "Hello 👋".unindent());
9007
9008 editor.handle_input(" :smile", window, cx);
9009 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
9010
9011 editor.handle_input(":", window, cx);
9012 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
9013
9014 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
9015 editor.handle_input(":wave", window, cx);
9016 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
9017
9018 editor.handle_input(":", window, cx);
9019 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
9020
9021 editor.handle_input(":1", window, cx);
9022 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
9023
9024 editor.handle_input(":", window, cx);
9025 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
9026
9027 // Ensure shortcode does not get replaced when it is part of a word
9028 editor.handle_input(" Test:wave", window, cx);
9029 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
9030
9031 editor.handle_input(":", window, cx);
9032 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
9033
9034 editor.set_auto_replace_emoji_shortcode(false);
9035
9036 // Ensure shortcode does not get replaced when auto replace is off
9037 editor.handle_input(" :wave", window, cx);
9038 assert_eq!(
9039 editor.text(cx),
9040 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
9041 );
9042
9043 editor.handle_input(":", window, cx);
9044 assert_eq!(
9045 editor.text(cx),
9046 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
9047 );
9048 });
9049}
9050
9051#[gpui::test]
9052async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
9053 init_test(cx, |_| {});
9054
9055 let (text, insertion_ranges) = marked_text_ranges(
9056 indoc! {"
9057 ˇ
9058 "},
9059 false,
9060 );
9061
9062 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
9063 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9064
9065 _ = editor.update_in(cx, |editor, window, cx| {
9066 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
9067
9068 editor
9069 .insert_snippet(&insertion_ranges, snippet, window, cx)
9070 .unwrap();
9071
9072 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
9073 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
9074 assert_eq!(editor.text(cx), expected_text);
9075 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
9076 }
9077
9078 assert(
9079 editor,
9080 cx,
9081 indoc! {"
9082 type «» =•
9083 "},
9084 );
9085
9086 assert!(editor.context_menu_visible(), "There should be a matches");
9087 });
9088}
9089
9090#[gpui::test]
9091async fn test_snippets(cx: &mut TestAppContext) {
9092 init_test(cx, |_| {});
9093
9094 let mut cx = EditorTestContext::new(cx).await;
9095
9096 cx.set_state(indoc! {"
9097 a.ˇ b
9098 a.ˇ b
9099 a.ˇ b
9100 "});
9101
9102 cx.update_editor(|editor, window, cx| {
9103 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
9104 let insertion_ranges = editor
9105 .selections
9106 .all(cx)
9107 .iter()
9108 .map(|s| s.range().clone())
9109 .collect::<Vec<_>>();
9110 editor
9111 .insert_snippet(&insertion_ranges, snippet, window, cx)
9112 .unwrap();
9113 });
9114
9115 cx.assert_editor_state(indoc! {"
9116 a.f(«oneˇ», two, «threeˇ») b
9117 a.f(«oneˇ», two, «threeˇ») b
9118 a.f(«oneˇ», two, «threeˇ») b
9119 "});
9120
9121 // Can't move earlier than the first tab stop
9122 cx.update_editor(|editor, window, cx| {
9123 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
9124 });
9125 cx.assert_editor_state(indoc! {"
9126 a.f(«oneˇ», two, «threeˇ») b
9127 a.f(«oneˇ», two, «threeˇ») b
9128 a.f(«oneˇ», two, «threeˇ») b
9129 "});
9130
9131 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9132 cx.assert_editor_state(indoc! {"
9133 a.f(one, «twoˇ», three) b
9134 a.f(one, «twoˇ», three) b
9135 a.f(one, «twoˇ», three) b
9136 "});
9137
9138 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
9139 cx.assert_editor_state(indoc! {"
9140 a.f(«oneˇ», two, «threeˇ») b
9141 a.f(«oneˇ», two, «threeˇ») b
9142 a.f(«oneˇ», two, «threeˇ») b
9143 "});
9144
9145 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9146 cx.assert_editor_state(indoc! {"
9147 a.f(one, «twoˇ», three) b
9148 a.f(one, «twoˇ», three) b
9149 a.f(one, «twoˇ», three) b
9150 "});
9151 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9152 cx.assert_editor_state(indoc! {"
9153 a.f(one, two, three)ˇ b
9154 a.f(one, two, three)ˇ b
9155 a.f(one, two, three)ˇ b
9156 "});
9157
9158 // As soon as the last tab stop is reached, snippet state is gone
9159 cx.update_editor(|editor, window, cx| {
9160 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
9161 });
9162 cx.assert_editor_state(indoc! {"
9163 a.f(one, two, three)ˇ b
9164 a.f(one, two, three)ˇ b
9165 a.f(one, two, three)ˇ b
9166 "});
9167}
9168
9169#[gpui::test]
9170async fn test_snippet_indentation(cx: &mut TestAppContext) {
9171 init_test(cx, |_| {});
9172
9173 let mut cx = EditorTestContext::new(cx).await;
9174
9175 cx.update_editor(|editor, window, cx| {
9176 let snippet = Snippet::parse(indoc! {"
9177 /*
9178 * Multiline comment with leading indentation
9179 *
9180 * $1
9181 */
9182 $0"})
9183 .unwrap();
9184 let insertion_ranges = editor
9185 .selections
9186 .all(cx)
9187 .iter()
9188 .map(|s| s.range().clone())
9189 .collect::<Vec<_>>();
9190 editor
9191 .insert_snippet(&insertion_ranges, snippet, window, cx)
9192 .unwrap();
9193 });
9194
9195 cx.assert_editor_state(indoc! {"
9196 /*
9197 * Multiline comment with leading indentation
9198 *
9199 * ˇ
9200 */
9201 "});
9202
9203 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9204 cx.assert_editor_state(indoc! {"
9205 /*
9206 * Multiline comment with leading indentation
9207 *
9208 *•
9209 */
9210 ˇ"});
9211}
9212
9213#[gpui::test]
9214async fn test_document_format_during_save(cx: &mut TestAppContext) {
9215 init_test(cx, |_| {});
9216
9217 let fs = FakeFs::new(cx.executor());
9218 fs.insert_file(path!("/file.rs"), Default::default()).await;
9219
9220 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
9221
9222 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9223 language_registry.add(rust_lang());
9224 let mut fake_servers = language_registry.register_fake_lsp(
9225 "Rust",
9226 FakeLspAdapter {
9227 capabilities: lsp::ServerCapabilities {
9228 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9229 ..Default::default()
9230 },
9231 ..Default::default()
9232 },
9233 );
9234
9235 let buffer = project
9236 .update(cx, |project, cx| {
9237 project.open_local_buffer(path!("/file.rs"), cx)
9238 })
9239 .await
9240 .unwrap();
9241
9242 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9243 let (editor, cx) = cx.add_window_view(|window, cx| {
9244 build_editor_with_project(project.clone(), buffer, window, cx)
9245 });
9246 editor.update_in(cx, |editor, window, cx| {
9247 editor.set_text("one\ntwo\nthree\n", window, cx)
9248 });
9249 assert!(cx.read(|cx| editor.is_dirty(cx)));
9250
9251 cx.executor().start_waiting();
9252 let fake_server = fake_servers.next().await.unwrap();
9253
9254 {
9255 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9256 move |params, _| async move {
9257 assert_eq!(
9258 params.text_document.uri,
9259 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9260 );
9261 assert_eq!(params.options.tab_size, 4);
9262 Ok(Some(vec![lsp::TextEdit::new(
9263 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9264 ", ".to_string(),
9265 )]))
9266 },
9267 );
9268 let save = editor
9269 .update_in(cx, |editor, window, cx| {
9270 editor.save(
9271 SaveOptions {
9272 format: true,
9273 autosave: false,
9274 },
9275 project.clone(),
9276 window,
9277 cx,
9278 )
9279 })
9280 .unwrap();
9281 cx.executor().start_waiting();
9282 save.await;
9283
9284 assert_eq!(
9285 editor.update(cx, |editor, cx| editor.text(cx)),
9286 "one, two\nthree\n"
9287 );
9288 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9289 }
9290
9291 {
9292 editor.update_in(cx, |editor, window, cx| {
9293 editor.set_text("one\ntwo\nthree\n", window, cx)
9294 });
9295 assert!(cx.read(|cx| editor.is_dirty(cx)));
9296
9297 // Ensure we can still save even if formatting hangs.
9298 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9299 move |params, _| async move {
9300 assert_eq!(
9301 params.text_document.uri,
9302 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9303 );
9304 futures::future::pending::<()>().await;
9305 unreachable!()
9306 },
9307 );
9308 let save = editor
9309 .update_in(cx, |editor, window, cx| {
9310 editor.save(
9311 SaveOptions {
9312 format: true,
9313 autosave: false,
9314 },
9315 project.clone(),
9316 window,
9317 cx,
9318 )
9319 })
9320 .unwrap();
9321 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9322 cx.executor().start_waiting();
9323 save.await;
9324 assert_eq!(
9325 editor.update(cx, |editor, cx| editor.text(cx)),
9326 "one\ntwo\nthree\n"
9327 );
9328 }
9329
9330 // Set rust language override and assert overridden tabsize is sent to language server
9331 update_test_language_settings(cx, |settings| {
9332 settings.languages.insert(
9333 "Rust".into(),
9334 LanguageSettingsContent {
9335 tab_size: NonZeroU32::new(8),
9336 ..Default::default()
9337 },
9338 );
9339 });
9340
9341 {
9342 editor.update_in(cx, |editor, window, cx| {
9343 editor.set_text("somehting_new\n", window, cx)
9344 });
9345 assert!(cx.read(|cx| editor.is_dirty(cx)));
9346 let _formatting_request_signal = fake_server
9347 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
9348 assert_eq!(
9349 params.text_document.uri,
9350 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9351 );
9352 assert_eq!(params.options.tab_size, 8);
9353 Ok(Some(vec![]))
9354 });
9355 let save = editor
9356 .update_in(cx, |editor, window, cx| {
9357 editor.save(
9358 SaveOptions {
9359 format: true,
9360 autosave: false,
9361 },
9362 project.clone(),
9363 window,
9364 cx,
9365 )
9366 })
9367 .unwrap();
9368 cx.executor().start_waiting();
9369 save.await;
9370 }
9371}
9372
9373#[gpui::test]
9374async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
9375 init_test(cx, |_| {});
9376
9377 let cols = 4;
9378 let rows = 10;
9379 let sample_text_1 = sample_text(rows, cols, 'a');
9380 assert_eq!(
9381 sample_text_1,
9382 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
9383 );
9384 let sample_text_2 = sample_text(rows, cols, 'l');
9385 assert_eq!(
9386 sample_text_2,
9387 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
9388 );
9389 let sample_text_3 = sample_text(rows, cols, 'v');
9390 assert_eq!(
9391 sample_text_3,
9392 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
9393 );
9394
9395 let fs = FakeFs::new(cx.executor());
9396 fs.insert_tree(
9397 path!("/a"),
9398 json!({
9399 "main.rs": sample_text_1,
9400 "other.rs": sample_text_2,
9401 "lib.rs": sample_text_3,
9402 }),
9403 )
9404 .await;
9405
9406 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
9407 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
9408 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
9409
9410 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9411 language_registry.add(rust_lang());
9412 let mut fake_servers = language_registry.register_fake_lsp(
9413 "Rust",
9414 FakeLspAdapter {
9415 capabilities: lsp::ServerCapabilities {
9416 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9417 ..Default::default()
9418 },
9419 ..Default::default()
9420 },
9421 );
9422
9423 let worktree = project.update(cx, |project, cx| {
9424 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
9425 assert_eq!(worktrees.len(), 1);
9426 worktrees.pop().unwrap()
9427 });
9428 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
9429
9430 let buffer_1 = project
9431 .update(cx, |project, cx| {
9432 project.open_buffer((worktree_id, "main.rs"), cx)
9433 })
9434 .await
9435 .unwrap();
9436 let buffer_2 = project
9437 .update(cx, |project, cx| {
9438 project.open_buffer((worktree_id, "other.rs"), cx)
9439 })
9440 .await
9441 .unwrap();
9442 let buffer_3 = project
9443 .update(cx, |project, cx| {
9444 project.open_buffer((worktree_id, "lib.rs"), cx)
9445 })
9446 .await
9447 .unwrap();
9448
9449 let multi_buffer = cx.new(|cx| {
9450 let mut multi_buffer = MultiBuffer::new(ReadWrite);
9451 multi_buffer.push_excerpts(
9452 buffer_1.clone(),
9453 [
9454 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9455 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9456 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9457 ],
9458 cx,
9459 );
9460 multi_buffer.push_excerpts(
9461 buffer_2.clone(),
9462 [
9463 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9464 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9465 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9466 ],
9467 cx,
9468 );
9469 multi_buffer.push_excerpts(
9470 buffer_3.clone(),
9471 [
9472 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9473 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9474 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9475 ],
9476 cx,
9477 );
9478 multi_buffer
9479 });
9480 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
9481 Editor::new(
9482 EditorMode::full(),
9483 multi_buffer,
9484 Some(project.clone()),
9485 window,
9486 cx,
9487 )
9488 });
9489
9490 multi_buffer_editor.update_in(cx, |editor, window, cx| {
9491 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
9492 s.select_ranges(Some(1..2))
9493 });
9494 editor.insert("|one|two|three|", window, cx);
9495 });
9496 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
9497 multi_buffer_editor.update_in(cx, |editor, window, cx| {
9498 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
9499 s.select_ranges(Some(60..70))
9500 });
9501 editor.insert("|four|five|six|", window, cx);
9502 });
9503 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
9504
9505 // First two buffers should be edited, but not the third one.
9506 assert_eq!(
9507 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
9508 "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}",
9509 );
9510 buffer_1.update(cx, |buffer, _| {
9511 assert!(buffer.is_dirty());
9512 assert_eq!(
9513 buffer.text(),
9514 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
9515 )
9516 });
9517 buffer_2.update(cx, |buffer, _| {
9518 assert!(buffer.is_dirty());
9519 assert_eq!(
9520 buffer.text(),
9521 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
9522 )
9523 });
9524 buffer_3.update(cx, |buffer, _| {
9525 assert!(!buffer.is_dirty());
9526 assert_eq!(buffer.text(), sample_text_3,)
9527 });
9528 cx.executor().run_until_parked();
9529
9530 cx.executor().start_waiting();
9531 let save = multi_buffer_editor
9532 .update_in(cx, |editor, window, cx| {
9533 editor.save(
9534 SaveOptions {
9535 format: true,
9536 autosave: false,
9537 },
9538 project.clone(),
9539 window,
9540 cx,
9541 )
9542 })
9543 .unwrap();
9544
9545 let fake_server = fake_servers.next().await.unwrap();
9546 fake_server
9547 .server
9548 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
9549 Ok(Some(vec![lsp::TextEdit::new(
9550 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9551 format!("[{} formatted]", params.text_document.uri),
9552 )]))
9553 })
9554 .detach();
9555 save.await;
9556
9557 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
9558 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
9559 assert_eq!(
9560 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
9561 uri!(
9562 "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}"
9563 ),
9564 );
9565 buffer_1.update(cx, |buffer, _| {
9566 assert!(!buffer.is_dirty());
9567 assert_eq!(
9568 buffer.text(),
9569 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
9570 )
9571 });
9572 buffer_2.update(cx, |buffer, _| {
9573 assert!(!buffer.is_dirty());
9574 assert_eq!(
9575 buffer.text(),
9576 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
9577 )
9578 });
9579 buffer_3.update(cx, |buffer, _| {
9580 assert!(!buffer.is_dirty());
9581 assert_eq!(buffer.text(), sample_text_3,)
9582 });
9583}
9584
9585#[gpui::test]
9586async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
9587 init_test(cx, |_| {});
9588
9589 let fs = FakeFs::new(cx.executor());
9590 fs.insert_tree(
9591 path!("/dir"),
9592 json!({
9593 "file1.rs": "fn main() { println!(\"hello\"); }",
9594 "file2.rs": "fn test() { println!(\"test\"); }",
9595 "file3.rs": "fn other() { println!(\"other\"); }\n",
9596 }),
9597 )
9598 .await;
9599
9600 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
9601 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
9602 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
9603
9604 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9605 language_registry.add(rust_lang());
9606
9607 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
9608 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
9609
9610 // Open three buffers
9611 let buffer_1 = project
9612 .update(cx, |project, cx| {
9613 project.open_buffer((worktree_id, "file1.rs"), cx)
9614 })
9615 .await
9616 .unwrap();
9617 let buffer_2 = project
9618 .update(cx, |project, cx| {
9619 project.open_buffer((worktree_id, "file2.rs"), cx)
9620 })
9621 .await
9622 .unwrap();
9623 let buffer_3 = project
9624 .update(cx, |project, cx| {
9625 project.open_buffer((worktree_id, "file3.rs"), cx)
9626 })
9627 .await
9628 .unwrap();
9629
9630 // Create a multi-buffer with all three buffers
9631 let multi_buffer = cx.new(|cx| {
9632 let mut multi_buffer = MultiBuffer::new(ReadWrite);
9633 multi_buffer.push_excerpts(
9634 buffer_1.clone(),
9635 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9636 cx,
9637 );
9638 multi_buffer.push_excerpts(
9639 buffer_2.clone(),
9640 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9641 cx,
9642 );
9643 multi_buffer.push_excerpts(
9644 buffer_3.clone(),
9645 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9646 cx,
9647 );
9648 multi_buffer
9649 });
9650
9651 let editor = cx.new_window_entity(|window, cx| {
9652 Editor::new(
9653 EditorMode::full(),
9654 multi_buffer,
9655 Some(project.clone()),
9656 window,
9657 cx,
9658 )
9659 });
9660
9661 // Edit only the first buffer
9662 editor.update_in(cx, |editor, window, cx| {
9663 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
9664 s.select_ranges(Some(10..10))
9665 });
9666 editor.insert("// edited", window, cx);
9667 });
9668
9669 // Verify that only buffer 1 is dirty
9670 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
9671 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9672 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9673
9674 // Get write counts after file creation (files were created with initial content)
9675 // We expect each file to have been written once during creation
9676 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
9677 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
9678 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
9679
9680 // Perform autosave
9681 let save_task = editor.update_in(cx, |editor, window, cx| {
9682 editor.save(
9683 SaveOptions {
9684 format: true,
9685 autosave: true,
9686 },
9687 project.clone(),
9688 window,
9689 cx,
9690 )
9691 });
9692 save_task.await.unwrap();
9693
9694 // Only the dirty buffer should have been saved
9695 assert_eq!(
9696 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
9697 1,
9698 "Buffer 1 was dirty, so it should have been written once during autosave"
9699 );
9700 assert_eq!(
9701 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
9702 0,
9703 "Buffer 2 was clean, so it should not have been written during autosave"
9704 );
9705 assert_eq!(
9706 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
9707 0,
9708 "Buffer 3 was clean, so it should not have been written during autosave"
9709 );
9710
9711 // Verify buffer states after autosave
9712 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9713 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9714 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9715
9716 // Now perform a manual save (format = true)
9717 let save_task = editor.update_in(cx, |editor, window, cx| {
9718 editor.save(
9719 SaveOptions {
9720 format: true,
9721 autosave: false,
9722 },
9723 project.clone(),
9724 window,
9725 cx,
9726 )
9727 });
9728 save_task.await.unwrap();
9729
9730 // During manual save, clean buffers don't get written to disk
9731 // They just get did_save called for language server notifications
9732 assert_eq!(
9733 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
9734 1,
9735 "Buffer 1 should only have been written once total (during autosave, not manual save)"
9736 );
9737 assert_eq!(
9738 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
9739 0,
9740 "Buffer 2 should not have been written at all"
9741 );
9742 assert_eq!(
9743 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
9744 0,
9745 "Buffer 3 should not have been written at all"
9746 );
9747}
9748
9749#[gpui::test]
9750async fn test_range_format_during_save(cx: &mut TestAppContext) {
9751 init_test(cx, |_| {});
9752
9753 let fs = FakeFs::new(cx.executor());
9754 fs.insert_file(path!("/file.rs"), Default::default()).await;
9755
9756 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9757
9758 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9759 language_registry.add(rust_lang());
9760 let mut fake_servers = language_registry.register_fake_lsp(
9761 "Rust",
9762 FakeLspAdapter {
9763 capabilities: lsp::ServerCapabilities {
9764 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
9765 ..Default::default()
9766 },
9767 ..Default::default()
9768 },
9769 );
9770
9771 let buffer = project
9772 .update(cx, |project, cx| {
9773 project.open_local_buffer(path!("/file.rs"), cx)
9774 })
9775 .await
9776 .unwrap();
9777
9778 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9779 let (editor, cx) = cx.add_window_view(|window, cx| {
9780 build_editor_with_project(project.clone(), buffer, window, cx)
9781 });
9782 editor.update_in(cx, |editor, window, cx| {
9783 editor.set_text("one\ntwo\nthree\n", window, cx)
9784 });
9785 assert!(cx.read(|cx| editor.is_dirty(cx)));
9786
9787 cx.executor().start_waiting();
9788 let fake_server = fake_servers.next().await.unwrap();
9789
9790 let save = editor
9791 .update_in(cx, |editor, window, cx| {
9792 editor.save(
9793 SaveOptions {
9794 format: true,
9795 autosave: false,
9796 },
9797 project.clone(),
9798 window,
9799 cx,
9800 )
9801 })
9802 .unwrap();
9803 fake_server
9804 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
9805 assert_eq!(
9806 params.text_document.uri,
9807 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9808 );
9809 assert_eq!(params.options.tab_size, 4);
9810 Ok(Some(vec![lsp::TextEdit::new(
9811 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9812 ", ".to_string(),
9813 )]))
9814 })
9815 .next()
9816 .await;
9817 cx.executor().start_waiting();
9818 save.await;
9819 assert_eq!(
9820 editor.update(cx, |editor, cx| editor.text(cx)),
9821 "one, two\nthree\n"
9822 );
9823 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9824
9825 editor.update_in(cx, |editor, window, cx| {
9826 editor.set_text("one\ntwo\nthree\n", window, cx)
9827 });
9828 assert!(cx.read(|cx| editor.is_dirty(cx)));
9829
9830 // Ensure we can still save even if formatting hangs.
9831 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
9832 move |params, _| async move {
9833 assert_eq!(
9834 params.text_document.uri,
9835 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9836 );
9837 futures::future::pending::<()>().await;
9838 unreachable!()
9839 },
9840 );
9841 let save = editor
9842 .update_in(cx, |editor, window, cx| {
9843 editor.save(
9844 SaveOptions {
9845 format: true,
9846 autosave: false,
9847 },
9848 project.clone(),
9849 window,
9850 cx,
9851 )
9852 })
9853 .unwrap();
9854 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9855 cx.executor().start_waiting();
9856 save.await;
9857 assert_eq!(
9858 editor.update(cx, |editor, cx| editor.text(cx)),
9859 "one\ntwo\nthree\n"
9860 );
9861 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9862
9863 // For non-dirty buffer, no formatting request should be sent
9864 let save = editor
9865 .update_in(cx, |editor, window, cx| {
9866 editor.save(
9867 SaveOptions {
9868 format: false,
9869 autosave: false,
9870 },
9871 project.clone(),
9872 window,
9873 cx,
9874 )
9875 })
9876 .unwrap();
9877 let _pending_format_request = fake_server
9878 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
9879 panic!("Should not be invoked");
9880 })
9881 .next();
9882 cx.executor().start_waiting();
9883 save.await;
9884
9885 // Set Rust language override and assert overridden tabsize is sent to language server
9886 update_test_language_settings(cx, |settings| {
9887 settings.languages.insert(
9888 "Rust".into(),
9889 LanguageSettingsContent {
9890 tab_size: NonZeroU32::new(8),
9891 ..Default::default()
9892 },
9893 );
9894 });
9895
9896 editor.update_in(cx, |editor, window, cx| {
9897 editor.set_text("somehting_new\n", window, cx)
9898 });
9899 assert!(cx.read(|cx| editor.is_dirty(cx)));
9900 let save = editor
9901 .update_in(cx, |editor, window, cx| {
9902 editor.save(
9903 SaveOptions {
9904 format: true,
9905 autosave: false,
9906 },
9907 project.clone(),
9908 window,
9909 cx,
9910 )
9911 })
9912 .unwrap();
9913 fake_server
9914 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
9915 assert_eq!(
9916 params.text_document.uri,
9917 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9918 );
9919 assert_eq!(params.options.tab_size, 8);
9920 Ok(Some(Vec::new()))
9921 })
9922 .next()
9923 .await;
9924 save.await;
9925}
9926
9927#[gpui::test]
9928async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
9929 init_test(cx, |settings| {
9930 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
9931 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
9932 ))
9933 });
9934
9935 let fs = FakeFs::new(cx.executor());
9936 fs.insert_file(path!("/file.rs"), Default::default()).await;
9937
9938 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9939
9940 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9941 language_registry.add(Arc::new(Language::new(
9942 LanguageConfig {
9943 name: "Rust".into(),
9944 matcher: LanguageMatcher {
9945 path_suffixes: vec!["rs".to_string()],
9946 ..Default::default()
9947 },
9948 ..LanguageConfig::default()
9949 },
9950 Some(tree_sitter_rust::LANGUAGE.into()),
9951 )));
9952 update_test_language_settings(cx, |settings| {
9953 // Enable Prettier formatting for the same buffer, and ensure
9954 // LSP is called instead of Prettier.
9955 settings.defaults.prettier = Some(PrettierSettings {
9956 allowed: true,
9957 ..PrettierSettings::default()
9958 });
9959 });
9960 let mut fake_servers = language_registry.register_fake_lsp(
9961 "Rust",
9962 FakeLspAdapter {
9963 capabilities: lsp::ServerCapabilities {
9964 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9965 ..Default::default()
9966 },
9967 ..Default::default()
9968 },
9969 );
9970
9971 let buffer = project
9972 .update(cx, |project, cx| {
9973 project.open_local_buffer(path!("/file.rs"), cx)
9974 })
9975 .await
9976 .unwrap();
9977
9978 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9979 let (editor, cx) = cx.add_window_view(|window, cx| {
9980 build_editor_with_project(project.clone(), buffer, window, cx)
9981 });
9982 editor.update_in(cx, |editor, window, cx| {
9983 editor.set_text("one\ntwo\nthree\n", window, cx)
9984 });
9985
9986 cx.executor().start_waiting();
9987 let fake_server = fake_servers.next().await.unwrap();
9988
9989 let format = editor
9990 .update_in(cx, |editor, window, cx| {
9991 editor.perform_format(
9992 project.clone(),
9993 FormatTrigger::Manual,
9994 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
9995 window,
9996 cx,
9997 )
9998 })
9999 .unwrap();
10000 fake_server
10001 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
10002 assert_eq!(
10003 params.text_document.uri,
10004 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10005 );
10006 assert_eq!(params.options.tab_size, 4);
10007 Ok(Some(vec![lsp::TextEdit::new(
10008 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
10009 ", ".to_string(),
10010 )]))
10011 })
10012 .next()
10013 .await;
10014 cx.executor().start_waiting();
10015 format.await;
10016 assert_eq!(
10017 editor.update(cx, |editor, cx| editor.text(cx)),
10018 "one, two\nthree\n"
10019 );
10020
10021 editor.update_in(cx, |editor, window, cx| {
10022 editor.set_text("one\ntwo\nthree\n", window, cx)
10023 });
10024 // Ensure we don't lock if formatting hangs.
10025 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10026 move |params, _| async move {
10027 assert_eq!(
10028 params.text_document.uri,
10029 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10030 );
10031 futures::future::pending::<()>().await;
10032 unreachable!()
10033 },
10034 );
10035 let format = editor
10036 .update_in(cx, |editor, window, cx| {
10037 editor.perform_format(
10038 project,
10039 FormatTrigger::Manual,
10040 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10041 window,
10042 cx,
10043 )
10044 })
10045 .unwrap();
10046 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
10047 cx.executor().start_waiting();
10048 format.await;
10049 assert_eq!(
10050 editor.update(cx, |editor, cx| editor.text(cx)),
10051 "one\ntwo\nthree\n"
10052 );
10053}
10054
10055#[gpui::test]
10056async fn test_multiple_formatters(cx: &mut TestAppContext) {
10057 init_test(cx, |settings| {
10058 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
10059 settings.defaults.formatter =
10060 Some(language_settings::SelectedFormatter::List(FormatterList(
10061 vec![
10062 Formatter::LanguageServer { name: None },
10063 Formatter::CodeActions(
10064 [
10065 ("code-action-1".into(), true),
10066 ("code-action-2".into(), true),
10067 ]
10068 .into_iter()
10069 .collect(),
10070 ),
10071 ]
10072 .into(),
10073 )))
10074 });
10075
10076 let fs = FakeFs::new(cx.executor());
10077 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
10078 .await;
10079
10080 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10081 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10082 language_registry.add(rust_lang());
10083
10084 let mut fake_servers = language_registry.register_fake_lsp(
10085 "Rust",
10086 FakeLspAdapter {
10087 capabilities: lsp::ServerCapabilities {
10088 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10089 execute_command_provider: Some(lsp::ExecuteCommandOptions {
10090 commands: vec!["the-command-for-code-action-1".into()],
10091 ..Default::default()
10092 }),
10093 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
10094 ..Default::default()
10095 },
10096 ..Default::default()
10097 },
10098 );
10099
10100 let buffer = project
10101 .update(cx, |project, cx| {
10102 project.open_local_buffer(path!("/file.rs"), cx)
10103 })
10104 .await
10105 .unwrap();
10106
10107 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10108 let (editor, cx) = cx.add_window_view(|window, cx| {
10109 build_editor_with_project(project.clone(), buffer, window, cx)
10110 });
10111
10112 cx.executor().start_waiting();
10113
10114 let fake_server = fake_servers.next().await.unwrap();
10115 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10116 move |_params, _| async move {
10117 Ok(Some(vec![lsp::TextEdit::new(
10118 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10119 "applied-formatting\n".to_string(),
10120 )]))
10121 },
10122 );
10123 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
10124 move |params, _| async move {
10125 assert_eq!(
10126 params.context.only,
10127 Some(vec!["code-action-1".into(), "code-action-2".into()])
10128 );
10129 let uri = lsp::Url::from_file_path(path!("/file.rs")).unwrap();
10130 Ok(Some(vec![
10131 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
10132 kind: Some("code-action-1".into()),
10133 edit: Some(lsp::WorkspaceEdit::new(
10134 [(
10135 uri.clone(),
10136 vec![lsp::TextEdit::new(
10137 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10138 "applied-code-action-1-edit\n".to_string(),
10139 )],
10140 )]
10141 .into_iter()
10142 .collect(),
10143 )),
10144 command: Some(lsp::Command {
10145 command: "the-command-for-code-action-1".into(),
10146 ..Default::default()
10147 }),
10148 ..Default::default()
10149 }),
10150 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
10151 kind: Some("code-action-2".into()),
10152 edit: Some(lsp::WorkspaceEdit::new(
10153 [(
10154 uri.clone(),
10155 vec![lsp::TextEdit::new(
10156 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10157 "applied-code-action-2-edit\n".to_string(),
10158 )],
10159 )]
10160 .into_iter()
10161 .collect(),
10162 )),
10163 ..Default::default()
10164 }),
10165 ]))
10166 },
10167 );
10168
10169 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
10170 move |params, _| async move { Ok(params) }
10171 });
10172
10173 let command_lock = Arc::new(futures::lock::Mutex::new(()));
10174 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
10175 let fake = fake_server.clone();
10176 let lock = command_lock.clone();
10177 move |params, _| {
10178 assert_eq!(params.command, "the-command-for-code-action-1");
10179 let fake = fake.clone();
10180 let lock = lock.clone();
10181 async move {
10182 lock.lock().await;
10183 fake.server
10184 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
10185 label: None,
10186 edit: lsp::WorkspaceEdit {
10187 changes: Some(
10188 [(
10189 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
10190 vec![lsp::TextEdit {
10191 range: lsp::Range::new(
10192 lsp::Position::new(0, 0),
10193 lsp::Position::new(0, 0),
10194 ),
10195 new_text: "applied-code-action-1-command\n".into(),
10196 }],
10197 )]
10198 .into_iter()
10199 .collect(),
10200 ),
10201 ..Default::default()
10202 },
10203 })
10204 .await
10205 .into_response()
10206 .unwrap();
10207 Ok(Some(json!(null)))
10208 }
10209 }
10210 });
10211
10212 cx.executor().start_waiting();
10213 editor
10214 .update_in(cx, |editor, window, cx| {
10215 editor.perform_format(
10216 project.clone(),
10217 FormatTrigger::Manual,
10218 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10219 window,
10220 cx,
10221 )
10222 })
10223 .unwrap()
10224 .await;
10225 editor.update(cx, |editor, cx| {
10226 assert_eq!(
10227 editor.text(cx),
10228 r#"
10229 applied-code-action-2-edit
10230 applied-code-action-1-command
10231 applied-code-action-1-edit
10232 applied-formatting
10233 one
10234 two
10235 three
10236 "#
10237 .unindent()
10238 );
10239 });
10240
10241 editor.update_in(cx, |editor, window, cx| {
10242 editor.undo(&Default::default(), window, cx);
10243 assert_eq!(editor.text(cx), "one \ntwo \nthree");
10244 });
10245
10246 // Perform a manual edit while waiting for an LSP command
10247 // that's being run as part of a formatting code action.
10248 let lock_guard = command_lock.lock().await;
10249 let format = editor
10250 .update_in(cx, |editor, window, cx| {
10251 editor.perform_format(
10252 project.clone(),
10253 FormatTrigger::Manual,
10254 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10255 window,
10256 cx,
10257 )
10258 })
10259 .unwrap();
10260 cx.run_until_parked();
10261 editor.update(cx, |editor, cx| {
10262 assert_eq!(
10263 editor.text(cx),
10264 r#"
10265 applied-code-action-1-edit
10266 applied-formatting
10267 one
10268 two
10269 three
10270 "#
10271 .unindent()
10272 );
10273
10274 editor.buffer.update(cx, |buffer, cx| {
10275 let ix = buffer.len(cx);
10276 buffer.edit([(ix..ix, "edited\n")], None, cx);
10277 });
10278 });
10279
10280 // Allow the LSP command to proceed. Because the buffer was edited,
10281 // the second code action will not be run.
10282 drop(lock_guard);
10283 format.await;
10284 editor.update_in(cx, |editor, window, cx| {
10285 assert_eq!(
10286 editor.text(cx),
10287 r#"
10288 applied-code-action-1-command
10289 applied-code-action-1-edit
10290 applied-formatting
10291 one
10292 two
10293 three
10294 edited
10295 "#
10296 .unindent()
10297 );
10298
10299 // The manual edit is undone first, because it is the last thing the user did
10300 // (even though the command completed afterwards).
10301 editor.undo(&Default::default(), window, cx);
10302 assert_eq!(
10303 editor.text(cx),
10304 r#"
10305 applied-code-action-1-command
10306 applied-code-action-1-edit
10307 applied-formatting
10308 one
10309 two
10310 three
10311 "#
10312 .unindent()
10313 );
10314
10315 // All the formatting (including the command, which completed after the manual edit)
10316 // is undone together.
10317 editor.undo(&Default::default(), window, cx);
10318 assert_eq!(editor.text(cx), "one \ntwo \nthree");
10319 });
10320}
10321
10322#[gpui::test]
10323async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
10324 init_test(cx, |settings| {
10325 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
10326 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
10327 ))
10328 });
10329
10330 let fs = FakeFs::new(cx.executor());
10331 fs.insert_file(path!("/file.ts"), Default::default()).await;
10332
10333 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10334
10335 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10336 language_registry.add(Arc::new(Language::new(
10337 LanguageConfig {
10338 name: "TypeScript".into(),
10339 matcher: LanguageMatcher {
10340 path_suffixes: vec!["ts".to_string()],
10341 ..Default::default()
10342 },
10343 ..LanguageConfig::default()
10344 },
10345 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
10346 )));
10347 update_test_language_settings(cx, |settings| {
10348 settings.defaults.prettier = Some(PrettierSettings {
10349 allowed: true,
10350 ..PrettierSettings::default()
10351 });
10352 });
10353 let mut fake_servers = language_registry.register_fake_lsp(
10354 "TypeScript",
10355 FakeLspAdapter {
10356 capabilities: lsp::ServerCapabilities {
10357 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
10358 ..Default::default()
10359 },
10360 ..Default::default()
10361 },
10362 );
10363
10364 let buffer = project
10365 .update(cx, |project, cx| {
10366 project.open_local_buffer(path!("/file.ts"), cx)
10367 })
10368 .await
10369 .unwrap();
10370
10371 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10372 let (editor, cx) = cx.add_window_view(|window, cx| {
10373 build_editor_with_project(project.clone(), buffer, window, cx)
10374 });
10375 editor.update_in(cx, |editor, window, cx| {
10376 editor.set_text(
10377 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
10378 window,
10379 cx,
10380 )
10381 });
10382
10383 cx.executor().start_waiting();
10384 let fake_server = fake_servers.next().await.unwrap();
10385
10386 let format = editor
10387 .update_in(cx, |editor, window, cx| {
10388 editor.perform_code_action_kind(
10389 project.clone(),
10390 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
10391 window,
10392 cx,
10393 )
10394 })
10395 .unwrap();
10396 fake_server
10397 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
10398 assert_eq!(
10399 params.text_document.uri,
10400 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
10401 );
10402 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
10403 lsp::CodeAction {
10404 title: "Organize Imports".to_string(),
10405 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
10406 edit: Some(lsp::WorkspaceEdit {
10407 changes: Some(
10408 [(
10409 params.text_document.uri.clone(),
10410 vec![lsp::TextEdit::new(
10411 lsp::Range::new(
10412 lsp::Position::new(1, 0),
10413 lsp::Position::new(2, 0),
10414 ),
10415 "".to_string(),
10416 )],
10417 )]
10418 .into_iter()
10419 .collect(),
10420 ),
10421 ..Default::default()
10422 }),
10423 ..Default::default()
10424 },
10425 )]))
10426 })
10427 .next()
10428 .await;
10429 cx.executor().start_waiting();
10430 format.await;
10431 assert_eq!(
10432 editor.update(cx, |editor, cx| editor.text(cx)),
10433 "import { a } from 'module';\n\nconst x = a;\n"
10434 );
10435
10436 editor.update_in(cx, |editor, window, cx| {
10437 editor.set_text(
10438 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
10439 window,
10440 cx,
10441 )
10442 });
10443 // Ensure we don't lock if code action hangs.
10444 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
10445 move |params, _| async move {
10446 assert_eq!(
10447 params.text_document.uri,
10448 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
10449 );
10450 futures::future::pending::<()>().await;
10451 unreachable!()
10452 },
10453 );
10454 let format = editor
10455 .update_in(cx, |editor, window, cx| {
10456 editor.perform_code_action_kind(
10457 project,
10458 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
10459 window,
10460 cx,
10461 )
10462 })
10463 .unwrap();
10464 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
10465 cx.executor().start_waiting();
10466 format.await;
10467 assert_eq!(
10468 editor.update(cx, |editor, cx| editor.text(cx)),
10469 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
10470 );
10471}
10472
10473#[gpui::test]
10474async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
10475 init_test(cx, |_| {});
10476
10477 let mut cx = EditorLspTestContext::new_rust(
10478 lsp::ServerCapabilities {
10479 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10480 ..Default::default()
10481 },
10482 cx,
10483 )
10484 .await;
10485
10486 cx.set_state(indoc! {"
10487 one.twoˇ
10488 "});
10489
10490 // The format request takes a long time. When it completes, it inserts
10491 // a newline and an indent before the `.`
10492 cx.lsp
10493 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
10494 let executor = cx.background_executor().clone();
10495 async move {
10496 executor.timer(Duration::from_millis(100)).await;
10497 Ok(Some(vec![lsp::TextEdit {
10498 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
10499 new_text: "\n ".into(),
10500 }]))
10501 }
10502 });
10503
10504 // Submit a format request.
10505 let format_1 = cx
10506 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10507 .unwrap();
10508 cx.executor().run_until_parked();
10509
10510 // Submit a second format request.
10511 let format_2 = cx
10512 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10513 .unwrap();
10514 cx.executor().run_until_parked();
10515
10516 // Wait for both format requests to complete
10517 cx.executor().advance_clock(Duration::from_millis(200));
10518 cx.executor().start_waiting();
10519 format_1.await.unwrap();
10520 cx.executor().start_waiting();
10521 format_2.await.unwrap();
10522
10523 // The formatting edits only happens once.
10524 cx.assert_editor_state(indoc! {"
10525 one
10526 .twoˇ
10527 "});
10528}
10529
10530#[gpui::test]
10531async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
10532 init_test(cx, |settings| {
10533 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
10534 });
10535
10536 let mut cx = EditorLspTestContext::new_rust(
10537 lsp::ServerCapabilities {
10538 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10539 ..Default::default()
10540 },
10541 cx,
10542 )
10543 .await;
10544
10545 // Set up a buffer white some trailing whitespace and no trailing newline.
10546 cx.set_state(
10547 &[
10548 "one ", //
10549 "twoˇ", //
10550 "three ", //
10551 "four", //
10552 ]
10553 .join("\n"),
10554 );
10555
10556 // Submit a format request.
10557 let format = cx
10558 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10559 .unwrap();
10560
10561 // Record which buffer changes have been sent to the language server
10562 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
10563 cx.lsp
10564 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
10565 let buffer_changes = buffer_changes.clone();
10566 move |params, _| {
10567 buffer_changes.lock().extend(
10568 params
10569 .content_changes
10570 .into_iter()
10571 .map(|e| (e.range.unwrap(), e.text)),
10572 );
10573 }
10574 });
10575
10576 // Handle formatting requests to the language server.
10577 cx.lsp
10578 .set_request_handler::<lsp::request::Formatting, _, _>({
10579 let buffer_changes = buffer_changes.clone();
10580 move |_, _| {
10581 // When formatting is requested, trailing whitespace has already been stripped,
10582 // and the trailing newline has already been added.
10583 assert_eq!(
10584 &buffer_changes.lock()[1..],
10585 &[
10586 (
10587 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
10588 "".into()
10589 ),
10590 (
10591 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
10592 "".into()
10593 ),
10594 (
10595 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
10596 "\n".into()
10597 ),
10598 ]
10599 );
10600
10601 // Insert blank lines between each line of the buffer.
10602 async move {
10603 Ok(Some(vec![
10604 lsp::TextEdit {
10605 range: lsp::Range::new(
10606 lsp::Position::new(1, 0),
10607 lsp::Position::new(1, 0),
10608 ),
10609 new_text: "\n".into(),
10610 },
10611 lsp::TextEdit {
10612 range: lsp::Range::new(
10613 lsp::Position::new(2, 0),
10614 lsp::Position::new(2, 0),
10615 ),
10616 new_text: "\n".into(),
10617 },
10618 ]))
10619 }
10620 }
10621 });
10622
10623 // After formatting the buffer, the trailing whitespace is stripped,
10624 // a newline is appended, and the edits provided by the language server
10625 // have been applied.
10626 format.await.unwrap();
10627 cx.assert_editor_state(
10628 &[
10629 "one", //
10630 "", //
10631 "twoˇ", //
10632 "", //
10633 "three", //
10634 "four", //
10635 "", //
10636 ]
10637 .join("\n"),
10638 );
10639
10640 // Undoing the formatting undoes the trailing whitespace removal, the
10641 // trailing newline, and the LSP edits.
10642 cx.update_buffer(|buffer, cx| buffer.undo(cx));
10643 cx.assert_editor_state(
10644 &[
10645 "one ", //
10646 "twoˇ", //
10647 "three ", //
10648 "four", //
10649 ]
10650 .join("\n"),
10651 );
10652}
10653
10654#[gpui::test]
10655async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
10656 cx: &mut TestAppContext,
10657) {
10658 init_test(cx, |_| {});
10659
10660 cx.update(|cx| {
10661 cx.update_global::<SettingsStore, _>(|settings, cx| {
10662 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10663 settings.auto_signature_help = Some(true);
10664 });
10665 });
10666 });
10667
10668 let mut cx = EditorLspTestContext::new_rust(
10669 lsp::ServerCapabilities {
10670 signature_help_provider: Some(lsp::SignatureHelpOptions {
10671 ..Default::default()
10672 }),
10673 ..Default::default()
10674 },
10675 cx,
10676 )
10677 .await;
10678
10679 let language = Language::new(
10680 LanguageConfig {
10681 name: "Rust".into(),
10682 brackets: BracketPairConfig {
10683 pairs: vec![
10684 BracketPair {
10685 start: "{".to_string(),
10686 end: "}".to_string(),
10687 close: true,
10688 surround: true,
10689 newline: true,
10690 },
10691 BracketPair {
10692 start: "(".to_string(),
10693 end: ")".to_string(),
10694 close: true,
10695 surround: true,
10696 newline: true,
10697 },
10698 BracketPair {
10699 start: "/*".to_string(),
10700 end: " */".to_string(),
10701 close: true,
10702 surround: true,
10703 newline: true,
10704 },
10705 BracketPair {
10706 start: "[".to_string(),
10707 end: "]".to_string(),
10708 close: false,
10709 surround: false,
10710 newline: true,
10711 },
10712 BracketPair {
10713 start: "\"".to_string(),
10714 end: "\"".to_string(),
10715 close: true,
10716 surround: true,
10717 newline: false,
10718 },
10719 BracketPair {
10720 start: "<".to_string(),
10721 end: ">".to_string(),
10722 close: false,
10723 surround: true,
10724 newline: true,
10725 },
10726 ],
10727 ..Default::default()
10728 },
10729 autoclose_before: "})]".to_string(),
10730 ..Default::default()
10731 },
10732 Some(tree_sitter_rust::LANGUAGE.into()),
10733 );
10734 let language = Arc::new(language);
10735
10736 cx.language_registry().add(language.clone());
10737 cx.update_buffer(|buffer, cx| {
10738 buffer.set_language(Some(language), cx);
10739 });
10740
10741 cx.set_state(
10742 &r#"
10743 fn main() {
10744 sampleˇ
10745 }
10746 "#
10747 .unindent(),
10748 );
10749
10750 cx.update_editor(|editor, window, cx| {
10751 editor.handle_input("(", window, cx);
10752 });
10753 cx.assert_editor_state(
10754 &"
10755 fn main() {
10756 sample(ˇ)
10757 }
10758 "
10759 .unindent(),
10760 );
10761
10762 let mocked_response = lsp::SignatureHelp {
10763 signatures: vec![lsp::SignatureInformation {
10764 label: "fn sample(param1: u8, param2: u8)".to_string(),
10765 documentation: None,
10766 parameters: Some(vec![
10767 lsp::ParameterInformation {
10768 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10769 documentation: None,
10770 },
10771 lsp::ParameterInformation {
10772 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10773 documentation: None,
10774 },
10775 ]),
10776 active_parameter: None,
10777 }],
10778 active_signature: Some(0),
10779 active_parameter: Some(0),
10780 };
10781 handle_signature_help_request(&mut cx, mocked_response).await;
10782
10783 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10784 .await;
10785
10786 cx.editor(|editor, _, _| {
10787 let signature_help_state = editor.signature_help_state.popover().cloned();
10788 assert_eq!(
10789 signature_help_state.unwrap().label,
10790 "param1: u8, param2: u8"
10791 );
10792 });
10793}
10794
10795#[gpui::test]
10796async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
10797 init_test(cx, |_| {});
10798
10799 cx.update(|cx| {
10800 cx.update_global::<SettingsStore, _>(|settings, cx| {
10801 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10802 settings.auto_signature_help = Some(false);
10803 settings.show_signature_help_after_edits = Some(false);
10804 });
10805 });
10806 });
10807
10808 let mut cx = EditorLspTestContext::new_rust(
10809 lsp::ServerCapabilities {
10810 signature_help_provider: Some(lsp::SignatureHelpOptions {
10811 ..Default::default()
10812 }),
10813 ..Default::default()
10814 },
10815 cx,
10816 )
10817 .await;
10818
10819 let language = Language::new(
10820 LanguageConfig {
10821 name: "Rust".into(),
10822 brackets: BracketPairConfig {
10823 pairs: vec![
10824 BracketPair {
10825 start: "{".to_string(),
10826 end: "}".to_string(),
10827 close: true,
10828 surround: true,
10829 newline: true,
10830 },
10831 BracketPair {
10832 start: "(".to_string(),
10833 end: ")".to_string(),
10834 close: true,
10835 surround: true,
10836 newline: true,
10837 },
10838 BracketPair {
10839 start: "/*".to_string(),
10840 end: " */".to_string(),
10841 close: true,
10842 surround: true,
10843 newline: true,
10844 },
10845 BracketPair {
10846 start: "[".to_string(),
10847 end: "]".to_string(),
10848 close: false,
10849 surround: false,
10850 newline: true,
10851 },
10852 BracketPair {
10853 start: "\"".to_string(),
10854 end: "\"".to_string(),
10855 close: true,
10856 surround: true,
10857 newline: false,
10858 },
10859 BracketPair {
10860 start: "<".to_string(),
10861 end: ">".to_string(),
10862 close: false,
10863 surround: true,
10864 newline: true,
10865 },
10866 ],
10867 ..Default::default()
10868 },
10869 autoclose_before: "})]".to_string(),
10870 ..Default::default()
10871 },
10872 Some(tree_sitter_rust::LANGUAGE.into()),
10873 );
10874 let language = Arc::new(language);
10875
10876 cx.language_registry().add(language.clone());
10877 cx.update_buffer(|buffer, cx| {
10878 buffer.set_language(Some(language), cx);
10879 });
10880
10881 // Ensure that signature_help is not called when no signature help is enabled.
10882 cx.set_state(
10883 &r#"
10884 fn main() {
10885 sampleˇ
10886 }
10887 "#
10888 .unindent(),
10889 );
10890 cx.update_editor(|editor, window, cx| {
10891 editor.handle_input("(", window, cx);
10892 });
10893 cx.assert_editor_state(
10894 &"
10895 fn main() {
10896 sample(ˇ)
10897 }
10898 "
10899 .unindent(),
10900 );
10901 cx.editor(|editor, _, _| {
10902 assert!(editor.signature_help_state.task().is_none());
10903 });
10904
10905 let mocked_response = lsp::SignatureHelp {
10906 signatures: vec![lsp::SignatureInformation {
10907 label: "fn sample(param1: u8, param2: u8)".to_string(),
10908 documentation: None,
10909 parameters: Some(vec![
10910 lsp::ParameterInformation {
10911 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10912 documentation: None,
10913 },
10914 lsp::ParameterInformation {
10915 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10916 documentation: None,
10917 },
10918 ]),
10919 active_parameter: None,
10920 }],
10921 active_signature: Some(0),
10922 active_parameter: Some(0),
10923 };
10924
10925 // Ensure that signature_help is called when enabled afte edits
10926 cx.update(|_, cx| {
10927 cx.update_global::<SettingsStore, _>(|settings, cx| {
10928 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10929 settings.auto_signature_help = Some(false);
10930 settings.show_signature_help_after_edits = Some(true);
10931 });
10932 });
10933 });
10934 cx.set_state(
10935 &r#"
10936 fn main() {
10937 sampleˇ
10938 }
10939 "#
10940 .unindent(),
10941 );
10942 cx.update_editor(|editor, window, cx| {
10943 editor.handle_input("(", window, cx);
10944 });
10945 cx.assert_editor_state(
10946 &"
10947 fn main() {
10948 sample(ˇ)
10949 }
10950 "
10951 .unindent(),
10952 );
10953 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10954 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10955 .await;
10956 cx.update_editor(|editor, _, _| {
10957 let signature_help_state = editor.signature_help_state.popover().cloned();
10958 assert!(signature_help_state.is_some());
10959 assert_eq!(
10960 signature_help_state.unwrap().label,
10961 "param1: u8, param2: u8"
10962 );
10963 editor.signature_help_state = SignatureHelpState::default();
10964 });
10965
10966 // Ensure that signature_help is called when auto signature help override is enabled
10967 cx.update(|_, cx| {
10968 cx.update_global::<SettingsStore, _>(|settings, cx| {
10969 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10970 settings.auto_signature_help = Some(true);
10971 settings.show_signature_help_after_edits = Some(false);
10972 });
10973 });
10974 });
10975 cx.set_state(
10976 &r#"
10977 fn main() {
10978 sampleˇ
10979 }
10980 "#
10981 .unindent(),
10982 );
10983 cx.update_editor(|editor, window, cx| {
10984 editor.handle_input("(", window, cx);
10985 });
10986 cx.assert_editor_state(
10987 &"
10988 fn main() {
10989 sample(ˇ)
10990 }
10991 "
10992 .unindent(),
10993 );
10994 handle_signature_help_request(&mut cx, mocked_response).await;
10995 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10996 .await;
10997 cx.editor(|editor, _, _| {
10998 let signature_help_state = editor.signature_help_state.popover().cloned();
10999 assert!(signature_help_state.is_some());
11000 assert_eq!(
11001 signature_help_state.unwrap().label,
11002 "param1: u8, param2: u8"
11003 );
11004 });
11005}
11006
11007#[gpui::test]
11008async fn test_signature_help(cx: &mut TestAppContext) {
11009 init_test(cx, |_| {});
11010 cx.update(|cx| {
11011 cx.update_global::<SettingsStore, _>(|settings, cx| {
11012 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11013 settings.auto_signature_help = Some(true);
11014 });
11015 });
11016 });
11017
11018 let mut cx = EditorLspTestContext::new_rust(
11019 lsp::ServerCapabilities {
11020 signature_help_provider: Some(lsp::SignatureHelpOptions {
11021 ..Default::default()
11022 }),
11023 ..Default::default()
11024 },
11025 cx,
11026 )
11027 .await;
11028
11029 // A test that directly calls `show_signature_help`
11030 cx.update_editor(|editor, window, cx| {
11031 editor.show_signature_help(&ShowSignatureHelp, window, cx);
11032 });
11033
11034 let mocked_response = lsp::SignatureHelp {
11035 signatures: vec![lsp::SignatureInformation {
11036 label: "fn sample(param1: u8, param2: u8)".to_string(),
11037 documentation: None,
11038 parameters: Some(vec![
11039 lsp::ParameterInformation {
11040 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11041 documentation: None,
11042 },
11043 lsp::ParameterInformation {
11044 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11045 documentation: None,
11046 },
11047 ]),
11048 active_parameter: None,
11049 }],
11050 active_signature: Some(0),
11051 active_parameter: Some(0),
11052 };
11053 handle_signature_help_request(&mut cx, mocked_response).await;
11054
11055 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11056 .await;
11057
11058 cx.editor(|editor, _, _| {
11059 let signature_help_state = editor.signature_help_state.popover().cloned();
11060 assert!(signature_help_state.is_some());
11061 assert_eq!(
11062 signature_help_state.unwrap().label,
11063 "param1: u8, param2: u8"
11064 );
11065 });
11066
11067 // When exiting outside from inside the brackets, `signature_help` is closed.
11068 cx.set_state(indoc! {"
11069 fn main() {
11070 sample(ˇ);
11071 }
11072
11073 fn sample(param1: u8, param2: u8) {}
11074 "});
11075
11076 cx.update_editor(|editor, window, cx| {
11077 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
11078 });
11079
11080 let mocked_response = lsp::SignatureHelp {
11081 signatures: Vec::new(),
11082 active_signature: None,
11083 active_parameter: None,
11084 };
11085 handle_signature_help_request(&mut cx, mocked_response).await;
11086
11087 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
11088 .await;
11089
11090 cx.editor(|editor, _, _| {
11091 assert!(!editor.signature_help_state.is_shown());
11092 });
11093
11094 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
11095 cx.set_state(indoc! {"
11096 fn main() {
11097 sample(ˇ);
11098 }
11099
11100 fn sample(param1: u8, param2: u8) {}
11101 "});
11102
11103 let mocked_response = lsp::SignatureHelp {
11104 signatures: vec![lsp::SignatureInformation {
11105 label: "fn sample(param1: u8, param2: u8)".to_string(),
11106 documentation: None,
11107 parameters: Some(vec![
11108 lsp::ParameterInformation {
11109 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11110 documentation: None,
11111 },
11112 lsp::ParameterInformation {
11113 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11114 documentation: None,
11115 },
11116 ]),
11117 active_parameter: None,
11118 }],
11119 active_signature: Some(0),
11120 active_parameter: Some(0),
11121 };
11122 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11123 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11124 .await;
11125 cx.editor(|editor, _, _| {
11126 assert!(editor.signature_help_state.is_shown());
11127 });
11128
11129 // Restore the popover with more parameter input
11130 cx.set_state(indoc! {"
11131 fn main() {
11132 sample(param1, param2ˇ);
11133 }
11134
11135 fn sample(param1: u8, param2: u8) {}
11136 "});
11137
11138 let mocked_response = lsp::SignatureHelp {
11139 signatures: vec![lsp::SignatureInformation {
11140 label: "fn sample(param1: u8, param2: u8)".to_string(),
11141 documentation: None,
11142 parameters: Some(vec![
11143 lsp::ParameterInformation {
11144 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11145 documentation: None,
11146 },
11147 lsp::ParameterInformation {
11148 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11149 documentation: None,
11150 },
11151 ]),
11152 active_parameter: None,
11153 }],
11154 active_signature: Some(0),
11155 active_parameter: Some(1),
11156 };
11157 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11158 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11159 .await;
11160
11161 // When selecting a range, the popover is gone.
11162 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
11163 cx.update_editor(|editor, window, cx| {
11164 editor.change_selections(None, window, cx, |s| {
11165 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
11166 })
11167 });
11168 cx.assert_editor_state(indoc! {"
11169 fn main() {
11170 sample(param1, «ˇparam2»);
11171 }
11172
11173 fn sample(param1: u8, param2: u8) {}
11174 "});
11175 cx.editor(|editor, _, _| {
11176 assert!(!editor.signature_help_state.is_shown());
11177 });
11178
11179 // When unselecting again, the popover is back if within the brackets.
11180 cx.update_editor(|editor, window, cx| {
11181 editor.change_selections(None, window, cx, |s| {
11182 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11183 })
11184 });
11185 cx.assert_editor_state(indoc! {"
11186 fn main() {
11187 sample(param1, ˇparam2);
11188 }
11189
11190 fn sample(param1: u8, param2: u8) {}
11191 "});
11192 handle_signature_help_request(&mut cx, mocked_response).await;
11193 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11194 .await;
11195 cx.editor(|editor, _, _| {
11196 assert!(editor.signature_help_state.is_shown());
11197 });
11198
11199 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
11200 cx.update_editor(|editor, window, cx| {
11201 editor.change_selections(None, window, cx, |s| {
11202 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
11203 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11204 })
11205 });
11206 cx.assert_editor_state(indoc! {"
11207 fn main() {
11208 sample(param1, ˇparam2);
11209 }
11210
11211 fn sample(param1: u8, param2: u8) {}
11212 "});
11213
11214 let mocked_response = lsp::SignatureHelp {
11215 signatures: vec![lsp::SignatureInformation {
11216 label: "fn sample(param1: u8, param2: u8)".to_string(),
11217 documentation: None,
11218 parameters: Some(vec![
11219 lsp::ParameterInformation {
11220 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11221 documentation: None,
11222 },
11223 lsp::ParameterInformation {
11224 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11225 documentation: None,
11226 },
11227 ]),
11228 active_parameter: None,
11229 }],
11230 active_signature: Some(0),
11231 active_parameter: Some(1),
11232 };
11233 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11234 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11235 .await;
11236 cx.update_editor(|editor, _, cx| {
11237 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
11238 });
11239 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
11240 .await;
11241 cx.update_editor(|editor, window, cx| {
11242 editor.change_selections(None, window, cx, |s| {
11243 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
11244 })
11245 });
11246 cx.assert_editor_state(indoc! {"
11247 fn main() {
11248 sample(param1, «ˇparam2»);
11249 }
11250
11251 fn sample(param1: u8, param2: u8) {}
11252 "});
11253 cx.update_editor(|editor, window, cx| {
11254 editor.change_selections(None, window, cx, |s| {
11255 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11256 })
11257 });
11258 cx.assert_editor_state(indoc! {"
11259 fn main() {
11260 sample(param1, ˇparam2);
11261 }
11262
11263 fn sample(param1: u8, param2: u8) {}
11264 "});
11265 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
11266 .await;
11267}
11268
11269#[gpui::test]
11270async fn test_completion_mode(cx: &mut TestAppContext) {
11271 init_test(cx, |_| {});
11272 let mut cx = EditorLspTestContext::new_rust(
11273 lsp::ServerCapabilities {
11274 completion_provider: Some(lsp::CompletionOptions {
11275 resolve_provider: Some(true),
11276 ..Default::default()
11277 }),
11278 ..Default::default()
11279 },
11280 cx,
11281 )
11282 .await;
11283
11284 struct Run {
11285 run_description: &'static str,
11286 initial_state: String,
11287 buffer_marked_text: String,
11288 completion_label: &'static str,
11289 completion_text: &'static str,
11290 expected_with_insert_mode: String,
11291 expected_with_replace_mode: String,
11292 expected_with_replace_subsequence_mode: String,
11293 expected_with_replace_suffix_mode: String,
11294 }
11295
11296 let runs = [
11297 Run {
11298 run_description: "Start of word matches completion text",
11299 initial_state: "before ediˇ after".into(),
11300 buffer_marked_text: "before <edi|> after".into(),
11301 completion_label: "editor",
11302 completion_text: "editor",
11303 expected_with_insert_mode: "before editorˇ after".into(),
11304 expected_with_replace_mode: "before editorˇ after".into(),
11305 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11306 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11307 },
11308 Run {
11309 run_description: "Accept same text at the middle of the word",
11310 initial_state: "before ediˇtor after".into(),
11311 buffer_marked_text: "before <edi|tor> after".into(),
11312 completion_label: "editor",
11313 completion_text: "editor",
11314 expected_with_insert_mode: "before editorˇtor after".into(),
11315 expected_with_replace_mode: "before editorˇ after".into(),
11316 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11317 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11318 },
11319 Run {
11320 run_description: "End of word matches completion text -- cursor at end",
11321 initial_state: "before torˇ after".into(),
11322 buffer_marked_text: "before <tor|> after".into(),
11323 completion_label: "editor",
11324 completion_text: "editor",
11325 expected_with_insert_mode: "before editorˇ after".into(),
11326 expected_with_replace_mode: "before editorˇ after".into(),
11327 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11328 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11329 },
11330 Run {
11331 run_description: "End of word matches completion text -- cursor at start",
11332 initial_state: "before ˇtor after".into(),
11333 buffer_marked_text: "before <|tor> after".into(),
11334 completion_label: "editor",
11335 completion_text: "editor",
11336 expected_with_insert_mode: "before editorˇtor after".into(),
11337 expected_with_replace_mode: "before editorˇ after".into(),
11338 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11339 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11340 },
11341 Run {
11342 run_description: "Prepend text containing whitespace",
11343 initial_state: "pˇfield: bool".into(),
11344 buffer_marked_text: "<p|field>: bool".into(),
11345 completion_label: "pub ",
11346 completion_text: "pub ",
11347 expected_with_insert_mode: "pub ˇfield: bool".into(),
11348 expected_with_replace_mode: "pub ˇ: bool".into(),
11349 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
11350 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
11351 },
11352 Run {
11353 run_description: "Add element to start of list",
11354 initial_state: "[element_ˇelement_2]".into(),
11355 buffer_marked_text: "[<element_|element_2>]".into(),
11356 completion_label: "element_1",
11357 completion_text: "element_1",
11358 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
11359 expected_with_replace_mode: "[element_1ˇ]".into(),
11360 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
11361 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
11362 },
11363 Run {
11364 run_description: "Add element to start of list -- first and second elements are equal",
11365 initial_state: "[elˇelement]".into(),
11366 buffer_marked_text: "[<el|element>]".into(),
11367 completion_label: "element",
11368 completion_text: "element",
11369 expected_with_insert_mode: "[elementˇelement]".into(),
11370 expected_with_replace_mode: "[elementˇ]".into(),
11371 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
11372 expected_with_replace_suffix_mode: "[elementˇ]".into(),
11373 },
11374 Run {
11375 run_description: "Ends with matching suffix",
11376 initial_state: "SubˇError".into(),
11377 buffer_marked_text: "<Sub|Error>".into(),
11378 completion_label: "SubscriptionError",
11379 completion_text: "SubscriptionError",
11380 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
11381 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11382 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11383 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
11384 },
11385 Run {
11386 run_description: "Suffix is a subsequence -- contiguous",
11387 initial_state: "SubˇErr".into(),
11388 buffer_marked_text: "<Sub|Err>".into(),
11389 completion_label: "SubscriptionError",
11390 completion_text: "SubscriptionError",
11391 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
11392 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11393 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11394 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
11395 },
11396 Run {
11397 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
11398 initial_state: "Suˇscrirr".into(),
11399 buffer_marked_text: "<Su|scrirr>".into(),
11400 completion_label: "SubscriptionError",
11401 completion_text: "SubscriptionError",
11402 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
11403 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11404 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11405 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
11406 },
11407 Run {
11408 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
11409 initial_state: "foo(indˇix)".into(),
11410 buffer_marked_text: "foo(<ind|ix>)".into(),
11411 completion_label: "node_index",
11412 completion_text: "node_index",
11413 expected_with_insert_mode: "foo(node_indexˇix)".into(),
11414 expected_with_replace_mode: "foo(node_indexˇ)".into(),
11415 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
11416 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
11417 },
11418 Run {
11419 run_description: "Replace range ends before cursor - should extend to cursor",
11420 initial_state: "before editˇo after".into(),
11421 buffer_marked_text: "before <{ed}>it|o after".into(),
11422 completion_label: "editor",
11423 completion_text: "editor",
11424 expected_with_insert_mode: "before editorˇo after".into(),
11425 expected_with_replace_mode: "before editorˇo after".into(),
11426 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
11427 expected_with_replace_suffix_mode: "before editorˇo after".into(),
11428 },
11429 Run {
11430 run_description: "Uses label for suffix matching",
11431 initial_state: "before ediˇtor after".into(),
11432 buffer_marked_text: "before <edi|tor> after".into(),
11433 completion_label: "editor",
11434 completion_text: "editor()",
11435 expected_with_insert_mode: "before editor()ˇtor after".into(),
11436 expected_with_replace_mode: "before editor()ˇ after".into(),
11437 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
11438 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
11439 },
11440 Run {
11441 run_description: "Case insensitive subsequence and suffix matching",
11442 initial_state: "before EDiˇtoR after".into(),
11443 buffer_marked_text: "before <EDi|toR> after".into(),
11444 completion_label: "editor",
11445 completion_text: "editor",
11446 expected_with_insert_mode: "before editorˇtoR after".into(),
11447 expected_with_replace_mode: "before editorˇ after".into(),
11448 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11449 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11450 },
11451 ];
11452
11453 for run in runs {
11454 let run_variations = [
11455 (LspInsertMode::Insert, run.expected_with_insert_mode),
11456 (LspInsertMode::Replace, run.expected_with_replace_mode),
11457 (
11458 LspInsertMode::ReplaceSubsequence,
11459 run.expected_with_replace_subsequence_mode,
11460 ),
11461 (
11462 LspInsertMode::ReplaceSuffix,
11463 run.expected_with_replace_suffix_mode,
11464 ),
11465 ];
11466
11467 for (lsp_insert_mode, expected_text) in run_variations {
11468 eprintln!(
11469 "run = {:?}, mode = {lsp_insert_mode:.?}",
11470 run.run_description,
11471 );
11472
11473 update_test_language_settings(&mut cx, |settings| {
11474 settings.defaults.completions = Some(CompletionSettings {
11475 lsp_insert_mode,
11476 words: WordsCompletionMode::Disabled,
11477 lsp: true,
11478 lsp_fetch_timeout_ms: 0,
11479 });
11480 });
11481
11482 cx.set_state(&run.initial_state);
11483 cx.update_editor(|editor, window, cx| {
11484 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11485 });
11486
11487 let counter = Arc::new(AtomicUsize::new(0));
11488 handle_completion_request_with_insert_and_replace(
11489 &mut cx,
11490 &run.buffer_marked_text,
11491 vec![(run.completion_label, run.completion_text)],
11492 counter.clone(),
11493 )
11494 .await;
11495 cx.condition(|editor, _| editor.context_menu_visible())
11496 .await;
11497 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11498
11499 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11500 editor
11501 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11502 .unwrap()
11503 });
11504 cx.assert_editor_state(&expected_text);
11505 handle_resolve_completion_request(&mut cx, None).await;
11506 apply_additional_edits.await.unwrap();
11507 }
11508 }
11509}
11510
11511#[gpui::test]
11512async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
11513 init_test(cx, |_| {});
11514 let mut cx = EditorLspTestContext::new_rust(
11515 lsp::ServerCapabilities {
11516 completion_provider: Some(lsp::CompletionOptions {
11517 resolve_provider: Some(true),
11518 ..Default::default()
11519 }),
11520 ..Default::default()
11521 },
11522 cx,
11523 )
11524 .await;
11525
11526 let initial_state = "SubˇError";
11527 let buffer_marked_text = "<Sub|Error>";
11528 let completion_text = "SubscriptionError";
11529 let expected_with_insert_mode = "SubscriptionErrorˇError";
11530 let expected_with_replace_mode = "SubscriptionErrorˇ";
11531
11532 update_test_language_settings(&mut cx, |settings| {
11533 settings.defaults.completions = Some(CompletionSettings {
11534 words: WordsCompletionMode::Disabled,
11535 // set the opposite here to ensure that the action is overriding the default behavior
11536 lsp_insert_mode: LspInsertMode::Insert,
11537 lsp: true,
11538 lsp_fetch_timeout_ms: 0,
11539 });
11540 });
11541
11542 cx.set_state(initial_state);
11543 cx.update_editor(|editor, window, cx| {
11544 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11545 });
11546
11547 let counter = Arc::new(AtomicUsize::new(0));
11548 handle_completion_request_with_insert_and_replace(
11549 &mut cx,
11550 &buffer_marked_text,
11551 vec![(completion_text, completion_text)],
11552 counter.clone(),
11553 )
11554 .await;
11555 cx.condition(|editor, _| editor.context_menu_visible())
11556 .await;
11557 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11558
11559 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11560 editor
11561 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11562 .unwrap()
11563 });
11564 cx.assert_editor_state(&expected_with_replace_mode);
11565 handle_resolve_completion_request(&mut cx, None).await;
11566 apply_additional_edits.await.unwrap();
11567
11568 update_test_language_settings(&mut cx, |settings| {
11569 settings.defaults.completions = Some(CompletionSettings {
11570 words: WordsCompletionMode::Disabled,
11571 // set the opposite here to ensure that the action is overriding the default behavior
11572 lsp_insert_mode: LspInsertMode::Replace,
11573 lsp: true,
11574 lsp_fetch_timeout_ms: 0,
11575 });
11576 });
11577
11578 cx.set_state(initial_state);
11579 cx.update_editor(|editor, window, cx| {
11580 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11581 });
11582 handle_completion_request_with_insert_and_replace(
11583 &mut cx,
11584 &buffer_marked_text,
11585 vec![(completion_text, completion_text)],
11586 counter.clone(),
11587 )
11588 .await;
11589 cx.condition(|editor, _| editor.context_menu_visible())
11590 .await;
11591 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
11592
11593 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11594 editor
11595 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
11596 .unwrap()
11597 });
11598 cx.assert_editor_state(&expected_with_insert_mode);
11599 handle_resolve_completion_request(&mut cx, None).await;
11600 apply_additional_edits.await.unwrap();
11601}
11602
11603#[gpui::test]
11604async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
11605 init_test(cx, |_| {});
11606 let mut cx = EditorLspTestContext::new_rust(
11607 lsp::ServerCapabilities {
11608 completion_provider: Some(lsp::CompletionOptions {
11609 resolve_provider: Some(true),
11610 ..Default::default()
11611 }),
11612 ..Default::default()
11613 },
11614 cx,
11615 )
11616 .await;
11617
11618 // scenario: surrounding text matches completion text
11619 let completion_text = "to_offset";
11620 let initial_state = indoc! {"
11621 1. buf.to_offˇsuffix
11622 2. buf.to_offˇsuf
11623 3. buf.to_offˇfix
11624 4. buf.to_offˇ
11625 5. into_offˇensive
11626 6. ˇsuffix
11627 7. let ˇ //
11628 8. aaˇzz
11629 9. buf.to_off«zzzzzˇ»suffix
11630 10. buf.«ˇzzzzz»suffix
11631 11. to_off«ˇzzzzz»
11632
11633 buf.to_offˇsuffix // newest cursor
11634 "};
11635 let completion_marked_buffer = indoc! {"
11636 1. buf.to_offsuffix
11637 2. buf.to_offsuf
11638 3. buf.to_offfix
11639 4. buf.to_off
11640 5. into_offensive
11641 6. suffix
11642 7. let //
11643 8. aazz
11644 9. buf.to_offzzzzzsuffix
11645 10. buf.zzzzzsuffix
11646 11. to_offzzzzz
11647
11648 buf.<to_off|suffix> // newest cursor
11649 "};
11650 let expected = indoc! {"
11651 1. buf.to_offsetˇ
11652 2. buf.to_offsetˇsuf
11653 3. buf.to_offsetˇfix
11654 4. buf.to_offsetˇ
11655 5. into_offsetˇensive
11656 6. to_offsetˇsuffix
11657 7. let to_offsetˇ //
11658 8. aato_offsetˇzz
11659 9. buf.to_offsetˇ
11660 10. buf.to_offsetˇsuffix
11661 11. to_offsetˇ
11662
11663 buf.to_offsetˇ // newest cursor
11664 "};
11665 cx.set_state(initial_state);
11666 cx.update_editor(|editor, window, cx| {
11667 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11668 });
11669 handle_completion_request_with_insert_and_replace(
11670 &mut cx,
11671 completion_marked_buffer,
11672 vec![(completion_text, completion_text)],
11673 Arc::new(AtomicUsize::new(0)),
11674 )
11675 .await;
11676 cx.condition(|editor, _| editor.context_menu_visible())
11677 .await;
11678 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11679 editor
11680 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11681 .unwrap()
11682 });
11683 cx.assert_editor_state(expected);
11684 handle_resolve_completion_request(&mut cx, None).await;
11685 apply_additional_edits.await.unwrap();
11686
11687 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
11688 let completion_text = "foo_and_bar";
11689 let initial_state = indoc! {"
11690 1. ooanbˇ
11691 2. zooanbˇ
11692 3. ooanbˇz
11693 4. zooanbˇz
11694 5. ooanˇ
11695 6. oanbˇ
11696
11697 ooanbˇ
11698 "};
11699 let completion_marked_buffer = indoc! {"
11700 1. ooanb
11701 2. zooanb
11702 3. ooanbz
11703 4. zooanbz
11704 5. ooan
11705 6. oanb
11706
11707 <ooanb|>
11708 "};
11709 let expected = indoc! {"
11710 1. foo_and_barˇ
11711 2. zfoo_and_barˇ
11712 3. foo_and_barˇz
11713 4. zfoo_and_barˇz
11714 5. ooanfoo_and_barˇ
11715 6. oanbfoo_and_barˇ
11716
11717 foo_and_barˇ
11718 "};
11719 cx.set_state(initial_state);
11720 cx.update_editor(|editor, window, cx| {
11721 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11722 });
11723 handle_completion_request_with_insert_and_replace(
11724 &mut cx,
11725 completion_marked_buffer,
11726 vec![(completion_text, completion_text)],
11727 Arc::new(AtomicUsize::new(0)),
11728 )
11729 .await;
11730 cx.condition(|editor, _| editor.context_menu_visible())
11731 .await;
11732 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11733 editor
11734 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11735 .unwrap()
11736 });
11737 cx.assert_editor_state(expected);
11738 handle_resolve_completion_request(&mut cx, None).await;
11739 apply_additional_edits.await.unwrap();
11740
11741 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
11742 // (expects the same as if it was inserted at the end)
11743 let completion_text = "foo_and_bar";
11744 let initial_state = indoc! {"
11745 1. ooˇanb
11746 2. zooˇanb
11747 3. ooˇanbz
11748 4. zooˇanbz
11749
11750 ooˇanb
11751 "};
11752 let completion_marked_buffer = indoc! {"
11753 1. ooanb
11754 2. zooanb
11755 3. ooanbz
11756 4. zooanbz
11757
11758 <oo|anb>
11759 "};
11760 let expected = indoc! {"
11761 1. foo_and_barˇ
11762 2. zfoo_and_barˇ
11763 3. foo_and_barˇz
11764 4. zfoo_and_barˇz
11765
11766 foo_and_barˇ
11767 "};
11768 cx.set_state(initial_state);
11769 cx.update_editor(|editor, window, cx| {
11770 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11771 });
11772 handle_completion_request_with_insert_and_replace(
11773 &mut cx,
11774 completion_marked_buffer,
11775 vec![(completion_text, completion_text)],
11776 Arc::new(AtomicUsize::new(0)),
11777 )
11778 .await;
11779 cx.condition(|editor, _| editor.context_menu_visible())
11780 .await;
11781 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11782 editor
11783 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11784 .unwrap()
11785 });
11786 cx.assert_editor_state(expected);
11787 handle_resolve_completion_request(&mut cx, None).await;
11788 apply_additional_edits.await.unwrap();
11789}
11790
11791// This used to crash
11792#[gpui::test]
11793async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
11794 init_test(cx, |_| {});
11795
11796 let buffer_text = indoc! {"
11797 fn main() {
11798 10.satu;
11799
11800 //
11801 // separate cursors so they open in different excerpts (manually reproducible)
11802 //
11803
11804 10.satu20;
11805 }
11806 "};
11807 let multibuffer_text_with_selections = indoc! {"
11808 fn main() {
11809 10.satuˇ;
11810
11811 //
11812
11813 //
11814
11815 10.satuˇ20;
11816 }
11817 "};
11818 let expected_multibuffer = indoc! {"
11819 fn main() {
11820 10.saturating_sub()ˇ;
11821
11822 //
11823
11824 //
11825
11826 10.saturating_sub()ˇ;
11827 }
11828 "};
11829
11830 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
11831 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
11832
11833 let fs = FakeFs::new(cx.executor());
11834 fs.insert_tree(
11835 path!("/a"),
11836 json!({
11837 "main.rs": buffer_text,
11838 }),
11839 )
11840 .await;
11841
11842 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11843 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11844 language_registry.add(rust_lang());
11845 let mut fake_servers = language_registry.register_fake_lsp(
11846 "Rust",
11847 FakeLspAdapter {
11848 capabilities: lsp::ServerCapabilities {
11849 completion_provider: Some(lsp::CompletionOptions {
11850 resolve_provider: None,
11851 ..lsp::CompletionOptions::default()
11852 }),
11853 ..lsp::ServerCapabilities::default()
11854 },
11855 ..FakeLspAdapter::default()
11856 },
11857 );
11858 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11859 let cx = &mut VisualTestContext::from_window(*workspace, cx);
11860 let buffer = project
11861 .update(cx, |project, cx| {
11862 project.open_local_buffer(path!("/a/main.rs"), cx)
11863 })
11864 .await
11865 .unwrap();
11866
11867 let multi_buffer = cx.new(|cx| {
11868 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
11869 multi_buffer.push_excerpts(
11870 buffer.clone(),
11871 [ExcerptRange::new(0..first_excerpt_end)],
11872 cx,
11873 );
11874 multi_buffer.push_excerpts(
11875 buffer.clone(),
11876 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
11877 cx,
11878 );
11879 multi_buffer
11880 });
11881
11882 let editor = workspace
11883 .update(cx, |_, window, cx| {
11884 cx.new(|cx| {
11885 Editor::new(
11886 EditorMode::Full {
11887 scale_ui_elements_with_buffer_font_size: false,
11888 show_active_line_background: false,
11889 sized_by_content: false,
11890 },
11891 multi_buffer.clone(),
11892 Some(project.clone()),
11893 window,
11894 cx,
11895 )
11896 })
11897 })
11898 .unwrap();
11899
11900 let pane = workspace
11901 .update(cx, |workspace, _, _| workspace.active_pane().clone())
11902 .unwrap();
11903 pane.update_in(cx, |pane, window, cx| {
11904 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
11905 });
11906
11907 let fake_server = fake_servers.next().await.unwrap();
11908
11909 editor.update_in(cx, |editor, window, cx| {
11910 editor.change_selections(None, window, cx, |s| {
11911 s.select_ranges([
11912 Point::new(1, 11)..Point::new(1, 11),
11913 Point::new(7, 11)..Point::new(7, 11),
11914 ])
11915 });
11916
11917 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
11918 });
11919
11920 editor.update_in(cx, |editor, window, cx| {
11921 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11922 });
11923
11924 fake_server
11925 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
11926 let completion_item = lsp::CompletionItem {
11927 label: "saturating_sub()".into(),
11928 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
11929 lsp::InsertReplaceEdit {
11930 new_text: "saturating_sub()".to_owned(),
11931 insert: lsp::Range::new(
11932 lsp::Position::new(7, 7),
11933 lsp::Position::new(7, 11),
11934 ),
11935 replace: lsp::Range::new(
11936 lsp::Position::new(7, 7),
11937 lsp::Position::new(7, 13),
11938 ),
11939 },
11940 )),
11941 ..lsp::CompletionItem::default()
11942 };
11943
11944 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
11945 })
11946 .next()
11947 .await
11948 .unwrap();
11949
11950 cx.condition(&editor, |editor, _| editor.context_menu_visible())
11951 .await;
11952
11953 editor
11954 .update_in(cx, |editor, window, cx| {
11955 editor
11956 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11957 .unwrap()
11958 })
11959 .await
11960 .unwrap();
11961
11962 editor.update(cx, |editor, cx| {
11963 assert_text_with_selections(editor, expected_multibuffer, cx);
11964 })
11965}
11966
11967#[gpui::test]
11968async fn test_completion(cx: &mut TestAppContext) {
11969 init_test(cx, |_| {});
11970
11971 let mut cx = EditorLspTestContext::new_rust(
11972 lsp::ServerCapabilities {
11973 completion_provider: Some(lsp::CompletionOptions {
11974 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11975 resolve_provider: Some(true),
11976 ..Default::default()
11977 }),
11978 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11979 ..Default::default()
11980 },
11981 cx,
11982 )
11983 .await;
11984 let counter = Arc::new(AtomicUsize::new(0));
11985
11986 cx.set_state(indoc! {"
11987 oneˇ
11988 two
11989 three
11990 "});
11991 cx.simulate_keystroke(".");
11992 handle_completion_request(
11993 indoc! {"
11994 one.|<>
11995 two
11996 three
11997 "},
11998 vec!["first_completion", "second_completion"],
11999 true,
12000 counter.clone(),
12001 &mut cx,
12002 )
12003 .await;
12004 cx.condition(|editor, _| editor.context_menu_visible())
12005 .await;
12006 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12007
12008 let _handler = handle_signature_help_request(
12009 &mut cx,
12010 lsp::SignatureHelp {
12011 signatures: vec![lsp::SignatureInformation {
12012 label: "test signature".to_string(),
12013 documentation: None,
12014 parameters: Some(vec![lsp::ParameterInformation {
12015 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
12016 documentation: None,
12017 }]),
12018 active_parameter: None,
12019 }],
12020 active_signature: None,
12021 active_parameter: None,
12022 },
12023 );
12024 cx.update_editor(|editor, window, cx| {
12025 assert!(
12026 !editor.signature_help_state.is_shown(),
12027 "No signature help was called for"
12028 );
12029 editor.show_signature_help(&ShowSignatureHelp, window, cx);
12030 });
12031 cx.run_until_parked();
12032 cx.update_editor(|editor, _, _| {
12033 assert!(
12034 !editor.signature_help_state.is_shown(),
12035 "No signature help should be shown when completions menu is open"
12036 );
12037 });
12038
12039 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12040 editor.context_menu_next(&Default::default(), window, cx);
12041 editor
12042 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12043 .unwrap()
12044 });
12045 cx.assert_editor_state(indoc! {"
12046 one.second_completionˇ
12047 two
12048 three
12049 "});
12050
12051 handle_resolve_completion_request(
12052 &mut cx,
12053 Some(vec![
12054 (
12055 //This overlaps with the primary completion edit which is
12056 //misbehavior from the LSP spec, test that we filter it out
12057 indoc! {"
12058 one.second_ˇcompletion
12059 two
12060 threeˇ
12061 "},
12062 "overlapping additional edit",
12063 ),
12064 (
12065 indoc! {"
12066 one.second_completion
12067 two
12068 threeˇ
12069 "},
12070 "\nadditional edit",
12071 ),
12072 ]),
12073 )
12074 .await;
12075 apply_additional_edits.await.unwrap();
12076 cx.assert_editor_state(indoc! {"
12077 one.second_completionˇ
12078 two
12079 three
12080 additional edit
12081 "});
12082
12083 cx.set_state(indoc! {"
12084 one.second_completion
12085 twoˇ
12086 threeˇ
12087 additional edit
12088 "});
12089 cx.simulate_keystroke(" ");
12090 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12091 cx.simulate_keystroke("s");
12092 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12093
12094 cx.assert_editor_state(indoc! {"
12095 one.second_completion
12096 two sˇ
12097 three sˇ
12098 additional edit
12099 "});
12100 handle_completion_request(
12101 indoc! {"
12102 one.second_completion
12103 two s
12104 three <s|>
12105 additional edit
12106 "},
12107 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
12108 true,
12109 counter.clone(),
12110 &mut cx,
12111 )
12112 .await;
12113 cx.condition(|editor, _| editor.context_menu_visible())
12114 .await;
12115 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12116
12117 cx.simulate_keystroke("i");
12118
12119 handle_completion_request(
12120 indoc! {"
12121 one.second_completion
12122 two si
12123 three <si|>
12124 additional edit
12125 "},
12126 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
12127 true,
12128 counter.clone(),
12129 &mut cx,
12130 )
12131 .await;
12132 cx.condition(|editor, _| editor.context_menu_visible())
12133 .await;
12134 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12135
12136 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12137 editor
12138 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12139 .unwrap()
12140 });
12141 cx.assert_editor_state(indoc! {"
12142 one.second_completion
12143 two sixth_completionˇ
12144 three sixth_completionˇ
12145 additional edit
12146 "});
12147
12148 apply_additional_edits.await.unwrap();
12149
12150 update_test_language_settings(&mut cx, |settings| {
12151 settings.defaults.show_completions_on_input = Some(false);
12152 });
12153 cx.set_state("editorˇ");
12154 cx.simulate_keystroke(".");
12155 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12156 cx.simulate_keystrokes("c l o");
12157 cx.assert_editor_state("editor.cloˇ");
12158 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12159 cx.update_editor(|editor, window, cx| {
12160 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12161 });
12162 handle_completion_request(
12163 "editor.<clo|>",
12164 vec!["close", "clobber"],
12165 true,
12166 counter.clone(),
12167 &mut cx,
12168 )
12169 .await;
12170 cx.condition(|editor, _| editor.context_menu_visible())
12171 .await;
12172 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
12173
12174 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12175 editor
12176 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12177 .unwrap()
12178 });
12179 cx.assert_editor_state("editor.clobberˇ");
12180 handle_resolve_completion_request(&mut cx, None).await;
12181 apply_additional_edits.await.unwrap();
12182}
12183
12184#[gpui::test]
12185async fn test_completion_reuse(cx: &mut TestAppContext) {
12186 init_test(cx, |_| {});
12187
12188 let mut cx = EditorLspTestContext::new_rust(
12189 lsp::ServerCapabilities {
12190 completion_provider: Some(lsp::CompletionOptions {
12191 trigger_characters: Some(vec![".".to_string()]),
12192 ..Default::default()
12193 }),
12194 ..Default::default()
12195 },
12196 cx,
12197 )
12198 .await;
12199
12200 let counter = Arc::new(AtomicUsize::new(0));
12201 cx.set_state("objˇ");
12202 cx.simulate_keystroke(".");
12203
12204 // Initial completion request returns complete results
12205 let is_incomplete = false;
12206 handle_completion_request(
12207 "obj.|<>",
12208 vec!["a", "ab", "abc"],
12209 is_incomplete,
12210 counter.clone(),
12211 &mut cx,
12212 )
12213 .await;
12214 cx.run_until_parked();
12215 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12216 cx.assert_editor_state("obj.ˇ");
12217 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12218
12219 // Type "a" - filters existing completions
12220 cx.simulate_keystroke("a");
12221 cx.run_until_parked();
12222 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12223 cx.assert_editor_state("obj.aˇ");
12224 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12225
12226 // Type "b" - filters existing completions
12227 cx.simulate_keystroke("b");
12228 cx.run_until_parked();
12229 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12230 cx.assert_editor_state("obj.abˇ");
12231 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12232
12233 // Type "c" - filters existing completions
12234 cx.simulate_keystroke("c");
12235 cx.run_until_parked();
12236 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12237 cx.assert_editor_state("obj.abcˇ");
12238 check_displayed_completions(vec!["abc"], &mut cx);
12239
12240 // Backspace to delete "c" - filters existing completions
12241 cx.update_editor(|editor, window, cx| {
12242 editor.backspace(&Backspace, window, cx);
12243 });
12244 cx.run_until_parked();
12245 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12246 cx.assert_editor_state("obj.abˇ");
12247 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12248
12249 // Moving cursor to the left dismisses menu.
12250 cx.update_editor(|editor, window, cx| {
12251 editor.move_left(&MoveLeft, window, cx);
12252 });
12253 cx.run_until_parked();
12254 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12255 cx.assert_editor_state("obj.aˇb");
12256 cx.update_editor(|editor, _, _| {
12257 assert_eq!(editor.context_menu_visible(), false);
12258 });
12259
12260 // Type "b" - new request
12261 cx.simulate_keystroke("b");
12262 let is_incomplete = false;
12263 handle_completion_request(
12264 "obj.<ab|>a",
12265 vec!["ab", "abc"],
12266 is_incomplete,
12267 counter.clone(),
12268 &mut cx,
12269 )
12270 .await;
12271 cx.run_until_parked();
12272 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12273 cx.assert_editor_state("obj.abˇb");
12274 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12275
12276 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
12277 cx.update_editor(|editor, window, cx| {
12278 editor.backspace(&Backspace, window, cx);
12279 });
12280 let is_incomplete = false;
12281 handle_completion_request(
12282 "obj.<a|>b",
12283 vec!["a", "ab", "abc"],
12284 is_incomplete,
12285 counter.clone(),
12286 &mut cx,
12287 )
12288 .await;
12289 cx.run_until_parked();
12290 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12291 cx.assert_editor_state("obj.aˇb");
12292 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12293
12294 // Backspace to delete "a" - dismisses menu.
12295 cx.update_editor(|editor, window, cx| {
12296 editor.backspace(&Backspace, window, cx);
12297 });
12298 cx.run_until_parked();
12299 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12300 cx.assert_editor_state("obj.ˇb");
12301 cx.update_editor(|editor, _, _| {
12302 assert_eq!(editor.context_menu_visible(), false);
12303 });
12304}
12305
12306#[gpui::test]
12307async fn test_word_completion(cx: &mut TestAppContext) {
12308 let lsp_fetch_timeout_ms = 10;
12309 init_test(cx, |language_settings| {
12310 language_settings.defaults.completions = Some(CompletionSettings {
12311 words: WordsCompletionMode::Fallback,
12312 lsp: true,
12313 lsp_fetch_timeout_ms: 10,
12314 lsp_insert_mode: LspInsertMode::Insert,
12315 });
12316 });
12317
12318 let mut cx = EditorLspTestContext::new_rust(
12319 lsp::ServerCapabilities {
12320 completion_provider: Some(lsp::CompletionOptions {
12321 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12322 ..lsp::CompletionOptions::default()
12323 }),
12324 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12325 ..lsp::ServerCapabilities::default()
12326 },
12327 cx,
12328 )
12329 .await;
12330
12331 let throttle_completions = Arc::new(AtomicBool::new(false));
12332
12333 let lsp_throttle_completions = throttle_completions.clone();
12334 let _completion_requests_handler =
12335 cx.lsp
12336 .server
12337 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
12338 let lsp_throttle_completions = lsp_throttle_completions.clone();
12339 let cx = cx.clone();
12340 async move {
12341 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
12342 cx.background_executor()
12343 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
12344 .await;
12345 }
12346 Ok(Some(lsp::CompletionResponse::Array(vec![
12347 lsp::CompletionItem {
12348 label: "first".into(),
12349 ..lsp::CompletionItem::default()
12350 },
12351 lsp::CompletionItem {
12352 label: "last".into(),
12353 ..lsp::CompletionItem::default()
12354 },
12355 ])))
12356 }
12357 });
12358
12359 cx.set_state(indoc! {"
12360 oneˇ
12361 two
12362 three
12363 "});
12364 cx.simulate_keystroke(".");
12365 cx.executor().run_until_parked();
12366 cx.condition(|editor, _| editor.context_menu_visible())
12367 .await;
12368 cx.update_editor(|editor, window, cx| {
12369 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12370 {
12371 assert_eq!(
12372 completion_menu_entries(&menu),
12373 &["first", "last"],
12374 "When LSP server is fast to reply, no fallback word completions are used"
12375 );
12376 } else {
12377 panic!("expected completion menu to be open");
12378 }
12379 editor.cancel(&Cancel, window, cx);
12380 });
12381 cx.executor().run_until_parked();
12382 cx.condition(|editor, _| !editor.context_menu_visible())
12383 .await;
12384
12385 throttle_completions.store(true, atomic::Ordering::Release);
12386 cx.simulate_keystroke(".");
12387 cx.executor()
12388 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
12389 cx.executor().run_until_parked();
12390 cx.condition(|editor, _| editor.context_menu_visible())
12391 .await;
12392 cx.update_editor(|editor, _, _| {
12393 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12394 {
12395 assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
12396 "When LSP server is slow, document words can be shown instead, if configured accordingly");
12397 } else {
12398 panic!("expected completion menu to be open");
12399 }
12400 });
12401}
12402
12403#[gpui::test]
12404async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
12405 init_test(cx, |language_settings| {
12406 language_settings.defaults.completions = Some(CompletionSettings {
12407 words: WordsCompletionMode::Enabled,
12408 lsp: true,
12409 lsp_fetch_timeout_ms: 0,
12410 lsp_insert_mode: LspInsertMode::Insert,
12411 });
12412 });
12413
12414 let mut cx = EditorLspTestContext::new_rust(
12415 lsp::ServerCapabilities {
12416 completion_provider: Some(lsp::CompletionOptions {
12417 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12418 ..lsp::CompletionOptions::default()
12419 }),
12420 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12421 ..lsp::ServerCapabilities::default()
12422 },
12423 cx,
12424 )
12425 .await;
12426
12427 let _completion_requests_handler =
12428 cx.lsp
12429 .server
12430 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
12431 Ok(Some(lsp::CompletionResponse::Array(vec![
12432 lsp::CompletionItem {
12433 label: "first".into(),
12434 ..lsp::CompletionItem::default()
12435 },
12436 lsp::CompletionItem {
12437 label: "last".into(),
12438 ..lsp::CompletionItem::default()
12439 },
12440 ])))
12441 });
12442
12443 cx.set_state(indoc! {"ˇ
12444 first
12445 last
12446 second
12447 "});
12448 cx.simulate_keystroke(".");
12449 cx.executor().run_until_parked();
12450 cx.condition(|editor, _| editor.context_menu_visible())
12451 .await;
12452 cx.update_editor(|editor, _, _| {
12453 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12454 {
12455 assert_eq!(
12456 completion_menu_entries(&menu),
12457 &["first", "last", "second"],
12458 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
12459 );
12460 } else {
12461 panic!("expected completion menu to be open");
12462 }
12463 });
12464}
12465
12466#[gpui::test]
12467async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
12468 init_test(cx, |language_settings| {
12469 language_settings.defaults.completions = Some(CompletionSettings {
12470 words: WordsCompletionMode::Disabled,
12471 lsp: true,
12472 lsp_fetch_timeout_ms: 0,
12473 lsp_insert_mode: LspInsertMode::Insert,
12474 });
12475 });
12476
12477 let mut cx = EditorLspTestContext::new_rust(
12478 lsp::ServerCapabilities {
12479 completion_provider: Some(lsp::CompletionOptions {
12480 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12481 ..lsp::CompletionOptions::default()
12482 }),
12483 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12484 ..lsp::ServerCapabilities::default()
12485 },
12486 cx,
12487 )
12488 .await;
12489
12490 let _completion_requests_handler =
12491 cx.lsp
12492 .server
12493 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
12494 panic!("LSP completions should not be queried when dealing with word completions")
12495 });
12496
12497 cx.set_state(indoc! {"ˇ
12498 first
12499 last
12500 second
12501 "});
12502 cx.update_editor(|editor, window, cx| {
12503 editor.show_word_completions(&ShowWordCompletions, window, cx);
12504 });
12505 cx.executor().run_until_parked();
12506 cx.condition(|editor, _| editor.context_menu_visible())
12507 .await;
12508 cx.update_editor(|editor, _, _| {
12509 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12510 {
12511 assert_eq!(
12512 completion_menu_entries(&menu),
12513 &["first", "last", "second"],
12514 "`ShowWordCompletions` action should show word completions"
12515 );
12516 } else {
12517 panic!("expected completion menu to be open");
12518 }
12519 });
12520
12521 cx.simulate_keystroke("l");
12522 cx.executor().run_until_parked();
12523 cx.condition(|editor, _| editor.context_menu_visible())
12524 .await;
12525 cx.update_editor(|editor, _, _| {
12526 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12527 {
12528 assert_eq!(
12529 completion_menu_entries(&menu),
12530 &["last"],
12531 "After showing word completions, further editing should filter them and not query the LSP"
12532 );
12533 } else {
12534 panic!("expected completion menu to be open");
12535 }
12536 });
12537}
12538
12539#[gpui::test]
12540async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
12541 init_test(cx, |language_settings| {
12542 language_settings.defaults.completions = Some(CompletionSettings {
12543 words: WordsCompletionMode::Fallback,
12544 lsp: false,
12545 lsp_fetch_timeout_ms: 0,
12546 lsp_insert_mode: LspInsertMode::Insert,
12547 });
12548 });
12549
12550 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
12551
12552 cx.set_state(indoc! {"ˇ
12553 0_usize
12554 let
12555 33
12556 4.5f32
12557 "});
12558 cx.update_editor(|editor, window, cx| {
12559 editor.show_completions(&ShowCompletions::default(), window, cx);
12560 });
12561 cx.executor().run_until_parked();
12562 cx.condition(|editor, _| editor.context_menu_visible())
12563 .await;
12564 cx.update_editor(|editor, window, cx| {
12565 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12566 {
12567 assert_eq!(
12568 completion_menu_entries(&menu),
12569 &["let"],
12570 "With no digits in the completion query, no digits should be in the word completions"
12571 );
12572 } else {
12573 panic!("expected completion menu to be open");
12574 }
12575 editor.cancel(&Cancel, window, cx);
12576 });
12577
12578 cx.set_state(indoc! {"3ˇ
12579 0_usize
12580 let
12581 3
12582 33.35f32
12583 "});
12584 cx.update_editor(|editor, window, cx| {
12585 editor.show_completions(&ShowCompletions::default(), window, cx);
12586 });
12587 cx.executor().run_until_parked();
12588 cx.condition(|editor, _| editor.context_menu_visible())
12589 .await;
12590 cx.update_editor(|editor, _, _| {
12591 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12592 {
12593 assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
12594 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
12595 } else {
12596 panic!("expected completion menu to be open");
12597 }
12598 });
12599}
12600
12601fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
12602 let position = || lsp::Position {
12603 line: params.text_document_position.position.line,
12604 character: params.text_document_position.position.character,
12605 };
12606 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12607 range: lsp::Range {
12608 start: position(),
12609 end: position(),
12610 },
12611 new_text: text.to_string(),
12612 }))
12613}
12614
12615#[gpui::test]
12616async fn test_multiline_completion(cx: &mut TestAppContext) {
12617 init_test(cx, |_| {});
12618
12619 let fs = FakeFs::new(cx.executor());
12620 fs.insert_tree(
12621 path!("/a"),
12622 json!({
12623 "main.ts": "a",
12624 }),
12625 )
12626 .await;
12627
12628 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12629 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12630 let typescript_language = Arc::new(Language::new(
12631 LanguageConfig {
12632 name: "TypeScript".into(),
12633 matcher: LanguageMatcher {
12634 path_suffixes: vec!["ts".to_string()],
12635 ..LanguageMatcher::default()
12636 },
12637 line_comments: vec!["// ".into()],
12638 ..LanguageConfig::default()
12639 },
12640 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
12641 ));
12642 language_registry.add(typescript_language.clone());
12643 let mut fake_servers = language_registry.register_fake_lsp(
12644 "TypeScript",
12645 FakeLspAdapter {
12646 capabilities: lsp::ServerCapabilities {
12647 completion_provider: Some(lsp::CompletionOptions {
12648 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12649 ..lsp::CompletionOptions::default()
12650 }),
12651 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12652 ..lsp::ServerCapabilities::default()
12653 },
12654 // Emulate vtsls label generation
12655 label_for_completion: Some(Box::new(|item, _| {
12656 let text = if let Some(description) = item
12657 .label_details
12658 .as_ref()
12659 .and_then(|label_details| label_details.description.as_ref())
12660 {
12661 format!("{} {}", item.label, description)
12662 } else if let Some(detail) = &item.detail {
12663 format!("{} {}", item.label, detail)
12664 } else {
12665 item.label.clone()
12666 };
12667 let len = text.len();
12668 Some(language::CodeLabel {
12669 text,
12670 runs: Vec::new(),
12671 filter_range: 0..len,
12672 })
12673 })),
12674 ..FakeLspAdapter::default()
12675 },
12676 );
12677 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12678 let cx = &mut VisualTestContext::from_window(*workspace, cx);
12679 let worktree_id = workspace
12680 .update(cx, |workspace, _window, cx| {
12681 workspace.project().update(cx, |project, cx| {
12682 project.worktrees(cx).next().unwrap().read(cx).id()
12683 })
12684 })
12685 .unwrap();
12686 let _buffer = project
12687 .update(cx, |project, cx| {
12688 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
12689 })
12690 .await
12691 .unwrap();
12692 let editor = workspace
12693 .update(cx, |workspace, window, cx| {
12694 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
12695 })
12696 .unwrap()
12697 .await
12698 .unwrap()
12699 .downcast::<Editor>()
12700 .unwrap();
12701 let fake_server = fake_servers.next().await.unwrap();
12702
12703 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
12704 let multiline_label_2 = "a\nb\nc\n";
12705 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
12706 let multiline_description = "d\ne\nf\n";
12707 let multiline_detail_2 = "g\nh\ni\n";
12708
12709 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
12710 move |params, _| async move {
12711 Ok(Some(lsp::CompletionResponse::Array(vec![
12712 lsp::CompletionItem {
12713 label: multiline_label.to_string(),
12714 text_edit: gen_text_edit(¶ms, "new_text_1"),
12715 ..lsp::CompletionItem::default()
12716 },
12717 lsp::CompletionItem {
12718 label: "single line label 1".to_string(),
12719 detail: Some(multiline_detail.to_string()),
12720 text_edit: gen_text_edit(¶ms, "new_text_2"),
12721 ..lsp::CompletionItem::default()
12722 },
12723 lsp::CompletionItem {
12724 label: "single line label 2".to_string(),
12725 label_details: Some(lsp::CompletionItemLabelDetails {
12726 description: Some(multiline_description.to_string()),
12727 detail: None,
12728 }),
12729 text_edit: gen_text_edit(¶ms, "new_text_2"),
12730 ..lsp::CompletionItem::default()
12731 },
12732 lsp::CompletionItem {
12733 label: multiline_label_2.to_string(),
12734 detail: Some(multiline_detail_2.to_string()),
12735 text_edit: gen_text_edit(¶ms, "new_text_3"),
12736 ..lsp::CompletionItem::default()
12737 },
12738 lsp::CompletionItem {
12739 label: "Label with many spaces and \t but without newlines".to_string(),
12740 detail: Some(
12741 "Details with many spaces and \t but without newlines".to_string(),
12742 ),
12743 text_edit: gen_text_edit(¶ms, "new_text_4"),
12744 ..lsp::CompletionItem::default()
12745 },
12746 ])))
12747 },
12748 );
12749
12750 editor.update_in(cx, |editor, window, cx| {
12751 cx.focus_self(window);
12752 editor.move_to_end(&MoveToEnd, window, cx);
12753 editor.handle_input(".", window, cx);
12754 });
12755 cx.run_until_parked();
12756 completion_handle.next().await.unwrap();
12757
12758 editor.update(cx, |editor, _| {
12759 assert!(editor.context_menu_visible());
12760 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12761 {
12762 let completion_labels = menu
12763 .completions
12764 .borrow()
12765 .iter()
12766 .map(|c| c.label.text.clone())
12767 .collect::<Vec<_>>();
12768 assert_eq!(
12769 completion_labels,
12770 &[
12771 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
12772 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
12773 "single line label 2 d e f ",
12774 "a b c g h i ",
12775 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
12776 ],
12777 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
12778 );
12779
12780 for completion in menu
12781 .completions
12782 .borrow()
12783 .iter() {
12784 assert_eq!(
12785 completion.label.filter_range,
12786 0..completion.label.text.len(),
12787 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
12788 );
12789 }
12790 } else {
12791 panic!("expected completion menu to be open");
12792 }
12793 });
12794}
12795
12796#[gpui::test]
12797async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
12798 init_test(cx, |_| {});
12799 let mut cx = EditorLspTestContext::new_rust(
12800 lsp::ServerCapabilities {
12801 completion_provider: Some(lsp::CompletionOptions {
12802 trigger_characters: Some(vec![".".to_string()]),
12803 ..Default::default()
12804 }),
12805 ..Default::default()
12806 },
12807 cx,
12808 )
12809 .await;
12810 cx.lsp
12811 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
12812 Ok(Some(lsp::CompletionResponse::Array(vec![
12813 lsp::CompletionItem {
12814 label: "first".into(),
12815 ..Default::default()
12816 },
12817 lsp::CompletionItem {
12818 label: "last".into(),
12819 ..Default::default()
12820 },
12821 ])))
12822 });
12823 cx.set_state("variableˇ");
12824 cx.simulate_keystroke(".");
12825 cx.executor().run_until_parked();
12826
12827 cx.update_editor(|editor, _, _| {
12828 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12829 {
12830 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
12831 } else {
12832 panic!("expected completion menu to be open");
12833 }
12834 });
12835
12836 cx.update_editor(|editor, window, cx| {
12837 editor.move_page_down(&MovePageDown::default(), window, cx);
12838 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12839 {
12840 assert!(
12841 menu.selected_item == 1,
12842 "expected PageDown to select the last item from the context menu"
12843 );
12844 } else {
12845 panic!("expected completion menu to stay open after PageDown");
12846 }
12847 });
12848
12849 cx.update_editor(|editor, window, cx| {
12850 editor.move_page_up(&MovePageUp::default(), window, cx);
12851 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12852 {
12853 assert!(
12854 menu.selected_item == 0,
12855 "expected PageUp to select the first item from the context menu"
12856 );
12857 } else {
12858 panic!("expected completion menu to stay open after PageUp");
12859 }
12860 });
12861}
12862
12863#[gpui::test]
12864async fn test_as_is_completions(cx: &mut TestAppContext) {
12865 init_test(cx, |_| {});
12866 let mut cx = EditorLspTestContext::new_rust(
12867 lsp::ServerCapabilities {
12868 completion_provider: Some(lsp::CompletionOptions {
12869 ..Default::default()
12870 }),
12871 ..Default::default()
12872 },
12873 cx,
12874 )
12875 .await;
12876 cx.lsp
12877 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
12878 Ok(Some(lsp::CompletionResponse::Array(vec![
12879 lsp::CompletionItem {
12880 label: "unsafe".into(),
12881 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12882 range: lsp::Range {
12883 start: lsp::Position {
12884 line: 1,
12885 character: 2,
12886 },
12887 end: lsp::Position {
12888 line: 1,
12889 character: 3,
12890 },
12891 },
12892 new_text: "unsafe".to_string(),
12893 })),
12894 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
12895 ..Default::default()
12896 },
12897 ])))
12898 });
12899 cx.set_state("fn a() {}\n nˇ");
12900 cx.executor().run_until_parked();
12901 cx.update_editor(|editor, window, cx| {
12902 editor.show_completions(
12903 &ShowCompletions {
12904 trigger: Some("\n".into()),
12905 },
12906 window,
12907 cx,
12908 );
12909 });
12910 cx.executor().run_until_parked();
12911
12912 cx.update_editor(|editor, window, cx| {
12913 editor.confirm_completion(&Default::default(), window, cx)
12914 });
12915 cx.executor().run_until_parked();
12916 cx.assert_editor_state("fn a() {}\n unsafeˇ");
12917}
12918
12919#[gpui::test]
12920async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
12921 init_test(cx, |_| {});
12922
12923 let mut cx = EditorLspTestContext::new_rust(
12924 lsp::ServerCapabilities {
12925 completion_provider: Some(lsp::CompletionOptions {
12926 trigger_characters: Some(vec![".".to_string()]),
12927 resolve_provider: Some(true),
12928 ..Default::default()
12929 }),
12930 ..Default::default()
12931 },
12932 cx,
12933 )
12934 .await;
12935
12936 cx.set_state("fn main() { let a = 2ˇ; }");
12937 cx.simulate_keystroke(".");
12938 let completion_item = lsp::CompletionItem {
12939 label: "Some".into(),
12940 kind: Some(lsp::CompletionItemKind::SNIPPET),
12941 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
12942 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
12943 kind: lsp::MarkupKind::Markdown,
12944 value: "```rust\nSome(2)\n```".to_string(),
12945 })),
12946 deprecated: Some(false),
12947 sort_text: Some("Some".to_string()),
12948 filter_text: Some("Some".to_string()),
12949 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
12950 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12951 range: lsp::Range {
12952 start: lsp::Position {
12953 line: 0,
12954 character: 22,
12955 },
12956 end: lsp::Position {
12957 line: 0,
12958 character: 22,
12959 },
12960 },
12961 new_text: "Some(2)".to_string(),
12962 })),
12963 additional_text_edits: Some(vec![lsp::TextEdit {
12964 range: lsp::Range {
12965 start: lsp::Position {
12966 line: 0,
12967 character: 20,
12968 },
12969 end: lsp::Position {
12970 line: 0,
12971 character: 22,
12972 },
12973 },
12974 new_text: "".to_string(),
12975 }]),
12976 ..Default::default()
12977 };
12978
12979 let closure_completion_item = completion_item.clone();
12980 let counter = Arc::new(AtomicUsize::new(0));
12981 let counter_clone = counter.clone();
12982 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
12983 let task_completion_item = closure_completion_item.clone();
12984 counter_clone.fetch_add(1, atomic::Ordering::Release);
12985 async move {
12986 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
12987 is_incomplete: true,
12988 item_defaults: None,
12989 items: vec![task_completion_item],
12990 })))
12991 }
12992 });
12993
12994 cx.condition(|editor, _| editor.context_menu_visible())
12995 .await;
12996 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
12997 assert!(request.next().await.is_some());
12998 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12999
13000 cx.simulate_keystrokes("S o m");
13001 cx.condition(|editor, _| editor.context_menu_visible())
13002 .await;
13003 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
13004 assert!(request.next().await.is_some());
13005 assert!(request.next().await.is_some());
13006 assert!(request.next().await.is_some());
13007 request.close();
13008 assert!(request.next().await.is_none());
13009 assert_eq!(
13010 counter.load(atomic::Ordering::Acquire),
13011 4,
13012 "With the completions menu open, only one LSP request should happen per input"
13013 );
13014}
13015
13016#[gpui::test]
13017async fn test_toggle_comment(cx: &mut TestAppContext) {
13018 init_test(cx, |_| {});
13019 let mut cx = EditorTestContext::new(cx).await;
13020 let language = Arc::new(Language::new(
13021 LanguageConfig {
13022 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
13023 ..Default::default()
13024 },
13025 Some(tree_sitter_rust::LANGUAGE.into()),
13026 ));
13027 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
13028
13029 // If multiple selections intersect a line, the line is only toggled once.
13030 cx.set_state(indoc! {"
13031 fn a() {
13032 «//b();
13033 ˇ»// «c();
13034 //ˇ» d();
13035 }
13036 "});
13037
13038 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13039
13040 cx.assert_editor_state(indoc! {"
13041 fn a() {
13042 «b();
13043 c();
13044 ˇ» d();
13045 }
13046 "});
13047
13048 // The comment prefix is inserted at the same column for every line in a
13049 // selection.
13050 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13051
13052 cx.assert_editor_state(indoc! {"
13053 fn a() {
13054 // «b();
13055 // c();
13056 ˇ»// d();
13057 }
13058 "});
13059
13060 // If a selection ends at the beginning of a line, that line is not toggled.
13061 cx.set_selections_state(indoc! {"
13062 fn a() {
13063 // b();
13064 «// c();
13065 ˇ» // d();
13066 }
13067 "});
13068
13069 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13070
13071 cx.assert_editor_state(indoc! {"
13072 fn a() {
13073 // b();
13074 «c();
13075 ˇ» // d();
13076 }
13077 "});
13078
13079 // If a selection span a single line and is empty, the line is toggled.
13080 cx.set_state(indoc! {"
13081 fn a() {
13082 a();
13083 b();
13084 ˇ
13085 }
13086 "});
13087
13088 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13089
13090 cx.assert_editor_state(indoc! {"
13091 fn a() {
13092 a();
13093 b();
13094 //•ˇ
13095 }
13096 "});
13097
13098 // If a selection span multiple lines, empty lines are not toggled.
13099 cx.set_state(indoc! {"
13100 fn a() {
13101 «a();
13102
13103 c();ˇ»
13104 }
13105 "});
13106
13107 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13108
13109 cx.assert_editor_state(indoc! {"
13110 fn a() {
13111 // «a();
13112
13113 // c();ˇ»
13114 }
13115 "});
13116
13117 // If a selection includes multiple comment prefixes, all lines are uncommented.
13118 cx.set_state(indoc! {"
13119 fn a() {
13120 «// a();
13121 /// b();
13122 //! c();ˇ»
13123 }
13124 "});
13125
13126 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13127
13128 cx.assert_editor_state(indoc! {"
13129 fn a() {
13130 «a();
13131 b();
13132 c();ˇ»
13133 }
13134 "});
13135}
13136
13137#[gpui::test]
13138async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
13139 init_test(cx, |_| {});
13140 let mut cx = EditorTestContext::new(cx).await;
13141 let language = Arc::new(Language::new(
13142 LanguageConfig {
13143 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
13144 ..Default::default()
13145 },
13146 Some(tree_sitter_rust::LANGUAGE.into()),
13147 ));
13148 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
13149
13150 let toggle_comments = &ToggleComments {
13151 advance_downwards: false,
13152 ignore_indent: true,
13153 };
13154
13155 // If multiple selections intersect a line, the line is only toggled once.
13156 cx.set_state(indoc! {"
13157 fn a() {
13158 // «b();
13159 // c();
13160 // ˇ» d();
13161 }
13162 "});
13163
13164 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13165
13166 cx.assert_editor_state(indoc! {"
13167 fn a() {
13168 «b();
13169 c();
13170 ˇ» d();
13171 }
13172 "});
13173
13174 // The comment prefix is inserted at the beginning of each line
13175 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13176
13177 cx.assert_editor_state(indoc! {"
13178 fn a() {
13179 // «b();
13180 // c();
13181 // ˇ» d();
13182 }
13183 "});
13184
13185 // If a selection ends at the beginning of a line, that line is not toggled.
13186 cx.set_selections_state(indoc! {"
13187 fn a() {
13188 // b();
13189 // «c();
13190 ˇ»// d();
13191 }
13192 "});
13193
13194 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13195
13196 cx.assert_editor_state(indoc! {"
13197 fn a() {
13198 // b();
13199 «c();
13200 ˇ»// d();
13201 }
13202 "});
13203
13204 // If a selection span a single line and is empty, the line is toggled.
13205 cx.set_state(indoc! {"
13206 fn a() {
13207 a();
13208 b();
13209 ˇ
13210 }
13211 "});
13212
13213 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13214
13215 cx.assert_editor_state(indoc! {"
13216 fn a() {
13217 a();
13218 b();
13219 //ˇ
13220 }
13221 "});
13222
13223 // If a selection span multiple lines, empty lines are not toggled.
13224 cx.set_state(indoc! {"
13225 fn a() {
13226 «a();
13227
13228 c();ˇ»
13229 }
13230 "});
13231
13232 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13233
13234 cx.assert_editor_state(indoc! {"
13235 fn a() {
13236 // «a();
13237
13238 // c();ˇ»
13239 }
13240 "});
13241
13242 // If a selection includes multiple comment prefixes, all lines are uncommented.
13243 cx.set_state(indoc! {"
13244 fn a() {
13245 // «a();
13246 /// b();
13247 //! c();ˇ»
13248 }
13249 "});
13250
13251 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13252
13253 cx.assert_editor_state(indoc! {"
13254 fn a() {
13255 «a();
13256 b();
13257 c();ˇ»
13258 }
13259 "});
13260}
13261
13262#[gpui::test]
13263async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
13264 init_test(cx, |_| {});
13265
13266 let language = Arc::new(Language::new(
13267 LanguageConfig {
13268 line_comments: vec!["// ".into()],
13269 ..Default::default()
13270 },
13271 Some(tree_sitter_rust::LANGUAGE.into()),
13272 ));
13273
13274 let mut cx = EditorTestContext::new(cx).await;
13275
13276 cx.language_registry().add(language.clone());
13277 cx.update_buffer(|buffer, cx| {
13278 buffer.set_language(Some(language), cx);
13279 });
13280
13281 let toggle_comments = &ToggleComments {
13282 advance_downwards: true,
13283 ignore_indent: false,
13284 };
13285
13286 // Single cursor on one line -> advance
13287 // Cursor moves horizontally 3 characters as well on non-blank line
13288 cx.set_state(indoc!(
13289 "fn a() {
13290 ˇdog();
13291 cat();
13292 }"
13293 ));
13294 cx.update_editor(|editor, window, cx| {
13295 editor.toggle_comments(toggle_comments, window, cx);
13296 });
13297 cx.assert_editor_state(indoc!(
13298 "fn a() {
13299 // dog();
13300 catˇ();
13301 }"
13302 ));
13303
13304 // Single selection on one line -> don't advance
13305 cx.set_state(indoc!(
13306 "fn a() {
13307 «dog()ˇ»;
13308 cat();
13309 }"
13310 ));
13311 cx.update_editor(|editor, window, cx| {
13312 editor.toggle_comments(toggle_comments, window, cx);
13313 });
13314 cx.assert_editor_state(indoc!(
13315 "fn a() {
13316 // «dog()ˇ»;
13317 cat();
13318 }"
13319 ));
13320
13321 // Multiple cursors on one line -> advance
13322 cx.set_state(indoc!(
13323 "fn a() {
13324 ˇdˇog();
13325 cat();
13326 }"
13327 ));
13328 cx.update_editor(|editor, window, cx| {
13329 editor.toggle_comments(toggle_comments, window, cx);
13330 });
13331 cx.assert_editor_state(indoc!(
13332 "fn a() {
13333 // dog();
13334 catˇ(ˇ);
13335 }"
13336 ));
13337
13338 // Multiple cursors on one line, with selection -> don't advance
13339 cx.set_state(indoc!(
13340 "fn a() {
13341 ˇdˇog«()ˇ»;
13342 cat();
13343 }"
13344 ));
13345 cx.update_editor(|editor, window, cx| {
13346 editor.toggle_comments(toggle_comments, window, cx);
13347 });
13348 cx.assert_editor_state(indoc!(
13349 "fn a() {
13350 // ˇdˇog«()ˇ»;
13351 cat();
13352 }"
13353 ));
13354
13355 // Single cursor on one line -> advance
13356 // Cursor moves to column 0 on blank line
13357 cx.set_state(indoc!(
13358 "fn a() {
13359 ˇdog();
13360
13361 cat();
13362 }"
13363 ));
13364 cx.update_editor(|editor, window, cx| {
13365 editor.toggle_comments(toggle_comments, window, cx);
13366 });
13367 cx.assert_editor_state(indoc!(
13368 "fn a() {
13369 // dog();
13370 ˇ
13371 cat();
13372 }"
13373 ));
13374
13375 // Single cursor on one line -> advance
13376 // Cursor starts and ends at column 0
13377 cx.set_state(indoc!(
13378 "fn a() {
13379 ˇ dog();
13380 cat();
13381 }"
13382 ));
13383 cx.update_editor(|editor, window, cx| {
13384 editor.toggle_comments(toggle_comments, window, cx);
13385 });
13386 cx.assert_editor_state(indoc!(
13387 "fn a() {
13388 // dog();
13389 ˇ cat();
13390 }"
13391 ));
13392}
13393
13394#[gpui::test]
13395async fn test_toggle_block_comment(cx: &mut TestAppContext) {
13396 init_test(cx, |_| {});
13397
13398 let mut cx = EditorTestContext::new(cx).await;
13399
13400 let html_language = Arc::new(
13401 Language::new(
13402 LanguageConfig {
13403 name: "HTML".into(),
13404 block_comment: Some(("<!-- ".into(), " -->".into())),
13405 ..Default::default()
13406 },
13407 Some(tree_sitter_html::LANGUAGE.into()),
13408 )
13409 .with_injection_query(
13410 r#"
13411 (script_element
13412 (raw_text) @injection.content
13413 (#set! injection.language "javascript"))
13414 "#,
13415 )
13416 .unwrap(),
13417 );
13418
13419 let javascript_language = Arc::new(Language::new(
13420 LanguageConfig {
13421 name: "JavaScript".into(),
13422 line_comments: vec!["// ".into()],
13423 ..Default::default()
13424 },
13425 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
13426 ));
13427
13428 cx.language_registry().add(html_language.clone());
13429 cx.language_registry().add(javascript_language.clone());
13430 cx.update_buffer(|buffer, cx| {
13431 buffer.set_language(Some(html_language), cx);
13432 });
13433
13434 // Toggle comments for empty selections
13435 cx.set_state(
13436 &r#"
13437 <p>A</p>ˇ
13438 <p>B</p>ˇ
13439 <p>C</p>ˇ
13440 "#
13441 .unindent(),
13442 );
13443 cx.update_editor(|editor, window, cx| {
13444 editor.toggle_comments(&ToggleComments::default(), window, cx)
13445 });
13446 cx.assert_editor_state(
13447 &r#"
13448 <!-- <p>A</p>ˇ -->
13449 <!-- <p>B</p>ˇ -->
13450 <!-- <p>C</p>ˇ -->
13451 "#
13452 .unindent(),
13453 );
13454 cx.update_editor(|editor, window, cx| {
13455 editor.toggle_comments(&ToggleComments::default(), window, cx)
13456 });
13457 cx.assert_editor_state(
13458 &r#"
13459 <p>A</p>ˇ
13460 <p>B</p>ˇ
13461 <p>C</p>ˇ
13462 "#
13463 .unindent(),
13464 );
13465
13466 // Toggle comments for mixture of empty and non-empty selections, where
13467 // multiple selections occupy a given line.
13468 cx.set_state(
13469 &r#"
13470 <p>A«</p>
13471 <p>ˇ»B</p>ˇ
13472 <p>C«</p>
13473 <p>ˇ»D</p>ˇ
13474 "#
13475 .unindent(),
13476 );
13477
13478 cx.update_editor(|editor, window, cx| {
13479 editor.toggle_comments(&ToggleComments::default(), window, cx)
13480 });
13481 cx.assert_editor_state(
13482 &r#"
13483 <!-- <p>A«</p>
13484 <p>ˇ»B</p>ˇ -->
13485 <!-- <p>C«</p>
13486 <p>ˇ»D</p>ˇ -->
13487 "#
13488 .unindent(),
13489 );
13490 cx.update_editor(|editor, window, cx| {
13491 editor.toggle_comments(&ToggleComments::default(), window, cx)
13492 });
13493 cx.assert_editor_state(
13494 &r#"
13495 <p>A«</p>
13496 <p>ˇ»B</p>ˇ
13497 <p>C«</p>
13498 <p>ˇ»D</p>ˇ
13499 "#
13500 .unindent(),
13501 );
13502
13503 // Toggle comments when different languages are active for different
13504 // selections.
13505 cx.set_state(
13506 &r#"
13507 ˇ<script>
13508 ˇvar x = new Y();
13509 ˇ</script>
13510 "#
13511 .unindent(),
13512 );
13513 cx.executor().run_until_parked();
13514 cx.update_editor(|editor, window, cx| {
13515 editor.toggle_comments(&ToggleComments::default(), window, cx)
13516 });
13517 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
13518 // Uncommenting and commenting from this position brings in even more wrong artifacts.
13519 cx.assert_editor_state(
13520 &r#"
13521 <!-- ˇ<script> -->
13522 // ˇvar x = new Y();
13523 <!-- ˇ</script> -->
13524 "#
13525 .unindent(),
13526 );
13527}
13528
13529#[gpui::test]
13530fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
13531 init_test(cx, |_| {});
13532
13533 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
13534 let multibuffer = cx.new(|cx| {
13535 let mut multibuffer = MultiBuffer::new(ReadWrite);
13536 multibuffer.push_excerpts(
13537 buffer.clone(),
13538 [
13539 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
13540 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
13541 ],
13542 cx,
13543 );
13544 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
13545 multibuffer
13546 });
13547
13548 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
13549 editor.update_in(cx, |editor, window, cx| {
13550 assert_eq!(editor.text(cx), "aaaa\nbbbb");
13551 editor.change_selections(None, window, cx, |s| {
13552 s.select_ranges([
13553 Point::new(0, 0)..Point::new(0, 0),
13554 Point::new(1, 0)..Point::new(1, 0),
13555 ])
13556 });
13557
13558 editor.handle_input("X", window, cx);
13559 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
13560 assert_eq!(
13561 editor.selections.ranges(cx),
13562 [
13563 Point::new(0, 1)..Point::new(0, 1),
13564 Point::new(1, 1)..Point::new(1, 1),
13565 ]
13566 );
13567
13568 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
13569 editor.change_selections(None, window, cx, |s| {
13570 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
13571 });
13572 editor.backspace(&Default::default(), window, cx);
13573 assert_eq!(editor.text(cx), "Xa\nbbb");
13574 assert_eq!(
13575 editor.selections.ranges(cx),
13576 [Point::new(1, 0)..Point::new(1, 0)]
13577 );
13578
13579 editor.change_selections(None, window, cx, |s| {
13580 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
13581 });
13582 editor.backspace(&Default::default(), window, cx);
13583 assert_eq!(editor.text(cx), "X\nbb");
13584 assert_eq!(
13585 editor.selections.ranges(cx),
13586 [Point::new(0, 1)..Point::new(0, 1)]
13587 );
13588 });
13589}
13590
13591#[gpui::test]
13592fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
13593 init_test(cx, |_| {});
13594
13595 let markers = vec![('[', ']').into(), ('(', ')').into()];
13596 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
13597 indoc! {"
13598 [aaaa
13599 (bbbb]
13600 cccc)",
13601 },
13602 markers.clone(),
13603 );
13604 let excerpt_ranges = markers.into_iter().map(|marker| {
13605 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
13606 ExcerptRange::new(context.clone())
13607 });
13608 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
13609 let multibuffer = cx.new(|cx| {
13610 let mut multibuffer = MultiBuffer::new(ReadWrite);
13611 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
13612 multibuffer
13613 });
13614
13615 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
13616 editor.update_in(cx, |editor, window, cx| {
13617 let (expected_text, selection_ranges) = marked_text_ranges(
13618 indoc! {"
13619 aaaa
13620 bˇbbb
13621 bˇbbˇb
13622 cccc"
13623 },
13624 true,
13625 );
13626 assert_eq!(editor.text(cx), expected_text);
13627 editor.change_selections(None, window, cx, |s| s.select_ranges(selection_ranges));
13628
13629 editor.handle_input("X", window, cx);
13630
13631 let (expected_text, expected_selections) = marked_text_ranges(
13632 indoc! {"
13633 aaaa
13634 bXˇbbXb
13635 bXˇbbXˇb
13636 cccc"
13637 },
13638 false,
13639 );
13640 assert_eq!(editor.text(cx), expected_text);
13641 assert_eq!(editor.selections.ranges(cx), expected_selections);
13642
13643 editor.newline(&Newline, window, cx);
13644 let (expected_text, expected_selections) = marked_text_ranges(
13645 indoc! {"
13646 aaaa
13647 bX
13648 ˇbbX
13649 b
13650 bX
13651 ˇbbX
13652 ˇb
13653 cccc"
13654 },
13655 false,
13656 );
13657 assert_eq!(editor.text(cx), expected_text);
13658 assert_eq!(editor.selections.ranges(cx), expected_selections);
13659 });
13660}
13661
13662#[gpui::test]
13663fn test_refresh_selections(cx: &mut TestAppContext) {
13664 init_test(cx, |_| {});
13665
13666 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
13667 let mut excerpt1_id = None;
13668 let multibuffer = cx.new(|cx| {
13669 let mut multibuffer = MultiBuffer::new(ReadWrite);
13670 excerpt1_id = multibuffer
13671 .push_excerpts(
13672 buffer.clone(),
13673 [
13674 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
13675 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
13676 ],
13677 cx,
13678 )
13679 .into_iter()
13680 .next();
13681 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
13682 multibuffer
13683 });
13684
13685 let editor = cx.add_window(|window, cx| {
13686 let mut editor = build_editor(multibuffer.clone(), window, cx);
13687 let snapshot = editor.snapshot(window, cx);
13688 editor.change_selections(None, window, cx, |s| {
13689 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
13690 });
13691 editor.begin_selection(
13692 Point::new(2, 1).to_display_point(&snapshot),
13693 true,
13694 1,
13695 window,
13696 cx,
13697 );
13698 assert_eq!(
13699 editor.selections.ranges(cx),
13700 [
13701 Point::new(1, 3)..Point::new(1, 3),
13702 Point::new(2, 1)..Point::new(2, 1),
13703 ]
13704 );
13705 editor
13706 });
13707
13708 // Refreshing selections is a no-op when excerpts haven't changed.
13709 _ = editor.update(cx, |editor, window, cx| {
13710 editor.change_selections(None, window, cx, |s| s.refresh());
13711 assert_eq!(
13712 editor.selections.ranges(cx),
13713 [
13714 Point::new(1, 3)..Point::new(1, 3),
13715 Point::new(2, 1)..Point::new(2, 1),
13716 ]
13717 );
13718 });
13719
13720 multibuffer.update(cx, |multibuffer, cx| {
13721 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
13722 });
13723 _ = editor.update(cx, |editor, window, cx| {
13724 // Removing an excerpt causes the first selection to become degenerate.
13725 assert_eq!(
13726 editor.selections.ranges(cx),
13727 [
13728 Point::new(0, 0)..Point::new(0, 0),
13729 Point::new(0, 1)..Point::new(0, 1)
13730 ]
13731 );
13732
13733 // Refreshing selections will relocate the first selection to the original buffer
13734 // location.
13735 editor.change_selections(None, window, cx, |s| s.refresh());
13736 assert_eq!(
13737 editor.selections.ranges(cx),
13738 [
13739 Point::new(0, 1)..Point::new(0, 1),
13740 Point::new(0, 3)..Point::new(0, 3)
13741 ]
13742 );
13743 assert!(editor.selections.pending_anchor().is_some());
13744 });
13745}
13746
13747#[gpui::test]
13748fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
13749 init_test(cx, |_| {});
13750
13751 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
13752 let mut excerpt1_id = None;
13753 let multibuffer = cx.new(|cx| {
13754 let mut multibuffer = MultiBuffer::new(ReadWrite);
13755 excerpt1_id = multibuffer
13756 .push_excerpts(
13757 buffer.clone(),
13758 [
13759 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
13760 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
13761 ],
13762 cx,
13763 )
13764 .into_iter()
13765 .next();
13766 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
13767 multibuffer
13768 });
13769
13770 let editor = cx.add_window(|window, cx| {
13771 let mut editor = build_editor(multibuffer.clone(), window, cx);
13772 let snapshot = editor.snapshot(window, cx);
13773 editor.begin_selection(
13774 Point::new(1, 3).to_display_point(&snapshot),
13775 false,
13776 1,
13777 window,
13778 cx,
13779 );
13780 assert_eq!(
13781 editor.selections.ranges(cx),
13782 [Point::new(1, 3)..Point::new(1, 3)]
13783 );
13784 editor
13785 });
13786
13787 multibuffer.update(cx, |multibuffer, cx| {
13788 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
13789 });
13790 _ = editor.update(cx, |editor, window, cx| {
13791 assert_eq!(
13792 editor.selections.ranges(cx),
13793 [Point::new(0, 0)..Point::new(0, 0)]
13794 );
13795
13796 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
13797 editor.change_selections(None, window, cx, |s| s.refresh());
13798 assert_eq!(
13799 editor.selections.ranges(cx),
13800 [Point::new(0, 3)..Point::new(0, 3)]
13801 );
13802 assert!(editor.selections.pending_anchor().is_some());
13803 });
13804}
13805
13806#[gpui::test]
13807async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
13808 init_test(cx, |_| {});
13809
13810 let language = Arc::new(
13811 Language::new(
13812 LanguageConfig {
13813 brackets: BracketPairConfig {
13814 pairs: vec![
13815 BracketPair {
13816 start: "{".to_string(),
13817 end: "}".to_string(),
13818 close: true,
13819 surround: true,
13820 newline: true,
13821 },
13822 BracketPair {
13823 start: "/* ".to_string(),
13824 end: " */".to_string(),
13825 close: true,
13826 surround: true,
13827 newline: true,
13828 },
13829 ],
13830 ..Default::default()
13831 },
13832 ..Default::default()
13833 },
13834 Some(tree_sitter_rust::LANGUAGE.into()),
13835 )
13836 .with_indents_query("")
13837 .unwrap(),
13838 );
13839
13840 let text = concat!(
13841 "{ }\n", //
13842 " x\n", //
13843 " /* */\n", //
13844 "x\n", //
13845 "{{} }\n", //
13846 );
13847
13848 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
13849 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
13850 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
13851 editor
13852 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
13853 .await;
13854
13855 editor.update_in(cx, |editor, window, cx| {
13856 editor.change_selections(None, window, cx, |s| {
13857 s.select_display_ranges([
13858 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
13859 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
13860 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
13861 ])
13862 });
13863 editor.newline(&Newline, window, cx);
13864
13865 assert_eq!(
13866 editor.buffer().read(cx).read(cx).text(),
13867 concat!(
13868 "{ \n", // Suppress rustfmt
13869 "\n", //
13870 "}\n", //
13871 " x\n", //
13872 " /* \n", //
13873 " \n", //
13874 " */\n", //
13875 "x\n", //
13876 "{{} \n", //
13877 "}\n", //
13878 )
13879 );
13880 });
13881}
13882
13883#[gpui::test]
13884fn test_highlighted_ranges(cx: &mut TestAppContext) {
13885 init_test(cx, |_| {});
13886
13887 let editor = cx.add_window(|window, cx| {
13888 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
13889 build_editor(buffer.clone(), window, cx)
13890 });
13891
13892 _ = editor.update(cx, |editor, window, cx| {
13893 struct Type1;
13894 struct Type2;
13895
13896 let buffer = editor.buffer.read(cx).snapshot(cx);
13897
13898 let anchor_range =
13899 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
13900
13901 editor.highlight_background::<Type1>(
13902 &[
13903 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
13904 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
13905 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
13906 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
13907 ],
13908 |_| Hsla::red(),
13909 cx,
13910 );
13911 editor.highlight_background::<Type2>(
13912 &[
13913 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
13914 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
13915 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
13916 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
13917 ],
13918 |_| Hsla::green(),
13919 cx,
13920 );
13921
13922 let snapshot = editor.snapshot(window, cx);
13923 let mut highlighted_ranges = editor.background_highlights_in_range(
13924 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
13925 &snapshot,
13926 cx.theme(),
13927 );
13928 // Enforce a consistent ordering based on color without relying on the ordering of the
13929 // highlight's `TypeId` which is non-executor.
13930 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
13931 assert_eq!(
13932 highlighted_ranges,
13933 &[
13934 (
13935 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
13936 Hsla::red(),
13937 ),
13938 (
13939 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
13940 Hsla::red(),
13941 ),
13942 (
13943 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
13944 Hsla::green(),
13945 ),
13946 (
13947 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
13948 Hsla::green(),
13949 ),
13950 ]
13951 );
13952 assert_eq!(
13953 editor.background_highlights_in_range(
13954 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
13955 &snapshot,
13956 cx.theme(),
13957 ),
13958 &[(
13959 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
13960 Hsla::red(),
13961 )]
13962 );
13963 });
13964}
13965
13966#[gpui::test]
13967async fn test_following(cx: &mut TestAppContext) {
13968 init_test(cx, |_| {});
13969
13970 let fs = FakeFs::new(cx.executor());
13971 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
13972
13973 let buffer = project.update(cx, |project, cx| {
13974 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
13975 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
13976 });
13977 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
13978 let follower = cx.update(|cx| {
13979 cx.open_window(
13980 WindowOptions {
13981 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
13982 gpui::Point::new(px(0.), px(0.)),
13983 gpui::Point::new(px(10.), px(80.)),
13984 ))),
13985 ..Default::default()
13986 },
13987 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
13988 )
13989 .unwrap()
13990 });
13991
13992 let is_still_following = Rc::new(RefCell::new(true));
13993 let follower_edit_event_count = Rc::new(RefCell::new(0));
13994 let pending_update = Rc::new(RefCell::new(None));
13995 let leader_entity = leader.root(cx).unwrap();
13996 let follower_entity = follower.root(cx).unwrap();
13997 _ = follower.update(cx, {
13998 let update = pending_update.clone();
13999 let is_still_following = is_still_following.clone();
14000 let follower_edit_event_count = follower_edit_event_count.clone();
14001 |_, window, cx| {
14002 cx.subscribe_in(
14003 &leader_entity,
14004 window,
14005 move |_, leader, event, window, cx| {
14006 leader.read(cx).add_event_to_update_proto(
14007 event,
14008 &mut update.borrow_mut(),
14009 window,
14010 cx,
14011 );
14012 },
14013 )
14014 .detach();
14015
14016 cx.subscribe_in(
14017 &follower_entity,
14018 window,
14019 move |_, _, event: &EditorEvent, _window, _cx| {
14020 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
14021 *is_still_following.borrow_mut() = false;
14022 }
14023
14024 if let EditorEvent::BufferEdited = event {
14025 *follower_edit_event_count.borrow_mut() += 1;
14026 }
14027 },
14028 )
14029 .detach();
14030 }
14031 });
14032
14033 // Update the selections only
14034 _ = leader.update(cx, |leader, window, cx| {
14035 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
14036 });
14037 follower
14038 .update(cx, |follower, window, cx| {
14039 follower.apply_update_proto(
14040 &project,
14041 pending_update.borrow_mut().take().unwrap(),
14042 window,
14043 cx,
14044 )
14045 })
14046 .unwrap()
14047 .await
14048 .unwrap();
14049 _ = follower.update(cx, |follower, _, cx| {
14050 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
14051 });
14052 assert!(*is_still_following.borrow());
14053 assert_eq!(*follower_edit_event_count.borrow(), 0);
14054
14055 // Update the scroll position only
14056 _ = leader.update(cx, |leader, window, cx| {
14057 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
14058 });
14059 follower
14060 .update(cx, |follower, window, cx| {
14061 follower.apply_update_proto(
14062 &project,
14063 pending_update.borrow_mut().take().unwrap(),
14064 window,
14065 cx,
14066 )
14067 })
14068 .unwrap()
14069 .await
14070 .unwrap();
14071 assert_eq!(
14072 follower
14073 .update(cx, |follower, _, cx| follower.scroll_position(cx))
14074 .unwrap(),
14075 gpui::Point::new(1.5, 3.5)
14076 );
14077 assert!(*is_still_following.borrow());
14078 assert_eq!(*follower_edit_event_count.borrow(), 0);
14079
14080 // Update the selections and scroll position. The follower's scroll position is updated
14081 // via autoscroll, not via the leader's exact scroll position.
14082 _ = leader.update(cx, |leader, window, cx| {
14083 leader.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
14084 leader.request_autoscroll(Autoscroll::newest(), cx);
14085 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
14086 });
14087 follower
14088 .update(cx, |follower, window, cx| {
14089 follower.apply_update_proto(
14090 &project,
14091 pending_update.borrow_mut().take().unwrap(),
14092 window,
14093 cx,
14094 )
14095 })
14096 .unwrap()
14097 .await
14098 .unwrap();
14099 _ = follower.update(cx, |follower, _, cx| {
14100 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
14101 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
14102 });
14103 assert!(*is_still_following.borrow());
14104
14105 // Creating a pending selection that precedes another selection
14106 _ = leader.update(cx, |leader, window, cx| {
14107 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
14108 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
14109 });
14110 follower
14111 .update(cx, |follower, window, cx| {
14112 follower.apply_update_proto(
14113 &project,
14114 pending_update.borrow_mut().take().unwrap(),
14115 window,
14116 cx,
14117 )
14118 })
14119 .unwrap()
14120 .await
14121 .unwrap();
14122 _ = follower.update(cx, |follower, _, cx| {
14123 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
14124 });
14125 assert!(*is_still_following.borrow());
14126
14127 // Extend the pending selection so that it surrounds another selection
14128 _ = leader.update(cx, |leader, window, cx| {
14129 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
14130 });
14131 follower
14132 .update(cx, |follower, window, cx| {
14133 follower.apply_update_proto(
14134 &project,
14135 pending_update.borrow_mut().take().unwrap(),
14136 window,
14137 cx,
14138 )
14139 })
14140 .unwrap()
14141 .await
14142 .unwrap();
14143 _ = follower.update(cx, |follower, _, cx| {
14144 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
14145 });
14146
14147 // Scrolling locally breaks the follow
14148 _ = follower.update(cx, |follower, window, cx| {
14149 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
14150 follower.set_scroll_anchor(
14151 ScrollAnchor {
14152 anchor: top_anchor,
14153 offset: gpui::Point::new(0.0, 0.5),
14154 },
14155 window,
14156 cx,
14157 );
14158 });
14159 assert!(!(*is_still_following.borrow()));
14160}
14161
14162#[gpui::test]
14163async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
14164 init_test(cx, |_| {});
14165
14166 let fs = FakeFs::new(cx.executor());
14167 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
14168 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14169 let pane = workspace
14170 .update(cx, |workspace, _, _| workspace.active_pane().clone())
14171 .unwrap();
14172
14173 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
14174
14175 let leader = pane.update_in(cx, |_, window, cx| {
14176 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
14177 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
14178 });
14179
14180 // Start following the editor when it has no excerpts.
14181 let mut state_message =
14182 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
14183 let workspace_entity = workspace.root(cx).unwrap();
14184 let follower_1 = cx
14185 .update_window(*workspace.deref(), |_, window, cx| {
14186 Editor::from_state_proto(
14187 workspace_entity,
14188 ViewId {
14189 creator: CollaboratorId::PeerId(PeerId::default()),
14190 id: 0,
14191 },
14192 &mut state_message,
14193 window,
14194 cx,
14195 )
14196 })
14197 .unwrap()
14198 .unwrap()
14199 .await
14200 .unwrap();
14201
14202 let update_message = Rc::new(RefCell::new(None));
14203 follower_1.update_in(cx, {
14204 let update = update_message.clone();
14205 |_, window, cx| {
14206 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
14207 leader.read(cx).add_event_to_update_proto(
14208 event,
14209 &mut update.borrow_mut(),
14210 window,
14211 cx,
14212 );
14213 })
14214 .detach();
14215 }
14216 });
14217
14218 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
14219 (
14220 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
14221 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
14222 )
14223 });
14224
14225 // Insert some excerpts.
14226 leader.update(cx, |leader, cx| {
14227 leader.buffer.update(cx, |multibuffer, cx| {
14228 multibuffer.set_excerpts_for_path(
14229 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
14230 buffer_1.clone(),
14231 vec![
14232 Point::row_range(0..3),
14233 Point::row_range(1..6),
14234 Point::row_range(12..15),
14235 ],
14236 0,
14237 cx,
14238 );
14239 multibuffer.set_excerpts_for_path(
14240 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
14241 buffer_2.clone(),
14242 vec![Point::row_range(0..6), Point::row_range(8..12)],
14243 0,
14244 cx,
14245 );
14246 });
14247 });
14248
14249 // Apply the update of adding the excerpts.
14250 follower_1
14251 .update_in(cx, |follower, window, cx| {
14252 follower.apply_update_proto(
14253 &project,
14254 update_message.borrow().clone().unwrap(),
14255 window,
14256 cx,
14257 )
14258 })
14259 .await
14260 .unwrap();
14261 assert_eq!(
14262 follower_1.update(cx, |editor, cx| editor.text(cx)),
14263 leader.update(cx, |editor, cx| editor.text(cx))
14264 );
14265 update_message.borrow_mut().take();
14266
14267 // Start following separately after it already has excerpts.
14268 let mut state_message =
14269 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
14270 let workspace_entity = workspace.root(cx).unwrap();
14271 let follower_2 = cx
14272 .update_window(*workspace.deref(), |_, window, cx| {
14273 Editor::from_state_proto(
14274 workspace_entity,
14275 ViewId {
14276 creator: CollaboratorId::PeerId(PeerId::default()),
14277 id: 0,
14278 },
14279 &mut state_message,
14280 window,
14281 cx,
14282 )
14283 })
14284 .unwrap()
14285 .unwrap()
14286 .await
14287 .unwrap();
14288 assert_eq!(
14289 follower_2.update(cx, |editor, cx| editor.text(cx)),
14290 leader.update(cx, |editor, cx| editor.text(cx))
14291 );
14292
14293 // Remove some excerpts.
14294 leader.update(cx, |leader, cx| {
14295 leader.buffer.update(cx, |multibuffer, cx| {
14296 let excerpt_ids = multibuffer.excerpt_ids();
14297 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
14298 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
14299 });
14300 });
14301
14302 // Apply the update of removing the excerpts.
14303 follower_1
14304 .update_in(cx, |follower, window, cx| {
14305 follower.apply_update_proto(
14306 &project,
14307 update_message.borrow().clone().unwrap(),
14308 window,
14309 cx,
14310 )
14311 })
14312 .await
14313 .unwrap();
14314 follower_2
14315 .update_in(cx, |follower, window, cx| {
14316 follower.apply_update_proto(
14317 &project,
14318 update_message.borrow().clone().unwrap(),
14319 window,
14320 cx,
14321 )
14322 })
14323 .await
14324 .unwrap();
14325 update_message.borrow_mut().take();
14326 assert_eq!(
14327 follower_1.update(cx, |editor, cx| editor.text(cx)),
14328 leader.update(cx, |editor, cx| editor.text(cx))
14329 );
14330}
14331
14332#[gpui::test]
14333async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14334 init_test(cx, |_| {});
14335
14336 let mut cx = EditorTestContext::new(cx).await;
14337 let lsp_store =
14338 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
14339
14340 cx.set_state(indoc! {"
14341 ˇfn func(abc def: i32) -> u32 {
14342 }
14343 "});
14344
14345 cx.update(|_, cx| {
14346 lsp_store.update(cx, |lsp_store, cx| {
14347 lsp_store
14348 .update_diagnostics(
14349 LanguageServerId(0),
14350 lsp::PublishDiagnosticsParams {
14351 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
14352 version: None,
14353 diagnostics: vec![
14354 lsp::Diagnostic {
14355 range: lsp::Range::new(
14356 lsp::Position::new(0, 11),
14357 lsp::Position::new(0, 12),
14358 ),
14359 severity: Some(lsp::DiagnosticSeverity::ERROR),
14360 ..Default::default()
14361 },
14362 lsp::Diagnostic {
14363 range: lsp::Range::new(
14364 lsp::Position::new(0, 12),
14365 lsp::Position::new(0, 15),
14366 ),
14367 severity: Some(lsp::DiagnosticSeverity::ERROR),
14368 ..Default::default()
14369 },
14370 lsp::Diagnostic {
14371 range: lsp::Range::new(
14372 lsp::Position::new(0, 25),
14373 lsp::Position::new(0, 28),
14374 ),
14375 severity: Some(lsp::DiagnosticSeverity::ERROR),
14376 ..Default::default()
14377 },
14378 ],
14379 },
14380 None,
14381 DiagnosticSourceKind::Pushed,
14382 &[],
14383 cx,
14384 )
14385 .unwrap()
14386 });
14387 });
14388
14389 executor.run_until_parked();
14390
14391 cx.update_editor(|editor, window, cx| {
14392 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
14393 });
14394
14395 cx.assert_editor_state(indoc! {"
14396 fn func(abc def: i32) -> ˇu32 {
14397 }
14398 "});
14399
14400 cx.update_editor(|editor, window, cx| {
14401 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
14402 });
14403
14404 cx.assert_editor_state(indoc! {"
14405 fn func(abc ˇdef: i32) -> u32 {
14406 }
14407 "});
14408
14409 cx.update_editor(|editor, window, cx| {
14410 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
14411 });
14412
14413 cx.assert_editor_state(indoc! {"
14414 fn func(abcˇ def: i32) -> u32 {
14415 }
14416 "});
14417
14418 cx.update_editor(|editor, window, cx| {
14419 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
14420 });
14421
14422 cx.assert_editor_state(indoc! {"
14423 fn func(abc def: i32) -> ˇu32 {
14424 }
14425 "});
14426}
14427
14428#[gpui::test]
14429async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14430 init_test(cx, |_| {});
14431
14432 let mut cx = EditorTestContext::new(cx).await;
14433
14434 let diff_base = r#"
14435 use some::mod;
14436
14437 const A: u32 = 42;
14438
14439 fn main() {
14440 println!("hello");
14441
14442 println!("world");
14443 }
14444 "#
14445 .unindent();
14446
14447 // Edits are modified, removed, modified, added
14448 cx.set_state(
14449 &r#"
14450 use some::modified;
14451
14452 ˇ
14453 fn main() {
14454 println!("hello there");
14455
14456 println!("around the");
14457 println!("world");
14458 }
14459 "#
14460 .unindent(),
14461 );
14462
14463 cx.set_head_text(&diff_base);
14464 executor.run_until_parked();
14465
14466 cx.update_editor(|editor, window, cx| {
14467 //Wrap around the bottom of the buffer
14468 for _ in 0..3 {
14469 editor.go_to_next_hunk(&GoToHunk, window, cx);
14470 }
14471 });
14472
14473 cx.assert_editor_state(
14474 &r#"
14475 ˇuse some::modified;
14476
14477
14478 fn main() {
14479 println!("hello there");
14480
14481 println!("around the");
14482 println!("world");
14483 }
14484 "#
14485 .unindent(),
14486 );
14487
14488 cx.update_editor(|editor, window, cx| {
14489 //Wrap around the top of the buffer
14490 for _ in 0..2 {
14491 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14492 }
14493 });
14494
14495 cx.assert_editor_state(
14496 &r#"
14497 use some::modified;
14498
14499
14500 fn main() {
14501 ˇ println!("hello there");
14502
14503 println!("around the");
14504 println!("world");
14505 }
14506 "#
14507 .unindent(),
14508 );
14509
14510 cx.update_editor(|editor, window, cx| {
14511 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14512 });
14513
14514 cx.assert_editor_state(
14515 &r#"
14516 use some::modified;
14517
14518 ˇ
14519 fn main() {
14520 println!("hello there");
14521
14522 println!("around the");
14523 println!("world");
14524 }
14525 "#
14526 .unindent(),
14527 );
14528
14529 cx.update_editor(|editor, window, cx| {
14530 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14531 });
14532
14533 cx.assert_editor_state(
14534 &r#"
14535 ˇuse some::modified;
14536
14537
14538 fn main() {
14539 println!("hello there");
14540
14541 println!("around the");
14542 println!("world");
14543 }
14544 "#
14545 .unindent(),
14546 );
14547
14548 cx.update_editor(|editor, window, cx| {
14549 for _ in 0..2 {
14550 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14551 }
14552 });
14553
14554 cx.assert_editor_state(
14555 &r#"
14556 use some::modified;
14557
14558
14559 fn main() {
14560 ˇ println!("hello there");
14561
14562 println!("around the");
14563 println!("world");
14564 }
14565 "#
14566 .unindent(),
14567 );
14568
14569 cx.update_editor(|editor, window, cx| {
14570 editor.fold(&Fold, window, cx);
14571 });
14572
14573 cx.update_editor(|editor, window, cx| {
14574 editor.go_to_next_hunk(&GoToHunk, window, cx);
14575 });
14576
14577 cx.assert_editor_state(
14578 &r#"
14579 ˇuse some::modified;
14580
14581
14582 fn main() {
14583 println!("hello there");
14584
14585 println!("around the");
14586 println!("world");
14587 }
14588 "#
14589 .unindent(),
14590 );
14591}
14592
14593#[test]
14594fn test_split_words() {
14595 fn split(text: &str) -> Vec<&str> {
14596 split_words(text).collect()
14597 }
14598
14599 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
14600 assert_eq!(split("hello_world"), &["hello_", "world"]);
14601 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
14602 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
14603 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
14604 assert_eq!(split("helloworld"), &["helloworld"]);
14605
14606 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
14607}
14608
14609#[gpui::test]
14610async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
14611 init_test(cx, |_| {});
14612
14613 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
14614 let mut assert = |before, after| {
14615 let _state_context = cx.set_state(before);
14616 cx.run_until_parked();
14617 cx.update_editor(|editor, window, cx| {
14618 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
14619 });
14620 cx.run_until_parked();
14621 cx.assert_editor_state(after);
14622 };
14623
14624 // Outside bracket jumps to outside of matching bracket
14625 assert("console.logˇ(var);", "console.log(var)ˇ;");
14626 assert("console.log(var)ˇ;", "console.logˇ(var);");
14627
14628 // Inside bracket jumps to inside of matching bracket
14629 assert("console.log(ˇvar);", "console.log(varˇ);");
14630 assert("console.log(varˇ);", "console.log(ˇvar);");
14631
14632 // When outside a bracket and inside, favor jumping to the inside bracket
14633 assert(
14634 "console.log('foo', [1, 2, 3]ˇ);",
14635 "console.log(ˇ'foo', [1, 2, 3]);",
14636 );
14637 assert(
14638 "console.log(ˇ'foo', [1, 2, 3]);",
14639 "console.log('foo', [1, 2, 3]ˇ);",
14640 );
14641
14642 // Bias forward if two options are equally likely
14643 assert(
14644 "let result = curried_fun()ˇ();",
14645 "let result = curried_fun()()ˇ;",
14646 );
14647
14648 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
14649 assert(
14650 indoc! {"
14651 function test() {
14652 console.log('test')ˇ
14653 }"},
14654 indoc! {"
14655 function test() {
14656 console.logˇ('test')
14657 }"},
14658 );
14659}
14660
14661#[gpui::test]
14662async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
14663 init_test(cx, |_| {});
14664
14665 let fs = FakeFs::new(cx.executor());
14666 fs.insert_tree(
14667 path!("/a"),
14668 json!({
14669 "main.rs": "fn main() { let a = 5; }",
14670 "other.rs": "// Test file",
14671 }),
14672 )
14673 .await;
14674 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14675
14676 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14677 language_registry.add(Arc::new(Language::new(
14678 LanguageConfig {
14679 name: "Rust".into(),
14680 matcher: LanguageMatcher {
14681 path_suffixes: vec!["rs".to_string()],
14682 ..Default::default()
14683 },
14684 brackets: BracketPairConfig {
14685 pairs: vec![BracketPair {
14686 start: "{".to_string(),
14687 end: "}".to_string(),
14688 close: true,
14689 surround: true,
14690 newline: true,
14691 }],
14692 disabled_scopes_by_bracket_ix: Vec::new(),
14693 },
14694 ..Default::default()
14695 },
14696 Some(tree_sitter_rust::LANGUAGE.into()),
14697 )));
14698 let mut fake_servers = language_registry.register_fake_lsp(
14699 "Rust",
14700 FakeLspAdapter {
14701 capabilities: lsp::ServerCapabilities {
14702 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
14703 first_trigger_character: "{".to_string(),
14704 more_trigger_character: None,
14705 }),
14706 ..Default::default()
14707 },
14708 ..Default::default()
14709 },
14710 );
14711
14712 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14713
14714 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14715
14716 let worktree_id = workspace
14717 .update(cx, |workspace, _, cx| {
14718 workspace.project().update(cx, |project, cx| {
14719 project.worktrees(cx).next().unwrap().read(cx).id()
14720 })
14721 })
14722 .unwrap();
14723
14724 let buffer = project
14725 .update(cx, |project, cx| {
14726 project.open_local_buffer(path!("/a/main.rs"), cx)
14727 })
14728 .await
14729 .unwrap();
14730 let editor_handle = workspace
14731 .update(cx, |workspace, window, cx| {
14732 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
14733 })
14734 .unwrap()
14735 .await
14736 .unwrap()
14737 .downcast::<Editor>()
14738 .unwrap();
14739
14740 cx.executor().start_waiting();
14741 let fake_server = fake_servers.next().await.unwrap();
14742
14743 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
14744 |params, _| async move {
14745 assert_eq!(
14746 params.text_document_position.text_document.uri,
14747 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
14748 );
14749 assert_eq!(
14750 params.text_document_position.position,
14751 lsp::Position::new(0, 21),
14752 );
14753
14754 Ok(Some(vec![lsp::TextEdit {
14755 new_text: "]".to_string(),
14756 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14757 }]))
14758 },
14759 );
14760
14761 editor_handle.update_in(cx, |editor, window, cx| {
14762 window.focus(&editor.focus_handle(cx));
14763 editor.change_selections(None, window, cx, |s| {
14764 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
14765 });
14766 editor.handle_input("{", window, cx);
14767 });
14768
14769 cx.executor().run_until_parked();
14770
14771 buffer.update(cx, |buffer, _| {
14772 assert_eq!(
14773 buffer.text(),
14774 "fn main() { let a = {5}; }",
14775 "No extra braces from on type formatting should appear in the buffer"
14776 )
14777 });
14778}
14779
14780#[gpui::test(iterations = 20, seeds(31))]
14781async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
14782 init_test(cx, |_| {});
14783
14784 let mut cx = EditorLspTestContext::new_rust(
14785 lsp::ServerCapabilities {
14786 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
14787 first_trigger_character: ".".to_string(),
14788 more_trigger_character: None,
14789 }),
14790 ..Default::default()
14791 },
14792 cx,
14793 )
14794 .await;
14795
14796 cx.update_buffer(|buffer, _| {
14797 // This causes autoindent to be async.
14798 buffer.set_sync_parse_timeout(Duration::ZERO)
14799 });
14800
14801 cx.set_state("fn c() {\n d()ˇ\n}\n");
14802 cx.simulate_keystroke("\n");
14803 cx.run_until_parked();
14804
14805 let buffer_cloned =
14806 cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap().clone());
14807 let mut request =
14808 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
14809 let buffer_cloned = buffer_cloned.clone();
14810 async move {
14811 buffer_cloned.update(&mut cx, |buffer, _| {
14812 assert_eq!(
14813 buffer.text(),
14814 "fn c() {\n d()\n .\n}\n",
14815 "OnTypeFormatting should triggered after autoindent applied"
14816 )
14817 })?;
14818
14819 Ok(Some(vec![]))
14820 }
14821 });
14822
14823 cx.simulate_keystroke(".");
14824 cx.run_until_parked();
14825
14826 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
14827 assert!(request.next().await.is_some());
14828 request.close();
14829 assert!(request.next().await.is_none());
14830}
14831
14832#[gpui::test]
14833async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
14834 init_test(cx, |_| {});
14835
14836 let fs = FakeFs::new(cx.executor());
14837 fs.insert_tree(
14838 path!("/a"),
14839 json!({
14840 "main.rs": "fn main() { let a = 5; }",
14841 "other.rs": "// Test file",
14842 }),
14843 )
14844 .await;
14845
14846 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14847
14848 let server_restarts = Arc::new(AtomicUsize::new(0));
14849 let closure_restarts = Arc::clone(&server_restarts);
14850 let language_server_name = "test language server";
14851 let language_name: LanguageName = "Rust".into();
14852
14853 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14854 language_registry.add(Arc::new(Language::new(
14855 LanguageConfig {
14856 name: language_name.clone(),
14857 matcher: LanguageMatcher {
14858 path_suffixes: vec!["rs".to_string()],
14859 ..Default::default()
14860 },
14861 ..Default::default()
14862 },
14863 Some(tree_sitter_rust::LANGUAGE.into()),
14864 )));
14865 let mut fake_servers = language_registry.register_fake_lsp(
14866 "Rust",
14867 FakeLspAdapter {
14868 name: language_server_name,
14869 initialization_options: Some(json!({
14870 "testOptionValue": true
14871 })),
14872 initializer: Some(Box::new(move |fake_server| {
14873 let task_restarts = Arc::clone(&closure_restarts);
14874 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
14875 task_restarts.fetch_add(1, atomic::Ordering::Release);
14876 futures::future::ready(Ok(()))
14877 });
14878 })),
14879 ..Default::default()
14880 },
14881 );
14882
14883 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14884 let _buffer = project
14885 .update(cx, |project, cx| {
14886 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
14887 })
14888 .await
14889 .unwrap();
14890 let _fake_server = fake_servers.next().await.unwrap();
14891 update_test_language_settings(cx, |language_settings| {
14892 language_settings.languages.insert(
14893 language_name.clone(),
14894 LanguageSettingsContent {
14895 tab_size: NonZeroU32::new(8),
14896 ..Default::default()
14897 },
14898 );
14899 });
14900 cx.executor().run_until_parked();
14901 assert_eq!(
14902 server_restarts.load(atomic::Ordering::Acquire),
14903 0,
14904 "Should not restart LSP server on an unrelated change"
14905 );
14906
14907 update_test_project_settings(cx, |project_settings| {
14908 project_settings.lsp.insert(
14909 "Some other server name".into(),
14910 LspSettings {
14911 binary: None,
14912 settings: None,
14913 initialization_options: Some(json!({
14914 "some other init value": false
14915 })),
14916 enable_lsp_tasks: false,
14917 },
14918 );
14919 });
14920 cx.executor().run_until_parked();
14921 assert_eq!(
14922 server_restarts.load(atomic::Ordering::Acquire),
14923 0,
14924 "Should not restart LSP server on an unrelated LSP settings change"
14925 );
14926
14927 update_test_project_settings(cx, |project_settings| {
14928 project_settings.lsp.insert(
14929 language_server_name.into(),
14930 LspSettings {
14931 binary: None,
14932 settings: None,
14933 initialization_options: Some(json!({
14934 "anotherInitValue": false
14935 })),
14936 enable_lsp_tasks: false,
14937 },
14938 );
14939 });
14940 cx.executor().run_until_parked();
14941 assert_eq!(
14942 server_restarts.load(atomic::Ordering::Acquire),
14943 1,
14944 "Should restart LSP server on a related LSP settings change"
14945 );
14946
14947 update_test_project_settings(cx, |project_settings| {
14948 project_settings.lsp.insert(
14949 language_server_name.into(),
14950 LspSettings {
14951 binary: None,
14952 settings: None,
14953 initialization_options: Some(json!({
14954 "anotherInitValue": false
14955 })),
14956 enable_lsp_tasks: false,
14957 },
14958 );
14959 });
14960 cx.executor().run_until_parked();
14961 assert_eq!(
14962 server_restarts.load(atomic::Ordering::Acquire),
14963 1,
14964 "Should not restart LSP server on a related LSP settings change that is the same"
14965 );
14966
14967 update_test_project_settings(cx, |project_settings| {
14968 project_settings.lsp.insert(
14969 language_server_name.into(),
14970 LspSettings {
14971 binary: None,
14972 settings: None,
14973 initialization_options: None,
14974 enable_lsp_tasks: false,
14975 },
14976 );
14977 });
14978 cx.executor().run_until_parked();
14979 assert_eq!(
14980 server_restarts.load(atomic::Ordering::Acquire),
14981 2,
14982 "Should restart LSP server on another related LSP settings change"
14983 );
14984}
14985
14986#[gpui::test]
14987async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
14988 init_test(cx, |_| {});
14989
14990 let mut cx = EditorLspTestContext::new_rust(
14991 lsp::ServerCapabilities {
14992 completion_provider: Some(lsp::CompletionOptions {
14993 trigger_characters: Some(vec![".".to_string()]),
14994 resolve_provider: Some(true),
14995 ..Default::default()
14996 }),
14997 ..Default::default()
14998 },
14999 cx,
15000 )
15001 .await;
15002
15003 cx.set_state("fn main() { let a = 2ˇ; }");
15004 cx.simulate_keystroke(".");
15005 let completion_item = lsp::CompletionItem {
15006 label: "some".into(),
15007 kind: Some(lsp::CompletionItemKind::SNIPPET),
15008 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
15009 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
15010 kind: lsp::MarkupKind::Markdown,
15011 value: "```rust\nSome(2)\n```".to_string(),
15012 })),
15013 deprecated: Some(false),
15014 sort_text: Some("fffffff2".to_string()),
15015 filter_text: Some("some".to_string()),
15016 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15017 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15018 range: lsp::Range {
15019 start: lsp::Position {
15020 line: 0,
15021 character: 22,
15022 },
15023 end: lsp::Position {
15024 line: 0,
15025 character: 22,
15026 },
15027 },
15028 new_text: "Some(2)".to_string(),
15029 })),
15030 additional_text_edits: Some(vec![lsp::TextEdit {
15031 range: lsp::Range {
15032 start: lsp::Position {
15033 line: 0,
15034 character: 20,
15035 },
15036 end: lsp::Position {
15037 line: 0,
15038 character: 22,
15039 },
15040 },
15041 new_text: "".to_string(),
15042 }]),
15043 ..Default::default()
15044 };
15045
15046 let closure_completion_item = completion_item.clone();
15047 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15048 let task_completion_item = closure_completion_item.clone();
15049 async move {
15050 Ok(Some(lsp::CompletionResponse::Array(vec![
15051 task_completion_item,
15052 ])))
15053 }
15054 });
15055
15056 request.next().await;
15057
15058 cx.condition(|editor, _| editor.context_menu_visible())
15059 .await;
15060 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
15061 editor
15062 .confirm_completion(&ConfirmCompletion::default(), window, cx)
15063 .unwrap()
15064 });
15065 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
15066
15067 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
15068 let task_completion_item = completion_item.clone();
15069 async move { Ok(task_completion_item) }
15070 })
15071 .next()
15072 .await
15073 .unwrap();
15074 apply_additional_edits.await.unwrap();
15075 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
15076}
15077
15078#[gpui::test]
15079async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
15080 init_test(cx, |_| {});
15081
15082 let mut cx = EditorLspTestContext::new_rust(
15083 lsp::ServerCapabilities {
15084 completion_provider: Some(lsp::CompletionOptions {
15085 trigger_characters: Some(vec![".".to_string()]),
15086 resolve_provider: Some(true),
15087 ..Default::default()
15088 }),
15089 ..Default::default()
15090 },
15091 cx,
15092 )
15093 .await;
15094
15095 cx.set_state("fn main() { let a = 2ˇ; }");
15096 cx.simulate_keystroke(".");
15097
15098 let item1 = lsp::CompletionItem {
15099 label: "method id()".to_string(),
15100 filter_text: Some("id".to_string()),
15101 detail: None,
15102 documentation: None,
15103 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15104 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15105 new_text: ".id".to_string(),
15106 })),
15107 ..lsp::CompletionItem::default()
15108 };
15109
15110 let item2 = lsp::CompletionItem {
15111 label: "other".to_string(),
15112 filter_text: Some("other".to_string()),
15113 detail: None,
15114 documentation: None,
15115 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15116 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15117 new_text: ".other".to_string(),
15118 })),
15119 ..lsp::CompletionItem::default()
15120 };
15121
15122 let item1 = item1.clone();
15123 cx.set_request_handler::<lsp::request::Completion, _, _>({
15124 let item1 = item1.clone();
15125 move |_, _, _| {
15126 let item1 = item1.clone();
15127 let item2 = item2.clone();
15128 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
15129 }
15130 })
15131 .next()
15132 .await;
15133
15134 cx.condition(|editor, _| editor.context_menu_visible())
15135 .await;
15136 cx.update_editor(|editor, _, _| {
15137 let context_menu = editor.context_menu.borrow_mut();
15138 let context_menu = context_menu
15139 .as_ref()
15140 .expect("Should have the context menu deployed");
15141 match context_menu {
15142 CodeContextMenu::Completions(completions_menu) => {
15143 let completions = completions_menu.completions.borrow_mut();
15144 assert_eq!(
15145 completions
15146 .iter()
15147 .map(|completion| &completion.label.text)
15148 .collect::<Vec<_>>(),
15149 vec!["method id()", "other"]
15150 )
15151 }
15152 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
15153 }
15154 });
15155
15156 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
15157 let item1 = item1.clone();
15158 move |_, item_to_resolve, _| {
15159 let item1 = item1.clone();
15160 async move {
15161 if item1 == item_to_resolve {
15162 Ok(lsp::CompletionItem {
15163 label: "method id()".to_string(),
15164 filter_text: Some("id".to_string()),
15165 detail: Some("Now resolved!".to_string()),
15166 documentation: Some(lsp::Documentation::String("Docs".to_string())),
15167 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15168 range: lsp::Range::new(
15169 lsp::Position::new(0, 22),
15170 lsp::Position::new(0, 22),
15171 ),
15172 new_text: ".id".to_string(),
15173 })),
15174 ..lsp::CompletionItem::default()
15175 })
15176 } else {
15177 Ok(item_to_resolve)
15178 }
15179 }
15180 }
15181 })
15182 .next()
15183 .await
15184 .unwrap();
15185 cx.run_until_parked();
15186
15187 cx.update_editor(|editor, window, cx| {
15188 editor.context_menu_next(&Default::default(), window, cx);
15189 });
15190
15191 cx.update_editor(|editor, _, _| {
15192 let context_menu = editor.context_menu.borrow_mut();
15193 let context_menu = context_menu
15194 .as_ref()
15195 .expect("Should have the context menu deployed");
15196 match context_menu {
15197 CodeContextMenu::Completions(completions_menu) => {
15198 let completions = completions_menu.completions.borrow_mut();
15199 assert_eq!(
15200 completions
15201 .iter()
15202 .map(|completion| &completion.label.text)
15203 .collect::<Vec<_>>(),
15204 vec!["method id() Now resolved!", "other"],
15205 "Should update first completion label, but not second as the filter text did not match."
15206 );
15207 }
15208 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
15209 }
15210 });
15211}
15212
15213#[gpui::test]
15214async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
15215 init_test(cx, |_| {});
15216 let mut cx = EditorLspTestContext::new_rust(
15217 lsp::ServerCapabilities {
15218 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
15219 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
15220 completion_provider: Some(lsp::CompletionOptions {
15221 resolve_provider: Some(true),
15222 ..Default::default()
15223 }),
15224 ..Default::default()
15225 },
15226 cx,
15227 )
15228 .await;
15229 cx.set_state(indoc! {"
15230 struct TestStruct {
15231 field: i32
15232 }
15233
15234 fn mainˇ() {
15235 let unused_var = 42;
15236 let test_struct = TestStruct { field: 42 };
15237 }
15238 "});
15239 let symbol_range = cx.lsp_range(indoc! {"
15240 struct TestStruct {
15241 field: i32
15242 }
15243
15244 «fn main»() {
15245 let unused_var = 42;
15246 let test_struct = TestStruct { field: 42 };
15247 }
15248 "});
15249 let mut hover_requests =
15250 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
15251 Ok(Some(lsp::Hover {
15252 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
15253 kind: lsp::MarkupKind::Markdown,
15254 value: "Function documentation".to_string(),
15255 }),
15256 range: Some(symbol_range),
15257 }))
15258 });
15259
15260 // Case 1: Test that code action menu hide hover popover
15261 cx.dispatch_action(Hover);
15262 hover_requests.next().await;
15263 cx.condition(|editor, _| editor.hover_state.visible()).await;
15264 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
15265 move |_, _, _| async move {
15266 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
15267 lsp::CodeAction {
15268 title: "Remove unused variable".to_string(),
15269 kind: Some(CodeActionKind::QUICKFIX),
15270 edit: Some(lsp::WorkspaceEdit {
15271 changes: Some(
15272 [(
15273 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
15274 vec![lsp::TextEdit {
15275 range: lsp::Range::new(
15276 lsp::Position::new(5, 4),
15277 lsp::Position::new(5, 27),
15278 ),
15279 new_text: "".to_string(),
15280 }],
15281 )]
15282 .into_iter()
15283 .collect(),
15284 ),
15285 ..Default::default()
15286 }),
15287 ..Default::default()
15288 },
15289 )]))
15290 },
15291 );
15292 cx.update_editor(|editor, window, cx| {
15293 editor.toggle_code_actions(
15294 &ToggleCodeActions {
15295 deployed_from: None,
15296 quick_launch: false,
15297 },
15298 window,
15299 cx,
15300 );
15301 });
15302 code_action_requests.next().await;
15303 cx.run_until_parked();
15304 cx.condition(|editor, _| editor.context_menu_visible())
15305 .await;
15306 cx.update_editor(|editor, _, _| {
15307 assert!(
15308 !editor.hover_state.visible(),
15309 "Hover popover should be hidden when code action menu is shown"
15310 );
15311 // Hide code actions
15312 editor.context_menu.take();
15313 });
15314
15315 // Case 2: Test that code completions hide hover popover
15316 cx.dispatch_action(Hover);
15317 hover_requests.next().await;
15318 cx.condition(|editor, _| editor.hover_state.visible()).await;
15319 let counter = Arc::new(AtomicUsize::new(0));
15320 let mut completion_requests =
15321 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15322 let counter = counter.clone();
15323 async move {
15324 counter.fetch_add(1, atomic::Ordering::Release);
15325 Ok(Some(lsp::CompletionResponse::Array(vec![
15326 lsp::CompletionItem {
15327 label: "main".into(),
15328 kind: Some(lsp::CompletionItemKind::FUNCTION),
15329 detail: Some("() -> ()".to_string()),
15330 ..Default::default()
15331 },
15332 lsp::CompletionItem {
15333 label: "TestStruct".into(),
15334 kind: Some(lsp::CompletionItemKind::STRUCT),
15335 detail: Some("struct TestStruct".to_string()),
15336 ..Default::default()
15337 },
15338 ])))
15339 }
15340 });
15341 cx.update_editor(|editor, window, cx| {
15342 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15343 });
15344 completion_requests.next().await;
15345 cx.condition(|editor, _| editor.context_menu_visible())
15346 .await;
15347 cx.update_editor(|editor, _, _| {
15348 assert!(
15349 !editor.hover_state.visible(),
15350 "Hover popover should be hidden when completion menu is shown"
15351 );
15352 });
15353}
15354
15355#[gpui::test]
15356async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
15357 init_test(cx, |_| {});
15358
15359 let mut cx = EditorLspTestContext::new_rust(
15360 lsp::ServerCapabilities {
15361 completion_provider: Some(lsp::CompletionOptions {
15362 trigger_characters: Some(vec![".".to_string()]),
15363 resolve_provider: Some(true),
15364 ..Default::default()
15365 }),
15366 ..Default::default()
15367 },
15368 cx,
15369 )
15370 .await;
15371
15372 cx.set_state("fn main() { let a = 2ˇ; }");
15373 cx.simulate_keystroke(".");
15374
15375 let unresolved_item_1 = lsp::CompletionItem {
15376 label: "id".to_string(),
15377 filter_text: Some("id".to_string()),
15378 detail: None,
15379 documentation: None,
15380 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15381 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15382 new_text: ".id".to_string(),
15383 })),
15384 ..lsp::CompletionItem::default()
15385 };
15386 let resolved_item_1 = lsp::CompletionItem {
15387 additional_text_edits: Some(vec![lsp::TextEdit {
15388 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
15389 new_text: "!!".to_string(),
15390 }]),
15391 ..unresolved_item_1.clone()
15392 };
15393 let unresolved_item_2 = lsp::CompletionItem {
15394 label: "other".to_string(),
15395 filter_text: Some("other".to_string()),
15396 detail: None,
15397 documentation: None,
15398 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15399 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15400 new_text: ".other".to_string(),
15401 })),
15402 ..lsp::CompletionItem::default()
15403 };
15404 let resolved_item_2 = lsp::CompletionItem {
15405 additional_text_edits: Some(vec![lsp::TextEdit {
15406 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
15407 new_text: "??".to_string(),
15408 }]),
15409 ..unresolved_item_2.clone()
15410 };
15411
15412 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
15413 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
15414 cx.lsp
15415 .server
15416 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
15417 let unresolved_item_1 = unresolved_item_1.clone();
15418 let resolved_item_1 = resolved_item_1.clone();
15419 let unresolved_item_2 = unresolved_item_2.clone();
15420 let resolved_item_2 = resolved_item_2.clone();
15421 let resolve_requests_1 = resolve_requests_1.clone();
15422 let resolve_requests_2 = resolve_requests_2.clone();
15423 move |unresolved_request, _| {
15424 let unresolved_item_1 = unresolved_item_1.clone();
15425 let resolved_item_1 = resolved_item_1.clone();
15426 let unresolved_item_2 = unresolved_item_2.clone();
15427 let resolved_item_2 = resolved_item_2.clone();
15428 let resolve_requests_1 = resolve_requests_1.clone();
15429 let resolve_requests_2 = resolve_requests_2.clone();
15430 async move {
15431 if unresolved_request == unresolved_item_1 {
15432 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
15433 Ok(resolved_item_1.clone())
15434 } else if unresolved_request == unresolved_item_2 {
15435 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
15436 Ok(resolved_item_2.clone())
15437 } else {
15438 panic!("Unexpected completion item {unresolved_request:?}")
15439 }
15440 }
15441 }
15442 })
15443 .detach();
15444
15445 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15446 let unresolved_item_1 = unresolved_item_1.clone();
15447 let unresolved_item_2 = unresolved_item_2.clone();
15448 async move {
15449 Ok(Some(lsp::CompletionResponse::Array(vec![
15450 unresolved_item_1,
15451 unresolved_item_2,
15452 ])))
15453 }
15454 })
15455 .next()
15456 .await;
15457
15458 cx.condition(|editor, _| editor.context_menu_visible())
15459 .await;
15460 cx.update_editor(|editor, _, _| {
15461 let context_menu = editor.context_menu.borrow_mut();
15462 let context_menu = context_menu
15463 .as_ref()
15464 .expect("Should have the context menu deployed");
15465 match context_menu {
15466 CodeContextMenu::Completions(completions_menu) => {
15467 let completions = completions_menu.completions.borrow_mut();
15468 assert_eq!(
15469 completions
15470 .iter()
15471 .map(|completion| &completion.label.text)
15472 .collect::<Vec<_>>(),
15473 vec!["id", "other"]
15474 )
15475 }
15476 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
15477 }
15478 });
15479 cx.run_until_parked();
15480
15481 cx.update_editor(|editor, window, cx| {
15482 editor.context_menu_next(&ContextMenuNext, window, cx);
15483 });
15484 cx.run_until_parked();
15485 cx.update_editor(|editor, window, cx| {
15486 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
15487 });
15488 cx.run_until_parked();
15489 cx.update_editor(|editor, window, cx| {
15490 editor.context_menu_next(&ContextMenuNext, window, cx);
15491 });
15492 cx.run_until_parked();
15493 cx.update_editor(|editor, window, cx| {
15494 editor
15495 .compose_completion(&ComposeCompletion::default(), window, cx)
15496 .expect("No task returned")
15497 })
15498 .await
15499 .expect("Completion failed");
15500 cx.run_until_parked();
15501
15502 cx.update_editor(|editor, _, cx| {
15503 assert_eq!(
15504 resolve_requests_1.load(atomic::Ordering::Acquire),
15505 1,
15506 "Should always resolve once despite multiple selections"
15507 );
15508 assert_eq!(
15509 resolve_requests_2.load(atomic::Ordering::Acquire),
15510 1,
15511 "Should always resolve once after multiple selections and applying the completion"
15512 );
15513 assert_eq!(
15514 editor.text(cx),
15515 "fn main() { let a = ??.other; }",
15516 "Should use resolved data when applying the completion"
15517 );
15518 });
15519}
15520
15521#[gpui::test]
15522async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
15523 init_test(cx, |_| {});
15524
15525 let item_0 = lsp::CompletionItem {
15526 label: "abs".into(),
15527 insert_text: Some("abs".into()),
15528 data: Some(json!({ "very": "special"})),
15529 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
15530 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
15531 lsp::InsertReplaceEdit {
15532 new_text: "abs".to_string(),
15533 insert: lsp::Range::default(),
15534 replace: lsp::Range::default(),
15535 },
15536 )),
15537 ..lsp::CompletionItem::default()
15538 };
15539 let items = iter::once(item_0.clone())
15540 .chain((11..51).map(|i| lsp::CompletionItem {
15541 label: format!("item_{}", i),
15542 insert_text: Some(format!("item_{}", i)),
15543 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
15544 ..lsp::CompletionItem::default()
15545 }))
15546 .collect::<Vec<_>>();
15547
15548 let default_commit_characters = vec!["?".to_string()];
15549 let default_data = json!({ "default": "data"});
15550 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
15551 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
15552 let default_edit_range = lsp::Range {
15553 start: lsp::Position {
15554 line: 0,
15555 character: 5,
15556 },
15557 end: lsp::Position {
15558 line: 0,
15559 character: 5,
15560 },
15561 };
15562
15563 let mut cx = EditorLspTestContext::new_rust(
15564 lsp::ServerCapabilities {
15565 completion_provider: Some(lsp::CompletionOptions {
15566 trigger_characters: Some(vec![".".to_string()]),
15567 resolve_provider: Some(true),
15568 ..Default::default()
15569 }),
15570 ..Default::default()
15571 },
15572 cx,
15573 )
15574 .await;
15575
15576 cx.set_state("fn main() { let a = 2ˇ; }");
15577 cx.simulate_keystroke(".");
15578
15579 let completion_data = default_data.clone();
15580 let completion_characters = default_commit_characters.clone();
15581 let completion_items = items.clone();
15582 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15583 let default_data = completion_data.clone();
15584 let default_commit_characters = completion_characters.clone();
15585 let items = completion_items.clone();
15586 async move {
15587 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15588 items,
15589 item_defaults: Some(lsp::CompletionListItemDefaults {
15590 data: Some(default_data.clone()),
15591 commit_characters: Some(default_commit_characters.clone()),
15592 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
15593 default_edit_range,
15594 )),
15595 insert_text_format: Some(default_insert_text_format),
15596 insert_text_mode: Some(default_insert_text_mode),
15597 }),
15598 ..lsp::CompletionList::default()
15599 })))
15600 }
15601 })
15602 .next()
15603 .await;
15604
15605 let resolved_items = Arc::new(Mutex::new(Vec::new()));
15606 cx.lsp
15607 .server
15608 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
15609 let closure_resolved_items = resolved_items.clone();
15610 move |item_to_resolve, _| {
15611 let closure_resolved_items = closure_resolved_items.clone();
15612 async move {
15613 closure_resolved_items.lock().push(item_to_resolve.clone());
15614 Ok(item_to_resolve)
15615 }
15616 }
15617 })
15618 .detach();
15619
15620 cx.condition(|editor, _| editor.context_menu_visible())
15621 .await;
15622 cx.run_until_parked();
15623 cx.update_editor(|editor, _, _| {
15624 let menu = editor.context_menu.borrow_mut();
15625 match menu.as_ref().expect("should have the completions menu") {
15626 CodeContextMenu::Completions(completions_menu) => {
15627 assert_eq!(
15628 completions_menu
15629 .entries
15630 .borrow()
15631 .iter()
15632 .map(|mat| mat.string.clone())
15633 .collect::<Vec<String>>(),
15634 items
15635 .iter()
15636 .map(|completion| completion.label.clone())
15637 .collect::<Vec<String>>()
15638 );
15639 }
15640 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
15641 }
15642 });
15643 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
15644 // with 4 from the end.
15645 assert_eq!(
15646 *resolved_items.lock(),
15647 [&items[0..16], &items[items.len() - 4..items.len()]]
15648 .concat()
15649 .iter()
15650 .cloned()
15651 .map(|mut item| {
15652 if item.data.is_none() {
15653 item.data = Some(default_data.clone());
15654 }
15655 item
15656 })
15657 .collect::<Vec<lsp::CompletionItem>>(),
15658 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
15659 );
15660 resolved_items.lock().clear();
15661
15662 cx.update_editor(|editor, window, cx| {
15663 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
15664 });
15665 cx.run_until_parked();
15666 // Completions that have already been resolved are skipped.
15667 assert_eq!(
15668 *resolved_items.lock(),
15669 items[items.len() - 17..items.len() - 4]
15670 .iter()
15671 .cloned()
15672 .map(|mut item| {
15673 if item.data.is_none() {
15674 item.data = Some(default_data.clone());
15675 }
15676 item
15677 })
15678 .collect::<Vec<lsp::CompletionItem>>()
15679 );
15680 resolved_items.lock().clear();
15681}
15682
15683#[gpui::test]
15684async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
15685 init_test(cx, |_| {});
15686
15687 let mut cx = EditorLspTestContext::new(
15688 Language::new(
15689 LanguageConfig {
15690 matcher: LanguageMatcher {
15691 path_suffixes: vec!["jsx".into()],
15692 ..Default::default()
15693 },
15694 overrides: [(
15695 "element".into(),
15696 LanguageConfigOverride {
15697 completion_query_characters: Override::Set(['-'].into_iter().collect()),
15698 ..Default::default()
15699 },
15700 )]
15701 .into_iter()
15702 .collect(),
15703 ..Default::default()
15704 },
15705 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
15706 )
15707 .with_override_query("(jsx_self_closing_element) @element")
15708 .unwrap(),
15709 lsp::ServerCapabilities {
15710 completion_provider: Some(lsp::CompletionOptions {
15711 trigger_characters: Some(vec![":".to_string()]),
15712 ..Default::default()
15713 }),
15714 ..Default::default()
15715 },
15716 cx,
15717 )
15718 .await;
15719
15720 cx.lsp
15721 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15722 Ok(Some(lsp::CompletionResponse::Array(vec![
15723 lsp::CompletionItem {
15724 label: "bg-blue".into(),
15725 ..Default::default()
15726 },
15727 lsp::CompletionItem {
15728 label: "bg-red".into(),
15729 ..Default::default()
15730 },
15731 lsp::CompletionItem {
15732 label: "bg-yellow".into(),
15733 ..Default::default()
15734 },
15735 ])))
15736 });
15737
15738 cx.set_state(r#"<p class="bgˇ" />"#);
15739
15740 // Trigger completion when typing a dash, because the dash is an extra
15741 // word character in the 'element' scope, which contains the cursor.
15742 cx.simulate_keystroke("-");
15743 cx.executor().run_until_parked();
15744 cx.update_editor(|editor, _, _| {
15745 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15746 {
15747 assert_eq!(
15748 completion_menu_entries(&menu),
15749 &["bg-blue", "bg-red", "bg-yellow"]
15750 );
15751 } else {
15752 panic!("expected completion menu to be open");
15753 }
15754 });
15755
15756 cx.simulate_keystroke("l");
15757 cx.executor().run_until_parked();
15758 cx.update_editor(|editor, _, _| {
15759 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15760 {
15761 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
15762 } else {
15763 panic!("expected completion menu to be open");
15764 }
15765 });
15766
15767 // When filtering completions, consider the character after the '-' to
15768 // be the start of a subword.
15769 cx.set_state(r#"<p class="yelˇ" />"#);
15770 cx.simulate_keystroke("l");
15771 cx.executor().run_until_parked();
15772 cx.update_editor(|editor, _, _| {
15773 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15774 {
15775 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
15776 } else {
15777 panic!("expected completion menu to be open");
15778 }
15779 });
15780}
15781
15782fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
15783 let entries = menu.entries.borrow();
15784 entries.iter().map(|mat| mat.string.clone()).collect()
15785}
15786
15787#[gpui::test]
15788async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
15789 init_test(cx, |settings| {
15790 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
15791 FormatterList(vec![Formatter::Prettier].into()),
15792 ))
15793 });
15794
15795 let fs = FakeFs::new(cx.executor());
15796 fs.insert_file(path!("/file.ts"), Default::default()).await;
15797
15798 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
15799 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15800
15801 language_registry.add(Arc::new(Language::new(
15802 LanguageConfig {
15803 name: "TypeScript".into(),
15804 matcher: LanguageMatcher {
15805 path_suffixes: vec!["ts".to_string()],
15806 ..Default::default()
15807 },
15808 ..Default::default()
15809 },
15810 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
15811 )));
15812 update_test_language_settings(cx, |settings| {
15813 settings.defaults.prettier = Some(PrettierSettings {
15814 allowed: true,
15815 ..PrettierSettings::default()
15816 });
15817 });
15818
15819 let test_plugin = "test_plugin";
15820 let _ = language_registry.register_fake_lsp(
15821 "TypeScript",
15822 FakeLspAdapter {
15823 prettier_plugins: vec![test_plugin],
15824 ..Default::default()
15825 },
15826 );
15827
15828 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
15829 let buffer = project
15830 .update(cx, |project, cx| {
15831 project.open_local_buffer(path!("/file.ts"), cx)
15832 })
15833 .await
15834 .unwrap();
15835
15836 let buffer_text = "one\ntwo\nthree\n";
15837 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
15838 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
15839 editor.update_in(cx, |editor, window, cx| {
15840 editor.set_text(buffer_text, window, cx)
15841 });
15842
15843 editor
15844 .update_in(cx, |editor, window, cx| {
15845 editor.perform_format(
15846 project.clone(),
15847 FormatTrigger::Manual,
15848 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
15849 window,
15850 cx,
15851 )
15852 })
15853 .unwrap()
15854 .await;
15855 assert_eq!(
15856 editor.update(cx, |editor, cx| editor.text(cx)),
15857 buffer_text.to_string() + prettier_format_suffix,
15858 "Test prettier formatting was not applied to the original buffer text",
15859 );
15860
15861 update_test_language_settings(cx, |settings| {
15862 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
15863 });
15864 let format = editor.update_in(cx, |editor, window, cx| {
15865 editor.perform_format(
15866 project.clone(),
15867 FormatTrigger::Manual,
15868 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
15869 window,
15870 cx,
15871 )
15872 });
15873 format.await.unwrap();
15874 assert_eq!(
15875 editor.update(cx, |editor, cx| editor.text(cx)),
15876 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
15877 "Autoformatting (via test prettier) was not applied to the original buffer text",
15878 );
15879}
15880
15881#[gpui::test]
15882async fn test_addition_reverts(cx: &mut TestAppContext) {
15883 init_test(cx, |_| {});
15884 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15885 let base_text = indoc! {r#"
15886 struct Row;
15887 struct Row1;
15888 struct Row2;
15889
15890 struct Row4;
15891 struct Row5;
15892 struct Row6;
15893
15894 struct Row8;
15895 struct Row9;
15896 struct Row10;"#};
15897
15898 // When addition hunks are not adjacent to carets, no hunk revert is performed
15899 assert_hunk_revert(
15900 indoc! {r#"struct Row;
15901 struct Row1;
15902 struct Row1.1;
15903 struct Row1.2;
15904 struct Row2;ˇ
15905
15906 struct Row4;
15907 struct Row5;
15908 struct Row6;
15909
15910 struct Row8;
15911 ˇstruct Row9;
15912 struct Row9.1;
15913 struct Row9.2;
15914 struct Row9.3;
15915 struct Row10;"#},
15916 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
15917 indoc! {r#"struct Row;
15918 struct Row1;
15919 struct Row1.1;
15920 struct Row1.2;
15921 struct Row2;ˇ
15922
15923 struct Row4;
15924 struct Row5;
15925 struct Row6;
15926
15927 struct Row8;
15928 ˇstruct Row9;
15929 struct Row9.1;
15930 struct Row9.2;
15931 struct Row9.3;
15932 struct Row10;"#},
15933 base_text,
15934 &mut cx,
15935 );
15936 // Same for selections
15937 assert_hunk_revert(
15938 indoc! {r#"struct Row;
15939 struct Row1;
15940 struct Row2;
15941 struct Row2.1;
15942 struct Row2.2;
15943 «ˇ
15944 struct Row4;
15945 struct» Row5;
15946 «struct Row6;
15947 ˇ»
15948 struct Row9.1;
15949 struct Row9.2;
15950 struct Row9.3;
15951 struct Row8;
15952 struct Row9;
15953 struct Row10;"#},
15954 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
15955 indoc! {r#"struct Row;
15956 struct Row1;
15957 struct Row2;
15958 struct Row2.1;
15959 struct Row2.2;
15960 «ˇ
15961 struct Row4;
15962 struct» Row5;
15963 «struct Row6;
15964 ˇ»
15965 struct Row9.1;
15966 struct Row9.2;
15967 struct Row9.3;
15968 struct Row8;
15969 struct Row9;
15970 struct Row10;"#},
15971 base_text,
15972 &mut cx,
15973 );
15974
15975 // When carets and selections intersect the addition hunks, those are reverted.
15976 // Adjacent carets got merged.
15977 assert_hunk_revert(
15978 indoc! {r#"struct Row;
15979 ˇ// something on the top
15980 struct Row1;
15981 struct Row2;
15982 struct Roˇw3.1;
15983 struct Row2.2;
15984 struct Row2.3;ˇ
15985
15986 struct Row4;
15987 struct ˇRow5.1;
15988 struct Row5.2;
15989 struct «Rowˇ»5.3;
15990 struct Row5;
15991 struct Row6;
15992 ˇ
15993 struct Row9.1;
15994 struct «Rowˇ»9.2;
15995 struct «ˇRow»9.3;
15996 struct Row8;
15997 struct Row9;
15998 «ˇ// something on bottom»
15999 struct Row10;"#},
16000 vec![
16001 DiffHunkStatusKind::Added,
16002 DiffHunkStatusKind::Added,
16003 DiffHunkStatusKind::Added,
16004 DiffHunkStatusKind::Added,
16005 DiffHunkStatusKind::Added,
16006 ],
16007 indoc! {r#"struct Row;
16008 ˇstruct Row1;
16009 struct Row2;
16010 ˇ
16011 struct Row4;
16012 ˇstruct Row5;
16013 struct Row6;
16014 ˇ
16015 ˇstruct Row8;
16016 struct Row9;
16017 ˇstruct Row10;"#},
16018 base_text,
16019 &mut cx,
16020 );
16021}
16022
16023#[gpui::test]
16024async fn test_modification_reverts(cx: &mut TestAppContext) {
16025 init_test(cx, |_| {});
16026 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16027 let base_text = indoc! {r#"
16028 struct Row;
16029 struct Row1;
16030 struct Row2;
16031
16032 struct Row4;
16033 struct Row5;
16034 struct Row6;
16035
16036 struct Row8;
16037 struct Row9;
16038 struct Row10;"#};
16039
16040 // Modification hunks behave the same as the addition ones.
16041 assert_hunk_revert(
16042 indoc! {r#"struct Row;
16043 struct Row1;
16044 struct Row33;
16045 ˇ
16046 struct Row4;
16047 struct Row5;
16048 struct Row6;
16049 ˇ
16050 struct Row99;
16051 struct Row9;
16052 struct Row10;"#},
16053 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
16054 indoc! {r#"struct Row;
16055 struct Row1;
16056 struct Row33;
16057 ˇ
16058 struct Row4;
16059 struct Row5;
16060 struct Row6;
16061 ˇ
16062 struct Row99;
16063 struct Row9;
16064 struct Row10;"#},
16065 base_text,
16066 &mut cx,
16067 );
16068 assert_hunk_revert(
16069 indoc! {r#"struct Row;
16070 struct Row1;
16071 struct Row33;
16072 «ˇ
16073 struct Row4;
16074 struct» Row5;
16075 «struct Row6;
16076 ˇ»
16077 struct Row99;
16078 struct Row9;
16079 struct Row10;"#},
16080 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
16081 indoc! {r#"struct Row;
16082 struct Row1;
16083 struct Row33;
16084 «ˇ
16085 struct Row4;
16086 struct» Row5;
16087 «struct Row6;
16088 ˇ»
16089 struct Row99;
16090 struct Row9;
16091 struct Row10;"#},
16092 base_text,
16093 &mut cx,
16094 );
16095
16096 assert_hunk_revert(
16097 indoc! {r#"ˇstruct Row1.1;
16098 struct Row1;
16099 «ˇstr»uct Row22;
16100
16101 struct ˇRow44;
16102 struct Row5;
16103 struct «Rˇ»ow66;ˇ
16104
16105 «struˇ»ct Row88;
16106 struct Row9;
16107 struct Row1011;ˇ"#},
16108 vec![
16109 DiffHunkStatusKind::Modified,
16110 DiffHunkStatusKind::Modified,
16111 DiffHunkStatusKind::Modified,
16112 DiffHunkStatusKind::Modified,
16113 DiffHunkStatusKind::Modified,
16114 DiffHunkStatusKind::Modified,
16115 ],
16116 indoc! {r#"struct Row;
16117 ˇstruct Row1;
16118 struct Row2;
16119 ˇ
16120 struct Row4;
16121 ˇstruct Row5;
16122 struct Row6;
16123 ˇ
16124 struct Row8;
16125 ˇstruct Row9;
16126 struct Row10;ˇ"#},
16127 base_text,
16128 &mut cx,
16129 );
16130}
16131
16132#[gpui::test]
16133async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
16134 init_test(cx, |_| {});
16135 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16136 let base_text = indoc! {r#"
16137 one
16138
16139 two
16140 three
16141 "#};
16142
16143 cx.set_head_text(base_text);
16144 cx.set_state("\nˇ\n");
16145 cx.executor().run_until_parked();
16146 cx.update_editor(|editor, _window, cx| {
16147 editor.expand_selected_diff_hunks(cx);
16148 });
16149 cx.executor().run_until_parked();
16150 cx.update_editor(|editor, window, cx| {
16151 editor.backspace(&Default::default(), window, cx);
16152 });
16153 cx.run_until_parked();
16154 cx.assert_state_with_diff(
16155 indoc! {r#"
16156
16157 - two
16158 - threeˇ
16159 +
16160 "#}
16161 .to_string(),
16162 );
16163}
16164
16165#[gpui::test]
16166async fn test_deletion_reverts(cx: &mut TestAppContext) {
16167 init_test(cx, |_| {});
16168 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16169 let base_text = indoc! {r#"struct Row;
16170struct Row1;
16171struct Row2;
16172
16173struct Row4;
16174struct Row5;
16175struct Row6;
16176
16177struct Row8;
16178struct Row9;
16179struct Row10;"#};
16180
16181 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
16182 assert_hunk_revert(
16183 indoc! {r#"struct Row;
16184 struct Row2;
16185
16186 ˇstruct Row4;
16187 struct Row5;
16188 struct Row6;
16189 ˇ
16190 struct Row8;
16191 struct Row10;"#},
16192 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16193 indoc! {r#"struct Row;
16194 struct Row2;
16195
16196 ˇstruct Row4;
16197 struct Row5;
16198 struct Row6;
16199 ˇ
16200 struct Row8;
16201 struct Row10;"#},
16202 base_text,
16203 &mut cx,
16204 );
16205 assert_hunk_revert(
16206 indoc! {r#"struct Row;
16207 struct Row2;
16208
16209 «ˇstruct Row4;
16210 struct» Row5;
16211 «struct Row6;
16212 ˇ»
16213 struct Row8;
16214 struct Row10;"#},
16215 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16216 indoc! {r#"struct Row;
16217 struct Row2;
16218
16219 «ˇstruct Row4;
16220 struct» Row5;
16221 «struct Row6;
16222 ˇ»
16223 struct Row8;
16224 struct Row10;"#},
16225 base_text,
16226 &mut cx,
16227 );
16228
16229 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
16230 assert_hunk_revert(
16231 indoc! {r#"struct Row;
16232 ˇstruct Row2;
16233
16234 struct Row4;
16235 struct Row5;
16236 struct Row6;
16237
16238 struct Row8;ˇ
16239 struct Row10;"#},
16240 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16241 indoc! {r#"struct Row;
16242 struct Row1;
16243 ˇstruct Row2;
16244
16245 struct Row4;
16246 struct Row5;
16247 struct Row6;
16248
16249 struct Row8;ˇ
16250 struct Row9;
16251 struct Row10;"#},
16252 base_text,
16253 &mut cx,
16254 );
16255 assert_hunk_revert(
16256 indoc! {r#"struct Row;
16257 struct Row2«ˇ;
16258 struct Row4;
16259 struct» Row5;
16260 «struct Row6;
16261
16262 struct Row8;ˇ»
16263 struct Row10;"#},
16264 vec![
16265 DiffHunkStatusKind::Deleted,
16266 DiffHunkStatusKind::Deleted,
16267 DiffHunkStatusKind::Deleted,
16268 ],
16269 indoc! {r#"struct Row;
16270 struct Row1;
16271 struct Row2«ˇ;
16272
16273 struct Row4;
16274 struct» Row5;
16275 «struct Row6;
16276
16277 struct Row8;ˇ»
16278 struct Row9;
16279 struct Row10;"#},
16280 base_text,
16281 &mut cx,
16282 );
16283}
16284
16285#[gpui::test]
16286async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
16287 init_test(cx, |_| {});
16288
16289 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
16290 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
16291 let base_text_3 =
16292 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
16293
16294 let text_1 = edit_first_char_of_every_line(base_text_1);
16295 let text_2 = edit_first_char_of_every_line(base_text_2);
16296 let text_3 = edit_first_char_of_every_line(base_text_3);
16297
16298 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
16299 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
16300 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
16301
16302 let multibuffer = cx.new(|cx| {
16303 let mut multibuffer = MultiBuffer::new(ReadWrite);
16304 multibuffer.push_excerpts(
16305 buffer_1.clone(),
16306 [
16307 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16308 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16309 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16310 ],
16311 cx,
16312 );
16313 multibuffer.push_excerpts(
16314 buffer_2.clone(),
16315 [
16316 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16317 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16318 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16319 ],
16320 cx,
16321 );
16322 multibuffer.push_excerpts(
16323 buffer_3.clone(),
16324 [
16325 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16326 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16327 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16328 ],
16329 cx,
16330 );
16331 multibuffer
16332 });
16333
16334 let fs = FakeFs::new(cx.executor());
16335 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
16336 let (editor, cx) = cx
16337 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
16338 editor.update_in(cx, |editor, _window, cx| {
16339 for (buffer, diff_base) in [
16340 (buffer_1.clone(), base_text_1),
16341 (buffer_2.clone(), base_text_2),
16342 (buffer_3.clone(), base_text_3),
16343 ] {
16344 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
16345 editor
16346 .buffer
16347 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
16348 }
16349 });
16350 cx.executor().run_until_parked();
16351
16352 editor.update_in(cx, |editor, window, cx| {
16353 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}");
16354 editor.select_all(&SelectAll, window, cx);
16355 editor.git_restore(&Default::default(), window, cx);
16356 });
16357 cx.executor().run_until_parked();
16358
16359 // When all ranges are selected, all buffer hunks are reverted.
16360 editor.update(cx, |editor, cx| {
16361 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");
16362 });
16363 buffer_1.update(cx, |buffer, _| {
16364 assert_eq!(buffer.text(), base_text_1);
16365 });
16366 buffer_2.update(cx, |buffer, _| {
16367 assert_eq!(buffer.text(), base_text_2);
16368 });
16369 buffer_3.update(cx, |buffer, _| {
16370 assert_eq!(buffer.text(), base_text_3);
16371 });
16372
16373 editor.update_in(cx, |editor, window, cx| {
16374 editor.undo(&Default::default(), window, cx);
16375 });
16376
16377 editor.update_in(cx, |editor, window, cx| {
16378 editor.change_selections(None, window, cx, |s| {
16379 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
16380 });
16381 editor.git_restore(&Default::default(), window, cx);
16382 });
16383
16384 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
16385 // but not affect buffer_2 and its related excerpts.
16386 editor.update(cx, |editor, cx| {
16387 assert_eq!(
16388 editor.text(cx),
16389 "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}"
16390 );
16391 });
16392 buffer_1.update(cx, |buffer, _| {
16393 assert_eq!(buffer.text(), base_text_1);
16394 });
16395 buffer_2.update(cx, |buffer, _| {
16396 assert_eq!(
16397 buffer.text(),
16398 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
16399 );
16400 });
16401 buffer_3.update(cx, |buffer, _| {
16402 assert_eq!(
16403 buffer.text(),
16404 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
16405 );
16406 });
16407
16408 fn edit_first_char_of_every_line(text: &str) -> String {
16409 text.split('\n')
16410 .map(|line| format!("X{}", &line[1..]))
16411 .collect::<Vec<_>>()
16412 .join("\n")
16413 }
16414}
16415
16416#[gpui::test]
16417async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) {
16418 init_test(cx, |_| {});
16419
16420 let cols = 4;
16421 let rows = 10;
16422 let sample_text_1 = sample_text(rows, cols, 'a');
16423 assert_eq!(
16424 sample_text_1,
16425 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
16426 );
16427 let sample_text_2 = sample_text(rows, cols, 'l');
16428 assert_eq!(
16429 sample_text_2,
16430 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
16431 );
16432 let sample_text_3 = sample_text(rows, cols, 'v');
16433 assert_eq!(
16434 sample_text_3,
16435 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
16436 );
16437
16438 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
16439 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
16440 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
16441
16442 let multi_buffer = cx.new(|cx| {
16443 let mut multibuffer = MultiBuffer::new(ReadWrite);
16444 multibuffer.push_excerpts(
16445 buffer_1.clone(),
16446 [
16447 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16448 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16449 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16450 ],
16451 cx,
16452 );
16453 multibuffer.push_excerpts(
16454 buffer_2.clone(),
16455 [
16456 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16457 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16458 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16459 ],
16460 cx,
16461 );
16462 multibuffer.push_excerpts(
16463 buffer_3.clone(),
16464 [
16465 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16466 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16467 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16468 ],
16469 cx,
16470 );
16471 multibuffer
16472 });
16473
16474 let fs = FakeFs::new(cx.executor());
16475 fs.insert_tree(
16476 "/a",
16477 json!({
16478 "main.rs": sample_text_1,
16479 "other.rs": sample_text_2,
16480 "lib.rs": sample_text_3,
16481 }),
16482 )
16483 .await;
16484 let project = Project::test(fs, ["/a".as_ref()], cx).await;
16485 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16486 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16487 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
16488 Editor::new(
16489 EditorMode::full(),
16490 multi_buffer,
16491 Some(project.clone()),
16492 window,
16493 cx,
16494 )
16495 });
16496 let multibuffer_item_id = workspace
16497 .update(cx, |workspace, window, cx| {
16498 assert!(
16499 workspace.active_item(cx).is_none(),
16500 "active item should be None before the first item is added"
16501 );
16502 workspace.add_item_to_active_pane(
16503 Box::new(multi_buffer_editor.clone()),
16504 None,
16505 true,
16506 window,
16507 cx,
16508 );
16509 let active_item = workspace
16510 .active_item(cx)
16511 .expect("should have an active item after adding the multi buffer");
16512 assert!(
16513 !active_item.is_singleton(cx),
16514 "A multi buffer was expected to active after adding"
16515 );
16516 active_item.item_id()
16517 })
16518 .unwrap();
16519 cx.executor().run_until_parked();
16520
16521 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16522 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
16523 s.select_ranges(Some(1..2))
16524 });
16525 editor.open_excerpts(&OpenExcerpts, window, cx);
16526 });
16527 cx.executor().run_until_parked();
16528 let first_item_id = workspace
16529 .update(cx, |workspace, window, cx| {
16530 let active_item = workspace
16531 .active_item(cx)
16532 .expect("should have an active item after navigating into the 1st buffer");
16533 let first_item_id = active_item.item_id();
16534 assert_ne!(
16535 first_item_id, multibuffer_item_id,
16536 "Should navigate into the 1st buffer and activate it"
16537 );
16538 assert!(
16539 active_item.is_singleton(cx),
16540 "New active item should be a singleton buffer"
16541 );
16542 assert_eq!(
16543 active_item
16544 .act_as::<Editor>(cx)
16545 .expect("should have navigated into an editor for the 1st buffer")
16546 .read(cx)
16547 .text(cx),
16548 sample_text_1
16549 );
16550
16551 workspace
16552 .go_back(workspace.active_pane().downgrade(), window, cx)
16553 .detach_and_log_err(cx);
16554
16555 first_item_id
16556 })
16557 .unwrap();
16558 cx.executor().run_until_parked();
16559 workspace
16560 .update(cx, |workspace, _, cx| {
16561 let active_item = workspace
16562 .active_item(cx)
16563 .expect("should have an active item after navigating back");
16564 assert_eq!(
16565 active_item.item_id(),
16566 multibuffer_item_id,
16567 "Should navigate back to the multi buffer"
16568 );
16569 assert!(!active_item.is_singleton(cx));
16570 })
16571 .unwrap();
16572
16573 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16574 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
16575 s.select_ranges(Some(39..40))
16576 });
16577 editor.open_excerpts(&OpenExcerpts, window, cx);
16578 });
16579 cx.executor().run_until_parked();
16580 let second_item_id = workspace
16581 .update(cx, |workspace, window, cx| {
16582 let active_item = workspace
16583 .active_item(cx)
16584 .expect("should have an active item after navigating into the 2nd buffer");
16585 let second_item_id = active_item.item_id();
16586 assert_ne!(
16587 second_item_id, multibuffer_item_id,
16588 "Should navigate away from the multibuffer"
16589 );
16590 assert_ne!(
16591 second_item_id, first_item_id,
16592 "Should navigate into the 2nd buffer and activate it"
16593 );
16594 assert!(
16595 active_item.is_singleton(cx),
16596 "New active item should be a singleton buffer"
16597 );
16598 assert_eq!(
16599 active_item
16600 .act_as::<Editor>(cx)
16601 .expect("should have navigated into an editor")
16602 .read(cx)
16603 .text(cx),
16604 sample_text_2
16605 );
16606
16607 workspace
16608 .go_back(workspace.active_pane().downgrade(), window, cx)
16609 .detach_and_log_err(cx);
16610
16611 second_item_id
16612 })
16613 .unwrap();
16614 cx.executor().run_until_parked();
16615 workspace
16616 .update(cx, |workspace, _, cx| {
16617 let active_item = workspace
16618 .active_item(cx)
16619 .expect("should have an active item after navigating back from the 2nd buffer");
16620 assert_eq!(
16621 active_item.item_id(),
16622 multibuffer_item_id,
16623 "Should navigate back from the 2nd buffer to the multi buffer"
16624 );
16625 assert!(!active_item.is_singleton(cx));
16626 })
16627 .unwrap();
16628
16629 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16630 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
16631 s.select_ranges(Some(70..70))
16632 });
16633 editor.open_excerpts(&OpenExcerpts, window, cx);
16634 });
16635 cx.executor().run_until_parked();
16636 workspace
16637 .update(cx, |workspace, window, cx| {
16638 let active_item = workspace
16639 .active_item(cx)
16640 .expect("should have an active item after navigating into the 3rd buffer");
16641 let third_item_id = active_item.item_id();
16642 assert_ne!(
16643 third_item_id, multibuffer_item_id,
16644 "Should navigate into the 3rd buffer and activate it"
16645 );
16646 assert_ne!(third_item_id, first_item_id);
16647 assert_ne!(third_item_id, second_item_id);
16648 assert!(
16649 active_item.is_singleton(cx),
16650 "New active item should be a singleton buffer"
16651 );
16652 assert_eq!(
16653 active_item
16654 .act_as::<Editor>(cx)
16655 .expect("should have navigated into an editor")
16656 .read(cx)
16657 .text(cx),
16658 sample_text_3
16659 );
16660
16661 workspace
16662 .go_back(workspace.active_pane().downgrade(), window, cx)
16663 .detach_and_log_err(cx);
16664 })
16665 .unwrap();
16666 cx.executor().run_until_parked();
16667 workspace
16668 .update(cx, |workspace, _, cx| {
16669 let active_item = workspace
16670 .active_item(cx)
16671 .expect("should have an active item after navigating back from the 3rd buffer");
16672 assert_eq!(
16673 active_item.item_id(),
16674 multibuffer_item_id,
16675 "Should navigate back from the 3rd buffer to the multi buffer"
16676 );
16677 assert!(!active_item.is_singleton(cx));
16678 })
16679 .unwrap();
16680}
16681
16682#[gpui::test]
16683async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16684 init_test(cx, |_| {});
16685
16686 let mut cx = EditorTestContext::new(cx).await;
16687
16688 let diff_base = r#"
16689 use some::mod;
16690
16691 const A: u32 = 42;
16692
16693 fn main() {
16694 println!("hello");
16695
16696 println!("world");
16697 }
16698 "#
16699 .unindent();
16700
16701 cx.set_state(
16702 &r#"
16703 use some::modified;
16704
16705 ˇ
16706 fn main() {
16707 println!("hello there");
16708
16709 println!("around the");
16710 println!("world");
16711 }
16712 "#
16713 .unindent(),
16714 );
16715
16716 cx.set_head_text(&diff_base);
16717 executor.run_until_parked();
16718
16719 cx.update_editor(|editor, window, cx| {
16720 editor.go_to_next_hunk(&GoToHunk, window, cx);
16721 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
16722 });
16723 executor.run_until_parked();
16724 cx.assert_state_with_diff(
16725 r#"
16726 use some::modified;
16727
16728
16729 fn main() {
16730 - println!("hello");
16731 + ˇ println!("hello there");
16732
16733 println!("around the");
16734 println!("world");
16735 }
16736 "#
16737 .unindent(),
16738 );
16739
16740 cx.update_editor(|editor, window, cx| {
16741 for _ in 0..2 {
16742 editor.go_to_next_hunk(&GoToHunk, window, cx);
16743 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
16744 }
16745 });
16746 executor.run_until_parked();
16747 cx.assert_state_with_diff(
16748 r#"
16749 - use some::mod;
16750 + ˇuse some::modified;
16751
16752
16753 fn main() {
16754 - println!("hello");
16755 + println!("hello there");
16756
16757 + println!("around the");
16758 println!("world");
16759 }
16760 "#
16761 .unindent(),
16762 );
16763
16764 cx.update_editor(|editor, window, cx| {
16765 editor.go_to_next_hunk(&GoToHunk, window, cx);
16766 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
16767 });
16768 executor.run_until_parked();
16769 cx.assert_state_with_diff(
16770 r#"
16771 - use some::mod;
16772 + use some::modified;
16773
16774 - const A: u32 = 42;
16775 ˇ
16776 fn main() {
16777 - println!("hello");
16778 + println!("hello there");
16779
16780 + println!("around the");
16781 println!("world");
16782 }
16783 "#
16784 .unindent(),
16785 );
16786
16787 cx.update_editor(|editor, window, cx| {
16788 editor.cancel(&Cancel, window, cx);
16789 });
16790
16791 cx.assert_state_with_diff(
16792 r#"
16793 use some::modified;
16794
16795 ˇ
16796 fn main() {
16797 println!("hello there");
16798
16799 println!("around the");
16800 println!("world");
16801 }
16802 "#
16803 .unindent(),
16804 );
16805}
16806
16807#[gpui::test]
16808async fn test_diff_base_change_with_expanded_diff_hunks(
16809 executor: BackgroundExecutor,
16810 cx: &mut TestAppContext,
16811) {
16812 init_test(cx, |_| {});
16813
16814 let mut cx = EditorTestContext::new(cx).await;
16815
16816 let diff_base = r#"
16817 use some::mod1;
16818 use some::mod2;
16819
16820 const A: u32 = 42;
16821 const B: u32 = 42;
16822 const C: u32 = 42;
16823
16824 fn main() {
16825 println!("hello");
16826
16827 println!("world");
16828 }
16829 "#
16830 .unindent();
16831
16832 cx.set_state(
16833 &r#"
16834 use some::mod2;
16835
16836 const A: u32 = 42;
16837 const C: u32 = 42;
16838
16839 fn main(ˇ) {
16840 //println!("hello");
16841
16842 println!("world");
16843 //
16844 //
16845 }
16846 "#
16847 .unindent(),
16848 );
16849
16850 cx.set_head_text(&diff_base);
16851 executor.run_until_parked();
16852
16853 cx.update_editor(|editor, window, cx| {
16854 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16855 });
16856 executor.run_until_parked();
16857 cx.assert_state_with_diff(
16858 r#"
16859 - use some::mod1;
16860 use some::mod2;
16861
16862 const A: u32 = 42;
16863 - const B: u32 = 42;
16864 const C: u32 = 42;
16865
16866 fn main(ˇ) {
16867 - println!("hello");
16868 + //println!("hello");
16869
16870 println!("world");
16871 + //
16872 + //
16873 }
16874 "#
16875 .unindent(),
16876 );
16877
16878 cx.set_head_text("new diff base!");
16879 executor.run_until_parked();
16880 cx.assert_state_with_diff(
16881 r#"
16882 - new diff base!
16883 + use some::mod2;
16884 +
16885 + const A: u32 = 42;
16886 + const C: u32 = 42;
16887 +
16888 + fn main(ˇ) {
16889 + //println!("hello");
16890 +
16891 + println!("world");
16892 + //
16893 + //
16894 + }
16895 "#
16896 .unindent(),
16897 );
16898}
16899
16900#[gpui::test]
16901async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
16902 init_test(cx, |_| {});
16903
16904 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
16905 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
16906 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
16907 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
16908 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
16909 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
16910
16911 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
16912 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
16913 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
16914
16915 let multi_buffer = cx.new(|cx| {
16916 let mut multibuffer = MultiBuffer::new(ReadWrite);
16917 multibuffer.push_excerpts(
16918 buffer_1.clone(),
16919 [
16920 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16921 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16922 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
16923 ],
16924 cx,
16925 );
16926 multibuffer.push_excerpts(
16927 buffer_2.clone(),
16928 [
16929 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16930 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16931 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
16932 ],
16933 cx,
16934 );
16935 multibuffer.push_excerpts(
16936 buffer_3.clone(),
16937 [
16938 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16939 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16940 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
16941 ],
16942 cx,
16943 );
16944 multibuffer
16945 });
16946
16947 let editor =
16948 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
16949 editor
16950 .update(cx, |editor, _window, cx| {
16951 for (buffer, diff_base) in [
16952 (buffer_1.clone(), file_1_old),
16953 (buffer_2.clone(), file_2_old),
16954 (buffer_3.clone(), file_3_old),
16955 ] {
16956 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
16957 editor
16958 .buffer
16959 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
16960 }
16961 })
16962 .unwrap();
16963
16964 let mut cx = EditorTestContext::for_editor(editor, cx).await;
16965 cx.run_until_parked();
16966
16967 cx.assert_editor_state(
16968 &"
16969 ˇaaa
16970 ccc
16971 ddd
16972
16973 ggg
16974 hhh
16975
16976
16977 lll
16978 mmm
16979 NNN
16980
16981 qqq
16982 rrr
16983
16984 uuu
16985 111
16986 222
16987 333
16988
16989 666
16990 777
16991
16992 000
16993 !!!"
16994 .unindent(),
16995 );
16996
16997 cx.update_editor(|editor, window, cx| {
16998 editor.select_all(&SelectAll, window, cx);
16999 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17000 });
17001 cx.executor().run_until_parked();
17002
17003 cx.assert_state_with_diff(
17004 "
17005 «aaa
17006 - bbb
17007 ccc
17008 ddd
17009
17010 ggg
17011 hhh
17012
17013
17014 lll
17015 mmm
17016 - nnn
17017 + NNN
17018
17019 qqq
17020 rrr
17021
17022 uuu
17023 111
17024 222
17025 333
17026
17027 + 666
17028 777
17029
17030 000
17031 !!!ˇ»"
17032 .unindent(),
17033 );
17034}
17035
17036#[gpui::test]
17037async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
17038 init_test(cx, |_| {});
17039
17040 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
17041 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
17042
17043 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
17044 let multi_buffer = cx.new(|cx| {
17045 let mut multibuffer = MultiBuffer::new(ReadWrite);
17046 multibuffer.push_excerpts(
17047 buffer.clone(),
17048 [
17049 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
17050 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
17051 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
17052 ],
17053 cx,
17054 );
17055 multibuffer
17056 });
17057
17058 let editor =
17059 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
17060 editor
17061 .update(cx, |editor, _window, cx| {
17062 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
17063 editor
17064 .buffer
17065 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
17066 })
17067 .unwrap();
17068
17069 let mut cx = EditorTestContext::for_editor(editor, cx).await;
17070 cx.run_until_parked();
17071
17072 cx.update_editor(|editor, window, cx| {
17073 editor.expand_all_diff_hunks(&Default::default(), window, cx)
17074 });
17075 cx.executor().run_until_parked();
17076
17077 // When the start of a hunk coincides with the start of its excerpt,
17078 // the hunk is expanded. When the start of a a hunk is earlier than
17079 // the start of its excerpt, the hunk is not expanded.
17080 cx.assert_state_with_diff(
17081 "
17082 ˇaaa
17083 - bbb
17084 + BBB
17085
17086 - ddd
17087 - eee
17088 + DDD
17089 + EEE
17090 fff
17091
17092 iii
17093 "
17094 .unindent(),
17095 );
17096}
17097
17098#[gpui::test]
17099async fn test_edits_around_expanded_insertion_hunks(
17100 executor: BackgroundExecutor,
17101 cx: &mut TestAppContext,
17102) {
17103 init_test(cx, |_| {});
17104
17105 let mut cx = EditorTestContext::new(cx).await;
17106
17107 let diff_base = r#"
17108 use some::mod1;
17109 use some::mod2;
17110
17111 const A: u32 = 42;
17112
17113 fn main() {
17114 println!("hello");
17115
17116 println!("world");
17117 }
17118 "#
17119 .unindent();
17120 executor.run_until_parked();
17121 cx.set_state(
17122 &r#"
17123 use some::mod1;
17124 use some::mod2;
17125
17126 const A: u32 = 42;
17127 const B: u32 = 42;
17128 const C: u32 = 42;
17129 ˇ
17130
17131 fn main() {
17132 println!("hello");
17133
17134 println!("world");
17135 }
17136 "#
17137 .unindent(),
17138 );
17139
17140 cx.set_head_text(&diff_base);
17141 executor.run_until_parked();
17142
17143 cx.update_editor(|editor, window, cx| {
17144 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17145 });
17146 executor.run_until_parked();
17147
17148 cx.assert_state_with_diff(
17149 r#"
17150 use some::mod1;
17151 use some::mod2;
17152
17153 const A: u32 = 42;
17154 + const B: u32 = 42;
17155 + const C: u32 = 42;
17156 + ˇ
17157
17158 fn main() {
17159 println!("hello");
17160
17161 println!("world");
17162 }
17163 "#
17164 .unindent(),
17165 );
17166
17167 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
17168 executor.run_until_parked();
17169
17170 cx.assert_state_with_diff(
17171 r#"
17172 use some::mod1;
17173 use some::mod2;
17174
17175 const A: u32 = 42;
17176 + const B: u32 = 42;
17177 + const C: u32 = 42;
17178 + const D: u32 = 42;
17179 + ˇ
17180
17181 fn main() {
17182 println!("hello");
17183
17184 println!("world");
17185 }
17186 "#
17187 .unindent(),
17188 );
17189
17190 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
17191 executor.run_until_parked();
17192
17193 cx.assert_state_with_diff(
17194 r#"
17195 use some::mod1;
17196 use some::mod2;
17197
17198 const A: u32 = 42;
17199 + const B: u32 = 42;
17200 + const C: u32 = 42;
17201 + const D: u32 = 42;
17202 + const E: u32 = 42;
17203 + ˇ
17204
17205 fn main() {
17206 println!("hello");
17207
17208 println!("world");
17209 }
17210 "#
17211 .unindent(),
17212 );
17213
17214 cx.update_editor(|editor, window, cx| {
17215 editor.delete_line(&DeleteLine, window, cx);
17216 });
17217 executor.run_until_parked();
17218
17219 cx.assert_state_with_diff(
17220 r#"
17221 use some::mod1;
17222 use some::mod2;
17223
17224 const A: u32 = 42;
17225 + const B: u32 = 42;
17226 + const C: u32 = 42;
17227 + const D: u32 = 42;
17228 + const E: u32 = 42;
17229 ˇ
17230 fn main() {
17231 println!("hello");
17232
17233 println!("world");
17234 }
17235 "#
17236 .unindent(),
17237 );
17238
17239 cx.update_editor(|editor, window, cx| {
17240 editor.move_up(&MoveUp, window, cx);
17241 editor.delete_line(&DeleteLine, window, cx);
17242 editor.move_up(&MoveUp, window, cx);
17243 editor.delete_line(&DeleteLine, window, cx);
17244 editor.move_up(&MoveUp, window, cx);
17245 editor.delete_line(&DeleteLine, window, cx);
17246 });
17247 executor.run_until_parked();
17248 cx.assert_state_with_diff(
17249 r#"
17250 use some::mod1;
17251 use some::mod2;
17252
17253 const A: u32 = 42;
17254 + const B: u32 = 42;
17255 ˇ
17256 fn main() {
17257 println!("hello");
17258
17259 println!("world");
17260 }
17261 "#
17262 .unindent(),
17263 );
17264
17265 cx.update_editor(|editor, window, cx| {
17266 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
17267 editor.delete_line(&DeleteLine, window, cx);
17268 });
17269 executor.run_until_parked();
17270 cx.assert_state_with_diff(
17271 r#"
17272 ˇ
17273 fn main() {
17274 println!("hello");
17275
17276 println!("world");
17277 }
17278 "#
17279 .unindent(),
17280 );
17281}
17282
17283#[gpui::test]
17284async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
17285 init_test(cx, |_| {});
17286
17287 let mut cx = EditorTestContext::new(cx).await;
17288 cx.set_head_text(indoc! { "
17289 one
17290 two
17291 three
17292 four
17293 five
17294 "
17295 });
17296 cx.set_state(indoc! { "
17297 one
17298 ˇthree
17299 five
17300 "});
17301 cx.run_until_parked();
17302 cx.update_editor(|editor, window, cx| {
17303 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17304 });
17305 cx.assert_state_with_diff(
17306 indoc! { "
17307 one
17308 - two
17309 ˇthree
17310 - four
17311 five
17312 "}
17313 .to_string(),
17314 );
17315 cx.update_editor(|editor, window, cx| {
17316 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17317 });
17318
17319 cx.assert_state_with_diff(
17320 indoc! { "
17321 one
17322 ˇthree
17323 five
17324 "}
17325 .to_string(),
17326 );
17327
17328 cx.set_state(indoc! { "
17329 one
17330 ˇTWO
17331 three
17332 four
17333 five
17334 "});
17335 cx.run_until_parked();
17336 cx.update_editor(|editor, window, cx| {
17337 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17338 });
17339
17340 cx.assert_state_with_diff(
17341 indoc! { "
17342 one
17343 - two
17344 + ˇTWO
17345 three
17346 four
17347 five
17348 "}
17349 .to_string(),
17350 );
17351 cx.update_editor(|editor, window, cx| {
17352 editor.move_up(&Default::default(), window, cx);
17353 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17354 });
17355 cx.assert_state_with_diff(
17356 indoc! { "
17357 one
17358 ˇTWO
17359 three
17360 four
17361 five
17362 "}
17363 .to_string(),
17364 );
17365}
17366
17367#[gpui::test]
17368async fn test_edits_around_expanded_deletion_hunks(
17369 executor: BackgroundExecutor,
17370 cx: &mut TestAppContext,
17371) {
17372 init_test(cx, |_| {});
17373
17374 let mut cx = EditorTestContext::new(cx).await;
17375
17376 let diff_base = r#"
17377 use some::mod1;
17378 use some::mod2;
17379
17380 const A: u32 = 42;
17381 const B: u32 = 42;
17382 const C: u32 = 42;
17383
17384
17385 fn main() {
17386 println!("hello");
17387
17388 println!("world");
17389 }
17390 "#
17391 .unindent();
17392 executor.run_until_parked();
17393 cx.set_state(
17394 &r#"
17395 use some::mod1;
17396 use some::mod2;
17397
17398 ˇconst B: u32 = 42;
17399 const C: u32 = 42;
17400
17401
17402 fn main() {
17403 println!("hello");
17404
17405 println!("world");
17406 }
17407 "#
17408 .unindent(),
17409 );
17410
17411 cx.set_head_text(&diff_base);
17412 executor.run_until_parked();
17413
17414 cx.update_editor(|editor, window, cx| {
17415 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17416 });
17417 executor.run_until_parked();
17418
17419 cx.assert_state_with_diff(
17420 r#"
17421 use some::mod1;
17422 use some::mod2;
17423
17424 - const A: u32 = 42;
17425 ˇconst B: u32 = 42;
17426 const C: u32 = 42;
17427
17428
17429 fn main() {
17430 println!("hello");
17431
17432 println!("world");
17433 }
17434 "#
17435 .unindent(),
17436 );
17437
17438 cx.update_editor(|editor, window, cx| {
17439 editor.delete_line(&DeleteLine, window, cx);
17440 });
17441 executor.run_until_parked();
17442 cx.assert_state_with_diff(
17443 r#"
17444 use some::mod1;
17445 use some::mod2;
17446
17447 - const A: u32 = 42;
17448 - const B: u32 = 42;
17449 ˇconst C: u32 = 42;
17450
17451
17452 fn main() {
17453 println!("hello");
17454
17455 println!("world");
17456 }
17457 "#
17458 .unindent(),
17459 );
17460
17461 cx.update_editor(|editor, window, cx| {
17462 editor.delete_line(&DeleteLine, window, cx);
17463 });
17464 executor.run_until_parked();
17465 cx.assert_state_with_diff(
17466 r#"
17467 use some::mod1;
17468 use some::mod2;
17469
17470 - const A: u32 = 42;
17471 - const B: u32 = 42;
17472 - const C: u32 = 42;
17473 ˇ
17474
17475 fn main() {
17476 println!("hello");
17477
17478 println!("world");
17479 }
17480 "#
17481 .unindent(),
17482 );
17483
17484 cx.update_editor(|editor, window, cx| {
17485 editor.handle_input("replacement", window, cx);
17486 });
17487 executor.run_until_parked();
17488 cx.assert_state_with_diff(
17489 r#"
17490 use some::mod1;
17491 use some::mod2;
17492
17493 - const A: u32 = 42;
17494 - const B: u32 = 42;
17495 - const C: u32 = 42;
17496 -
17497 + replacementˇ
17498
17499 fn main() {
17500 println!("hello");
17501
17502 println!("world");
17503 }
17504 "#
17505 .unindent(),
17506 );
17507}
17508
17509#[gpui::test]
17510async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17511 init_test(cx, |_| {});
17512
17513 let mut cx = EditorTestContext::new(cx).await;
17514
17515 let base_text = r#"
17516 one
17517 two
17518 three
17519 four
17520 five
17521 "#
17522 .unindent();
17523 executor.run_until_parked();
17524 cx.set_state(
17525 &r#"
17526 one
17527 two
17528 fˇour
17529 five
17530 "#
17531 .unindent(),
17532 );
17533
17534 cx.set_head_text(&base_text);
17535 executor.run_until_parked();
17536
17537 cx.update_editor(|editor, window, cx| {
17538 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17539 });
17540 executor.run_until_parked();
17541
17542 cx.assert_state_with_diff(
17543 r#"
17544 one
17545 two
17546 - three
17547 fˇour
17548 five
17549 "#
17550 .unindent(),
17551 );
17552
17553 cx.update_editor(|editor, window, cx| {
17554 editor.backspace(&Backspace, window, cx);
17555 editor.backspace(&Backspace, window, cx);
17556 });
17557 executor.run_until_parked();
17558 cx.assert_state_with_diff(
17559 r#"
17560 one
17561 two
17562 - threeˇ
17563 - four
17564 + our
17565 five
17566 "#
17567 .unindent(),
17568 );
17569}
17570
17571#[gpui::test]
17572async fn test_edit_after_expanded_modification_hunk(
17573 executor: BackgroundExecutor,
17574 cx: &mut TestAppContext,
17575) {
17576 init_test(cx, |_| {});
17577
17578 let mut cx = EditorTestContext::new(cx).await;
17579
17580 let diff_base = r#"
17581 use some::mod1;
17582 use some::mod2;
17583
17584 const A: u32 = 42;
17585 const B: u32 = 42;
17586 const C: u32 = 42;
17587 const D: u32 = 42;
17588
17589
17590 fn main() {
17591 println!("hello");
17592
17593 println!("world");
17594 }"#
17595 .unindent();
17596
17597 cx.set_state(
17598 &r#"
17599 use some::mod1;
17600 use some::mod2;
17601
17602 const A: u32 = 42;
17603 const B: u32 = 42;
17604 const C: u32 = 43ˇ
17605 const D: u32 = 42;
17606
17607
17608 fn main() {
17609 println!("hello");
17610
17611 println!("world");
17612 }"#
17613 .unindent(),
17614 );
17615
17616 cx.set_head_text(&diff_base);
17617 executor.run_until_parked();
17618 cx.update_editor(|editor, window, cx| {
17619 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17620 });
17621 executor.run_until_parked();
17622
17623 cx.assert_state_with_diff(
17624 r#"
17625 use some::mod1;
17626 use some::mod2;
17627
17628 const A: u32 = 42;
17629 const B: u32 = 42;
17630 - const C: u32 = 42;
17631 + const C: u32 = 43ˇ
17632 const D: u32 = 42;
17633
17634
17635 fn main() {
17636 println!("hello");
17637
17638 println!("world");
17639 }"#
17640 .unindent(),
17641 );
17642
17643 cx.update_editor(|editor, window, cx| {
17644 editor.handle_input("\nnew_line\n", window, cx);
17645 });
17646 executor.run_until_parked();
17647
17648 cx.assert_state_with_diff(
17649 r#"
17650 use some::mod1;
17651 use some::mod2;
17652
17653 const A: u32 = 42;
17654 const B: u32 = 42;
17655 - const C: u32 = 42;
17656 + const C: u32 = 43
17657 + new_line
17658 + ˇ
17659 const D: u32 = 42;
17660
17661
17662 fn main() {
17663 println!("hello");
17664
17665 println!("world");
17666 }"#
17667 .unindent(),
17668 );
17669}
17670
17671#[gpui::test]
17672async fn test_stage_and_unstage_added_file_hunk(
17673 executor: BackgroundExecutor,
17674 cx: &mut TestAppContext,
17675) {
17676 init_test(cx, |_| {});
17677
17678 let mut cx = EditorTestContext::new(cx).await;
17679 cx.update_editor(|editor, _, cx| {
17680 editor.set_expand_all_diff_hunks(cx);
17681 });
17682
17683 let working_copy = r#"
17684 ˇfn main() {
17685 println!("hello, world!");
17686 }
17687 "#
17688 .unindent();
17689
17690 cx.set_state(&working_copy);
17691 executor.run_until_parked();
17692
17693 cx.assert_state_with_diff(
17694 r#"
17695 + ˇfn main() {
17696 + println!("hello, world!");
17697 + }
17698 "#
17699 .unindent(),
17700 );
17701 cx.assert_index_text(None);
17702
17703 cx.update_editor(|editor, window, cx| {
17704 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
17705 });
17706 executor.run_until_parked();
17707 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
17708 cx.assert_state_with_diff(
17709 r#"
17710 + ˇfn main() {
17711 + println!("hello, world!");
17712 + }
17713 "#
17714 .unindent(),
17715 );
17716
17717 cx.update_editor(|editor, window, cx| {
17718 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
17719 });
17720 executor.run_until_parked();
17721 cx.assert_index_text(None);
17722}
17723
17724async fn setup_indent_guides_editor(
17725 text: &str,
17726 cx: &mut TestAppContext,
17727) -> (BufferId, EditorTestContext) {
17728 init_test(cx, |_| {});
17729
17730 let mut cx = EditorTestContext::new(cx).await;
17731
17732 let buffer_id = cx.update_editor(|editor, window, cx| {
17733 editor.set_text(text, window, cx);
17734 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
17735
17736 buffer_ids[0]
17737 });
17738
17739 (buffer_id, cx)
17740}
17741
17742fn assert_indent_guides(
17743 range: Range<u32>,
17744 expected: Vec<IndentGuide>,
17745 active_indices: Option<Vec<usize>>,
17746 cx: &mut EditorTestContext,
17747) {
17748 let indent_guides = cx.update_editor(|editor, window, cx| {
17749 let snapshot = editor.snapshot(window, cx).display_snapshot;
17750 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
17751 editor,
17752 MultiBufferRow(range.start)..MultiBufferRow(range.end),
17753 true,
17754 &snapshot,
17755 cx,
17756 );
17757
17758 indent_guides.sort_by(|a, b| {
17759 a.depth.cmp(&b.depth).then(
17760 a.start_row
17761 .cmp(&b.start_row)
17762 .then(a.end_row.cmp(&b.end_row)),
17763 )
17764 });
17765 indent_guides
17766 });
17767
17768 if let Some(expected) = active_indices {
17769 let active_indices = cx.update_editor(|editor, window, cx| {
17770 let snapshot = editor.snapshot(window, cx).display_snapshot;
17771 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
17772 });
17773
17774 assert_eq!(
17775 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
17776 expected,
17777 "Active indent guide indices do not match"
17778 );
17779 }
17780
17781 assert_eq!(indent_guides, expected, "Indent guides do not match");
17782}
17783
17784fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
17785 IndentGuide {
17786 buffer_id,
17787 start_row: MultiBufferRow(start_row),
17788 end_row: MultiBufferRow(end_row),
17789 depth,
17790 tab_size: 4,
17791 settings: IndentGuideSettings {
17792 enabled: true,
17793 line_width: 1,
17794 active_line_width: 1,
17795 ..Default::default()
17796 },
17797 }
17798}
17799
17800#[gpui::test]
17801async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
17802 let (buffer_id, mut cx) = setup_indent_guides_editor(
17803 &"
17804 fn main() {
17805 let a = 1;
17806 }"
17807 .unindent(),
17808 cx,
17809 )
17810 .await;
17811
17812 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
17813}
17814
17815#[gpui::test]
17816async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
17817 let (buffer_id, mut cx) = setup_indent_guides_editor(
17818 &"
17819 fn main() {
17820 let a = 1;
17821 let b = 2;
17822 }"
17823 .unindent(),
17824 cx,
17825 )
17826 .await;
17827
17828 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
17829}
17830
17831#[gpui::test]
17832async fn test_indent_guide_nested(cx: &mut TestAppContext) {
17833 let (buffer_id, mut cx) = setup_indent_guides_editor(
17834 &"
17835 fn main() {
17836 let a = 1;
17837 if a == 3 {
17838 let b = 2;
17839 } else {
17840 let c = 3;
17841 }
17842 }"
17843 .unindent(),
17844 cx,
17845 )
17846 .await;
17847
17848 assert_indent_guides(
17849 0..8,
17850 vec![
17851 indent_guide(buffer_id, 1, 6, 0),
17852 indent_guide(buffer_id, 3, 3, 1),
17853 indent_guide(buffer_id, 5, 5, 1),
17854 ],
17855 None,
17856 &mut cx,
17857 );
17858}
17859
17860#[gpui::test]
17861async fn test_indent_guide_tab(cx: &mut TestAppContext) {
17862 let (buffer_id, mut cx) = setup_indent_guides_editor(
17863 &"
17864 fn main() {
17865 let a = 1;
17866 let b = 2;
17867 let c = 3;
17868 }"
17869 .unindent(),
17870 cx,
17871 )
17872 .await;
17873
17874 assert_indent_guides(
17875 0..5,
17876 vec![
17877 indent_guide(buffer_id, 1, 3, 0),
17878 indent_guide(buffer_id, 2, 2, 1),
17879 ],
17880 None,
17881 &mut cx,
17882 );
17883}
17884
17885#[gpui::test]
17886async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
17887 let (buffer_id, mut cx) = setup_indent_guides_editor(
17888 &"
17889 fn main() {
17890 let a = 1;
17891
17892 let c = 3;
17893 }"
17894 .unindent(),
17895 cx,
17896 )
17897 .await;
17898
17899 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
17900}
17901
17902#[gpui::test]
17903async fn test_indent_guide_complex(cx: &mut TestAppContext) {
17904 let (buffer_id, mut cx) = setup_indent_guides_editor(
17905 &"
17906 fn main() {
17907 let a = 1;
17908
17909 let c = 3;
17910
17911 if a == 3 {
17912 let b = 2;
17913 } else {
17914 let c = 3;
17915 }
17916 }"
17917 .unindent(),
17918 cx,
17919 )
17920 .await;
17921
17922 assert_indent_guides(
17923 0..11,
17924 vec![
17925 indent_guide(buffer_id, 1, 9, 0),
17926 indent_guide(buffer_id, 6, 6, 1),
17927 indent_guide(buffer_id, 8, 8, 1),
17928 ],
17929 None,
17930 &mut cx,
17931 );
17932}
17933
17934#[gpui::test]
17935async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
17936 let (buffer_id, mut cx) = setup_indent_guides_editor(
17937 &"
17938 fn main() {
17939 let a = 1;
17940
17941 let c = 3;
17942
17943 if a == 3 {
17944 let b = 2;
17945 } else {
17946 let c = 3;
17947 }
17948 }"
17949 .unindent(),
17950 cx,
17951 )
17952 .await;
17953
17954 assert_indent_guides(
17955 1..11,
17956 vec![
17957 indent_guide(buffer_id, 1, 9, 0),
17958 indent_guide(buffer_id, 6, 6, 1),
17959 indent_guide(buffer_id, 8, 8, 1),
17960 ],
17961 None,
17962 &mut cx,
17963 );
17964}
17965
17966#[gpui::test]
17967async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
17968 let (buffer_id, mut cx) = setup_indent_guides_editor(
17969 &"
17970 fn main() {
17971 let a = 1;
17972
17973 let c = 3;
17974
17975 if a == 3 {
17976 let b = 2;
17977 } else {
17978 let c = 3;
17979 }
17980 }"
17981 .unindent(),
17982 cx,
17983 )
17984 .await;
17985
17986 assert_indent_guides(
17987 1..10,
17988 vec![
17989 indent_guide(buffer_id, 1, 9, 0),
17990 indent_guide(buffer_id, 6, 6, 1),
17991 indent_guide(buffer_id, 8, 8, 1),
17992 ],
17993 None,
17994 &mut cx,
17995 );
17996}
17997
17998#[gpui::test]
17999async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
18000 let (buffer_id, mut cx) = setup_indent_guides_editor(
18001 &"
18002 fn main() {
18003 if a {
18004 b(
18005 c,
18006 d,
18007 )
18008 } else {
18009 e(
18010 f
18011 )
18012 }
18013 }"
18014 .unindent(),
18015 cx,
18016 )
18017 .await;
18018
18019 assert_indent_guides(
18020 0..11,
18021 vec![
18022 indent_guide(buffer_id, 1, 10, 0),
18023 indent_guide(buffer_id, 2, 5, 1),
18024 indent_guide(buffer_id, 7, 9, 1),
18025 indent_guide(buffer_id, 3, 4, 2),
18026 indent_guide(buffer_id, 8, 8, 2),
18027 ],
18028 None,
18029 &mut cx,
18030 );
18031
18032 cx.update_editor(|editor, window, cx| {
18033 editor.fold_at(MultiBufferRow(2), window, cx);
18034 assert_eq!(
18035 editor.display_text(cx),
18036 "
18037 fn main() {
18038 if a {
18039 b(⋯
18040 )
18041 } else {
18042 e(
18043 f
18044 )
18045 }
18046 }"
18047 .unindent()
18048 );
18049 });
18050
18051 assert_indent_guides(
18052 0..11,
18053 vec![
18054 indent_guide(buffer_id, 1, 10, 0),
18055 indent_guide(buffer_id, 2, 5, 1),
18056 indent_guide(buffer_id, 7, 9, 1),
18057 indent_guide(buffer_id, 8, 8, 2),
18058 ],
18059 None,
18060 &mut cx,
18061 );
18062}
18063
18064#[gpui::test]
18065async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
18066 let (buffer_id, mut cx) = setup_indent_guides_editor(
18067 &"
18068 block1
18069 block2
18070 block3
18071 block4
18072 block2
18073 block1
18074 block1"
18075 .unindent(),
18076 cx,
18077 )
18078 .await;
18079
18080 assert_indent_guides(
18081 1..10,
18082 vec![
18083 indent_guide(buffer_id, 1, 4, 0),
18084 indent_guide(buffer_id, 2, 3, 1),
18085 indent_guide(buffer_id, 3, 3, 2),
18086 ],
18087 None,
18088 &mut cx,
18089 );
18090}
18091
18092#[gpui::test]
18093async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
18094 let (buffer_id, mut cx) = setup_indent_guides_editor(
18095 &"
18096 block1
18097 block2
18098 block3
18099
18100 block1
18101 block1"
18102 .unindent(),
18103 cx,
18104 )
18105 .await;
18106
18107 assert_indent_guides(
18108 0..6,
18109 vec![
18110 indent_guide(buffer_id, 1, 2, 0),
18111 indent_guide(buffer_id, 2, 2, 1),
18112 ],
18113 None,
18114 &mut cx,
18115 );
18116}
18117
18118#[gpui::test]
18119async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
18120 let (buffer_id, mut cx) = setup_indent_guides_editor(
18121 &"
18122 function component() {
18123 \treturn (
18124 \t\t\t
18125 \t\t<div>
18126 \t\t\t<abc></abc>
18127 \t\t</div>
18128 \t)
18129 }"
18130 .unindent(),
18131 cx,
18132 )
18133 .await;
18134
18135 assert_indent_guides(
18136 0..8,
18137 vec![
18138 indent_guide(buffer_id, 1, 6, 0),
18139 indent_guide(buffer_id, 2, 5, 1),
18140 indent_guide(buffer_id, 4, 4, 2),
18141 ],
18142 None,
18143 &mut cx,
18144 );
18145}
18146
18147#[gpui::test]
18148async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
18149 let (buffer_id, mut cx) = setup_indent_guides_editor(
18150 &"
18151 function component() {
18152 \treturn (
18153 \t
18154 \t\t<div>
18155 \t\t\t<abc></abc>
18156 \t\t</div>
18157 \t)
18158 }"
18159 .unindent(),
18160 cx,
18161 )
18162 .await;
18163
18164 assert_indent_guides(
18165 0..8,
18166 vec![
18167 indent_guide(buffer_id, 1, 6, 0),
18168 indent_guide(buffer_id, 2, 5, 1),
18169 indent_guide(buffer_id, 4, 4, 2),
18170 ],
18171 None,
18172 &mut cx,
18173 );
18174}
18175
18176#[gpui::test]
18177async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
18178 let (buffer_id, mut cx) = setup_indent_guides_editor(
18179 &"
18180 block1
18181
18182
18183
18184 block2
18185 "
18186 .unindent(),
18187 cx,
18188 )
18189 .await;
18190
18191 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
18192}
18193
18194#[gpui::test]
18195async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
18196 let (buffer_id, mut cx) = setup_indent_guides_editor(
18197 &"
18198 def a:
18199 \tb = 3
18200 \tif True:
18201 \t\tc = 4
18202 \t\td = 5
18203 \tprint(b)
18204 "
18205 .unindent(),
18206 cx,
18207 )
18208 .await;
18209
18210 assert_indent_guides(
18211 0..6,
18212 vec![
18213 indent_guide(buffer_id, 1, 5, 0),
18214 indent_guide(buffer_id, 3, 4, 1),
18215 ],
18216 None,
18217 &mut cx,
18218 );
18219}
18220
18221#[gpui::test]
18222async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
18223 let (buffer_id, mut cx) = setup_indent_guides_editor(
18224 &"
18225 fn main() {
18226 let a = 1;
18227 }"
18228 .unindent(),
18229 cx,
18230 )
18231 .await;
18232
18233 cx.update_editor(|editor, window, cx| {
18234 editor.change_selections(None, window, cx, |s| {
18235 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
18236 });
18237 });
18238
18239 assert_indent_guides(
18240 0..3,
18241 vec![indent_guide(buffer_id, 1, 1, 0)],
18242 Some(vec![0]),
18243 &mut cx,
18244 );
18245}
18246
18247#[gpui::test]
18248async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
18249 let (buffer_id, mut cx) = setup_indent_guides_editor(
18250 &"
18251 fn main() {
18252 if 1 == 2 {
18253 let a = 1;
18254 }
18255 }"
18256 .unindent(),
18257 cx,
18258 )
18259 .await;
18260
18261 cx.update_editor(|editor, window, cx| {
18262 editor.change_selections(None, window, cx, |s| {
18263 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
18264 });
18265 });
18266
18267 assert_indent_guides(
18268 0..4,
18269 vec![
18270 indent_guide(buffer_id, 1, 3, 0),
18271 indent_guide(buffer_id, 2, 2, 1),
18272 ],
18273 Some(vec![1]),
18274 &mut cx,
18275 );
18276
18277 cx.update_editor(|editor, window, cx| {
18278 editor.change_selections(None, window, cx, |s| {
18279 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
18280 });
18281 });
18282
18283 assert_indent_guides(
18284 0..4,
18285 vec![
18286 indent_guide(buffer_id, 1, 3, 0),
18287 indent_guide(buffer_id, 2, 2, 1),
18288 ],
18289 Some(vec![1]),
18290 &mut cx,
18291 );
18292
18293 cx.update_editor(|editor, window, cx| {
18294 editor.change_selections(None, window, cx, |s| {
18295 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
18296 });
18297 });
18298
18299 assert_indent_guides(
18300 0..4,
18301 vec![
18302 indent_guide(buffer_id, 1, 3, 0),
18303 indent_guide(buffer_id, 2, 2, 1),
18304 ],
18305 Some(vec![0]),
18306 &mut cx,
18307 );
18308}
18309
18310#[gpui::test]
18311async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
18312 let (buffer_id, mut cx) = setup_indent_guides_editor(
18313 &"
18314 fn main() {
18315 let a = 1;
18316
18317 let b = 2;
18318 }"
18319 .unindent(),
18320 cx,
18321 )
18322 .await;
18323
18324 cx.update_editor(|editor, window, cx| {
18325 editor.change_selections(None, window, cx, |s| {
18326 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
18327 });
18328 });
18329
18330 assert_indent_guides(
18331 0..5,
18332 vec![indent_guide(buffer_id, 1, 3, 0)],
18333 Some(vec![0]),
18334 &mut cx,
18335 );
18336}
18337
18338#[gpui::test]
18339async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
18340 let (buffer_id, mut cx) = setup_indent_guides_editor(
18341 &"
18342 def m:
18343 a = 1
18344 pass"
18345 .unindent(),
18346 cx,
18347 )
18348 .await;
18349
18350 cx.update_editor(|editor, window, cx| {
18351 editor.change_selections(None, window, cx, |s| {
18352 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
18353 });
18354 });
18355
18356 assert_indent_guides(
18357 0..3,
18358 vec![indent_guide(buffer_id, 1, 2, 0)],
18359 Some(vec![0]),
18360 &mut cx,
18361 );
18362}
18363
18364#[gpui::test]
18365async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
18366 init_test(cx, |_| {});
18367 let mut cx = EditorTestContext::new(cx).await;
18368 let text = indoc! {
18369 "
18370 impl A {
18371 fn b() {
18372 0;
18373 3;
18374 5;
18375 6;
18376 7;
18377 }
18378 }
18379 "
18380 };
18381 let base_text = indoc! {
18382 "
18383 impl A {
18384 fn b() {
18385 0;
18386 1;
18387 2;
18388 3;
18389 4;
18390 }
18391 fn c() {
18392 5;
18393 6;
18394 7;
18395 }
18396 }
18397 "
18398 };
18399
18400 cx.update_editor(|editor, window, cx| {
18401 editor.set_text(text, window, cx);
18402
18403 editor.buffer().update(cx, |multibuffer, cx| {
18404 let buffer = multibuffer.as_singleton().unwrap();
18405 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
18406
18407 multibuffer.set_all_diff_hunks_expanded(cx);
18408 multibuffer.add_diff(diff, cx);
18409
18410 buffer.read(cx).remote_id()
18411 })
18412 });
18413 cx.run_until_parked();
18414
18415 cx.assert_state_with_diff(
18416 indoc! { "
18417 impl A {
18418 fn b() {
18419 0;
18420 - 1;
18421 - 2;
18422 3;
18423 - 4;
18424 - }
18425 - fn c() {
18426 5;
18427 6;
18428 7;
18429 }
18430 }
18431 ˇ"
18432 }
18433 .to_string(),
18434 );
18435
18436 let mut actual_guides = cx.update_editor(|editor, window, cx| {
18437 editor
18438 .snapshot(window, cx)
18439 .buffer_snapshot
18440 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
18441 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
18442 .collect::<Vec<_>>()
18443 });
18444 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
18445 assert_eq!(
18446 actual_guides,
18447 vec![
18448 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
18449 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
18450 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
18451 ]
18452 );
18453}
18454
18455#[gpui::test]
18456async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
18457 init_test(cx, |_| {});
18458 let mut cx = EditorTestContext::new(cx).await;
18459
18460 let diff_base = r#"
18461 a
18462 b
18463 c
18464 "#
18465 .unindent();
18466
18467 cx.set_state(
18468 &r#"
18469 ˇA
18470 b
18471 C
18472 "#
18473 .unindent(),
18474 );
18475 cx.set_head_text(&diff_base);
18476 cx.update_editor(|editor, window, cx| {
18477 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18478 });
18479 executor.run_until_parked();
18480
18481 let both_hunks_expanded = r#"
18482 - a
18483 + ˇA
18484 b
18485 - c
18486 + C
18487 "#
18488 .unindent();
18489
18490 cx.assert_state_with_diff(both_hunks_expanded.clone());
18491
18492 let hunk_ranges = cx.update_editor(|editor, window, cx| {
18493 let snapshot = editor.snapshot(window, cx);
18494 let hunks = editor
18495 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
18496 .collect::<Vec<_>>();
18497 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
18498 let buffer_id = hunks[0].buffer_id;
18499 hunks
18500 .into_iter()
18501 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
18502 .collect::<Vec<_>>()
18503 });
18504 assert_eq!(hunk_ranges.len(), 2);
18505
18506 cx.update_editor(|editor, _, cx| {
18507 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18508 });
18509 executor.run_until_parked();
18510
18511 let second_hunk_expanded = r#"
18512 ˇA
18513 b
18514 - c
18515 + C
18516 "#
18517 .unindent();
18518
18519 cx.assert_state_with_diff(second_hunk_expanded);
18520
18521 cx.update_editor(|editor, _, cx| {
18522 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18523 });
18524 executor.run_until_parked();
18525
18526 cx.assert_state_with_diff(both_hunks_expanded.clone());
18527
18528 cx.update_editor(|editor, _, cx| {
18529 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
18530 });
18531 executor.run_until_parked();
18532
18533 let first_hunk_expanded = r#"
18534 - a
18535 + ˇA
18536 b
18537 C
18538 "#
18539 .unindent();
18540
18541 cx.assert_state_with_diff(first_hunk_expanded);
18542
18543 cx.update_editor(|editor, _, cx| {
18544 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
18545 });
18546 executor.run_until_parked();
18547
18548 cx.assert_state_with_diff(both_hunks_expanded);
18549
18550 cx.set_state(
18551 &r#"
18552 ˇA
18553 b
18554 "#
18555 .unindent(),
18556 );
18557 cx.run_until_parked();
18558
18559 // TODO this cursor position seems bad
18560 cx.assert_state_with_diff(
18561 r#"
18562 - ˇa
18563 + A
18564 b
18565 "#
18566 .unindent(),
18567 );
18568
18569 cx.update_editor(|editor, window, cx| {
18570 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18571 });
18572
18573 cx.assert_state_with_diff(
18574 r#"
18575 - ˇa
18576 + A
18577 b
18578 - c
18579 "#
18580 .unindent(),
18581 );
18582
18583 let hunk_ranges = cx.update_editor(|editor, window, cx| {
18584 let snapshot = editor.snapshot(window, cx);
18585 let hunks = editor
18586 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
18587 .collect::<Vec<_>>();
18588 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
18589 let buffer_id = hunks[0].buffer_id;
18590 hunks
18591 .into_iter()
18592 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
18593 .collect::<Vec<_>>()
18594 });
18595 assert_eq!(hunk_ranges.len(), 2);
18596
18597 cx.update_editor(|editor, _, cx| {
18598 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
18599 });
18600 executor.run_until_parked();
18601
18602 cx.assert_state_with_diff(
18603 r#"
18604 - ˇa
18605 + A
18606 b
18607 "#
18608 .unindent(),
18609 );
18610}
18611
18612#[gpui::test]
18613async fn test_toggle_deletion_hunk_at_start_of_file(
18614 executor: BackgroundExecutor,
18615 cx: &mut TestAppContext,
18616) {
18617 init_test(cx, |_| {});
18618 let mut cx = EditorTestContext::new(cx).await;
18619
18620 let diff_base = r#"
18621 a
18622 b
18623 c
18624 "#
18625 .unindent();
18626
18627 cx.set_state(
18628 &r#"
18629 ˇb
18630 c
18631 "#
18632 .unindent(),
18633 );
18634 cx.set_head_text(&diff_base);
18635 cx.update_editor(|editor, window, cx| {
18636 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18637 });
18638 executor.run_until_parked();
18639
18640 let hunk_expanded = r#"
18641 - a
18642 ˇb
18643 c
18644 "#
18645 .unindent();
18646
18647 cx.assert_state_with_diff(hunk_expanded.clone());
18648
18649 let hunk_ranges = cx.update_editor(|editor, window, cx| {
18650 let snapshot = editor.snapshot(window, cx);
18651 let hunks = editor
18652 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
18653 .collect::<Vec<_>>();
18654 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
18655 let buffer_id = hunks[0].buffer_id;
18656 hunks
18657 .into_iter()
18658 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
18659 .collect::<Vec<_>>()
18660 });
18661 assert_eq!(hunk_ranges.len(), 1);
18662
18663 cx.update_editor(|editor, _, cx| {
18664 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18665 });
18666 executor.run_until_parked();
18667
18668 let hunk_collapsed = r#"
18669 ˇb
18670 c
18671 "#
18672 .unindent();
18673
18674 cx.assert_state_with_diff(hunk_collapsed);
18675
18676 cx.update_editor(|editor, _, cx| {
18677 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18678 });
18679 executor.run_until_parked();
18680
18681 cx.assert_state_with_diff(hunk_expanded.clone());
18682}
18683
18684#[gpui::test]
18685async fn test_display_diff_hunks(cx: &mut TestAppContext) {
18686 init_test(cx, |_| {});
18687
18688 let fs = FakeFs::new(cx.executor());
18689 fs.insert_tree(
18690 path!("/test"),
18691 json!({
18692 ".git": {},
18693 "file-1": "ONE\n",
18694 "file-2": "TWO\n",
18695 "file-3": "THREE\n",
18696 }),
18697 )
18698 .await;
18699
18700 fs.set_head_for_repo(
18701 path!("/test/.git").as_ref(),
18702 &[
18703 ("file-1".into(), "one\n".into()),
18704 ("file-2".into(), "two\n".into()),
18705 ("file-3".into(), "three\n".into()),
18706 ],
18707 "deadbeef",
18708 );
18709
18710 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
18711 let mut buffers = vec![];
18712 for i in 1..=3 {
18713 let buffer = project
18714 .update(cx, |project, cx| {
18715 let path = format!(path!("/test/file-{}"), i);
18716 project.open_local_buffer(path, cx)
18717 })
18718 .await
18719 .unwrap();
18720 buffers.push(buffer);
18721 }
18722
18723 let multibuffer = cx.new(|cx| {
18724 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
18725 multibuffer.set_all_diff_hunks_expanded(cx);
18726 for buffer in &buffers {
18727 let snapshot = buffer.read(cx).snapshot();
18728 multibuffer.set_excerpts_for_path(
18729 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
18730 buffer.clone(),
18731 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
18732 DEFAULT_MULTIBUFFER_CONTEXT,
18733 cx,
18734 );
18735 }
18736 multibuffer
18737 });
18738
18739 let editor = cx.add_window(|window, cx| {
18740 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
18741 });
18742 cx.run_until_parked();
18743
18744 let snapshot = editor
18745 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
18746 .unwrap();
18747 let hunks = snapshot
18748 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
18749 .map(|hunk| match hunk {
18750 DisplayDiffHunk::Unfolded {
18751 display_row_range, ..
18752 } => display_row_range,
18753 DisplayDiffHunk::Folded { .. } => unreachable!(),
18754 })
18755 .collect::<Vec<_>>();
18756 assert_eq!(
18757 hunks,
18758 [
18759 DisplayRow(2)..DisplayRow(4),
18760 DisplayRow(7)..DisplayRow(9),
18761 DisplayRow(12)..DisplayRow(14),
18762 ]
18763 );
18764}
18765
18766#[gpui::test]
18767async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
18768 init_test(cx, |_| {});
18769
18770 let mut cx = EditorTestContext::new(cx).await;
18771 cx.set_head_text(indoc! { "
18772 one
18773 two
18774 three
18775 four
18776 five
18777 "
18778 });
18779 cx.set_index_text(indoc! { "
18780 one
18781 two
18782 three
18783 four
18784 five
18785 "
18786 });
18787 cx.set_state(indoc! {"
18788 one
18789 TWO
18790 ˇTHREE
18791 FOUR
18792 five
18793 "});
18794 cx.run_until_parked();
18795 cx.update_editor(|editor, window, cx| {
18796 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
18797 });
18798 cx.run_until_parked();
18799 cx.assert_index_text(Some(indoc! {"
18800 one
18801 TWO
18802 THREE
18803 FOUR
18804 five
18805 "}));
18806 cx.set_state(indoc! { "
18807 one
18808 TWO
18809 ˇTHREE-HUNDRED
18810 FOUR
18811 five
18812 "});
18813 cx.run_until_parked();
18814 cx.update_editor(|editor, window, cx| {
18815 let snapshot = editor.snapshot(window, cx);
18816 let hunks = editor
18817 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
18818 .collect::<Vec<_>>();
18819 assert_eq!(hunks.len(), 1);
18820 assert_eq!(
18821 hunks[0].status(),
18822 DiffHunkStatus {
18823 kind: DiffHunkStatusKind::Modified,
18824 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
18825 }
18826 );
18827
18828 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
18829 });
18830 cx.run_until_parked();
18831 cx.assert_index_text(Some(indoc! {"
18832 one
18833 TWO
18834 THREE-HUNDRED
18835 FOUR
18836 five
18837 "}));
18838}
18839
18840#[gpui::test]
18841fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
18842 init_test(cx, |_| {});
18843
18844 let editor = cx.add_window(|window, cx| {
18845 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
18846 build_editor(buffer, window, cx)
18847 });
18848
18849 let render_args = Arc::new(Mutex::new(None));
18850 let snapshot = editor
18851 .update(cx, |editor, window, cx| {
18852 let snapshot = editor.buffer().read(cx).snapshot(cx);
18853 let range =
18854 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
18855
18856 struct RenderArgs {
18857 row: MultiBufferRow,
18858 folded: bool,
18859 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
18860 }
18861
18862 let crease = Crease::inline(
18863 range,
18864 FoldPlaceholder::test(),
18865 {
18866 let toggle_callback = render_args.clone();
18867 move |row, folded, callback, _window, _cx| {
18868 *toggle_callback.lock() = Some(RenderArgs {
18869 row,
18870 folded,
18871 callback,
18872 });
18873 div()
18874 }
18875 },
18876 |_row, _folded, _window, _cx| div(),
18877 );
18878
18879 editor.insert_creases(Some(crease), cx);
18880 let snapshot = editor.snapshot(window, cx);
18881 let _div = snapshot.render_crease_toggle(
18882 MultiBufferRow(1),
18883 false,
18884 cx.entity().clone(),
18885 window,
18886 cx,
18887 );
18888 snapshot
18889 })
18890 .unwrap();
18891
18892 let render_args = render_args.lock().take().unwrap();
18893 assert_eq!(render_args.row, MultiBufferRow(1));
18894 assert!(!render_args.folded);
18895 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
18896
18897 cx.update_window(*editor, |_, window, cx| {
18898 (render_args.callback)(true, window, cx)
18899 })
18900 .unwrap();
18901 let snapshot = editor
18902 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
18903 .unwrap();
18904 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
18905
18906 cx.update_window(*editor, |_, window, cx| {
18907 (render_args.callback)(false, window, cx)
18908 })
18909 .unwrap();
18910 let snapshot = editor
18911 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
18912 .unwrap();
18913 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
18914}
18915
18916#[gpui::test]
18917async fn test_input_text(cx: &mut TestAppContext) {
18918 init_test(cx, |_| {});
18919 let mut cx = EditorTestContext::new(cx).await;
18920
18921 cx.set_state(
18922 &r#"ˇone
18923 two
18924
18925 three
18926 fourˇ
18927 five
18928
18929 siˇx"#
18930 .unindent(),
18931 );
18932
18933 cx.dispatch_action(HandleInput(String::new()));
18934 cx.assert_editor_state(
18935 &r#"ˇone
18936 two
18937
18938 three
18939 fourˇ
18940 five
18941
18942 siˇx"#
18943 .unindent(),
18944 );
18945
18946 cx.dispatch_action(HandleInput("AAAA".to_string()));
18947 cx.assert_editor_state(
18948 &r#"AAAAˇone
18949 two
18950
18951 three
18952 fourAAAAˇ
18953 five
18954
18955 siAAAAˇx"#
18956 .unindent(),
18957 );
18958}
18959
18960#[gpui::test]
18961async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
18962 init_test(cx, |_| {});
18963
18964 let mut cx = EditorTestContext::new(cx).await;
18965 cx.set_state(
18966 r#"let foo = 1;
18967let foo = 2;
18968let foo = 3;
18969let fooˇ = 4;
18970let foo = 5;
18971let foo = 6;
18972let foo = 7;
18973let foo = 8;
18974let foo = 9;
18975let foo = 10;
18976let foo = 11;
18977let foo = 12;
18978let foo = 13;
18979let foo = 14;
18980let foo = 15;"#,
18981 );
18982
18983 cx.update_editor(|e, window, cx| {
18984 assert_eq!(
18985 e.next_scroll_position,
18986 NextScrollCursorCenterTopBottom::Center,
18987 "Default next scroll direction is center",
18988 );
18989
18990 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
18991 assert_eq!(
18992 e.next_scroll_position,
18993 NextScrollCursorCenterTopBottom::Top,
18994 "After center, next scroll direction should be top",
18995 );
18996
18997 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
18998 assert_eq!(
18999 e.next_scroll_position,
19000 NextScrollCursorCenterTopBottom::Bottom,
19001 "After top, next scroll direction should be bottom",
19002 );
19003
19004 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19005 assert_eq!(
19006 e.next_scroll_position,
19007 NextScrollCursorCenterTopBottom::Center,
19008 "After bottom, scrolling should start over",
19009 );
19010
19011 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19012 assert_eq!(
19013 e.next_scroll_position,
19014 NextScrollCursorCenterTopBottom::Top,
19015 "Scrolling continues if retriggered fast enough"
19016 );
19017 });
19018
19019 cx.executor()
19020 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
19021 cx.executor().run_until_parked();
19022 cx.update_editor(|e, _, _| {
19023 assert_eq!(
19024 e.next_scroll_position,
19025 NextScrollCursorCenterTopBottom::Center,
19026 "If scrolling is not triggered fast enough, it should reset"
19027 );
19028 });
19029}
19030
19031#[gpui::test]
19032async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
19033 init_test(cx, |_| {});
19034 let mut cx = EditorLspTestContext::new_rust(
19035 lsp::ServerCapabilities {
19036 definition_provider: Some(lsp::OneOf::Left(true)),
19037 references_provider: Some(lsp::OneOf::Left(true)),
19038 ..lsp::ServerCapabilities::default()
19039 },
19040 cx,
19041 )
19042 .await;
19043
19044 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
19045 let go_to_definition = cx
19046 .lsp
19047 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
19048 move |params, _| async move {
19049 if empty_go_to_definition {
19050 Ok(None)
19051 } else {
19052 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
19053 uri: params.text_document_position_params.text_document.uri,
19054 range: lsp::Range::new(
19055 lsp::Position::new(4, 3),
19056 lsp::Position::new(4, 6),
19057 ),
19058 })))
19059 }
19060 },
19061 );
19062 let references = cx
19063 .lsp
19064 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
19065 Ok(Some(vec![lsp::Location {
19066 uri: params.text_document_position.text_document.uri,
19067 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
19068 }]))
19069 });
19070 (go_to_definition, references)
19071 };
19072
19073 cx.set_state(
19074 &r#"fn one() {
19075 let mut a = ˇtwo();
19076 }
19077
19078 fn two() {}"#
19079 .unindent(),
19080 );
19081 set_up_lsp_handlers(false, &mut cx);
19082 let navigated = cx
19083 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
19084 .await
19085 .expect("Failed to navigate to definition");
19086 assert_eq!(
19087 navigated,
19088 Navigated::Yes,
19089 "Should have navigated to definition from the GetDefinition response"
19090 );
19091 cx.assert_editor_state(
19092 &r#"fn one() {
19093 let mut a = two();
19094 }
19095
19096 fn «twoˇ»() {}"#
19097 .unindent(),
19098 );
19099
19100 let editors = cx.update_workspace(|workspace, _, cx| {
19101 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
19102 });
19103 cx.update_editor(|_, _, test_editor_cx| {
19104 assert_eq!(
19105 editors.len(),
19106 1,
19107 "Initially, only one, test, editor should be open in the workspace"
19108 );
19109 assert_eq!(
19110 test_editor_cx.entity(),
19111 editors.last().expect("Asserted len is 1").clone()
19112 );
19113 });
19114
19115 set_up_lsp_handlers(true, &mut cx);
19116 let navigated = cx
19117 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
19118 .await
19119 .expect("Failed to navigate to lookup references");
19120 assert_eq!(
19121 navigated,
19122 Navigated::Yes,
19123 "Should have navigated to references as a fallback after empty GoToDefinition response"
19124 );
19125 // We should not change the selections in the existing file,
19126 // if opening another milti buffer with the references
19127 cx.assert_editor_state(
19128 &r#"fn one() {
19129 let mut a = two();
19130 }
19131
19132 fn «twoˇ»() {}"#
19133 .unindent(),
19134 );
19135 let editors = cx.update_workspace(|workspace, _, cx| {
19136 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
19137 });
19138 cx.update_editor(|_, _, test_editor_cx| {
19139 assert_eq!(
19140 editors.len(),
19141 2,
19142 "After falling back to references search, we open a new editor with the results"
19143 );
19144 let references_fallback_text = editors
19145 .into_iter()
19146 .find(|new_editor| *new_editor != test_editor_cx.entity())
19147 .expect("Should have one non-test editor now")
19148 .read(test_editor_cx)
19149 .text(test_editor_cx);
19150 assert_eq!(
19151 references_fallback_text, "fn one() {\n let mut a = two();\n}",
19152 "Should use the range from the references response and not the GoToDefinition one"
19153 );
19154 });
19155}
19156
19157#[gpui::test]
19158async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
19159 init_test(cx, |_| {});
19160 cx.update(|cx| {
19161 let mut editor_settings = EditorSettings::get_global(cx).clone();
19162 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
19163 EditorSettings::override_global(editor_settings, cx);
19164 });
19165 let mut cx = EditorLspTestContext::new_rust(
19166 lsp::ServerCapabilities {
19167 definition_provider: Some(lsp::OneOf::Left(true)),
19168 references_provider: Some(lsp::OneOf::Left(true)),
19169 ..lsp::ServerCapabilities::default()
19170 },
19171 cx,
19172 )
19173 .await;
19174 let original_state = r#"fn one() {
19175 let mut a = ˇtwo();
19176 }
19177
19178 fn two() {}"#
19179 .unindent();
19180 cx.set_state(&original_state);
19181
19182 let mut go_to_definition = cx
19183 .lsp
19184 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
19185 move |_, _| async move { Ok(None) },
19186 );
19187 let _references = cx
19188 .lsp
19189 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
19190 panic!("Should not call for references with no go to definition fallback")
19191 });
19192
19193 let navigated = cx
19194 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
19195 .await
19196 .expect("Failed to navigate to lookup references");
19197 go_to_definition
19198 .next()
19199 .await
19200 .expect("Should have called the go_to_definition handler");
19201
19202 assert_eq!(
19203 navigated,
19204 Navigated::No,
19205 "Should have navigated to references as a fallback after empty GoToDefinition response"
19206 );
19207 cx.assert_editor_state(&original_state);
19208 let editors = cx.update_workspace(|workspace, _, cx| {
19209 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
19210 });
19211 cx.update_editor(|_, _, _| {
19212 assert_eq!(
19213 editors.len(),
19214 1,
19215 "After unsuccessful fallback, no other editor should have been opened"
19216 );
19217 });
19218}
19219
19220#[gpui::test]
19221async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
19222 init_test(cx, |_| {});
19223
19224 let language = Arc::new(Language::new(
19225 LanguageConfig::default(),
19226 Some(tree_sitter_rust::LANGUAGE.into()),
19227 ));
19228
19229 let text = r#"
19230 #[cfg(test)]
19231 mod tests() {
19232 #[test]
19233 fn runnable_1() {
19234 let a = 1;
19235 }
19236
19237 #[test]
19238 fn runnable_2() {
19239 let a = 1;
19240 let b = 2;
19241 }
19242 }
19243 "#
19244 .unindent();
19245
19246 let fs = FakeFs::new(cx.executor());
19247 fs.insert_file("/file.rs", Default::default()).await;
19248
19249 let project = Project::test(fs, ["/a".as_ref()], cx).await;
19250 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19251 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19252 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
19253 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
19254
19255 let editor = cx.new_window_entity(|window, cx| {
19256 Editor::new(
19257 EditorMode::full(),
19258 multi_buffer,
19259 Some(project.clone()),
19260 window,
19261 cx,
19262 )
19263 });
19264
19265 editor.update_in(cx, |editor, window, cx| {
19266 let snapshot = editor.buffer().read(cx).snapshot(cx);
19267 editor.tasks.insert(
19268 (buffer.read(cx).remote_id(), 3),
19269 RunnableTasks {
19270 templates: vec![],
19271 offset: snapshot.anchor_before(43),
19272 column: 0,
19273 extra_variables: HashMap::default(),
19274 context_range: BufferOffset(43)..BufferOffset(85),
19275 },
19276 );
19277 editor.tasks.insert(
19278 (buffer.read(cx).remote_id(), 8),
19279 RunnableTasks {
19280 templates: vec![],
19281 offset: snapshot.anchor_before(86),
19282 column: 0,
19283 extra_variables: HashMap::default(),
19284 context_range: BufferOffset(86)..BufferOffset(191),
19285 },
19286 );
19287
19288 // Test finding task when cursor is inside function body
19289 editor.change_selections(None, window, cx, |s| {
19290 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
19291 });
19292 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
19293 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
19294
19295 // Test finding task when cursor is on function name
19296 editor.change_selections(None, window, cx, |s| {
19297 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
19298 });
19299 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
19300 assert_eq!(row, 8, "Should find task when cursor is on function name");
19301 });
19302}
19303
19304#[gpui::test]
19305async fn test_folding_buffers(cx: &mut TestAppContext) {
19306 init_test(cx, |_| {});
19307
19308 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
19309 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
19310 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
19311
19312 let fs = FakeFs::new(cx.executor());
19313 fs.insert_tree(
19314 path!("/a"),
19315 json!({
19316 "first.rs": sample_text_1,
19317 "second.rs": sample_text_2,
19318 "third.rs": sample_text_3,
19319 }),
19320 )
19321 .await;
19322 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19323 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19324 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19325 let worktree = project.update(cx, |project, cx| {
19326 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
19327 assert_eq!(worktrees.len(), 1);
19328 worktrees.pop().unwrap()
19329 });
19330 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
19331
19332 let buffer_1 = project
19333 .update(cx, |project, cx| {
19334 project.open_buffer((worktree_id, "first.rs"), cx)
19335 })
19336 .await
19337 .unwrap();
19338 let buffer_2 = project
19339 .update(cx, |project, cx| {
19340 project.open_buffer((worktree_id, "second.rs"), cx)
19341 })
19342 .await
19343 .unwrap();
19344 let buffer_3 = project
19345 .update(cx, |project, cx| {
19346 project.open_buffer((worktree_id, "third.rs"), cx)
19347 })
19348 .await
19349 .unwrap();
19350
19351 let multi_buffer = cx.new(|cx| {
19352 let mut multi_buffer = MultiBuffer::new(ReadWrite);
19353 multi_buffer.push_excerpts(
19354 buffer_1.clone(),
19355 [
19356 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19357 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19358 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19359 ],
19360 cx,
19361 );
19362 multi_buffer.push_excerpts(
19363 buffer_2.clone(),
19364 [
19365 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19366 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19367 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19368 ],
19369 cx,
19370 );
19371 multi_buffer.push_excerpts(
19372 buffer_3.clone(),
19373 [
19374 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19375 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19376 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19377 ],
19378 cx,
19379 );
19380 multi_buffer
19381 });
19382 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19383 Editor::new(
19384 EditorMode::full(),
19385 multi_buffer.clone(),
19386 Some(project.clone()),
19387 window,
19388 cx,
19389 )
19390 });
19391
19392 assert_eq!(
19393 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19394 "\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",
19395 );
19396
19397 multi_buffer_editor.update(cx, |editor, cx| {
19398 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
19399 });
19400 assert_eq!(
19401 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19402 "\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",
19403 "After folding the first buffer, its text should not be displayed"
19404 );
19405
19406 multi_buffer_editor.update(cx, |editor, cx| {
19407 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
19408 });
19409 assert_eq!(
19410 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19411 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
19412 "After folding the second buffer, its text should not be displayed"
19413 );
19414
19415 multi_buffer_editor.update(cx, |editor, cx| {
19416 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
19417 });
19418 assert_eq!(
19419 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19420 "\n\n\n\n\n",
19421 "After folding the third buffer, its text should not be displayed"
19422 );
19423
19424 // Emulate selection inside the fold logic, that should work
19425 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19426 editor
19427 .snapshot(window, cx)
19428 .next_line_boundary(Point::new(0, 4));
19429 });
19430
19431 multi_buffer_editor.update(cx, |editor, cx| {
19432 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
19433 });
19434 assert_eq!(
19435 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19436 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
19437 "After unfolding the second buffer, its text should be displayed"
19438 );
19439
19440 // Typing inside of buffer 1 causes that buffer to be unfolded.
19441 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19442 assert_eq!(
19443 multi_buffer
19444 .read(cx)
19445 .snapshot(cx)
19446 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
19447 .collect::<String>(),
19448 "bbbb"
19449 );
19450 editor.change_selections(None, window, cx, |selections| {
19451 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
19452 });
19453 editor.handle_input("B", window, cx);
19454 });
19455
19456 assert_eq!(
19457 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19458 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
19459 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
19460 );
19461
19462 multi_buffer_editor.update(cx, |editor, cx| {
19463 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
19464 });
19465 assert_eq!(
19466 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19467 "\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",
19468 "After unfolding the all buffers, all original text should be displayed"
19469 );
19470}
19471
19472#[gpui::test]
19473async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
19474 init_test(cx, |_| {});
19475
19476 let sample_text_1 = "1111\n2222\n3333".to_string();
19477 let sample_text_2 = "4444\n5555\n6666".to_string();
19478 let sample_text_3 = "7777\n8888\n9999".to_string();
19479
19480 let fs = FakeFs::new(cx.executor());
19481 fs.insert_tree(
19482 path!("/a"),
19483 json!({
19484 "first.rs": sample_text_1,
19485 "second.rs": sample_text_2,
19486 "third.rs": sample_text_3,
19487 }),
19488 )
19489 .await;
19490 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19491 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19492 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19493 let worktree = project.update(cx, |project, cx| {
19494 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
19495 assert_eq!(worktrees.len(), 1);
19496 worktrees.pop().unwrap()
19497 });
19498 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
19499
19500 let buffer_1 = project
19501 .update(cx, |project, cx| {
19502 project.open_buffer((worktree_id, "first.rs"), cx)
19503 })
19504 .await
19505 .unwrap();
19506 let buffer_2 = project
19507 .update(cx, |project, cx| {
19508 project.open_buffer((worktree_id, "second.rs"), cx)
19509 })
19510 .await
19511 .unwrap();
19512 let buffer_3 = project
19513 .update(cx, |project, cx| {
19514 project.open_buffer((worktree_id, "third.rs"), cx)
19515 })
19516 .await
19517 .unwrap();
19518
19519 let multi_buffer = cx.new(|cx| {
19520 let mut multi_buffer = MultiBuffer::new(ReadWrite);
19521 multi_buffer.push_excerpts(
19522 buffer_1.clone(),
19523 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
19524 cx,
19525 );
19526 multi_buffer.push_excerpts(
19527 buffer_2.clone(),
19528 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
19529 cx,
19530 );
19531 multi_buffer.push_excerpts(
19532 buffer_3.clone(),
19533 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
19534 cx,
19535 );
19536 multi_buffer
19537 });
19538
19539 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19540 Editor::new(
19541 EditorMode::full(),
19542 multi_buffer,
19543 Some(project.clone()),
19544 window,
19545 cx,
19546 )
19547 });
19548
19549 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
19550 assert_eq!(
19551 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19552 full_text,
19553 );
19554
19555 multi_buffer_editor.update(cx, |editor, cx| {
19556 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
19557 });
19558 assert_eq!(
19559 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19560 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
19561 "After folding the first buffer, its text should not be displayed"
19562 );
19563
19564 multi_buffer_editor.update(cx, |editor, cx| {
19565 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
19566 });
19567
19568 assert_eq!(
19569 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19570 "\n\n\n\n\n\n7777\n8888\n9999",
19571 "After folding the second buffer, its text should not be displayed"
19572 );
19573
19574 multi_buffer_editor.update(cx, |editor, cx| {
19575 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
19576 });
19577 assert_eq!(
19578 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19579 "\n\n\n\n\n",
19580 "After folding the third buffer, its text should not be displayed"
19581 );
19582
19583 multi_buffer_editor.update(cx, |editor, cx| {
19584 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
19585 });
19586 assert_eq!(
19587 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19588 "\n\n\n\n4444\n5555\n6666\n\n",
19589 "After unfolding the second buffer, its text should be displayed"
19590 );
19591
19592 multi_buffer_editor.update(cx, |editor, cx| {
19593 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
19594 });
19595 assert_eq!(
19596 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19597 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
19598 "After unfolding the first buffer, its text should be displayed"
19599 );
19600
19601 multi_buffer_editor.update(cx, |editor, cx| {
19602 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
19603 });
19604 assert_eq!(
19605 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19606 full_text,
19607 "After unfolding all buffers, all original text should be displayed"
19608 );
19609}
19610
19611#[gpui::test]
19612async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
19613 init_test(cx, |_| {});
19614
19615 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
19616
19617 let fs = FakeFs::new(cx.executor());
19618 fs.insert_tree(
19619 path!("/a"),
19620 json!({
19621 "main.rs": sample_text,
19622 }),
19623 )
19624 .await;
19625 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19626 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19627 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19628 let worktree = project.update(cx, |project, cx| {
19629 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
19630 assert_eq!(worktrees.len(), 1);
19631 worktrees.pop().unwrap()
19632 });
19633 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
19634
19635 let buffer_1 = project
19636 .update(cx, |project, cx| {
19637 project.open_buffer((worktree_id, "main.rs"), cx)
19638 })
19639 .await
19640 .unwrap();
19641
19642 let multi_buffer = cx.new(|cx| {
19643 let mut multi_buffer = MultiBuffer::new(ReadWrite);
19644 multi_buffer.push_excerpts(
19645 buffer_1.clone(),
19646 [ExcerptRange::new(
19647 Point::new(0, 0)
19648 ..Point::new(
19649 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
19650 0,
19651 ),
19652 )],
19653 cx,
19654 );
19655 multi_buffer
19656 });
19657 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19658 Editor::new(
19659 EditorMode::full(),
19660 multi_buffer,
19661 Some(project.clone()),
19662 window,
19663 cx,
19664 )
19665 });
19666
19667 let selection_range = Point::new(1, 0)..Point::new(2, 0);
19668 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19669 enum TestHighlight {}
19670 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
19671 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
19672 editor.highlight_text::<TestHighlight>(
19673 vec![highlight_range.clone()],
19674 HighlightStyle::color(Hsla::green()),
19675 cx,
19676 );
19677 editor.change_selections(None, window, cx, |s| s.select_ranges(Some(highlight_range)));
19678 });
19679
19680 let full_text = format!("\n\n{sample_text}");
19681 assert_eq!(
19682 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19683 full_text,
19684 );
19685}
19686
19687#[gpui::test]
19688async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
19689 init_test(cx, |_| {});
19690 cx.update(|cx| {
19691 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
19692 "keymaps/default-linux.json",
19693 cx,
19694 )
19695 .unwrap();
19696 cx.bind_keys(default_key_bindings);
19697 });
19698
19699 let (editor, cx) = cx.add_window_view(|window, cx| {
19700 let multi_buffer = MultiBuffer::build_multi(
19701 [
19702 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
19703 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
19704 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
19705 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
19706 ],
19707 cx,
19708 );
19709 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
19710
19711 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
19712 // fold all but the second buffer, so that we test navigating between two
19713 // adjacent folded buffers, as well as folded buffers at the start and
19714 // end the multibuffer
19715 editor.fold_buffer(buffer_ids[0], cx);
19716 editor.fold_buffer(buffer_ids[2], cx);
19717 editor.fold_buffer(buffer_ids[3], cx);
19718
19719 editor
19720 });
19721 cx.simulate_resize(size(px(1000.), px(1000.)));
19722
19723 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
19724 cx.assert_excerpts_with_selections(indoc! {"
19725 [EXCERPT]
19726 ˇ[FOLDED]
19727 [EXCERPT]
19728 a1
19729 b1
19730 [EXCERPT]
19731 [FOLDED]
19732 [EXCERPT]
19733 [FOLDED]
19734 "
19735 });
19736 cx.simulate_keystroke("down");
19737 cx.assert_excerpts_with_selections(indoc! {"
19738 [EXCERPT]
19739 [FOLDED]
19740 [EXCERPT]
19741 ˇa1
19742 b1
19743 [EXCERPT]
19744 [FOLDED]
19745 [EXCERPT]
19746 [FOLDED]
19747 "
19748 });
19749 cx.simulate_keystroke("down");
19750 cx.assert_excerpts_with_selections(indoc! {"
19751 [EXCERPT]
19752 [FOLDED]
19753 [EXCERPT]
19754 a1
19755 ˇb1
19756 [EXCERPT]
19757 [FOLDED]
19758 [EXCERPT]
19759 [FOLDED]
19760 "
19761 });
19762 cx.simulate_keystroke("down");
19763 cx.assert_excerpts_with_selections(indoc! {"
19764 [EXCERPT]
19765 [FOLDED]
19766 [EXCERPT]
19767 a1
19768 b1
19769 ˇ[EXCERPT]
19770 [FOLDED]
19771 [EXCERPT]
19772 [FOLDED]
19773 "
19774 });
19775 cx.simulate_keystroke("down");
19776 cx.assert_excerpts_with_selections(indoc! {"
19777 [EXCERPT]
19778 [FOLDED]
19779 [EXCERPT]
19780 a1
19781 b1
19782 [EXCERPT]
19783 ˇ[FOLDED]
19784 [EXCERPT]
19785 [FOLDED]
19786 "
19787 });
19788 for _ in 0..5 {
19789 cx.simulate_keystroke("down");
19790 cx.assert_excerpts_with_selections(indoc! {"
19791 [EXCERPT]
19792 [FOLDED]
19793 [EXCERPT]
19794 a1
19795 b1
19796 [EXCERPT]
19797 [FOLDED]
19798 [EXCERPT]
19799 ˇ[FOLDED]
19800 "
19801 });
19802 }
19803
19804 cx.simulate_keystroke("up");
19805 cx.assert_excerpts_with_selections(indoc! {"
19806 [EXCERPT]
19807 [FOLDED]
19808 [EXCERPT]
19809 a1
19810 b1
19811 [EXCERPT]
19812 ˇ[FOLDED]
19813 [EXCERPT]
19814 [FOLDED]
19815 "
19816 });
19817 cx.simulate_keystroke("up");
19818 cx.assert_excerpts_with_selections(indoc! {"
19819 [EXCERPT]
19820 [FOLDED]
19821 [EXCERPT]
19822 a1
19823 b1
19824 ˇ[EXCERPT]
19825 [FOLDED]
19826 [EXCERPT]
19827 [FOLDED]
19828 "
19829 });
19830 cx.simulate_keystroke("up");
19831 cx.assert_excerpts_with_selections(indoc! {"
19832 [EXCERPT]
19833 [FOLDED]
19834 [EXCERPT]
19835 a1
19836 ˇb1
19837 [EXCERPT]
19838 [FOLDED]
19839 [EXCERPT]
19840 [FOLDED]
19841 "
19842 });
19843 cx.simulate_keystroke("up");
19844 cx.assert_excerpts_with_selections(indoc! {"
19845 [EXCERPT]
19846 [FOLDED]
19847 [EXCERPT]
19848 ˇa1
19849 b1
19850 [EXCERPT]
19851 [FOLDED]
19852 [EXCERPT]
19853 [FOLDED]
19854 "
19855 });
19856 for _ in 0..5 {
19857 cx.simulate_keystroke("up");
19858 cx.assert_excerpts_with_selections(indoc! {"
19859 [EXCERPT]
19860 ˇ[FOLDED]
19861 [EXCERPT]
19862 a1
19863 b1
19864 [EXCERPT]
19865 [FOLDED]
19866 [EXCERPT]
19867 [FOLDED]
19868 "
19869 });
19870 }
19871}
19872
19873#[gpui::test]
19874async fn test_inline_completion_text(cx: &mut TestAppContext) {
19875 init_test(cx, |_| {});
19876
19877 // Simple insertion
19878 assert_highlighted_edits(
19879 "Hello, world!",
19880 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
19881 true,
19882 cx,
19883 |highlighted_edits, cx| {
19884 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
19885 assert_eq!(highlighted_edits.highlights.len(), 1);
19886 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
19887 assert_eq!(
19888 highlighted_edits.highlights[0].1.background_color,
19889 Some(cx.theme().status().created_background)
19890 );
19891 },
19892 )
19893 .await;
19894
19895 // Replacement
19896 assert_highlighted_edits(
19897 "This is a test.",
19898 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
19899 false,
19900 cx,
19901 |highlighted_edits, cx| {
19902 assert_eq!(highlighted_edits.text, "That is a test.");
19903 assert_eq!(highlighted_edits.highlights.len(), 1);
19904 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
19905 assert_eq!(
19906 highlighted_edits.highlights[0].1.background_color,
19907 Some(cx.theme().status().created_background)
19908 );
19909 },
19910 )
19911 .await;
19912
19913 // Multiple edits
19914 assert_highlighted_edits(
19915 "Hello, world!",
19916 vec![
19917 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
19918 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
19919 ],
19920 false,
19921 cx,
19922 |highlighted_edits, cx| {
19923 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
19924 assert_eq!(highlighted_edits.highlights.len(), 2);
19925 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
19926 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
19927 assert_eq!(
19928 highlighted_edits.highlights[0].1.background_color,
19929 Some(cx.theme().status().created_background)
19930 );
19931 assert_eq!(
19932 highlighted_edits.highlights[1].1.background_color,
19933 Some(cx.theme().status().created_background)
19934 );
19935 },
19936 )
19937 .await;
19938
19939 // Multiple lines with edits
19940 assert_highlighted_edits(
19941 "First line\nSecond line\nThird line\nFourth line",
19942 vec![
19943 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
19944 (
19945 Point::new(2, 0)..Point::new(2, 10),
19946 "New third line".to_string(),
19947 ),
19948 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
19949 ],
19950 false,
19951 cx,
19952 |highlighted_edits, cx| {
19953 assert_eq!(
19954 highlighted_edits.text,
19955 "Second modified\nNew third line\nFourth updated line"
19956 );
19957 assert_eq!(highlighted_edits.highlights.len(), 3);
19958 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
19959 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
19960 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
19961 for highlight in &highlighted_edits.highlights {
19962 assert_eq!(
19963 highlight.1.background_color,
19964 Some(cx.theme().status().created_background)
19965 );
19966 }
19967 },
19968 )
19969 .await;
19970}
19971
19972#[gpui::test]
19973async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
19974 init_test(cx, |_| {});
19975
19976 // Deletion
19977 assert_highlighted_edits(
19978 "Hello, world!",
19979 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
19980 true,
19981 cx,
19982 |highlighted_edits, cx| {
19983 assert_eq!(highlighted_edits.text, "Hello, world!");
19984 assert_eq!(highlighted_edits.highlights.len(), 1);
19985 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
19986 assert_eq!(
19987 highlighted_edits.highlights[0].1.background_color,
19988 Some(cx.theme().status().deleted_background)
19989 );
19990 },
19991 )
19992 .await;
19993
19994 // Insertion
19995 assert_highlighted_edits(
19996 "Hello, world!",
19997 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
19998 true,
19999 cx,
20000 |highlighted_edits, cx| {
20001 assert_eq!(highlighted_edits.highlights.len(), 1);
20002 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
20003 assert_eq!(
20004 highlighted_edits.highlights[0].1.background_color,
20005 Some(cx.theme().status().created_background)
20006 );
20007 },
20008 )
20009 .await;
20010}
20011
20012async fn assert_highlighted_edits(
20013 text: &str,
20014 edits: Vec<(Range<Point>, String)>,
20015 include_deletions: bool,
20016 cx: &mut TestAppContext,
20017 assertion_fn: impl Fn(HighlightedText, &App),
20018) {
20019 let window = cx.add_window(|window, cx| {
20020 let buffer = MultiBuffer::build_simple(text, cx);
20021 Editor::new(EditorMode::full(), buffer, None, window, cx)
20022 });
20023 let cx = &mut VisualTestContext::from_window(*window, cx);
20024
20025 let (buffer, snapshot) = window
20026 .update(cx, |editor, _window, cx| {
20027 (
20028 editor.buffer().clone(),
20029 editor.buffer().read(cx).snapshot(cx),
20030 )
20031 })
20032 .unwrap();
20033
20034 let edits = edits
20035 .into_iter()
20036 .map(|(range, edit)| {
20037 (
20038 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
20039 edit,
20040 )
20041 })
20042 .collect::<Vec<_>>();
20043
20044 let text_anchor_edits = edits
20045 .clone()
20046 .into_iter()
20047 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
20048 .collect::<Vec<_>>();
20049
20050 let edit_preview = window
20051 .update(cx, |_, _window, cx| {
20052 buffer
20053 .read(cx)
20054 .as_singleton()
20055 .unwrap()
20056 .read(cx)
20057 .preview_edits(text_anchor_edits.into(), cx)
20058 })
20059 .unwrap()
20060 .await;
20061
20062 cx.update(|_window, cx| {
20063 let highlighted_edits = inline_completion_edit_text(
20064 &snapshot.as_singleton().unwrap().2,
20065 &edits,
20066 &edit_preview,
20067 include_deletions,
20068 cx,
20069 );
20070 assertion_fn(highlighted_edits, cx)
20071 });
20072}
20073
20074#[track_caller]
20075fn assert_breakpoint(
20076 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
20077 path: &Arc<Path>,
20078 expected: Vec<(u32, Breakpoint)>,
20079) {
20080 if expected.len() == 0usize {
20081 assert!(!breakpoints.contains_key(path), "{}", path.display());
20082 } else {
20083 let mut breakpoint = breakpoints
20084 .get(path)
20085 .unwrap()
20086 .into_iter()
20087 .map(|breakpoint| {
20088 (
20089 breakpoint.row,
20090 Breakpoint {
20091 message: breakpoint.message.clone(),
20092 state: breakpoint.state,
20093 condition: breakpoint.condition.clone(),
20094 hit_condition: breakpoint.hit_condition.clone(),
20095 },
20096 )
20097 })
20098 .collect::<Vec<_>>();
20099
20100 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
20101
20102 assert_eq!(expected, breakpoint);
20103 }
20104}
20105
20106fn add_log_breakpoint_at_cursor(
20107 editor: &mut Editor,
20108 log_message: &str,
20109 window: &mut Window,
20110 cx: &mut Context<Editor>,
20111) {
20112 let (anchor, bp) = editor
20113 .breakpoints_at_cursors(window, cx)
20114 .first()
20115 .and_then(|(anchor, bp)| {
20116 if let Some(bp) = bp {
20117 Some((*anchor, bp.clone()))
20118 } else {
20119 None
20120 }
20121 })
20122 .unwrap_or_else(|| {
20123 let cursor_position: Point = editor.selections.newest(cx).head();
20124
20125 let breakpoint_position = editor
20126 .snapshot(window, cx)
20127 .display_snapshot
20128 .buffer_snapshot
20129 .anchor_before(Point::new(cursor_position.row, 0));
20130
20131 (breakpoint_position, Breakpoint::new_log(&log_message))
20132 });
20133
20134 editor.edit_breakpoint_at_anchor(
20135 anchor,
20136 bp,
20137 BreakpointEditAction::EditLogMessage(log_message.into()),
20138 cx,
20139 );
20140}
20141
20142#[gpui::test]
20143async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
20144 init_test(cx, |_| {});
20145
20146 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
20147 let fs = FakeFs::new(cx.executor());
20148 fs.insert_tree(
20149 path!("/a"),
20150 json!({
20151 "main.rs": sample_text,
20152 }),
20153 )
20154 .await;
20155 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20156 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20157 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20158
20159 let fs = FakeFs::new(cx.executor());
20160 fs.insert_tree(
20161 path!("/a"),
20162 json!({
20163 "main.rs": sample_text,
20164 }),
20165 )
20166 .await;
20167 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20168 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20169 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20170 let worktree_id = workspace
20171 .update(cx, |workspace, _window, cx| {
20172 workspace.project().update(cx, |project, cx| {
20173 project.worktrees(cx).next().unwrap().read(cx).id()
20174 })
20175 })
20176 .unwrap();
20177
20178 let buffer = project
20179 .update(cx, |project, cx| {
20180 project.open_buffer((worktree_id, "main.rs"), cx)
20181 })
20182 .await
20183 .unwrap();
20184
20185 let (editor, cx) = cx.add_window_view(|window, cx| {
20186 Editor::new(
20187 EditorMode::full(),
20188 MultiBuffer::build_from_buffer(buffer, cx),
20189 Some(project.clone()),
20190 window,
20191 cx,
20192 )
20193 });
20194
20195 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
20196 let abs_path = project.read_with(cx, |project, cx| {
20197 project
20198 .absolute_path(&project_path, cx)
20199 .map(|path_buf| Arc::from(path_buf.to_owned()))
20200 .unwrap()
20201 });
20202
20203 // assert we can add breakpoint on the first line
20204 editor.update_in(cx, |editor, window, cx| {
20205 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20206 editor.move_to_end(&MoveToEnd, window, cx);
20207 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20208 });
20209
20210 let breakpoints = editor.update(cx, |editor, cx| {
20211 editor
20212 .breakpoint_store()
20213 .as_ref()
20214 .unwrap()
20215 .read(cx)
20216 .all_source_breakpoints(cx)
20217 .clone()
20218 });
20219
20220 assert_eq!(1, breakpoints.len());
20221 assert_breakpoint(
20222 &breakpoints,
20223 &abs_path,
20224 vec![
20225 (0, Breakpoint::new_standard()),
20226 (3, Breakpoint::new_standard()),
20227 ],
20228 );
20229
20230 editor.update_in(cx, |editor, window, cx| {
20231 editor.move_to_beginning(&MoveToBeginning, window, cx);
20232 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20233 });
20234
20235 let breakpoints = editor.update(cx, |editor, cx| {
20236 editor
20237 .breakpoint_store()
20238 .as_ref()
20239 .unwrap()
20240 .read(cx)
20241 .all_source_breakpoints(cx)
20242 .clone()
20243 });
20244
20245 assert_eq!(1, breakpoints.len());
20246 assert_breakpoint(
20247 &breakpoints,
20248 &abs_path,
20249 vec![(3, Breakpoint::new_standard())],
20250 );
20251
20252 editor.update_in(cx, |editor, window, cx| {
20253 editor.move_to_end(&MoveToEnd, window, cx);
20254 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20255 });
20256
20257 let breakpoints = editor.update(cx, |editor, cx| {
20258 editor
20259 .breakpoint_store()
20260 .as_ref()
20261 .unwrap()
20262 .read(cx)
20263 .all_source_breakpoints(cx)
20264 .clone()
20265 });
20266
20267 assert_eq!(0, breakpoints.len());
20268 assert_breakpoint(&breakpoints, &abs_path, vec![]);
20269}
20270
20271#[gpui::test]
20272async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
20273 init_test(cx, |_| {});
20274
20275 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
20276
20277 let fs = FakeFs::new(cx.executor());
20278 fs.insert_tree(
20279 path!("/a"),
20280 json!({
20281 "main.rs": sample_text,
20282 }),
20283 )
20284 .await;
20285 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20286 let (workspace, cx) =
20287 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20288
20289 let worktree_id = workspace.update(cx, |workspace, cx| {
20290 workspace.project().update(cx, |project, cx| {
20291 project.worktrees(cx).next().unwrap().read(cx).id()
20292 })
20293 });
20294
20295 let buffer = project
20296 .update(cx, |project, cx| {
20297 project.open_buffer((worktree_id, "main.rs"), cx)
20298 })
20299 .await
20300 .unwrap();
20301
20302 let (editor, cx) = cx.add_window_view(|window, cx| {
20303 Editor::new(
20304 EditorMode::full(),
20305 MultiBuffer::build_from_buffer(buffer, cx),
20306 Some(project.clone()),
20307 window,
20308 cx,
20309 )
20310 });
20311
20312 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
20313 let abs_path = project.read_with(cx, |project, cx| {
20314 project
20315 .absolute_path(&project_path, cx)
20316 .map(|path_buf| Arc::from(path_buf.to_owned()))
20317 .unwrap()
20318 });
20319
20320 editor.update_in(cx, |editor, window, cx| {
20321 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
20322 });
20323
20324 let breakpoints = editor.update(cx, |editor, cx| {
20325 editor
20326 .breakpoint_store()
20327 .as_ref()
20328 .unwrap()
20329 .read(cx)
20330 .all_source_breakpoints(cx)
20331 .clone()
20332 });
20333
20334 assert_breakpoint(
20335 &breakpoints,
20336 &abs_path,
20337 vec![(0, Breakpoint::new_log("hello world"))],
20338 );
20339
20340 // Removing a log message from a log breakpoint should remove it
20341 editor.update_in(cx, |editor, window, cx| {
20342 add_log_breakpoint_at_cursor(editor, "", window, cx);
20343 });
20344
20345 let breakpoints = editor.update(cx, |editor, cx| {
20346 editor
20347 .breakpoint_store()
20348 .as_ref()
20349 .unwrap()
20350 .read(cx)
20351 .all_source_breakpoints(cx)
20352 .clone()
20353 });
20354
20355 assert_breakpoint(&breakpoints, &abs_path, vec![]);
20356
20357 editor.update_in(cx, |editor, window, cx| {
20358 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20359 editor.move_to_end(&MoveToEnd, window, cx);
20360 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20361 // Not adding a log message to a standard breakpoint shouldn't remove it
20362 add_log_breakpoint_at_cursor(editor, "", window, cx);
20363 });
20364
20365 let breakpoints = editor.update(cx, |editor, cx| {
20366 editor
20367 .breakpoint_store()
20368 .as_ref()
20369 .unwrap()
20370 .read(cx)
20371 .all_source_breakpoints(cx)
20372 .clone()
20373 });
20374
20375 assert_breakpoint(
20376 &breakpoints,
20377 &abs_path,
20378 vec![
20379 (0, Breakpoint::new_standard()),
20380 (3, Breakpoint::new_standard()),
20381 ],
20382 );
20383
20384 editor.update_in(cx, |editor, window, cx| {
20385 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
20386 });
20387
20388 let breakpoints = editor.update(cx, |editor, cx| {
20389 editor
20390 .breakpoint_store()
20391 .as_ref()
20392 .unwrap()
20393 .read(cx)
20394 .all_source_breakpoints(cx)
20395 .clone()
20396 });
20397
20398 assert_breakpoint(
20399 &breakpoints,
20400 &abs_path,
20401 vec![
20402 (0, Breakpoint::new_standard()),
20403 (3, Breakpoint::new_log("hello world")),
20404 ],
20405 );
20406
20407 editor.update_in(cx, |editor, window, cx| {
20408 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
20409 });
20410
20411 let breakpoints = editor.update(cx, |editor, cx| {
20412 editor
20413 .breakpoint_store()
20414 .as_ref()
20415 .unwrap()
20416 .read(cx)
20417 .all_source_breakpoints(cx)
20418 .clone()
20419 });
20420
20421 assert_breakpoint(
20422 &breakpoints,
20423 &abs_path,
20424 vec![
20425 (0, Breakpoint::new_standard()),
20426 (3, Breakpoint::new_log("hello Earth!!")),
20427 ],
20428 );
20429}
20430
20431/// This also tests that Editor::breakpoint_at_cursor_head is working properly
20432/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
20433/// or when breakpoints were placed out of order. This tests for a regression too
20434#[gpui::test]
20435async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
20436 init_test(cx, |_| {});
20437
20438 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
20439 let fs = FakeFs::new(cx.executor());
20440 fs.insert_tree(
20441 path!("/a"),
20442 json!({
20443 "main.rs": sample_text,
20444 }),
20445 )
20446 .await;
20447 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20448 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20449 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20450
20451 let fs = FakeFs::new(cx.executor());
20452 fs.insert_tree(
20453 path!("/a"),
20454 json!({
20455 "main.rs": sample_text,
20456 }),
20457 )
20458 .await;
20459 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20460 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20461 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20462 let worktree_id = workspace
20463 .update(cx, |workspace, _window, cx| {
20464 workspace.project().update(cx, |project, cx| {
20465 project.worktrees(cx).next().unwrap().read(cx).id()
20466 })
20467 })
20468 .unwrap();
20469
20470 let buffer = project
20471 .update(cx, |project, cx| {
20472 project.open_buffer((worktree_id, "main.rs"), cx)
20473 })
20474 .await
20475 .unwrap();
20476
20477 let (editor, cx) = cx.add_window_view(|window, cx| {
20478 Editor::new(
20479 EditorMode::full(),
20480 MultiBuffer::build_from_buffer(buffer, cx),
20481 Some(project.clone()),
20482 window,
20483 cx,
20484 )
20485 });
20486
20487 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
20488 let abs_path = project.read_with(cx, |project, cx| {
20489 project
20490 .absolute_path(&project_path, cx)
20491 .map(|path_buf| Arc::from(path_buf.to_owned()))
20492 .unwrap()
20493 });
20494
20495 // assert we can add breakpoint on the first line
20496 editor.update_in(cx, |editor, window, cx| {
20497 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20498 editor.move_to_end(&MoveToEnd, window, cx);
20499 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20500 editor.move_up(&MoveUp, window, cx);
20501 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20502 });
20503
20504 let breakpoints = editor.update(cx, |editor, cx| {
20505 editor
20506 .breakpoint_store()
20507 .as_ref()
20508 .unwrap()
20509 .read(cx)
20510 .all_source_breakpoints(cx)
20511 .clone()
20512 });
20513
20514 assert_eq!(1, breakpoints.len());
20515 assert_breakpoint(
20516 &breakpoints,
20517 &abs_path,
20518 vec![
20519 (0, Breakpoint::new_standard()),
20520 (2, Breakpoint::new_standard()),
20521 (3, Breakpoint::new_standard()),
20522 ],
20523 );
20524
20525 editor.update_in(cx, |editor, window, cx| {
20526 editor.move_to_beginning(&MoveToBeginning, window, cx);
20527 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20528 editor.move_to_end(&MoveToEnd, window, cx);
20529 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20530 // Disabling a breakpoint that doesn't exist should do nothing
20531 editor.move_up(&MoveUp, window, cx);
20532 editor.move_up(&MoveUp, window, cx);
20533 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20534 });
20535
20536 let breakpoints = editor.update(cx, |editor, cx| {
20537 editor
20538 .breakpoint_store()
20539 .as_ref()
20540 .unwrap()
20541 .read(cx)
20542 .all_source_breakpoints(cx)
20543 .clone()
20544 });
20545
20546 let disable_breakpoint = {
20547 let mut bp = Breakpoint::new_standard();
20548 bp.state = BreakpointState::Disabled;
20549 bp
20550 };
20551
20552 assert_eq!(1, breakpoints.len());
20553 assert_breakpoint(
20554 &breakpoints,
20555 &abs_path,
20556 vec![
20557 (0, disable_breakpoint.clone()),
20558 (2, Breakpoint::new_standard()),
20559 (3, disable_breakpoint.clone()),
20560 ],
20561 );
20562
20563 editor.update_in(cx, |editor, window, cx| {
20564 editor.move_to_beginning(&MoveToBeginning, window, cx);
20565 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
20566 editor.move_to_end(&MoveToEnd, window, cx);
20567 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
20568 editor.move_up(&MoveUp, window, cx);
20569 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20570 });
20571
20572 let breakpoints = editor.update(cx, |editor, cx| {
20573 editor
20574 .breakpoint_store()
20575 .as_ref()
20576 .unwrap()
20577 .read(cx)
20578 .all_source_breakpoints(cx)
20579 .clone()
20580 });
20581
20582 assert_eq!(1, breakpoints.len());
20583 assert_breakpoint(
20584 &breakpoints,
20585 &abs_path,
20586 vec![
20587 (0, Breakpoint::new_standard()),
20588 (2, disable_breakpoint),
20589 (3, Breakpoint::new_standard()),
20590 ],
20591 );
20592}
20593
20594#[gpui::test]
20595async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
20596 init_test(cx, |_| {});
20597 let capabilities = lsp::ServerCapabilities {
20598 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
20599 prepare_provider: Some(true),
20600 work_done_progress_options: Default::default(),
20601 })),
20602 ..Default::default()
20603 };
20604 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
20605
20606 cx.set_state(indoc! {"
20607 struct Fˇoo {}
20608 "});
20609
20610 cx.update_editor(|editor, _, cx| {
20611 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
20612 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
20613 editor.highlight_background::<DocumentHighlightRead>(
20614 &[highlight_range],
20615 |theme| theme.colors().editor_document_highlight_read_background,
20616 cx,
20617 );
20618 });
20619
20620 let mut prepare_rename_handler = cx
20621 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
20622 move |_, _, _| async move {
20623 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
20624 start: lsp::Position {
20625 line: 0,
20626 character: 7,
20627 },
20628 end: lsp::Position {
20629 line: 0,
20630 character: 10,
20631 },
20632 })))
20633 },
20634 );
20635 let prepare_rename_task = cx
20636 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
20637 .expect("Prepare rename was not started");
20638 prepare_rename_handler.next().await.unwrap();
20639 prepare_rename_task.await.expect("Prepare rename failed");
20640
20641 let mut rename_handler =
20642 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
20643 let edit = lsp::TextEdit {
20644 range: lsp::Range {
20645 start: lsp::Position {
20646 line: 0,
20647 character: 7,
20648 },
20649 end: lsp::Position {
20650 line: 0,
20651 character: 10,
20652 },
20653 },
20654 new_text: "FooRenamed".to_string(),
20655 };
20656 Ok(Some(lsp::WorkspaceEdit::new(
20657 // Specify the same edit twice
20658 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
20659 )))
20660 });
20661 let rename_task = cx
20662 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
20663 .expect("Confirm rename was not started");
20664 rename_handler.next().await.unwrap();
20665 rename_task.await.expect("Confirm rename failed");
20666 cx.run_until_parked();
20667
20668 // Despite two edits, only one is actually applied as those are identical
20669 cx.assert_editor_state(indoc! {"
20670 struct FooRenamedˇ {}
20671 "});
20672}
20673
20674#[gpui::test]
20675async fn test_rename_without_prepare(cx: &mut TestAppContext) {
20676 init_test(cx, |_| {});
20677 // These capabilities indicate that the server does not support prepare rename.
20678 let capabilities = lsp::ServerCapabilities {
20679 rename_provider: Some(lsp::OneOf::Left(true)),
20680 ..Default::default()
20681 };
20682 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
20683
20684 cx.set_state(indoc! {"
20685 struct Fˇoo {}
20686 "});
20687
20688 cx.update_editor(|editor, _window, cx| {
20689 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
20690 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
20691 editor.highlight_background::<DocumentHighlightRead>(
20692 &[highlight_range],
20693 |theme| theme.colors().editor_document_highlight_read_background,
20694 cx,
20695 );
20696 });
20697
20698 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
20699 .expect("Prepare rename was not started")
20700 .await
20701 .expect("Prepare rename failed");
20702
20703 let mut rename_handler =
20704 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
20705 let edit = lsp::TextEdit {
20706 range: lsp::Range {
20707 start: lsp::Position {
20708 line: 0,
20709 character: 7,
20710 },
20711 end: lsp::Position {
20712 line: 0,
20713 character: 10,
20714 },
20715 },
20716 new_text: "FooRenamed".to_string(),
20717 };
20718 Ok(Some(lsp::WorkspaceEdit::new(
20719 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
20720 )))
20721 });
20722 let rename_task = cx
20723 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
20724 .expect("Confirm rename was not started");
20725 rename_handler.next().await.unwrap();
20726 rename_task.await.expect("Confirm rename failed");
20727 cx.run_until_parked();
20728
20729 // Correct range is renamed, as `surrounding_word` is used to find it.
20730 cx.assert_editor_state(indoc! {"
20731 struct FooRenamedˇ {}
20732 "});
20733}
20734
20735#[gpui::test]
20736async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
20737 init_test(cx, |_| {});
20738 let mut cx = EditorTestContext::new(cx).await;
20739
20740 let language = Arc::new(
20741 Language::new(
20742 LanguageConfig::default(),
20743 Some(tree_sitter_html::LANGUAGE.into()),
20744 )
20745 .with_brackets_query(
20746 r#"
20747 ("<" @open "/>" @close)
20748 ("</" @open ">" @close)
20749 ("<" @open ">" @close)
20750 ("\"" @open "\"" @close)
20751 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
20752 "#,
20753 )
20754 .unwrap(),
20755 );
20756 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
20757
20758 cx.set_state(indoc! {"
20759 <span>ˇ</span>
20760 "});
20761 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
20762 cx.assert_editor_state(indoc! {"
20763 <span>
20764 ˇ
20765 </span>
20766 "});
20767
20768 cx.set_state(indoc! {"
20769 <span><span></span>ˇ</span>
20770 "});
20771 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
20772 cx.assert_editor_state(indoc! {"
20773 <span><span></span>
20774 ˇ</span>
20775 "});
20776
20777 cx.set_state(indoc! {"
20778 <span>ˇ
20779 </span>
20780 "});
20781 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
20782 cx.assert_editor_state(indoc! {"
20783 <span>
20784 ˇ
20785 </span>
20786 "});
20787}
20788
20789#[gpui::test(iterations = 10)]
20790async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
20791 init_test(cx, |_| {});
20792
20793 let fs = FakeFs::new(cx.executor());
20794 fs.insert_tree(
20795 path!("/dir"),
20796 json!({
20797 "a.ts": "a",
20798 }),
20799 )
20800 .await;
20801
20802 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
20803 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20804 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20805
20806 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
20807 language_registry.add(Arc::new(Language::new(
20808 LanguageConfig {
20809 name: "TypeScript".into(),
20810 matcher: LanguageMatcher {
20811 path_suffixes: vec!["ts".to_string()],
20812 ..Default::default()
20813 },
20814 ..Default::default()
20815 },
20816 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
20817 )));
20818 let mut fake_language_servers = language_registry.register_fake_lsp(
20819 "TypeScript",
20820 FakeLspAdapter {
20821 capabilities: lsp::ServerCapabilities {
20822 code_lens_provider: Some(lsp::CodeLensOptions {
20823 resolve_provider: Some(true),
20824 }),
20825 execute_command_provider: Some(lsp::ExecuteCommandOptions {
20826 commands: vec!["_the/command".to_string()],
20827 ..lsp::ExecuteCommandOptions::default()
20828 }),
20829 ..lsp::ServerCapabilities::default()
20830 },
20831 ..FakeLspAdapter::default()
20832 },
20833 );
20834
20835 let (buffer, _handle) = project
20836 .update(cx, |p, cx| {
20837 p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
20838 })
20839 .await
20840 .unwrap();
20841 cx.executor().run_until_parked();
20842
20843 let fake_server = fake_language_servers.next().await.unwrap();
20844
20845 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
20846 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
20847 drop(buffer_snapshot);
20848 let actions = cx
20849 .update_window(*workspace, |_, window, cx| {
20850 project.code_actions(&buffer, anchor..anchor, window, cx)
20851 })
20852 .unwrap();
20853
20854 fake_server
20855 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
20856 Ok(Some(vec![
20857 lsp::CodeLens {
20858 range: lsp::Range::default(),
20859 command: Some(lsp::Command {
20860 title: "Code lens command".to_owned(),
20861 command: "_the/command".to_owned(),
20862 arguments: None,
20863 }),
20864 data: None,
20865 },
20866 lsp::CodeLens {
20867 range: lsp::Range::default(),
20868 command: Some(lsp::Command {
20869 title: "Command not in capabilities".to_owned(),
20870 command: "not in capabilities".to_owned(),
20871 arguments: None,
20872 }),
20873 data: None,
20874 },
20875 lsp::CodeLens {
20876 range: lsp::Range {
20877 start: lsp::Position {
20878 line: 1,
20879 character: 1,
20880 },
20881 end: lsp::Position {
20882 line: 1,
20883 character: 1,
20884 },
20885 },
20886 command: Some(lsp::Command {
20887 title: "Command not in range".to_owned(),
20888 command: "_the/command".to_owned(),
20889 arguments: None,
20890 }),
20891 data: None,
20892 },
20893 ]))
20894 })
20895 .next()
20896 .await;
20897
20898 let actions = actions.await.unwrap();
20899 assert_eq!(
20900 actions.len(),
20901 1,
20902 "Should have only one valid action for the 0..0 range"
20903 );
20904 let action = actions[0].clone();
20905 let apply = project.update(cx, |project, cx| {
20906 project.apply_code_action(buffer.clone(), action, true, cx)
20907 });
20908
20909 // Resolving the code action does not populate its edits. In absence of
20910 // edits, we must execute the given command.
20911 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
20912 |mut lens, _| async move {
20913 let lens_command = lens.command.as_mut().expect("should have a command");
20914 assert_eq!(lens_command.title, "Code lens command");
20915 lens_command.arguments = Some(vec![json!("the-argument")]);
20916 Ok(lens)
20917 },
20918 );
20919
20920 // While executing the command, the language server sends the editor
20921 // a `workspaceEdit` request.
20922 fake_server
20923 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
20924 let fake = fake_server.clone();
20925 move |params, _| {
20926 assert_eq!(params.command, "_the/command");
20927 let fake = fake.clone();
20928 async move {
20929 fake.server
20930 .request::<lsp::request::ApplyWorkspaceEdit>(
20931 lsp::ApplyWorkspaceEditParams {
20932 label: None,
20933 edit: lsp::WorkspaceEdit {
20934 changes: Some(
20935 [(
20936 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
20937 vec![lsp::TextEdit {
20938 range: lsp::Range::new(
20939 lsp::Position::new(0, 0),
20940 lsp::Position::new(0, 0),
20941 ),
20942 new_text: "X".into(),
20943 }],
20944 )]
20945 .into_iter()
20946 .collect(),
20947 ),
20948 ..Default::default()
20949 },
20950 },
20951 )
20952 .await
20953 .into_response()
20954 .unwrap();
20955 Ok(Some(json!(null)))
20956 }
20957 }
20958 })
20959 .next()
20960 .await;
20961
20962 // Applying the code lens command returns a project transaction containing the edits
20963 // sent by the language server in its `workspaceEdit` request.
20964 let transaction = apply.await.unwrap();
20965 assert!(transaction.0.contains_key(&buffer));
20966 buffer.update(cx, |buffer, cx| {
20967 assert_eq!(buffer.text(), "Xa");
20968 buffer.undo(cx);
20969 assert_eq!(buffer.text(), "a");
20970 });
20971}
20972
20973#[gpui::test]
20974async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
20975 init_test(cx, |_| {});
20976
20977 let fs = FakeFs::new(cx.executor());
20978 let main_text = r#"fn main() {
20979println!("1");
20980println!("2");
20981println!("3");
20982println!("4");
20983println!("5");
20984}"#;
20985 let lib_text = "mod foo {}";
20986 fs.insert_tree(
20987 path!("/a"),
20988 json!({
20989 "lib.rs": lib_text,
20990 "main.rs": main_text,
20991 }),
20992 )
20993 .await;
20994
20995 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20996 let (workspace, cx) =
20997 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20998 let worktree_id = workspace.update(cx, |workspace, cx| {
20999 workspace.project().update(cx, |project, cx| {
21000 project.worktrees(cx).next().unwrap().read(cx).id()
21001 })
21002 });
21003
21004 let expected_ranges = vec![
21005 Point::new(0, 0)..Point::new(0, 0),
21006 Point::new(1, 0)..Point::new(1, 1),
21007 Point::new(2, 0)..Point::new(2, 2),
21008 Point::new(3, 0)..Point::new(3, 3),
21009 ];
21010
21011 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
21012 let editor_1 = workspace
21013 .update_in(cx, |workspace, window, cx| {
21014 workspace.open_path(
21015 (worktree_id, "main.rs"),
21016 Some(pane_1.downgrade()),
21017 true,
21018 window,
21019 cx,
21020 )
21021 })
21022 .unwrap()
21023 .await
21024 .downcast::<Editor>()
21025 .unwrap();
21026 pane_1.update(cx, |pane, cx| {
21027 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21028 open_editor.update(cx, |editor, cx| {
21029 assert_eq!(
21030 editor.display_text(cx),
21031 main_text,
21032 "Original main.rs text on initial open",
21033 );
21034 assert_eq!(
21035 editor
21036 .selections
21037 .all::<Point>(cx)
21038 .into_iter()
21039 .map(|s| s.range())
21040 .collect::<Vec<_>>(),
21041 vec![Point::zero()..Point::zero()],
21042 "Default selections on initial open",
21043 );
21044 })
21045 });
21046 editor_1.update_in(cx, |editor, window, cx| {
21047 editor.change_selections(None, window, cx, |s| {
21048 s.select_ranges(expected_ranges.clone());
21049 });
21050 });
21051
21052 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
21053 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
21054 });
21055 let editor_2 = workspace
21056 .update_in(cx, |workspace, window, cx| {
21057 workspace.open_path(
21058 (worktree_id, "main.rs"),
21059 Some(pane_2.downgrade()),
21060 true,
21061 window,
21062 cx,
21063 )
21064 })
21065 .unwrap()
21066 .await
21067 .downcast::<Editor>()
21068 .unwrap();
21069 pane_2.update(cx, |pane, cx| {
21070 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21071 open_editor.update(cx, |editor, cx| {
21072 assert_eq!(
21073 editor.display_text(cx),
21074 main_text,
21075 "Original main.rs text on initial open in another panel",
21076 );
21077 assert_eq!(
21078 editor
21079 .selections
21080 .all::<Point>(cx)
21081 .into_iter()
21082 .map(|s| s.range())
21083 .collect::<Vec<_>>(),
21084 vec![Point::zero()..Point::zero()],
21085 "Default selections on initial open in another panel",
21086 );
21087 })
21088 });
21089
21090 editor_2.update_in(cx, |editor, window, cx| {
21091 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
21092 });
21093
21094 let _other_editor_1 = workspace
21095 .update_in(cx, |workspace, window, cx| {
21096 workspace.open_path(
21097 (worktree_id, "lib.rs"),
21098 Some(pane_1.downgrade()),
21099 true,
21100 window,
21101 cx,
21102 )
21103 })
21104 .unwrap()
21105 .await
21106 .downcast::<Editor>()
21107 .unwrap();
21108 pane_1
21109 .update_in(cx, |pane, window, cx| {
21110 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
21111 })
21112 .await
21113 .unwrap();
21114 drop(editor_1);
21115 pane_1.update(cx, |pane, cx| {
21116 pane.active_item()
21117 .unwrap()
21118 .downcast::<Editor>()
21119 .unwrap()
21120 .update(cx, |editor, cx| {
21121 assert_eq!(
21122 editor.display_text(cx),
21123 lib_text,
21124 "Other file should be open and active",
21125 );
21126 });
21127 assert_eq!(pane.items().count(), 1, "No other editors should be open");
21128 });
21129
21130 let _other_editor_2 = workspace
21131 .update_in(cx, |workspace, window, cx| {
21132 workspace.open_path(
21133 (worktree_id, "lib.rs"),
21134 Some(pane_2.downgrade()),
21135 true,
21136 window,
21137 cx,
21138 )
21139 })
21140 .unwrap()
21141 .await
21142 .downcast::<Editor>()
21143 .unwrap();
21144 pane_2
21145 .update_in(cx, |pane, window, cx| {
21146 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
21147 })
21148 .await
21149 .unwrap();
21150 drop(editor_2);
21151 pane_2.update(cx, |pane, cx| {
21152 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21153 open_editor.update(cx, |editor, cx| {
21154 assert_eq!(
21155 editor.display_text(cx),
21156 lib_text,
21157 "Other file should be open and active in another panel too",
21158 );
21159 });
21160 assert_eq!(
21161 pane.items().count(),
21162 1,
21163 "No other editors should be open in another pane",
21164 );
21165 });
21166
21167 let _editor_1_reopened = workspace
21168 .update_in(cx, |workspace, window, cx| {
21169 workspace.open_path(
21170 (worktree_id, "main.rs"),
21171 Some(pane_1.downgrade()),
21172 true,
21173 window,
21174 cx,
21175 )
21176 })
21177 .unwrap()
21178 .await
21179 .downcast::<Editor>()
21180 .unwrap();
21181 let _editor_2_reopened = workspace
21182 .update_in(cx, |workspace, window, cx| {
21183 workspace.open_path(
21184 (worktree_id, "main.rs"),
21185 Some(pane_2.downgrade()),
21186 true,
21187 window,
21188 cx,
21189 )
21190 })
21191 .unwrap()
21192 .await
21193 .downcast::<Editor>()
21194 .unwrap();
21195 pane_1.update(cx, |pane, cx| {
21196 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21197 open_editor.update(cx, |editor, cx| {
21198 assert_eq!(
21199 editor.display_text(cx),
21200 main_text,
21201 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
21202 );
21203 assert_eq!(
21204 editor
21205 .selections
21206 .all::<Point>(cx)
21207 .into_iter()
21208 .map(|s| s.range())
21209 .collect::<Vec<_>>(),
21210 expected_ranges,
21211 "Previous editor in the 1st panel had selections and should get them restored on reopen",
21212 );
21213 })
21214 });
21215 pane_2.update(cx, |pane, cx| {
21216 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21217 open_editor.update(cx, |editor, cx| {
21218 assert_eq!(
21219 editor.display_text(cx),
21220 r#"fn main() {
21221⋯rintln!("1");
21222⋯intln!("2");
21223⋯ntln!("3");
21224println!("4");
21225println!("5");
21226}"#,
21227 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
21228 );
21229 assert_eq!(
21230 editor
21231 .selections
21232 .all::<Point>(cx)
21233 .into_iter()
21234 .map(|s| s.range())
21235 .collect::<Vec<_>>(),
21236 vec![Point::zero()..Point::zero()],
21237 "Previous editor in the 2nd pane had no selections changed hence should restore none",
21238 );
21239 })
21240 });
21241}
21242
21243#[gpui::test]
21244async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
21245 init_test(cx, |_| {});
21246
21247 let fs = FakeFs::new(cx.executor());
21248 let main_text = r#"fn main() {
21249println!("1");
21250println!("2");
21251println!("3");
21252println!("4");
21253println!("5");
21254}"#;
21255 let lib_text = "mod foo {}";
21256 fs.insert_tree(
21257 path!("/a"),
21258 json!({
21259 "lib.rs": lib_text,
21260 "main.rs": main_text,
21261 }),
21262 )
21263 .await;
21264
21265 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21266 let (workspace, cx) =
21267 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
21268 let worktree_id = workspace.update(cx, |workspace, cx| {
21269 workspace.project().update(cx, |project, cx| {
21270 project.worktrees(cx).next().unwrap().read(cx).id()
21271 })
21272 });
21273
21274 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
21275 let editor = workspace
21276 .update_in(cx, |workspace, window, cx| {
21277 workspace.open_path(
21278 (worktree_id, "main.rs"),
21279 Some(pane.downgrade()),
21280 true,
21281 window,
21282 cx,
21283 )
21284 })
21285 .unwrap()
21286 .await
21287 .downcast::<Editor>()
21288 .unwrap();
21289 pane.update(cx, |pane, cx| {
21290 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21291 open_editor.update(cx, |editor, cx| {
21292 assert_eq!(
21293 editor.display_text(cx),
21294 main_text,
21295 "Original main.rs text on initial open",
21296 );
21297 })
21298 });
21299 editor.update_in(cx, |editor, window, cx| {
21300 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
21301 });
21302
21303 cx.update_global(|store: &mut SettingsStore, cx| {
21304 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
21305 s.restore_on_file_reopen = Some(false);
21306 });
21307 });
21308 editor.update_in(cx, |editor, window, cx| {
21309 editor.fold_ranges(
21310 vec![
21311 Point::new(1, 0)..Point::new(1, 1),
21312 Point::new(2, 0)..Point::new(2, 2),
21313 Point::new(3, 0)..Point::new(3, 3),
21314 ],
21315 false,
21316 window,
21317 cx,
21318 );
21319 });
21320 pane.update_in(cx, |pane, window, cx| {
21321 pane.close_all_items(&CloseAllItems::default(), window, cx)
21322 })
21323 .await
21324 .unwrap();
21325 pane.update(cx, |pane, _| {
21326 assert!(pane.active_item().is_none());
21327 });
21328 cx.update_global(|store: &mut SettingsStore, cx| {
21329 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
21330 s.restore_on_file_reopen = Some(true);
21331 });
21332 });
21333
21334 let _editor_reopened = workspace
21335 .update_in(cx, |workspace, window, cx| {
21336 workspace.open_path(
21337 (worktree_id, "main.rs"),
21338 Some(pane.downgrade()),
21339 true,
21340 window,
21341 cx,
21342 )
21343 })
21344 .unwrap()
21345 .await
21346 .downcast::<Editor>()
21347 .unwrap();
21348 pane.update(cx, |pane, cx| {
21349 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21350 open_editor.update(cx, |editor, cx| {
21351 assert_eq!(
21352 editor.display_text(cx),
21353 main_text,
21354 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
21355 );
21356 })
21357 });
21358}
21359
21360#[gpui::test]
21361async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
21362 struct EmptyModalView {
21363 focus_handle: gpui::FocusHandle,
21364 }
21365 impl EventEmitter<DismissEvent> for EmptyModalView {}
21366 impl Render for EmptyModalView {
21367 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
21368 div()
21369 }
21370 }
21371 impl Focusable for EmptyModalView {
21372 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
21373 self.focus_handle.clone()
21374 }
21375 }
21376 impl workspace::ModalView for EmptyModalView {}
21377 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
21378 EmptyModalView {
21379 focus_handle: cx.focus_handle(),
21380 }
21381 }
21382
21383 init_test(cx, |_| {});
21384
21385 let fs = FakeFs::new(cx.executor());
21386 let project = Project::test(fs, [], cx).await;
21387 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21388 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
21389 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21390 let editor = cx.new_window_entity(|window, cx| {
21391 Editor::new(
21392 EditorMode::full(),
21393 buffer,
21394 Some(project.clone()),
21395 window,
21396 cx,
21397 )
21398 });
21399 workspace
21400 .update(cx, |workspace, window, cx| {
21401 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
21402 })
21403 .unwrap();
21404 editor.update_in(cx, |editor, window, cx| {
21405 editor.open_context_menu(&OpenContextMenu, window, cx);
21406 assert!(editor.mouse_context_menu.is_some());
21407 });
21408 workspace
21409 .update(cx, |workspace, window, cx| {
21410 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
21411 })
21412 .unwrap();
21413 cx.read(|cx| {
21414 assert!(editor.read(cx).mouse_context_menu.is_none());
21415 });
21416}
21417
21418#[gpui::test]
21419async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
21420 init_test(cx, |_| {});
21421
21422 let fs = FakeFs::new(cx.executor());
21423 fs.insert_file(path!("/file.html"), Default::default())
21424 .await;
21425
21426 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
21427
21428 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21429 let html_language = Arc::new(Language::new(
21430 LanguageConfig {
21431 name: "HTML".into(),
21432 matcher: LanguageMatcher {
21433 path_suffixes: vec!["html".to_string()],
21434 ..LanguageMatcher::default()
21435 },
21436 brackets: BracketPairConfig {
21437 pairs: vec![BracketPair {
21438 start: "<".into(),
21439 end: ">".into(),
21440 close: true,
21441 ..Default::default()
21442 }],
21443 ..Default::default()
21444 },
21445 ..Default::default()
21446 },
21447 Some(tree_sitter_html::LANGUAGE.into()),
21448 ));
21449 language_registry.add(html_language);
21450 let mut fake_servers = language_registry.register_fake_lsp(
21451 "HTML",
21452 FakeLspAdapter {
21453 capabilities: lsp::ServerCapabilities {
21454 completion_provider: Some(lsp::CompletionOptions {
21455 resolve_provider: Some(true),
21456 ..Default::default()
21457 }),
21458 ..Default::default()
21459 },
21460 ..Default::default()
21461 },
21462 );
21463
21464 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21465 let cx = &mut VisualTestContext::from_window(*workspace, cx);
21466
21467 let worktree_id = workspace
21468 .update(cx, |workspace, _window, cx| {
21469 workspace.project().update(cx, |project, cx| {
21470 project.worktrees(cx).next().unwrap().read(cx).id()
21471 })
21472 })
21473 .unwrap();
21474 project
21475 .update(cx, |project, cx| {
21476 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
21477 })
21478 .await
21479 .unwrap();
21480 let editor = workspace
21481 .update(cx, |workspace, window, cx| {
21482 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
21483 })
21484 .unwrap()
21485 .await
21486 .unwrap()
21487 .downcast::<Editor>()
21488 .unwrap();
21489
21490 let fake_server = fake_servers.next().await.unwrap();
21491 editor.update_in(cx, |editor, window, cx| {
21492 editor.set_text("<ad></ad>", window, cx);
21493 editor.change_selections(None, window, cx, |selections| {
21494 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
21495 });
21496 let Some((buffer, _)) = editor
21497 .buffer
21498 .read(cx)
21499 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
21500 else {
21501 panic!("Failed to get buffer for selection position");
21502 };
21503 let buffer = buffer.read(cx);
21504 let buffer_id = buffer.remote_id();
21505 let opening_range =
21506 buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3));
21507 let closing_range =
21508 buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
21509 let mut linked_ranges = HashMap::default();
21510 linked_ranges.insert(
21511 buffer_id,
21512 vec![(opening_range.clone(), vec![closing_range.clone()])],
21513 );
21514 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
21515 });
21516 let mut completion_handle =
21517 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
21518 Ok(Some(lsp::CompletionResponse::Array(vec![
21519 lsp::CompletionItem {
21520 label: "head".to_string(),
21521 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
21522 lsp::InsertReplaceEdit {
21523 new_text: "head".to_string(),
21524 insert: lsp::Range::new(
21525 lsp::Position::new(0, 1),
21526 lsp::Position::new(0, 3),
21527 ),
21528 replace: lsp::Range::new(
21529 lsp::Position::new(0, 1),
21530 lsp::Position::new(0, 3),
21531 ),
21532 },
21533 )),
21534 ..Default::default()
21535 },
21536 ])))
21537 });
21538 editor.update_in(cx, |editor, window, cx| {
21539 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
21540 });
21541 cx.run_until_parked();
21542 completion_handle.next().await.unwrap();
21543 editor.update(cx, |editor, _| {
21544 assert!(
21545 editor.context_menu_visible(),
21546 "Completion menu should be visible"
21547 );
21548 });
21549 editor.update_in(cx, |editor, window, cx| {
21550 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
21551 });
21552 cx.executor().run_until_parked();
21553 editor.update(cx, |editor, cx| {
21554 assert_eq!(editor.text(cx), "<head></head>");
21555 });
21556}
21557
21558#[gpui::test]
21559async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
21560 init_test(cx, |_| {});
21561
21562 let fs = FakeFs::new(cx.executor());
21563 fs.insert_tree(
21564 path!("/root"),
21565 json!({
21566 "a": {
21567 "main.rs": "fn main() {}",
21568 },
21569 "foo": {
21570 "bar": {
21571 "external_file.rs": "pub mod external {}",
21572 }
21573 }
21574 }),
21575 )
21576 .await;
21577
21578 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
21579 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21580 language_registry.add(rust_lang());
21581 let _fake_servers = language_registry.register_fake_lsp(
21582 "Rust",
21583 FakeLspAdapter {
21584 ..FakeLspAdapter::default()
21585 },
21586 );
21587 let (workspace, cx) =
21588 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
21589 let worktree_id = workspace.update(cx, |workspace, cx| {
21590 workspace.project().update(cx, |project, cx| {
21591 project.worktrees(cx).next().unwrap().read(cx).id()
21592 })
21593 });
21594
21595 let assert_language_servers_count =
21596 |expected: usize, context: &str, cx: &mut VisualTestContext| {
21597 project.update(cx, |project, cx| {
21598 let current = project
21599 .lsp_store()
21600 .read(cx)
21601 .as_local()
21602 .unwrap()
21603 .language_servers
21604 .len();
21605 assert_eq!(expected, current, "{context}");
21606 });
21607 };
21608
21609 assert_language_servers_count(
21610 0,
21611 "No servers should be running before any file is open",
21612 cx,
21613 );
21614 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
21615 let main_editor = workspace
21616 .update_in(cx, |workspace, window, cx| {
21617 workspace.open_path(
21618 (worktree_id, "main.rs"),
21619 Some(pane.downgrade()),
21620 true,
21621 window,
21622 cx,
21623 )
21624 })
21625 .unwrap()
21626 .await
21627 .downcast::<Editor>()
21628 .unwrap();
21629 pane.update(cx, |pane, cx| {
21630 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21631 open_editor.update(cx, |editor, cx| {
21632 assert_eq!(
21633 editor.display_text(cx),
21634 "fn main() {}",
21635 "Original main.rs text on initial open",
21636 );
21637 });
21638 assert_eq!(open_editor, main_editor);
21639 });
21640 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
21641
21642 let external_editor = workspace
21643 .update_in(cx, |workspace, window, cx| {
21644 workspace.open_abs_path(
21645 PathBuf::from("/root/foo/bar/external_file.rs"),
21646 OpenOptions::default(),
21647 window,
21648 cx,
21649 )
21650 })
21651 .await
21652 .expect("opening external file")
21653 .downcast::<Editor>()
21654 .expect("downcasted external file's open element to editor");
21655 pane.update(cx, |pane, cx| {
21656 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21657 open_editor.update(cx, |editor, cx| {
21658 assert_eq!(
21659 editor.display_text(cx),
21660 "pub mod external {}",
21661 "External file is open now",
21662 );
21663 });
21664 assert_eq!(open_editor, external_editor);
21665 });
21666 assert_language_servers_count(
21667 1,
21668 "Second, external, *.rs file should join the existing server",
21669 cx,
21670 );
21671
21672 pane.update_in(cx, |pane, window, cx| {
21673 pane.close_active_item(&CloseActiveItem::default(), window, cx)
21674 })
21675 .await
21676 .unwrap();
21677 pane.update_in(cx, |pane, window, cx| {
21678 pane.navigate_backward(window, cx);
21679 });
21680 cx.run_until_parked();
21681 pane.update(cx, |pane, cx| {
21682 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21683 open_editor.update(cx, |editor, cx| {
21684 assert_eq!(
21685 editor.display_text(cx),
21686 "pub mod external {}",
21687 "External file is open now",
21688 );
21689 });
21690 });
21691 assert_language_servers_count(
21692 1,
21693 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
21694 cx,
21695 );
21696
21697 cx.update(|_, cx| {
21698 workspace::reload(&workspace::Reload::default(), cx);
21699 });
21700 assert_language_servers_count(
21701 1,
21702 "After reloading the worktree with local and external files opened, only one project should be started",
21703 cx,
21704 );
21705}
21706
21707#[gpui::test]
21708async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
21709 init_test(cx, |_| {});
21710
21711 let mut cx = EditorTestContext::new(cx).await;
21712 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
21713 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
21714
21715 // test cursor move to start of each line on tab
21716 // for `if`, `elif`, `else`, `while`, `with` and `for`
21717 cx.set_state(indoc! {"
21718 def main():
21719 ˇ for item in items:
21720 ˇ while item.active:
21721 ˇ if item.value > 10:
21722 ˇ continue
21723 ˇ elif item.value < 0:
21724 ˇ break
21725 ˇ else:
21726 ˇ with item.context() as ctx:
21727 ˇ yield count
21728 ˇ else:
21729 ˇ log('while else')
21730 ˇ else:
21731 ˇ log('for else')
21732 "});
21733 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
21734 cx.assert_editor_state(indoc! {"
21735 def main():
21736 ˇfor item in items:
21737 ˇwhile item.active:
21738 ˇif item.value > 10:
21739 ˇcontinue
21740 ˇelif item.value < 0:
21741 ˇbreak
21742 ˇelse:
21743 ˇwith item.context() as ctx:
21744 ˇyield count
21745 ˇelse:
21746 ˇlog('while else')
21747 ˇelse:
21748 ˇlog('for else')
21749 "});
21750 // test relative indent is preserved when tab
21751 // for `if`, `elif`, `else`, `while`, `with` and `for`
21752 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
21753 cx.assert_editor_state(indoc! {"
21754 def main():
21755 ˇfor item in items:
21756 ˇwhile item.active:
21757 ˇif item.value > 10:
21758 ˇcontinue
21759 ˇelif item.value < 0:
21760 ˇbreak
21761 ˇelse:
21762 ˇwith item.context() as ctx:
21763 ˇyield count
21764 ˇelse:
21765 ˇlog('while else')
21766 ˇelse:
21767 ˇlog('for else')
21768 "});
21769
21770 // test cursor move to start of each line on tab
21771 // for `try`, `except`, `else`, `finally`, `match` and `def`
21772 cx.set_state(indoc! {"
21773 def main():
21774 ˇ try:
21775 ˇ fetch()
21776 ˇ except ValueError:
21777 ˇ handle_error()
21778 ˇ else:
21779 ˇ match value:
21780 ˇ case _:
21781 ˇ finally:
21782 ˇ def status():
21783 ˇ return 0
21784 "});
21785 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
21786 cx.assert_editor_state(indoc! {"
21787 def main():
21788 ˇtry:
21789 ˇfetch()
21790 ˇexcept ValueError:
21791 ˇhandle_error()
21792 ˇelse:
21793 ˇmatch value:
21794 ˇcase _:
21795 ˇfinally:
21796 ˇdef status():
21797 ˇreturn 0
21798 "});
21799 // test relative indent is preserved when tab
21800 // for `try`, `except`, `else`, `finally`, `match` and `def`
21801 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
21802 cx.assert_editor_state(indoc! {"
21803 def main():
21804 ˇtry:
21805 ˇfetch()
21806 ˇexcept ValueError:
21807 ˇhandle_error()
21808 ˇelse:
21809 ˇmatch value:
21810 ˇcase _:
21811 ˇfinally:
21812 ˇdef status():
21813 ˇreturn 0
21814 "});
21815}
21816
21817#[gpui::test]
21818async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
21819 init_test(cx, |_| {});
21820
21821 let mut cx = EditorTestContext::new(cx).await;
21822 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
21823 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
21824
21825 // test `else` auto outdents when typed inside `if` block
21826 cx.set_state(indoc! {"
21827 def main():
21828 if i == 2:
21829 return
21830 ˇ
21831 "});
21832 cx.update_editor(|editor, window, cx| {
21833 editor.handle_input("else:", window, cx);
21834 });
21835 cx.assert_editor_state(indoc! {"
21836 def main():
21837 if i == 2:
21838 return
21839 else:ˇ
21840 "});
21841
21842 // test `except` auto outdents when typed inside `try` block
21843 cx.set_state(indoc! {"
21844 def main():
21845 try:
21846 i = 2
21847 ˇ
21848 "});
21849 cx.update_editor(|editor, window, cx| {
21850 editor.handle_input("except:", window, cx);
21851 });
21852 cx.assert_editor_state(indoc! {"
21853 def main():
21854 try:
21855 i = 2
21856 except:ˇ
21857 "});
21858
21859 // test `else` auto outdents when typed inside `except` block
21860 cx.set_state(indoc! {"
21861 def main():
21862 try:
21863 i = 2
21864 except:
21865 j = 2
21866 ˇ
21867 "});
21868 cx.update_editor(|editor, window, cx| {
21869 editor.handle_input("else:", window, cx);
21870 });
21871 cx.assert_editor_state(indoc! {"
21872 def main():
21873 try:
21874 i = 2
21875 except:
21876 j = 2
21877 else:ˇ
21878 "});
21879
21880 // test `finally` auto outdents when typed inside `else` block
21881 cx.set_state(indoc! {"
21882 def main():
21883 try:
21884 i = 2
21885 except:
21886 j = 2
21887 else:
21888 k = 2
21889 ˇ
21890 "});
21891 cx.update_editor(|editor, window, cx| {
21892 editor.handle_input("finally:", window, cx);
21893 });
21894 cx.assert_editor_state(indoc! {"
21895 def main():
21896 try:
21897 i = 2
21898 except:
21899 j = 2
21900 else:
21901 k = 2
21902 finally:ˇ
21903 "});
21904
21905 // TODO: test `except` auto outdents when typed inside `try` block right after for block
21906 // cx.set_state(indoc! {"
21907 // def main():
21908 // try:
21909 // for i in range(n):
21910 // pass
21911 // ˇ
21912 // "});
21913 // cx.update_editor(|editor, window, cx| {
21914 // editor.handle_input("except:", window, cx);
21915 // });
21916 // cx.assert_editor_state(indoc! {"
21917 // def main():
21918 // try:
21919 // for i in range(n):
21920 // pass
21921 // except:ˇ
21922 // "});
21923
21924 // TODO: test `else` auto outdents when typed inside `except` block right after for block
21925 // cx.set_state(indoc! {"
21926 // def main():
21927 // try:
21928 // i = 2
21929 // except:
21930 // for i in range(n):
21931 // pass
21932 // ˇ
21933 // "});
21934 // cx.update_editor(|editor, window, cx| {
21935 // editor.handle_input("else:", window, cx);
21936 // });
21937 // cx.assert_editor_state(indoc! {"
21938 // def main():
21939 // try:
21940 // i = 2
21941 // except:
21942 // for i in range(n):
21943 // pass
21944 // else:ˇ
21945 // "});
21946
21947 // TODO: test `finally` auto outdents when typed inside `else` block right after for block
21948 // cx.set_state(indoc! {"
21949 // def main():
21950 // try:
21951 // i = 2
21952 // except:
21953 // j = 2
21954 // else:
21955 // for i in range(n):
21956 // pass
21957 // ˇ
21958 // "});
21959 // cx.update_editor(|editor, window, cx| {
21960 // editor.handle_input("finally:", window, cx);
21961 // });
21962 // cx.assert_editor_state(indoc! {"
21963 // def main():
21964 // try:
21965 // i = 2
21966 // except:
21967 // j = 2
21968 // else:
21969 // for i in range(n):
21970 // pass
21971 // finally:ˇ
21972 // "});
21973
21974 // test `else` stays at correct indent when typed after `for` block
21975 cx.set_state(indoc! {"
21976 def main():
21977 for i in range(10):
21978 if i == 3:
21979 break
21980 ˇ
21981 "});
21982 cx.update_editor(|editor, window, cx| {
21983 editor.handle_input("else:", window, cx);
21984 });
21985 cx.assert_editor_state(indoc! {"
21986 def main():
21987 for i in range(10):
21988 if i == 3:
21989 break
21990 else:ˇ
21991 "});
21992
21993 // test does not outdent on typing after line with square brackets
21994 cx.set_state(indoc! {"
21995 def f() -> list[str]:
21996 ˇ
21997 "});
21998 cx.update_editor(|editor, window, cx| {
21999 editor.handle_input("a", window, cx);
22000 });
22001 cx.assert_editor_state(indoc! {"
22002 def f() -> list[str]:
22003 aˇ
22004 "});
22005}
22006
22007#[gpui::test]
22008async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
22009 init_test(cx, |_| {});
22010 update_test_language_settings(cx, |settings| {
22011 settings.defaults.extend_comment_on_newline = Some(false);
22012 });
22013 let mut cx = EditorTestContext::new(cx).await;
22014 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
22015 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22016
22017 // test correct indent after newline on comment
22018 cx.set_state(indoc! {"
22019 # COMMENT:ˇ
22020 "});
22021 cx.update_editor(|editor, window, cx| {
22022 editor.newline(&Newline, window, cx);
22023 });
22024 cx.assert_editor_state(indoc! {"
22025 # COMMENT:
22026 ˇ
22027 "});
22028
22029 // test correct indent after newline in brackets
22030 cx.set_state(indoc! {"
22031 {ˇ}
22032 "});
22033 cx.update_editor(|editor, window, cx| {
22034 editor.newline(&Newline, window, cx);
22035 });
22036 cx.run_until_parked();
22037 cx.assert_editor_state(indoc! {"
22038 {
22039 ˇ
22040 }
22041 "});
22042
22043 cx.set_state(indoc! {"
22044 (ˇ)
22045 "});
22046 cx.update_editor(|editor, window, cx| {
22047 editor.newline(&Newline, window, cx);
22048 });
22049 cx.run_until_parked();
22050 cx.assert_editor_state(indoc! {"
22051 (
22052 ˇ
22053 )
22054 "});
22055
22056 // do not indent after empty lists or dictionaries
22057 cx.set_state(indoc! {"
22058 a = []ˇ
22059 "});
22060 cx.update_editor(|editor, window, cx| {
22061 editor.newline(&Newline, window, cx);
22062 });
22063 cx.run_until_parked();
22064 cx.assert_editor_state(indoc! {"
22065 a = []
22066 ˇ
22067 "});
22068}
22069
22070fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
22071 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
22072 point..point
22073}
22074
22075#[track_caller]
22076fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
22077 let (text, ranges) = marked_text_ranges(marked_text, true);
22078 assert_eq!(editor.text(cx), text);
22079 assert_eq!(
22080 editor.selections.ranges(cx),
22081 ranges,
22082 "Assert selections are {}",
22083 marked_text
22084 );
22085}
22086
22087pub fn handle_signature_help_request(
22088 cx: &mut EditorLspTestContext,
22089 mocked_response: lsp::SignatureHelp,
22090) -> impl Future<Output = ()> + use<> {
22091 let mut request =
22092 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
22093 let mocked_response = mocked_response.clone();
22094 async move { Ok(Some(mocked_response)) }
22095 });
22096
22097 async move {
22098 request.next().await;
22099 }
22100}
22101
22102#[track_caller]
22103pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
22104 cx.update_editor(|editor, _, _| {
22105 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
22106 let entries = menu.entries.borrow();
22107 let entries = entries
22108 .iter()
22109 .map(|entry| entry.string.as_str())
22110 .collect::<Vec<_>>();
22111 assert_eq!(entries, expected);
22112 } else {
22113 panic!("Expected completions menu");
22114 }
22115 });
22116}
22117
22118/// Handle completion request passing a marked string specifying where the completion
22119/// should be triggered from using '|' character, what range should be replaced, and what completions
22120/// should be returned using '<' and '>' to delimit the range.
22121///
22122/// Also see `handle_completion_request_with_insert_and_replace`.
22123#[track_caller]
22124pub fn handle_completion_request(
22125 marked_string: &str,
22126 completions: Vec<&'static str>,
22127 is_incomplete: bool,
22128 counter: Arc<AtomicUsize>,
22129 cx: &mut EditorLspTestContext,
22130) -> impl Future<Output = ()> {
22131 let complete_from_marker: TextRangeMarker = '|'.into();
22132 let replace_range_marker: TextRangeMarker = ('<', '>').into();
22133 let (_, mut marked_ranges) = marked_text_ranges_by(
22134 marked_string,
22135 vec![complete_from_marker.clone(), replace_range_marker.clone()],
22136 );
22137
22138 let complete_from_position =
22139 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
22140 let replace_range =
22141 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
22142
22143 let mut request =
22144 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
22145 let completions = completions.clone();
22146 counter.fetch_add(1, atomic::Ordering::Release);
22147 async move {
22148 assert_eq!(params.text_document_position.text_document.uri, url.clone());
22149 assert_eq!(
22150 params.text_document_position.position,
22151 complete_from_position
22152 );
22153 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
22154 is_incomplete: is_incomplete,
22155 item_defaults: None,
22156 items: completions
22157 .iter()
22158 .map(|completion_text| lsp::CompletionItem {
22159 label: completion_text.to_string(),
22160 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
22161 range: replace_range,
22162 new_text: completion_text.to_string(),
22163 })),
22164 ..Default::default()
22165 })
22166 .collect(),
22167 })))
22168 }
22169 });
22170
22171 async move {
22172 request.next().await;
22173 }
22174}
22175
22176/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
22177/// given instead, which also contains an `insert` range.
22178///
22179/// This function uses markers to define ranges:
22180/// - `|` marks the cursor position
22181/// - `<>` marks the replace range
22182/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
22183pub fn handle_completion_request_with_insert_and_replace(
22184 cx: &mut EditorLspTestContext,
22185 marked_string: &str,
22186 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
22187 counter: Arc<AtomicUsize>,
22188) -> impl Future<Output = ()> {
22189 let complete_from_marker: TextRangeMarker = '|'.into();
22190 let replace_range_marker: TextRangeMarker = ('<', '>').into();
22191 let insert_range_marker: TextRangeMarker = ('{', '}').into();
22192
22193 let (_, mut marked_ranges) = marked_text_ranges_by(
22194 marked_string,
22195 vec![
22196 complete_from_marker.clone(),
22197 replace_range_marker.clone(),
22198 insert_range_marker.clone(),
22199 ],
22200 );
22201
22202 let complete_from_position =
22203 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
22204 let replace_range =
22205 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
22206
22207 let insert_range = match marked_ranges.remove(&insert_range_marker) {
22208 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
22209 _ => lsp::Range {
22210 start: replace_range.start,
22211 end: complete_from_position,
22212 },
22213 };
22214
22215 let mut request =
22216 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
22217 let completions = completions.clone();
22218 counter.fetch_add(1, atomic::Ordering::Release);
22219 async move {
22220 assert_eq!(params.text_document_position.text_document.uri, url.clone());
22221 assert_eq!(
22222 params.text_document_position.position, complete_from_position,
22223 "marker `|` position doesn't match",
22224 );
22225 Ok(Some(lsp::CompletionResponse::Array(
22226 completions
22227 .iter()
22228 .map(|(label, new_text)| lsp::CompletionItem {
22229 label: label.to_string(),
22230 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
22231 lsp::InsertReplaceEdit {
22232 insert: insert_range,
22233 replace: replace_range,
22234 new_text: new_text.to_string(),
22235 },
22236 )),
22237 ..Default::default()
22238 })
22239 .collect(),
22240 )))
22241 }
22242 });
22243
22244 async move {
22245 request.next().await;
22246 }
22247}
22248
22249fn handle_resolve_completion_request(
22250 cx: &mut EditorLspTestContext,
22251 edits: Option<Vec<(&'static str, &'static str)>>,
22252) -> impl Future<Output = ()> {
22253 let edits = edits.map(|edits| {
22254 edits
22255 .iter()
22256 .map(|(marked_string, new_text)| {
22257 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
22258 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
22259 lsp::TextEdit::new(replace_range, new_text.to_string())
22260 })
22261 .collect::<Vec<_>>()
22262 });
22263
22264 let mut request =
22265 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
22266 let edits = edits.clone();
22267 async move {
22268 Ok(lsp::CompletionItem {
22269 additional_text_edits: edits,
22270 ..Default::default()
22271 })
22272 }
22273 });
22274
22275 async move {
22276 request.next().await;
22277 }
22278}
22279
22280pub(crate) fn update_test_language_settings(
22281 cx: &mut TestAppContext,
22282 f: impl Fn(&mut AllLanguageSettingsContent),
22283) {
22284 cx.update(|cx| {
22285 SettingsStore::update_global(cx, |store, cx| {
22286 store.update_user_settings::<AllLanguageSettings>(cx, f);
22287 });
22288 });
22289}
22290
22291pub(crate) fn update_test_project_settings(
22292 cx: &mut TestAppContext,
22293 f: impl Fn(&mut ProjectSettings),
22294) {
22295 cx.update(|cx| {
22296 SettingsStore::update_global(cx, |store, cx| {
22297 store.update_user_settings::<ProjectSettings>(cx, f);
22298 });
22299 });
22300}
22301
22302pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
22303 cx.update(|cx| {
22304 assets::Assets.load_test_fonts(cx);
22305 let store = SettingsStore::test(cx);
22306 cx.set_global(store);
22307 theme::init(theme::LoadThemes::JustBase, cx);
22308 release_channel::init(SemanticVersion::default(), cx);
22309 client::init_settings(cx);
22310 language::init(cx);
22311 Project::init_settings(cx);
22312 workspace::init_settings(cx);
22313 crate::init(cx);
22314 });
22315
22316 update_test_language_settings(cx, f);
22317}
22318
22319#[track_caller]
22320fn assert_hunk_revert(
22321 not_reverted_text_with_selections: &str,
22322 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
22323 expected_reverted_text_with_selections: &str,
22324 base_text: &str,
22325 cx: &mut EditorLspTestContext,
22326) {
22327 cx.set_state(not_reverted_text_with_selections);
22328 cx.set_head_text(base_text);
22329 cx.executor().run_until_parked();
22330
22331 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
22332 let snapshot = editor.snapshot(window, cx);
22333 let reverted_hunk_statuses = snapshot
22334 .buffer_snapshot
22335 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
22336 .map(|hunk| hunk.status().kind)
22337 .collect::<Vec<_>>();
22338
22339 editor.git_restore(&Default::default(), window, cx);
22340 reverted_hunk_statuses
22341 });
22342 cx.executor().run_until_parked();
22343 cx.assert_editor_state(expected_reverted_text_with_selections);
22344 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
22345}
22346
22347#[gpui::test(iterations = 10)]
22348async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
22349 init_test(cx, |_| {});
22350
22351 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
22352 let counter = diagnostic_requests.clone();
22353
22354 let fs = FakeFs::new(cx.executor());
22355 fs.insert_tree(
22356 path!("/a"),
22357 json!({
22358 "first.rs": "fn main() { let a = 5; }",
22359 "second.rs": "// Test file",
22360 }),
22361 )
22362 .await;
22363
22364 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22365 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22366 let cx = &mut VisualTestContext::from_window(*workspace, cx);
22367
22368 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
22369 language_registry.add(rust_lang());
22370 let mut fake_servers = language_registry.register_fake_lsp(
22371 "Rust",
22372 FakeLspAdapter {
22373 capabilities: lsp::ServerCapabilities {
22374 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
22375 lsp::DiagnosticOptions {
22376 identifier: None,
22377 inter_file_dependencies: true,
22378 workspace_diagnostics: true,
22379 work_done_progress_options: Default::default(),
22380 },
22381 )),
22382 ..Default::default()
22383 },
22384 ..Default::default()
22385 },
22386 );
22387
22388 let editor = workspace
22389 .update(cx, |workspace, window, cx| {
22390 workspace.open_abs_path(
22391 PathBuf::from(path!("/a/first.rs")),
22392 OpenOptions::default(),
22393 window,
22394 cx,
22395 )
22396 })
22397 .unwrap()
22398 .await
22399 .unwrap()
22400 .downcast::<Editor>()
22401 .unwrap();
22402 let fake_server = fake_servers.next().await.unwrap();
22403 let server_id = fake_server.server.server_id();
22404 let mut first_request = fake_server
22405 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
22406 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
22407 let result_id = Some(new_result_id.to_string());
22408 assert_eq!(
22409 params.text_document.uri,
22410 lsp::Url::from_file_path(path!("/a/first.rs")).unwrap()
22411 );
22412 async move {
22413 Ok(lsp::DocumentDiagnosticReportResult::Report(
22414 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
22415 related_documents: None,
22416 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
22417 items: Vec::new(),
22418 result_id,
22419 },
22420 }),
22421 ))
22422 }
22423 });
22424
22425 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
22426 project.update(cx, |project, cx| {
22427 let buffer_id = editor
22428 .read(cx)
22429 .buffer()
22430 .read(cx)
22431 .as_singleton()
22432 .expect("created a singleton buffer")
22433 .read(cx)
22434 .remote_id();
22435 let buffer_result_id = project
22436 .lsp_store()
22437 .read(cx)
22438 .result_id(server_id, buffer_id, cx);
22439 assert_eq!(expected, buffer_result_id);
22440 });
22441 };
22442
22443 ensure_result_id(None, cx);
22444 cx.executor().advance_clock(Duration::from_millis(60));
22445 cx.executor().run_until_parked();
22446 assert_eq!(
22447 diagnostic_requests.load(atomic::Ordering::Acquire),
22448 1,
22449 "Opening file should trigger diagnostic request"
22450 );
22451 first_request
22452 .next()
22453 .await
22454 .expect("should have sent the first diagnostics pull request");
22455 ensure_result_id(Some("1".to_string()), cx);
22456
22457 // Editing should trigger diagnostics
22458 editor.update_in(cx, |editor, window, cx| {
22459 editor.handle_input("2", window, cx)
22460 });
22461 cx.executor().advance_clock(Duration::from_millis(60));
22462 cx.executor().run_until_parked();
22463 assert_eq!(
22464 diagnostic_requests.load(atomic::Ordering::Acquire),
22465 2,
22466 "Editing should trigger diagnostic request"
22467 );
22468 ensure_result_id(Some("2".to_string()), cx);
22469
22470 // Moving cursor should not trigger diagnostic request
22471 editor.update_in(cx, |editor, window, cx| {
22472 editor.change_selections(None, window, cx, |s| {
22473 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
22474 });
22475 });
22476 cx.executor().advance_clock(Duration::from_millis(60));
22477 cx.executor().run_until_parked();
22478 assert_eq!(
22479 diagnostic_requests.load(atomic::Ordering::Acquire),
22480 2,
22481 "Cursor movement should not trigger diagnostic request"
22482 );
22483 ensure_result_id(Some("2".to_string()), cx);
22484 // Multiple rapid edits should be debounced
22485 for _ in 0..5 {
22486 editor.update_in(cx, |editor, window, cx| {
22487 editor.handle_input("x", window, cx)
22488 });
22489 }
22490 cx.executor().advance_clock(Duration::from_millis(60));
22491 cx.executor().run_until_parked();
22492
22493 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
22494 assert!(
22495 final_requests <= 4,
22496 "Multiple rapid edits should be debounced (got {final_requests} requests)",
22497 );
22498 ensure_result_id(Some(final_requests.to_string()), cx);
22499}
22500
22501#[gpui::test]
22502async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
22503 // Regression test for issue #11671
22504 // Previously, adding a cursor after moving multiple cursors would reset
22505 // the cursor count instead of adding to the existing cursors.
22506 init_test(cx, |_| {});
22507 let mut cx = EditorTestContext::new(cx).await;
22508
22509 // Create a simple buffer with cursor at start
22510 cx.set_state(indoc! {"
22511 ˇaaaa
22512 bbbb
22513 cccc
22514 dddd
22515 eeee
22516 ffff
22517 gggg
22518 hhhh"});
22519
22520 // Add 2 cursors below (so we have 3 total)
22521 cx.update_editor(|editor, window, cx| {
22522 editor.add_selection_below(&Default::default(), window, cx);
22523 editor.add_selection_below(&Default::default(), window, cx);
22524 });
22525
22526 // Verify we have 3 cursors
22527 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
22528 assert_eq!(
22529 initial_count, 3,
22530 "Should have 3 cursors after adding 2 below"
22531 );
22532
22533 // Move down one line
22534 cx.update_editor(|editor, window, cx| {
22535 editor.move_down(&MoveDown, window, cx);
22536 });
22537
22538 // Add another cursor below
22539 cx.update_editor(|editor, window, cx| {
22540 editor.add_selection_below(&Default::default(), window, cx);
22541 });
22542
22543 // Should now have 4 cursors (3 original + 1 new)
22544 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
22545 assert_eq!(
22546 final_count, 4,
22547 "Should have 4 cursors after moving and adding another"
22548 );
22549}
22550
22551#[gpui::test(iterations = 10)]
22552async fn test_document_colors(cx: &mut TestAppContext) {
22553 let expected_color = Rgba {
22554 r: 0.33,
22555 g: 0.33,
22556 b: 0.33,
22557 a: 0.33,
22558 };
22559
22560 init_test(cx, |_| {});
22561
22562 let fs = FakeFs::new(cx.executor());
22563 fs.insert_tree(
22564 path!("/a"),
22565 json!({
22566 "first.rs": "fn main() { let a = 5; }",
22567 }),
22568 )
22569 .await;
22570
22571 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22572 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22573 let cx = &mut VisualTestContext::from_window(*workspace, cx);
22574
22575 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
22576 language_registry.add(rust_lang());
22577 let mut fake_servers = language_registry.register_fake_lsp(
22578 "Rust",
22579 FakeLspAdapter {
22580 capabilities: lsp::ServerCapabilities {
22581 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
22582 ..lsp::ServerCapabilities::default()
22583 },
22584 name: "rust-analyzer",
22585 ..FakeLspAdapter::default()
22586 },
22587 );
22588 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
22589 "Rust",
22590 FakeLspAdapter {
22591 capabilities: lsp::ServerCapabilities {
22592 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
22593 ..lsp::ServerCapabilities::default()
22594 },
22595 name: "not-rust-analyzer",
22596 ..FakeLspAdapter::default()
22597 },
22598 );
22599
22600 let editor = workspace
22601 .update(cx, |workspace, window, cx| {
22602 workspace.open_abs_path(
22603 PathBuf::from(path!("/a/first.rs")),
22604 OpenOptions::default(),
22605 window,
22606 cx,
22607 )
22608 })
22609 .unwrap()
22610 .await
22611 .unwrap()
22612 .downcast::<Editor>()
22613 .unwrap();
22614 let fake_language_server = fake_servers.next().await.unwrap();
22615 let fake_language_server_without_capabilities =
22616 fake_servers_without_capabilities.next().await.unwrap();
22617 let requests_made = Arc::new(AtomicUsize::new(0));
22618 let closure_requests_made = Arc::clone(&requests_made);
22619 let mut color_request_handle = fake_language_server
22620 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
22621 let requests_made = Arc::clone(&closure_requests_made);
22622 async move {
22623 assert_eq!(
22624 params.text_document.uri,
22625 lsp::Url::from_file_path(path!("/a/first.rs")).unwrap()
22626 );
22627 requests_made.fetch_add(1, atomic::Ordering::Release);
22628 Ok(vec![
22629 lsp::ColorInformation {
22630 range: lsp::Range {
22631 start: lsp::Position {
22632 line: 0,
22633 character: 0,
22634 },
22635 end: lsp::Position {
22636 line: 0,
22637 character: 1,
22638 },
22639 },
22640 color: lsp::Color {
22641 red: 0.33,
22642 green: 0.33,
22643 blue: 0.33,
22644 alpha: 0.33,
22645 },
22646 },
22647 lsp::ColorInformation {
22648 range: lsp::Range {
22649 start: lsp::Position {
22650 line: 0,
22651 character: 0,
22652 },
22653 end: lsp::Position {
22654 line: 0,
22655 character: 1,
22656 },
22657 },
22658 color: lsp::Color {
22659 red: 0.33,
22660 green: 0.33,
22661 blue: 0.33,
22662 alpha: 0.33,
22663 },
22664 },
22665 ])
22666 }
22667 });
22668
22669 let _handle = fake_language_server_without_capabilities
22670 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
22671 panic!("Should not be called");
22672 });
22673 cx.executor().advance_clock(Duration::from_millis(100));
22674 color_request_handle.next().await.unwrap();
22675 cx.run_until_parked();
22676 assert_eq!(
22677 1,
22678 requests_made.load(atomic::Ordering::Acquire),
22679 "Should query for colors once per editor open"
22680 );
22681 editor.update_in(cx, |editor, _, cx| {
22682 assert_eq!(
22683 vec![expected_color],
22684 extract_color_inlays(editor, cx),
22685 "Should have an initial inlay"
22686 );
22687 });
22688
22689 // opening another file in a split should not influence the LSP query counter
22690 workspace
22691 .update(cx, |workspace, window, cx| {
22692 assert_eq!(
22693 workspace.panes().len(),
22694 1,
22695 "Should have one pane with one editor"
22696 );
22697 workspace.move_item_to_pane_in_direction(
22698 &MoveItemToPaneInDirection {
22699 direction: SplitDirection::Right,
22700 focus: false,
22701 clone: true,
22702 },
22703 window,
22704 cx,
22705 );
22706 })
22707 .unwrap();
22708 cx.run_until_parked();
22709 workspace
22710 .update(cx, |workspace, _, cx| {
22711 let panes = workspace.panes();
22712 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
22713 for pane in panes {
22714 let editor = pane
22715 .read(cx)
22716 .active_item()
22717 .and_then(|item| item.downcast::<Editor>())
22718 .expect("Should have opened an editor in each split");
22719 let editor_file = editor
22720 .read(cx)
22721 .buffer()
22722 .read(cx)
22723 .as_singleton()
22724 .expect("test deals with singleton buffers")
22725 .read(cx)
22726 .file()
22727 .expect("test buffese should have a file")
22728 .path();
22729 assert_eq!(
22730 editor_file.as_ref(),
22731 Path::new("first.rs"),
22732 "Both editors should be opened for the same file"
22733 )
22734 }
22735 })
22736 .unwrap();
22737
22738 cx.executor().advance_clock(Duration::from_millis(500));
22739 let save = editor.update_in(cx, |editor, window, cx| {
22740 editor.move_to_end(&MoveToEnd, window, cx);
22741 editor.handle_input("dirty", window, cx);
22742 editor.save(
22743 SaveOptions {
22744 format: true,
22745 autosave: true,
22746 },
22747 project.clone(),
22748 window,
22749 cx,
22750 )
22751 });
22752 save.await.unwrap();
22753
22754 color_request_handle.next().await.unwrap();
22755 cx.run_until_parked();
22756 assert_eq!(
22757 3,
22758 requests_made.load(atomic::Ordering::Acquire),
22759 "Should query for colors once per save and once per formatting after save"
22760 );
22761
22762 drop(editor);
22763 let close = workspace
22764 .update(cx, |workspace, window, cx| {
22765 workspace.active_pane().update(cx, |pane, cx| {
22766 pane.close_active_item(&CloseActiveItem::default(), window, cx)
22767 })
22768 })
22769 .unwrap();
22770 close.await.unwrap();
22771 let close = workspace
22772 .update(cx, |workspace, window, cx| {
22773 workspace.active_pane().update(cx, |pane, cx| {
22774 pane.close_active_item(&CloseActiveItem::default(), window, cx)
22775 })
22776 })
22777 .unwrap();
22778 close.await.unwrap();
22779 assert_eq!(
22780 3,
22781 requests_made.load(atomic::Ordering::Acquire),
22782 "After saving and closing all editors, no extra requests should be made"
22783 );
22784 workspace
22785 .update(cx, |workspace, _, cx| {
22786 assert!(
22787 workspace.active_item(cx).is_none(),
22788 "Should close all editors"
22789 )
22790 })
22791 .unwrap();
22792
22793 workspace
22794 .update(cx, |workspace, window, cx| {
22795 workspace.active_pane().update(cx, |pane, cx| {
22796 pane.navigate_backward(window, cx);
22797 })
22798 })
22799 .unwrap();
22800 cx.executor().advance_clock(Duration::from_millis(100));
22801 cx.run_until_parked();
22802 let editor = workspace
22803 .update(cx, |workspace, _, cx| {
22804 workspace
22805 .active_item(cx)
22806 .expect("Should have reopened the editor again after navigating back")
22807 .downcast::<Editor>()
22808 .expect("Should be an editor")
22809 })
22810 .unwrap();
22811 color_request_handle.next().await.unwrap();
22812 assert_eq!(
22813 3,
22814 requests_made.load(atomic::Ordering::Acquire),
22815 "Cache should be reused on buffer close and reopen"
22816 );
22817 editor.update(cx, |editor, cx| {
22818 assert_eq!(
22819 vec![expected_color],
22820 extract_color_inlays(editor, cx),
22821 "Should have an initial inlay"
22822 );
22823 });
22824}
22825
22826#[track_caller]
22827fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
22828 editor
22829 .all_inlays(cx)
22830 .into_iter()
22831 .filter_map(|inlay| inlay.get_color())
22832 .map(Rgba::from)
22833 .collect()
22834}